From 876ee2ab49c38369f56c679686ee34d0c450cb64 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Sun, 22 Mar 2026 09:57:44 +0100 Subject: [PATCH] Add servant management features: Implement endpoints for hiring, dismissing, and setting pay levels for servants in the FalukantController. Update UserHouse model to include servant-related attributes. Enhance frontend components to manage servant details, including staffing state and household order, with corresponding localization updates in multiple languages. --- backend/controllers/falukantController.js | 3 + ...60320002000-add-servants-to-user-house.cjs | 53 ++ backend/models/falukant/data/user_house.js | 20 + backend/routers/falukantRouter.js | 3 + backend/services/falukantService.js | 283 +++++++- backend/sql/add_servants_to_user_house.sql | 7 + docs/FALUKANT_SERVANTS_CONCEPT.md | 336 ++++++++++ docs/FALUKANT_SERVANTS_IMPLEMENTATION_SPEC.md | 628 ++++++++++++++++++ frontend/src/i18n/locales/de/falukant.json | 43 ++ frontend/src/i18n/locales/en/falukant.json | 71 +- frontend/src/i18n/locales/es/falukant.json | 43 ++ frontend/src/views/falukant/HouseView.vue | 188 ++++++ 12 files changed, 1661 insertions(+), 17 deletions(-) create mode 100644 backend/migrations/20260320002000-add-servants-to-user-house.cjs create mode 100644 backend/sql/add_servants_to_user_house.sql create mode 100644 docs/FALUKANT_SERVANTS_CONCEPT.md create mode 100644 docs/FALUKANT_SERVANTS_IMPLEMENTATION_SPEC.md diff --git a/backend/controllers/falukantController.js b/backend/controllers/falukantController.js index a202285..226b4f3 100644 --- a/backend/controllers/falukantController.js +++ b/backend/controllers/falukantController.js @@ -146,6 +146,9 @@ class FalukantController { this.getUserHouse = this._wrapWithUser((userId) => this.service.getUserHouse(userId)); this.getBuyableHouses = this._wrapWithUser((userId) => this.service.getBuyableHouses(userId)); this.buyUserHouse = this._wrapWithUser((userId, req) => this.service.buyUserHouse(userId, req.body.houseId), { successStatus: 201 }); + this.hireServants = this._wrapWithUser((userId, req) => this.service.hireServants(userId, req.body?.amount), { successStatus: 201 }); + this.dismissServants = this._wrapWithUser((userId, req) => this.service.dismissServants(userId, req.body?.amount)); + this.setServantPayLevel = this._wrapWithUser((userId, req) => this.service.setServantPayLevel(userId, req.body?.payLevel)); this.getPartyTypes = this._wrapWithUser((userId) => this.service.getPartyTypes(userId)); this.createParty = this._wrapWithUser((userId, req) => { diff --git a/backend/migrations/20260320002000-add-servants-to-user-house.cjs b/backend/migrations/20260320002000-add-servants-to-user-house.cjs new file mode 100644 index 0000000..b83d1a8 --- /dev/null +++ b/backend/migrations/20260320002000-add-servants-to-user-house.cjs @@ -0,0 +1,53 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn( + { schema: 'falukant_data', tableName: 'user_house' }, + 'servant_count', + { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0 + } + ); + + await queryInterface.addColumn( + { schema: 'falukant_data', tableName: 'user_house' }, + 'servant_quality', + { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 50 + } + ); + + await queryInterface.addColumn( + { schema: 'falukant_data', tableName: 'user_house' }, + 'servant_pay_level', + { + type: Sequelize.STRING(20), + allowNull: false, + defaultValue: 'normal' + } + ); + + await queryInterface.addColumn( + { schema: 'falukant_data', tableName: 'user_house' }, + 'household_order', + { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 55 + } + ); + }, + + async down(queryInterface) { + await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'household_order'); + await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_pay_level'); + await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_quality'); + await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_count'); + } +}; diff --git a/backend/models/falukant/data/user_house.js b/backend/models/falukant/data/user_house.js index ad51590..8946243 100644 --- a/backend/models/falukant/data/user_house.js +++ b/backend/models/falukant/data/user_house.js @@ -24,6 +24,26 @@ UserHouse.init({ allowNull: false, defaultValue: 100 }, + servantCount: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0 + }, + servantQuality: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 50 + }, + servantPayLevel: { + type: DataTypes.STRING(20), + allowNull: false, + defaultValue: 'normal' + }, + householdOrder: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 55 + }, houseTypeId: { type: DataTypes.INTEGER, allowNull: false diff --git a/backend/routers/falukantRouter.js b/backend/routers/falukantRouter.js index 5af0455..10fd166 100644 --- a/backend/routers/falukantRouter.js +++ b/backend/routers/falukantRouter.js @@ -65,6 +65,9 @@ router.get('/houses/buyable', falukantController.getBuyableHouses); router.get('/houses', falukantController.getUserHouse); router.post('/houses/renovate-all', falukantController.renovateAll); router.post('/houses/renovate', falukantController.renovate); +router.post('/houses/servants/hire', falukantController.hireServants); +router.post('/houses/servants/dismiss', falukantController.dismissServants); +router.post('/houses/servants/pay-level', falukantController.setServantPayLevel); router.post('/houses', falukantController.buyUserHouse); router.get('/party/types', falukantController.getPartyTypes); router.post('/party', falukantController.createParty); diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index a2ddbf7..99f1dd9 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -4014,25 +4014,45 @@ class FalukantService extends BaseService { async getUserHouse(hashedUserId) { try { - const user = await User.findOne({ - where: { hashedId: hashedUserId }, + const falukantUser = await this.getFalukantUserByHashedId(hashedUserId); + const userHouse = await UserHouse.findOne({ + where: { userId: falukantUser.id }, include: [{ - model: FalukantUser, - as: 'falukantData', - include: [{ - model: UserHouse, - as: 'userHouse', - include: [{ - model: HouseType, - as: 'houseType', - attributes: ['position', 'cost'] - }], - attributes: ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition'] - }], - } + model: HouseType, + as: 'houseType', + attributes: ['id', 'position', 'cost', 'labelTr'] + }], + attributes: [ + 'roofCondition', + 'wallCondition', + 'floorCondition', + 'windowCondition', + 'servantCount', + 'servantQuality', + 'servantPayLevel', + 'householdOrder', + 'houseTypeId' ] }); - return user.falukantData[0].userHouse ?? { position: 0, roofCondition: 100, wallCondition: 100, floorCondition: 100, windowCondition: 100 }; + + if (!userHouse) { + return { + position: 0, + roofCondition: 100, + wallCondition: 100, + floorCondition: 100, + windowCondition: 100, + servantCount: 0, + servantQuality: 50, + servantPayLevel: 'normal', + householdOrder: 55, + servantSummary: this.buildServantSummary(null, falukantUser.character) + }; + } + + const plainHouse = userHouse.get({ plain: true }); + plainHouse.servantSummary = this.buildServantSummary(plainHouse, falukantUser.character); + return plainHouse; } catch (error) { console.log(error); return {}; @@ -4089,9 +4109,14 @@ class FalukantService extends BaseService { if (oldHouse) { await oldHouse.destroy(); } + const servantDefaults = this.getInitialServantState(house.houseType, falukantUser.character); await UserHouse.create({ userId: falukantUser.id, houseTypeId: house.houseTypeId, + servantCount: servantDefaults.servantCount, + servantQuality: servantDefaults.servantQuality, + servantPayLevel: servantDefaults.servantPayLevel, + householdOrder: servantDefaults.householdOrder }); await house.destroy(); await updateFalukantUserMoney(falukantUser.id, -housePrice, "housebuy", falukantUser.id); @@ -4109,6 +4134,232 @@ class FalukantService extends BaseService { return (house.houseType.cost / 100 * houseQuality).toFixed(2); } + getInitialServantState(houseType, character) { + const expected = this.getServantExpectation(houseType, character); + return { + servantCount: expected.min, + servantQuality: 50, + servantPayLevel: 'normal', + householdOrder: this.calculateHouseholdOrder({ + servantCount: expected.min, + servantQuality: 50, + servantPayLevel: 'normal', + houseType, + character + }) + }; + } + + getPayLevelMultiplier(payLevel) { + switch (payLevel) { + case 'low': + return 0.8; + case 'high': + return 1.3; + default: + return 1; + } + } + + getPayLevelQualityShift(payLevel) { + switch (payLevel) { + case 'low': + return -6; + case 'high': + return 6; + default: + return 0; + } + } + + getServantExpectation(houseType, character) { + const housePosition = Number(houseType?.position || 0); + const titleLevel = Number(character?.nobleTitle?.level || character?.titleOfNobility?.level || 0); + + let min = 0; + let max = 1; + + if (housePosition >= 6) { + min = 4; + max = 8; + } else if (housePosition === 5) { + min = 3; + max = 6; + } else if (housePosition === 4) { + min = 2; + max = 4; + } else if (housePosition === 3) { + min = 1; + max = 2; + } + + const titleBonus = Math.max(0, Math.floor(titleLevel / 3)); + return { + min: min + titleBonus, + max: max + titleBonus + }; + } + + calculateHouseholdOrder({ servantCount, servantQuality, servantPayLevel, houseType, character }) { + const expectation = this.getServantExpectation(houseType, character); + const missing = Math.max(0, expectation.min - servantCount); + const excessive = Math.max(0, servantCount - expectation.max); + const qualityPart = Math.round((Number(servantQuality || 0) - 50) * 0.35); + const payPart = this.getPayLevelQualityShift(servantPayLevel); + const fitPenalty = (missing * 10) + (excessive * 4); + return Math.max(0, Math.min(100, 55 + qualityPart + payPart - fitPenalty)); + } + + calculateServantMonthlyCost({ servantCount, servantQuality, servantPayLevel, houseType }) { + const basePerServant = Math.max(20, Math.round((Number(houseType?.cost || 0) / 1000) + 40)); + const qualityFactor = 1 + ((Number(servantQuality || 50) - 50) / 200); + const payFactor = this.getPayLevelMultiplier(servantPayLevel); + return Math.round(servantCount * basePerServant * qualityFactor * payFactor * 100) / 100; + } + + buildServantSummary(userHouse, character) { + const expectation = this.getServantExpectation(userHouse?.houseType, character); + const servantCount = Number(userHouse?.servantCount || 0); + const servantQuality = Number(userHouse?.servantQuality ?? 50); + const servantPayLevel = userHouse?.servantPayLevel || 'normal'; + const householdOrder = Number( + userHouse?.householdOrder ?? + this.calculateHouseholdOrder({ + servantCount, + servantQuality, + servantPayLevel, + houseType: userHouse?.houseType, + character + }) + ); + + let staffingState = 'fitting'; + if (servantCount < expectation.min) staffingState = 'understaffed'; + if (servantCount > expectation.max) staffingState = 'overstaffed'; + + let orderState = 'stable'; + if (householdOrder < 35) orderState = 'chaotic'; + else if (householdOrder < 55) orderState = 'strained'; + else if (householdOrder > 80) orderState = 'excellent'; + + return { + expectedMin: expectation.min, + expectedMax: expectation.max, + monthlyCost: this.calculateServantMonthlyCost({ + servantCount, + servantQuality, + servantPayLevel, + houseType: userHouse?.houseType + }), + staffingState, + orderState + }; + } + + async getOwnedUserHouse(hashedUserId) { + const falukantUser = await this.getFalukantUserByHashedId(hashedUserId); + const house = await UserHouse.findOne({ + where: { userId: falukantUser.id }, + include: [{ model: HouseType, as: 'houseType', attributes: ['id', 'position', 'cost', 'labelTr'] }] + }); + if (!house) { + throw new Error('House not found'); + } + return { falukantUser, house }; + } + + async hireServants(hashedUserId, amount = 1) { + const hireAmount = Math.max(1, Math.min(Number(amount) || 1, 10)); + const { falukantUser, house } = await this.getOwnedUserHouse(hashedUserId); + const hireCost = Math.round(hireAmount * (40 + ((house.houseType?.cost || 0) / 2000)) * 100) / 100; + if (Number(falukantUser.money) < hireCost) { + throw new Error('notenoughmoney.'); + } + + house.servantCount = Number(house.servantCount || 0) + hireAmount; + house.servantQuality = Math.min(100, Number(house.servantQuality || 50) + Math.max(1, hireAmount)); + house.householdOrder = this.calculateHouseholdOrder({ + servantCount: house.servantCount, + servantQuality: house.servantQuality, + servantPayLevel: house.servantPayLevel, + houseType: house.houseType, + character: falukantUser.character + }); + await house.save(); + await updateFalukantUserMoney(falukantUser.id, -hireCost, 'servants_hired', falukantUser.id); + + const user = await User.findByPk(falukantUser.userId); + notifyUser(user.hashedId, 'falukantHouseUpdate', {}); + notifyUser(user.hashedId, 'falukantUpdateStatus', {}); + return { + amount: hireAmount, + cost: hireCost, + servantSummary: this.buildServantSummary(house, falukantUser.character) + }; + } + + async dismissServants(hashedUserId, amount = 1) { + const dismissAmount = Math.max(1, Math.min(Number(amount) || 1, 10)); + const { falukantUser, house } = await this.getOwnedUserHouse(hashedUserId); + const prevCount = Number(house.servantCount || 0); + if (prevCount <= 0) { + throw new Error('No servants to dismiss'); + } + + // Symmetrisch zu hireServants: Qualitätsänderung skaliert mit tatsächlich entlassener Anzahl + const actualDismissed = Math.min(dismissAmount, prevCount); + house.servantCount = Math.max(0, prevCount - dismissAmount); + house.servantQuality = Math.max( + 0, + Number(house.servantQuality || 50) - Math.max(1, actualDismissed) + ); + house.householdOrder = this.calculateHouseholdOrder({ + servantCount: house.servantCount, + servantQuality: house.servantQuality, + servantPayLevel: house.servantPayLevel, + houseType: house.houseType, + character: falukantUser.character + }); + await house.save(); + + const user = await User.findByPk(falukantUser.userId); + notifyUser(user.hashedId, 'falukantHouseUpdate', {}); + notifyUser(user.hashedId, 'falukantUpdateStatus', {}); + return { + amount: dismissAmount, + servantSummary: this.buildServantSummary(house, falukantUser.character) + }; + } + + async setServantPayLevel(hashedUserId, payLevel) { + const normalizedPayLevel = ['low', 'normal', 'high'].includes(payLevel) ? payLevel : 'normal'; + const { falukantUser, house } = await this.getOwnedUserHouse(hashedUserId); + + const previousPayLevel = house.servantPayLevel || 'normal'; + const oldShift = this.getPayLevelQualityShift(previousPayLevel); + const newShift = this.getPayLevelQualityShift(normalizedPayLevel); + const baseQuality = Number(house.servantQuality || 50) - oldShift; + + house.servantPayLevel = normalizedPayLevel; + house.servantQuality = Math.max(0, Math.min(100, baseQuality + newShift)); + house.householdOrder = this.calculateHouseholdOrder({ + servantCount: house.servantCount, + servantQuality: house.servantQuality, + servantPayLevel: house.servantPayLevel, + houseType: house.houseType, + character: falukantUser.character + }); + await house.save(); + + const user = await User.findByPk(falukantUser.userId); + notifyUser(user.hashedId, 'falukantHouseUpdate', {}); + notifyUser(user.hashedId, 'falukantUpdateStatus', {}); + return { + payLevel: normalizedPayLevel, + servantSummary: this.buildServantSummary(house, falukantUser.character) + }; + } + async getPartyTypes(hashedUserId) { const falukantUser = await getFalukantUserOrFail(hashedUserId); const engagedCount = await Relationship.count({ diff --git a/backend/sql/add_servants_to_user_house.sql b/backend/sql/add_servants_to_user_house.sql new file mode 100644 index 0000000..2cf25aa --- /dev/null +++ b/backend/sql/add_servants_to_user_house.sql @@ -0,0 +1,7 @@ +-- PostgreSQL only + +ALTER TABLE falukant_data.user_house + ADD COLUMN IF NOT EXISTS servant_count integer NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS servant_quality integer NOT NULL DEFAULT 50, + ADD COLUMN IF NOT EXISTS servant_pay_level varchar(20) NOT NULL DEFAULT 'normal', + ADD COLUMN IF NOT EXISTS household_order integer NOT NULL DEFAULT 55; diff --git a/docs/FALUKANT_SERVANTS_CONCEPT.md b/docs/FALUKANT_SERVANTS_CONCEPT.md new file mode 100644 index 0000000..12f1ffd --- /dev/null +++ b/docs/FALUKANT_SERVANTS_CONCEPT.md @@ -0,0 +1,336 @@ +# Falukant: Konzept Dienerschaft + +Dieses Dokument beschreibt ein eigenständiges Dienerschaftssystem für Falukant. Die Dienerschaft hängt bewusst am Haus und nicht primär an Familie oder Liebschaften. + +## 1. Grundentscheidung + +Dienerschaft ist Teil des Hausstands. + +Warum: +- Diener versorgen Haushalt, Gebäude, Gäste und Repräsentation. +- Die Größe und Qualität der Dienerschaft hängt stärker an Hausgröße und Stand als an einzelnen Familienbeziehungen. +- Spätere Systeme wie Diskretion, Skandalabwehr, Botengänge, Schutz und Festkultur lassen sich so an einer Stelle bündeln. + +Folgerung: +- Hauptansicht: `HouseView` +- Datenträger: `user_house` plus eigene Dienerstruktur +- Familie, Liebschaften, Ruf und Untergrund nutzen die Effekte mit, besitzen das System aber nicht selbst. + +## 2. Spielziel + +Die Dienerschaft soll vier Dinge leisten: +- laufende Kosten und Standesdruck erzeugen +- Komfort und Ordnung des Haushalts darstellen +- Repräsentation und Ansehen beeinflussen +- Diskretion und Risiko in Familien- und Liebschaftsfragen mitsteuern + +Die erste Ausbaustufe bleibt bewusst einfach und abstrahiert. Einzelne Namen oder tiefes Personalmanagement kommen erst später. + +## 3. Kernmodell + +### 3.1 Erste Ausbaustufe: abstrakte Dienerschaft + +Der Spieler verwaltet keine einzelnen Diener, sondern einen Haushalt mit wenigen Zuständen: +- `servantCount` +- `servantQuality` +- `servantPayLevel` +- `householdOrder` + +Empfohlene Bedeutung: +- `servantCount`: tatsächliche Zahl der Bediensteten +- `servantQuality`: Ausbildungs- und Verlässlichkeitsniveau +- `servantPayLevel`: wie gut der Haushalt bezahlt und versorgt wird +- `householdOrder`: Ergebniswert für Disziplin, Sauberkeit, Organisation + +### 3.2 Spätere Ausbaustufe + +Erst später werden Rollen differenziert: +- Hausverwalter / Haushofmeister +- Kammerdiener / Zofen +- Küchenpersonal +- Stallpersonal +- Kinder- und Pflegepersonal +- Wachen / Torpersonal + +Diese zweite Stufe ist ausdrücklich nicht Teil des ersten Implementierungspakets. + +## 4. Verbindung zum Haus + +Die Dienerschaft ist an das Haus gekoppelt. + +Das Haus bestimmt: +- maximal sinnvolle Dienerzahl +- erwartete Mindestzahl je nach Stand +- Ansehenswirkung von Über- oder Unterbesetzung +- Kostenfaktor + +Ein kleines Haus mit zu großer Dienerschaft wirkt verschwenderisch. +Ein großes oder nobles Haus mit zu wenig Dienern wirkt ungeordnet, geizig oder standeswidrig. + +## 5. Haus- und Standeslogik + +Die Zielgröße der Dienerschaft entsteht aus zwei Faktoren: +- Hausgröße / Haustyp +- gesellschaftlicher Stand + +### 5.1 Erwartungswert + +Jeder Haushalt hat einen erwarteten Bereich: +- `expectedServantsMin` +- `expectedServantsMax` + +Dieser Bereich wird aus Haus und Titel abgeleitet. + +Beispielhafte Richtung: +- Holzhaus, niedriger Stand: 0 bis 1 +- kleines Familienhaus: 1 bis 3 +- Stadthaus oder höherer Adel: 3 bis 8 +- Hochadel und Hofnähe: deutlich darüber + +Wichtig: +- Das sind keine finalen Balancing-Zahlen. +- Das Balancing bleibt eine spätere Phase. + +## 6. Zentrale Spielwerte + +### 6.1 Dienerzahl + +Die Dienerzahl ist der wichtigste Primärwert. + +Zu wenig Diener: +- schlechtere Haushaltsordnung +- negativer Einfluss auf Ansehen in hohen Ständen +- höhere Spannungen im Haus +- weniger Diskretion und schwächerer Schutz vor Gerüchten + +Zu viele Diener: +- unnötige Kosten +- bei niedrigem Stand möglicher Vorwurf von Verschwendung oder Anmaßung +- höheres Risiko für Klatsch, weil mehr Personen Wissen tragen + +### 6.2 Qualität + +Qualität beschreibt Verlässlichkeit und Niveau. + +Niedrige Qualität: +- Haus funktioniert nur grob +- Diskretion schlecht +- Feste und Repräsentation schwächer +- höheres Risiko für Gerede, Unordnung, Pannen + +Hohe Qualität: +- besserer Hauszustand im Alltag +- stärkere Diskretion +- besserer Eindruck bei Gästen +- positive Wirkung auf Ehekomfort und Familienruhe + +### 6.3 Bezahlung + +Die Bezahlung ist ein Steuerungswert. + +Niedrige Bezahlung: +- spart kurzfristig Geld +- senkt Loyalität und Qualität +- erhöht Gerüchte- und Diebstahlrisiko + +Hohe Bezahlung: +- kostet mehr +- verbessert Loyalität, Qualität und Diskretion + +### 6.4 Haushaltsordnung + +`householdOrder` ist ein abgeleiteter Zustand. + +Er hängt ab von: +- Dienerzahl im Verhältnis zur Sollgröße +- Qualität +- Bezahlung +- Hauszustand + +Auswirkungen: +- bessere Ordnung stabilisiert Ehe- und Familienwerte +- schlechte Ordnung verschlechtert Komfort und Ansehen +- sie beeinflusst spätere Fest- und Besuchssysteme + +## 7. Systemwirkungen + +### 7.1 Geld + +Dienerschaft erzeugt laufende Kosten. + +Monatliche Kosten hängen ab von: +- Dienerzahl +- Qualitätsniveau +- Bezahlungsstufe +- Hausgröße + +Später kann darin auch Nahrung, Kleidung und Ausstattung enthalten sein. + +### 7.2 Ansehen + +Ansehen wird nicht direkt nur durch „mehr Diener = besser“ berechnet. + +Stattdessen wirkt: +- Passung zum Stand +- Ordnung und Auftreten +- offensichtliche Unterversorgung +- offensichtliche Verschwendung + +Faustregel: +- hohe Stände werden stärker nach Hausführung beurteilt +- niedrige Stände dürfen einfacher leben +- extreme Abweichungen nach oben oder unten wirken negativ + +### 7.3 Familie und Ehe + +Die Familie nutzt die Hauswirkung mit. + +Positive Effekte guter Dienerschaft: +- mehr Komfort +- geringere Alltagsbelastung +- bessere Ehezufriedenheit +- geringerer Haushaltsstress + +Negative Effekte schlechter Dienerschaft: +- Unruhe im Haus +- Streit über Kosten und Ordnung +- zusätzliche Spannungen bei Ehe und Kindern + +### 7.4 Liebschaften und Skandale + +Dienerschaft beeinflusst Diskretion. + +Gut bezahlte, loyale und kleine, passende Dienerschaft: +- schützt Geheimnisse besser +- senkt Skandal- und Gerüchterisiko + +Unzufriedene oder zu große Dienerschaft: +- erhöht Klatsch +- macht verdeckte Beziehungen sichtbarer +- verbessert die Chancen von Untergrundaktivitäten, etwas aufzudecken + +### 7.5 Untergrund / Aufdeckung + +Das Untergrundsystem soll später auf Dienerschaft zugreifen können. + +Beispiel: +- unzufriedenes Personal erhöht Erfolg bei `investigate_affair` +- sehr diskreter Haushalt erschwert Aufdeckung und Erpressung + +## 8. Standeslogik + +Die Bewertung der Dienerschaft ist standesabhängig. + +### Niedrige Stände + +Erlaubt: +- kleine oder keine Dienerschaft + +Negativ: +- zu große Dienerschaft bei kleinem Haus +- demonstrative Übertreibung + +### Mittlere Stände + +Erwartet: +- geordneter kleiner Haushalt +- passende Grundversorgung + +Negativ: +- sichtbare Unordnung +- geizige Unterbesetzung +- übertriebener Luxus + +### Hohe Stände + +Erwartet: +- repräsentative, funktionierende Dienerschaft + +Negativ: +- zu wenig Personal +- schlechter Hauszustand trotz Rang +- öffentlich erkennbare Überforderung im Haushalt + +## 9. UI-Richtung + +Die erste Oberfläche gehört in `HouseView`. + +Empfohlene Elemente: +- Überblickskarte „Dienerschaft“ +- Ist-Zahl, Sollbereich, Qualität, Bezahlungsstufe, Haushaltsordnung +- einfache Aktionen: + - Diener einstellen + - Diener entlassen + - Bezahlung anheben + - Bezahlung senken + +Zusätzliche Anzeigen: +- erwarteter Bereich nach Haus und Stand +- Monatskosten +- Haupteffekte auf Ordnung, Ansehen und Diskretion + +Wichtig: +- kein Mikromanagement pro Diener in der ersten Version +- keine Personallisten im MVP + +## 10. Daemon-/Tick-Sicht + +Die eigentliche Veränderung der Zustände soll durch den externen Daemon laufen. + +Daily: +- Drift von Loyalität und Ordnung +- kleine Folgen schlechter Versorgung +- Diskretionswirkung auf Familien- und Liebschaftssysteme + +Monthly: +- Kosten abbuchen +- Unterversorgung bewerten +- Qualität und Loyalität nachziehen +- Ansehenswirkung aus Passung und Ordnung anwenden + +## 11. MVP-Schnitt + +Erste spielbare Version: +- Dienerschaft ist ein Hauswert +- nur aggregierte Werte, keine Einzelrollen +- UI in `HouseView` +- monatliche Kosten +- grobe Effekte auf: + - Haushaltsordnung + - Ansehen + - Ehezufriedenheit + - Diskretion bei Liebschaften + +Noch nicht im MVP: +- benannte Diener +- Intrigen einzelner Bediensteter +- eigene Dienerereignisse mit langen Ketten +- tiefes Rollenmanagement + +## 12. Spätere Ausbauten + +Später interessant: +- Dienerschaft als Voraussetzung für bestimmte Feste +- Spezialrollen wie Amme, Leibdiener, Spion im Haushalt +- interne Konflikte unter Dienern +- Diebstahl, Bestechung, Illoyalität +- Hauspersonal als Quelle für Gerüchte oder Schutz +- Untergrund kann Personal bestechen + +## 13. Offene Designentscheidungen + +1. Soll die erste Version mit einer absoluten `servantCount` arbeiten oder mit Stufen wie klein / passend / groß? +2. Soll `householdOrder` direkt gespeichert oder komplett aus anderen Werten berechnet werden? +3. Soll Bezahlung als Prozentwert, feste Stufe oder Freitext-Enum geführt werden? +4. Wie stark soll Dienerschaft bereits in der ersten Version auf Liebschaften und Untergrund wirken? +5. Sollen Feste weiter ihr eigenes `servantRatio` behalten oder später an das neue System angebunden werden? + +## 14. Empfehlung + +Empfohlene erste Umsetzung: +- `servantCount` als absolute Zahl +- `servantQuality` als einfacher Wert 0 bis 100 +- `servantPayLevel` als feste Stufen `low`, `normal`, `high` +- `householdOrder` als gespeicherter, vom Daemon gepflegter Zustand + +Diese Variante ist einfach genug für ein erstes Spielsystem, aber stark genug, um später Familie, Ruf, Untergrund und Feste daran anzuschließen. diff --git a/docs/FALUKANT_SERVANTS_IMPLEMENTATION_SPEC.md b/docs/FALUKANT_SERVANTS_IMPLEMENTATION_SPEC.md new file mode 100644 index 0000000..8f0e41a --- /dev/null +++ b/docs/FALUKANT_SERVANTS_IMPLEMENTATION_SPEC.md @@ -0,0 +1,628 @@ +# Falukant: Dienerschaft – Daemon-, Technik- und Umsetzungs-Spezifikation + +Dieses Dokument bündelt die umsetzungsreife Spezifikation für das Dienerschaftssystem in einer Datei. + +Es ersetzt für die technische Umsetzung die sonst übliche Aufteilung in: +- Daemon-Spec +- Daemon-Handoff +- technisches Konzept +- Implementierungs-Backlog + +Die fachliche Grundidee bleibt in [FALUKANT_SERVANTS_CONCEPT.md](/mnt/share/torsten/Programs/YourPart3/docs/FALUKANT_SERVANTS_CONCEPT.md) beschrieben. Dieses Dokument hier ist die Arbeitsgrundlage für Implementierung und Daemon-Anbindung. + +## 1. Zielbild + +Die Dienerschaft ist ein Haussystem mit vier Kernwerten: +- `servantCount` +- `servantQuality` +- `servantPayLevel` +- `householdOrder` + +Diese Werte wirken auf: +- monatliche Kosten +- Repräsentation und Ansehen +- Komfort und Ordnung des Haushalts +- Ehezufriedenheit und Haushaltsfrieden +- Diskretion bei Liebschaften +- spätere Untergrund-Aufdeckungen + +## 2. Systemgrenzen + +In Scope der ersten Version: +- Dienerschaft hängt an `user_house` +- House-UI zeigt und verändert Dienerwerte +- externer Daemon verarbeitet Daily- und Monthly-Effekte +- Familie, Liebschaften und Untergrund nutzen die resultierenden Werte mit + +Nicht in Scope der ersten Version: +- einzelne benannte Diener +- eigene Dienerrollen wie Küchenpersonal, Wachen, Zofen +- eigene Eventketten nur für Diener +- finales Balancing + +## 3. Datenmodell + +### 3.1 Bereits vorhandene Hausfelder + +In `falukant_data.user_house`: +- `servant_count integer not null default 0` +- `servant_quality integer not null default 50` +- `servant_pay_level varchar(20) not null default 'normal'` +- `household_order integer not null default 55` + +### 3.2 Wertebereiche + +- `servant_count`: `0..999` +- `servant_quality`: `0..100` +- `servant_pay_level`: `low | normal | high` +- `household_order`: `0..100` + +### 3.3 Abgeleitete Werte + +Diese Werte müssen nicht persistent gespeichert werden, sondern können im Backend oder Daemon berechnet werden: +- `expectedServantsMin` +- `expectedServantsMax` +- `staffingState` +- `orderState` +- `monthlyServantCost` +- `discretionModifier` +- `servantReputationModifier` +- `marriageComfortModifier` + +## 4. Erwartungswert der Dienerschaft + +Die Sollgröße hängt von Haus und Stand ab. + +### 4.1 Basis nach Hausposition + +`house.house_type.position` ist die grobe Hausklasse. + +Empfohlene erste Regel: + +| Hausposition | Basis Min | Basis Max | +|-------------|-----------|-----------| +| `<= 2` | 0 | 1 | +| `3` | 1 | 2 | +| `4` | 2 | 4 | +| `5` | 3 | 6 | +| `>= 6` | 4 | 8 | + +### 4.2 Standesbonus + +Aus `character.noble_title.level`: + +```text +titleBonus = floor(level / 3), mindestens 0 +expectedMin = baseMin + titleBonus +expectedMax = baseMax + titleBonus +``` + +### 4.3 Zustandsklassen + +```text +if servantCount < expectedMin => understaffed +if servantCount > expectedMax => overstaffed +sonst => fitting +``` + +## 5. Daily-Regeln für den externen Daemon + +## 5.1 Daily-Input + +Pro Falukant-User mit Haus braucht der Daemon: +- `falukant_user.id` +- `user.id` bzw. `user.hashed_id` für Benachrichtigung +- `character.id` +- `character.reputation` +- `character.noble_title_id` und idealerweise `character.nobleTitle.level` +- `user_house.house_type_id` +- `house_type.position` +- `house_type.cost` +- `servant_count` +- `servant_quality` +- `servant_pay_level` +- `household_order` +- optional für Verknüpfungen: + - `marriage_satisfaction` oder `relationship_state.marriage_satisfaction` + - aktive Liebschaften mit `visibility`, `discretion`, `risk` + +## 5.2 Daily-Hilfswerte + +```text +payShift(low) = -6 +payShift(normal) = 0 +payShift(high) = +6 + +missing = max(0, expectedMin - servantCount) +excessive = max(0, servantCount - expectedMax) + +qualityPart = round((servantQuality - 50) * 0.35) +payPart = payShift(servantPayLevel) +fitPenalty = missing * 10 + excessive * 4 +``` + +## 5.3 Daily-Zielwert für Haushaltsordnung + +```text +targetHouseholdOrder = clamp( + 55 + qualityPart + payPart - fitPenalty, + 0, + 100 +) +``` + +## 5.4 Daily-Drift der Haushaltsordnung + +Die Ordnung springt nicht hart, sondern driftet langsam: + +```text +newHouseholdOrder = oldHouseholdOrder + +if oldHouseholdOrder < targetHouseholdOrder: + newHouseholdOrder += min(2, targetHouseholdOrder - oldHouseholdOrder) + +if oldHouseholdOrder > targetHouseholdOrder: + newHouseholdOrder -= min(2, oldHouseholdOrder - targetHouseholdOrder) +``` + +Zusatzregel: +- bei `servantPayLevel = low` und `servantCount < expectedMin` zusätzlich `-1` +- bei `servantPayLevel = high` und `servantQuality >= 65` zusätzlich `+1` + +Danach clamp auf `0..100`. + +## 5.5 Daily-Drift der Dienerqualität + +Die Qualität ändert sich langsam: + +```text +qualityDelta = 0 + +if servantPayLevel = low: qualityDelta -= 1 +if servantPayLevel = high: qualityDelta += 1 + +if servantCount < expectedMin: qualityDelta -= 1 +if servantCount > expectedMax + 2: qualityDelta -= 1 + +if householdOrder >= 80: qualityDelta += 1 +if householdOrder <= 30: qualityDelta -= 1 +``` + +Danach: +- auf `-2..+2` pro Tag begrenzen +- `servantQuality = clamp(servantQuality + qualityDelta, 0, 100)` + +## 5.6 Daily-Effekt auf Ansehen + +Der Daily-Rufeffekt ist klein, damit Monats- und Ereigniseffekte wichtiger bleiben. + +```text +reputationDelta = 0 + +if titleLevel >= 4 and servantCount < expectedMin: + reputationDelta -= 0.15 * missing + +if titleLevel <= 1 and servantCount > expectedMax: + reputationDelta -= 0.10 * excessive + +if householdOrder >= 85 and servantCount between expectedMin and expectedMax: + reputationDelta += 0.05 + +if householdOrder <= 25: + reputationDelta -= 0.20 +``` + +Rundung: +- intern als Dezimalwert möglich +- falls nur Ganzzahlen gespeichert werden, über Tagespuffer oder Rundungsregel aggregieren + +## 5.7 Daily-Effekt auf Ehe / Haushalt + +Wenn ein Ehe-Zufriedenheitssystem vorhanden ist: + +```text +marriageDelta = 0 + +if householdOrder >= 75: marriageDelta += 0.10 +if householdOrder <= 35: marriageDelta -= 0.15 +if servantCount < expectedMin: marriageDelta -= 0.10 +``` + +Wenn noch kein eigener Wert gespeichert wird: +- diese Regel für später vormerken +- aktuell nur `householdTension` oder UI-Ableitungen beeinflussen + +## 5.8 Daily-Effekt auf Liebschaften / Diskretion + +Der Daemon berechnet einen Diskretionsmodifikator: + +```text +discretionModifier = 0 + +if servantQuality >= 70 and servantPayLevel = high and servantCount <= expectedMax: + discretionModifier -= 8 + +if servantPayLevel = low: + discretionModifier += 6 + +if servantCount > expectedMax + 1: + discretionModifier += 4 + +if householdOrder <= 35: + discretionModifier += 5 +``` + +Bedeutung: +- negativer Wert verbessert Geheimhaltung +- positiver Wert erhöht Entdeckungsrisiko + +Anwendung: +- bei aktiven Liebschaften auf Sichtbarkeit/Skandalchance +- bei Untergrundaktivitäten als Erfolgsmodifikator + +## 5.9 Daily-Notifications + +Daily sendet nicht für jede Teildrift ein eigenes Event. + +Wenn sich einer dieser Punkte relevant verändert: +- `household_order` +- `servant_quality` +- `reputation` +- Ehe-/Liebschaftsfolgen über Diener + +dann: +- `falukantUpdateFamily` mit `reason: "daily"` +- danach `falukantUpdateStatus` + +Es gibt keinen separaten `reason` für Dienerschaft. + +## 6. Monthly-Regeln für den externen Daemon + +## 6.1 Monthly-Input + +Wie Daily, zusätzlich: +- aktuelles Geld `falukant_user.money` + +## 6.2 Monatskosten + +```text +basePerServant = max(20, round((houseType.cost / 1000) + 40)) +qualityFactor = 1 + ((servantQuality - 50) / 200) +payFactor(low) = 0.8 +payFactor(normal) = 1.0 +payFactor(high) = 1.3 + +monthlyServantCost = servantCount * basePerServant * qualityFactor * payFactor +``` + +Auf 2 Nachkommastellen runden. + +## 6.3 Abbuchung + +Wenn genügend Geld vorhanden: +- Geld abziehen +- Aktivität z. B. `servants_monthly` + +Wenn nicht genügend Geld vorhanden: +- so viel wie möglich abziehen oder auf 0 fallen lassen, je nach vorhandener Gesamtlogik +- Unterversorgung markieren + +Empfehlung für die erste Version: +- vollständige Abbuchung nur wenn genug Geld da +- sonst `underfunded = true` + +## 6.4 Folgen von Unterversorgung + +Bei Unterversorgung im Monat: + +```text +servantQuality -= 4 +householdOrder -= 6 +``` + +Zusätzlich: +- wenn `titleLevel >= 4`: `reputation -= 1` +- wenn aktive Liebschaften vorhanden: Diskretionsmalus für den Folgemonat + +## 6.5 Monatsbonus bei gutem Haushalt + +Wenn gleichzeitig: +- `servantCount` innerhalb Sollbereich +- `servantQuality >= 70` +- `householdOrder >= 80` +- `servantPayLevel != low` + +dann: +- `reputation += 1` für hohe Stände ab `titleLevel >= 3` +- kleiner Ehe-/Komfortbonus, falls System vorhanden + +## 6.6 Monthly-Notifications + +Nach Monatsverarbeitung: +- `falukantUpdateFamily` mit `reason: "monthly"` +- danach `falukantUpdateStatus` + +## 7. Handoff an den externen Daemon + +## 7.1 Der externe Daemon muss lesen + +Aus Backend/DB: +- `falukant_data.user_house` +- `falukant_type.house` +- `falukant_data.falukant_user` +- `falukant_data.character` +- Titel/Stand +- optional aktive Ehe-/Liebschaftsdaten + +## 7.2 Der externe Daemon muss schreiben + +Mindestens: +- `user_house.servant_quality` +- `user_house.household_order` +- `character.reputation` oder entsprechender Rufwert + +Optional, falls vorhanden: +- `relationship_state.marriage_satisfaction` +- Hilfs-/Logtabellen für Monatskosten und Unterversorgung + +## 7.3 Der externe Daemon muss senden + +Bei relevanten Änderungen: +- `falukantUpdateFamily` +- `falukantUpdateStatus` + +`reason` nur: +- `daily` +- `monthly` + +Keine zusätzlichen Diener-Reason-Werte. + +## 7.4 Idempotenz + +Der Daemon muss verhindern, dass Daily/Monthly doppelt auf denselben Tick laufen. + +Empfohlen: +- eigene Tick-Marker außerhalb dieses Projekts +- oder Zeitstempel in Worker-Logs + +## 8. Backend-Aufgaben in diesem Projekt + +## 8.1 Bereits erledigt + +- Hausfelder in `user_house` +- Migration +- Produktions-SQL +- House-API mit Dienerwerten +- UI in `HouseView` +- direkte Spieleraktionen: + - einstellen + - entlassen + - Bezahlungsstufe ändern + +## 8.2 Noch sinnvolle Backend-Nacharbeiten + +- eigenes Money-Label für Monatskosten, z. B. `servants_monthly` +- optional eigener Read-Endpunkt nur für Dienerschaft +- optionale Validierungsgrenzen serverseitig weiter schärfen +- später: Ableitung von `householdTension` stärker an Diener koppeln + +## 9. UI-Anforderungen + +Die House-UI soll anzeigen: +- aktuelle Dienerzahl +- Sollbereich +- Monatskosten +- Qualität +- Haushaltsordnung +- Bezahlungsstufe +- Besetzungsstatus +- Ordnungsstatus + +Die UI soll direkt erlauben: +- `+1` Diener +- `-1` Diener +- Pay-Level wechseln + +Die UI braucht keine Daemon-Sonderlogik außer normalen House-/Status-Refresh. + +## 10. API-Schnittstellen + +Bereits vorgesehen: +- `GET /api/falukant/houses` +- `POST /api/falukant/houses/servants/hire` +- `POST /api/falukant/houses/servants/dismiss` +- `POST /api/falukant/houses/servants/pay-level` + +### Beispiel-Response für `GET /houses` + +```json +{ + "roofCondition": 100, + "wallCondition": 100, + "floorCondition": 100, + "windowCondition": 100, + "servantCount": 3, + "servantQuality": 58, + "servantPayLevel": "normal", + "householdOrder": 63, + "houseType": { + "id": 5, + "position": 5, + "cost": 273000, + "labelTr": "family_house" + }, + "servantSummary": { + "expectedMin": 3, + "expectedMax": 6, + "monthlyCost": 925.4, + "staffingState": "fitting", + "orderState": "stable" + } +} +``` + +## 11. Technische Architektur + +### 11.1 Quelle der Wahrheit + +Quelle der Wahrheit für: +- Stammdaten und persistente Hauswerte: dieses Backend / Datenbank +- Tick-Ausführung: externer Daemon + +### 11.2 Verantwortungstrennung + +Dieses Projekt: +- speichert Werte +- bietet UI und API +- berechnet einfache Hilfswerte für Anzeige + +Externer Daemon: +- tägliche und monatliche Veränderung +- Kostenabbuchung +- Reputationseffekte +- Verknüpfung mit Familie, Liebschaften und Untergrund + +### 11.3 Warum so + +Damit: +- Spiellogik nicht doppelt tickt +- UI trotzdem schon benutzbar ist +- der Daemon später nur auf stabile Felder aufsetzen muss + +## 12. Implementierungs-Backlog + +## B1 Datenbasis + +Status: erledigt + +Aufgaben: +- Hausfelder in `user_house` +- Migration +- Produktions-SQL + +Done: +- Felder vorhanden +- Model aktualisiert + +## B2 Haus-Service + +Status: erledigt + +Aufgaben: +- Sollbereich berechnen +- Monatskosten berechnen +- Zustandslabels ableiten + +Done: +- `servantSummary` wird im House-Read geliefert + +## B3 Spieleraktionen + +Status: erledigt + +Aufgaben: +- einstellen +- entlassen +- Bezahlung ändern + +Done: +- Endpunkte vorhanden +- UI verdrahtet + +## B4 House-UI + +Status: erledigt + +Aufgaben: +- Anzeige in `HouseView` +- Aktionen +- Locale-Texte + +Done: +- HouseView zeigt den Dienerblock + +## B5 Daemon Daily + +Status: offen + +Aufgaben: +- `expectedMin/Max` im Worker berechnen +- `householdOrder` driften +- `servantQuality` driften +- kleinen Reputationseffekt anwenden +- Diskretionsmodifikator für Liebschaften ableiten +- `daily`-Refresh senden + +Done-Kriterien: +- täglicher Tick verändert Hauswerte nachvollziehbar +- keine zusätzlichen UI-Reason-Werte nötig + +## B6 Daemon Monthly + +Status: offen + +Aufgaben: +- Monatskosten berechnen +- Geld abbuchen +- Unterversorgung behandeln +- Monatsrufeffekte anwenden +- `monthly`-Refresh senden + +Done-Kriterien: +- Monatskosten und Unterversorgung sind im Spiel spürbar + +## B7 Integration mit Familie / Liebschaften + +Status: offen + +Aufgaben: +- `householdOrder` auf Ehekomfort mappen +- Diskretionsmodifikator in Skandal-/Liebschaftslogik einbeziehen +- schlechte Bezahlung oder Überbesetzung als Gerüchtefaktor nutzen + +Done-Kriterien: +- Dienerschaft beeinflusst Familien- und Liebschaftssystem real + +## B8 Integration mit Untergrund + +Status: offen + +Aufgaben: +- `investigate_affair` nutzt Dienerwerte +- schlechter Haushalt erhöht Aufdeckungschance +- guter, diskreter Haushalt senkt Erfolgswahrscheinlichkeit + +Done-Kriterien: +- Untergrund spürt Dienerschaft in Erfolgsmodifikatoren + +## B9 Balancing + +Status: offen, bewusst spätere Phase + +Aufgaben: +- Kosten, Rufwerte, Driftgeschwindigkeiten und Schwellwerte feinjustieren + +## 13. Produktionshinweise + +Wenn keine Migrationen laufen: +- [add_servants_to_user_house.sql](/mnt/share/torsten/Programs/YourPart3/backend/sql/add_servants_to_user_house.sql) ausführen + +Der externe Daemon muss erst danach aktiviert werden, damit die Felder sicher vorhanden sind. + +## 14. Empfehlung für die nächste Reihenfolge + +Empfohlene Reihenfolge ab jetzt: +1. Produktions-SQL einspielen +2. B5 Daily im externen Daemon +3. B6 Monthly im externen Daemon +4. B7 Familie/Liebschaften anbinden +5. B8 Untergrund anbinden +6. B9 Balancing + +## 15. Kurzfazit + +Die Haus- und UI-Basis ist bereits eingebaut. Für eine vollständige Spielwirkung fehlen jetzt vor allem die beiden externen Worker-Blöcke: +- tägliche Drift +- monatliche Kosten und Folgen + +Mit dieser Datei sollte der externe Daemon direkt implementierbar sein, ohne weitere Konzeptdokumente zu benötigen. diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json index 24e9fdd..02d3e59 100644 --- a/frontend/src/i18n/locales/de/falukant.json +++ b/frontend/src/i18n/locales/de/falukant.json @@ -826,8 +826,51 @@ "price": "Kaufpreis", "worth": "Restwert", "sell": "Verkaufen", + "sellConfirm": "Möchtest du dein Haus wirklich verkaufen?", + "sellSuccess": "Das Haus wurde verkauft.", + "sellError": "Das Haus konnte nicht verkauft werden.", + "buySuccess": "Das Haus wurde gekauft.", + "buyError": "Das Haus konnte nicht gekauft werden.", "renovate": "Renovieren", "renovateAll": "Komplett renovieren", + "servants": { + "title": "Dienerschaft", + "description": "Verwalte Hauspersonal, Ordnung und laufende Kosten deines Haushalts.", + "count": "Dienerzahl", + "expectedRange": "Erwarteter Bereich", + "monthlyCost": "Monatskosten", + "quality": "Qualität", + "householdOrder": "Haushaltsordnung", + "payLevel": "Bezahlung", + "payLevels": { + "low": "Niedrig", + "normal": "Normal", + "high": "Großzügig" + }, + "staffingState": { + "label": "Besetzung", + "understaffed": "Unterbesetzt", + "fitting": "Passend", + "overstaffed": "Überbesetzt" + }, + "orderState": { + "label": "Ordnungszustand", + "chaotic": "Chaotisch", + "strained": "Angespannt", + "stable": "Stabil", + "excellent": "Vorbildlich" + }, + "actions": { + "hire": "1 Diener einstellen", + "dismiss": "1 Diener entlassen", + "hireSuccess": "Die Dienerschaft wurde erweitert.", + "hireError": "Die Dienerschaft konnte nicht erweitert werden.", + "dismissSuccess": "Ein Diener wurde entlassen.", + "dismissError": "Der Diener konnte nicht entlassen werden.", + "payLevelSuccess": "Die Bezahlung der Dienerschaft wurde angepasst.", + "payLevelError": "Die Bezahlung konnte nicht angepasst werden." + } + }, "status": { "roofCondition": "Dach", "wallCondition": "Wände", diff --git a/frontend/src/i18n/locales/en/falukant.json b/frontend/src/i18n/locales/en/falukant.json index f8c96de..2442cf7 100644 --- a/frontend/src/i18n/locales/en/falukant.json +++ b/frontend/src/i18n/locales/en/falukant.json @@ -149,7 +149,7 @@ "all": "All history" } }, - "activities": { + "activities": { "Product sale": "Product sale", "Production cost": "Production cost", "Sell all products": "Sell all products", @@ -181,6 +181,75 @@ } } }, + "house": { + "title": "House", + "statusreport": "House condition", + "element": "Element", + "state": "Condition", + "buyablehouses": "Buy a house", + "buy": "Buy", + "price": "Purchase price", + "worth": "Residual value", + "sell": "Sell", + "sellConfirm": "Do you really want to sell your house?", + "sellSuccess": "The house has been sold.", + "sellError": "The house could not be sold.", + "buySuccess": "The house has been bought.", + "buyError": "The house could not be bought.", + "renovate": "Renovate", + "renovateAll": "Renovate completely", + "servants": { + "title": "Servants", + "description": "Manage household staff, order and recurring costs in your home.", + "count": "Servant count", + "expectedRange": "Expected range", + "monthlyCost": "Monthly cost", + "quality": "Quality", + "householdOrder": "Household order", + "payLevel": "Pay level", + "payLevels": { + "low": "Low", + "normal": "Normal", + "high": "Generous" + }, + "staffingState": { + "label": "Staffing", + "understaffed": "Understaffed", + "fitting": "Fitting", + "overstaffed": "Overstaffed" + }, + "orderState": { + "label": "Order state", + "chaotic": "Chaotic", + "strained": "Strained", + "stable": "Stable", + "excellent": "Excellent" + }, + "actions": { + "hire": "Hire 1 servant", + "dismiss": "Dismiss 1 servant", + "hireSuccess": "The household staff has been expanded.", + "hireError": "The staff could not be expanded.", + "dismissSuccess": "A servant has been dismissed.", + "dismissError": "The servant could not be dismissed.", + "payLevelSuccess": "Servant pay has been updated.", + "payLevelError": "Servant pay could not be updated." + } + }, + "status": { + "roofCondition": "Roof", + "wallCondition": "Walls", + "floorCondition": "Floors", + "windowCondition": "Windows" + }, + "type": { + "backyard_room": "Backyard room", + "wooden_house": "Wooden house", + "straw_hut": "Straw hut", + "family_house": "Family house", + "townhouse": "Townhouse" + } + }, "newdirector": { "title": "New Director", "age": "Age", diff --git a/frontend/src/i18n/locales/es/falukant.json b/frontend/src/i18n/locales/es/falukant.json index 09e9b13..a5afa70 100644 --- a/frontend/src/i18n/locales/es/falukant.json +++ b/frontend/src/i18n/locales/es/falukant.json @@ -792,8 +792,51 @@ "price": "Precio de compra", "worth": "Valor restante", "sell": "Vender", + "sellConfirm": "¿De verdad quieres vender tu casa?", + "sellSuccess": "La casa ha sido vendida.", + "sellError": "No se pudo vender la casa.", + "buySuccess": "La casa ha sido comprada.", + "buyError": "No se pudo comprar la casa.", "renovate": "Renovar", "renovateAll": "Renovar por completo", + "servants": { + "title": "Servicio doméstico", + "description": "Administra el personal, el orden y los costes periódicos de tu casa.", + "count": "Número de sirvientes", + "expectedRange": "Rango esperado", + "monthlyCost": "Coste mensual", + "quality": "Calidad", + "householdOrder": "Orden del hogar", + "payLevel": "Pago", + "payLevels": { + "low": "Bajo", + "normal": "Normal", + "high": "Generoso" + }, + "staffingState": { + "label": "Dotación", + "understaffed": "Insuficiente", + "fitting": "Adecuada", + "overstaffed": "Excesiva" + }, + "orderState": { + "label": "Estado del orden", + "chaotic": "Caótico", + "strained": "Tenso", + "stable": "Estable", + "excellent": "Excelente" + }, + "actions": { + "hire": "Contratar 1 sirviente", + "dismiss": "Despedir 1 sirviente", + "hireSuccess": "Se ha ampliado el servicio doméstico.", + "hireError": "No se pudo ampliar el servicio doméstico.", + "dismissSuccess": "Se ha despedido a un sirviente.", + "dismissError": "No se pudo despedir al sirviente.", + "payLevelSuccess": "Se ha ajustado el pago del servicio.", + "payLevelError": "No se pudo ajustar el pago." + } + }, "status": { "roofCondition": "Techo", "wallCondition": "Paredes", diff --git a/frontend/src/views/falukant/HouseView.vue b/frontend/src/views/falukant/HouseView.vue index 3695e57..b9a8129 100644 --- a/frontend/src/views/falukant/HouseView.vue +++ b/frontend/src/views/falukant/HouseView.vue @@ -34,6 +34,65 @@ +
+
+
+

{{ $t('falukant.house.servants.title') }}

+

{{ $t('falukant.house.servants.description') }}

+
+
+ + +
+
+ +
+
+ {{ $t('falukant.house.servants.count') }} + {{ userHouse.servantCount || 0 }} +
+
+ {{ $t('falukant.house.servants.expectedRange') }} + {{ servantSummary.expectedMin }} - {{ servantSummary.expectedMax }} +
+
+ {{ $t('falukant.house.servants.monthlyCost') }} + {{ formatPrice(servantSummary.monthlyCost || 0) }} {{ currency }} +
+
+ {{ $t('falukant.house.servants.quality') }} + {{ userHouse.servantQuality || 0 }} +
+
+ {{ $t('falukant.house.servants.householdOrder') }} + {{ userHouse.householdOrder || 0 }} +
+
+ {{ $t('falukant.house.servants.staffingState.label') }} + {{ $t(`falukant.house.servants.staffingState.${servantSummary.staffingState || 'fitting'}`) }} +
+
+ +
+ +
+ {{ $t('falukant.house.servants.orderState.label') }}: + {{ $t(`falukant.house.servants.orderState.${servantSummary.orderState || 'stable'}`) }} +
+
+
+

{{ $t('falukant.house.buyablehouses') }}

@@ -67,6 +126,7 @@ import StatusBar from '@/components/falukant/StatusBar.vue'; import apiClient from '@/utils/axios.js'; import { mapState } from 'vuex'; +import { showError, showSuccess, confirmAction } from '@/utils/feedback.js'; export default { name: 'HouseView', @@ -76,6 +136,15 @@ export default { userHouse: null, houseType: {}, status: {}, + servantSummary: { + expectedMin: 0, + expectedMax: 0, + monthlyCost: 0, + staffingState: 'fitting', + orderState: 'stable' + }, + servantPayLevel: 'normal', + servantPayOptions: ['low', 'normal', 'high'], buyableHouses: [], currency: '€' }; @@ -94,6 +163,8 @@ export default { this.houseType = this.userHouse.houseType; const { roofCondition, wallCondition, floorCondition, windowCondition } = this.userHouse; this.status = { roofCondition, wallCondition, floorCondition, windowCondition }; + this.servantSummary = this.userHouse.servantSummary || this.servantSummary; + this.servantPayLevel = this.userHouse.servantPayLevel || 'normal'; const buyRes = await apiClient.get('/api/falukant/houses/buyable'); this.buyableHouses = buyRes.data; @@ -172,19 +243,60 @@ export default { } }, async sellHouse() { + const confirmed = await confirmAction(this, { + title: this.$t('falukant.house.sell'), + text: this.$t('falukant.house.sellConfirm') + }); + if (!confirmed) return; try { await apiClient.post('/api/falukant/houses/sell'); await this.loadData(); + showSuccess(this, this.$t('falukant.house.sellSuccess')); } catch (err) { console.error('Error selling house', err); + showError(this, this.$t('falukant.house.sellError')); } }, async buyHouse(id) { try { await apiClient.post('/api/falukant/houses', { houseId: id }); await this.loadData(); + showSuccess(this, this.$t('falukant.house.buySuccess')); } catch (err) { console.error('Error buying house', err); + showError(this, this.$t('falukant.house.buyError')); + } + }, + async hireServant() { + try { + await apiClient.post('/api/falukant/houses/servants/hire', { amount: 1 }); + await this.loadData(); + showSuccess(this, this.$t('falukant.house.servants.actions.hireSuccess')); + } catch (err) { + console.error('Error hiring servant', err); + showError(this, this.$t('falukant.house.servants.actions.hireError')); + } + }, + async dismissServant() { + try { + await apiClient.post('/api/falukant/houses/servants/dismiss', { amount: 1 }); + await this.loadData(); + showSuccess(this, this.$t('falukant.house.servants.actions.dismissSuccess')); + } catch (err) { + console.error('Error dismissing servant', err); + showError(this, this.$t('falukant.house.servants.actions.dismissError')); + } + }, + async updateServantPayLevel() { + try { + await apiClient.post('/api/falukant/houses/servants/pay-level', { + payLevel: this.servantPayLevel + }); + await this.loadData(); + showSuccess(this, this.$t('falukant.house.servants.actions.payLevelSuccess')); + } catch (err) { + console.error('Error updating servant pay level', err); + showError(this, this.$t('falukant.house.servants.actions.payLevelError')); } }, handleDaemonMessage(evt) { @@ -258,6 +370,78 @@ h2 { padding: 18px; } +.servants-panel { + padding: 18px; +} + +.servants-panel__header { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: flex-start; + margin-bottom: 16px; +} + +.servants-panel__header h3 { + margin: 0 0 6px; +} + +.servants-panel__header p { + margin: 0; + color: var(--color-text-secondary); +} + +.servants-panel__actions { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.servants-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 12px; + margin-bottom: 16px; +} + +.servant-card { + display: flex; + flex-direction: column; + gap: 6px; + padding: 14px 16px; + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + background: rgba(255, 255, 255, 0.68); +} + +.servant-card__label { + color: var(--color-text-secondary); + font-size: 0.88rem; +} + +.servants-settings { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + flex-wrap: wrap; +} + +.servants-settings__label { + display: flex; + flex-direction: column; + gap: 6px; + font-weight: 600; +} + +.servants-settings__select { + min-width: 200px; +} + +.servants-settings__state { + color: var(--color-text-secondary); +} + .buyable-houses { display: flex; flex-direction: column; @@ -356,6 +540,10 @@ button { flex-direction: column; } + .servants-panel__header { + flex-direction: column; + } + .house { width: min(341px, 100%); margin: 0 auto;