Add new requirements for nobility titles and enhance service logic: Introduce checks for reputation, house position, house condition, office rank, and lover count in the FalukantService. Update title requirements in the initialization script to include these new criteria. Enhance localization for requirements in English, German, and Spanish, ensuring accurate translations for new conditions.

This commit is contained in:
Torsten Schulz (local)
2026-03-23 10:31:32 +01:00
parent b3607849d2
commit 80d8caee88
8 changed files with 763 additions and 33 deletions

View File

@@ -110,6 +110,33 @@ function calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent) {
return min + (max - min) * (knowledgeFactor / 100);
}
const POLITICAL_OFFICE_RANKS = {
assessor: 1,
councillor: 1,
council: 2,
beadle: 2,
'town-clerk': 2,
mayor: 3,
'master-builder': 2,
'village-major': 2,
judge: 3,
bailif: 3,
taxman: 2,
sheriff: 3,
consultant: 3,
treasurer: 4,
hangman: 2,
'territorial-council': 3,
'territorial-council-speaker': 4,
'ruler-consultant': 4,
'state-administrator': 4,
'super-state-administrator': 5,
governor: 5,
'ministry-helper': 4,
minister: 5,
chancellor: 6
};
async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
// Wenn worthPercent nicht übergeben wurde, hole es aus der Datenbank
if (worthPercent === null) {
@@ -5244,6 +5271,21 @@ class FalukantService extends BaseService {
case 'branches':
fulfilled = fulfilled && await this.checkBranchesRequirement(hashedUserId, requirement);
break;
case 'reputation':
fulfilled = fulfilled && await this.checkReputationRequirement(user, requirement);
break;
case 'house_position':
fulfilled = fulfilled && await this.checkHousePositionRequirement(user, requirement);
break;
case 'house_condition':
fulfilled = fulfilled && await this.checkHouseConditionRequirement(user, requirement);
break;
case 'office_rank_any':
fulfilled = fulfilled && await this.checkOfficeRankAnyRequirement(user, requirement);
break;
case 'lover_count_max':
fulfilled = fulfilled && await this.checkLoverCountMaxRequirement(user, requirement);
break;
default:
fulfilled = false;
};
@@ -5278,6 +5320,103 @@ class FalukantService extends BaseService {
return branchCount >= requirement.requirementValue;
}
async checkReputationRequirement(user, requirement) {
const character = user.character || await FalukantCharacter.findOne({
where: { userId: user.id },
attributes: ['reputation']
});
return Number(character?.reputation || 0) >= Number(requirement.requirementValue || 0);
}
async checkHousePositionRequirement(user, requirement) {
const house = await UserHouse.findOne({
where: { userId: user.id },
include: [{ model: HouseType, as: 'houseType', attributes: ['position'] }]
});
return Number(house?.houseType?.position || 0) >= Number(requirement.requirementValue || 0);
}
async checkHouseConditionRequirement(user, requirement) {
const house = await UserHouse.findOne({
where: { userId: user.id },
attributes: ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition']
});
if (!house) return false;
const averageCondition = (
Number(house.roofCondition || 0) +
Number(house.wallCondition || 0) +
Number(house.floorCondition || 0) +
Number(house.windowCondition || 0)
) / 4;
return averageCondition >= Number(requirement.requirementValue || 0);
}
async getHighestOfficeRankAny(userId) {
const character = await FalukantCharacter.findOne({
where: { userId },
attributes: ['id']
});
if (!character) return 0;
const [politicalOffices, politicalHistories, churchOffices] = 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']
}),
ChurchOffice.findAll({
where: { characterId: character.id },
include: [{ model: ChurchOfficeType, as: 'type', attributes: ['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));
return Math.max(0, ...politicalRanks, ...politicalHistoryRanks, ...churchRanks);
}
async checkOfficeRankAnyRequirement(user, requirement) {
const highestRank = await this.getHighestOfficeRankAny(user.id);
return highestRank >= Number(requirement.requirementValue || 0);
}
async checkLoverCountMaxRequirement(user, requirement) {
const character = user.character || await FalukantCharacter.findOne({
where: { userId: user.id },
attributes: ['id']
});
if (!character) return false;
const loverType = await RelationshipType.findOne({
where: { tr: 'lover' },
attributes: ['id']
});
if (!loverType) return true;
const loverRelationships = await Relationship.findAll({
where: {
character1Id: character.id,
relationshipTypeId: loverType.id
},
include: [{
model: RelationshipState,
as: 'state',
required: false
}]
});
const activeLoverCount = loverRelationships.filter((rel) => (rel.state?.active ?? true) !== false).length;
return activeLoverCount <= Number(requirement.requirementValue || 0);
}
async getHealth(hashedUserId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
const healthActivities = FalukantService.HEALTH_ACTIVITIES.map((activity) => { return { tr: activity.tr, cost: activity.cost } });

View File

@@ -0,0 +1,120 @@
-- PostgreSQL only
-- Ersetzt die Standesanforderungen fuer Falukant durch das erweiterte Profilmodell.
DELETE FROM falukant_type.title_requirement
WHERE title_id IN (
SELECT id
FROM falukant_type.title
WHERE label_tr IN (
'civil', 'sir', 'townlord', 'by', 'landlord', 'knight',
'baron', 'count', 'palsgrave', 'margrave', 'landgrave',
'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke',
'prince-regent', 'king'
)
);
INSERT INTO falukant_type.title_requirement (title_id, requirement_type, requirement_value)
SELECT tm.id, req.requirement_type, req.requirement_value
FROM (
SELECT id, label_tr
FROM falukant_type.title
WHERE label_tr IN (
'civil', 'sir', 'townlord', 'by', 'landlord', 'knight',
'baron', 'count', 'palsgrave', 'margrave', 'landgrave',
'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke',
'prince-regent', 'king'
)
) tm
JOIN (
VALUES
('civil', 'money', 5000::numeric),
('civil', 'cost', 500::numeric),
('sir', 'branches', 2::numeric),
('sir', 'cost', 1000::numeric),
('townlord', 'cost', 3000::numeric),
('townlord', 'money', 12000::numeric),
('townlord', 'reputation', 18::numeric),
('by', 'cost', 5000::numeric),
('by', 'money', 18000::numeric),
('by', 'house_position', 1::numeric),
('landlord', 'cost', 7500::numeric),
('landlord', 'money', 26000::numeric),
('landlord', 'reputation', 24::numeric),
('landlord', 'house_condition', 60::numeric),
('knight', 'cost', 11000::numeric),
('knight', 'money', 38000::numeric),
('knight', 'office_rank_any', 1::numeric),
('baron', 'branches', 4::numeric),
('baron', 'cost', 16000::numeric),
('baron', 'money', 55000::numeric),
('baron', 'house_position', 2::numeric),
('count', 'cost', 23000::numeric),
('count', 'money', 80000::numeric),
('count', 'reputation', 32::numeric),
('count', 'house_condition', 68::numeric),
('palsgrave', 'cost', 32000::numeric),
('palsgrave', 'money', 115000::numeric),
('palsgrave', 'office_rank_any', 2::numeric),
('palsgrave', 'house_position', 3::numeric),
('margrave', 'cost', 45000::numeric),
('margrave', 'money', 165000::numeric),
('margrave', 'reputation', 40::numeric),
('margrave', 'house_condition', 72::numeric),
('margrave', 'lover_count_max', 2::numeric),
('landgrave', 'cost', 62000::numeric),
('landgrave', 'money', 230000::numeric),
('landgrave', 'office_rank_any', 3::numeric),
('landgrave', 'house_position', 4::numeric),
('ruler', 'cost', 85000::numeric),
('ruler', 'money', 320000::numeric),
('ruler', 'reputation', 48::numeric),
('ruler', 'house_condition', 76::numeric),
('elector', 'cost', 115000::numeric),
('elector', 'money', 440000::numeric),
('elector', 'office_rank_any', 4::numeric),
('elector', 'house_position', 5::numeric),
('elector', 'lover_count_max', 2::numeric),
('imperial-prince', 'cost', 155000::numeric),
('imperial-prince', 'money', 600000::numeric),
('imperial-prince', 'reputation', 56::numeric),
('imperial-prince', 'house_condition', 80::numeric),
('duke', 'cost', 205000::numeric),
('duke', 'money', 820000::numeric),
('duke', 'office_rank_any', 5::numeric),
('duke', 'house_position', 6::numeric),
('grand-duke', 'cost', 270000::numeric),
('grand-duke', 'money', 1120000::numeric),
('grand-duke', 'reputation', 64::numeric),
('grand-duke', 'house_condition', 84::numeric),
('grand-duke', 'lover_count_max', 1::numeric),
('prince-regent', 'cost', 360000::numeric),
('prince-regent', 'money', 1520000::numeric),
('prince-regent', 'office_rank_any', 6::numeric),
('prince-regent', 'house_position', 7::numeric),
('king', 'cost', 500000::numeric),
('king', 'money', 2100000::numeric),
('king', 'reputation', 72::numeric),
('king', 'house_position', 8::numeric),
('king', 'house_condition', 88::numeric),
('king', 'lover_count_max', 1::numeric)
) AS req(label_tr, requirement_type, requirement_value)
ON req.label_tr = tm.label_tr
ON CONFLICT (title_id, requirement_type)
DO UPDATE SET requirement_value = EXCLUDED.requirement_value;

View File

@@ -299,22 +299,22 @@ async function initializeFalukantTitleRequirements() {
const titleRequirements = [
{ labelTr: "civil", requirements: [{ type: "money", value: 5000 }, { type: "cost", value: 500 }] },
{ labelTr: "sir", requirements: [{ type: "branches", value: 2 }, { type: "cost", value: 1000 }] },
{ labelTr: "townlord", requirements: [{ type: "cost", value: 3000 }] },
{ labelTr: "by", requirements: [{ type: "cost", value: 6000 }] },
{ labelTr: "landlord", requirements: [{ type: "cost", value: 9000 }] },
{ labelTr: "knight", requirements: [{ type: "cost", value: 11000 }] },
{ labelTr: "baron", requirements: [{ type: "branches", value: 4 }, { type: "cost", value: 15000 }] },
{ labelTr: "count", requirements: [{ type: "cost", value: 19000 }] },
{ labelTr: "palsgrave", requirements: [{ type: "cost", value: 25000 }] },
{ labelTr: "margrave", requirements: [{ type: "cost", value: 33000 }] },
{ labelTr: "landgrave", requirements: [{ type: "cost", value: 47000 }] },
{ labelTr: "ruler", requirements: [{ type: "cost", value: 66000 }] },
{ labelTr: "elector", requirements: [{ type: "cost", value: 79000 }] },
{ labelTr: "imperial-prince", requirements: [{ type: "cost", value: 99999 }] },
{ labelTr: "duke", requirements: [{ type: "cost", value: 130000 }] },
{ labelTr: "grand-duke",requirements: [{ type: "cost", value: 170000 }] },
{ labelTr: "prince-regent", requirements: [{ type: "cost", value: 270000 }] },
{ labelTr: "king", requirements: [{ type: "cost", value: 500000 }] },
{ labelTr: "townlord", requirements: [{ type: "cost", value: 3000 }, { type: "money", value: 12000 }, { type: "reputation", value: 18 }] },
{ labelTr: "by", requirements: [{ type: "cost", value: 5000 }, { type: "money", value: 18000 }, { type: "house_position", value: 1 }] },
{ labelTr: "landlord", requirements: [{ type: "cost", value: 7500 }, { type: "money", value: 26000 }, { type: "reputation", value: 24 }, { type: "house_condition", value: 60 }] },
{ labelTr: "knight", requirements: [{ type: "cost", value: 11000 }, { type: "money", value: 38000 }, { type: "office_rank_any", value: 1 }] },
{ 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: "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: "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: "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 }] },
];
const titles = await TitleOfNobility.findAll();
@@ -325,13 +325,6 @@ async function initializeFalukantTitleRequirements() {
const title = titles.find(t => t.labelTr === titleReq.labelTr);
if (!title) continue;
if (i > 1) {
titleReq.requirements.push({
type: "money",
value: 5000 * Math.pow(3, i - 1),
});
}
for (const req of titleReq.requirements) {
requirementsToInsert.push({
titleId: title.id,
@@ -341,6 +334,7 @@ async function initializeFalukantTitleRequirements() {
}
}
await TitleRequirement.destroy({ where: {} });
await TitleRequirement.bulkCreate(requirementsToInsert, { ignoreDuplicates: true });
}

View File

@@ -0,0 +1,431 @@
# Falukant: Sozialstatus / Standesaufstieg Erweiterte Spezifikation
## 1. Ziel
Der Aufstieg im Sozialstatus soll ab den höheren Ständen nicht mehr nur von Geld und einzelnen festen Anforderungen abhängen, sondern von einer Mischung aus gesellschaftlicher Stellung, öffentlicher Wahrnehmung und repräsentativem Lebensstandard.
Gleichzeitig soll das System:
- die frühen Aufstiege einfach halten
- spätere Aufstiege spürbar schwieriger machen
- mehr Geldbindung erzeugen
- nicht bei jedem Stand dieselben Faktoren verlangen
## 2. Grundprinzip
Der Standesaufstieg bleibt ein aktiver Spielzug des Nutzers.
Das heißt:
- der Spieler beantragt den nächsten Stand weiterhin aktiv in der UI
- das Backend prüft die Voraussetzungen
- bei Erfolg wird der Titel erhöht und die Kosten werden abgezogen
Neu ist:
- spätere Titel haben einen größeren und variableren Anforderungskatalog
- nicht jeder Titel prüft alle möglichen Faktoren
- pro nächstem Titel wird nur eine Auswahl relevanter Faktoren verlangt
- Kosten und Schwellen steigen schwach exponentiell
## 3. Bestehender Zustand
Aktuell:
- der nächste Titel wird über `level + 1` bestimmt
- Anforderungen kommen aus `TitleRequirement`
- geprüft werden bisher vor allem:
- `money`
- `cost`
- `branches`
- Aufstieg läuft manuell über `POST /api/falukant/nobility`
- Cooldown: 7 Tage
Das bleibt als technisches Grundmuster erhalten.
## 4. Fachliche Leitentscheidung
### 4.1 Frühe Titel
- Erster Aufstieg:
- darf weiterhin direkt kaufbar sein
- keine komplexen sozialen Bedingungen nötig
- Zweiter Aufstieg:
- bleibt wie bisher
- keine Änderung nötig
### 4.2 Ab dem dritten relevanten Standessprung
Ab dann sollen zusätzliche Faktoren einbezogen werden:
- höchstes bisheriges politisches Amt
- höchstes bisheriges kirchliches Amt
- Beliebtheit / Ansehen
- derzeitiges Haus
- Hauszustand
- Anzahl Liebhaber / Mätressen
Wichtig:
- nicht jeder spätere Stand nutzt alle Faktoren gleichzeitig
- pro Titel wird nur eine Auswahl davon aktiv
- dadurch bleibt das System lebendig statt schematisch
## 5. Neue Einflussfaktoren
## 5.1 Höchstes bisheriges politisches Amt
Nicht nur das aktuelle Amt zählt, sondern das höchste jemals gehaltene politische Amt.
Begründung:
- frühere Machtstellung bleibt gesellschaftlich wirksam
- ehemalige Amtsinhaber profitieren weiter vom Status ihrer Laufbahn
Empfohlene Auswertung:
- pro `political_office_type.name` existiert ein Rangwert
- für den Spieler zählt das Maximum aus aktiven und historischen politischen Ämtern
## 5.2 Höchstes bisheriges kirchliches Amt
Analog zur Politik:
- nicht nur aktuell besetzte Kirchenämter
- sondern höchstes jemals erreiches Kirchenamt
Bevorzugt anhand von:
- `church_office_type.hierarchy_level`
## 5.3 Beliebtheit / Ansehen
Der Stand soll nicht nur gekauft oder verwaltet, sondern auch öffentlich getragen werden.
Dafür wird genutzt:
- `character.reputation`
Das ist bewusst derselbe soziale Hauptwert, der auch an anderen Stellen bereits funktioniert.
## 5.4 Derzeitiges Haus
Das aktuelle Haus ist sichtbarer Ausdruck des Standes.
Relevanter Wert:
- `house.position`
Je höher das Haus, desto plausibler ein höherer sozialer Aufstieg.
## 5.5 Hauszustand
Nicht nur Hausgröße, auch sein Zustand zählt.
Zu berücksichtigen:
- Dach
- Wände
- Boden
- Fenster
Empfohlener abgeleiteter Wert:
- `houseConditionAverage = AVG(roofCondition, wallCondition, floorCondition, windowCondition)`
## 5.6 Anzahl Liebhaber / Mätressen
Dieser Faktor ist bewusst nicht rein negativ.
Er soll je nach Stand unterschiedlich gewertet werden:
- niedrige und mittlere Stände:
- viele offene Nebenbeziehungen schaden
- höhere Stände:
- eine gepflegte repräsentative Nebenbeziehung kann toleriert oder sogar sozial passend wirken
- zu viele Beziehungen bleiben aber auch dort schädlich
Darum soll die Anforderung nicht als starres „je mehr, desto besser/schlechter“ funktionieren, sondern titelabhängig.
Empfohlene Grundlage:
- aktive Beziehungen mit Rollen:
- `secret_affair`
- `lover`
- `mistress_or_favorite`
## 6. Titelabhängige Anforderungssets
## 6.1 Keine starre Vollprüfung
Ab den späteren Ständen wird pro nächstem Titel nicht alles geprüft, sondern ein Set aus:
- Pflichtfaktoren
- Auswahlfaktoren
### Pflichtfaktoren
Immer:
- `cost`
Je nach Stand meistens auch:
- Mindestansehen oder Hausniveau
### Auswahlfaktoren
Aus einem Pool von:
- Politik
- Kirche
- Ansehen
- Hausposition
- Hauszustand
- Liebhaber-/Mätressensituation
- ggf. weiter weiterhin Niederlassungen
## 6.2 Titelprofil statt Zufall pro Klick
Die Auswahl soll nicht bei jedem Aufruf neu würfeln.
Stattdessen:
- jeder Zieltitel hat ein fest definiertes Profil
- dieses Profil wirkt aber so, als ob nicht immer dieselben gesellschaftlichen Dinge zählen
Beispiel:
- Titel A prüft:
- Geld
- Ansehen
- Hausposition
- Titel B prüft:
- Geld
- politisches oder kirchliches Spitzenamt
- Hauszustand
- Titel C prüft:
- Geld
- Ansehen
- Haus
- kontrollierte repräsentative Nebenbeziehung
Das ist besser als echter Zufall, weil:
- nachvollziehbar
- testbar
- balancierbar
## 7. Schwellenlogik
## 7.1 Schwach exponentiell steigend
Die Anforderungen sollen nicht linear, sondern schwach exponentiell steigen.
Das betrifft vor allem:
- Kosten
- Mindestansehen
- Mindesthausniveau
- Mindestwert für Amtseinfluss
Empfohlene Denkweise:
- frühe Stände: leicht erreichbar
- mittlere Stände: merklich teurer
- hohe Stände: deutlich selektiver, aber nicht absurd
## 7.2 Beispielhafte Entwicklung
### Früh
- Geld dominiert
- kaum oder keine sozialen Zusatzbedingungen
### Mittel
- Geld plus 1 bis 2 soziale Bedingungen
### Hoch
- Geld plus 2 bis 3 soziale Bedingungen
- mindestens eine Repräsentationsbedingung:
- Haus oder Hauszustand
- mindestens eine Anerkennungsbedingung:
- Ansehen oder Amt
### Sehr hoch
- Geld plus 3 bis 4 Bedingungen
- politische oder kirchliche Laufbahn gewinnt an Gewicht
- Haus und Ansehen werden praktisch unverzichtbar
## 8. Faktorlogik im Detail
## 8.1 Politik / Kirche
Für spätere Stände genügt nicht jedes Amt.
Empfohlen:
- niedrige hohe Titel:
- irgendein relevantes Amt reicht
- spätere hohe Titel:
- nur hohe Ränge zählen
Regel:
- erfüllt, wenn `maxPoliticalRank` oder `maxChurchRank` über Titel-Schwelle liegt
## 8.2 Beliebtheit
Empfohlen:
- frühe soziale Titel: moderate Rufschwelle
- hohe Titel: Ruf wird Pflichtfaktor
## 8.3 Haus
Empfohlen:
- `house.position` wird ab mittleren Ständen wichtig
- `houseConditionAverage` wird ab späteren Ständen zusätzlich relevant
Das verhindert:
- „großes Haus, aber verwahrlost“
- oder „hoher Titel im ruinösen Haushalt“
## 8.4 Liebhaber / Mätressen
Dieser Faktor soll nur bei manchen Titeln überhaupt aktiv sein.
Mögliche Logik:
- niedrige/mittlere Stände:
- `0` oder `1` toleriert
- `>= 2` negativ
- höhere Stände:
- genau `1` repräsentative Nebenbeziehung kann neutral oder positiv sein
- `0` ist ebenfalls zulässig
- `>= 2` oder hohe Sichtbarkeit negativ
Das eignet sich besonders als optionaler Profilfaktor, nicht als Universalregel.
## 9. Vorgeschlagene Aufstiegsarchitektur
## 9.1 Stufe 1
- direkt kaufbar
- nur Kosten
## 9.2 Stufe 2
- bleibt wie bisher
## 9.3 Ab Stufe 3
Jeder Zieltitel bekommt:
- `baseCost`
- `costExponentFactor`
- `requiredChecks`
- `optionalCheckPool`
- `optionalCheckCount`
Die Prüfung lautet dann:
1. Kosten erfüllt
2. alle Pflichtchecks erfüllt
3. aus dem Auswahlpool mindestens `optionalCheckCount` erfüllt
Damit bleibt das System flexibel, aber klar.
## 10. UI-Auswirkung
Die Adel-/Standesansicht sollte nicht nur „fehlend/erfüllt“ zeigen, sondern künftig:
- aktive Pflichtanforderungen
- optionale Faktoren
- wie viele davon erfüllt werden müssen
- bereits erfüllte Faktoren optisch markieren
Beispiel:
- „Erfülle 2 von 3 gesellschaftlichen Voraussetzungen“
Dadurch versteht der Nutzer:
- warum der Aufstieg noch blockiert ist
- wo er am effizientesten investieren kann
## 11. Geldbindung
Mehr Geldinvestition soll ausdrücklich Teil des Systems sein.
Darum:
- jeder höhere Stand hat eine klar steigende Aufstiegskostenbasis
- Hauspflege und Hausgröße binden zusätzlich Kapital
- politische und kirchliche Karriere kostet indirekt ebenfalls Ressourcen
- repräsentative Liebhaber-/Mätressenführung kann bei manchen Profilen als teure, aber hilfreiche soziale Form auftreten
## 12. Verhältnis zu Daemon und Echtzeit
Der Standesaufstieg selbst bleibt weiterhin eine direkte Backend-Prüfung beim Spieler-Klick, nicht ein Daily-Daemon-Aufstieg.
Der Daemon kann aber vorbereitende Werte beeinflussen:
- Ruf
- Hauszustand
- aktive Amtshistorie
- Beziehungen / Sichtbarkeit
Das heißt:
- der Daemon verändert die Voraussetzungen
- der eigentliche Standesaufstieg bleibt ein aktiver Kauf-/Antragsvorgang
## 13. Empfohlene technische Erweiterung
Die aktuelle reine `TitleRequirement`-Logik ist für das erweiterte Modell zu schmal.
Empfohlen ist eine zusätzliche Titelprofil-Logik im Backend:
- je Titel ein Profilobjekt mit:
- Pflichtfaktoren
- Auswahlfaktoren
- Mindestanzahl erfüllter Auswahlfaktoren
- Kostenbasis
- Progressionsfaktor
Dabei kann das bestehende Requirements-Modell weiterhin für einfache Titel dienen.
## 14. Umsetzungsreihenfolge
### Phase 1
- ersten Aufstieg unverändert kaufbar lassen
- zweiten Aufstieg unverändert lassen
- ab späteren Titeln Backend-Profilprüfung einführen
### Phase 2
- UI um Pflicht-/Optionsanzeige erweitern
- soziale Faktoren sichtbar machen
### Phase 3
- Balancing
- feinere Titelprofile
- stärkere Verzahnung mit Politik, Kirche und Liebschaften
## 15. Kernaussage
Das System soll nicht „jeder Titel verlangt alles“ sein, sondern:
- frühe Aufstiege simpel
- spätere Aufstiege gesellschaftlich glaubwürdig
- steigende Kosten
- wechselnde, aber definierte Faktorprofile
- Haus, Ruf, Amt und Nebenbeziehungen werden echte Standeswerkzeuge

View File

@@ -925,7 +925,12 @@
"requirement": {
"money": "Vermögen mindestens {amount}",
"cost": "Kosten: {amount}",
"branches": "Mindestens {amount} Niederlassungen"
"branches": "Mindestens {amount} Niederlassungen",
"reputation": "Beliebtheit mindestens {amount}",
"house_position": "Hausstand mindestens Stufe {amount}",
"house_condition": "Hauszustand mindestens {amount}",
"office_rank_any": "Höchstes politisches oder kirchliches Amt mindestens Rang {amount}",
"lover_count_max": "Höchstens {amount} Liebhaber oder Mätressen"
},
"advance": {
"confirm": "Aufsteigen beantragen"

View File

@@ -331,6 +331,16 @@
}
},
"nobility": {
"requirement": {
"money": "Wealth at least {amount}",
"cost": "Cost: {amount}",
"branches": "At least {amount} branches",
"reputation": "Popularity at least {amount}",
"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}",
"lover_count_max": "At most {amount} lovers or favorites"
},
"cooldown": "You can only advance again on {date}."
},
"mood": {

View File

@@ -891,7 +891,12 @@
"requirement": {
"money": "Patrimonio mínimo {amount}",
"cost": "Coste: {amount}",
"branches": "Al menos {amount} sucursales"
"branches": "Al menos {amount} sucursales",
"reputation": "Popularidad mínima {amount}",
"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}",
"lover_count_max": "Como máximo {amount} amantes o favoritos"
},
"advance": {
"confirm": "Solicitar ascenso"

View File

@@ -25,7 +25,7 @@
</p>
<ul class="prerequisites" v-if="next.requirements && next.requirements.length > 0">
<li v-for="req in next.requirements" :key="req.titleId">
{{ $t(`falukant.nobility.requirement.${req.requirementType}`, { amount: formatCost(req.requirementValue) }) }}
{{ formatRequirement(req.requirementType, req.requirementValue) }}
</li>
</ul>
<button v-if="canAdvance" @click="applyAdvance" class="button" :disabled="isAdvancing">
@@ -133,12 +133,7 @@
this.$root.$refs.errorDialog?.open(retryStr ? `${msg}${this.$t('falukant.nobility.cooldown', { date: retryStr })}` : msg);
} else if (resp.data?.message === 'nobilityRequirements') {
const unmet = resp.data?.unmet || [];
const items = unmet.map(u => {
if (u.type === 'money') return this.$t('falukant.nobility.requirement.money', { amount: this.formatCost(u.required) });
if (u.type === 'cost') return this.$t('falukant.nobility.requirement.cost', { amount: this.formatCost(u.required) });
if (u.type === 'branches') return this.$t('falukant.nobility.requirement.branches', { amount: u.required });
return `${u.type}: ${u.required}`;
}).join('\n');
const items = unmet.map(u => this.formatRequirement(u.type, u.required)).join('\n');
const base = this.$t('falukant.nobility.errors.unmet');
this.$root.$refs.errorDialog?.open(`${base}\n${items}`);
} else {
@@ -159,6 +154,37 @@
formatCost(val) {
return new Intl.NumberFormat(navigator.language, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(val);
},
formatRequirement(type, rawValue) {
const numericValue = Number(rawValue);
const amount = ['money', 'cost'].includes(type)
? this.formatCost(numericValue)
: rawValue;
const key = `falukant.nobility.requirement.${type}`;
const translated = this.$t(key, { amount });
if (translated && translated !== key) {
return translated;
}
switch (type) {
case 'money':
return `Vermögen mindestens ${amount}`;
case 'cost':
return `Kosten: ${amount}`;
case 'branches':
return `Mindestens ${amount} Niederlassungen`;
case 'reputation':
return `Beliebtheit mindestens ${amount}`;
case 'house_position':
return `Hausstand mindestens Stufe ${amount}`;
case 'house_condition':
return `Hauszustand mindestens ${amount}`;
case 'office_rank_any':
return `Höchstes politisches oder kirchliches Amt mindestens Rang ${amount}`;
case 'lover_count_max':
return `Höchstens ${amount} Liebhaber oder Mätressen`;
default:
return `${type}: ${amount}`;
}
},
formatDate(isoString) {
const d = new Date(isoString);
const now = new Date();