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:
@@ -517,6 +517,167 @@ class FalukantService extends BaseService {
|
||||
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) {
|
||||
if (!state) return 'low';
|
||||
if ((state.visibility ?? 0) >= 60 || (state.monthsUnderfunded ?? 0) >= 2) return 'high';
|
||||
@@ -526,7 +687,11 @@ class FalukantService extends BaseService {
|
||||
|
||||
calculateLoverStatusFit(ownTitleId, targetTitleId) {
|
||||
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) {
|
||||
@@ -3026,6 +3191,13 @@ class FalukantService extends BaseService {
|
||||
const activeMarriage = activeRelationships.find(r => r.relationshipType === 'married') || activeRelationships[0] || null;
|
||||
const marriageSatisfaction = activeMarriage?.state?.marriageSatisfaction ?? null;
|
||||
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
|
||||
.filter(r => r.relationshipType === 'lover')
|
||||
.filter(r => (r.state?.active ?? true) !== false)
|
||||
@@ -3057,6 +3229,17 @@ class FalukantService extends BaseService {
|
||||
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 = {
|
||||
relationships: activeRelationships.map((r) => ({
|
||||
...r,
|
||||
@@ -3065,7 +3248,9 @@ class FalukantService extends BaseService {
|
||||
})),
|
||||
marriageSatisfaction,
|
||||
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,
|
||||
deathPartners: relationships.filter(r => r.relationshipType === 'widowed'),
|
||||
children: children.map(({ _createdAt, ...rest }) => rest),
|
||||
@@ -3203,6 +3388,7 @@ class FalukantService extends BaseService {
|
||||
statusFit: target.statusFit,
|
||||
active: true
|
||||
});
|
||||
await this.refreshHouseholdTensionState(user, user.character);
|
||||
|
||||
await notifyUser(hashedUserId, 'falukantUpdateFamily', { reason: 'daily' });
|
||||
await notifyUser(hashedUserId, 'falukantUpdateStatus', {});
|
||||
@@ -3224,8 +3410,9 @@ class FalukantService extends BaseService {
|
||||
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 this.refreshHouseholdTensionState(user, user.character);
|
||||
return {
|
||||
success: true,
|
||||
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) {
|
||||
const parsedRelationshipId = Number.parseInt(relationshipId, 10);
|
||||
if (Number.isNaN(parsedRelationshipId)) {
|
||||
@@ -3245,6 +3545,7 @@ class FalukantService extends BaseService {
|
||||
updateData.loverRole = 'lover';
|
||||
}
|
||||
await state.update(updateData);
|
||||
await this.refreshHouseholdTensionState(user, user.character);
|
||||
return {
|
||||
success: true,
|
||||
relationshipId: parsedRelationshipId,
|
||||
@@ -3264,6 +3565,7 @@ class FalukantService extends BaseService {
|
||||
active: false,
|
||||
acknowledged: false
|
||||
});
|
||||
await this.refreshHouseholdTensionState(user, user.character);
|
||||
return {
|
||||
success: true,
|
||||
relationshipId: parsedRelationshipId,
|
||||
@@ -4031,6 +4333,8 @@ class FalukantService extends BaseService {
|
||||
'servantQuality',
|
||||
'servantPayLevel',
|
||||
'householdOrder',
|
||||
'householdTensionScore',
|
||||
'householdTensionReasonsJson',
|
||||
'houseTypeId'
|
||||
]
|
||||
});
|
||||
@@ -4046,6 +4350,8 @@ class FalukantService extends BaseService {
|
||||
servantQuality: 50,
|
||||
servantPayLevel: 'normal',
|
||||
householdOrder: 55,
|
||||
householdTensionScore: 10,
|
||||
householdTensionReasonsJson: [],
|
||||
servantSummary: this.buildServantSummary(null, falukantUser.character)
|
||||
};
|
||||
}
|
||||
@@ -4116,7 +4422,9 @@ class FalukantService extends BaseService {
|
||||
servantCount: servantDefaults.servantCount,
|
||||
servantQuality: servantDefaults.servantQuality,
|
||||
servantPayLevel: servantDefaults.servantPayLevel,
|
||||
householdOrder: servantDefaults.householdOrder
|
||||
householdOrder: servantDefaults.householdOrder,
|
||||
householdTensionScore: 10,
|
||||
householdTensionReasonsJson: []
|
||||
});
|
||||
await house.destroy();
|
||||
await updateFalukantUserMoney(falukantUser.id, -housePrice, "housebuy", falukantUser.id);
|
||||
@@ -4287,6 +4595,7 @@ class FalukantService extends BaseService {
|
||||
});
|
||||
await house.save();
|
||||
await updateFalukantUserMoney(falukantUser.id, -hireCost, 'servants_hired', falukantUser.id);
|
||||
await this.refreshHouseholdTensionState(falukantUser, falukantUser.character);
|
||||
|
||||
const user = await User.findByPk(falukantUser.userId);
|
||||
notifyUser(user.hashedId, 'falukantHouseUpdate', {});
|
||||
@@ -4321,6 +4630,7 @@ class FalukantService extends BaseService {
|
||||
character: falukantUser.character
|
||||
});
|
||||
await house.save();
|
||||
await this.refreshHouseholdTensionState(falukantUser, falukantUser.character);
|
||||
|
||||
const user = await User.findByPk(falukantUser.userId);
|
||||
notifyUser(user.hashedId, 'falukantHouseUpdate', {});
|
||||
@@ -4350,6 +4660,7 @@ class FalukantService extends BaseService {
|
||||
character: falukantUser.character
|
||||
});
|
||||
await house.save();
|
||||
await this.refreshHouseholdTensionState(falukantUser, falukantUser.character);
|
||||
|
||||
const user = await User.findByPk(falukantUser.userId);
|
||||
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) {
|
||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||
const engagedCount = await Relationship.count({
|
||||
|
||||
Reference in New Issue
Block a user