Add marriage management features: Implement endpoints for spending time with, gifting to, and reconciling with spouses in the FalukantController. Update UserHouse model to include household tension attributes. Enhance frontend components to manage marriage actions and display household tension details, including localization updates in multiple languages.

This commit is contained in:
Torsten Schulz (local)
2026-03-23 09:34:56 +01:00
parent 2055c11fd9
commit f7e0d97174
23 changed files with 1997 additions and 52 deletions

View File

@@ -120,6 +120,12 @@ class FalukantController {
this.service.setLoverMaintenance(userId, req.params.relationshipId, req.body?.maintenanceLevel)); this.service.setLoverMaintenance(userId, req.params.relationshipId, req.body?.maintenanceLevel));
this.createLoverRelationship = this._wrapWithUser((userId, req) => this.createLoverRelationship = this._wrapWithUser((userId, req) =>
this.service.createLoverRelationship(userId, req.body?.targetCharacterId, req.body?.loverRole), { successStatus: 201 }); this.service.createLoverRelationship(userId, req.body?.targetCharacterId, req.body?.loverRole), { successStatus: 201 });
this.spendTimeWithSpouse = this._wrapWithUser((userId) =>
this.service.spendTimeWithSpouse(userId));
this.giftToSpouse = this._wrapWithUser((userId, req) =>
this.service.giftToSpouse(userId, req.body?.giftLevel));
this.reconcileMarriage = this._wrapWithUser((userId) =>
this.service.reconcileMarriage(userId));
this.acknowledgeLover = this._wrapWithUser((userId, req) => this.acknowledgeLover = this._wrapWithUser((userId, req) =>
this.service.acknowledgeLover(userId, req.params.relationshipId)); this.service.acknowledgeLover(userId, req.params.relationshipId));
this.endLoverRelationship = this._wrapWithUser((userId, req) => this.endLoverRelationship = this._wrapWithUser((userId, req) =>
@@ -149,6 +155,7 @@ class FalukantController {
this.hireServants = this._wrapWithUser((userId, req) => this.service.hireServants(userId, req.body?.amount), { 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.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.setServantPayLevel = this._wrapWithUser((userId, req) => this.service.setServantPayLevel(userId, req.body?.payLevel));
this.tidyHousehold = this._wrapWithUser((userId) => this.service.tidyHousehold(userId));
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) => {

View File

@@ -0,0 +1,36 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn(
{ schema: 'falukant_data', tableName: 'user_house' },
'household_tension_score',
{
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 10
}
);
await queryInterface.addColumn(
{ schema: 'falukant_data', tableName: 'user_house' },
'household_tension_reasons_json',
{
type: Sequelize.JSONB,
allowNull: true
}
);
},
async down(queryInterface) {
await queryInterface.removeColumn(
{ schema: 'falukant_data', tableName: 'user_house' },
'household_tension_reasons_json'
);
await queryInterface.removeColumn(
{ schema: 'falukant_data', tableName: 'user_house' },
'household_tension_score'
);
}
};

View File

@@ -44,6 +44,15 @@ UserHouse.init({
allowNull: false, allowNull: false,
defaultValue: 55 defaultValue: 55
}, },
householdTensionScore: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 10
},
householdTensionReasonsJson: {
type: DataTypes.JSONB,
allowNull: true
},
houseTypeId: { houseTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false allowNull: false

View File

@@ -48,6 +48,9 @@ router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageP
router.post('/family/cancel-wooing', falukantController.cancelWooing); router.post('/family/cancel-wooing', falukantController.cancelWooing);
router.post('/family/set-heir', falukantController.setHeir); router.post('/family/set-heir', falukantController.setHeir);
router.post('/family/lover', falukantController.createLoverRelationship); router.post('/family/lover', falukantController.createLoverRelationship);
router.post('/family/marriage/spend-time', falukantController.spendTimeWithSpouse);
router.post('/family/marriage/gift', falukantController.giftToSpouse);
router.post('/family/marriage/reconcile', falukantController.reconcileMarriage);
router.post('/family/lover/:relationshipId/maintenance', falukantController.setLoverMaintenance); router.post('/family/lover/:relationshipId/maintenance', falukantController.setLoverMaintenance);
router.post('/family/lover/:relationshipId/acknowledge', falukantController.acknowledgeLover); router.post('/family/lover/:relationshipId/acknowledge', falukantController.acknowledgeLover);
router.post('/family/lover/:relationshipId/end', falukantController.endLoverRelationship); router.post('/family/lover/:relationshipId/end', falukantController.endLoverRelationship);
@@ -68,6 +71,7 @@ router.post('/houses/renovate', falukantController.renovate);
router.post('/houses/servants/hire', falukantController.hireServants); router.post('/houses/servants/hire', falukantController.hireServants);
router.post('/houses/servants/dismiss', falukantController.dismissServants); router.post('/houses/servants/dismiss', falukantController.dismissServants);
router.post('/houses/servants/pay-level', falukantController.setServantPayLevel); router.post('/houses/servants/pay-level', falukantController.setServantPayLevel);
router.post('/houses/order', falukantController.tidyHousehold);
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);

View File

@@ -517,6 +517,167 @@ class FalukantService extends BaseService {
return 'stable'; return 'stable';
} }
getHouseholdTensionLabel(score) {
if (score == null) return null;
if (score >= 60) return 'high';
if (score >= 25) return 'medium';
return 'low';
}
clampScore(value) {
return Math.max(0, Math.min(100, Math.round(Number(value) || 0)));
}
calculateHouseholdTension({ lovers = [], marriageSatisfaction = null, userHouse = null, children = [] }) {
let score = 10;
const reasons = [];
for (const lover of lovers) {
const visibility = Number(lover.visibility || 0);
const monthsUnderfunded = Number(lover.monthsUnderfunded || 0);
const statusFit = Number(lover.statusFit || 0);
if (visibility >= 60) {
score += 18;
reasons.push('visibleLover');
} else if (visibility >= 35) {
score += 10;
reasons.push('noticeableLover');
} else {
score += 4;
}
if (monthsUnderfunded >= 1) {
score += 6;
reasons.push('underfundedLover');
}
if (monthsUnderfunded >= 2) score += 6;
if (lover.acknowledged) {
score += 4;
reasons.push('acknowledgedAffair');
}
if (statusFit === -1) score += 3;
if (statusFit <= -2) {
score += 6;
reasons.push('statusMismatch');
}
}
for (const child of children) {
if (child.birthContext !== 'lover') continue;
score += child.publicKnown ? 6 : 2;
if (child.publicKnown) reasons.push('loverChild');
if (child.legitimacy === 'acknowledged_bastard') score += 2;
if (child.legitimacy === 'hidden_bastard') score += 4;
}
const householdOrder = Number(userHouse?.householdOrder ?? 55);
const servantCount = Number(userHouse?.servantCount ?? 0);
const servantQuality = Number(userHouse?.servantQuality ?? 50);
const servantPayLevel = userHouse?.servantPayLevel || 'normal';
const expectation = this.getServantExpectation(userHouse?.houseType, userHouse?.character || null);
if (householdOrder >= 80) score -= 6;
else if (householdOrder >= 65) score -= 3;
if (householdOrder <= 35) {
score += 8;
reasons.push('disorder');
} else if (householdOrder <= 50) {
score += 4;
}
if (servantCount < expectation.min) {
score += 5;
reasons.push('tooFewServants');
}
if (servantPayLevel === 'low') score += 2;
if (servantQuality >= 70 && servantPayLevel === 'high') score -= 3;
if (marriageSatisfaction != null && marriageSatisfaction <= 35) {
score += 6;
reasons.push('marriageCrisis');
}
if (marriageSatisfaction != null && marriageSatisfaction >= 75) score -= 2;
const normalizedScore = this.clampScore(score);
return {
score: normalizedScore,
label: this.getHouseholdTensionLabel(normalizedScore),
reasons: [...new Set(reasons)]
};
}
async refreshHouseholdTensionState(falukantUser, character = falukantUser?.character) {
if (!falukantUser?.id || !character?.id) return null;
const userHouse = await UserHouse.findOne({
where: { userId: falukantUser.id },
include: [{ model: HouseType, as: 'houseType', attributes: ['id', 'position', 'cost', 'labelTr'] }]
});
if (!userHouse) return null;
const relationshipTypes = await RelationshipType.findAll({
where: { tr: ['lover', 'married'] },
attributes: ['id', 'tr']
});
const relationshipTypeIds = relationshipTypes.map((type) => type.id);
const relationshipTypeMap = Object.fromEntries(relationshipTypes.map((type) => [type.id, type.tr]));
const relationships = relationshipTypeIds.length
? await Relationship.findAll({
where: {
character1Id: character.id,
relationshipTypeId: relationshipTypeIds
},
include: [{ model: RelationshipState, as: 'state', required: false }],
attributes: ['relationshipTypeId']
})
: [];
const marriage = relationships.find((rel) => relationshipTypeMap[rel.relationshipTypeId] === 'married') || null;
const lovers = relationships
.filter((rel) => relationshipTypeMap[rel.relationshipTypeId] === 'lover')
.map((rel) => rel.state)
.filter((state) => (state?.active ?? true) !== false)
.map((state) => ({
visibility: state?.visibility ?? 0,
monthsUnderfunded: state?.monthsUnderfunded ?? 0,
acknowledged: !!state?.acknowledged,
statusFit: state?.statusFit ?? 0
}));
const children = await ChildRelation.findAll({
where: {
[Op.or]: [
{ fatherCharacterId: character.id },
{ motherCharacterId: character.id }
]
},
attributes: ['birthContext', 'legitimacy', 'publicKnown']
});
userHouse.setDataValue('character', character);
const householdTension = this.calculateHouseholdTension({
lovers,
marriageSatisfaction: marriage?.state?.marriageSatisfaction ?? null,
userHouse,
children: children.map((rel) => ({
birthContext: rel.birthContext,
legitimacy: rel.legitimacy,
publicKnown: !!rel.publicKnown
}))
});
await userHouse.update({
householdTensionScore: householdTension.score,
householdTensionReasonsJson: householdTension.reasons
});
return householdTension;
}
getLoverRiskState(state) { getLoverRiskState(state) {
if (!state) return 'low'; if (!state) return 'low';
if ((state.visibility ?? 0) >= 60 || (state.monthsUnderfunded ?? 0) >= 2) return 'high'; if ((state.visibility ?? 0) >= 60 || (state.monthsUnderfunded ?? 0) >= 2) return 'high';
@@ -526,7 +687,11 @@ class FalukantService extends BaseService {
calculateLoverStatusFit(ownTitleId, targetTitleId) { calculateLoverStatusFit(ownTitleId, targetTitleId) {
const diff = Math.abs(Number(ownTitleId || 0) - Number(targetTitleId || 0)); const diff = Math.abs(Number(ownTitleId || 0) - Number(targetTitleId || 0));
return Math.max(0, 100 - diff * 20); if (diff === 0) return 2;
if (diff === 1) return 1;
if (diff === 2) return 0;
if (diff === 3) return -1;
return -2;
} }
calculateLoverBaseCost(ownTitleId, targetTitleId) { calculateLoverBaseCost(ownTitleId, targetTitleId) {
@@ -3026,6 +3191,13 @@ class FalukantService extends BaseService {
const activeMarriage = activeRelationships.find(r => r.relationshipType === 'married') || activeRelationships[0] || null; const activeMarriage = activeRelationships.find(r => r.relationshipType === 'married') || activeRelationships[0] || null;
const marriageSatisfaction = activeMarriage?.state?.marriageSatisfaction ?? null; const marriageSatisfaction = activeMarriage?.state?.marriageSatisfaction ?? null;
const marriageState = this.getMarriageStateLabel(marriageSatisfaction); const marriageState = this.getMarriageStateLabel(marriageSatisfaction);
const userHouse = await UserHouse.findOne({
where: { userId: user.id },
include: [{ model: HouseType, as: 'houseType', attributes: ['id', 'position', 'cost', 'labelTr'] }]
});
if (userHouse) {
userHouse.setDataValue('character', character);
}
const lovers = relationships const lovers = relationships
.filter(r => r.relationshipType === 'lover') .filter(r => r.relationshipType === 'lover')
.filter(r => (r.state?.active ?? true) !== false) .filter(r => (r.state?.active ?? true) !== false)
@@ -3057,6 +3229,17 @@ class FalukantService extends BaseService {
state, state,
}; };
}); });
const derivedHouseholdTension = this.calculateHouseholdTension({
lovers,
marriageSatisfaction,
userHouse,
children
});
const householdTension = {
score: Number(userHouse?.householdTensionScore ?? derivedHouseholdTension.score),
reasons: Array.isArray(userHouse?.householdTensionReasonsJson) ? userHouse.householdTensionReasonsJson : derivedHouseholdTension.reasons
};
householdTension.label = this.getHouseholdTensionLabel(householdTension.score);
const family = { const family = {
relationships: activeRelationships.map((r) => ({ relationships: activeRelationships.map((r) => ({
...r, ...r,
@@ -3065,7 +3248,9 @@ class FalukantService extends BaseService {
})), })),
marriageSatisfaction, marriageSatisfaction,
marriageState, marriageState,
householdTension: lovers.some(l => l.riskState === 'high') ? 'high' : lovers.some(l => l.riskState === 'medium') ? 'medium' : 'low', householdTension: householdTension.label,
householdTensionScore: householdTension.score,
householdTensionReasons: householdTension.reasons,
lovers, lovers,
deathPartners: relationships.filter(r => r.relationshipType === 'widowed'), deathPartners: relationships.filter(r => r.relationshipType === 'widowed'),
children: children.map(({ _createdAt, ...rest }) => rest), children: children.map(({ _createdAt, ...rest }) => rest),
@@ -3203,6 +3388,7 @@ class FalukantService extends BaseService {
statusFit: target.statusFit, statusFit: target.statusFit,
active: true active: true
}); });
await this.refreshHouseholdTensionState(user, user.character);
await notifyUser(hashedUserId, 'falukantUpdateFamily', { reason: 'daily' }); await notifyUser(hashedUserId, 'falukantUpdateFamily', { reason: 'daily' });
await notifyUser(hashedUserId, 'falukantUpdateStatus', {}); await notifyUser(hashedUserId, 'falukantUpdateStatus', {});
@@ -3224,8 +3410,9 @@ class FalukantService extends BaseService {
throw { status: 400, message: 'maintenanceLevel must be between 0 and 100' }; throw { status: 400, message: 'maintenanceLevel must be between 0 and 100' };
} }
const { state } = await this.getOwnedLoverRelationState(hashedUserId, parsedRelationshipId); const { user, state } = await this.getOwnedLoverRelationState(hashedUserId, parsedRelationshipId);
await state.update({ maintenanceLevel: parsedMaintenance }); await state.update({ maintenanceLevel: parsedMaintenance });
await this.refreshHouseholdTensionState(user, user.character);
return { return {
success: true, success: true,
relationshipId: parsedRelationshipId, relationshipId: parsedRelationshipId,
@@ -3233,6 +3420,119 @@ class FalukantService extends BaseService {
}; };
} }
async spendTimeWithSpouse(hashedUserId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
if (!user?.character?.id) throw new Error('User or character not found');
const marriage = await Relationship.findOne({
where: { character1Id: user.character.id },
include: [
{ model: RelationshipType, as: 'relationshipType', where: { tr: 'married' } },
{ model: RelationshipState, as: 'state', required: false }
]
});
if (!marriage) throw { status: 409, message: 'No active marriage found' };
let state = marriage.state;
if (!state) {
state = await RelationshipState.create({
relationshipId: marriage.id,
...this.buildDefaultRelationshipState('married')
});
}
const nextSatisfaction = this.clampScore((state.marriageSatisfaction ?? 55) + 2);
const nextPublicStability = this.clampScore((state.marriagePublicStability ?? 55) + 1);
await state.update({
marriageSatisfaction: nextSatisfaction,
marriagePublicStability: nextPublicStability
});
await this.refreshHouseholdTensionState(user, user.character);
await notifyUser(hashedUserId, 'falukantUpdateFamily', { reason: 'daily' });
await notifyUser(hashedUserId, 'falukantUpdateStatus', {});
return { success: true, marriageSatisfaction: nextSatisfaction };
}
async giftToSpouse(hashedUserId, giftLevel) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
if (!user?.character?.id) throw new Error('User or character not found');
const marriage = await Relationship.findOne({
where: { character1Id: user.character.id },
include: [
{ model: RelationshipType, as: 'relationshipType', where: { tr: 'married' } },
{ model: RelationshipState, as: 'state', required: false }
]
});
if (!marriage) throw { status: 409, message: 'No active marriage found' };
const levelConfig = {
small: { cost: 25, satisfaction: 2, publicStability: 1 },
decent: { cost: 80, satisfaction: 4, publicStability: 2 },
lavish: { cost: 180, satisfaction: 7, publicStability: 3 }
};
const config = levelConfig[giftLevel] || levelConfig.small;
if (Number(user.money) < config.cost) throw new Error('notenoughmoney.');
let state = marriage.state;
if (!state) {
state = await RelationshipState.create({
relationshipId: marriage.id,
...this.buildDefaultRelationshipState('married')
});
}
const nextSatisfaction = this.clampScore((state.marriageSatisfaction ?? 55) + config.satisfaction);
const nextPublicStability = this.clampScore((state.marriagePublicStability ?? 55) + config.publicStability);
await sequelize.transaction(async () => {
await state.update({
marriageSatisfaction: nextSatisfaction,
marriagePublicStability: nextPublicStability
});
await updateFalukantUserMoney(user.id, -config.cost, 'marriage_gift', user.id);
});
await this.refreshHouseholdTensionState(user, user.character);
await notifyUser(hashedUserId, 'falukantUpdateFamily', { reason: 'daily' });
await notifyUser(hashedUserId, 'falukantUpdateStatus', {});
return { success: true, marriageSatisfaction: nextSatisfaction, cost: config.cost };
}
async reconcileMarriage(hashedUserId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
if (!user?.character?.id) throw new Error('User or character not found');
const marriage = await Relationship.findOne({
where: { character1Id: user.character.id },
include: [
{ model: RelationshipType, as: 'relationshipType', where: { tr: 'married' } },
{ model: RelationshipState, as: 'state', required: false }
]
});
if (!marriage) throw { status: 409, message: 'No active marriage found' };
let state = marriage.state;
if (!state) {
state = await RelationshipState.create({
relationshipId: marriage.id,
...this.buildDefaultRelationshipState('married')
});
}
const nextSatisfaction = this.clampScore((state.marriageSatisfaction ?? 55) + 1);
const nextPublicStability = this.clampScore((state.marriagePublicStability ?? 55) + 1);
await state.update({
marriageSatisfaction: nextSatisfaction,
marriagePublicStability: nextPublicStability
});
await notifyUser(hashedUserId, 'falukantUpdateFamily', { reason: 'daily' });
await notifyUser(hashedUserId, 'falukantUpdateStatus', {});
return { success: true, marriageSatisfaction: nextSatisfaction };
}
async acknowledgeLover(hashedUserId, relationshipId) { async acknowledgeLover(hashedUserId, relationshipId) {
const parsedRelationshipId = Number.parseInt(relationshipId, 10); const parsedRelationshipId = Number.parseInt(relationshipId, 10);
if (Number.isNaN(parsedRelationshipId)) { if (Number.isNaN(parsedRelationshipId)) {
@@ -3245,6 +3545,7 @@ class FalukantService extends BaseService {
updateData.loverRole = 'lover'; updateData.loverRole = 'lover';
} }
await state.update(updateData); await state.update(updateData);
await this.refreshHouseholdTensionState(user, user.character);
return { return {
success: true, success: true,
relationshipId: parsedRelationshipId, relationshipId: parsedRelationshipId,
@@ -3264,6 +3565,7 @@ class FalukantService extends BaseService {
active: false, active: false,
acknowledged: false acknowledged: false
}); });
await this.refreshHouseholdTensionState(user, user.character);
return { return {
success: true, success: true,
relationshipId: parsedRelationshipId, relationshipId: parsedRelationshipId,
@@ -4031,6 +4333,8 @@ class FalukantService extends BaseService {
'servantQuality', 'servantQuality',
'servantPayLevel', 'servantPayLevel',
'householdOrder', 'householdOrder',
'householdTensionScore',
'householdTensionReasonsJson',
'houseTypeId' 'houseTypeId'
] ]
}); });
@@ -4046,6 +4350,8 @@ class FalukantService extends BaseService {
servantQuality: 50, servantQuality: 50,
servantPayLevel: 'normal', servantPayLevel: 'normal',
householdOrder: 55, householdOrder: 55,
householdTensionScore: 10,
householdTensionReasonsJson: [],
servantSummary: this.buildServantSummary(null, falukantUser.character) servantSummary: this.buildServantSummary(null, falukantUser.character)
}; };
} }
@@ -4116,7 +4422,9 @@ class FalukantService extends BaseService {
servantCount: servantDefaults.servantCount, servantCount: servantDefaults.servantCount,
servantQuality: servantDefaults.servantQuality, servantQuality: servantDefaults.servantQuality,
servantPayLevel: servantDefaults.servantPayLevel, servantPayLevel: servantDefaults.servantPayLevel,
householdOrder: servantDefaults.householdOrder householdOrder: servantDefaults.householdOrder,
householdTensionScore: 10,
householdTensionReasonsJson: []
}); });
await house.destroy(); await house.destroy();
await updateFalukantUserMoney(falukantUser.id, -housePrice, "housebuy", falukantUser.id); await updateFalukantUserMoney(falukantUser.id, -housePrice, "housebuy", falukantUser.id);
@@ -4287,6 +4595,7 @@ class FalukantService extends BaseService {
}); });
await house.save(); await house.save();
await updateFalukantUserMoney(falukantUser.id, -hireCost, 'servants_hired', falukantUser.id); await updateFalukantUserMoney(falukantUser.id, -hireCost, 'servants_hired', falukantUser.id);
await this.refreshHouseholdTensionState(falukantUser, falukantUser.character);
const user = await User.findByPk(falukantUser.userId); const user = await User.findByPk(falukantUser.userId);
notifyUser(user.hashedId, 'falukantHouseUpdate', {}); notifyUser(user.hashedId, 'falukantHouseUpdate', {});
@@ -4321,6 +4630,7 @@ class FalukantService extends BaseService {
character: falukantUser.character character: falukantUser.character
}); });
await house.save(); await house.save();
await this.refreshHouseholdTensionState(falukantUser, falukantUser.character);
const user = await User.findByPk(falukantUser.userId); const user = await User.findByPk(falukantUser.userId);
notifyUser(user.hashedId, 'falukantHouseUpdate', {}); notifyUser(user.hashedId, 'falukantHouseUpdate', {});
@@ -4350,6 +4660,7 @@ class FalukantService extends BaseService {
character: falukantUser.character character: falukantUser.character
}); });
await house.save(); await house.save();
await this.refreshHouseholdTensionState(falukantUser, falukantUser.character);
const user = await User.findByPk(falukantUser.userId); const user = await User.findByPk(falukantUser.userId);
notifyUser(user.hashedId, 'falukantHouseUpdate', {}); notifyUser(user.hashedId, 'falukantHouseUpdate', {});
@@ -4360,6 +4671,30 @@ class FalukantService extends BaseService {
}; };
} }
async tidyHousehold(hashedUserId) {
const { falukantUser, house } = await this.getOwnedUserHouse(hashedUserId);
const tidyCost = 15;
if (Number(falukantUser.money) < tidyCost) {
throw new Error('notenoughmoney.');
}
house.householdOrder = this.clampScore(Number(house.householdOrder || 55) + 3);
await house.save();
await updateFalukantUserMoney(falukantUser.id, -tidyCost, 'household_order', falukantUser.id);
await this.refreshHouseholdTensionState(falukantUser, falukantUser.character);
const user = await User.findByPk(falukantUser.userId);
notifyUser(user.hashedId, 'falukantHouseUpdate', {});
notifyUser(user.hashedId, 'falukantUpdateFamily', { reason: 'daily' });
notifyUser(user.hashedId, 'falukantUpdateStatus', {});
return {
success: true,
cost: tidyCost,
householdOrder: house.householdOrder,
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({

View File

@@ -0,0 +1,5 @@
ALTER TABLE falukant_data.user_house
ADD COLUMN IF NOT EXISTS household_tension_score integer NOT NULL DEFAULT 10;
ALTER TABLE falukant_data.user_house
ADD COLUMN IF NOT EXISTS household_tension_reasons_json jsonb NULL;

View File

@@ -0,0 +1,569 @@
# Falukant: Steuerung von Ehezustand und Hausfrieden
Dieses Dokument beschreibt:
- wie Spieler `Ehezustand` und `Hausfrieden` direkt beeinflussen können
- welche Werte dafür im Backend sichtbar und änderbar sein müssen
- was der externe Daemon täglich und monatlich berechnen soll
Die Datei ist bewusst als gemeinsame Arbeitsgrundlage für UI, Backend und externen Daemon formuliert.
## 1. Zielbild
Es soll zwei getrennte, aber gekoppelte Systeme geben:
- `marriageSatisfaction`
- numerisch `0..100`
- individueller Kernwert der Ehe
- `householdTension`
- aggregierter Haushaltszustand
- nach außen in UI als `low | medium | high`
- intern sinnvollerweise als numerischer Spannungswert `0..100`
Interpretation:
- `marriageSatisfaction` beschreibt die Qualität der Paarbeziehung
- `householdTension` beschreibt Spannungen im gesamten Haus
- Liebschaften
- Unterversorgung
- Ordnung
- Kinderkonflikte
- Dienerschaft
## 2. Werte und Ableitungen
## 2.1 Ehe
Bestehend:
- `relationship_state.marriage_satisfaction`
Neu sinnvoll:
- `relationship_state.marriage_public_stability`
- besteht bereits
- soll aktiv genutzt werden
- optional später:
- `last_affection_action_at`
- `last_conflict_action_at`
- `last_shared_time_at`
UI-Ableitung:
- `0..19` => `broken`
- `20..39` => `fragile`
- `40..59` => `strained`
- `60..79` => `stable`
- `80..100` => `harmonious`
## 2.2 Hausfrieden
Der bisherige reine UI-Helfer
- `low`
- `medium`
- `high`
reicht für eine echte Steuerung nicht aus.
Neu sinnvoll:
- interner Wert `householdTensionScore`
- Bereich `0..100`
- `0` = sehr ruhig
- `100` = offener Hauskonflikt
UI-Ableitung:
- `0..24` => `low`
- `25..59` => `medium`
- `60..100` => `high`
Falls kein eigener Persistenzwert angelegt werden soll, darf der Daemon den Score auch nur berechnen und als API-Feld zurückgeben.
## 3. Direkte Spieleraktionen
Es braucht direkte Spielzüge, die der Spieler bewusst auslösen kann.
Wichtig:
- nicht jede Aktion muss sofort große Werte ändern
- direkte Aktionen sollen kleine, klare Effekte haben
- der Daemon übernimmt Drift, Gegenkräfte und Folgewirkungen
## 3.1 Ehe-Aktionen
Diese Aktionen gehören fachlich in `FamilyView`.
### A. Zeit mit Ehepartner verbringen
Zweck:
- Standardaktion zur Pflege der Beziehung
Regel:
- verfügbar nur bei aktiver Ehe
- Cooldown: `1x pro Tag`
- Kosten: `0` oder sehr klein
Soforteffekt:
- `marriageSatisfaction +2`
- `householdTensionScore -1`
Modifikatoren:
- wenn aktive sichtbare Liebschaft `visibility >= 45`: nur `+1`
- wenn `householdOrder <= 35`: kein Bonus auf Hausfrieden
- wenn `marriageSatisfaction < 25`: stattdessen nur `+1`
### B. Geschenk an Ehepartner
Zweck:
- Geld gegen schnellere Stabilisierung
Regel:
- verfügbar nur bei aktiver Ehe
- Stufen: `small`, `decent`, `lavish`
- Cooldown: `1x pro 3 Tage`
Soforteffekt:
- `small`: `marriageSatisfaction +2`
- `decent`: `marriageSatisfaction +4`
- `lavish`: `marriageSatisfaction +7`
Nebeneffekt:
- `marriagePublicStability +1/+2/+3`
Malus:
- bei gleichzeitig unterfinanzierter Liebschaft halbierter Effekt
### C. Streit schlichten
Zweck:
- gezielte Krisenintervention
Regel:
- verfügbar nur wenn `householdTensionScore >= 35` oder `marriageSatisfaction <= 50`
- Cooldown: `1x pro 2 Tage`
Soforteffekt:
- `householdTensionScore -4`
- `marriageSatisfaction +1`
Malus:
- wenn `visibility` einer aktiven Liebschaft `>= 60`, dann nur `householdTensionScore -2`
### D. Fest nur für den Haushalt
Zweck:
- Hausfrieden über Geld und Repräsentation stützen
Regel:
- verfügbar bei vorhandenem Haus
- kleiner interner Hausakt, nicht großes Reputationsfest
- Cooldown: `1x pro Monat`
Soforteffekt:
- `householdTensionScore -6`
- `marriageSatisfaction +2`
- `householdOrder +2`
Malus:
- bei unterbesetzter Dienerschaft nur halbe Wirkung
## 3.2 Haus-Aktionen
Diese Aktionen gehören fachlich in `HouseView`.
### A. Haus ordnen
Zweck:
- kleine direkte Ordnungsmaßnahme
Regel:
- Cooldown: `1x pro Tag`
- Kosten: niedrig
Soforteffekt:
- `householdOrder +3`
- wenn `householdOrder > 70`: stattdessen nur `+1`
Indirekter Effekt:
- besserer Daily-Wert für `householdTensionScore`
### B. Diener einstellen
Bereits vorhanden.
Neue fachliche Wirkung:
- wenn vorher `servantCount < expectedMin`
- sofort `householdTensionScore -2`
### C. Diener entlassen
Bereits vorhanden.
Neue fachliche Wirkung:
- wenn danach `servantCount < expectedMin`
- sofort `householdTensionScore +3`
### D. Bezahlung erhöhen
Bereits vorhanden.
Neue fachliche Wirkung:
- wenn von `low -> normal` oder `normal -> high`
- sofort `householdOrder +2`
- `householdTensionScore -1`
## 3.3 Familien-/Kinder-Aktionen
### A. Uneheliches Kind anerkennen
Zweck:
- offenere, geordnetere Lösung statt versteckter Konfliktlage
Soforteffekt:
- `publicKnown = true`
- `legitimacy = acknowledged_bastard`
- `householdTensionScore -2`, wenn Beziehung bereits öffentlich geordnet
- `householdTensionScore +3`, wenn Ehe schwach und Beziehung skandalös
Eheeffekt:
- `marriageSatisfaction -2` bis `-6` je nach Sichtbarkeit und Stand
### B. Erbenfrage regeln
Wenn uneheliche Kinder sichtbar werden, kann die UI später eine Handlung
`Erbfolge klären` bekommen.
Erste Version:
- nur vorgemerkt
- noch keine direkte Aktion nötig
## 3.4 Liebschafts-Aktionen mit Einfluss auf Ehe und Haus
Bestehend:
- Unterhalt ändern
- Beziehung anerkennen
- Beziehung beenden
Diese Aktionen sollen explizit folgende Sofortwirkung haben:
### Unterhalt erhöhen
- `monthsUnderfunded` baut sich später im Daemon ab
- sofort kein großer Ehebonus
- aber `householdTensionScore -1`, wenn vorher Unterversorgung bestand
### Beziehung anerkennen
- `visibility` steigt nicht automatisch hart, aber öffentlicher Charakter nimmt zu
- bei hohen Ständen geordnet eher neutral bis leicht positiv für Ehe-Stabilität
- bei niedrigen Ständen eher negativ
Sofortregel:
- Standesgruppe `0-1`: `marriageSatisfaction -3`, `householdTensionScore +2`
- Standesgruppe `2`: `marriageSatisfaction -1`, `householdTensionScore +1`
- Standesgruppe `3`: `marriagePublicStability +1`, `householdTensionScore -1`, wenn Diskretion und Versorgung gut sind
### Beziehung beenden
- sofort `householdTensionScore -3`, wenn Liebschaft riskant war
- sofort `marriageSatisfaction +1`, wenn aktive Ehe existiert
- aber bei hoher `affection >= 70` auch möglicher Malus auf Stimmungssystem später
## 4. Daemon-Berechnung
## 4.1 Daily-Input
Der externe Daemon braucht pro Spielerfigur:
- aktive Ehebeziehung mit `marriageSatisfaction`, `marriagePublicStability`
- aktive Liebschaften mit:
- `loverRole`
- `visibility`
- `discretion`
- `maintenanceLevel`
- `statusFit`
- `monthsUnderfunded`
- `acknowledged`
- Kinderdaten:
- `legitimacy`
- `birthContext`
- `publicKnown`
- Hausdaten:
- `servantCount`
- `servantQuality`
- `servantPayLevel`
- `householdOrder`
- Charakterdaten:
- `titleOfNobility`
- `reputation`
## 4.2 Daily-Berechnung für Ehe
Grunddrift:
```text
marriageDelta = 0
if marriageSatisfaction > 55: marriageDelta -= 1 every 3 days
if marriageSatisfaction < 55: marriageDelta += 1 every 5 days
```
Liebschaften:
```text
for each active lover:
if visibility >= 60: marriageDelta -= 2
else if visibility >= 35: marriageDelta -= 1
if monthsUnderfunded >= 2: marriageDelta -= 1
if acknowledged = true and statusGroup <= 1: marriageDelta -= 1
if acknowledged = true and statusGroup = 3 and visibility <= 35 and maintenanceLevel >= 60:
marriageDelta += 0 or +1 every few days
```
Zu jung:
```text
if minAge <= 15: marriageDelta -= 1
if minAge <= 13: marriageDelta -= 2
```
Haus:
```text
if householdOrder >= 75: marriageDelta += 1
if householdOrder <= 35: marriageDelta -= 1
if householdTensionScore >= 60: marriageDelta -= 1
```
Dienerschaft:
```text
if servantCount < expectedMin: marriageDelta -= 1
if servantPayLevel = high and servantQuality >= 70 and householdOrder >= 70:
marriageDelta += 1 every 3 days
```
Danach:
- clamp `0..100`
## 4.3 Daily-Berechnung für Hausfrieden
Interner Wert:
```text
householdTensionScore = base
```
Empfohlene Berechnung:
```text
base = 10
for each active lover:
if visibility >= 60: base += 18
else if visibility >= 35: base += 10
else: base += 4
if monthsUnderfunded >= 1: base += 6
if monthsUnderfunded >= 2: base += 6
if acknowledged = true: base += 4
if statusFit = -1: base += 3
if statusFit = -2: base += 6
```
Kinder:
```text
for each child where birthContext = 'lover':
if publicKnown = true: base += 6
else: base += 2
if legitimacy = 'acknowledged_bastard': base += 2
if legitimacy = 'hidden_bastard': base += 4
```
Haus:
```text
if householdOrder >= 80: base -= 6
else if householdOrder >= 65: base -= 3
if householdOrder <= 35: base += 8
else if householdOrder <= 50: base += 4
```
Dienerschaft:
```text
if servantCount < expectedMin: base += 5
if servantPayLevel = low: base += 2
if servantQuality >= 70 and servantPayLevel = high: base -= 3
```
Ehe:
```text
if marriageSatisfaction <= 35: base += 6
if marriageSatisfaction >= 75: base -= 2
```
Danach:
- clamp `0..100`
- UI-Ableitung auf `low/medium/high`
## 4.4 Monthly-Berechnung
Monatlich soll der Daemon zusätzlich:
- Dienerkosten abbuchen
- Liebschaftskosten abbuchen
- bei Unterversorgung `householdTensionScore` stärker erhöhen
- langfristige Ordnungs- oder Eheboni addieren
Empfohlene Zusatzregeln:
```text
if a lover was underfunded this month:
householdTensionScore += 4
if servantCount far below expectedMin for full month:
householdTensionScore += 3
if householdOrder >= 80 for full month:
marriageSatisfaction += 1
if householdOrder <= 30 for full month:
marriageSatisfaction -= 2
```
## 5. UI-Anforderungen
## 5.1 FamilyView
Neu sinnvolle Aktionen:
- `Zeit miteinander verbringen`
- `Geschenk machen`
- `Streit schlichten`
- `Liebschaft beenden`
- `Uneheliches Kind anerkennen`
Zusätzlich hilfreiche Anzeige:
- kurze Ursachenliste für `Hausfrieden`
- z. B. `sichtbare Liebschaft`
- `Unruhe im Haus`
- `zu wenig Diener`
- `anerkanntes uneheliches Kind`
## 5.2 HouseView
Neu sinnvolle Aktionen:
- `Haus ordnen`
- vorhandene Dieneraktionen mit klarer Auswirkungstextzeile
Anzeige:
- `Haushaltsordnung`
- `erwartete Dienerzahl`
- `Auswirkung auf Hausfrieden`
## 6. Backend-Anforderungen
## 6.1 Direktaktionen
Dieses Projekt sollte Endpunkte für direkte Einflussaktionen bereitstellen:
- `POST /api/falukant/family/marriage/spend-time`
- `POST /api/falukant/family/marriage/gift`
- `POST /api/falukant/family/marriage/reconcile`
- `POST /api/falukant/houses/order`
- später optional:
- `POST /api/falukant/family/children/acknowledge`
## 6.2 API-Rückgabe
Family-API sollte zusätzlich liefern:
- `marriageSatisfaction`
- `marriageState`
- `marriagePublicStability`
- `householdTension`
- `householdTensionScore`
- optional:
- `householdTensionReasons[]`
House-API sollte zusätzlich liefern:
- `householdOrder`
- `expectedServantsMin`
- `expectedServantsMax`
- `marriageComfortModifier`
## 7. Priorisierte Umsetzung
## Phase A
- `statusFit`-Fehler korrigieren
- direkte Ehe-Aktionen `Zeit`, `Geschenk`, `Streit schlichten`
- direkte Haus-Aktion `Haus ordnen`
- Family-API um `householdTensionScore` erweitern
## Phase B
- externer Daemon berechnet Daily-Drift für Ehe und Hausfrieden
- Dienerschaft fließt in Hausfrieden ein
- Liebschaften und Unterversorgung wirken vollständig auf Hausfrieden
## Phase C
- uneheliche Kinder als aktiver Konfliktfaktor
- Anerkennungsaktion
- genauere Ursachenlisten in der UI
## 8. Offene Balancing-Punkte
Diese Werte sind absichtlich noch nicht final:
- exakte Geldkosten für Ehe-Aktionen
- Stärke der Boni für hohe Stände
- Stärke des Malus bei sichtbaren Liebschaften
- Stärke der Dienerwirkung auf Ehe und Haus
Die Struktur sollte jetzt aber stabil genug sein, damit UI und Daemon unabhängig voneinander anfangen können.

View File

@@ -0,0 +1,137 @@
# Falukant: Daemon-Handoff für Ehe und Hausfrieden
Dieses Dokument beschreibt den Stand nach Phase A.
## 1. Was im Projekt jetzt vorhanden ist
Backend-/API-seitig vorhanden:
- `relationship_state.marriage_satisfaction`
- `relationship_state.marriage_public_stability`
- aktive Liebschaften mit:
- `visibility`
- `discretion`
- `maintenance_level`
- `status_fit`
- `months_underfunded`
- `acknowledged`
- `user_house` mit:
- `servant_count`
- `servant_quality`
- `servant_pay_level`
- `household_order`
- `household_tension_score`
- `household_tension_reasons_json`
- Family-API liefert jetzt zusätzlich:
- `householdTension`
- `householdTensionScore`
- `householdTensionReasons`
Direkte Spieleraktionen vorhanden:
- `POST /api/falukant/family/marriage/spend-time`
- `POST /api/falukant/family/marriage/gift`
- `POST /api/falukant/family/marriage/reconcile`
- `POST /api/falukant/houses/order`
## 2. Daily-Input für den externen Daemon
Pro betroffenem Falukant-User:
- `falukant_user.id`
- `user.id` / `user.hashed_id`
- aktive Ehe-`relationship` mit `relationship_state`
- aktive Liebschaften mit `relationship_state`
- Kinder mit:
- `birth_context`
- `legitimacy`
- `public_known`
- Haus mit:
- `servant_count`
- `servant_quality`
- `servant_pay_level`
- `household_order`
- Charakter mit:
- `reputation`
- `title_of_nobility`
## 3. Was der Daemon täglich berechnen soll
### Ehe
- Drift von `marriage_satisfaction`
- Drift von `marriage_public_stability`
- Einfluss aus:
- sichtbaren Liebschaften
- unterfinanzierten Liebschaften
- Standesunterschieden
- Dienerschaft / Haushaltsordnung
- zu jungen Liebschaften
### Hausfrieden
Der Daemon soll intern einen numerischen Spannungswert pflegen oder berechnen:
- `householdTensionScore` `0..100`
Einflussfaktoren:
- sichtbare Liebschaften
- anerkannte Liebschaften
- unterfinanzierte Liebschaften
- Kinder aus Liebschaften
- Haushaltsordnung
- Dienerschaft
- schwache Ehe
UI-Ableitung:
- `0..24` => `low`
- `25..59` => `medium`
- `60..100` => `high`
## 4. Was der Daemon zurückschreiben soll
Pflicht:
- `relationship_state.marriage_satisfaction`
- `relationship_state.marriage_public_stability`
- `user_house.household_tension_score`
- `user_house.household_tension_reasons_json`
- lover-state-Felder bei Änderungen:
- `visibility`
- `discretion`
- `months_underfunded`
- optional `notes_json` / `flags_json`
## 5. Socket-/Refresh-Verhalten
Wenn Daily-/Monthly-Verarbeitung Ehe oder Hausfrieden betrifft:
- `falukantUpdateFamily` mit `reason: "daily"` oder `reason: "monthly"`
- danach `falukantUpdateStatus`
Wenn ein Sonderereignis entsteht:
- `reason: "scandal"` zusätzlich
## 6. Wichtige Phase-A-Regel
Die neuen Direktaktionen geben nur Sofortimpulse:
- `spend-time`
- `gift`
- `reconcile`
- `house/order`
Der Daemon ist weiterhin verantwortlich für:
- Rückdrift
- Gegenkräfte
- Langzeiteffekte
- Balancing
Kurz:
- UI/Backend setzen kleine direkte Impulse
- der Daemon bestimmt die dauerhafte Entwicklung

View File

@@ -0,0 +1,472 @@
# Falukant: Produktionszertifikate Fach- und Integrationsspezifikation
## 1. Ziel
Das Produktionssystem soll stärker an den tatsächlichen gesellschaftlichen und fachlichen Fortschritt eines Spielers gebunden werden. Ein Spieler darf nur Produkte herstellen, deren Zertifikatsstufe seiner aktuellen Produktionsfreigabe entspricht.
Die Zertifikatsstufe steigt nicht sofort bei jeder Einzelaktion, sondern wird ausschließlich im externen Daemon einmal täglich neu berechnet.
Dieses Dokument beschreibt:
- das fachliche Modell der Produktionszertifikate
- die Faktoren für Aufstieg und Begrenzung
- die Daily-Berechnung im Daemon
- die Kommunikation zwischen Daemon und UI
- die Einbindung in die bestehende Backend-/UI-Struktur
Wichtig:
- Die nötigen DB-Grundlagen sind bereits vorhanden.
- Der Daemon muss keine neuen Schemaänderungen erwarten.
- Bestehende Felder wie `falukant_data.falukant_user.certificate` und `falukant_type.product.category` bleiben die führende Basis.
## 2. Bestehende technische Basis
Bereits vorhanden:
- `falukant_data.falukant_user.certificate`
- aktuelle Produktionsfreigabe des Spielers
- `falukant_type.product.category`
- erforderliche Zertifikatsstufe des Produkts
- `falukant_data.knowledge`
- Produktwissen je Charakter und Produkt
- `falukant_data.production`
- Produktionsvorgänge
- `falukant_data.character.reputation`
- Ansehen des Spielercharakters
- `falukant_data.character.title_of_nobility`
- Adelstitel
- `falukant_data.user_house.house_type_id`
- aktuelles Haus
- politische und kirchliche Ämter
- `falukant_data.political_office`
- `falukant_data.church_office`
- `falukant_log.political_office_history`
- `falukant_type.church_office_type.hierarchy_level`
Bestehende Produktfreigabe im Backend:
- Produkte werden bereits in `falukantService.getProducts()` über `product.category <= user.certificate` gefiltert.
Das heißt:
- Zertifikatslogik muss nicht neu in die Produktionsfreigabe eingebaut werden.
- Es muss nur die Berechnung und Fortschreibung von `falukant_user.certificate` sauber geregelt werden.
## 3. Fachmodell
### 3.1 Zertifikatsstufen
Die Zertifikatsstufe bleibt eine einfache ganze Zahl im Feld `falukant_user.certificate`.
Empfohlene Bedeutung:
| Stufe | Bedeutung |
|------|-----------|
| `1` | Grundproduktion, einfache Güter |
| `2` | Gehobene Alltagsproduktion |
| `3` | Fortgeschrittene Manufaktur |
| `4` | Anspruchsvolle Qualitätsproduktion |
| `5` | Hochwertige oder prestigegebundene Produktion |
Wenn im Typensystem bereits höhere `product.category`-Werte existieren, gilt dieselbe Logik entsprechend weiter.
### 3.2 Führungsprinzip
Die Zertifikatsstufe ist kein reines Wissenslevel.
Sie soll ausdrücken, ob ein Haushalt/Betrieb gesellschaftlich und fachlich als ausreichend etabliert gilt, um komplexere Produktion zu verantworten.
Darum fließen mehrere Faktoren ein:
- Durchschnittliches Produktwissen
- Anzahl abgeschlossener Produktionen
- höchstes politisches oder kirchliches Amt
- Adelstitel
- Ansehen
- derzeitiges Haus
## 4. Berechnungslogik
## 4.1 Grundidee
Der Daemon berechnet einmal täglich einen `certificateScore`.
Aus diesem `certificateScore` wird eine Zielstufe `targetCertificate` abgeleitet.
Die gespeicherte Stufe `falukant_user.certificate` wird dann höchstens um `+1` pro Tag angehoben. Senkungen sind optional und in der ersten Version nicht vorgesehen.
Dadurch gilt:
- Aufstieg ist spürbar, aber nicht sprunghaft
- kurzfristige Schwankungen führen nicht zu hektischen Freischaltungen
- Balancing bleibt später leichter
## 4.2 Eingangsgrößen
Für jeden Spielercharakter mit `falukant_user`:
- `avgKnowledge`
- Durchschnitt aus `falukant_data.knowledge.knowledge` des Spielercharakters
- `completedProductions`
- Anzahl abgeschlossener Produktionen des Spielers
- `highestPoliticalOfficeRank`
- höchster politischer Amtsrang
- `highestChurchOfficeRank`
- höchster kirchlicher Amtsrang
- `highestOfficeRank`
- Maximum aus politischem und kirchlichem Rang
- `nobilityLevel`
- aus `title_of_nobility`
- `reputation`
- aus `character.reputation`
- `housePosition`
- aus `house.position`
## 4.3 Normalisierung der Faktoren
### Produktwissen
`knowledgePoints`:
- `0`, wenn `avgKnowledge < 20`
- `1`, wenn `avgKnowledge >= 20`
- `2`, wenn `avgKnowledge >= 35`
- `3`, wenn `avgKnowledge >= 50`
- `4`, wenn `avgKnowledge >= 65`
- `5`, wenn `avgKnowledge >= 80`
### Produktionsmenge
`productionPoints`:
- `0`, wenn `completedProductions < 5`
- `1`, wenn `completedProductions >= 5`
- `2`, wenn `completedProductions >= 20`
- `3`, wenn `completedProductions >= 50`
- `4`, wenn `completedProductions >= 100`
- `5`, wenn `completedProductions >= 200`
### Politische / kirchliche Stellung
`officePoints`:
- politische Ämter:
- über definierte Mapping-Tabelle im Daemon von `office_type.name -> rank`
- kirchliche Ämter:
- bevorzugt `church_office_type.hierarchy_level`
- dann:
- `highestOfficeRank = max(highestPoliticalOfficeRank, highestChurchOfficeRank)`
- `officePoints = min(5, highestOfficeRank)`
Empfehlung für politische Mapping-Tabelle:
- einfache Kommunalämter: `1`
- regionale Ämter: `2`
- hohe Regionalämter: `3`
- reichs- oder königsnahe Spitzenämter: `4` bis `5`
Das Mapping lebt im Daemon und kann balanciert werden, ohne DB-Änderungen.
### Adel
`nobilityPoints`:
- aus `title_of_nobility.level`
- `nobilityPoints = clamp(level - 1, 0, 5)`
### Ansehen
`reputationPoints`:
- `0`, wenn `reputation < 20`
- `1`, wenn `reputation >= 20`
- `2`, wenn `reputation >= 40`
- `3`, wenn `reputation >= 60`
- `4`, wenn `reputation >= 75`
- `5`, wenn `reputation >= 90`
### Haus
`housePoints`:
- aus `house.position`
- Vorschlag:
- `0`, wenn `position <= 1`
- `1`, wenn `position >= 2`
- `2`, wenn `position >= 4`
- `3`, wenn `position >= 6`
- `4`, wenn `position >= 8`
- `5`, wenn `position >= 10`
Die genauen Schwellen können im Balancing später angepasst werden.
## 4.4 Gesamtwert
Der Daemon berechnet:
```text
certificateScore =
knowledgePoints * 0.35 +
productionPoints * 0.20 +
officePoints * 0.15 +
nobilityPoints * 0.10 +
reputationPoints * 0.10 +
housePoints * 0.10
```
Zusätzlich gelten Mindestanforderungen je Stufe.
## 4.5 Mindestanforderungen je Zertifikatsstufe
Eine höhere Zielstufe darf nur erreicht werden, wenn neben dem `certificateScore` auch harte Mindestgrenzen erfüllt sind.
### Für Zertifikat 2
- `avgKnowledge >= 25`
- `completedProductions >= 5`
### Für Zertifikat 3
- `avgKnowledge >= 40`
- `completedProductions >= 20`
- mindestens einer der Statusfaktoren erfüllt:
- `officePoints >= 1`
- oder `nobilityPoints >= 1`
- oder `reputationPoints >= 2`
- oder `housePoints >= 1`
### Für Zertifikat 4
- `avgKnowledge >= 55`
- `completedProductions >= 60`
- mindestens zwei Statusfaktoren erfüllt
### Für Zertifikat 5
- `avgKnowledge >= 70`
- `completedProductions >= 150`
- `reputationPoints >= 3`
- mindestens zwei der folgenden:
- `officePoints >= 2`
- `nobilityPoints >= 2`
- `housePoints >= 2`
## 4.6 Ableitung der Zielstufe
Vorschlag:
- `targetCertificate = 1`, wenn `certificateScore < 1.2`
- `targetCertificate = 2`, wenn `certificateScore >= 1.2`
- `targetCertificate = 3`, wenn `certificateScore >= 2.1`
- `targetCertificate = 4`, wenn `certificateScore >= 3.0`
- `targetCertificate = 5`, wenn `certificateScore >= 4.0`
Danach werden die Mindestanforderungen geprüft.
Wenn eine Schwelle rechnerisch erreicht ist, die Mindestanforderungen aber fehlen, bleibt der Spieler auf der niedrigeren Zielstufe.
## 4.7 Fortschreibung
Daily-Regel:
- wenn `targetCertificate > currentCertificate`
- dann `newCertificate = currentCertificate + 1`
- sonst
- `newCertificate = currentCertificate`
Für die erste Version keine automatische Herabstufung.
Ausnahmen, die bereits im Daemon berücksichtigt werden dürfen:
- `Bankrott`
- Wenn der Spieler wirtschaftlich zusammenbricht, darf das Zertifikat gesenkt werden.
- Die genaue Definition von Bankrott lebt im Daemon bzw. im Wirtschaftssystem.
- `Tod ohne Kinder`
- Stirbt der Spielercharakter und es gibt keinen erbberechtigten Nachfolger, darf das Zertifikat auf den Grundzustand des Nachfolgesystems bzw. auf eine definierte niedrige Stufe zurückgesetzt werden.
- Dieser Fall darf bereits jetzt im Daemon umgesetzt werden.
Nicht vorgesehen für die erste Version:
- Downgrade wegen normaler Alltagsschwankungen
- Downgrade wegen vorübergehend schlechter Werte bei Wissen, Ruf, Haus oder Amt
## 5. Daemon-Verhalten
## 5.1 Ausführungszeitpunkt
Die Zertifikatsprüfung läuft ausschließlich im Daily-Tick.
Nicht bei:
- Produktionsstart
- Produktionsende
- Wissensänderung
- Hauswechsel
- Amtswechsel
Diese Aktionen verändern nur die Eingangsgrößen. Die eigentliche Zertifikatsanpassung erfolgt erst im nächsten Daily-Lauf.
## 5.2 Daemon-Hinweis
Für den Daemon gilt ausdrücklich:
- die relevanten DB-Felder sind bereits vorhanden
- es müssen für diese Funktion keine zusätzlichen Schemaänderungen mehr eingeplant werden
- der Daemon soll direkt mit den vorhandenen Tabellen arbeiten
## 5.3 Empfohlener Daily-Ablauf
Für jeden Spieler mit `falukant_user`:
1. Spielercharakter laden
2. `avgKnowledge` berechnen
3. Anzahl abgeschlossener Produktionen laden
4. höchstes aktives oder historisches politisches Amt ermitteln
5. höchstes aktives kirchliches Amt ermitteln
6. Adelstitel, Ruf und Haus laden
7. `certificateScore` und `targetCertificate` berechnen
8. falls Aufstieg möglich:
- `falukant_user.certificate` um genau `+1` erhöhen
9. Event an UI senden
## 6. Event-Kommunikation zwischen Daemon und UI
## 6.1 Neues Event
Zusätzlich zum allgemeinen `falukantUpdateStatus` wird ein gezieltes Zertifikats-Event empfohlen:
### `falukantUpdateProductionCertificate`
Payload:
```json
{
"event": "falukantUpdateProductionCertificate",
"user_id": 123,
"reason": "daily_recalculation",
"old_certificate": 2,
"new_certificate": 3
}
```
`reason` ist in der ersten Version fest:
- `daily_recalculation`
## 6.2 Wann senden
Wenn sich die Zertifikatsstufe geändert hat:
1. `falukantUpdateProductionCertificate`
2. danach `falukantUpdateStatus`
Wenn sich die Stufe nicht geändert hat:
- kein spezielles Zertifikats-Event nötig
- normales `falukantUpdateStatus` bleibt anderen Systemen vorbehalten
## 6.3 UI-Reaktion
### BranchView
Bei `falukantUpdateProductionCertificate`:
- Produkte neu laden
- Produktionsbereich neu laden
- optional kurzer Hinweis:
- „Neues Handelszertifikat erreicht“
### OverviewView
Bei `falukantUpdateProductionCertificate`:
- Falukant-Status neu laden
- Produktionsüberblick neu laden
- Zertifikatsaufstieg visuell hervorheben
### StatusBar / DashboardWidget
- auf denselben Nutzer filtern
- Zertifikat/Produktionsstatus neu laden
## 6.4 Deduplizierung
Da direkt nach dem Zertifikats-Event oft `falukantUpdateStatus` folgt, soll die UI wie bei anderen Falukant-Events entprellen:
- beide Events dürfen denselben kurzen Refresh-Puffer nutzen
- ein Zertifikatsaufstieg darf keinen doppelten Reload-Sturm auslösen
## 7. API- und UI-Empfehlungen
## 7.1 Sichtbare Anzeige
Die UI sollte mittelfristig anzeigen:
- aktuelle Zertifikatsstufe
- nächste Stufe
- Fortschrittsfaktoren
- Wissen
- Produktionen
- Amt
- Adel
- Ruf
- Haus
## 7.2 Empfohlene Backend-Ausgabe
Zusätzlich zur bestehenden User-/Overview-API ist später sinnvoll:
- `certificate`
- `nextCertificate`
- `certificateFactors`
- `avgKnowledge`
- `completedProductions`
- `highestOfficeRank`
- `nobilityLevel`
- `reputation`
- `housePosition`
Das ist für die erste Daemon-Integration aber optional.
## 8. Balancing-Hinweis
Die genannten Schwellen und Gewichte sind bewusst als Spielregelrahmen zu verstehen, nicht als endgültiges Balancing.
Für die erste produktive Version gilt:
- keine zusätzlichen Stufen oder Nebensysteme
- keine normale Herabstufung im Alltagsbetrieb
- Herabstufung nur in Sonderfällen wie `Bankrott` oder `Tod ohne Kinder`
- Daily-Aufstieg maximal `+1`
Balancing erst nach Live-Erfahrung.
## 9. Umsetzungsreihenfolge
### P1
- Daemon: Daily-Berechnung von `certificate`
- Event `falukantUpdateProductionCertificate`
- UI: gezielter Refresh in Branch/Overview
### P2
- UI: Sichtbarer Zertifikatsstatus und Aufstiegshinweis
- Backend/API: optionale Faktor-Ausgabe
### P3
- Balancing
- feinere Sonderfallregeln für `Bankrott`
- feinere politische Mapping-Tabelle
## 10. Done-Kriterien
Fertig ist die erste Version, wenn:
- nur Produkte mit `product.category <= falukant_user.certificate` produzierbar sind
- der Daemon die Zertifikatsprüfung genau einmal täglich ausführt
- der Daemon bei Aufstieg das Zertifikat fortschreibt
- die UI auf das Zertifikats-Event gezielt reagiert
- keine neuen DB-Änderungen für diese Funktion nötig sind

View File

@@ -108,7 +108,7 @@ export default {
}, },
setupSocketListeners() { setupSocketListeners() {
this.teardownSocketListeners(); this.teardownSocketListeners();
const daemonEvents = ['falukantUpdateStatus', 'falukantUpdateFamily', 'children_update', 'stock_change', 'familychanged']; const daemonEvents = ['falukantUpdateStatus', 'falukantUpdateFamily', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'];
if (this.daemonSocket) { if (this.daemonSocket) {
this._daemonMessageHandler = (event) => { this._daemonMessageHandler = (event) => {
if (event.data === 'ping') return; if (event.data === 'ping') return;
@@ -129,11 +129,15 @@ export default {
this._childrenSocketHandler = (data) => { this._childrenSocketHandler = (data) => {
if (this.matchesCurrentUser(data)) this.queueFetchData(); if (this.matchesCurrentUser(data)) this.queueFetchData();
}; };
this._productionCertificateSocketHandler = (data) => {
if (this.matchesCurrentUser(data)) this.queueFetchData();
};
this._branchSocketHandler = () => this.queueFetchData(); this._branchSocketHandler = () => this.queueFetchData();
this.socket.on('falukantUpdateStatus', this._statusSocketHandler); this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
this.socket.on('falukantUpdateFamily', this._familySocketHandler); this.socket.on('falukantUpdateFamily', this._familySocketHandler);
this.socket.on('children_update', this._childrenSocketHandler); this.socket.on('children_update', this._childrenSocketHandler);
this.socket.on('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
this.socket.on('falukantBranchUpdate', this._branchSocketHandler); this.socket.on('falukantBranchUpdate', this._branchSocketHandler);
} }
}, },
@@ -146,6 +150,7 @@ export default {
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler); if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler); if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler); if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
if (this._productionCertificateSocketHandler) this.socket.off('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
if (this._branchSocketHandler) this.socket.off('falukantBranchUpdate', this._branchSocketHandler); if (this._branchSocketHandler) this.socket.off('falukantBranchUpdate', this._branchSocketHandler);
} }
}, },

View File

@@ -177,12 +177,14 @@ export default {
this._statusSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data }); this._statusSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
this._familySocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data }); this._familySocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
this._childrenSocketHandler = (data) => this.handleEvent({ event: 'children_update', ...data }); this._childrenSocketHandler = (data) => this.handleEvent({ event: 'children_update', ...data });
this._productionCertificateSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...data });
this._stockSocketHandler = (data) => this.handleEvent({ event: 'stock_change', ...data }); this._stockSocketHandler = (data) => this.handleEvent({ event: 'stock_change', ...data });
this._familyChangedSocketHandler = (data) => this.handleEvent({ event: 'familychanged', ...data }); this._familyChangedSocketHandler = (data) => this.handleEvent({ event: 'familychanged', ...data });
this.socket.on('falukantUpdateStatus', this._statusSocketHandler); this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
this.socket.on('falukantUpdateFamily', this._familySocketHandler); this.socket.on('falukantUpdateFamily', this._familySocketHandler);
this.socket.on('children_update', this._childrenSocketHandler); this.socket.on('children_update', this._childrenSocketHandler);
this.socket.on('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
this.socket.on('stock_change', this._stockSocketHandler); this.socket.on('stock_change', this._stockSocketHandler);
this.socket.on('familychanged', this._familyChangedSocketHandler); this.socket.on('familychanged', this._familyChangedSocketHandler);
}, },
@@ -191,6 +193,7 @@ export default {
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler); if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler); if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler); if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
if (this._productionCertificateSocketHandler) this.socket.off('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
if (this._stockSocketHandler) this.socket.off('stock_change', this._stockSocketHandler); if (this._stockSocketHandler) this.socket.off('stock_change', this._stockSocketHandler);
if (this._familyChangedSocketHandler) this.socket.off('familychanged', this._familyChangedSocketHandler); if (this._familyChangedSocketHandler) this.socket.off('familychanged', this._familyChangedSocketHandler);
} }
@@ -201,7 +204,7 @@ export default {
this._daemonHandler = (event) => { this._daemonHandler = (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (['falukantUpdateStatus', 'falukantUpdateFamily', 'children_update', 'stock_change', 'familychanged'].includes(data.event)) { if (['falukantUpdateStatus', 'falukantUpdateFamily', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'].includes(data.event)) {
this.handleEvent(data); this.handleEvent(data);
} }
} catch (_) {} } catch (_) {}
@@ -241,6 +244,7 @@ export default {
case 'falukantUpdateStatus': case 'falukantUpdateStatus':
case 'falukantUpdateFamily': case 'falukantUpdateFamily':
case 'children_update': case 'children_update':
case 'falukantUpdateProductionCertificate':
case 'stock_change': case 'stock_change':
case 'familychanged': case 'familychanged':
this.queueStatusRefresh(); this.queueStatusRefresh();

View File

@@ -235,7 +235,7 @@ export default {
}); });
}, },
async fetchImage(image) { async fetchImage(image) {
const userId = localStorage.getItem('userid'); const userId = localStorage.getItem('userid') || sessionStorage.getItem('userid');
try { try {
const response = await apiClient.get(`/api/socialnetwork/image/${image.hash}`, { const response = await apiClient.get(`/api/socialnetwork/image/${image.hash}`, {
headers: { headers: {

View File

@@ -130,7 +130,8 @@
"years": "Jahre", "years": "Jahre",
"days": "Tage", "days": "Tage",
"mainbranch": "Heimatstadt", "mainbranch": "Heimatstadt",
"nobleTitle": "Stand" "nobleTitle": "Stand",
"certificate": "Zertifikat"
}, },
"productions": { "productions": {
"title": "Produktionen" "title": "Produktionen"
@@ -219,6 +220,7 @@
}, },
"branch": { "branch": {
"title": "Filiale", "title": "Filiale",
"currentCertificate": "Derzeitiges Zertifikat",
"tabs": { "tabs": {
"director": "Direktor", "director": "Direktor",
"inventory": "Inventar", "inventory": "Inventar",
@@ -526,9 +528,34 @@
}, },
"householdTension": { "householdTension": {
"label": "Hausfrieden", "label": "Hausfrieden",
"score": "Spannungswert",
"reasonsLabel": "Aktuelle Ursachen",
"low": "Ruhig", "low": "Ruhig",
"medium": "Unruhig", "medium": "Unruhig",
"high": "Belastet" "high": "Belastet",
"reasons": {
"visibleLover": "Sichtbare Liebschaft",
"noticeableLover": "Auffällige Liebschaft",
"underfundedLover": "Unterversorgte Liebschaft",
"acknowledgedAffair": "Anerkannte Liebschaft",
"statusMismatch": "Standesunterschied",
"loverChild": "Kind aus Liebschaft",
"disorder": "Unordnung im Haus",
"tooFewServants": "Zu wenig Diener",
"marriageCrisis": "Ehekrise"
}
},
"marriageActions": {
"title": "Ehe pflegen",
"spendTime": "Zeit miteinander verbringen",
"giftSmall": "Kleines Geschenk",
"giftDecent": "Gutes Geschenk",
"giftLavish": "Großzügiges Geschenk",
"reconcile": "Streit schlichten",
"spendTimeSuccess": "Die gemeinsame Zeit hat die Ehe stabilisiert.",
"giftSuccess": "Das Geschenk hat die Ehe verbessert.",
"reconcileSuccess": "Der Streit wurde fürs Erste geschlichtet.",
"actionError": "Die Aktion konnte nicht ausgeführt werden."
}, },
"relationships": { "relationships": {
"name": "Name" "name": "Name"
@@ -863,12 +890,15 @@
"actions": { "actions": {
"hire": "1 Diener einstellen", "hire": "1 Diener einstellen",
"dismiss": "1 Diener entlassen", "dismiss": "1 Diener entlassen",
"tidy": "Haus ordnen",
"hireSuccess": "Die Dienerschaft wurde erweitert.", "hireSuccess": "Die Dienerschaft wurde erweitert.",
"hireError": "Die Dienerschaft konnte nicht erweitert werden.", "hireError": "Die Dienerschaft konnte nicht erweitert werden.",
"dismissSuccess": "Ein Diener wurde entlassen.", "dismissSuccess": "Ein Diener wurde entlassen.",
"dismissError": "Der Diener konnte nicht entlassen werden.", "dismissError": "Der Diener konnte nicht entlassen werden.",
"payLevelSuccess": "Die Bezahlung der Dienerschaft wurde angepasst.", "payLevelSuccess": "Die Bezahlung der Dienerschaft wurde angepasst.",
"payLevelError": "Die Bezahlung konnte nicht angepasst werden." "payLevelError": "Die Bezahlung konnte nicht angepasst werden.",
"tidySuccess": "Das Haus wurde geordnet.",
"tidyError": "Das Haus konnte nicht geordnet werden."
} }
}, },
"status": { "status": {

View File

@@ -111,7 +111,8 @@
"years": "Years", "years": "Years",
"days": "Days", "days": "Days",
"mainbranch": "Home city", "mainbranch": "Home city",
"nobleTitle": "Title" "nobleTitle": "Title",
"certificate": "Certificate"
} }
}, },
"health": { "health": {
@@ -228,12 +229,15 @@
"actions": { "actions": {
"hire": "Hire 1 servant", "hire": "Hire 1 servant",
"dismiss": "Dismiss 1 servant", "dismiss": "Dismiss 1 servant",
"tidy": "Tidy household",
"hireSuccess": "The household staff has been expanded.", "hireSuccess": "The household staff has been expanded.",
"hireError": "The staff could not be expanded.", "hireError": "The staff could not be expanded.",
"dismissSuccess": "A servant has been dismissed.", "dismissSuccess": "A servant has been dismissed.",
"dismissError": "The servant could not be dismissed.", "dismissError": "The servant could not be dismissed.",
"payLevelSuccess": "Servant pay has been updated.", "payLevelSuccess": "Servant pay has been updated.",
"payLevelError": "Servant pay could not be updated." "payLevelError": "Servant pay could not be updated.",
"tidySuccess": "The household has been put in order.",
"tidyError": "The household could not be put in order."
} }
}, },
"status": { "status": {
@@ -261,6 +265,7 @@
"noProposals": "No director candidates available." "noProposals": "No director candidates available."
}, },
"branch": { "branch": {
"currentCertificate": "Current certificate",
"selection": { "selection": {
"title": "Branch Selection", "title": "Branch Selection",
"selected": "Selected Branch", "selected": "Selected Branch",
@@ -540,9 +545,34 @@
}, },
"householdTension": { "householdTension": {
"label": "Household Tension", "label": "Household Tension",
"score": "Tension score",
"reasonsLabel": "Current causes",
"low": "Calm", "low": "Calm",
"medium": "Uneasy", "medium": "Uneasy",
"high": "Strained" "high": "Strained",
"reasons": {
"visibleLover": "Visible affair",
"noticeableLover": "Noticeable affair",
"underfundedLover": "Underfunded affair",
"acknowledgedAffair": "Acknowledged affair",
"statusMismatch": "Status mismatch",
"loverChild": "Child from an affair",
"disorder": "Disorder in the house",
"tooFewServants": "Too few servants",
"marriageCrisis": "Marriage crisis"
}
},
"marriageActions": {
"title": "Support the marriage",
"spendTime": "Spend time together",
"giftSmall": "Small gift",
"giftDecent": "Decent gift",
"giftLavish": "Lavish gift",
"reconcile": "Reconcile dispute",
"spendTimeSuccess": "The time together has stabilized the marriage.",
"giftSuccess": "The gift has improved the marriage.",
"reconcileSuccess": "The dispute has been eased for now.",
"actionError": "The action could not be completed."
}, },
"lovers": { "lovers": {
"title": "Lovers and Mistresses", "title": "Lovers and Mistresses",

View File

@@ -120,7 +120,8 @@
"age": "Edad", "age": "Edad",
"years": "años", "years": "años",
"mainbranch": "Ciudad natal", "mainbranch": "Ciudad natal",
"nobleTitle": "Rango" "nobleTitle": "Rango",
"certificate": "Certificado"
}, },
"productions": { "productions": {
"title": "Producciones" "title": "Producciones"
@@ -207,6 +208,7 @@
}, },
"branch": { "branch": {
"title": "Sucursal", "title": "Sucursal",
"currentCertificate": "Certificado actual",
"tabs": { "tabs": {
"director": "Director", "director": "Director",
"inventory": "Inventario", "inventory": "Inventario",
@@ -510,9 +512,34 @@
}, },
"householdTension": { "householdTension": {
"label": "Tensión del hogar", "label": "Tensión del hogar",
"score": "Valor de tensión",
"reasonsLabel": "Causas actuales",
"low": "Calmo", "low": "Calmo",
"medium": "Inquieto", "medium": "Inquieto",
"high": "Tenso" "high": "Tenso",
"reasons": {
"visibleLover": "Relación visible",
"noticeableLover": "Relación llamativa",
"underfundedLover": "Relación infrafinanciada",
"acknowledgedAffair": "Relación reconocida",
"statusMismatch": "Desajuste social",
"loverChild": "Hijo de una relación",
"disorder": "Desorden en la casa",
"tooFewServants": "Muy pocos sirvientes",
"marriageCrisis": "Crisis matrimonial"
}
},
"marriageActions": {
"title": "Cuidar el matrimonio",
"spendTime": "Pasar tiempo juntos",
"giftSmall": "Regalo pequeño",
"giftDecent": "Buen regalo",
"giftLavish": "Regalo generoso",
"reconcile": "Resolver disputa",
"spendTimeSuccess": "El tiempo compartido ha estabilizado el matrimonio.",
"giftSuccess": "El regalo ha mejorado el matrimonio.",
"reconcileSuccess": "La disputa se ha calmado por ahora.",
"actionError": "No se pudo realizar la acción."
}, },
"relationships": { "relationships": {
"name": "Nombre" "name": "Nombre"
@@ -829,12 +856,15 @@
"actions": { "actions": {
"hire": "Contratar 1 sirviente", "hire": "Contratar 1 sirviente",
"dismiss": "Despedir 1 sirviente", "dismiss": "Despedir 1 sirviente",
"tidy": "Ordenar la casa",
"hireSuccess": "Se ha ampliado el servicio doméstico.", "hireSuccess": "Se ha ampliado el servicio doméstico.",
"hireError": "No se pudo ampliar el servicio doméstico.", "hireError": "No se pudo ampliar el servicio doméstico.",
"dismissSuccess": "Se ha despedido a un sirviente.", "dismissSuccess": "Se ha despedido a un sirviente.",
"dismissError": "No se pudo despedir al sirviente.", "dismissError": "No se pudo despedir al sirviente.",
"payLevelSuccess": "Se ha ajustado el pago del servicio.", "payLevelSuccess": "Se ha ajustado el pago del servicio.",
"payLevelError": "No se pudo ajustar el pago." "payLevelError": "No se pudo ajustar el pago.",
"tidySuccess": "La casa ha sido ordenada.",
"tidyError": "No se pudo ordenar la casa."
} }
}, },
"status": { "status": {

View File

@@ -6,12 +6,44 @@ import apiClient from '../utils/axios.js';
import { io } from 'socket.io-client'; import { io } from 'socket.io-client';
import { getDaemonSocketUrl, getSocketIoUrl } from '../utils/appConfig.js'; import { getDaemonSocketUrl, getSocketIoUrl } from '../utils/appConfig.js';
const AUTH_KEYS = ['isLoggedIn', 'user', 'userid'];
function getStoredValue(key) {
return localStorage.getItem(key) ?? sessionStorage.getItem(key);
}
function getStoredUser() {
const storedUser = getStoredValue('user');
if (!storedUser) return null;
try {
return JSON.parse(storedUser);
} catch (_) {
return null;
}
}
function clearAuthStorage() {
AUTH_KEYS.forEach((key) => {
localStorage.removeItem(key);
sessionStorage.removeItem(key);
});
}
function persistAuthStorage(user, rememberMe) {
const targetStorage = rememberMe ? localStorage : sessionStorage;
clearAuthStorage();
targetStorage.setItem('isLoggedIn', 'true');
targetStorage.setItem('user', JSON.stringify(user));
targetStorage.setItem('userid', user?.id || '');
}
const store = createStore({ const store = createStore({
state: { state: {
isLoggedIn: localStorage.getItem('isLoggedIn') === 'true', isLoggedIn: getStoredValue('isLoggedIn') === 'true',
connectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error' connectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error'
daemonConnectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error' daemonConnectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error'
user: JSON.parse(localStorage.getItem('user')) || null, user: getStoredUser(),
// Reconnect state management // Reconnect state management
backendRetryCount: 0, backendRetryCount: 0,
daemonRetryCount: 0, daemonRetryCount: 0,
@@ -52,11 +84,12 @@ const store = createStore({
menuNeedsUpdate: false, menuNeedsUpdate: false,
}, },
mutations: { mutations: {
async dologin(state, user) { async dologin(state, payload) {
const loginPayload = payload?.user ? payload : { user: payload, rememberMe: true };
const { user, rememberMe = true } = loginPayload;
state.isLoggedIn = true; state.isLoggedIn = true;
state.user = user; state.user = user;
localStorage.setItem('isLoggedIn', 'true'); persistAuthStorage(user, rememberMe);
localStorage.setItem('user', JSON.stringify(user));
state.menuNeedsUpdate = true; state.menuNeedsUpdate = true;
if (user.param.filter(param => ['birthdate', 'gender'].includes(param.name)).length < 2) { if (user.param.filter(param => ['birthdate', 'gender'].includes(param.name)).length < 2) {
router.push({ path: '/settings/personal' }); router.push({ path: '/settings/personal' });
@@ -65,8 +98,7 @@ const store = createStore({
async dologout(state) { async dologout(state) {
state.isLoggedIn = false; state.isLoggedIn = false;
state.user = null; state.user = null;
localStorage.removeItem('isLoggedIn'); clearAuthStorage();
localStorage.removeItem('user');
localStorage.removeItem('menu'); localStorage.removeItem('menu');
state.menuNeedsUpdate = false; state.menuNeedsUpdate = false;
@@ -145,8 +177,8 @@ const store = createStore({
}, },
}, },
actions: { actions: {
async login({ commit, dispatch }, user) { async login({ commit, dispatch }, payload) {
await commit('dologin', user); await commit('dologin', payload);
await dispatch('initializeSocket'); await dispatch('initializeSocket');
await dispatch('initializeDaemonSocket'); await dispatch('initializeDaemonSocket');
const socket = this.getters.socket; const socket = this.getters.socket;

View File

@@ -7,6 +7,11 @@
<span class="branch-kicker">Niederlassung</span> <span class="branch-kicker">Niederlassung</span>
<h2>{{ $t('falukant.branch.title') }}</h2> <h2>{{ $t('falukant.branch.title') }}</h2>
<p>Produktion, Lager, Verkauf und Transport in einer spielweltbezogenen Steuerflaeche.</p> <p>Produktion, Lager, Verkauf und Transport in einer spielweltbezogenen Steuerflaeche.</p>
<div class="branch-hero__meta">
<span class="branch-hero__badge">
{{ $t('falukant.branch.currentCertificate') }}: {{ currentCertificate ?? '---' }}
</span>
</div>
</div> </div>
</section> </section>
@@ -398,11 +403,13 @@ export default {
branchTaxes: null, branchTaxes: null,
branchTaxesLoading: false, branchTaxesLoading: false,
branchTaxesError: null, branchTaxesError: null,
currentCertificate: null,
pendingBranchRefresh: null,
}; };
}, },
computed: { computed: {
...mapState(['socket', 'daemonSocket']), ...mapState(['socket', 'daemonSocket', 'user']),
freeVehiclesByType() { freeVehiclesByType() {
const grouped = {}; const grouped = {};
for (const v of this.vehicles || []) { for (const v of this.vehicles || []) {
@@ -436,6 +443,7 @@ export default {
await this.loadBranches(); await this.loadBranches();
const branchId = this.$route.params.branchId; const branchId = this.$route.params.branchId;
await this.loadCurrentCertificate();
await this.loadProducts(); await this.loadProducts();
if (branchId) { if (branchId) {
@@ -454,6 +462,7 @@ export default {
// Live-Socket-Events (Backend Socket.io) // Live-Socket-Events (Backend Socket.io)
if (this.socket) { if (this.socket) {
this.socket.on('falukantUpdateStatus', (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data })); this.socket.on('falukantUpdateStatus', (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data }));
this.socket.on('falukantUpdateProductionCertificate', (data) => this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...data }));
this.socket.on('falukantBranchUpdate', (data) => this.handleEvent({ event: 'falukantBranchUpdate', ...data })); this.socket.on('falukantBranchUpdate', (data) => this.handleEvent({ event: 'falukantBranchUpdate', ...data }));
this.socket.on('transport_arrived', (data) => this.handleEvent({ event: 'transport_arrived', ...data })); this.socket.on('transport_arrived', (data) => this.handleEvent({ event: 'transport_arrived', ...data }));
this.socket.on('inventory_updated', (data) => this.handleEvent({ event: 'inventory_updated', ...data })); this.socket.on('inventory_updated', (data) => this.handleEvent({ event: 'inventory_updated', ...data }));
@@ -463,12 +472,17 @@ export default {
}, },
beforeUnmount() { beforeUnmount() {
if (this.pendingBranchRefresh) {
clearTimeout(this.pendingBranchRefresh);
this.pendingBranchRefresh = null;
}
// Daemon WebSocket: Listener entfernen (der Socket selbst wird beim Logout geschlossen) // Daemon WebSocket: Listener entfernen (der Socket selbst wird beim Logout geschlossen)
if (this.daemonSocket) { if (this.daemonSocket) {
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage); this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
} }
if (this.socket) { if (this.socket) {
this.socket.off('falukantUpdateStatus'); this.socket.off('falukantUpdateStatus');
this.socket.off('falukantUpdateProductionCertificate');
this.socket.off('falukantBranchUpdate'); this.socket.off('falukantBranchUpdate');
this.socket.off('transport_arrived'); this.socket.off('transport_arrived');
this.socket.off('inventory_updated'); this.socket.off('inventory_updated');
@@ -493,6 +507,34 @@ export default {
}, },
methods: { methods: {
matchesCurrentUser(eventData) {
if (eventData?.user_id == null) {
return true;
}
const currentIds = [this.user?.id, this.user?.hashedId]
.filter(Boolean)
.map((value) => String(value));
return currentIds.includes(String(eventData.user_id));
},
queueBranchRefresh() {
if (this.pendingBranchRefresh) {
clearTimeout(this.pendingBranchRefresh);
}
this.pendingBranchRefresh = setTimeout(async () => {
this.pendingBranchRefresh = null;
this.$refs.statusBar?.fetchStatus();
await this.loadCurrentCertificate();
await this.loadProducts();
this.$refs.productionSection?.loadProductions();
this.$refs.productionSection?.loadStorage();
this.$refs.storageSection?.loadStorageData();
this.$refs.saleSection?.loadInventory();
if (this.$refs.revenueSection) {
this.$refs.revenueSection.products = this.products;
this.$refs.revenueSection.refresh && this.$refs.revenueSection.refresh();
}
}, 120);
},
async loadBranches() { async loadBranches() {
try { try {
const result = await apiClient.get('/api/falukant/branches'); const result = await apiClient.get('/api/falukant/branches');
@@ -512,6 +554,14 @@ export default {
console.error('Error loading branches:', error); console.error('Error loading branches:', error);
} }
}, },
async loadCurrentCertificate() {
try {
const result = await apiClient.get('/api/falukant/user');
this.currentCertificate = result.data?.certificate ?? null;
} catch (error) {
console.error('Error loading certificate:', error);
}
},
async loadProducts() { async loadProducts() {
try { try {
@@ -771,6 +821,9 @@ export default {
}, },
handleEvent(eventData) { handleEvent(eventData) {
if (!this.matchesCurrentUser(eventData)) {
return;
}
switch (eventData.event) { switch (eventData.event) {
case 'production_ready': case 'production_ready':
this.$refs.productionSection?.loadProductions(); this.$refs.productionSection?.loadProductions();
@@ -798,30 +851,12 @@ export default {
this.$refs.productionSection?.loadStorage(); this.$refs.productionSection?.loadStorage();
break; break;
case 'falukantUpdateStatus': case 'falukantUpdateStatus':
case 'falukantUpdateProductionCertificate':
case 'falukantBranchUpdate': case 'falukantBranchUpdate':
if (this.$refs.statusBar) { this.queueBranchRefresh();
this.$refs.statusBar.fetchStatus();
}
if (this.$refs.productionSection) {
this.$refs.productionSection.loadProductions();
this.$refs.productionSection.loadStorage();
}
if (this.$refs.storageSection) {
this.$refs.storageSection.loadStorageData();
}
if (this.$refs.saleSection) {
this.$refs.saleSection.loadInventory();
}
break; break;
case 'knowledge_update': case 'knowledge_update':
this.loadProducts(); this.queueBranchRefresh();
if (this.$refs.revenueSection) {
this.$refs.revenueSection.products = this.products;
this.$refs.revenueSection.refresh && this.$refs.revenueSection.refresh();
}
break; break;
case 'transport_arrived': case 'transport_arrived':
// Leerer Transport angekommen - Fahrzeug wurde zurückgeholt // Leerer Transport angekommen - Fahrzeug wurde zurückgeholt
@@ -1149,6 +1184,22 @@ export default {
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.branch-hero__meta {
margin-top: 12px;
}
.branch-hero__badge {
display: inline-flex;
align-items: center;
padding: 6px 10px;
border: 1px solid rgba(138, 84, 17, 0.16);
border-radius: 999px;
background: rgba(255, 255, 255, 0.7);
color: #7a4b12;
font-size: 0.9rem;
font-weight: 600;
}
.branch-tab-content { .branch-tab-content {
margin-top: 16px; margin-top: 16px;
padding: 18px; padding: 18px;

View File

@@ -149,6 +149,39 @@
{{ $t('falukant.family.householdTension.' + householdTension) }} {{ $t('falukant.family.householdTension.' + householdTension) }}
</span> </span>
</div> </div>
<div class="marriage-overview__item" v-if="householdTensionScore != null">
<span class="marriage-overview__label">{{ $t('falukant.family.householdTension.score') }}</span>
<strong>{{ householdTensionScore }}</strong>
</div>
</section>
<section v-if="relationships.length > 0 && relationships[0].relationshipType === 'married'" class="marriage-actions surface-card">
<h3>{{ $t('falukant.family.marriageActions.title') }}</h3>
<div class="marriage-actions__buttons">
<button class="button button--secondary" @click="spendTimeWithSpouse">
{{ $t('falukant.family.marriageActions.spendTime') }}
</button>
<button class="button button--secondary" @click="giftToSpouse('small')">
{{ $t('falukant.family.marriageActions.giftSmall') }}
</button>
<button class="button button--secondary" @click="giftToSpouse('decent')">
{{ $t('falukant.family.marriageActions.giftDecent') }}
</button>
<button class="button button--secondary" @click="giftToSpouse('lavish')">
{{ $t('falukant.family.marriageActions.giftLavish') }}
</button>
<button class="button button--secondary" @click="reconcileMarriage">
{{ $t('falukant.family.marriageActions.reconcile') }}
</button>
</div>
<div v-if="householdTensionReasons.length > 0" class="marriage-actions__reasons">
<span class="marriage-actions__reasons-label">{{ $t('falukant.family.householdTension.reasonsLabel') }}</span>
<div class="marriage-actions__reason-list">
<span v-for="reason in householdTensionReasons" :key="reason" class="lover-meta-badge lover-meta-badge--warning">
{{ $t('falukant.family.householdTension.reasons.' + reason) }}
</span>
</div>
</div>
</section> </section>
<div class="children-section"> <div class="children-section">
@@ -351,6 +384,8 @@ export default {
marriageSatisfaction: null, marriageSatisfaction: null,
marriageState: null, marriageState: null,
householdTension: null, householdTension: null,
householdTensionScore: null,
householdTensionReasons: [],
selectedChild: null, selectedChild: null,
pendingFamilyRefresh: null pendingFamilyRefresh: null
} }
@@ -495,11 +530,46 @@ export default {
this.marriageSatisfaction = response.data.marriageSatisfaction; this.marriageSatisfaction = response.data.marriageSatisfaction;
this.marriageState = response.data.marriageState; this.marriageState = response.data.marriageState;
this.householdTension = response.data.householdTension; this.householdTension = response.data.householdTension;
this.householdTensionScore = response.data.householdTensionScore;
this.householdTensionReasons = response.data.householdTensionReasons || [];
} catch (error) { } catch (error) {
console.error('Error loading family data:', error); console.error('Error loading family data:', error);
} }
}, },
async spendTimeWithSpouse() {
try {
await apiClient.post('/api/falukant/family/marriage/spend-time');
await this.loadFamilyData();
showSuccess(this, this.$t('falukant.family.marriageActions.spendTimeSuccess'));
} catch (error) {
console.error('Error spending time with spouse:', error);
showError(this, this.$t('falukant.family.marriageActions.actionError'));
}
},
async giftToSpouse(giftLevel) {
try {
await apiClient.post('/api/falukant/family/marriage/gift', { giftLevel });
await this.loadFamilyData();
showSuccess(this, this.$t('falukant.family.marriageActions.giftSuccess'));
} catch (error) {
console.error('Error gifting spouse:', error);
showError(this, this.$t('falukant.family.marriageActions.actionError'));
}
},
async reconcileMarriage() {
try {
await apiClient.post('/api/falukant/family/marriage/reconcile');
await this.loadFamilyData();
showSuccess(this, this.$t('falukant.family.marriageActions.reconcileSuccess'));
} catch (error) {
console.error('Error reconciling marriage:', error);
showError(this, this.$t('falukant.family.marriageActions.actionError'));
}
},
async loadOwnCharacter() { async loadOwnCharacter() {
try { try {
const response = await apiClient.get('/api/falukant/user'); const response = await apiClient.get('/api/falukant/user');
@@ -814,6 +884,39 @@ export default {
font-size: 0.88rem; font-size: 0.88rem;
} }
.marriage-actions {
display: grid;
gap: 12px;
margin-bottom: 18px;
padding: 16px 18px;
}
.marriage-actions h3 {
margin: 0;
}
.marriage-actions__buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.marriage-actions__reasons {
display: grid;
gap: 8px;
}
.marriage-actions__reasons-label {
color: var(--color-text-secondary);
font-size: 0.88rem;
}
.marriage-actions__reason-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.inline-status-pill { .inline-status-pill {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;

View File

@@ -47,6 +47,9 @@
<button class="button-secondary" :disabled="(userHouse.servantCount || 0) <= 0" @click="dismissServant"> <button class="button-secondary" :disabled="(userHouse.servantCount || 0) <= 0" @click="dismissServant">
{{ $t('falukant.house.servants.actions.dismiss') }} {{ $t('falukant.house.servants.actions.dismiss') }}
</button> </button>
<button class="button-secondary" @click="tidyHousehold">
{{ $t('falukant.house.servants.actions.tidy') }}
</button>
</div> </div>
</div> </div>
@@ -71,6 +74,10 @@
<span class="servant-card__label">{{ $t('falukant.house.servants.householdOrder') }}</span> <span class="servant-card__label">{{ $t('falukant.house.servants.householdOrder') }}</span>
<strong>{{ userHouse.householdOrder || 0 }}</strong> <strong>{{ userHouse.householdOrder || 0 }}</strong>
</article> </article>
<article class="servant-card">
<span class="servant-card__label">{{ $t('falukant.family.householdTension.score') }}</span>
<strong>{{ userHouse.householdTensionScore ?? 0 }}</strong>
</article>
<article class="servant-card"> <article class="servant-card">
<span class="servant-card__label">{{ $t('falukant.house.servants.staffingState.label') }}</span> <span class="servant-card__label">{{ $t('falukant.house.servants.staffingState.label') }}</span>
<strong>{{ $t(`falukant.house.servants.staffingState.${servantSummary.staffingState || 'fitting'}`) }}</strong> <strong>{{ $t(`falukant.house.servants.staffingState.${servantSummary.staffingState || 'fitting'}`) }}</strong>
@@ -91,6 +98,14 @@
<strong>{{ $t(`falukant.house.servants.orderState.${servantSummary.orderState || 'stable'}`) }}</strong> <strong>{{ $t(`falukant.house.servants.orderState.${servantSummary.orderState || 'stable'}`) }}</strong>
</div> </div>
</div> </div>
<div v-if="Array.isArray(userHouse.householdTensionReasonsJson) && userHouse.householdTensionReasonsJson.length > 0" class="servants-reasons">
<span class="servants-reasons__label">{{ $t('falukant.family.householdTension.reasonsLabel') }}</span>
<div class="servants-reasons__list">
<span v-for="reason in userHouse.householdTensionReasonsJson" :key="reason" class="servants-reasons__badge">
{{ $t('falukant.family.householdTension.reasons.' + reason) }}
</span>
</div>
</div>
</section> </section>
<div class="buyable-houses"> <div class="buyable-houses">
@@ -299,6 +314,16 @@ export default {
showError(this, this.$t('falukant.house.servants.actions.payLevelError')); showError(this, this.$t('falukant.house.servants.actions.payLevelError'));
} }
}, },
async tidyHousehold() {
try {
await apiClient.post('/api/falukant/houses/order');
await this.loadData();
showSuccess(this, this.$t('falukant.house.servants.actions.tidySuccess'));
} catch (err) {
console.error('Error tidying household', err);
showError(this, this.$t('falukant.house.servants.actions.tidyError'));
}
},
handleDaemonMessage(evt) { handleDaemonMessage(evt) {
try { try {
const msg = JSON.parse(evt.data); const msg = JSON.parse(evt.data);
@@ -344,6 +369,17 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
/* AppContent gibt dem letzten Kind flex:1 + min-height:0 — sonst schrumpfen
Spalten-Kinder und überlagern sich (z. B. „Dienerschaft“ unter „Kaufe ein Haus“). */
flex: 0 0 auto;
min-height: min-content;
width: 100%;
}
.existing-house,
.servants-panel,
.buyable-houses {
flex-shrink: 0;
} }
h2 { h2 {
@@ -442,6 +478,34 @@ h2 {
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.servants-reasons {
display: grid;
gap: 8px;
margin-top: 16px;
}
.servants-reasons__label {
color: var(--color-text-secondary);
font-size: 0.88rem;
}
.servants-reasons__list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.servants-reasons__badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 999px;
background: rgba(188, 84, 61, 0.12);
color: #9a3c26;
font-size: 0.8rem;
font-weight: 700;
}
.buyable-houses { .buyable-houses {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -24,6 +24,11 @@
</div> </div>
<section v-if="falukantUser?.character" class="falukant-summary-grid"> <section v-if="falukantUser?.character" class="falukant-summary-grid">
<article class="summary-card surface-card">
<span class="summary-card__label">{{ $t('falukant.overview.metadata.certificate') }}</span>
<strong>{{ falukantUser?.certificate ?? '---' }}</strong>
<p>Bestimmt, welche Produktkategorien du derzeit herstellen darfst.</p>
</article>
<article class="summary-card surface-card"> <article class="summary-card surface-card">
<span class="summary-card__label">Niederlassungen</span> <span class="summary-card__label">Niederlassungen</span>
<strong>{{ branchCount }}</strong> <strong>{{ branchCount }}</strong>
@@ -109,6 +114,10 @@
<span>{{ $t('falukant.overview.metadata.mainbranch') }}</span> <span>{{ $t('falukant.overview.metadata.mainbranch') }}</span>
<strong>{{ falukantUser?.mainBranchRegion?.name }}</strong> <strong>{{ falukantUser?.mainBranchRegion?.name }}</strong>
</div> </div>
<div class="detail-list__item">
<span>{{ $t('falukant.overview.metadata.certificate') }}</span>
<strong>{{ falukantUser?.certificate ?? '---' }}</strong>
</div>
</div> </div>
</section> </section>
<section class="overview-panel surface-card"> <section class="overview-panel surface-card">
@@ -347,6 +356,7 @@ export default {
this.socket.off("falukantUserUpdated", this.fetchFalukantUser); this.socket.off("falukantUserUpdated", this.fetchFalukantUser);
this.socket.off("falukantUpdateStatus"); this.socket.off("falukantUpdateStatus");
this.socket.off("falukantUpdateFamily"); this.socket.off("falukantUpdateFamily");
this.socket.off("falukantUpdateProductionCertificate");
this.socket.off("children_update"); this.socket.off("children_update");
this.socket.off("falukantBranchUpdate"); this.socket.off("falukantBranchUpdate");
this.socket.off("stock_change"); this.socket.off("stock_change");
@@ -362,6 +372,9 @@ export default {
this.socket.on("falukantUpdateFamily", (data) => { this.socket.on("falukantUpdateFamily", (data) => {
this.handleEvent({ event: 'falukantUpdateFamily', ...data }); this.handleEvent({ event: 'falukantUpdateFamily', ...data });
}); });
this.socket.on("falukantUpdateProductionCertificate", (data) => {
this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...data });
});
this.socket.on("children_update", (data) => { this.socket.on("children_update", (data) => {
this.handleEvent({ event: 'children_update', ...data }); this.handleEvent({ event: 'children_update', ...data });
}); });
@@ -428,6 +441,7 @@ export default {
switch (eventData.event) { switch (eventData.event) {
case 'falukantUpdateStatus': case 'falukantUpdateStatus':
case 'falukantUpdateFamily': case 'falukantUpdateFamily':
case 'falukantUpdateProductionCertificate':
case 'children_update': case 'children_update':
case 'falukantBranchUpdate': case 'falukantBranchUpdate':
this.queueOverviewRefresh(); this.queueOverviewRefresh();

View File

@@ -57,7 +57,6 @@
<h2>{{ $t('home.nologin.login.submit') }}</h2> <h2>{{ $t('home.nologin.login.submit') }}</h2>
<p class="login-panel__hint">Mit bestehendem Konto direkt einloggen oder alternativ ohne Konto den Random-Chat testen.</p> <p class="login-panel__hint">Mit bestehendem Konto direkt einloggen oder alternativ ohne Konto den Random-Chat testen.</p>
<div class="quick-access-actions"> <div class="quick-access-actions">
<button type="button" class="primary-action" @click="doLogin">{{ $t('home.nologin.login.submit') }}</button>
<button type="button" class="secondary-action" @click="openRegisterDialog">{{ $t('home.nologin.login.register') }}</button> <button type="button" class="secondary-action" @click="openRegisterDialog">{{ $t('home.nologin.login.register') }}</button>
<button type="button" class="secondary-action" @click="openRandomChat">{{ $t('home.nologin.startrandomchat') }}</button> <button type="button" class="secondary-action" @click="openRandomChat">{{ $t('home.nologin.startrandomchat') }}</button>
</div> </div>
@@ -69,9 +68,12 @@
:title="$t('home.nologin.login.passworddescription')" @keydown.enter="doLogin" :title="$t('home.nologin.login.passworddescription')" @keydown.enter="doLogin"
ref="passwordInput"> ref="passwordInput">
</div> </div>
<div class="login-submit-row">
<button type="button" class="primary-action" @click="doLogin">{{ $t('home.nologin.login.submit') }}</button>
</div>
<div class="stay-logged-in-row"> <div class="stay-logged-in-row">
<label class="stay-logged-in-label"> <label class="stay-logged-in-label">
<input class="stay-logged-in-checkbox" type="checkbox"> <input v-model="rememberMe" class="stay-logged-in-checkbox" type="checkbox">
<span>{{ $t('home.nologin.login.stayLoggedIn') }}</span> <span>{{ $t('home.nologin.login.stayLoggedIn') }}</span>
</label> </label>
</div> </div>
@@ -112,6 +114,7 @@ export default {
return { return {
username: '', username: '',
password: '', password: '',
rememberMe: true,
}; };
}, },
components: { components: {
@@ -140,7 +143,7 @@ export default {
async doLogin() { async doLogin() {
try { try {
const response = await apiClient.post('/api/auth/login', { username: this.username, password: this.password }); const response = await apiClient.post('/api/auth/login', { username: this.username, password: this.password });
this.login(response.data); this.login({ user: response.data, rememberMe: this.rememberMe });
} catch (error) { } catch (error) {
const errorKey = error?.response?.data?.error || 'network'; const errorKey = error?.response?.data?.error || 'network';
this.$root.$refs.errorDialog.open(`tr:error.${errorKey}`); this.$root.$refs.errorDialog.open(`tr:error.${errorKey}`);
@@ -326,6 +329,11 @@ export default {
gap: 0.65rem; gap: 0.65rem;
} }
.login-submit-row {
display: flex;
justify-content: flex-start;
}
.primary-action, .primary-action,
.secondary-action { .secondary-action {
align-self: flex-start; align-self: flex-start;

View File

@@ -3131,7 +3131,7 @@ export default {
console.log('User username:', this.$store?.state?.user?.username); console.log('User username:', this.$store?.state?.user?.username);
const highscoreData = { const highscoreData = {
userId: this.$store?.state?.user?.id || localStorage.getItem('userid') || 'guest', userId: this.$store?.state?.user?.id || localStorage.getItem('userid') || sessionStorage.getItem('userid') || 'guest',
nickname: this.$store?.state?.user?.nickname || this.$store?.state?.user?.name || this.$store?.state?.user?.username || 'Gast', nickname: this.$store?.state?.user?.nickname || this.$store?.state?.user?.name || this.$store?.state?.user?.username || 'Gast',
passengersDelivered: this.passengersDelivered, passengersDelivered: this.passengersDelivered,
playtime: playTime, playtime: playTime,

View File

@@ -205,7 +205,7 @@ export default {
} }
}, },
async fetchImage(image) { async fetchImage(image) {
const userId = localStorage.getItem('userid'); const userId = localStorage.getItem('userid') || sessionStorage.getItem('userid');
try { try {
const response = await apiClient.get(`/api/socialnetwork/image/${image.hash}`, { const response = await apiClient.get(`/api/socialnetwork/image/${image.hash}`, {
headers: { headers: {