fix staged nation tax rate consumption
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
import type { ScenarioConfig } from '@sammo-ts/logic';
|
||||
|
||||
import type { InMemoryTurnWorld, TurnCalendarHandler, TurnCalendarContext } from './inMemoryWorld.js';
|
||||
import { resolveAppliedNationRate } from './nationTaxRate.js';
|
||||
import type { TurnGeneral } from './types.js';
|
||||
|
||||
const resolveNumber = (source: Record<string, unknown>, keys: string[], fallback: number): number => {
|
||||
@@ -30,8 +31,6 @@ const resolveNumber = (source: Record<string, unknown>, keys: string[], fallback
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const resolveNationRate = (nation: Nation): number => asNumber(nation.meta.rate, 20);
|
||||
|
||||
const resolveNationBill = (nation: Nation): number => asNumber(nation.meta.bill, 100);
|
||||
|
||||
const resolveOfficerCity = (meta: Record<string, unknown>): number => {
|
||||
@@ -72,7 +71,7 @@ const buildNationIncomeContext = (nation: Nation, trait: NationTraitModule | nul
|
||||
? (type: TriggerNationalIncomeType, amount: number) => trait.onCalcNationalIncome!(actionContext, type, amount)
|
||||
: undefined;
|
||||
return {
|
||||
rate: resolveNationRate(nation),
|
||||
rate: resolveAppliedNationRate(nation.meta),
|
||||
modifyIncome,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createIncomeActionContext, type NationTraitModule } from '@sammo-ts/log
|
||||
|
||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||
import type { MonthlyEventActionHandler } from './monthlyEventHandler.js';
|
||||
import { resolveAppliedNationRate } from './nationTaxRate.js';
|
||||
|
||||
type SemiAnnualResource = 'gold' | 'rice';
|
||||
|
||||
@@ -21,8 +22,6 @@ const resolveBasePopulationIncrease = (world: InMemoryTurnWorld): number => {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : 5_000;
|
||||
};
|
||||
|
||||
const resolveNationRate = (meta: Record<string, unknown>): number => asNumber(meta.rate, 20);
|
||||
|
||||
const decayDomesticValue = (value: number): number => roundLegacyIntegerColumn(value * 0.99);
|
||||
|
||||
const applyResourceMaintenance = (value: number, ratios: readonly [number, number][]): number => {
|
||||
@@ -82,15 +81,11 @@ export const createProcessSemiAnnualHandler = (options: {
|
||||
|
||||
const basePopulationIncrease = resolveBasePopulationIncrease(world);
|
||||
for (const nation of world.listNations()) {
|
||||
const rate = resolveNationRate(nation.meta);
|
||||
const rate = resolveAppliedNationRate(nation.meta);
|
||||
let populationRatio = (30 - rate) / 200;
|
||||
const trait = options.nationTraits?.get(nation.typeCode);
|
||||
if (trait?.onCalcNationalIncome) {
|
||||
populationRatio = trait.onCalcNationalIncome(
|
||||
createIncomeActionContext(nation),
|
||||
'pop',
|
||||
populationRatio
|
||||
);
|
||||
populationRatio = trait.onCalcNationalIncome(createIncomeActionContext(nation), 'pop', populationRatio);
|
||||
}
|
||||
const genericRatio = (20 - rate) / 200;
|
||||
const trustDiff = 20 - rate;
|
||||
@@ -114,15 +109,9 @@ export const createProcessSemiAnnualHandler = (options: {
|
||||
agriculture: roundLegacyIntegerColumn(
|
||||
Math.min(city.agricultureMax, city.agriculture * (1 + genericRatio))
|
||||
),
|
||||
commerce: roundLegacyIntegerColumn(
|
||||
Math.min(city.commerceMax, city.commerce * (1 + genericRatio))
|
||||
),
|
||||
security: roundLegacyIntegerColumn(
|
||||
Math.min(city.securityMax, city.security * (1 + genericRatio))
|
||||
),
|
||||
defence: roundLegacyIntegerColumn(
|
||||
Math.min(city.defenceMax, city.defence * (1 + genericRatio))
|
||||
),
|
||||
commerce: roundLegacyIntegerColumn(Math.min(city.commerceMax, city.commerce * (1 + genericRatio))),
|
||||
security: roundLegacyIntegerColumn(Math.min(city.securityMax, city.security * (1 + genericRatio))),
|
||||
defence: roundLegacyIntegerColumn(Math.min(city.defenceMax, city.defence * (1 + genericRatio))),
|
||||
wall: roundLegacyIntegerColumn(Math.min(city.wallMax, city.wall * (1 + genericRatio))),
|
||||
meta: {
|
||||
...city.meta,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { asNumber } from '@sammo-ts/common';
|
||||
|
||||
const DEFAULT_NATION_RATE = 20;
|
||||
|
||||
export const resolveAppliedNationRate = (meta: Record<string, unknown>): number => {
|
||||
const stagedRate = meta.rate_tmp;
|
||||
if (typeof stagedRate === 'number' && Number.isFinite(stagedRate)) {
|
||||
return stagedRate;
|
||||
}
|
||||
return asNumber(meta.rate, DEFAULT_NATION_RATE);
|
||||
};
|
||||
@@ -258,4 +258,60 @@ describe('core monthly event actions at the real month boundary', () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the staged rate_tmp for income when the desired rate changes mid-period', async () => {
|
||||
const nation = buildNation();
|
||||
nation.meta = { ...nation.meta, rate: 35, rate_tmp: 10 };
|
||||
const world = buildWorld(
|
||||
[
|
||||
{
|
||||
id: 3,
|
||||
targetCode: 'month',
|
||||
priority: 1,
|
||||
condition: true,
|
||||
action: [['ProcessIncome', 'gold']],
|
||||
meta: {},
|
||||
},
|
||||
],
|
||||
(getWorld) => {
|
||||
const incomeHandler = createIncomeHandler({
|
||||
getWorld,
|
||||
scenarioConfig: {
|
||||
stat: {
|
||||
total: 300,
|
||||
min: 10,
|
||||
max: 100,
|
||||
npcTotal: 150,
|
||||
npcMax: 50,
|
||||
npcMin: 10,
|
||||
chiefMin: 70,
|
||||
},
|
||||
iconPath: '',
|
||||
map: {},
|
||||
const: { baseGold: 0, baseRice: 0 },
|
||||
environment: { mapName: map.id, unitSet: 'default' },
|
||||
},
|
||||
nationTraits: new Map(),
|
||||
});
|
||||
return new Map([['ProcessIncome', createProcessIncomeActionHandler(incomeHandler)]]);
|
||||
},
|
||||
{
|
||||
currentMonth: 6,
|
||||
generals: [buildGeneral(1, 1, 3)],
|
||||
nations: [nation],
|
||||
cities: [buildCity()],
|
||||
}
|
||||
);
|
||||
|
||||
await world.advanceMonth(new Date('0190-07-01T00:00:00.000Z'));
|
||||
|
||||
expect(world.getNationById(1)).toMatchObject({
|
||||
gold: 10_105,
|
||||
meta: {
|
||||
rate: 35,
|
||||
rate_tmp: 10,
|
||||
prev_income_gold: 105,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@ integration('core monthly event action persistence', () => {
|
||||
power: 0,
|
||||
level: 1,
|
||||
typeCode: 'che_중립',
|
||||
meta: { rate: 20, bill: 0, chief_set: 1 },
|
||||
meta: { rate: 35, rate_tmp: 10, bill: 0, chief_set: 1 },
|
||||
};
|
||||
const city: City = {
|
||||
id: cityId,
|
||||
@@ -287,9 +287,14 @@ integration('core monthly event action persistence', () => {
|
||||
expect(
|
||||
await db.nation.findUnique({ where: { id: nationId }, select: { gold: true, rice: true, meta: true } })
|
||||
).toEqual({
|
||||
gold: 10_210,
|
||||
gold: 10_105,
|
||||
rice: 20_000,
|
||||
meta: expect.objectContaining({ chief_set: 0, prev_income_gold: 210 }),
|
||||
meta: expect.objectContaining({
|
||||
rate: 35,
|
||||
rate_tmp: 10,
|
||||
chief_set: 0,
|
||||
prev_income_gold: 105,
|
||||
}),
|
||||
});
|
||||
expect(await db.city.findUnique({ where: { id: cityId }, select: { meta: true } })).toEqual({
|
||||
meta: expect.objectContaining({ officer_set: 0 }),
|
||||
|
||||
@@ -93,13 +93,15 @@ const populationTrait: NationTraitModule = {
|
||||
onCalcNationalIncome: (_context, type, amount) => (type === 'pop' ? amount * 1.2 : amount),
|
||||
};
|
||||
|
||||
const buildHarness = (options: {
|
||||
cities?: City[];
|
||||
nations?: Nation[];
|
||||
generals?: TurnGeneral[];
|
||||
configConst?: Record<string, unknown>;
|
||||
traits?: ReadonlyMap<string, NationTraitModule>;
|
||||
} = {}) => {
|
||||
const buildHarness = (
|
||||
options: {
|
||||
cities?: City[];
|
||||
nations?: Nation[];
|
||||
generals?: TurnGeneral[];
|
||||
configConst?: Record<string, unknown>;
|
||||
traits?: ReadonlyMap<string, NationTraitModule>;
|
||||
} = {}
|
||||
) => {
|
||||
const state: TurnWorldState = {
|
||||
id: 1,
|
||||
currentYear: 193,
|
||||
@@ -152,11 +154,7 @@ describe('ProcessSemiAnnual monthly action', () => {
|
||||
it('preserves the global popIncrease order, neutral double decay, supplied filtering, and nation trait', async () => {
|
||||
const { world, handler } = buildHarness({
|
||||
configConst: { basePopIncreaseAmount: 15_000 },
|
||||
cities: [
|
||||
buildCity(1),
|
||||
buildCity(2, { supplyState: 0 }),
|
||||
buildCity(3, { nationId: 0 }),
|
||||
],
|
||||
cities: [buildCity(1), buildCity(2, { supplyState: 0 }), buildCity(3, { nationId: 0 })],
|
||||
nations: [buildNation(1, { typeCode: populationTrait.key })],
|
||||
traits: new Map([[populationTrait.key, populationTrait]]),
|
||||
});
|
||||
@@ -224,6 +222,25 @@ describe('ProcessSemiAnnual monthly action', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the staged rate_tmp when the desired rate changes mid-period', async () => {
|
||||
const { world, handler } = buildHarness({
|
||||
cities: [buildCity(1)],
|
||||
nations: [buildNation(1, { meta: { rate: 50, rate_tmp: 20 } })],
|
||||
});
|
||||
|
||||
await handler(['gold'], environment, event);
|
||||
|
||||
expect(world.getCityById(1)).toMatchObject({
|
||||
population: 15_525,
|
||||
agriculture: 991,
|
||||
commerce: 991,
|
||||
security: 991,
|
||||
defence: 991,
|
||||
wall: 991,
|
||||
meta: { trust: 55, dead: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it('applies strict legacy resource thresholds to generals and nations for either resource', async () => {
|
||||
const amounts = [1_000, 1_001, 10_000, 10_001, 100_000, 100_001];
|
||||
const { world, handler } = buildHarness({
|
||||
@@ -236,18 +253,10 @@ describe('ProcessSemiAnnual monthly action', () => {
|
||||
await handler(['gold'], environment, event);
|
||||
await handler(['rice'], environment, event);
|
||||
|
||||
expect(world.listGenerals().map((general) => general.gold)).toEqual([
|
||||
1_000, 991, 9_900, 9_701, 97_000, 97_001,
|
||||
]);
|
||||
expect(world.listGenerals().map((general) => general.rice)).toEqual([
|
||||
1_000, 991, 9_900, 9_701, 97_000, 97_001,
|
||||
]);
|
||||
expect(world.listNations().map((nation) => nation.gold)).toEqual([
|
||||
1_000, 991, 9_900, 9_701, 97_000, 95_001,
|
||||
]);
|
||||
expect(world.listNations().map((nation) => nation.rice)).toEqual([
|
||||
1_000, 991, 9_900, 9_701, 97_000, 95_001,
|
||||
]);
|
||||
expect(world.listGenerals().map((general) => general.gold)).toEqual([1_000, 991, 9_900, 9_701, 97_000, 97_001]);
|
||||
expect(world.listGenerals().map((general) => general.rice)).toEqual([1_000, 991, 9_900, 9_701, 97_000, 97_001]);
|
||||
expect(world.listNations().map((nation) => nation.gold)).toEqual([1_000, 991, 9_900, 9_701, 97_000, 95_001]);
|
||||
expect(world.listNations().map((nation) => nation.rice)).toEqual([1_000, 991, 9_900, 9_701, 97_000, 95_001]);
|
||||
});
|
||||
|
||||
it('rejects resources outside the legacy enum', () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ const nation: Nation = {
|
||||
power: 0,
|
||||
level: 1,
|
||||
typeCode: 'che_중립',
|
||||
meta: { rate: 20 },
|
||||
meta: { rate: 50, rate_tmp: 20 },
|
||||
};
|
||||
|
||||
const buildCity = (id: number, nationIdValue: number): City => ({
|
||||
@@ -265,7 +265,7 @@ integration('monthly semi-annual database persistence', () => {
|
||||
expect(await db.nation.findUniqueOrThrow({ where: { id: nationId } })).toMatchObject({
|
||||
gold: 95_001,
|
||||
rice: 7_777,
|
||||
meta: { rate: 20 },
|
||||
meta: { rate: 50, rate_tmp: 20 },
|
||||
});
|
||||
} finally {
|
||||
await dbHooks.close();
|
||||
|
||||
Reference in New Issue
Block a user