Implemented houses

This commit is contained in:
Torsten Schulz
2025-05-08 17:38:51 +02:00
parent b15d93a798
commit a9e6c82275
17 changed files with 1129 additions and 156 deletions

View File

@@ -70,7 +70,7 @@ export default {
}
this.statusItems = [
{ key: "age", icon: "👶", value: age },
{ key: "wealth", icon: "💰", value: money },
{ key: "wealth", icon: "💰", value: Intl.NumberFormat(navigator.language, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(money) },
{ key: "health", icon: "❤️", value: healthStatus },
{ key: "events", icon: "📰", value: events || null },
];
@@ -80,12 +80,15 @@ export default {
},
async handleDaemonSocketMessage(event) {
try {
if (event.data === 'ping') {
return;
}
const data = JSON.parse(event.data);
if (data.event === "falukantUpdateStatus") {
this.fetchStatus();
}
} catch (error) {
console.error("Error parsing daemonSocket message:", error);
console.error("Error parsing daemonSocket message:", error, event.data);
}
},
openPage(url, hasSubmenu = false) {

View File

@@ -211,8 +211,22 @@
"accept": "Werbung mit diesem Partner starten",
"wooing": {
"gifts": "Werbegeschenke",
"sendGift": "Werbegeschenk senden"
}
"sendGift": "Werbegeschenk senden",
"gift": "Geschenk",
"value": "Kosten",
"effect": "Wirkung"
},
"giftAffect": {
"0": "Keiner",
"1": "Sehr niedrig",
"2": "Niedrig",
"3": "Mittel",
"4": "Hoch",
"5": "Sehr hoch"
},
"mood": "Stimmung",
"progress": "Zuneigung"
},
"relationships": {
"name": "Name"
@@ -242,6 +256,15 @@
"addSpouse": "Ehepartner hinzufügen",
"viewDetails": "Details anzeigen",
"remove": "Entfernen"
},
"sendgift": {
"error": {
"nogiftselected": "Bitte wähle ein Geschenk aus.",
"generic": "Ein unbekannter Fehler ist aufgetreten.",
"tooOften": "Du kannst nicht so oft Geschenke machen.",
"insufficientFunds": "Du hast nicht genug Geld."
},
"success": "Das Geschenk wurde überreicht."
}
},
"product": {
@@ -291,9 +314,15 @@
"changeValue": "Wertänderung",
"time": "Zeit",
"activities": {
"Product sale": "Produktverkauf",
"Product sale": "Produkte verkauft",
"Production cost": "Produktionskosten",
"Sell all products": "Alle Produkte verkaufen"
"Sell all products": "Alle Produkte verkauft",
"sell products": "Produkte verkauft",
"director starts production": "Direktor beginnt Produktion",
"Buy storage (type: field)": "Lagerplatz gekauft (Typ: Feld)",
"Buy storage (type: iron)": "Lagerplatz gekauft (Typ: Eisen)",
"Buy storage (type: stone)": "Lagerplatz gekauft (Typ: Stein)",
"Buy storage (type: wood)": "Lagerplatz gekauft (Typ: Holz)"
}
},
"newdirector": {
@@ -327,6 +356,63 @@
"Cat": "Katze",
"Dog": "Hund",
"Horse": "Pferd"
},
"mood": {
"happy": "Glücklich",
"sad": "Traurig",
"angry": "Wütend",
"scared": "Verängstigt",
"surprised": "Überrascht",
"normal": "Normal"
},
"character": {
"brave": "Mutig",
"kind": "Freundlich",
"greedy": "Gierig",
"wise": "Weise",
"loyal": "Loyal",
"cunning": "Listig",
"generous": "Großzügig",
"arrogant": "Arrogant",
"honest": "Ehrlich",
"ambitious": "Ehrgeizig",
"patient": "Geduldig",
"impatient": "Ungeduldig",
"selfish": "Egoistisch",
"charismatic": "Charismatisch",
"empathetic": "Einfühlsam",
"timid": "Schüchtern",
"stubborn": "Stur",
"resourceful": "Einfallsreich",
"reckless": "Rücksichtslos",
"disciplined": "Diszipliniert",
"optimistic": "Optimistisch",
"pessimistic": "Pessimistisch",
"manipulative": "Manipulativ",
"independent": "Unabhängig",
"dependent": "Abhängig",
"adventurous": "Abenteuerlustig",
"humble": "Bescheiden",
"vengeful": "Rachsüchtig",
"pragmatic": "Pragmatisch",
"idealistic": "Idealistisch"
},
"house": {
"title": "Haus",
"statusreport": "Zustand des Hauses",
"element": "Bereich",
"state": "Zustand",
"buyablehouses": "Kaufe ein Haus",
"buy": "Kaufen",
"price": "Kaufpreis",
"worth": "Restwert",
"sell": "Verkaufen",
"status": {
"roofCondition": "Dach",
"wallCondition": "Wände",
"floorCondition": "Böden",
"windowCondition": "Fenster"
}
}
}
}

