diff --git a/backend/controllers/navigationController.js b/backend/controllers/navigationController.js index 126abb6..524f6d3 100755 --- a/backend/controllers/navigationController.js +++ b/backend/controllers/navigationController.js @@ -216,6 +216,10 @@ const menuStructure = { visible: ["all"], path: "/settings/language-assistant" }, + falukant: { + visible: ["all"], + path: "/settings/falukant" + }, personal: { visible: ["all"], path: "/settings/personal" diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index be85c25..b78ebfa 100755 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -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 diff --git a/backend/services/settingsService.js b/backend/services/settingsService.js index 627f84e..c99ba6b 100755 --- a/backend/services/settingsService.js +++ b/backend/services/settingsService.js @@ -37,6 +37,18 @@ class SettingsService extends BaseService{ adult_verification_status: { datatype: 'string', setting: 'account', orderId: 910, minAge: 18 }, adult_verification_request: { datatype: 'string', setting: 'account', orderId: 911, minAge: 18 }, adult_upload_blocked: { datatype: 'bool', setting: 'account', orderId: 912, minAge: 18 }, + falukantPortraitStyle: { + datatype: 'singleselect', + setting: 'falukant', + orderId: 920, + options: ['classic', 'medieval'] + }, + falukantFigureMode: { + datatype: 'singleselect', + setting: 'falukant', + orderId: 921, + options: ['portraits', '3d', 'both'] + }, }; const definition = specialTypes[description]; if (!definition) { @@ -59,9 +71,38 @@ class SettingsService extends BaseService{ immutable: false } }); + if (Array.isArray(definition.options) && definition.options.length > 0) { + let orderId = 1; + for (const option of definition.options) { + await UserParamValue.findOrCreate({ + where: { + userParamTypeId: paramType.id, + value: option + }, + defaults: { + userParamTypeId: paramType.id, + value: option, + orderId: orderId++ + } + }); + } + } return paramType; } + async ensureSettingsGroup(type) { + const groupDefinitions = { + falukant: ['falukantPortraitStyle', 'falukantFigureMode'] + }; + const descriptions = groupDefinitions[type]; + if (!descriptions) { + return; + } + for (const description of descriptions) { + await this.ensureSpecialUserParamType(description); + } + } + parseAdultVerificationRequest(value) { if (!value) return null; try { @@ -298,6 +339,7 @@ class SettingsService extends BaseService{ } async filterSettings(hashedUserId, type) { + await this.ensureSettingsGroup(type); const user = await this.getUserByHashedId(hashedUserId); const userParams = await this.getUserParams(user.id, ['birthdate', 'gender']); let birthdate = null; diff --git a/backend/utils/initializeSettings.js b/backend/utils/initializeSettings.js index 8d77a60..7a76966 100755 --- a/backend/utils/initializeSettings.js +++ b/backend/utils/initializeSettings.js @@ -25,6 +25,10 @@ const initializeSettings = async () => { where: { name: 'languageAssistant' }, defaults: { name: 'languageAssistant' } }); + await SettingsType.findOrCreate({ + where: { name: 'falukant' }, + defaults: { name: 'falukant' } + }); }; export default initializeSettings; diff --git a/backend/utils/initializeTypes.js b/backend/utils/initializeTypes.js index 9a2dec7..406fdd7 100755 --- a/backend/utils/initializeTypes.js +++ b/backend/utils/initializeTypes.js @@ -51,6 +51,8 @@ const initializeTypes = async () => { adult_upload_blocked: { type: 'bool', setting: 'account', minAge: 18 }, llm_settings: { type: 'string', setting: 'languageAssistant' }, llm_api_key: { type: 'string', setting: 'languageAssistant' }, + falukantPortraitStyle: { type: 'singleselect', setting: 'falukant' }, + falukantFigureMode: { type: 'singleselect', setting: 'falukant' }, }; let orderId = 1; for (const key of Object.keys(userParams)) { @@ -78,7 +80,9 @@ const initializeTypes = async () => { interestedInGender: ['male', 'female'], smokes: ['never', 'socially', 'often', 'daily'], drinks: ['never', 'socially', 'often', 'daily'], - brasize: ['Keine', 'AA', 'A', 'B', 'C', 'D', 'E (DD)', 'F (E)', 'G (F)', 'H (FF)', 'I (G)', 'J (GG)', 'K (H)'] + brasize: ['Keine', 'AA', 'A', 'B', 'C', 'D', 'E (DD)', 'F (E)', 'G (F)', 'H (FF)', 'I (G)', 'J (GG)', 'K (H)'], + falukantPortraitStyle: ['classic', 'medieval'], + falukantFigureMode: ['portraits', '3d', 'both'] }; Object.keys(valuesList).forEach(async (key) => { const values = valuesList[key]; diff --git a/docs/FALUKANT_FAMILY_AGE_RULES.md b/docs/FALUKANT_FAMILY_AGE_RULES.md new file mode 100644 index 0000000..8d788ef --- /dev/null +++ b/docs/FALUKANT_FAMILY_AGE_RULES.md @@ -0,0 +1,78 @@ +# Falukant: Altersregeln für Werbung, Ehe und Kinder + +## Verbindliche Spielregel + +Ein realer Kalendertag entspricht einem Falukant-Spieljahr. Die Altersprüfung +verwendet daher die Differenz zwischen `CURRENT_DATE` und `birthdate` in Tagen. + +| Bereich | Mindestalter | Regel | +| --- | ---: | --- | +| Werbung | 12 | Partner können vorgeschlagen und ein Vorschlag kann angenommen werden. | +| Hochzeit | 14 | Eine Verlobung kann durch eine Hochzeitsfeier vollzogen werden. | +| Gemeinsamer Haushalt und Kinder | 16 | Erst dann darf die Spielmechanik eine Schwangerschaft oder Geburt aus einer Ehe erzeugen. | + +Die Regeln gelten für **beide** Personen einer Beziehung. Sie bilden bewusst +Spielphasen ab; es gibt keine individuelle Prüfung körperlicher Reife und keine +explizite Sexualmechanik. + +## Bereits im Backend und Daemon umgesetzt + +- `backend/services/falukantService.js` + - erzeugt Heiratsvorschläge nur für mindestens 12 Jahre alte Partner; + - prüft beim Annehmen eines Vorschlags beide Alter erneut; + - bietet den Hochzeitstyp erst ab 14 an und prüft beim Bestellen der Feier + beide Verlobte serverseitig; + - plant die Hochzeits-Schwangerschaft erst, wenn beide mindestens 16 sind. +- `src/valuerecalculationworker.h` + - vollzieht die Hochzeit nach der mindestens einen Tag alten Hochzeitsfeier + nur, wenn beide Verlobte mindestens 14 sind. +- `src/usercharacterworker.h` + - berücksichtigt für die bestehende zufällige Ehe-Kinderlogik ausschließlich + Paare, bei denen beide mindestens 16 sind. + +## Auftrag für den externen Daemon + +Falls der produktive Daemon aus diesem Repository herausgelöst ist, müssen die +folgenden Guards in dessen gleichnamige Queries übernommen werden. + +### 1. Übergang `engaged` → `married` + +Beim Verarbeiten einer Hochzeit sind beide Charaktere zu prüfen: + +```sql +c.birthdate <= CURRENT_DATE - INTERVAL '14 days' +AND partner.birthdate <= CURRENT_DATE - INTERVAL '14 days' +``` + +Die bestehende Wartezeit von `party.created_at <= NOW() - INTERVAL '1 day'` +bleibt unverändert. Erfüllt ein Paar die Altersregel noch nicht, bleibt es +verlobt; die bereits angelegte Feier wird nicht verworfen und kann nach Erreichen +des Alters vollzogen werden. + +### 2. Eheliche Schwangerschaften und Geburten + +Jeder automatische Empfängnis- oder Geburtenkandidat muss beide Bedingungen +erfüllen: + +```sql +mother.birthdate <= CURRENT_DATE - INTERVAL '16 days' +AND father.birthdate <= CURRENT_DATE - INTERVAL '16 days' +``` + +Das gilt sowohl für die Zufallslogik als auch für einen geplanten +`pregnancy_due_at`-Pfad. Beim geplanten Pfad soll der Daemon die Felder nicht +leeren, solange mindestens ein Elternteil noch unter 16 ist; die Schwangerschaft +wird erst verarbeitet, sobald beide die Grenze erreicht haben. Admin-Tools +können weiterhin ein bewusstes, separat protokolliertes Override anbieten. + +### 3. Tests für den Daemon + +- 13/13 Jahre, verlobt, Hochzeitsfeier älter als 24 Stunden: bleibt `engaged`. +- 14/14 Jahre, verlobt, Hochzeitsfeier älter als 24 Stunden: wird `married`. +- Verheiratet, ein Elternteil 15: keine automatische Schwangerschaft/Geburt. +- Verheiratet, beide 16: normaler Schwangerschafts-/Geburtspfad ist möglich. + +## Bestehende Daten + +Es ist keine Migration nötig. Bereits bestehende Ehen bleiben bestehen; die +Grenzen steuern ausschließlich neue Übergänge und automatische Kinderereignisse. diff --git a/frontend/public/images/falukant/avatar/female00-medieval.png b/frontend/public/images/falukant/avatar/female00-medieval.png new file mode 100644 index 0000000..5fe617f Binary files /dev/null and b/frontend/public/images/falukant/avatar/female00-medieval.png differ diff --git a/frontend/public/images/falukant/avatar/female01-medieval.png b/frontend/public/images/falukant/avatar/female01-medieval.png new file mode 100644 index 0000000..990838d Binary files /dev/null and b/frontend/public/images/falukant/avatar/female01-medieval.png differ diff --git a/frontend/public/images/falukant/avatar/female02-medieval.png b/frontend/public/images/falukant/avatar/female02-medieval.png new file mode 100644 index 0000000..17f0286 Binary files /dev/null and b/frontend/public/images/falukant/avatar/female02-medieval.png differ diff --git a/frontend/public/images/falukant/avatar/male00-medieval.png b/frontend/public/images/falukant/avatar/male00-medieval.png new file mode 100644 index 0000000..dd334f6 Binary files /dev/null and b/frontend/public/images/falukant/avatar/male00-medieval.png differ diff --git a/frontend/public/images/falukant/avatar/male01-medieval.png b/frontend/public/images/falukant/avatar/male01-medieval.png new file mode 100644 index 0000000..5e2b471 Binary files /dev/null and b/frontend/public/images/falukant/avatar/male01-medieval.png differ diff --git a/frontend/public/images/falukant/avatar/male02-medieval.png b/frontend/public/images/falukant/avatar/male02-medieval.png new file mode 100644 index 0000000..62b6f6b Binary files /dev/null and b/frontend/public/images/falukant/avatar/male02-medieval.png differ diff --git a/frontend/src/components/AppSectionBar.vue b/frontend/src/components/AppSectionBar.vue index 1fc6aa9..67d4d44 100755 --- a/frontend/src/components/AppSectionBar.vue +++ b/frontend/src/components/AppSectionBar.vue @@ -68,6 +68,7 @@ const TITLE_MAP = { 'Flirt settings': 'sectionBar.titles.flirtSettings', 'Account settings': 'sectionBar.titles.accountSettings', 'Language assistant settings': 'sectionBar.titles.languageAssistantSettings', + 'Falukant settings': 'sectionBar.titles.falukantSettings', Interests: 'sectionBar.titles.interests', AdminInterests: 'sectionBar.titles.adminInterests', AdminUsers: 'sectionBar.titles.adminUsers', diff --git a/frontend/src/components/SettingsWidget.vue b/frontend/src/components/SettingsWidget.vue index 0f72fce..07dd4c4 100755 --- a/frontend/src/components/SettingsWidget.vue +++ b/frontend/src/components/SettingsWidget.vue @@ -4,51 +4,51 @@ { return { value: option.id, - captionTr: `settings.personal.${fieldName}.${option.value}` + captionTr: this.settingOptionTr(fieldName, option.value) } }); }, + settingLabelTr(fieldName) { + return this.resolveSettingTr([ + `settings.${this.settingsType}.label.${fieldName}`, + `settings.personal.label.${fieldName}` + ]); + }, + settingTooltipTr(fieldName, immutable) { + if (immutable) { + return this.$t('settings.immutable.tooltip'); + } + return this.resolveSettingTr([ + `settings.${this.settingsType}.tooltip.${fieldName}`, + `settings.personal.tooltip.${fieldName}` + ]); + }, + settingOptionTr(fieldName, optionValue) { + return this.resolveSettingTr([ + `settings.${this.settingsType}.${fieldName}.${optionValue}`, + `settings.personal.${fieldName}.${optionValue}` + ]); + }, + resolveSettingTr(candidates) { + for (const key of candidates) { + if (this.$te(key)) { + return key; + } + } + return candidates[0]; + }, async handleInput(settingId, value) { if (['object', 'array'].includes(typeof value)) { return; @@ -232,7 +261,7 @@ export default { } const fieldNames = immutableFields.map(field => - this.$t(`settings.personal.label.${field.name}`) + this.$t(this.settingLabelTr(field.name)) ).join(', '); return this.$t('settings.immutable.supportMessage.specific', { fields: fieldNames }); diff --git a/frontend/src/components/falukant/DirectorInfo.vue b/frontend/src/components/falukant/DirectorInfo.vue index c8f17da..eb178e1 100755 --- a/frontend/src/components/falukant/DirectorInfo.vue +++ b/frontend/src/components/falukant/DirectorInfo.vue @@ -9,6 +9,16 @@
+
+ +

{{ $t('falukant.titles.' + director.character.gender + '.' + director.character.nobleTitle.labelTr) }} {{ director.character.definedFirstName.name }} {{ director.character.definedLastName.name }} @@ -192,7 +202,9 @@ + + diff --git a/frontend/src/i18n/locales/ceb/general.json b/frontend/src/i18n/locales/ceb/general.json index 0496d6a..ee4125b 100755 --- a/frontend/src/i18n/locales/ceb/general.json +++ b/frontend/src/i18n/locales/ceb/general.json @@ -173,6 +173,7 @@ "flirtSettings": "Flirt", "accountSettings": "Account", "languageAssistantSettings": "Katabang sa pinulongan", + "falukantSettings": "Falukant", "interests": "Mga interes", "adminInterests": "Pagdumala sa interes", "adminUsers": "Mga user", diff --git a/frontend/src/i18n/locales/ceb/settings.json b/frontend/src/i18n/locales/ceb/settings.json index 8220660..3c92060 100755 --- a/frontend/src/i18n/locales/ceb/settings.json +++ b/frontend/src/i18n/locales/ceb/settings.json @@ -138,6 +138,26 @@ "view": { "title": "Panagway" }, + "falukant": { + "title": "Falukant", + "label": { + "falukantPortraitStyle": "Estilo sa portrait", + "falukantFigureMode": "Paagi sa pagpakita" + }, + "tooltip": { + "falukantPortraitStyle": "Pilia ang estilo sa mga portrait sa Falukant.", + "falukantFigureMode": "Pilia kung portraits, 3D nga mga hulagway, o pareho ang ipakita." + }, + "falukantPortraitStyle": { + "classic": "Klasiko", + "medieval": "Medyebal" + }, + "falukantFigureMode": { + "portraits": "Portraits lang", + "3d": "3D nga mga hulagway lang", + "both": "Portraits ug 3D nga mga hulagway" + } + }, "sexuality": { "title": "Sekswalidad" }, diff --git a/frontend/src/i18n/locales/de/general.json b/frontend/src/i18n/locales/de/general.json index a127bf9..939a679 100755 --- a/frontend/src/i18n/locales/de/general.json +++ b/frontend/src/i18n/locales/de/general.json @@ -173,6 +173,7 @@ "flirtSettings": "Flirt", "accountSettings": "Account", "languageAssistantSettings": "Sprachassistent", + "falukantSettings": "Falukant", "interests": "Interessen", "adminInterests": "Interessenverwaltung", "adminUsers": "Benutzer", diff --git a/frontend/src/i18n/locales/de/settings.json b/frontend/src/i18n/locales/de/settings.json index b0265f4..f6ac07f 100755 --- a/frontend/src/i18n/locales/de/settings.json +++ b/frontend/src/i18n/locales/de/settings.json @@ -138,6 +138,26 @@ "view": { "title": "Aussehen" }, + "falukant": { + "title": "Falukant", + "label": { + "falukantPortraitStyle": "Portrait-Stil", + "falukantFigureMode": "Darstellung" + }, + "tooltip": { + "falukantPortraitStyle": "Wähle den Stil der Falukant-Portraits.", + "falukantFigureMode": "Lege fest, ob Portraits, 3D-Figuren oder beides angezeigt werden sollen." + }, + "falukantPortraitStyle": { + "classic": "Klassisch", + "medieval": "Mittelalterlich" + }, + "falukantFigureMode": { + "portraits": "Nur Portraits", + "3d": "Nur 3D-Figuren", + "both": "Portraits und 3D-Figuren" + } + }, "sexuality": { "title": "Sexualität" }, diff --git a/frontend/src/i18n/locales/en/general.json b/frontend/src/i18n/locales/en/general.json index 181a94d..e1c17be 100755 --- a/frontend/src/i18n/locales/en/general.json +++ b/frontend/src/i18n/locales/en/general.json @@ -173,6 +173,7 @@ "flirtSettings": "Flirt", "accountSettings": "Account", "languageAssistantSettings": "Language assistant", + "falukantSettings": "Falukant", "interests": "Interests", "adminInterests": "Interest administration", "adminUsers": "Users", diff --git a/frontend/src/i18n/locales/en/settings.json b/frontend/src/i18n/locales/en/settings.json index 6bff9d3..02bf141 100755 --- a/frontend/src/i18n/locales/en/settings.json +++ b/frontend/src/i18n/locales/en/settings.json @@ -138,6 +138,26 @@ "view": { "title": "Appearance" }, + "falukant": { + "title": "Falukant", + "label": { + "falukantPortraitStyle": "Portrait style", + "falukantFigureMode": "Display mode" + }, + "tooltip": { + "falukantPortraitStyle": "Choose the style for Falukant portraits.", + "falukantFigureMode": "Choose whether Falukant should show portraits, 3D characters, or both." + }, + "falukantPortraitStyle": { + "classic": "Classic", + "medieval": "Medieval" + }, + "falukantFigureMode": { + "portraits": "Portraits only", + "3d": "3D characters only", + "both": "Portraits and 3D characters" + } + }, "sexuality": { "title": "Sexuality" }, diff --git a/frontend/src/i18n/locales/es/general.json b/frontend/src/i18n/locales/es/general.json index b0b78fe..7a7795c 100755 --- a/frontend/src/i18n/locales/es/general.json +++ b/frontend/src/i18n/locales/es/general.json @@ -173,6 +173,7 @@ "flirtSettings": "Coqueteo", "accountSettings": "Cuenta", "languageAssistantSettings": "Asistente de idiomas", + "falukantSettings": "Falukant", "interests": "Intereses", "adminInterests": "Administración de intereses", "adminUsers": "Usuarios", diff --git a/frontend/src/i18n/locales/es/settings.json b/frontend/src/i18n/locales/es/settings.json index f168042..1e09766 100755 --- a/frontend/src/i18n/locales/es/settings.json +++ b/frontend/src/i18n/locales/es/settings.json @@ -138,6 +138,26 @@ "view": { "title": "Apariencia" }, + "falukant": { + "title": "Falukant", + "label": { + "falukantPortraitStyle": "Estilo del retrato", + "falukantFigureMode": "Modo de visualización" + }, + "tooltip": { + "falukantPortraitStyle": "Elige el estilo de los retratos de Falukant.", + "falukantFigureMode": "Elige si Falukant debe mostrar retratos, figuras 3D o ambos." + }, + "falukantPortraitStyle": { + "classic": "Clásico", + "medieval": "Medieval" + }, + "falukantFigureMode": { + "portraits": "Solo retratos", + "3d": "Solo figuras 3D", + "both": "Retratos y figuras 3D" + } + }, "sexuality": { "title": "Sexualidad" }, diff --git a/frontend/src/i18n/locales/fr/general.json b/frontend/src/i18n/locales/fr/general.json index c9cd8ea..2a702eb 100755 --- a/frontend/src/i18n/locales/fr/general.json +++ b/frontend/src/i18n/locales/fr/general.json @@ -173,6 +173,7 @@ "flirtSettings": "flirter", "accountSettings": "compte", "languageAssistantSettings": "Assistant vocal", + "falukantSettings": "Falukant", "interests": "Interessen", "adminInterests": "Gestion des intérêts", "adminUsers": "Benutzer", diff --git a/frontend/src/i18n/locales/fr/settings.json b/frontend/src/i18n/locales/fr/settings.json index a60e34c..4e9f32d 100755 --- a/frontend/src/i18n/locales/fr/settings.json +++ b/frontend/src/i18n/locales/fr/settings.json @@ -138,6 +138,26 @@ "view": { "title": "Regarder" }, + "falukant": { + "title": "Falukant", + "label": { + "falukantPortraitStyle": "Style de portrait", + "falukantFigureMode": "Mode d'affichage" + }, + "tooltip": { + "falukantPortraitStyle": "Choisissez le style des portraits Falukant.", + "falukantFigureMode": "Choisissez si Falukant doit afficher des portraits, des personnages 3D ou les deux." + }, + "falukantPortraitStyle": { + "classic": "Classique", + "medieval": "Médiéval" + }, + "falukantFigureMode": { + "portraits": "Portraits uniquement", + "3d": "Personnages 3D uniquement", + "both": "Portraits et personnages 3D" + } + }, "sexuality": { "title": "sexualité" }, diff --git a/frontend/src/router/settingsRoutes.js b/frontend/src/router/settingsRoutes.js index 84640ba..8c29876 100755 --- a/frontend/src/router/settingsRoutes.js +++ b/frontend/src/router/settingsRoutes.js @@ -5,6 +5,7 @@ const SexualitySettingsView = () => import('../views/settings/SexualityView.vue' const AccountSettingsView = () => import('../views/settings/AccountView.vue'); const InterestsView = () => import('../views/settings/InterestsView.vue'); const LanguageAssistantView = () => import('../views/settings/LanguageAssistantView.vue'); +const FalukantSettingsView = () => import('../views/settings/FalukantView.vue'); const settingsRoutes = [ { @@ -49,6 +50,12 @@ const settingsRoutes = [ component: LanguageAssistantView, meta: { requiresAuth: true } }, + { + path: '/settings/falukant', + name: 'Falukant settings', + component: FalukantSettingsView, + meta: { requiresAuth: true } + }, ]; export default settingsRoutes; diff --git a/frontend/src/utils/falukantVisualSettings.js b/frontend/src/utils/falukantVisualSettings.js new file mode 100644 index 0000000..b58f3ac --- /dev/null +++ b/frontend/src/utils/falukantVisualSettings.js @@ -0,0 +1,60 @@ +import apiClient from '@/utils/axios.js'; + +export const FALUKANT_VISUAL_DEFAULTS = Object.freeze({ + portraitStyle: 'classic', + figureMode: 'both', +}); + +export function showFalukantPortraits(figureMode) { + return figureMode === 'portraits' || figureMode === 'both'; +} + +export function showFalukant3dFigures(figureMode) { + return figureMode === '3d' || figureMode === 'both'; +} + +export function resolveFalukantPortraitAtlas(genderRaw, portraitStyle) { + const gender = String(genderRaw || 'male').toLowerCase() === 'female' ? 'female' : 'male'; + + if (portraitStyle === 'medieval') { + // Falukant liefert hier noch keine Haarfarbe; bis dahin geschlechtsbasierter Atlas-Fallback. + return gender === 'female' + ? '/images/falukant/avatar/female00-medieval.png' + : '/images/falukant/avatar/male02-medieval.png'; + } + + return `/images/falukant/avatar/${gender}.png`; +} + +export async function loadFalukantVisualSettings(userId) { + if (!userId) { + return { ...FALUKANT_VISUAL_DEFAULTS }; + } + + try { + const { data } = await apiClient.post('/api/settings/filter', { + userid: userId, + type: 'falukant' + }); + + const values = { ...FALUKANT_VISUAL_DEFAULTS }; + for (const setting of Array.isArray(data) ? data : []) { + const selected = Array.isArray(setting.options) + ? setting.options.find((option) => String(option.id) === String(setting.value)) + : null; + const normalizedValue = selected?.value || null; + + if (setting.name === 'falukantPortraitStyle' && normalizedValue) { + values.portraitStyle = normalizedValue; + } + if (setting.name === 'falukantFigureMode' && normalizedValue) { + values.figureMode = normalizedValue; + } + } + + return values; + } catch (error) { + console.error('Error loading Falukant visual settings:', error); + return { ...FALUKANT_VISUAL_DEFAULTS }; + } +} diff --git a/frontend/src/views/falukant/BranchView.vue b/frontend/src/views/falukant/BranchView.vue index b02b021..0e881d3 100755 --- a/frontend/src/views/falukant/BranchView.vue +++ b/frontend/src/views/falukant/BranchView.vue @@ -80,6 +80,7 @@ :branchId="selectedBranch.id" :vehicles="vehicles" :branches="branches" + :visual-settings="visualSettings" ref="directorInfo" @transportCreated="handleTransportCreated" /> @@ -386,6 +387,10 @@ import BuyVehicleDialog from '@/dialogues/falukant/BuyVehicleDialog.vue'; import apiClient from '@/utils/axios.js'; import { mapState } from 'vuex'; import { showError, showSuccess, showApiError } from '@/utils/feedback.js'; +import { + FALUKANT_VISUAL_DEFAULTS, + loadFalukantVisualSettings, +} from '@/utils/falukantVisualSettings.js'; const CERTIFICATE_PRODUCT_LEVELS = [ { level: 1, products: ['fish', 'meat', 'leather', 'wood', 'stone', 'milk', 'cheese', 'bread', 'wheat', 'grain', 'carrot'] }, @@ -476,6 +481,7 @@ export default { inDebtorsPrison: false }, pendingBranchRefresh: null, + visualSettings: { ...FALUKANT_VISUAL_DEFAULTS }, }; }, @@ -528,6 +534,7 @@ export default { }, async mounted() { + this.visualSettings = await loadFalukantVisualSettings(this.user?.id); await this.loadBranches(); const branchId = this.$route.params.branchId; diff --git a/frontend/src/views/falukant/ChurchView.vue b/frontend/src/views/falukant/ChurchView.vue index 23348d6..bea2cf3 100755 --- a/frontend/src/views/falukant/ChurchView.vue +++ b/frontend/src/views/falukant/ChurchView.vue @@ -41,10 +41,23 @@ {{ $t(`falukant.church.baptism.gender.${person.gender}`) }} - - +
+ +
+ + +
+
{{ person.age }} @@ -76,8 +89,21 @@ {{ pos.region.name }} - {{ $t(`falukant.titles.${pos.character.gender}.${pos.character.title || 'noncivil'}`) }} - {{ pos.character.name }} +
+ +
+ {{ $t(`falukant.titles.${pos.character.gender}.${pos.character.title || 'noncivil'}`) }} + {{ pos.character.name }} +
+
@@ -187,7 +213,12 @@ import MessageDialog from '@/dialogues/standard/MessageDialog.vue' import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue' import apiClient from '@/utils/axios.js' import SimpleTabs from '@/components/SimpleTabs.vue' +import FalukantCharacterVisual from '@/components/falukant/FalukantCharacterVisual.vue' import { mapState } from 'vuex' +import { + FALUKANT_VISUAL_DEFAULTS, + loadFalukantVisualSettings, +} from '@/utils/falukantVisualSettings.js' export default { name: 'ChurchView', @@ -196,6 +227,7 @@ export default { MessageDialog, ErrorDialog, SimpleTabs, + FalukantCharacterVisual, }, data() { return { @@ -215,7 +247,8 @@ export default { current: false, available: false, applications: false - } + }, + visualSettings: { ...FALUKANT_VISUAL_DEFAULTS }, } }, computed: { @@ -236,6 +269,7 @@ export default { } }, async mounted() { + this.visualSettings = await loadFalukantVisualSettings(this.user?.id); await this.loadNotBaptisedChildren(); await this.loadOwnCharacterId(); await Promise.all([ @@ -495,6 +529,21 @@ export default { } + + +