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 });
}
@@ -359,4 +353,4 @@ async function initializeFalukantTownProductWorth() {
from \"falukant_type\".\"product\" ftp, \"falukant_data\".\"region\" fdr';
sequelize.query(deleteQuery);
sequelize.query(createQuery);
}
}