View File

@@ -3,6 +3,7 @@ import Createview from '../views/falukant/CreateView.vue';
import FalukantOverviewView from '../views/falukant/OverviewView.vue';
import MoneyHistoryView from '../views/falukant/MoneyHistoryView.vue';
import FamilyView from '../views/falukant/FamilyView.vue';
import HouseView from '../views/falukant/HouseView.vue';
const falukantRoutes = [
{
@@ -35,6 +36,12 @@ const falukantRoutes = [
component: FamilyView,
meta: { requiresAuth: true }
},
{
path: '/falukant/house',
name: 'HouseView',
component: HouseView,
meta: { requiresAuth: true },
},
];
export default falukantRoutes;

View File

@@ -3,31 +3,50 @@
<StatusBar />
<div class="contentscroll">
<!-- Titel -->
<h2>{{ $t('falukant.family.title') }}</h2>
<!-- Ehepartner -->
<div class="spouse-section">
<h3>{{ $t('falukant.family.spouse.title') }}</h3>
<div v-if="relationships.length > 0">
<table>
<tr>
<td>{{ $t('falukant.family.relationships.name') }}</td>
<td>
{{ $t('falukant.titles.' + relationships[0].character2.gender + '.' +
relationships[0].character2.nobleTitle) }}
{{ relationships[0].character2.firstName }}
</td>
</tr>
<tr>
<td>{{ $t('falukant.family.spouse.age') }}</td>
<td>{{ relationships[0].character2.age }}</td>
</tr>
<tr>
<td>{{ $t('falukant.family.spouse.status') }}</td>
<td>{{ $t('falukant.family.statuses.' + relationships[0].relationshipType) }}</td>
</tr>
</table>
<div class="relationship">
<table>
<tr>
<td>{{ $t('falukant.family.relationships.name') }}</td>
<td>
{{ $t('falukant.titles.' + relationships[0].character2.gender + '.' +
relationships[0].character2.nobleTitle) }}
{{ relationships[0].character2.firstName }}
</td>
</tr>
<tr>
<td>{{ $t('falukant.family.spouse.age') }}</td>
<td>{{ relationships[0].character2.age }}</td>
</tr>
<tr>
<td>{{ $t('falukant.family.spouse.mood') }}</td>
<td>{{ $t(`falukant.mood.${relationships[0].character2.mood.tr}`) }}</td>
</tr>
<tr>
<td>{{ $t('falukant.family.spouse.status') }}</td>
<td>{{ $t('falukant.family.statuses.' + relationships[0].relationshipType) }}</td>
</tr>
<tr v-if="relationships[0].relationshipType === 'wooing'">
<td>{{ $t('falukant.family.spouse.progress') }}</td>
<td>
<div class="progress">
<div class="progress-inner" :style="{
width: relationships[0].progress + '%',
backgroundColor: progressColor(relationships[0].progress)
}"></div>
</div>
</td>
</tr>
</table>
<ul>
<li v-for="characteristic in relationships[0].character2.characterTrait"
:key="characteristic.id">{{ $t(`falukant.character.${characteristic.tr}`) }}</li>
</ul>
</div>
<div v-if="relationships[0].relationshipType === 'wooing'">
<h3>{{ $t('falukant.family.spouse.wooing.gifts') }}</h3>
<table class="spouse-table">
@@ -35,6 +54,7 @@
<tr>
<th></th>
<th>{{ $t('falukant.family.spouse.wooing.gift') }}</th>
<th>{{ $t('falukant.family.spouse.wooing.effect') }}</th>
<th>{{ $t('falukant.family.spouse.wooing.value') }}</th>
</tr>
</thead>
@@ -42,12 +62,14 @@
<tr v-for="gift in gifts" :key="gift.id">
<td><input type="radio" name="gift" :value="gift.id" v-model="selectedGiftId"></td>
<td>{{ $t(`falukant.gifts.${gift.name}`) }}</td>
<td>{{ $t(`falukant.family.spouse.giftAffect.${getEffect(gift)}`) }}</td>
<td>{{ formatCost(gift.cost) }}</td>
</tr>
</tbody>
</table>
<div>
<button @click="sendGift" class="button">{{ $t('falukant.family.spouse.wooing.sendGift') }}</button>
<button @click="sendGift" class="button">{{ $t('falukant.family.spouse.wooing.sendGift')
}}</button>
</div>
</div>
</div>
@@ -67,7 +89,7 @@
v-model="selectedProposalId"></td>
<td>{{
$t(`falukant.titles.${proposal.proposedCharacterGender}.${proposal.proposedCharacterNobleTitle}`)
}} {{ proposal.proposedCharacterName }}</td>
}} {{ proposal.proposedCharacterName }}</td>
<td>{{ proposal.proposedCharacterAge }}</td>
<td>{{ formatCost(proposal.cost) }}</td>
</tr>
@@ -80,7 +102,6 @@
</div>
</div>
<!-- Kinder -->
<div class="children-section">
<h3>{{ $t('falukant.family.children.title') }}</h3>
<div v-if="children && children.length > 0">
@@ -131,9 +152,6 @@
</div>
</div>
<!-- Dialog-Beispiele oder ähnliche Komponenten -->
<MessageDialog ref="messageDialog" />
<ErrorDialog ref="errorDialog" />
</template>
<script>
@@ -160,7 +178,9 @@ export default {
proposals: [],
selectedProposalId: null,
gifts: [],
selectedGiftId: null
selectedGiftId: null,
moodAffects: [],
characterAffects: []
}
},
computed: {
@@ -169,12 +189,13 @@ export default {
async mounted() {
await this.loadFamilyData();
await this.loadGifts();
await this.loadMoodAffects();
await this.loadCharacterAffects();
},
methods: {
async loadFamilyData() {
try {
const response = await apiClient.get('/api/falukant/family');
console.log(response.data);
this.relationships = response.data.relationships;
this.children = response.data.children;
this.lovers = response.data.lovers;
@@ -193,6 +214,22 @@ export default {
return new Intl.NumberFormat(navigator.language, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
},
getEffect(gift) {
const relationship = this.relationships[0];
const partner = relationship.character2;
const currentMoodId = partner.mood?.id ?? partner.mood_id;
const moodEntry = gift.moodsAffects.find(ma => ma.mood_id === currentMoodId);
const moodValue = moodEntry ? moodEntry.suitability : 0;
let highestCharacterValue = 0;
for (const trait of partner.characterTrait) {
const charEntry = gift.charactersAffects.find(ca => ca.trait_id === trait.id);
if (charEntry && charEntry.suitability > highestCharacterValue) {
highestCharacterValue = charEntry.suitability;
}
}
return Math.round((moodValue + highestCharacterValue) / 2);
},
async acceptProposal() {
const response = await apiClient.post('/api/falukant/family/acceptmarriageproposal'
, { proposalId: this.selectedProposalId });
@@ -200,19 +237,54 @@ export default {
},
async loadGifts() {
const response = await apiClient.get('/api/falukant/family/gifts');
this.gifts = response.data;
const response = await apiClient.get('/api/falukant/family/gifts');
this.gifts = response.data;
},
async sendGift() {
if (!this.selectedGiftId) {
alert('Please select a gift');
this.$root.$refs.errorDialog.open(`tr:falukant.family.sendgift.error.nogiftselected`);
return;
}
const response = await apiClient.post('/api/falukant/family/gift'
, { giftId: this.selectedGiftId });
this.loadFamilyData();
}
try {
await apiClient.post('/api/falukant/family/gift'
, { giftId: this.selectedGiftId });
this.loadFamilyData();
this.$root.$refs.messageDialog.open('tr:falukant.family.sendgift.success');
} catch (error) {
console.log(error.response);
if (error.response.status === 412) {
this.$root.$refs.errorDialog.open(`tr:falukant.family.sendgift.error.${error.response.data.error}`);
} else {
this.$root.$refs.errorDialog.open(`tr:falukant.family.sendgift.error.generic`);
}
}
},
async loadMoodAffects() {
try {
const response = await apiClient.get('/api/falukant/mood/affect');
this.moodAffects = response.data;
} catch (error) {
console.error(error);
}
},
async loadCharacterAffects() {
try {
const response = await apiClient.get('/api/falukant/character/affect');
this.characterAffects = response.data;
} catch (error) {
console.error(error);
}
},
progressColor(p) {
const pct = Math.max(0, Math.min(100, p)) / 100;
const red = Math.round(255 * (1 - pct));
const green = Math.round(255 * pct);
return `rgb(${red}, ${green}, 0)`;
},
}
}
</script>
@@ -284,4 +356,28 @@ export default {
h2 {
padding-top: 20px;
}
.relationship>table,
.relationship>ul {
display: inline-block;
margin-right: 1em;
vertical-align: top;
}
.relationship>ul {
list-style: none;
}
.progress {
width: 100%;
background-color: #e5e7eb;
border-radius: 0.25rem;
overflow: hidden;
height: 1rem;
}
.progress-inner {
height: 100%;
transition: width 0.3s ease, background-color 0.3s ease;
}
</style>

View File

@@ -0,0 +1,259 @@
<template>
<div class="houseView">
<StatusBar />
<h2>{{ $t('falukant.house.title') }}</h2>
<div class="existingHouse">
<div :style="houseStyle(picturePosition)" class="house"></div>
<div class="statusreport">
<h3>{{ $t('falukant.house.statusreport') }}</h3>
<table>
<thead>
<tr>
<th>{{ $t('falukant.house.element') }}</th>
<th>{{ $t('falukant.house.state') }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="status, index in status">
<td>{{ $t(`falukant.house.status.${index}`) }}</td>
<td>{{ status }} %</td>
<td><button v-if="status < 100">{{ $t('falukant.house.renovate') }} ({{
$t('falukant.house.cost') }}: {{ getRenovationCost(index, status) }}</button></td>
</tr>
<tr>
<td>{{ $t('falukant.house.worth') }}</td>
<td>{{ getWorth(status) }}</td>
<td><button @click="sellHouse">{{ $t('falukant.house.sell') }}</button></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="buyablehouses">
<h3>{{ $t('falukant.house.buyablehouses') }}</h3>
<div style="overflow:auto">
<div style="display: flex; flex-direction: row" v-for="house in buyableHouses">
<div style="width:100px; height:100px; display: hidden;">
<div :style="houseStyle(house.houseType.position)" class="housePreview buyableHouseInfo"></div>
</div>
<div class="buyableHouseInfo">
<h4 style="display: inline;">{{ $t('falukant.house.statusreport') }}</h4>
<table>
<tbody>
<template v-for="value, key in house">
<tr v-if="key != 'houseType' && key != 'id'">
<td>{{ $t(`falukant.house.status.${key}`) }}</td>
<td>{{ value }} %</td>
</tr>
</template>
</tbody>
</table>
</div>
<div>
{{ $t('falukant.house.price') }}: {{ buyCost(house) }}
</div>
<div>
<button @click="buyHouse(house.id)">{{ $t('falukant.house.buy') }}</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from "vuex";
export default {
name: 'HouseView',
components: {
StatusBar
},
data() {
return {
houseTypes: [],
userHouse: {},
houseType: {},
status: {},
buyableHouses: [],
picturePosition: 0,
}
},
methods: {
async loadHouseTypes() {
try {
const houseTypesResult = await apiClient.get('/api/falukant/houses/types');
this.houseTypes = houseTypesResult.data;
} catch (error) {
}
},
async loadUserHouse() {
try {
const userHouseResult = await apiClient.get('/api/falukant/houses');
Object.assign(this.userHouse, userHouseResult.data);
const { houseType, ...houseStatus } = this.userHouse;
this.status = houseStatus;
this.picturePosition = parseInt(houseType.position);
this.houseType = houseType;
} catch (error) {
console.error('Fehler beim Laden des Hauses:', error);
this.userHouse = null;
this.status = null;
}
},
async loadBuyableHouses() {
try {
const buyableHousesResult = await apiClient.get('/api/falukant/houses/buyable');
this.buyableHouses = buyableHousesResult.data;
} catch (error) {
console.error('Fehler beim Laden der kaufbaren Häuser:', error);
}
},
houseStyle(housePosition) {
const columns = 3;
const spriteSize = 341; // Breite & Höhe eines einzelnen Hauses
let calculatePosition = Math.max(housePosition - 1, 0);
const x = (calculatePosition % columns) * spriteSize;
const y = Math.floor(calculatePosition / columns) * spriteSize;
return {
backgroundImage: 'url("/images/falukant/houses.png")',
backgroundPosition: `-${x}px -${y}px`,
backgroundSize: `${columns * spriteSize}px auto`, // z.B. 1023px auto
};
},
buyCost(house) {
const houseQuality = (house.roofCondition + house.windowCondition + house.floorCondition + house.wallCondition) / 4;
return (house.houseType.cost / 100 * houseQuality).toFixed(2);
},
getWorth() {
const house = {...this.userHouse, houseType: this.houseType};
const buyWorth = this.buyCost(house);
return (buyWorth * 0.8).toFixed(2);
},
async buyHouse(houseId) {
try {
const response = await apiClient.post('/api/falukant/houses',
{
houseId: houseId,
}
);
this.$router.push({ name: 'HouseView' });
} catch (error) {
console.error('Fehler beim Kaufen des Hauses:', error);
}
},
async getHouseData() {
await this.loadUserHouse();
await this.loadBuyableHouses();
}
},
computed: {
...mapState(['socket', 'daemonSocket']),
getHouseStyle() {
if (!this.userHouse || this.userHouse.position === undefined || this.userHouse.position === null) {
return {};
}
return this.houseStyle(this.userHouse.position);
},
getHouseType(position) {
const houseType = this.houseTypes[position];
return houseType;
},
getHouseStatus(position) {
const houseStatus = this.houseStatuses[position];
return houseStatus;
},
getRenovationCost(index, status) {
const houseType = this.houseTypes[position];
const renovationCost = houseType.renovationCosts[status];
return renovationCost;
},
},
created() {
},
async mounted() {
this.loadHouseTypes();
await this.getHouseData();
if (this.socket) {
this.socket.on("falukantHouseUpdate", this.getHouseData);
}
if (this.daemonSocket) {
this.daemonSocket.addEventListener("message", this.handleDaemonSocketMessage);
}
},
beforeUnmount() {
if (this.socket) {
this.socket.off("falukantHouseUpdate", this.fetchStatus);
}
if (this.daemonSocket) {
this.daemonSocket.removeEventListener("message", this.handleDaemonSocketMessage);
}
},
watch: {
}
}
</script>
<style lang="scss" scoped>
h2 {
padding-top: 20px;
}
.existingHouse {
display: block;
width: auto;
height: 255px;
}
Element {
background-position: 71px 54px;
}
.house {
border: 1px solid #ccc;
border-radius: 4px;
background-repeat: no-repeat;
image-rendering: crisp-edges;
transform: scale(0.7);
transform-origin: top left;
display: inline-block;
overflow: hidden;
width: 341px;
height: 341px;
}
.statusreport {
display: inline-block;
vertical-align: top;
height: 250px;
}
.buyableHouseInfo {
vertical-align: top;
}
.housePreview {
transform: scale(0.2);
width: 341px;
height: 341px;
transform-origin: top left;
}
.houseView {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.buyablehouses {
display: flex;
flex-direction: column;
overflow: hidden;
}
</style>

View File

@@ -91,6 +91,7 @@
</div>
<div class="imagecontainer">
<div :style="getAvatarStyle" class="avatar"></div>
<div :style="getHouseStyle" class="house"></div>
</div>
</div>
</template>
@@ -168,6 +169,24 @@ export default {
height: `${height}px`,
};
},
getHouseStyle() {
if (!this.falukantUser) return {};
const imageUrl = '/images/falukant/houses.png';
const housePosition = this.falukantUser.house ? this.falukantUser.house.type.position : 0;
const x = housePosition % 3;
const y = Math.floor(housePosition / 3);
return {
backgroundImage: `url(${imageUrl})`,
backgroundPosition: `-${x * 341}px -${y * 341}px`,
backgroundSize: "341px 341px",
width: "114px",
height: "114px",
};
},
getAgeColor(age) {
const ageGroup = this.getAgeGroup(age);
return ageGroup === 'child' ? 'blue' : ageGroup === 'teen' ? 'green' : ageGroup === 'adult' ? 'red' : 'gray';
},
moneyValue() {
const m = this.falukantUser?.money;
return typeof m === 'string' ? parseFloat(m) : m;
@@ -298,7 +317,16 @@ export default {
image-rendering: crisp-edges;
}
.house {
border: 1px solid #ccc;
border-radius: 4px;
background-repeat: no-repeat;
background-size: cover;
image-rendering: crisp-edges;
}
h2 {
padding-top: 20px;
}
</style>