From ce36315b58a19b9e2878c2bf04d9819720ee0ab4 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 23 Mar 2026 10:47:54 +0100 Subject: [PATCH] Enhance NobilityView with new house position and condition formatting: Introduce methods to format house position labels and house condition descriptions based on numeric values. Update requirement translations to utilize these new methods for improved clarity and localization. --- backend/services/falukantService.js | 127 ++++++++++++++---- .../update_nobility_requirements_extended.sql | 12 +- .../falukant/initializeFalukantPredefines.js | 8 +- frontend/src/i18n/locales/de/falukant.json | 5 + frontend/src/i18n/locales/en/falukant.json | 5 + frontend/src/i18n/locales/es/falukant.json | 5 + frontend/src/views/falukant/NobilityView.vue | 55 +++++++- 7 files changed, 181 insertions(+), 36 deletions(-) diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index e64b38f..dae4501 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -5223,22 +5223,28 @@ class FalukantService extends BaseService { nextAdvanceAt = next.toISOString(); } const currentTitleLevel = nobility.level; - const nextTitle = await TitleOfNobility.findOne({ - where: { - level: currentTitleLevel + 1 - }, - include: [ - { - model: TitleRequirement, - as: 'requirements', - } - ], - attributes: ['labelTr'] - }); + const [nextTitle, highestPoliticalOffice, highestOfficeAny] = await Promise.all([ + TitleOfNobility.findOne({ + where: { + level: currentTitleLevel + 1 + }, + include: [ + { + model: TitleRequirement, + as: 'requirements', + } + ], + attributes: ['labelTr'] + }), + this.getHighestPoliticalOfficeInfo(falukantUser.id), + this.getHighestOfficeAnyInfo(falukantUser.id) + ]); return { current: nobility, next: nextTitle, - nextAdvanceAt + nextAdvanceAt, + highestPoliticalOffice, + highestOfficeAny }; } @@ -5283,9 +5289,15 @@ class FalukantService extends BaseService { case 'office_rank_any': fulfilled = fulfilled && await this.checkOfficeRankAnyRequirement(user, requirement); break; + case 'office_rank_political': + fulfilled = fulfilled && await this.checkOfficeRankPoliticalRequirement(user, requirement); + break; case 'lover_count_max': fulfilled = fulfilled && await this.checkLoverCountMaxRequirement(user, requirement); break; + case 'lover_count_min': + fulfilled = fulfilled && await this.checkLoverCountMinRequirement(user, requirement); + break; default: fulfilled = false; }; @@ -5351,12 +5363,46 @@ class FalukantService extends BaseService { return averageCondition >= Number(requirement.requirementValue || 0); } - async getHighestOfficeRankAny(userId) { + async getHighestPoliticalOfficeInfo(userId) { const character = await FalukantCharacter.findOne({ where: { userId }, attributes: ['id'] }); - if (!character) return 0; + if (!character) return { rank: 0, name: null }; + + const [politicalOffices, politicalHistories] = await Promise.all([ + PoliticalOffice.findAll({ + where: { characterId: character.id }, + include: [{ model: PoliticalOfficeType, as: 'officeType', attributes: ['name'] }], + attributes: ['officeTypeId'] + }), + PoliticalOfficeHistory.findAll({ + where: { characterId: character.id }, + include: [{ model: PoliticalOfficeType, as: 'officeTypeHistory', attributes: ['name'] }], + attributes: ['officeTypeId'] + }) + ]); + + const candidates = [ + ...politicalOffices.map((office) => ({ + rank: POLITICAL_OFFICE_RANKS[office.officeType?.name] || 0, + name: office.officeType?.name || null + })), + ...politicalHistories.map((history) => ({ + rank: POLITICAL_OFFICE_RANKS[history.officeTypeHistory?.name] || 0, + name: history.officeTypeHistory?.name || null + })) + ].sort((a, b) => b.rank - a.rank); + + return candidates[0] || { rank: 0, name: null }; + } + + async getHighestOfficeAnyInfo(userId) { + const character = await FalukantCharacter.findOne({ + where: { userId }, + attributes: ['id'] + }); + if (!character) return { rank: 0, name: null, source: null }; const [politicalOffices, politicalHistories, churchOffices] = await Promise.all([ PoliticalOffice.findAll({ @@ -5371,16 +5417,35 @@ class FalukantService extends BaseService { }), ChurchOffice.findAll({ where: { characterId: character.id }, - include: [{ model: ChurchOfficeType, as: 'type', attributes: ['hierarchyLevel'] }], + include: [{ model: ChurchOfficeType, as: 'type', attributes: ['name', 'hierarchyLevel'] }], attributes: ['officeTypeId'] }) ]); - const politicalRanks = politicalOffices.map((office) => POLITICAL_OFFICE_RANKS[office.officeType?.name] || 0); - const politicalHistoryRanks = politicalHistories.map((history) => POLITICAL_OFFICE_RANKS[history.officeTypeHistory?.name] || 0); - const churchRanks = churchOffices.map((office) => Number(office.type?.hierarchyLevel || 0)); + const candidates = [ + ...politicalOffices.map((office) => ({ + rank: POLITICAL_OFFICE_RANKS[office.officeType?.name] || 0, + name: office.officeType?.name || null, + source: 'political' + })), + ...politicalHistories.map((history) => ({ + rank: POLITICAL_OFFICE_RANKS[history.officeTypeHistory?.name] || 0, + name: history.officeTypeHistory?.name || null, + source: 'political' + })), + ...churchOffices.map((office) => ({ + rank: Number(office.type?.hierarchyLevel || 0), + name: office.type?.name || null, + source: 'church' + })) + ].sort((a, b) => b.rank - a.rank); - return Math.max(0, ...politicalRanks, ...politicalHistoryRanks, ...churchRanks); + return candidates[0] || { rank: 0, name: null, source: null }; + } + + async getHighestOfficeRankAny(userId) { + const highest = await this.getHighestOfficeAnyInfo(userId); + return Number(highest?.rank || 0); } async checkOfficeRankAnyRequirement(user, requirement) { @@ -5388,18 +5453,33 @@ class FalukantService extends BaseService { return highestRank >= Number(requirement.requirementValue || 0); } + async checkOfficeRankPoliticalRequirement(user, requirement) { + const highest = await this.getHighestPoliticalOfficeInfo(user.id); + return Number(highest?.rank || 0) >= Number(requirement.requirementValue || 0); + } + async checkLoverCountMaxRequirement(user, requirement) { + const activeLoverCount = await this.getActiveLoverCount(user); + return activeLoverCount <= Number(requirement.requirementValue || 0); + } + + async checkLoverCountMinRequirement(user, requirement) { + const activeLoverCount = await this.getActiveLoverCount(user); + return activeLoverCount >= Number(requirement.requirementValue || 0); + } + + async getActiveLoverCount(user) { const character = user.character || await FalukantCharacter.findOne({ where: { userId: user.id }, attributes: ['id'] }); - if (!character) return false; + if (!character) return 0; const loverType = await RelationshipType.findOne({ where: { tr: 'lover' }, attributes: ['id'] }); - if (!loverType) return true; + if (!loverType) return 0; const loverRelationships = await Relationship.findAll({ where: { @@ -5413,8 +5493,7 @@ class FalukantService extends BaseService { }] }); - const activeLoverCount = loverRelationships.filter((rel) => (rel.state?.active ?? true) !== false).length; - return activeLoverCount <= Number(requirement.requirementValue || 0); + return loverRelationships.filter((rel) => (rel.state?.active ?? true) !== false).length; } async getHealth(hashedUserId) { diff --git a/backend/sql/update_nobility_requirements_extended.sql b/backend/sql/update_nobility_requirements_extended.sql index b18b375..0b29176 100644 --- a/backend/sql/update_nobility_requirements_extended.sql +++ b/backend/sql/update_nobility_requirements_extended.sql @@ -69,7 +69,7 @@ JOIN ( ('margrave', 'money', 165000::numeric), ('margrave', 'reputation', 40::numeric), ('margrave', 'house_condition', 72::numeric), - ('margrave', 'lover_count_max', 2::numeric), + ('margrave', 'lover_count_min', 1::numeric), ('landgrave', 'cost', 62000::numeric), ('landgrave', 'money', 230000::numeric), @@ -83,9 +83,9 @@ JOIN ( ('elector', 'cost', 115000::numeric), ('elector', 'money', 440000::numeric), - ('elector', 'office_rank_any', 4::numeric), + ('elector', 'office_rank_political', 4::numeric), ('elector', 'house_position', 5::numeric), - ('elector', 'lover_count_max', 2::numeric), + ('elector', 'lover_count_max', 3::numeric), ('imperial-prince', 'cost', 155000::numeric), ('imperial-prince', 'money', 600000::numeric), @@ -101,7 +101,8 @@ JOIN ( ('grand-duke', 'money', 1120000::numeric), ('grand-duke', 'reputation', 64::numeric), ('grand-duke', 'house_condition', 84::numeric), - ('grand-duke', 'lover_count_max', 1::numeric), + ('grand-duke', 'lover_count_min', 1::numeric), + ('grand-duke', 'lover_count_max', 3::numeric), ('prince-regent', 'cost', 360000::numeric), ('prince-regent', 'money', 1520000::numeric), @@ -113,7 +114,8 @@ JOIN ( ('king', 'reputation', 72::numeric), ('king', 'house_position', 8::numeric), ('king', 'house_condition', 88::numeric), - ('king', 'lover_count_max', 1::numeric) + ('king', 'lover_count_min', 1::numeric), + ('king', 'lover_count_max', 4::numeric) ) AS req(label_tr, requirement_type, requirement_value) ON req.label_tr = tm.label_tr ON CONFLICT (title_id, requirement_type) diff --git a/backend/utils/falukant/initializeFalukantPredefines.js b/backend/utils/falukant/initializeFalukantPredefines.js index 15056b3..690ba6f 100644 --- a/backend/utils/falukant/initializeFalukantPredefines.js +++ b/backend/utils/falukant/initializeFalukantPredefines.js @@ -306,15 +306,15 @@ async function initializeFalukantTitleRequirements() { { labelTr: "baron", requirements: [{ type: "branches", value: 4 }, { type: "cost", value: 16000 }, { type: "money", value: 55000 }, { type: "house_position", value: 2 }] }, { labelTr: "count", requirements: [{ type: "cost", value: 23000 }, { type: "money", value: 80000 }, { type: "reputation", value: 32 }, { type: "house_condition", value: 68 }] }, { labelTr: "palsgrave", requirements: [{ type: "cost", value: 32000 }, { type: "money", value: 115000 }, { type: "office_rank_any", value: 2 }, { type: "house_position", value: 3 }] }, - { labelTr: "margrave", requirements: [{ type: "cost", value: 45000 }, { type: "money", value: 165000 }, { type: "reputation", value: 40 }, { type: "house_condition", value: 72 }, { type: "lover_count_max", value: 2 }] }, + { labelTr: "margrave", requirements: [{ type: "cost", value: 45000 }, { type: "money", value: 165000 }, { type: "reputation", value: 40 }, { type: "house_condition", value: 72 }, { type: "lover_count_min", value: 1 }] }, { labelTr: "landgrave", requirements: [{ type: "cost", value: 62000 }, { type: "money", value: 230000 }, { type: "office_rank_any", value: 3 }, { type: "house_position", value: 4 }] }, { labelTr: "ruler", requirements: [{ type: "cost", value: 85000 }, { type: "money", value: 320000 }, { type: "reputation", value: 48 }, { type: "house_condition", value: 76 }] }, - { labelTr: "elector", requirements: [{ type: "cost", value: 115000 }, { type: "money", value: 440000 }, { type: "office_rank_any", value: 4 }, { type: "house_position", value: 5 }, { type: "lover_count_max", value: 2 }] }, + { labelTr: "elector", requirements: [{ type: "cost", value: 115000 }, { type: "money", value: 440000 }, { type: "office_rank_political", value: 4 }, { type: "house_position", value: 5 }, { type: "lover_count_max", value: 3 }] }, { labelTr: "imperial-prince", requirements: [{ type: "cost", value: 155000 }, { type: "money", value: 600000 }, { type: "reputation", value: 56 }, { type: "house_condition", value: 80 }] }, { labelTr: "duke", requirements: [{ type: "cost", value: 205000 }, { type: "money", value: 820000 }, { type: "office_rank_any", value: 5 }, { type: "house_position", value: 6 }] }, - { labelTr: "grand-duke",requirements: [{ type: "cost", value: 270000 }, { type: "money", value: 1120000 }, { type: "reputation", value: 64 }, { type: "house_condition", value: 84 }, { type: "lover_count_max", value: 1 }] }, + { labelTr: "grand-duke",requirements: [{ type: "cost", value: 270000 }, { type: "money", value: 1120000 }, { type: "reputation", value: 64 }, { type: "house_condition", value: 84 }, { type: "lover_count_min", value: 1 }, { type: "lover_count_max", value: 3 }] }, { labelTr: "prince-regent", requirements: [{ type: "cost", value: 360000 }, { type: "money", value: 1520000 }, { type: "office_rank_any", value: 6 }, { type: "house_position", value: 7 }] }, - { labelTr: "king", requirements: [{ type: "cost", value: 500000 }, { type: "money", value: 2100000 }, { type: "reputation", value: 72 }, { type: "house_position", value: 8 }, { type: "house_condition", value: 88 }, { type: "lover_count_max", value: 1 }] }, + { labelTr: "king", requirements: [{ type: "cost", value: 500000 }, { type: "money", value: 2100000 }, { type: "reputation", value: 72 }, { type: "house_position", value: 8 }, { type: "house_condition", value: 88 }, { type: "lover_count_min", value: 1 }, { type: "lover_count_max", value: 4 }] }, ]; const titles = await TitleOfNobility.findAll(); diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json index bb3a9c2..3d09e78 100644 --- a/frontend/src/i18n/locales/de/falukant.json +++ b/frontend/src/i18n/locales/de/falukant.json @@ -921,6 +921,9 @@ "overview": "Übersicht", "advance": "Erweitern" }, + "highestPoliticalOffice": "Höchstes politisches Amt", + "highestOfficeAny": "Höchstes Amt insgesamt", + "none": "keines", "nextTitle": "Nächster möglicher Titel", "requirement": { "money": "Vermögen mindestens {amount}", @@ -930,6 +933,8 @@ "house_position": "Hausstand mindestens Stufe {amount}", "house_condition": "Hauszustand mindestens {amount}", "office_rank_any": "Höchstes politisches oder kirchliches Amt mindestens Rang {amount}", + "office_rank_political": "Höchstes politisches Amt mindestens Rang {amount}", + "lover_count_min": "Mindestens {amount} Liebhaber oder Mätressen", "lover_count_max": "Höchstens {amount} Liebhaber oder Mätressen" }, "advance": { diff --git a/frontend/src/i18n/locales/en/falukant.json b/frontend/src/i18n/locales/en/falukant.json index a5ad5c0..1a67465 100644 --- a/frontend/src/i18n/locales/en/falukant.json +++ b/frontend/src/i18n/locales/en/falukant.json @@ -331,6 +331,9 @@ } }, "nobility": { + "highestPoliticalOffice": "Highest political office", + "highestOfficeAny": "Highest office overall", + "none": "none", "requirement": { "money": "Wealth at least {amount}", "cost": "Cost: {amount}", @@ -339,6 +342,8 @@ "house_position": "House status at least level {amount}", "house_condition": "House condition at least {amount}", "office_rank_any": "Highest political or church office at least rank {amount}", + "office_rank_political": "Highest political office at least rank {amount}", + "lover_count_min": "At least {amount} lovers or favorites", "lover_count_max": "At most {amount} lovers or favorites" }, "cooldown": "You can only advance again on {date}." diff --git a/frontend/src/i18n/locales/es/falukant.json b/frontend/src/i18n/locales/es/falukant.json index 15ba086..321abce 100644 --- a/frontend/src/i18n/locales/es/falukant.json +++ b/frontend/src/i18n/locales/es/falukant.json @@ -887,6 +887,9 @@ "overview": "Resumen", "advance": "Ascender" }, + "highestPoliticalOffice": "Cargo político más alto", + "highestOfficeAny": "Cargo más alto en total", + "none": "ninguno", "nextTitle": "Siguiente título posible", "requirement": { "money": "Patrimonio mínimo {amount}", @@ -896,6 +899,8 @@ "house_position": "Casa al menos nivel {amount}", "house_condition": "Estado de la casa al menos {amount}", "office_rank_any": "Cargo político o eclesiástico más alto al menos rango {amount}", + "office_rank_political": "Cargo político más alto al menos rango {amount}", + "lover_count_min": "Al menos {amount} amantes o favoritos", "lover_count_max": "Como máximo {amount} amantes o favoritos" }, "advance": { diff --git a/frontend/src/views/falukant/NobilityView.vue b/frontend/src/views/falukant/NobilityView.vue index 0624300..fa876fd 100644 --- a/frontend/src/views/falukant/NobilityView.vue +++ b/frontend/src/views/falukant/NobilityView.vue @@ -13,6 +13,14 @@ {{ $t(`falukant.titles.${gender}.${current.labelTr}`) }}

+

+ {{ $t('falukant.nobility.highestPoliticalOffice') }}: + {{ formatOfficeInfo(highestPoliticalOffice, 'political') }} +

+

+ {{ $t('falukant.nobility.highestOfficeAny') }}: + {{ formatOfficeInfo(highestOfficeAny, highestOfficeAny?.source) }} +

@@ -63,6 +71,8 @@ ], current: { labelTr: '', requirements: [], charactersWithNobleTitle: [] }, next: { labelTr: '', requirements: [] }, + highestPoliticalOffice: null, + highestOfficeAny: null, nextAdvanceAt: null, isAdvancing: false }; @@ -111,6 +121,8 @@ const { data } = await apiClient.get('/api/falukant/nobility'); this.current = data.current || { labelTr: '', requirements: [], charactersWithNobleTitle: [] }; this.next = data.next || { labelTr: '', requirements: [] }; + this.highestPoliticalOffice = data.highestPoliticalOffice || null; + this.highestOfficeAny = data.highestOfficeAny || null; this.nextAdvanceAt = data.nextAdvanceAt || null; } catch (err) { console.error('Error loading nobility:', err); @@ -161,7 +173,7 @@ : rawValue; const key = `falukant.nobility.requirement.${type}`; const translated = this.$t(key, { amount }); - if (translated && translated !== key) { + if (translated && translated !== key && !['house_position', 'house_condition'].includes(type)) { return translated; } switch (type) { @@ -174,17 +186,54 @@ case 'reputation': return `Beliebtheit mindestens ${amount}`; case 'house_position': - return `Hausstand mindestens Stufe ${amount}`; + return `Hausstand mindestens ${this.getHousePositionLabel(numericValue)}`; case 'house_condition': - return `Hauszustand mindestens ${amount}`; + return `Hauszustand mindestens ${this.formatHouseCondition(numericValue)}`; case 'office_rank_any': return `Höchstes politisches oder kirchliches Amt mindestens Rang ${amount}`; + case 'office_rank_political': + return `Höchstes politisches Amt mindestens Rang ${amount}`; + case 'lover_count_min': + return `Mindestens ${amount} Liebhaber oder Mätressen`; case 'lover_count_max': return `Höchstens ${amount} Liebhaber oder Mätressen`; default: return `${type}: ${amount}`; } }, + formatOfficeInfo(info, source) { + if (!info?.name) { + return this.$t('falukant.nobility.none'); + } + const baseKey = source === 'church' ? 'falukant.church.offices' : 'falukant.politics.positions'; + const label = this.$te(`${baseKey}.${info.name}`) ? this.$t(`${baseKey}.${info.name}`) : info.name; + return `${label} (Rang ${info.rank})`; + }, + getHousePositionLabel(position) { + const labels = { + 1: 'Unter der Brücke', + 2: 'eine Strohhütte', + 3: 'ein Holzhaus', + 4: 'ein Hinterhofzimmer', + 5: 'ein kleines Familienhaus', + 6: 'ein Stadthaus', + 7: 'eine Villa', + 8: 'ein Herrenhaus', + 9: 'ein Schloss' + }; + return labels[position] || `Haus-Stufe ${position}`; + }, + formatHouseCondition(value) { + if (Number.isNaN(value)) { + return value; + } + if (value >= 0.95) return 'nahezu makellos'; + if (value >= 0.9) return 'sehr gut'; + if (value >= 0.8) return 'gut'; + if (value >= 0.7) return 'ordentlich'; + if (value >= 0.6) return 'brauchbar'; + return `${Math.round(value * 100)} %`; + }, formatDate(isoString) { const d = new Date(isoString); const now = new Date();