Add TitleBenefit model and integrate benefits into FalukantService: Introduced TitleBenefit for managing title-related advantages, including tax exemptions and office eligibility. Updated service methods to utilize these benefits for character reputation, tax distribution, and allowed office types, enhancing gameplay mechanics and user experience.

This commit is contained in:
Torsten Schulz (local)
2026-02-06 09:50:31 +01:00
parent a60c6d173c
commit c7a05c3213
8 changed files with 235 additions and 26 deletions

View File

@@ -45,6 +45,7 @@ import FalukantStockType from './falukant/type/stock.js';
import Knowledge from './falukant/data/product_knowledge.js'; import Knowledge from './falukant/data/product_knowledge.js';
import ProductType from './falukant/type/product.js'; import ProductType from './falukant/type/product.js';
import TitleOfNobility from './falukant/type/title_of_nobility.js'; import TitleOfNobility from './falukant/type/title_of_nobility.js';
import TitleBenefit from './falukant/type/title_benefit.js';
import TitleRequirement from './falukant/type/title_requirement.js'; import TitleRequirement from './falukant/type/title_requirement.js';
import Branch from './falukant/data/branch.js'; import Branch from './falukant/data/branch.js';
import BranchType from './falukant/type/branch.js'; import BranchType from './falukant/type/branch.js';
@@ -352,6 +353,8 @@ export default function setupAssociations() {
TitleRequirement.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' }); TitleRequirement.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' });
TitleOfNobility.hasMany(TitleRequirement, { foreignKey: 'titleId', as: 'requirements' }); TitleOfNobility.hasMany(TitleRequirement, { foreignKey: 'titleId', as: 'requirements' });
TitleOfNobility.hasMany(TitleBenefit, { foreignKey: 'titleId', as: 'benefits' });
TitleBenefit.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' });
Branch.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' }); Branch.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasMany(Branch, { foreignKey: 'regionId', as: 'branches' }); RegionData.hasMany(Branch, { foreignKey: 'regionId', as: 'branches' });

View File

@@ -0,0 +1,41 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
/**
* Vorteile pro Stand (Adelstitel).
* benefit_type: 'tax_share' | 'tax_exempt' | 'office_eligibility' | 'free_party_type' | 'reputation_bonus'
* parameters: JSONB, z.B. { officeTypeNames: [] }, { partyTypeIds: [] }, { minPercent: 5, maxPercent: 15 }
*/
class TitleBenefit extends Model {}
TitleBenefit.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
titleId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'title_id'
},
benefitType: {
type: DataTypes.STRING,
allowNull: false,
field: 'benefit_type'
},
parameters: {
type: DataTypes.JSONB,
allowNull: true,
defaultValue: {}
}
}, {
sequelize,
modelName: 'TitleBenefit',
tableName: 'title_benefit',
schema: 'falukant_type',
timestamps: false,
underscored: true
});
export default TitleBenefit;

View File

