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.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) => {
|
||||
|
||||
@@ -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,
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user