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:
@@ -146,6 +146,9 @@ class FalukantController {
|
|||||||
this.getUserHouse = this._wrapWithUser((userId) => this.service.getUserHouse(userId));
|
this.getUserHouse = this._wrapWithUser((userId) => this.service.getUserHouse(userId));
|
||||||
this.getBuyableHouses = this._wrapWithUser((userId) => this.service.getBuyableHouses(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.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.getPartyTypes = this._wrapWithUser((userId) => this.service.getPartyTypes(userId));
|
||||||
this.createParty = this._wrapWithUser((userId, req) => {
|
this.createParty = this._wrapWithUser((userId, req) => {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -24,6 +24,26 @@ UserHouse.init({
|
|||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 100
|
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: {
|
houseTypeId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false
|
allowNull: false
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ router.get('/houses/buyable', falukantController.getBuyableHouses);
|
|||||||
router.get('/houses', falukantController.getUserHouse);
|
router.get('/houses', falukantController.getUserHouse);
|
||||||
router.post('/houses/renovate-all', falukantController.renovateAll);
|
router.post('/houses/renovate-all', falukantController.renovateAll);
|
||||||
router.post('/houses/renovate', falukantController.renovate);
|
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.post('/houses', falukantController.buyUserHouse);
|
||||||
router.get('/party/types', falukantController.getPartyTypes);
|
router.get('/party/types', falukantController.getPartyTypes);
|
||||||
router.post('/party', falukantController.createParty);
|
router.post('/party', falukantController.createParty);
|
||||||
|
|||||||
@@ -4014,25 +4014,45 @@ class FalukantService extends BaseService {
|
|||||||
|
|
||||||
async getUserHouse(hashedUserId) {
|
async getUserHouse(hashedUserId) {
|
||||||
try {
|
try {
|
||||||
const user = await User.findOne({
|
const falukantUser = await this.getFalukantUserByHashedId(hashedUserId);
|
||||||
where: { hashedId: hashedUserId },
|
const userHouse = await UserHouse.findOne({
|
||||||
|
where: { userId: falukantUser.id },
|
||||||
include: [{
|
include: [{
|
||||||
model: FalukantUser,
|
model: HouseType,
|
||||||
as: 'falukantData',
|
as: 'houseType',
|
||||||
include: [{
|
attributes: ['id', 'position', 'cost', 'labelTr']
|
||||||
model: UserHouse,
|
}],
|
||||||
as: 'userHouse',
|
attributes: [
|
||||||
include: [{
|
'roofCondition',
|
||||||
model: HouseType,
|
'wallCondition',
|
||||||
as: 'houseType',
|
'floorCondition',
|
||||||
attributes: ['position', 'cost']
|
'windowCondition',
|
||||||
}],
|
'servantCount',
|
||||||
attributes: ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition']
|
'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) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return {};
|
return {};
|
||||||
@@ -4089,9 +4109,14 @@ class FalukantService extends BaseService {
|
|||||||
if (oldHouse) {
|
if (oldHouse) {
|
||||||
await oldHouse.destroy();
|
await oldHouse.destroy();
|
||||||
}
|
}
|
||||||
|
const servantDefaults = this.getInitialServantState(house.houseType, falukantUser.character);
|
||||||
await UserHouse.create({
|
await UserHouse.create({
|
||||||
userId: falukantUser.id,
|
userId: falukantUser.id,
|
||||||
houseTypeId: house.houseTypeId,
|
houseTypeId: house.houseTypeId,
|
||||||
|
servantCount: servantDefaults.servantCount,
|
||||||
|
servantQuality: servantDefaults.servantQuality,
|
||||||
|
servantPayLevel: servantDefaults.servantPayLevel,
|
||||||
|
householdOrder: servantDefaults.householdOrder
|
||||||
});
|
});
|
||||||
await house.destroy();
|
await house.destroy();
|
||||||
await updateFalukantUserMoney(falukantUser.id, -housePrice, "housebuy", falukantUser.id);
|
await updateFalukantUserMoney(falukantUser.id, -housePrice, "housebuy", falukantUser.id);
|
||||||
@@ -4109,6 +4134,232 @@ class FalukantService extends BaseService {
|
|||||||
return (house.houseType.cost / 100 * houseQuality).toFixed(2);
|
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) {
|
async getPartyTypes(hashedUserId) {
|
||||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||||
const engagedCount = await Relationship.count({
|
const engagedCount = await Relationship.count({
|
||||||
|
|||||||
7
backend/sql/add_servants_to_user_house.sql
Normal file
7
backend/sql/add_servants_to_user_house.sql
Normal 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;
|
||||||
336
docs/FALUKANT_SERVANTS_CONCEPT.md
Normal file
336
docs/FALUKANT_SERVANTS_CONCEPT.md
Normal 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.
|
||||||
628
docs/FALUKANT_SERVANTS_IMPLEMENTATION_SPEC.md
Normal file
628
docs/FALUKANT_SERVANTS_IMPLEMENTATION_SPEC.md
Normal 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.
|
||||||
@@ -826,8 +826,51 @@
|
|||||||
"price": "Kaufpreis",
|
"price": "Kaufpreis",
|
||||||
"worth": "Restwert",
|
"worth": "Restwert",
|
||||||
"sell": "Verkaufen",
|
"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",
|
"renovate": "Renovieren",
|
||||||
"renovateAll": "Komplett 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": {
|
"status": {
|
||||||
"roofCondition": "Dach",
|
"roofCondition": "Dach",
|
||||||
"wallCondition": "Wände",
|
"wallCondition": "Wände",
|
||||||
|
|||||||
@@ -149,7 +149,7 @@
|
|||||||
"all": "All history"
|
"all": "All history"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"activities": {
|
"activities": {
|
||||||
"Product sale": "Product sale",
|
"Product sale": "Product sale",
|
||||||
"Production cost": "Production cost",
|
"Production cost": "Production cost",
|
||||||
"Sell all products": "Sell all products",
|
"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": {
|
"newdirector": {
|
||||||
"title": "New Director",
|
"title": "New Director",
|
||||||
"age": "Age",
|
"age": "Age",
|
||||||
|
|||||||
@@ -792,8 +792,51 @@
|
|||||||
"price": "Precio de compra",
|
"price": "Precio de compra",
|
||||||
"worth": "Valor restante",
|
"worth": "Valor restante",
|
||||||
"sell": "Vender",
|
"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",
|
"renovate": "Renovar",
|
||||||
"renovateAll": "Renovar por completo",
|
"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": {
|
"status": {
|
||||||
"roofCondition": "Techo",
|
"roofCondition": "Techo",
|
||||||
"wallCondition": "Paredes",
|
"wallCondition": "Paredes",
|
||||||
|
|||||||
@@ -34,6 +34,65 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="buyable-houses">
|
||||||
<h3>{{ $t('falukant.house.buyablehouses') }}</h3>
|
<h3>{{ $t('falukant.house.buyablehouses') }}</h3>
|
||||||
<div class="houses-list">
|
<div class="houses-list">
|
||||||
@@ -67,6 +126,7 @@
|
|||||||
import StatusBar from '@/components/falukant/StatusBar.vue';
|
import StatusBar from '@/components/falukant/StatusBar.vue';
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
|
import { showError, showSuccess, confirmAction } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HouseView',
|
name: 'HouseView',
|
||||||
@@ -76,6 +136,15 @@ export default {
|
|||||||
userHouse: null,
|
userHouse: null,
|
||||||
houseType: {},
|
houseType: {},
|
||||||
status: {},
|
status: {},
|
||||||
|
servantSummary: {
|
||||||
|
expectedMin: 0,
|
||||||
|
expectedMax: 0,
|
||||||
|
monthlyCost: 0,
|
||||||
|
staffingState: 'fitting',
|
||||||
|
orderState: 'stable'
|
||||||
|
},
|
||||||
|
servantPayLevel: 'normal',
|
||||||
|
servantPayOptions: ['low', 'normal', 'high'],
|
||||||
buyableHouses: [],
|
buyableHouses: [],
|
||||||
currency: '€'
|
currency: '€'
|
||||||
};
|
};
|
||||||
@@ -94,6 +163,8 @@ export default {
|
|||||||
this.houseType = this.userHouse.houseType;
|
this.houseType = this.userHouse.houseType;
|
||||||
const { roofCondition, wallCondition, floorCondition, windowCondition } = this.userHouse;
|
const { roofCondition, wallCondition, floorCondition, windowCondition } = this.userHouse;
|
||||||
this.status = { roofCondition, wallCondition, floorCondition, windowCondition };
|
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');
|
const buyRes = await apiClient.get('/api/falukant/houses/buyable');
|
||||||
this.buyableHouses = buyRes.data;
|
this.buyableHouses = buyRes.data;
|
||||||
@@ -172,19 +243,60 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async sellHouse() {
|
async sellHouse() {
|
||||||
|
const confirmed = await confirmAction(this, {
|
||||||
|
title: this.$t('falukant.house.sell'),
|
||||||
|
text: this.$t('falukant.house.sellConfirm')
|
||||||
|
});
|
||||||
|
if (!confirmed) return;
|
||||||
try {
|
try {
|
||||||
await apiClient.post('/api/falukant/houses/sell');
|
await apiClient.post('/api/falukant/houses/sell');
|
||||||
await this.loadData();
|
await this.loadData();
|
||||||
|
showSuccess(this, this.$t('falukant.house.sellSuccess'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error selling house', err);
|
console.error('Error selling house', err);
|
||||||
|
showError(this, this.$t('falukant.house.sellError'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async buyHouse(id) {
|
async buyHouse(id) {
|
||||||
try {
|
try {
|
||||||
await apiClient.post('/api/falukant/houses', { houseId: id });
|
await apiClient.post('/api/falukant/houses', { houseId: id });
|
||||||
await this.loadData();
|
await this.loadData();
|
||||||
|
showSuccess(this, this.$t('falukant.house.buySuccess'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error buying house', 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) {
|
handleDaemonMessage(evt) {
|
||||||
@@ -258,6 +370,78 @@ h2 {
|
|||||||
padding: 18px;
|
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 {
|
.buyable-houses {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -356,6 +540,10 @@ button {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.servants-panel__header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.house {
|
.house {
|
||||||
width: min(341px, 100%);
|
width: min(341px, 100%);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
Reference in New Issue
Block a user