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();