feat(falukant): add character visual representation and settings
All checks were successful
Deploy to production / deploy (push) Successful in 2m53s

- Introduced FalukantCharacterVisual component to display character portraits and 3D figures based on user settings.
- Updated DirectorInfo, ChurchView, PoliticsView, and other components to utilize the new character visual component.
- Added visual settings management for users, allowing selection between portrait styles and display modes (portraits, 3D, or both).
- Implemented age rules for engagement and marriage in the backend, ensuring compliance with specified age limits.
- Created a new settings view for Falukant to manage visual preferences.
- Added new images for medieval character portraits.
- Updated translations for new settings and features in multiple languages.
This commit is contained in:
Torsten Schulz (local)
2026-07-24 10:55:19 +02:00
parent c13339f771
commit 16703f467f
36 changed files with 811 additions and 49 deletions

View File

@@ -92,6 +92,13 @@ function calcAge(birthdate) {
return differenceInDays(now, b);
}
// Ein realer Tag entspricht einem Falukant-Spieljahr.
const FAMILY_AGE = Object.freeze({
MIN_WOOING: 12,
MIN_MARRIAGE: 14,
MIN_HOUSEHOLD_AND_CHILDREN: 16,
});
async function getFalukantUserOrFail(hashedId) {
const user = await FalukantUser.findOne({
include: [{ model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId } }]
@@ -3571,6 +3578,17 @@ class FalukantService extends BaseService {
return { scheduled: false };
}
const parents = await FalukantCharacter.unscoped().findAll({
where: { id: [motherId, fatherId] },
attributes: ['id', 'birthdate'],
});
if (
parents.length !== 2
|| parents.some((parent) => calcAge(parent.birthdate) < FAMILY_AGE.MIN_HOUSEHOLD_AND_CHILDREN)
) {
return { scheduled: false };
}
const stateRow = await RelationshipState.findOne({ where: { relationshipId } });
const flags = stateRow?.flagsJson && typeof stateRow.flagsJson === 'object'
? { ...stateRow.flagsJson }
@@ -3899,10 +3917,10 @@ class FalukantService extends BaseService {
pregnancy,
};
const ownAge = calcAge(character.birthdate);
if (ownAge >= 12) {
if (ownAge >= FAMILY_AGE.MIN_WOOING) {
family.possibleLovers = await this.getPossibleLovers(character.id);
}
if (ownAge >= 12 && family.relationships.length === 0) {
if (ownAge >= FAMILY_AGE.MIN_WOOING && family.relationships.length === 0) {
family.possiblePartners = await this.getPossiblePartners(character.id);
if (family.possiblePartners.length === 0) {
await this.createPossiblePartners(
@@ -3937,7 +3955,7 @@ class FalukantService extends BaseService {
attributes: ['id', 'regionId', 'birthdate', 'titleOfNobility']
});
if (!requester?.id) return [];
if (calcAge(requester.birthdate) < 12) return [];
if (calcAge(requester.birthdate) < FAMILY_AGE.MIN_WOOING) return [];
const existingRelationships = await Relationship.findAll({
where: {
@@ -4549,6 +4567,7 @@ class FalukantService extends BaseService {
id: { [Op.ne]: requestingCharacterId },
gender: { [Op.ne]: requestingCharacterGender },
regionId: requestingRegionId,
birthdate: { [Op.lte]: new Date(Date.now() - FAMILY_AGE.MIN_WOOING * 24 * 60 * 60 * 1000) },
createdAt: { [Op.lt]: new Date(new Date() - 12 * 24 * 60 * 60 * 1000) },
titleOfNobility: { [Op.between]: [requestingCharacterTitleOfNobility - 1, requestingCharacterTitleOfNobility + 1] }
},
@@ -4588,6 +4607,18 @@ class FalukantService extends BaseService {
if (!proposal) {
throw new Error('Proposal not found');
}
const proposedCharacter = await FalukantCharacter.findByPk(proposal.proposedCharacterId, {
attributes: ['id', 'birthdate'],
});
if (
!proposedCharacter
|| calcAge(character.birthdate) < FAMILY_AGE.MIN_WOOING
|| calcAge(proposedCharacter.birthdate) < FAMILY_AGE.MIN_WOOING
) {
const error = new Error('Werbung ist erst ab 12 Spieljahren möglich');
error.status = 422;
throw error;
}
if (user.money < proposal.cost) {
throw new Error('Not enough money to accept the proposal');
}
@@ -5510,7 +5541,12 @@ class FalukantService extends BaseService {
async getPartyTypes(hashedUserId) {
const falukantUser = await getFalukantUserOrFail(hashedUserId);
const engagedCount = await Relationship.count({
const character = await FalukantCharacter.findOne({
where: { userId: falukantUser.id },
attributes: ['id', 'birthdate'],
});
const engagedCount = character && calcAge(character.birthdate) >= FAMILY_AGE.MIN_MARRIAGE
? await Relationship.count({
include: [
{
model: RelationshipType,
@@ -5537,7 +5573,8 @@ class FalukantService extends BaseService {
{ '$character2.user_id$': falukantUser.id }
]
}
});
})
: 0;
const orConditions = [{ forMarriage: false }];
if (engagedCount > 0) {
orConditions.push({ forMarriage: true });
@@ -5586,7 +5623,35 @@ class FalukantService extends BaseService {
throw new Error('Einige ausgewählte Adelstitel existieren nicht');
}
const character = await FalukantCharacter.findOne({ where: { userId: falukantUser.id }, attributes: ['titleOfNobility'] });
const character = await FalukantCharacter.findOne({ where: { userId: falukantUser.id }, attributes: ['id', 'birthdate', 'titleOfNobility'] });
if (ptype.forMarriage) {
if (!character || calcAge(character.birthdate) < FAMILY_AGE.MIN_MARRIAGE) {
const error = new Error('Eine Hochzeit ist erst ab 14 Spieljahren möglich');
error.status = 422;
throw error;
}
const engagement = await Relationship.findOne({
where: {
[Op.or]: [
{ character1Id: character.id },
{ character2Id: character.id },
],
},
include: [{ model: RelationshipType, as: 'relationshipType', where: { tr: 'engaged' } }],
attributes: ['character1Id', 'character2Id'],
});
const partnerId = engagement?.character1Id === character.id
? engagement.character2Id
: engagement?.character1Id;
const partner = partnerId
? await FalukantCharacter.findByPk(partnerId, { attributes: ['birthdate'] })
: null;
if (!partner || calcAge(partner.birthdate) < FAMILY_AGE.MIN_MARRIAGE) {
const error = new Error('Beide Verlobten müssen mindestens 14 Spieljahre alt sein');
error.status = 422;
throw error;
}
}
const partyTypeCost = character && (await isPartyTypeFreeForTitle(character.titleOfNobility, ptype.id, ptype.tr)) ? 0 : (ptype.cost || 0);
let cost = partyTypeCost + (music.cost || 0) + (banquette.cost || 0);
cost += (50 / servantRatio - 1) * 1000;
@@ -6747,7 +6812,7 @@ class FalukantService extends BaseService {
{
model: FalukantCharacter,
as: 'holder',
attributes: ['id', 'gender'],
attributes: ['id', 'gender', 'birthdate'],
include: [
{
model: FalukantPredefineFirstname,
@@ -6828,7 +6893,8 @@ class FalukantService extends BaseService {
definedFirstName: o.holder.definedFirstName,
definedLastName: o.holder.definedLastName,
nobleTitle: o.holder.nobleTitle,
gender: o.holder.gender
gender: o.holder.gender,
age: o.holder.birthdate ? calcAge(o.holder.birthdate) : null
}
: null,
termEnds,
@@ -8302,7 +8368,7 @@ ORDER BY r.id`,
{
model: FalukantCharacter,
as: 'holder',
attributes: ['id', 'gender'],
attributes: ['id', 'gender', 'birthdate'],
required: false,
include: [
{
@@ -8368,7 +8434,8 @@ ORDER BY r.id`,
id: o.holder.id,
name: `${o.holder.definedFirstName?.name || ''} ${o.holder.definedLastName?.name || ''}`.trim(),
gender: o.holder.gender,
title: o.holder.nobleTitle?.labelTr
title: o.holder.nobleTitle?.labelTr,
age: o.holder.birthdate ? calcAge(o.holder.birthdate) : null
}
: null,
supervisor: o.supervisor