fix: align scenario 2400 monthly parity
This commit is contained in:
@@ -157,6 +157,7 @@ const DEFAULT_WAR_CONFIG = {
|
||||
const DEFAULT_AFTER_CONFIG = {
|
||||
techLevelIncYear: 5,
|
||||
initialAllowedTechLevel: 1,
|
||||
maxTechLevel: 12,
|
||||
defaultCityWall: 1000,
|
||||
baseGold: 0,
|
||||
baseRice: 2000,
|
||||
@@ -235,7 +236,7 @@ export const buildWarAftermathConfig = (
|
||||
['initialAllowedTechLevel'],
|
||||
DEFAULT_AFTER_CONFIG.initialAllowedTechLevel
|
||||
),
|
||||
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 0),
|
||||
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], DEFAULT_AFTER_CONFIG.maxTechLevel),
|
||||
defaultCityWall: resolveNumber(constValues, ['defaultCityWall'], DEFAULT_AFTER_CONFIG.defaultCityWall),
|
||||
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], DEFAULT_AFTER_CONFIG.baseGold),
|
||||
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], DEFAULT_AFTER_CONFIG.baseRice),
|
||||
|
||||
@@ -23,6 +23,7 @@ import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js'
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
|
||||
|
||||
const ACTION_NAME = '임관';
|
||||
const ARGS_SCHEMA = z.object({
|
||||
@@ -43,7 +44,11 @@ export class ActionDefinition<
|
||||
> implements GeneralActionDefinition<TriggerState, AppointmentArgs> {
|
||||
public readonly key = 'che_임관';
|
||||
public readonly name = ACTION_NAME;
|
||||
constructor(private readonly env: TurnCommandEnv) {}
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
|
||||
constructor(private readonly env: TurnCommandEnv) {
|
||||
this.pipeline = new GeneralActionPipeline(env.generalActionModules ?? []);
|
||||
}
|
||||
|
||||
getInheritanceActiveActionAmount(): number {
|
||||
return 1;
|
||||
@@ -116,7 +121,11 @@ export class ActionDefinition<
|
||||
troopId: 0,
|
||||
experience:
|
||||
context.general.experience +
|
||||
(context.destNationGeneralCount < this.env.initialNationGenLimit ? 700 : 100),
|
||||
this.pipeline.onCalcStat(
|
||||
context,
|
||||
'experience',
|
||||
context.destNationGeneralCount < this.env.initialNationGenLimit ? 700 : 100
|
||||
),
|
||||
meta: {
|
||||
...context.general.meta,
|
||||
officer_city: 0,
|
||||
|
||||
@@ -64,6 +64,10 @@ export interface DispatchResolveContext<
|
||||
aftermathConfig: WarAftermathConfig;
|
||||
}
|
||||
|
||||
export const orderDefenderGenerals = <TriggerState extends GeneralTriggerState>(
|
||||
generals: General<TriggerState>[]
|
||||
): General<TriggerState>[] => [...generals].sort((left, right) => left.id - right.id);
|
||||
|
||||
const ACTION_NAME = '출병';
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destCityId: z.number(),
|
||||
@@ -86,7 +90,12 @@ const fixtureNumber = (value: unknown, fallback = 0): number =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
||||
|
||||
const formatFixtureDate = (value: Date | undefined): string =>
|
||||
value ? value.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/u, '') : '1970-01-01 00:00:00';
|
||||
value
|
||||
? value
|
||||
.toISOString()
|
||||
.replace('T', ' ')
|
||||
.replace(/\.\d{3}Z$/u, '')
|
||||
: '1970-01-01 00:00:00';
|
||||
|
||||
const buildBattleGeneralFixture = <TriggerState extends GeneralTriggerState>(general: General<TriggerState>) => {
|
||||
const meta = general.meta;
|
||||
@@ -105,9 +114,7 @@ const buildBattleGeneralFixture = <TriggerState extends GeneralTriggerState>(gen
|
||||
inheritBuff = parsed;
|
||||
} else if (typeof parsed === 'object' && parsed !== null) {
|
||||
inheritBuff = Object.fromEntries(
|
||||
Object.entries(parsed).filter(
|
||||
(entry): entry is [string, number] => typeof entry[1] === 'number'
|
||||
)
|
||||
Object.entries(parsed).filter((entry): entry is [string, number] => typeof entry[1] === 'number')
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
@@ -556,12 +563,14 @@ export class ActionDefinition<
|
||||
defenderCity.meta.term = 3;
|
||||
const defenderNation = defenderCity.nationId > 0 ? (nationMap.get(defenderCity.nationId) ?? null) : null;
|
||||
|
||||
const defenderGenerals = generals.filter(
|
||||
(general) =>
|
||||
general.cityId === defenderCity.id &&
|
||||
general.nationId === defenderCity.nationId &&
|
||||
general.crew > 0 &&
|
||||
(unitSet.crewTypes?.some((crewType) => crewType.id === general.crewTypeId) ?? false)
|
||||
const defenderGenerals = orderDefenderGenerals(
|
||||
generals.filter(
|
||||
(general) =>
|
||||
general.cityId === defenderCity.id &&
|
||||
general.nationId === defenderCity.nationId &&
|
||||
general.crew > 0 &&
|
||||
(unitSet.crewTypes?.some((crewType) => crewType.id === general.crewTypeId) ?? false)
|
||||
)
|
||||
);
|
||||
const traceGeneralIds = new Set(process.env.CORE_AI_TRACE_GENERAL_IDS?.split(',') ?? []);
|
||||
const shouldTraceWar =
|
||||
@@ -647,9 +656,7 @@ export class ActionDefinition<
|
||||
// to deploy. Preserve that ordering before snapshotting city effects.
|
||||
let frontStatePatches: Array<{ id: number; frontState: number }> = [];
|
||||
if (battle.conquered && context.map && context.diplomacy) {
|
||||
const connections = new Map(
|
||||
context.map.cities.map((city) => [city.id, city.connections ?? []] as const)
|
||||
);
|
||||
const connections = new Map(context.map.cities.map((city) => [city.id, city.connections ?? []] as const));
|
||||
const nearbyCityIds = new Set([defenderCity.id, ...(connections.get(defenderCity.id) ?? [])]);
|
||||
const nearbyNationIds = new Set<number>([aftermath.conquest?.conquerNationId ?? attackerNation.id]);
|
||||
for (const city of cities) {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* the value rounded to six significant decimal digits on the next read.
|
||||
* Keep those boundaries separate so later SQL expressions use binary32 state.
|
||||
*/
|
||||
export const storeLegacyCityTrust = (value: number): number => Math.fround(value);
|
||||
import { readLegacyStoredFloat, toLegacyStoredFloat } from '@sammo-ts/logic/compat/legacyFloat.js';
|
||||
|
||||
export const readLegacyCityTrust = (value: number): number => Number(Math.fround(value).toPrecision(6));
|
||||
export const storeLegacyCityTrust = (value: number): number => toLegacyStoredFloat(value);
|
||||
|
||||
export const readLegacyCityTrust = (value: number): number => readLegacyStoredFloat(value);
|
||||
|
||||
@@ -3,8 +3,27 @@
|
||||
// command/battle update, so both boundaries are part of the game state.
|
||||
export const toLegacyStoredFloat = (value: number): number => Math.fround(value);
|
||||
|
||||
export const readLegacyStoredFloat = (value: number): number =>
|
||||
Number(Math.fround(value).toPrecision(6));
|
||||
const roundHalfEven = (value: number): number => {
|
||||
const lower = Math.floor(value);
|
||||
const fraction = value - lower;
|
||||
const tolerance = Number.EPSILON * Math.max(1, Math.abs(value)) * 4;
|
||||
if (Math.abs(fraction - 0.5) <= tolerance) {
|
||||
return lower % 2 === 0 ? lower : lower + 1;
|
||||
}
|
||||
return Math.round(value);
|
||||
};
|
||||
|
||||
export const readLegacyStoredFloat = (value: number): number => {
|
||||
const stored = Math.fround(value);
|
||||
if (!Number.isFinite(stored) || stored === 0) {
|
||||
return stored;
|
||||
}
|
||||
const sign = stored < 0 ? -1 : 1;
|
||||
const absolute = Math.abs(stored);
|
||||
const exponent = Math.floor(Math.log10(absolute));
|
||||
const scale = 10 ** (5 - exponent);
|
||||
return sign * (roundHalfEven(absolute * scale) / scale);
|
||||
};
|
||||
|
||||
export const addLegacyStoredFloat = (current: number, delta: number): number =>
|
||||
toLegacyStoredFloat(readLegacyStoredFloat(current) + delta);
|
||||
|
||||
@@ -101,7 +101,9 @@ export interface General<TriggerState extends GeneralTriggerState = GeneralTrigg
|
||||
meta: GeneralMeta;
|
||||
lastTurn?: GeneralLastTurn;
|
||||
turnTime?: Date;
|
||||
turnTick?: number;
|
||||
recentWarTime?: Date | null;
|
||||
recentWarTick?: number | null;
|
||||
}
|
||||
|
||||
export interface City {
|
||||
|
||||
@@ -494,7 +494,11 @@ export const resolveWarAftermath = <TriggerState extends GeneralTriggerState = G
|
||||
const defenderNation = input.defenderNation;
|
||||
const cityKilled = cityReport?.killed ?? 0;
|
||||
|
||||
if ((cityReport?.dead ?? 0) > 0) {
|
||||
// Ref branches on WarUnitCity::getPhase(), not accumulated city
|
||||
// casualties. A city can retain dead casualties from earlier battles
|
||||
// while being conquered before its wall receives a phase.
|
||||
const cityPhase = cityReport?.phase ?? ((cityReport?.dead ?? 0) > 0 ? 1 : 0);
|
||||
if (cityPhase > 0) {
|
||||
const crewTypeIndex = buildCrewTypeIndex(input.unitSet);
|
||||
const crewType = crewTypeIndex.get(input.config.castleCrewTypeId);
|
||||
const riceCoef = crewType?.rice ?? 1;
|
||||
|
||||
@@ -165,6 +165,7 @@ const resolveUnitReport = (unit: WarUnit): WarUnitReport => {
|
||||
isAttacker: unit.isAttacker(),
|
||||
killed: unit.getKilled(),
|
||||
dead: unit.getDead(),
|
||||
phase: unit.getPhase(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -176,6 +177,7 @@ const resolveUnitReport = (unit: WarUnit): WarUnitReport => {
|
||||
isAttacker: unit.isAttacker(),
|
||||
killed: unit.getKilled(),
|
||||
dead: unit.getDead(),
|
||||
phase: unit.getPhase(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -186,6 +188,7 @@ const resolveUnitReport = (unit: WarUnit): WarUnitReport => {
|
||||
isAttacker: unit.isAttacker(),
|
||||
killed: unit.getKilled(),
|
||||
dead: unit.getDead(),
|
||||
phase: unit.getPhase(),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -112,6 +112,8 @@ export interface WarUnitReport {
|
||||
isAttacker: boolean;
|
||||
killed: number;
|
||||
dead: number;
|
||||
/** Number of battle phases consumed by this unit. */
|
||||
phase?: number;
|
||||
}
|
||||
|
||||
export interface WarBattleMetrics {
|
||||
|
||||
@@ -112,10 +112,6 @@ export class WarUnitGeneral<
|
||||
super.setOppose(oppose);
|
||||
increaseMetaNumber(this.general.meta, RANK_WARNUM, 1);
|
||||
|
||||
if (!oppose) {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseTurnTime = this.isAttacker()
|
||||
? this.general.turnTime
|
||||
: oppose instanceof WarUnitGeneral
|
||||
@@ -126,6 +122,16 @@ export class WarUnitGeneral<
|
||||
}
|
||||
const phase = clamp(this.getRealPhase(), 0, 99);
|
||||
this.general.recentWarTime = new Date(baseTurnTime.getTime());
|
||||
const baseTurnTick = this.isAttacker()
|
||||
? this.general.turnTick
|
||||
: oppose instanceof WarUnitGeneral
|
||||
? oppose.general.turnTick
|
||||
: this.general.turnTick;
|
||||
if (baseTurnTick !== undefined) {
|
||||
this.general.recentWarTick = baseTurnTick - (baseTurnTick % 100) + phase;
|
||||
} else {
|
||||
delete this.general.recentWarTick;
|
||||
}
|
||||
this.general.meta.recent_war_phase = phase;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { City, General, Nation } from '../src/domain/entities.js';
|
||||
import { resolveGeneralAction } from '../src/actions/engine.js';
|
||||
import { ActionDefinition } from '../src/actions/turn/general/che_출병.js';
|
||||
import { ActionDefinition, orderDefenderGenerals } from '../src/actions/turn/general/che_출병.js';
|
||||
import type { DispatchResolveContext } from '../src/actions/turn/general/che_출병.js';
|
||||
import type { TurnSchedule } from '../src/turn/calendar.js';
|
||||
import type { WarAftermathConfig, WarEngineConfig } from '../src/war/types.js';
|
||||
@@ -183,6 +183,7 @@ describe('che_출병', () => {
|
||||
const defenderCity = buildCity(2, defenderNation.id);
|
||||
const neutralCity = buildCity(3, 0);
|
||||
const attacker = buildGeneral(1, attackerNation.id, attackerCity.id);
|
||||
attacker.turnTime = new Date('2000-01-01T00:00:00Z');
|
||||
const defender = buildGeneral(2, defenderNation.id, defenderCity.id);
|
||||
defender.crew = 0;
|
||||
defenderCity.defence = 0;
|
||||
@@ -204,9 +205,36 @@ describe('che_출병', () => {
|
||||
id: 'test-map',
|
||||
name: 'test-map',
|
||||
cities: [
|
||||
{ id: 1, name: 'City1', level: 2, region: 1, position: { x: 0, y: 0 }, connections: [2, 3], max: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 }, initial: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 } },
|
||||
{ id: 2, name: 'City2', level: 2, region: 1, position: { x: 1, y: 0 }, connections: [1], max: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 }, initial: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 } },
|
||||
{ id: 3, name: 'City3', level: 2, region: 1, position: { x: 0, y: 1 }, connections: [1], max: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 }, initial: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 } },
|
||||
{
|
||||
id: 1,
|
||||
name: 'City1',
|
||||
level: 2,
|
||||
region: 1,
|
||||
position: { x: 0, y: 0 },
|
||||
connections: [2, 3],
|
||||
max: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 },
|
||||
initial: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 },
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'City2',
|
||||
level: 2,
|
||||
region: 1,
|
||||
position: { x: 1, y: 0 },
|
||||
connections: [1],
|
||||
max: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 },
|
||||
initial: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 },
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'City3',
|
||||
level: 2,
|
||||
region: 1,
|
||||
position: { x: 0, y: 1 },
|
||||
connections: [1],
|
||||
max: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 },
|
||||
initial: { population: 1, agriculture: 1, commerce: 1, security: 1, defence: 1, wall: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
diplomacy: [
|
||||
@@ -235,6 +263,7 @@ describe('che_출병', () => {
|
||||
);
|
||||
|
||||
expect(resolution.logs.length).toBeGreaterThan(0);
|
||||
expect(resolution.general.recentWarTime?.toISOString()).toBe(attacker.turnTime.toISOString());
|
||||
expect(resolution.patches?.generals.some((patch) => patch.id === defender.id)).toBe(true);
|
||||
expect(resolution.patches?.cities.some((patch) => patch.id === defenderCity.id)).toBe(true);
|
||||
expect({ city: resolution.city, patches: resolution.patches?.cities }).toMatchObject({
|
||||
@@ -250,6 +279,13 @@ describe('che_출병', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('orders equal-priority defender inputs by general number before the stable battle sort', () => {
|
||||
const defender2 = buildGeneral(2, 2, 2);
|
||||
const defender3 = buildGeneral(3, 2, 2);
|
||||
|
||||
expect(orderDefenderGenerals([defender3, defender2]).map((general) => general.id)).toEqual([2, 3]);
|
||||
});
|
||||
|
||||
it('prefers an enemy on the shortest route layer before considering the next layer', () => {
|
||||
const attackerNation = buildNation(1);
|
||||
const attackerCity = buildCity(1, attackerNation.id);
|
||||
|
||||
@@ -20,6 +20,7 @@ import { commandSpec as destroySpec } from '../../src/actions/turn/general/che_
|
||||
import { commandSpec as agitateSpec } from '../../src/actions/turn/general/che_선동.js';
|
||||
import { commandSpec as seizeSpec } from '../../src/actions/turn/general/che_탈취.js';
|
||||
import { commandSpec as fireSpec } from '../../src/actions/turn/general/che_화계.js';
|
||||
import { ActionDefinition as AppointmentAction } from '../../src/actions/turn/general/che_임관.js';
|
||||
import type { TurnCommandEnv } from '../../src/actions/turn/commandEnv.js';
|
||||
import {
|
||||
createItemActionModules,
|
||||
@@ -42,6 +43,7 @@ import {
|
||||
import { readLegacyCityTrust, storeLegacyCityTrust } from '../../src/actions/turn/general/legacyCityTrust.js';
|
||||
import { roundLegacyRecruitCost } from '../../src/actions/turn/general/che_징병.js';
|
||||
import { resolveLegacyDomesticTrust } from '../../src/actions/turn/general/che_상업투자.js';
|
||||
import { traitModule as ambitiousPersonality } from '../../src/actionModules/traits/personality/che_출세.js';
|
||||
|
||||
describe('General Commands New Scenario', () => {
|
||||
it('truncates generated NPC dex like GeneralBuilder integer arguments', () => {
|
||||
@@ -75,6 +77,8 @@ describe('General Commands New Scenario', () => {
|
||||
expect(toLegacyStoredTech(value)).not.toBe(Number(Math.fround(value).toPrecision(6)));
|
||||
expect(readLegacyStoredTech(624.0966796875)).toBe(624.097);
|
||||
expect(addLegacyStoredTech(624.0966796875, 22.9)).toBe(Math.fround(624.097 + 22.9));
|
||||
expect(readLegacyStoredTech(533.3125)).toBe(533.312);
|
||||
expect(readLegacyStoredTech(533.4375)).toBe(533.438);
|
||||
});
|
||||
|
||||
it('separates MariaDB FLOAT trust storage from its six-digit PHP read value', () => {
|
||||
@@ -84,6 +88,7 @@ describe('General Commands New Scenario', () => {
|
||||
expect(stored).not.toBe(readLegacyCityTrust(stored));
|
||||
expect(readLegacyCityTrust(stored)).toBe(88.3068);
|
||||
expect(readLegacyCityTrust(storeLegacyCityTrust(readLegacyCityTrust(stored) + 10))).toBe(98.3068);
|
||||
expect(readLegacyCityTrust(storeLegacyCityTrust(93.40625))).toBe(93.4062);
|
||||
});
|
||||
|
||||
it('rounds recruitment cost across the PHP half boundary', () => {
|
||||
@@ -100,6 +105,38 @@ describe('General Commands New Scenario', () => {
|
||||
expect(resolveLegacyDomesticTrust(null)).toBe(50);
|
||||
});
|
||||
|
||||
it('applies the personality experience modifier to appointment rewards', () => {
|
||||
const action = new AppointmentAction({
|
||||
initialNationGenLimit: 10,
|
||||
generalActionModules: [ambitiousPersonality],
|
||||
} as unknown as TurnCommandEnv);
|
||||
const general = {
|
||||
id: 1,
|
||||
name: 'General',
|
||||
experience: 1_000,
|
||||
role: {
|
||||
personality: 'che_출세',
|
||||
specialDomestic: null,
|
||||
specialWar: null,
|
||||
items: { horse: null, weapon: null, book: null, item: null },
|
||||
},
|
||||
meta: {},
|
||||
} as General;
|
||||
const result = action.resolve(
|
||||
{
|
||||
general,
|
||||
destNation: { id: 2, name: 'Nation', meta: {} } as Nation,
|
||||
destNationGeneralCount: 10,
|
||||
destCityId: 3,
|
||||
addLog: () => undefined,
|
||||
} as unknown as Parameters<typeof action.resolve>[0],
|
||||
{ destNationId: 2 }
|
||||
);
|
||||
const generalPatch = result.effects.find((effect) => effect.type === 'general:patch');
|
||||
|
||||
expect(generalPatch).toMatchObject({ patch: { experience: 1_110 } });
|
||||
});
|
||||
|
||||
// 1. Setup Environment
|
||||
const systemEnv: TurnCommandEnv = {
|
||||
develCost: 100,
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { UnitSetDefinition } from '../src/world/types.js';
|
||||
import { resolveWarAftermath } from '../src/war/aftermath.js';
|
||||
import type { WarAftermathConfig } from '../src/war/types.js';
|
||||
import { LogFormat } from '../src/logging/types.js';
|
||||
import { buildWarAftermathConfig } from '../src/actions/turn/actionContextHelpers.js';
|
||||
import type { ScenarioConfig } from '../src/scenario/types.js';
|
||||
|
||||
const buildUnitSet = (): UnitSetDefinition => ({
|
||||
id: 'test',
|
||||
@@ -129,6 +131,12 @@ const buildGeneral = (id: number, nationId: number, cityId: number): General =>
|
||||
});
|
||||
|
||||
describe('war aftermath', () => {
|
||||
it('defaults the omitted legacy maximum tech level to 12', () => {
|
||||
const config = buildWarAftermathConfig({ const: {} } as ScenarioConfig, 999);
|
||||
|
||||
expect(config.maxTechLevel).toBe(12);
|
||||
});
|
||||
|
||||
it('updates tech and diplomacy deltas', () => {
|
||||
const attackerNation = buildNation(1);
|
||||
const defenderNation = buildNation(2);
|
||||
@@ -268,6 +276,51 @@ describe('war aftermath', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the city battle phase, not retained casualties, for conquered supply-city rice', () => {
|
||||
const attackerNation = buildNation(1);
|
||||
const defenderNation = buildNation(2);
|
||||
defenderNation.rice = 6000;
|
||||
defenderNation.capitalCityId = 3;
|
||||
const attackerCity = buildCity(1, 1);
|
||||
const defenderCity = buildCity(2, 2);
|
||||
const defenderCapital = buildCity(3, 2);
|
||||
defenderCity.meta.supply = 1;
|
||||
const attacker = buildGeneral(1, 1, 1);
|
||||
|
||||
resolveWarAftermath({
|
||||
battle: {
|
||||
attacker,
|
||||
defenders: [],
|
||||
defenderCity,
|
||||
logs: [],
|
||||
conquered: true,
|
||||
reports: [
|
||||
{
|
||||
id: defenderCity.id,
|
||||
type: 'city',
|
||||
name: defenderCity.name,
|
||||
isAttacker: false,
|
||||
killed: 0,
|
||||
dead: 100,
|
||||
phase: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
attackerNation,
|
||||
defenderNation,
|
||||
attackerCity,
|
||||
defenderCity,
|
||||
nations: [attackerNation, defenderNation],
|
||||
cities: [attackerCity, defenderCity, defenderCapital],
|
||||
generals: [attacker],
|
||||
unitSet: buildUnitSet(),
|
||||
config: buildConfig(),
|
||||
time: { year: 200, month: 1, startYear: 180 },
|
||||
});
|
||||
|
||||
expect(defenderNation.rice).toBe(6500);
|
||||
});
|
||||
|
||||
it('applies conquest collapse rewards', () => {
|
||||
const rng = new RandUtil(new ConstantRNG(0));
|
||||
const attackerNation = buildNation(1);
|
||||
|
||||
@@ -312,6 +312,7 @@ describe('war triggers', () => {
|
||||
const city = buildCity();
|
||||
const attackerGeneral = buildGeneral(80);
|
||||
attackerGeneral.turnTime = new Date('2026-07-26T13:38:45.000Z');
|
||||
attackerGeneral.turnTick = 123_456;
|
||||
|
||||
const attacker = new WarUnitGeneral(
|
||||
rng,
|
||||
@@ -338,8 +339,13 @@ describe('war triggers', () => {
|
||||
attacker.setOppose(defender);
|
||||
defender.setOppose(attacker);
|
||||
expect(attackerGeneral.recentWarTime?.toISOString()).toBe('2026-07-26T13:38:45.000Z');
|
||||
expect(attackerGeneral.recentWarTick).toBe(123_400);
|
||||
expect(attackerGeneral.meta.recent_war_phase).toBe(0);
|
||||
|
||||
attackerGeneral.turnTick = 123_556;
|
||||
attacker.setOppose(null);
|
||||
expect(attackerGeneral.recentWarTick).toBe(123_500);
|
||||
|
||||
attacker.beginPhase();
|
||||
|
||||
const [module] = await loadWarTriggerModules(['che_필살']);
|
||||
|
||||
Reference in New Issue
Block a user