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.

This commit is contained in:
Torsten Schulz (local)
2026-03-22 09:57:44 +01:00
parent 2977b152a2
commit 876ee2ab49
12 changed files with 1661 additions and 17 deletions

View File

@@ -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) => {

View File

@@ -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');
}
};

View File

@@ -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

View File

@@ -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);

View File

@@ -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({

View File

@@ -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;

View File

@@ -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.

View File

@@ -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.

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -34,6 +34,65 @@
</div>
</div>
<section v-if="userHouse" class="servants-panel surface-card">
<div class="servants-panel__header">
<div>
<h3>{{ $t('falukant.house.servants.title') }}</h3>
<p>{{ $t('falukant.house.servants.description') }}</p>
</div>
<div class="servants-panel__actions">
<button @click="hireServant">
{{ $t('falukant.house.servants.actions.hire') }}
</button>
<button class="button-secondary" :disabled="(userHouse.servantCount || 0) <= 0" @click="dismissServant">
{{ $t('falukant.house.servants.actions.dismiss') }}
</button>
</div>
</div>
<div class="servants-grid">
<article class="servant-card">
<span class="servant-card__label">{{ $t('falukant.house.servants.count') }}</span>
<strong>{{ userHouse.servantCount || 0 }}</strong>
</article>
<article class="servant-card">
<span class="servant-card__label">{{ $t('falukant.house.servants.expectedRange') }}</span>
<strong>{{ servantSummary.expectedMin }} - {{ servantSummary.expectedMax }}</strong>
</article>
<article class="servant-card">
<span class="servant-card__label">{{ $t('falukant.house.servants.monthlyCost') }}</span>
<strong>{{ formatPrice(servantSummary.monthlyCost || 0) }} {{ currency }}</strong>
</article>
<article class="servant-card">
<span class="servant-card__label">{{ $t('falukant.house.servants.quality') }}</span>
<strong>{{ userHouse.servantQuality || 0 }}</strong>
</article>
<article class="servant-card">
<span class="servant-card__label">{{ $t('falukant.house.servants.householdOrder') }}</span>
<strong>{{ userHouse.householdOrder || 0 }}</strong>
</article>
<article class="servant-card">
<span class="servant-card__label">{{ $t('falukant.house.servants.staffingState.label') }}</span>
<strong>{{ $t(`falukant.house.servants.staffingState.${servantSummary.staffingState || 'fitting'}`) }}</strong>
</article>
</div>
<div class="servants-settings">
<label class="servants-settings__label">
{{ $t('falukant.house.servants.payLevel') }}
<select v-model="servantPayLevel" @change="updateServantPayLevel" class="servants-settings__select">
<option v-for="option in servantPayOptions" :key="option" :value="option">
{{ $t(`falukant.house.servants.payLevels.${option}`) }}
</option>
</select>
</label>
<div class="servants-settings__state">
{{ $t('falukant.house.servants.orderState.label') }}:
<strong>{{ $t(`falukant.house.servants.orderState.${servantSummary.orderState || 'stable'}`) }}</strong>
</div>
</div>
</section>
<div class="buyable-houses">
<h3>{{ $t('falukant.house.buyablehouses') }}</h3>
<div class="houses-list">
@@ -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;