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:
@@ -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 } });
|
||||
|
||||
120
backend/sql/update_nobility_requirements_extended.sql
Normal file
120
backend/sql/update_nobility_requirements_extended.sql
Normal 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;
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -359,4 +353,4 @@ async function initializeFalukantTownProductWorth() {
|
||||
from \"falukant_type\".\"product\" ftp, \"falukant_data\".\"region\" fdr';
|
||||
sequelize.query(deleteQuery);
|
||||
sequelize.query(createQuery);
|
||||
}
|
||||
}
|
||||
|
||||
431
docs/FALUKANT_NOBILITY_ADVANCEMENT_SPEC.md
Normal file
431
docs/FALUKANT_NOBILITY_ADVANCEMENT_SPEC.md
Normal 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
|
||||
@@ -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"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user