@@ -51,6 +51,7 @@ import ProductType from './falukant/type/product.js';
import Knowledge from './falukant/data/product_knowledge.js'; import Knowledge from './falukant/data/product_knowledge.js';
import TitleRequirement from './falukant/type/title_requirement.js'; import TitleRequirement from './falukant/type/title_requirement.js';
import TitleOfNobility from './falukant/type/title_of_nobility.js'; import TitleOfNobility from './falukant/type/title_of_nobility.js';
import TitleBenefit from './falukant/type/title_benefit.js';
import BranchType from './falukant/type/branch.js'; import BranchType from './falukant/type/branch.js';
import Branch from './falukant/data/branch.js'; import Branch from './falukant/data/branch.js';
import Production from './falukant/data/production.js'; import Production from './falukant/data/production.js';
@@ -201,6 +202,7 @@ const models = {
ProductType, ProductType,
Knowledge, Knowledge,
TitleOfNobility, TitleOfNobility,
TitleBenefit,
TitleRequirement, TitleRequirement,
BranchType, BranchType,
Branch, Branch,

View File

@@ -10,6 +10,7 @@ import RegionType from '../models/falukant/type/region.js';
import FalukantStock from '../models/falukant/data/stock.js'; import FalukantStock from '../models/falukant/data/stock.js';
import FalukantStockType from '../models/falukant/type/stock.js'; import FalukantStockType from '../models/falukant/type/stock.js';
import TitleOfNobility from '../models/falukant/type/title_of_nobility.js'; import TitleOfNobility from '../models/falukant/type/title_of_nobility.js';
import TitleBenefit from '../models/falukant/type/title_benefit.js';
import Branch from '../models/falukant/data/branch.js'; import Branch from '../models/falukant/data/branch.js';
import BranchType from '../models/falukant/type/branch.js'; import BranchType from '../models/falukant/type/branch.js';
import Production from '../models/falukant/data/production.js'; import Production from '../models/falukant/data/production.js';
@@ -167,6 +168,7 @@ async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPe
async function getCumulativeTaxPercentWithExemptions(userId, regionId) { async function getCumulativeTaxPercentWithExemptions(userId, regionId) {
if (!regionId) return 0; if (!regionId) return 0;
if (await hasTitleTaxExempt(userId)) return 0;
// fetch user's political offices (active) and their region types // fetch user's political offices (active) and their region types
const offices = await PoliticalOffice.findAll({ const offices = await PoliticalOffice.findAll({
where: { userId }, where: { userId },
@@ -212,6 +214,81 @@ async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPe
return parseFloat(val) || 0; return parseFloat(val) || 0;
} }
/** Standesvorteil: Steuerbefreiung für bestimmte Titel */
async function hasTitleTaxExempt(falukantUserId) {
const char = await FalukantCharacter.findOne({ where: { userId: falukantUserId }, attributes: ['titleOfNobility'] });
if (!char?.titleOfNobility) return false;
const benefit = await TitleBenefit.findOne({ where: { titleId: char.titleOfNobility, benefitType: 'tax_exempt' } });
return !!benefit;
}
/**
* Oberster Stand einer Region bekommt die Steuereinnahmen; Aufteilung auf alle Mitglieder dieses Standes.
* Returns { recipientUserIds: number[], useTreasury: boolean }. useTreasury true = an TREASURY_FALUKANT_USER_ID zahlen.
*/
async function getTaxRecipientsForRegion(regionId) {
const treasuryId = process.env.TREASURY_FALUKANT_USER_ID ? parseInt(process.env.TREASURY_FALUKANT_USER_ID, 10) : null;
const chars = await FalukantCharacter.findAll({
where: { regionId },
attributes: ['userId', 'titleOfNobility'],
include: [{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['id', 'level'], required: true }]
});
if (!chars.length) return { recipientUserIds: [], useTreasury: true };
const maxLevel = Math.max(...chars.map(c => c.nobleTitle?.level ?? 0));
const topTitleId = chars.find(c => (c.nobleTitle?.level ?? 0) === maxLevel)?.nobleTitle?.id;
if (!topTitleId) return { recipientUserIds: [], useTreasury: true };
const hasTaxShare = await TitleBenefit.findOne({ where: { titleId: topTitleId, benefitType: 'tax_share' } });
if (!hasTaxShare) return { recipientUserIds: [], useTreasury: true };
const topCharUserIds = [...new Set(chars.filter(c => c.titleOfNobility === topTitleId).map(c => c.userId).filter(Boolean))];
if (!topCharUserIds.length) return { recipientUserIds: [], useTreasury: true };
return { recipientUserIds: topCharUserIds, useTreasury: false };
}
/** Standesvorteil: Welche politischen Ämter (officeType.name) darf dieser Titel besetzen? */
async function getAllowedOfficeTypeNamesByTitle(titleId) {
const benefits = await TitleBenefit.findAll({
where: { titleId, benefitType: 'office_eligibility' },
attributes: ['parameters']
});
const names = new Set();
for (const b of benefits) {
const arr = b.parameters?.officeTypeNames;
if (Array.isArray(arr)) arr.forEach(n => names.add(n));
}
return names;
}
/** Standesvorteil: Ist dieser Festtyp (partyTypeId oder partyType.tr) für diesen Titel kostenfrei? */
async function isPartyTypeFreeForTitle(titleId, partyTypeId, partyTypeTr) {
const benefits = await TitleBenefit.findAll({
where: { titleId, benefitType: 'free_party_type' },
attributes: ['parameters']
});
for (const b of benefits) {
const p = b.parameters || {};
const ids = p.partyTypeIds;
const trs = p.partyTypeLabelTrs || p.partyTypeTrs;
if (Array.isArray(ids) && ids.includes(partyTypeId)) return true;
if (Array.isArray(trs) && trs.includes(partyTypeTr)) return true;
}
return false;
}
/** Standesvorteil: Beliebtheits-Bonus 515 % (nur Anzeige, nicht gespeichert). Deterministisch pro Charakter. */
async function getDisplayReputation(character) {
const base = character?.reputation ?? 0;
const benefit = await TitleBenefit.findOne({
where: { titleId: character?.titleOfNobility, benefitType: 'reputation_bonus' },
attributes: ['parameters']
});
if (!benefit?.parameters) return base;
const minP = benefit.parameters.minPercent ?? 5;
const maxP = benefit.parameters.maxPercent ?? 15;
const range = Math.max(1, maxP - minP + 1);
const bonusPercent = minP + (Math.abs(character.id) % range);
return Math.min(100, Math.round(base * (1 + bonusPercent / 100)));
}
function calculateMarriageCost(titleOfNobility, age) { function calculateMarriageCost(titleOfNobility, age) {
const minTitle = 1; const minTitle = 1;
const adjustedTitle = titleOfNobility - minTitle + 1; const adjustedTitle = titleOfNobility - minTitle + 1;
@@ -494,7 +571,7 @@ class FalukantService extends BaseService {
{ {
model: FalukantCharacter, model: FalukantCharacter,
as: 'character', as: 'character',
attributes: ['id', 'birthdate', 'health', 'reputation'], attributes: ['id', 'birthdate', 'health', 'reputation', 'titleOfNobility'],
}, },
], ],
attributes: ['id', 'money'] attributes: ['id', 'money']
@@ -529,6 +606,10 @@ class FalukantService extends BaseService {
falukantUser.character.setDataValue('relationshipsAsCharacter2', rawRelsAs2.filter(r => typeMap[r.relationshipTypeId]).map(attachType)); falukantUser.character.setDataValue('relationshipsAsCharacter2', rawRelsAs2.filter(r => typeMap[r.relationshipTypeId]).map(attachType));
} }
if (falukantUser.character?.birthdate) falukantUser.character.setDataValue('age', calcAge(falukantUser.character.birthdate)); if (falukantUser.character?.birthdate) falukantUser.character.setDataValue('age', calcAge(falukantUser.character.birthdate));
if (falukantUser.character?.id) {
const displayRep = await getDisplayReputation(falukantUser.character);
falukantUser.character.setDataValue('reputationDisplay', displayRep);
}
// Aggregate status additions: children counts and unread notifications // Aggregate status additions: children counts and unread notifications
try { try {
@@ -1677,11 +1758,22 @@ class FalukantService extends BaseService {
const moneyResult = await updateFalukantUserMoney(user.id, net, `Product sale (net)`, user.id); const moneyResult = await updateFalukantUserMoney(user.id, net, `Product sale (net)`, user.id);
if (!moneyResult.success) throw new Error('Failed to update money for seller'); if (!moneyResult.success) throw new Error('Failed to update money for seller');
// Book tax to treasury (if configured) // Book tax: oberster Stand der Region oder Treasury
const treasuryId = process.env.TREASURY_FALUKANT_USER_ID; if (taxValue > 0) {
if (treasuryId && taxValue > 0) { const { recipientUserIds, useTreasury } = await getTaxRecipientsForRegion(branch.regionId);
const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), taxValue, `Sales tax (${cumulativeTax}%)`, user.id); if (!useTreasury && recipientUserIds.length > 0) {
if (!taxResult.success) throw new Error('Failed to update money for treasury'); const share = Math.round((taxValue / recipientUserIds.length) * 100) / 100;
for (const recipientId of recipientUserIds) {
const taxResult = await updateFalukantUserMoney(recipientId, share, `Steueranteil Region (Verkauf)`, user.id);
if (!taxResult.success) throw new Error('Failed to update money for tax recipient');
}
} else {
const treasuryId = process.env.TREASURY_FALUKANT_USER_ID;
if (treasuryId) {
const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), taxValue, `Sales tax (${cumulativeTax}%)`, user.id);
if (!taxResult.success) throw new Error('Failed to update money for treasury');
}
}
} }
let remaining = quantity; let remaining = quantity;
for (const inv of inventory) { for (const inv of inventory) {
@@ -1746,30 +1838,22 @@ class FalukantService extends BaseService {
}); });
if (!inventory.length) return { success: true, revenue: 0 }; if (!inventory.length) return { success: true, revenue: 0 };
let total = 0; let total = 0;
/** regionId -> total tax amount to distribute in that region */
const taxPerRegion = new Map();
for (const item of inventory) { for (const item of inventory) {
const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0; const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0;
const regionId = item.stock.branch.regionId; const regionId = item.stock.branch.regionId;
const pricePerUnit = await calcRegionalSellPrice(item.productType, knowledgeVal, regionId); const pricePerUnit = await calcRegionalSellPrice(item.productType, knowledgeVal, regionId);
const cumulativeTax = await getCumulativeTaxPercentWithExemptions(user.id, regionId); const cumulativeTax = await getCumulativeTaxPercentWithExemptions(falukantUser.id, regionId);
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
total += item.quantity * adjustedPricePerUnit;
await this.addSellItem(item.stock.branch.id, falukantUser.id, item.productType.id, item.quantity);
}
// compute tax per region (using cumulative tax per region) and aggregate
let totalTax = 0;
for (const item of inventory) {
const regionId = item.stock.branch.regionId;
const region = await RegionData.findOne({ where: { id: regionId } });
const cumulativeTax = await getCumulativeTaxPercent(regionId);
const pricePerUnit = await calcRegionalSellPrice(item.productType, item.productType.knowledges?.[0]?.knowledge || 0, regionId);
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100)); const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100; const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
const itemRevenue = item.quantity * adjustedPricePerUnit; const itemRevenue = item.quantity * adjustedPricePerUnit;
total += itemRevenue;
const itemTax = Math.round((itemRevenue * cumulativeTax / 100) * 100) / 100; const itemTax = Math.round((itemRevenue * cumulativeTax / 100) * 100) / 100;
totalTax += itemTax; if (itemTax > 0) taxPerRegion.set(regionId, (taxPerRegion.get(regionId) || 0) + itemTax);
await this.addSellItem(item.stock.branch.id, falukantUser.id, item.productType.id, item.quantity);
} }
const totalTax = [...taxPerRegion.values()].reduce((s, t) => s + t, 0);
const totalNet = Math.round((total - totalTax) * 100) / 100; const totalNet = Math.round((total - totalTax) * 100) / 100;
const moneyResult = await updateFalukantUserMoney( const moneyResult = await updateFalukantUserMoney(
@@ -1781,9 +1865,19 @@ class FalukantService extends BaseService {
if (!moneyResult.success) throw new Error('Failed to update money for seller'); if (!moneyResult.success) throw new Error('Failed to update money for seller');
const treasuryId = process.env.TREASURY_FALUKANT_USER_ID; const treasuryId = process.env.TREASURY_FALUKANT_USER_ID;
if (treasuryId && totalTax > 0) { for (const [regionId, regionTax] of taxPerRegion) {
const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), Math.round(totalTax * 100) / 100, `Sales tax (aggregate)`, falukantUser.id); if (regionTax <= 0) continue;
if (!taxResult.success) throw new Error('Failed to update money for treasury'); const { recipientUserIds, useTreasury } = await getTaxRecipientsForRegion(regionId);
if (!useTreasury && recipientUserIds.length > 0) {
const share = Math.round((regionTax / recipientUserIds.length) * 100) / 100;
for (const recipientId of recipientUserIds) {
const taxResult = await updateFalukantUserMoney(recipientId, share, `Steueranteil Region (Verkauf)`, falukantUser.id);
if (!taxResult.success) throw new Error('Failed to update money for tax recipient');
}
} else if (treasuryId) {
const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), Math.round(regionTax * 100) / 100, `Sales tax (aggregate)`, falukantUser.id);
if (!taxResult.success) throw new Error('Failed to update money for treasury');
}
} }
for (const item of inventory) { for (const item of inventory) {
await Inventory.destroy({ where: { id: item.id } }); await Inventory.destroy({ where: { id: item.id } });
@@ -3567,7 +3661,9 @@ class FalukantService extends BaseService {
throw new Error('Einige ausgewählte Adelstitel existieren nicht'); throw new Error('Einige ausgewählte Adelstitel existieren nicht');
} }
let cost = (ptype.cost || 0) + (music.cost || 0) + (banquette.cost || 0); const character = await FalukantCharacter.findOne({ where: { userId: falukantUser.id }, attributes: ['titleOfNobility'] });
const partyTypeCost = character && (await isPartyTypeFreeForTitle(character.titleOfNobility, ptype.id, ptype.tr)) ? 0 : (ptype.cost || 0);
let cost = partyTypeCost + (music.cost || 0) + (banquette.cost || 0);
cost += (50 / servantRatio - 1) * 1000; cost += (50 / servantRatio - 1) * 1000;
const nobilityCost = nobilities.reduce((sum, n) => sum + ((n.id ^ 5) * 1000), 0); const nobilityCost = nobilities.reduce((sum, n) => sum + ((n.id ^ 5) * 1000), 0);
cost += nobilityCost; cost += nobilityCost;
@@ -4483,7 +4579,13 @@ class FalukantService extends BaseService {
} }
] ]
}); });
const titleId = character.titleOfNobility ?? character.nobleTitle?.id;
const allowedOfficeNames = await getAllowedOfficeTypeNamesByTitle(titleId);
const result = openPositions const result = openPositions
.filter(election => {
if (allowedOfficeNames.size > 0 && !allowedOfficeNames.has(election.officeType?.name)) return false;
return true;
})
.filter(election => { .filter(election => {
const prereqs = election.officeType.prerequisites || []; const prereqs = election.officeType.prerequisites || [];
return prereqs.some(pr => { return prereqs.some(pr => {

View File

@@ -9,6 +9,7 @@ import PromotionalGiftMood from "../../models/falukant/predefine/promotional_gif
import { sequelize } from '../sequelize.js'; import { sequelize } from '../sequelize.js';
import HouseType from '../../models/falukant/type/house.js'; import HouseType from '../../models/falukant/type/house.js';
import TitleOfNobility from "../../models/falukant/type/title_of_nobility.js"; import TitleOfNobility from "../../models/falukant/type/title_of_nobility.js";
import TitleBenefit from "../../models/falukant/type/title_benefit.js";
import PartyType from "../../models/falukant/type/party.js"; import PartyType from "../../models/falukant/type/party.js";
import MusicType from "../../models/falukant/type/music.js"; import MusicType from "../../models/falukant/type/music.js";
import BanquetteType from "../../models/falukant/type/banquette.js"; import BanquetteType from "../../models/falukant/type/banquette.js";
@@ -39,7 +40,8 @@ export const initializeFalukantTypes = async () => {
// Adelstitel VOR Haustypen initialisieren // Adelstitel VOR Haustypen initialisieren
await initializeFalukantTitlesOfNobility(); await initializeFalukantTitlesOfNobility();
await initializeTitleBenefits();
await initializeFalukantHouseTypes(); await initializeFalukantHouseTypes();
await initializeFalukantPartyTypes(); await initializeFalukantPartyTypes();
await initializeFalukantMusicTypes(); await initializeFalukantMusicTypes();
@@ -1204,6 +1206,65 @@ export const initializeFalukantTitlesOfNobility = async () => {
} }
}; };
/** Standesvorteile: tax_share, tax_exempt, office_eligibility, free_party_type, reputation_bonus */
async function initializeTitleBenefits() {
const titles = await TitleOfNobility.findAll({ attributes: ['id', 'labelTr', 'level'] });
const byLabel = new Map(titles.map(t => [t.labelTr, t]));
const benefits = [];
// tax_share: oberster Stand einer Region bekommt Steuer Titel mit hohem level (z.B. ab count)
const taxShareTitles = ['count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'];
for (const label of taxShareTitles) {
const t = byLabel.get(label);
if (t) benefits.push({ titleId: t.id, benefitType: 'tax_share', parameters: {} });
}
// tax_exempt: z.B. noncivil, oder hohe Titel
const taxExemptTitles = ['noncivil', 'king', 'prince-regent'];
for (const label of taxExemptTitles) {
const t = byLabel.get(label);
if (t) benefits.push({ titleId: t.id, benefitType: 'tax_exempt', parameters: {} });
}
// office_eligibility: pro Titel eine Zeile mit allen erlaubten Ämtern (officeTypeNames)
const officeEligibility = [
{ label: 'assessor', titles: ['civil', 'sir', 'townlord', 'by', 'landlord', 'knight', 'baron', 'count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'] },
{ label: 'council', titles: ['sir', 'townlord', 'by', 'landlord', 'knight', 'baron', 'count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'] },
{ label: 'taxman', titles: ['townlord', 'by', 'landlord', 'knight', 'baron', 'count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'] },
{ label: 'chancellor', titles: ['king'] }
];
const titleToOffices = new Map();
for (const { label: officeName, titles: allowedTitles } of officeEligibility) {
for (const label of allowedTitles) {
const t = byLabel.get(label);
if (t) {
const list = titleToOffices.get(t.id) || [];
if (!list.includes(officeName)) list.push(officeName);
titleToOffices.set(t.id, list);
}
}
}
for (const [titleId, officeTypeNames] of titleToOffices) {
benefits.push({ titleId, benefitType: 'office_eligibility', parameters: { officeTypeNames } });
}
// free_party_type: z.B. "wedding" für civil+ (partyTypeIds oder labelTr)
const freePartyTitles = ['sir', 'townlord', 'by', 'landlord', 'knight', 'baron', 'count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'];
for (const label of freePartyTitles) {
const t = byLabel.get(label);
if (t) benefits.push({ titleId: t.id, benefitType: 'free_party_type', parameters: { partyTypeLabelTrs: ['wedding'] } });
}
// reputation_bonus: zufällig 515 % für ausgewählte Stände (hier: knight, count, duke)
const reputationBonusTitles = ['knight', 'count', 'duke'];
for (const label of reputationBonusTitles) {
const t = byLabel.get(label);
if (t) benefits.push({ titleId: t.id, benefitType: 'reputation_bonus', parameters: { minPercent: 5, maxPercent: 15 } });
}
for (const b of benefits) {
await TitleBenefit.findOrCreate({
where: { titleId: b.titleId, benefitType: b.benefitType },
defaults: { parameters: b.parameters }
});
}
console.log(`[Falukant] Standesvorteile (title_benefit) initialisiert: ${benefits.length} Einträge`);
}
const weatherTypes = [ const weatherTypes = [
{ tr: "sunny" }, { tr: "sunny" },
{ tr: "cloudy" }, { tr: "cloudy" },

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 B

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 599 B

After

Width:  |  Height:  |  Size: 582 B