feat(politics): add new political office benefits and enhance database migration
All checks were successful
Deploy to production / deploy (push) Successful in 2m54s

- Introduced new benefits including 'reputation_periodic', 'appoint_politicians', 'set_regional_tax', 'free_lover_slots', 'guard_protection', and 'court_immunity' to the political office system.
- Updated database migration to add and remove the 'last_political_daily_salary_on' column using SQL queries for better performance.
- Enhanced localization files for multiple languages to support new benefits, improving user experience across the application.
- Updated UI components to display new benefits correctly in the PoliticsView, ensuring accurate representation of political office functionalities.
This commit is contained in:
Torsten Schulz (local)
2026-04-02 15:27:05 +02:00
parent 49713d957d
commit 5d06d97737
10 changed files with 303 additions and 19 deletions

View File

@@ -2,17 +2,10 @@
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn(
{
tableName: 'falukant_user',
schema: 'falukant_data'
},
'last_political_daily_salary_on',
{
type: Sequelize.DATEONLY,
allowNull: true
}
);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.falukant_user
ADD COLUMN IF NOT EXISTS last_political_daily_salary_on date NULL;
`);
await queryInterface.sequelize.query(`
DO $$
@@ -51,13 +44,10 @@ module.exports = {
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn(
{
tableName: 'falukant_user',
schema: 'falukant_data'
},
'last_political_daily_salary_on'
);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.falukant_user
DROP COLUMN IF EXISTS last_political_daily_salary_on;
`);
await queryInterface.sequelize.query(`
DO $$

View File

@@ -6101,6 +6101,25 @@ class FalukantService extends BaseService {
if (amount > 0) {
out.push({ tr: 'daily_salary', params: { amount } });
}
} else if (tr === 'reputation_periodic' || (tr === 'reputation' && (v.intervalDays != null || v.everyDays != null))) {
const intervalDays = Math.max(1, Number(v.intervalDays ?? v.everyDays ?? 7));
const gain = Math.max(1, Number(v.gain ?? v.reputationGain ?? 1));
out.push({ tr: 'reputation_periodic', params: { intervalDays, gain } });
} else if (tr === 'appoint_politicians' || (tr === 'influence' && Array.isArray(v.officeTrs) && v.officeTrs.length)) {
const officeTrs = Array.isArray(v.officeTrs) ? v.officeTrs.filter((x) => typeof x === 'string') : [];
if (officeTrs.length) {
out.push({ tr: 'appoint_politicians', params: { officeTrs } });
}
} else if (tr === 'set_regional_tax' || tr === 'set_regionl_tax') {
const scope = typeof v.scope === 'string' && v.scope ? v.scope : 'local';
out.push({ tr: 'set_regional_tax', params: { scope } });
} else if (tr === 'free_lover_slots') {
const count = Math.max(1, Number(v.count ?? 1));
out.push({ tr: 'free_lover_slots', params: { count } });
} else if (tr === 'guard_protection') {
out.push({ tr: 'guard_protection', params: {} });
} else if (tr === 'court_immunity') {
out.push({ tr: 'court_immunity', params: {} });
} else if (tr) {
out.push({ tr: 'generic_benefit', params: { code: tr } });
}

View File

@@ -0,0 +1,9 @@
-- Spalte für tägliches Politik-Gehalt (Sequelize: FalukantUser.lastPoliticalDailySalaryOn).
-- Entspricht Migration: backend/migrations/20260401120000-politics-benefits-and-daily-salary.cjs
--
-- Bevorzugt: Migration per CLI ausführen (führt auch die political_office_benefit-Anpassung aus).
-- Nur diese Spalte ohne CLI: dieses Skript einmal ausführen; die Migration nutzt ADD COLUMN IF NOT EXISTS
-- und kann danach trotzdem noch sauber durchlaufen.
ALTER TABLE falukant_data.falukant_user
ADD COLUMN IF NOT EXISTS last_political_daily_salary_on date NULL;

View File

@@ -0,0 +1,108 @@
-- =============================================================================
-- Zusätzliche politische Amtsvorteile (reputation_periodic, Ernennungen, Steuerkompetenz, …)
-- =============================================================================
-- Voraussetzung: falukant_political_office_benefits.sql (oder gleichwertiges Schema) ist angelegt.
-- Idempotent: mehrfach ausführbar.
--
-- Hinweis: Frische Dev-DBs bekommen dieselben Zeilen über initializePoliticalOfficeBenefits (Node).
INSERT INTO falukant_type.political_office_benefit_type (tr)
SELECT 'reputation_periodic' WHERE NOT EXISTS (SELECT 1 FROM falukant_type.political_office_benefit_type t WHERE t.tr = 'reputation_periodic');
INSERT INTO falukant_type.political_office_benefit_type (tr)
SELECT 'appoint_politicians' WHERE NOT EXISTS (SELECT 1 FROM falukant_type.political_office_benefit_type t WHERE t.tr = 'appoint_politicians');
INSERT INTO falukant_type.political_office_benefit_type (tr)
SELECT 'set_regional_tax' WHERE NOT EXISTS (SELECT 1 FROM falukant_type.political_office_benefit_type t WHERE t.tr = 'set_regional_tax');
INSERT INTO falukant_type.political_office_benefit_type (tr)
SELECT 'free_lover_slots' WHERE NOT EXISTS (SELECT 1 FROM falukant_type.political_office_benefit_type t WHERE t.tr = 'free_lover_slots');
INSERT INTO falukant_type.political_office_benefit_type (tr)
SELECT 'guard_protection' WHERE NOT EXISTS (SELECT 1 FROM falukant_type.political_office_benefit_type t WHERE t.tr = 'guard_protection');
INSERT INTO falukant_type.political_office_benefit_type (tr)
SELECT 'court_immunity' WHERE NOT EXISTS (SELECT 1 FROM falukant_type.political_office_benefit_type t WHERE t.tr = 'court_immunity');
-- reputation_periodic
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
SELECT ot.id, bt.id, v::jsonb FROM falukant_type.political_office_type ot
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'reputation_periodic'
CROSS JOIN (VALUES
('assessor', '{"intervalDays":28,"gain":1}'::text),
('master-builder', '{"intervalDays":21,"gain":1}'::text),
('village-major', '{"intervalDays":21,"gain":1}'::text),
('councillor', '{"intervalDays":28,"gain":1}'::text),
('council', '{"intervalDays":21,"gain":1}'::text),
('mayor', '{"intervalDays":14,"gain":2}'::text),
('town-clerk', '{"intervalDays":21,"gain":1}'::text),
('judge', '{"intervalDays":14,"gain":2}'::text),
('bailif', '{"intervalDays":21,"gain":1}'::text),
('sheriff', '{"intervalDays":14,"gain":2}'::text),
('taxman', '{"intervalDays":21,"gain":2}'::text),
('treasurer', '{"intervalDays":14,"gain":3}'::text),
('consultant', '{"intervalDays":21,"gain":2}'::text),
('hangman', '{"intervalDays":21,"gain":1}'::text),
('territorial-council', '{"intervalDays":14,"gain":2}'::text),
('territorial-council-speaker', '{"intervalDays":10,"gain":3}'::text),
('ruler-consultant', '{"intervalDays":10,"gain":3}'::text),
('state-administrator', '{"intervalDays":10,"gain":3}'::text),
('super-state-administrator', '{"intervalDays":7,"gain":4}'::text),
('governor', '{"intervalDays":7,"gain":4}'::text),
('minister', '{"intervalDays":10,"gain":3}'::text),
('ministry-helper', '{"intervalDays":14,"gain":2}'::text),
('chancellor', '{"intervalDays":7,"gain":5}'::text)
) AS s(office_name, v)
WHERE ot.name = s.office_name
AND NOT EXISTS (SELECT 1 FROM falukant_predefine.political_office_benefit x WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id);
-- appoint_politicians
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
SELECT ot.id, bt.id, v::jsonb FROM falukant_type.political_office_type ot
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'appoint_politicians'
CROSS JOIN (VALUES
('mayor', '{"officeTrs":["beadle","town-clerk"]}'::text),
('judge', '{"officeTrs":["bailif"]}'::text),
('governor', '{"officeTrs":["state-administrator","consultant"]}'::text),
('super-state-administrator', '{"officeTrs":["territorial-council","hangman"]}'::text),
('chancellor', '{"officeTrs":["minister","ministry-helper","super-state-administrator"]}'::text)
) AS s(office_name, v)
WHERE ot.name = s.office_name
AND NOT EXISTS (SELECT 1 FROM falukant_predefine.political_office_benefit x WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id);
-- set_regional_tax
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
SELECT ot.id, bt.id, v::jsonb FROM falukant_type.political_office_type ot
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'set_regional_tax'
CROSS JOIN (VALUES
('taxman', '{"scope":"local"}'::text),
('treasurer', '{"scope":"shire"}'::text),
('super-state-administrator', '{"scope":"duchy"}'::text),
('chancellor', '{"scope":"national"}'::text)
) AS s(office_name, v)
WHERE ot.name = s.office_name
AND NOT EXISTS (SELECT 1 FROM falukant_predefine.political_office_benefit x WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id);
-- free_lover_slots
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
SELECT ot.id, bt.id, v::jsonb FROM falukant_type.political_office_type ot
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'free_lover_slots'
CROSS JOIN (VALUES
('councillor', '{"count":1}'::text),
('mayor', '{"count":1}'::text),
('minister', '{"count":1}'::text),
('governor', '{"count":2}'::text),
('chancellor', '{"count":3}'::text)
) AS s(office_name, v)
WHERE ot.name = s.office_name
AND NOT EXISTS (SELECT 1 FROM falukant_predefine.political_office_benefit x WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id);
-- guard_protection / court_immunity
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
SELECT ot.id, bt.id, '{}'::jsonb FROM falukant_type.political_office_type ot
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'guard_protection'
CROSS JOIN (VALUES ('sheriff'), ('hangman'), ('minister')) AS s(office_name)
WHERE ot.name = s.office_name
AND NOT EXISTS (SELECT 1 FROM falukant_predefine.political_office_benefit x WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id);
INSERT INTO falukant_predefine.political_office_benefit (office_type_id, benefit_type_id, value)
SELECT ot.id, bt.id, '{}'::jsonb FROM falukant_type.political_office_type ot
JOIN falukant_type.political_office_benefit_type bt ON bt.tr = 'court_immunity'
CROSS JOIN (VALUES ('judge'), ('councillor'), ('chancellor')) AS s(office_name)
WHERE ot.name = s.office_name
AND NOT EXISTS (SELECT 1 FROM falukant_predefine.political_office_benefit x WHERE x.office_type_id = ot.id AND x.benefit_type_id = bt.id);

View File

@@ -356,6 +356,7 @@ const politicalOfficeBenefitTypes = [
{ tr: 'salary' },
{ tr: 'daily_salary' },
{ tr: 'reputation' },
{ tr: 'reputation_periodic' },
{ tr: 'influence' },
{ tr: 'access_level' },
{ tr: 'housing_allowance' },
@@ -363,6 +364,60 @@ const politicalOfficeBenefitTypes = [
{ tr: 'guard_protection' },
{ tr: 'court_immunity' },
{ tr: 'set_regionl_tax' },
{ tr: 'set_regional_tax' },
{ tr: 'appoint_politicians' },
{ tr: 'free_lover_slots' },
];
/** Zusätzliche Amtsvorteile (Anzeige; Spielmechanik z. T. noch offen). SQL-Pendant: backend/sql/falukant_political_office_extra_benefits.sql */
const politicalOfficeExtraBenefitSeeds = [
{ officeTr: 'assessor', benefitTr: 'reputation_periodic', value: { intervalDays: 28, gain: 1 } },
{ officeTr: 'master-builder', benefitTr: 'reputation_periodic', value: { intervalDays: 21, gain: 1 } },
{ officeTr: 'village-major', benefitTr: 'reputation_periodic', value: { intervalDays: 21, gain: 1 } },
{ officeTr: 'councillor', benefitTr: 'reputation_periodic', value: { intervalDays: 28, gain: 1 } },
{ officeTr: 'council', benefitTr: 'reputation_periodic', value: { intervalDays: 21, gain: 1 } },
{ officeTr: 'mayor', benefitTr: 'reputation_periodic', value: { intervalDays: 14, gain: 2 } },
{ officeTr: 'town-clerk', benefitTr: 'reputation_periodic', value: { intervalDays: 21, gain: 1 } },
{ officeTr: 'judge', benefitTr: 'reputation_periodic', value: { intervalDays: 14, gain: 2 } },
{ officeTr: 'bailif', benefitTr: 'reputation_periodic', value: { intervalDays: 21, gain: 1 } },
{ officeTr: 'sheriff', benefitTr: 'reputation_periodic', value: { intervalDays: 14, gain: 2 } },
{ officeTr: 'taxman', benefitTr: 'reputation_periodic', value: { intervalDays: 21, gain: 2 } },
{ officeTr: 'treasurer', benefitTr: 'reputation_periodic', value: { intervalDays: 14, gain: 3 } },
{ officeTr: 'consultant', benefitTr: 'reputation_periodic', value: { intervalDays: 21, gain: 2 } },
{ officeTr: 'hangman', benefitTr: 'reputation_periodic', value: { intervalDays: 21, gain: 1 } },
{ officeTr: 'territorial-council', benefitTr: 'reputation_periodic', value: { intervalDays: 14, gain: 2 } },
{ officeTr: 'territorial-council-speaker', benefitTr: 'reputation_periodic', value: { intervalDays: 10, gain: 3 } },
{ officeTr: 'ruler-consultant', benefitTr: 'reputation_periodic', value: { intervalDays: 10, gain: 3 } },
{ officeTr: 'state-administrator', benefitTr: 'reputation_periodic', value: { intervalDays: 10, gain: 3 } },
{ officeTr: 'super-state-administrator', benefitTr: 'reputation_periodic', value: { intervalDays: 7, gain: 4 } },
{ officeTr: 'governor', benefitTr: 'reputation_periodic', value: { intervalDays: 7, gain: 4 } },
{ officeTr: 'minister', benefitTr: 'reputation_periodic', value: { intervalDays: 10, gain: 3 } },
{ officeTr: 'ministry-helper', benefitTr: 'reputation_periodic', value: { intervalDays: 14, gain: 2 } },
{ officeTr: 'chancellor', benefitTr: 'reputation_periodic', value: { intervalDays: 7, gain: 5 } },
{ officeTr: 'mayor', benefitTr: 'appoint_politicians', value: { officeTrs: ['beadle', 'town-clerk'] } },
{ officeTr: 'judge', benefitTr: 'appoint_politicians', value: { officeTrs: ['bailif'] } },
{ officeTr: 'governor', benefitTr: 'appoint_politicians', value: { officeTrs: ['state-administrator', 'consultant'] } },
{ officeTr: 'super-state-administrator', benefitTr: 'appoint_politicians', value: { officeTrs: ['territorial-council', 'hangman'] } },
{ officeTr: 'chancellor', benefitTr: 'appoint_politicians', value: { officeTrs: ['minister', 'ministry-helper', 'super-state-administrator'] } },
{ officeTr: 'taxman', benefitTr: 'set_regional_tax', value: { scope: 'local' } },
{ officeTr: 'treasurer', benefitTr: 'set_regional_tax', value: { scope: 'shire' } },
{ officeTr: 'super-state-administrator', benefitTr: 'set_regional_tax', value: { scope: 'duchy' } },
{ officeTr: 'chancellor', benefitTr: 'set_regional_tax', value: { scope: 'national' } },
{ officeTr: 'councillor', benefitTr: 'free_lover_slots', value: { count: 1 } },
{ officeTr: 'mayor', benefitTr: 'free_lover_slots', value: { count: 1 } },
{ officeTr: 'minister', benefitTr: 'free_lover_slots', value: { count: 1 } },
{ officeTr: 'governor', benefitTr: 'free_lover_slots', value: { count: 2 } },
{ officeTr: 'chancellor', benefitTr: 'free_lover_slots', value: { count: 3 } },
{ officeTr: 'sheriff', benefitTr: 'guard_protection', value: {} },
{ officeTr: 'hangman', benefitTr: 'guard_protection', value: {} },
{ officeTr: 'minister', benefitTr: 'guard_protection', value: {} },
{ officeTr: 'judge', benefitTr: 'court_immunity', value: {} },
{ officeTr: 'councillor', benefitTr: 'court_immunity', value: {} },
{ officeTr: 'chancellor', benefitTr: 'court_immunity', value: {} },
];
const politicalOffices = [
@@ -1092,8 +1147,24 @@ export const initializePoliticalOfficeBenefits = async () => {
if (wasCreated) dailyCreated += 1;
}
let extraCreated = 0;
for (const { officeTr, benefitTr, value } of politicalOfficeExtraBenefitSeeds) {
const office = await PoliticalOfficeType.findOne({ where: { name: officeTr } });
const bType = await PoliticalOfficeBenefitType.findOne({ where: { tr: benefitTr } });
if (!office || !bType) continue;
const [, wasCreated] = await PoliticalOfficeBenefit.findOrCreate({
where: { officeTypeId: office.id, benefitTypeId: bType.id },
defaults: {
officeTypeId: office.id,
benefitTypeId: bType.id,
value: value && typeof value === 'object' ? value : {}
}
});
if (wasCreated) extraCreated += 1;
}
console.log(
`[Falukant] PoliticalOfficeBenefits: Steuer neu=${taxCreated}, Tageslohn neu=${dailyCreated} (gesamt Ämter=${allOffices.length})`
`[Falukant] PoliticalOfficeBenefits: Steuer neu=${taxCreated}, Tageslohn neu=${dailyCreated}, weitere Vorteile neu=${extraCreated} (Ämter gesamt=${allOffices.length})`
);
};