merge: match scorched-earth command boundaries
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import type { City, GeneralTriggerState, Nation, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
|
import type { City, General, GeneralTriggerState, Nation, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
|
||||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||||
import {
|
import {
|
||||||
beChief,
|
beChief,
|
||||||
@@ -15,20 +15,22 @@ import type {
|
|||||||
GeneralActionOutcome,
|
GeneralActionOutcome,
|
||||||
GeneralActionResolveContext,
|
GeneralActionResolveContext,
|
||||||
} from '@sammo-ts/logic/actions/engine.js';
|
} from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { createCityPatchEffect, createLogEffect, createNationPatchEffect } from '@sammo-ts/logic/actions/engine.js';
|
import {
|
||||||
|
createCityPatchEffect,
|
||||||
|
createGeneralPatchEffect,
|
||||||
|
createLogEffect,
|
||||||
|
createNationPatchEffect,
|
||||||
|
} from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||||
import { JosaUtil } from '@sammo-ts/common';
|
import { JosaUtil } from '@sammo-ts/common';
|
||||||
import type { NationTurnCommandSpec } from './index.js';
|
import type { NationTurnCommandSpec } from './index.js';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
import { normalizeLegacyIntegerArg, parseArgsWithSchema } from '../parseArgs.js';
|
||||||
|
|
||||||
const ARGS_SCHEMA = z.object({
|
const ARGS_SCHEMA = z.object({
|
||||||
destCityId: z.preprocess(
|
destCityId: z.preprocess(normalizeLegacyIntegerArg, z.number().int().positive()),
|
||||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
|
||||||
z.number().int().positive()
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
export type ScorchedEarthArgs = z.infer<typeof ARGS_SCHEMA>;
|
export type ScorchedEarthArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ export interface ScorchedEarthResolveContext<
|
|||||||
> extends GeneralActionResolveContext<TriggerState> {
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
destCity: City;
|
destCity: City;
|
||||||
destNation: Nation | null;
|
destNation: Nation | null;
|
||||||
|
friendlyGenerals: Array<General<TriggerState>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACTION_NAME = '초토화';
|
const ACTION_NAME = '초토화';
|
||||||
@@ -61,9 +64,9 @@ const calcReturnAmount = (destCity: City): number => {
|
|||||||
|
|
||||||
const calcReducedValue = (current: number, max: number, ratio: number): number => {
|
const calcReducedValue = (current: number, max: number, ratio: number): number => {
|
||||||
if (max <= 0) {
|
if (max <= 0) {
|
||||||
return Math.max(0, Math.floor(current * ratio));
|
return Math.max(0, Math.round(current * ratio));
|
||||||
}
|
}
|
||||||
return Math.max(Math.floor(max * 0.1), Math.floor(current * ratio));
|
return Math.max(Math.round(max * 0.1), Math.round(current * ratio));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 초토화 실행을 위한 정의/제약을 구성한다.
|
// 초토화 실행을 위한 정의/제약을 구성한다.
|
||||||
@@ -141,12 +144,16 @@ export class ActionDefinition<
|
|||||||
const reducedComm = calcReducedValue(destCity.commerce, destCity.commerceMax, 0.2);
|
const reducedComm = calcReducedValue(destCity.commerce, destCity.commerceMax, 0.2);
|
||||||
const reducedSecu = calcReducedValue(destCity.security, destCity.securityMax, 0.2);
|
const reducedSecu = calcReducedValue(destCity.security, destCity.securityMax, 0.2);
|
||||||
const reducedDef = calcReducedValue(destCity.defence, destCity.defenceMax, 0.2);
|
const reducedDef = calcReducedValue(destCity.defence, destCity.defenceMax, 0.2);
|
||||||
const reducedWall = Math.max(Math.floor(destCity.wallMax * 0.1), Math.floor(destCity.wall * 0.5));
|
const reducedWall = Math.max(Math.round(destCity.wallMax * 0.1), Math.round(destCity.wall * 0.5));
|
||||||
const trust = typeof destCity.meta?.trust === 'number' ? Number(destCity.meta.trust) : 0;
|
const trust = typeof destCity.meta?.trust === 'number' ? Number(destCity.meta.trust) : 0;
|
||||||
|
|
||||||
const expGain = 5 * (PRE_REQ_TURN + 1);
|
const expGain = 5 * (PRE_REQ_TURN + 1);
|
||||||
general.experience = Math.max(0, Math.floor(general.experience * 0.9)) + expGain;
|
general.experience = Math.max(0, Math.round(general.experience * 0.9 + expGain));
|
||||||
general.dedication += expGain;
|
general.dedication += expGain;
|
||||||
|
general.meta = {
|
||||||
|
...general.meta,
|
||||||
|
betray: Number(general.meta.betray ?? 0) + 1,
|
||||||
|
};
|
||||||
|
|
||||||
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||||
createCityPatchEffect(
|
createCityPatchEffect(
|
||||||
@@ -159,6 +166,7 @@ export class ActionDefinition<
|
|||||||
security: reducedSecu,
|
security: reducedSecu,
|
||||||
defence: reducedDef,
|
defence: reducedDef,
|
||||||
wall: reducedWall,
|
wall: reducedWall,
|
||||||
|
conflict: {},
|
||||||
meta: {
|
meta: {
|
||||||
...destCity.meta,
|
...destCity.meta,
|
||||||
trust: Math.max(50, trust),
|
trust: Math.max(50, trust),
|
||||||
@@ -210,6 +218,26 @@ export class ActionDefinition<
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
for (const friendlyGeneral of context.friendlyGenerals) {
|
||||||
|
if (friendlyGeneral.id === general.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
effects.push(
|
||||||
|
createGeneralPatchEffect(
|
||||||
|
{
|
||||||
|
...(friendlyGeneral.officerLevel >= 5
|
||||||
|
? { experience: Math.round(friendlyGeneral.experience * 0.9) }
|
||||||
|
: {}),
|
||||||
|
meta: {
|
||||||
|
...friendlyGeneral.meta,
|
||||||
|
betray: Number(friendlyGeneral.meta.betray ?? 0) + 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
friendlyGeneral.id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return { effects };
|
return { effects };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,10 +257,12 @@ export const actionContextBuilder: ActionContextBuilder<ScorchedEarthArgs> = (ba
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const destNation = worldRef.getNationById(destCity.nationId);
|
const destNation = worldRef.getNationById(destCity.nationId);
|
||||||
|
const friendlyGenerals = worldRef.listGenerals().filter((general) => general.nationId === base.general.nationId);
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
destCity,
|
destCity,
|
||||||
destNation: destNation ?? null,
|
destNation: destNation ?? null,
|
||||||
|
friendlyGenerals,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -469,6 +469,7 @@ describe('Nation Missing Actions', () => {
|
|||||||
nation,
|
nation,
|
||||||
destCity,
|
destCity,
|
||||||
destNation,
|
destNation,
|
||||||
|
friendlyGenerals: [general],
|
||||||
rng: {} as any,
|
rng: {} as any,
|
||||||
addLog: () => {},
|
addLog: () => {},
|
||||||
} as any,
|
} as any,
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export const projectCoreDatabaseSnapshot = (rows: {
|
|||||||
experience: row.experience,
|
experience: row.experience,
|
||||||
dedication: row.dedication,
|
dedication: row.dedication,
|
||||||
officerLevel: row.officerLevel,
|
officerLevel: row.officerLevel,
|
||||||
|
betray: row.betray,
|
||||||
personality: row.personality ?? null,
|
personality: row.personality ?? null,
|
||||||
specialDomestic: row.specialDomestic ?? null,
|
specialDomestic: row.specialDomestic ?? null,
|
||||||
specialWar: row.specialWar ?? null,
|
specialWar: row.specialWar ?? null,
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ const buildGeneral = (row: Record<string, unknown>, turnTime: Date): TurnGeneral
|
|||||||
dex4: readNumber(row, 'dex4', readNumber(meta, 'dex4')),
|
dex4: readNumber(row, 'dex4', readNumber(meta, 'dex4')),
|
||||||
dex5: readNumber(row, 'dex5', readNumber(meta, 'dex5')),
|
dex5: readNumber(row, 'dex5', readNumber(meta, 'dex5')),
|
||||||
explevel: readNumber(row, 'expLevel', readNumber(meta, 'explevel')),
|
explevel: readNumber(row, 'expLevel', readNumber(meta, 'explevel')),
|
||||||
|
betray: readNumber(row, 'betray', readNumber(meta, 'betray')),
|
||||||
officerCityId: readNumber(row, 'officerCityId', readNumber(meta, 'officerCityId')),
|
officerCityId: readNumber(row, 'officerCityId', readNumber(meta, 'officerCityId')),
|
||||||
block: readNumber(row, 'blockState', readNumber(meta, 'block')),
|
block: readNumber(row, 'blockState', readNumber(meta, 'block')),
|
||||||
},
|
},
|
||||||
@@ -500,6 +501,7 @@ const projectWorld = (
|
|||||||
dedication: toDatabaseInt(general.dedication),
|
dedication: toDatabaseInt(general.dedication),
|
||||||
expLevel: readNumber(general.meta, 'explevel'),
|
expLevel: readNumber(general.meta, 'explevel'),
|
||||||
officerLevel: general.officerLevel,
|
officerLevel: general.officerLevel,
|
||||||
|
betray: readNumber(general.meta, 'betray'),
|
||||||
personality: general.role.personality,
|
personality: general.role.personality,
|
||||||
specialDomestic: general.role.specialDomestic,
|
specialDomestic: general.role.specialDomestic,
|
||||||
specialWar: general.role.specialWar,
|
specialWar: general.role.specialWar,
|
||||||
|
|||||||
@@ -4231,3 +4231,427 @@ integration('nation deception target, multistep, movement, RNG, and cooldown bou
|
|||||||
120_000
|
120_000
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type ScorchedEarthOutcome = 'fallback' | 'intermediate' | 'completed';
|
||||||
|
|
||||||
|
const scorchedEarthBoundaryCases: Array<{
|
||||||
|
name: string;
|
||||||
|
destCityId: unknown;
|
||||||
|
fixturePatches?: FixturePatches;
|
||||||
|
outcome: ScorchedEarthOutcome;
|
||||||
|
coreResolution?: boolean;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
name: 'rejects a missing destination city',
|
||||||
|
destCityId: 99,
|
||||||
|
outcome: 'fallback',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rejects a destination city occupied by another nation',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: { cities: { 70: { nationId: 2 } } },
|
||||||
|
outcome: 'fallback',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rejects a neutral destination city',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: { cities: { 70: { nationId: 0 } } },
|
||||||
|
outcome: 'fallback',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rejects a source city occupied by another nation',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: { cities: { 3: { nationId: 2 } } },
|
||||||
|
outcome: 'fallback',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rejects an unsupplied source city',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: { cities: { 3: { supplyState: 0 } } },
|
||||||
|
outcome: 'fallback',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rejects an unsupplied destination city',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: { cities: { 70: { supplyState: 0 } } },
|
||||||
|
outcome: 'fallback',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rejects an actor below chief rank',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: { generals: { 1: { officerLevel: 4, officerCityId: 0 } } },
|
||||||
|
outcome: 'fallback',
|
||||||
|
coreResolution: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rejects the national capital',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: { nations: { 1: { capitalCityId: 70 } } },
|
||||||
|
outcome: 'fallback',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rejects a remaining diplomacy restriction',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: { nations: { 1: { diplomacyLimit: 1 } } },
|
||||||
|
outcome: 'fallback',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rejects any outgoing war relation',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: { diplomacy: { '1:2': { state: 0 } } },
|
||||||
|
outcome: 'fallback',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rejects an outgoing self-war relation',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: { diplomacy: { '1:1': { state: 0 } } },
|
||||||
|
outcome: 'fallback',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'starts at term one without side effects',
|
||||||
|
destCityId: 70,
|
||||||
|
outcome: 'intermediate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'continues to term two without side effects',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: {
|
||||||
|
nations: {
|
||||||
|
1: {
|
||||||
|
turnLastByOfficerLevel: {
|
||||||
|
12: { command: '초토화', arg: { destCityID: 70 }, term: 1 },
|
||||||
|
},
|
||||||
|
coreTurnLastByOfficerLevel: {
|
||||||
|
12: { command: '초토화', arg: { destCityId: 70 }, term: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outcome: 'intermediate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'restarts at term one after another command interrupted the stack',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: {
|
||||||
|
nations: {
|
||||||
|
1: {
|
||||||
|
turnLastByOfficerLevel: {
|
||||||
|
12: { command: '필사즉생', arg: {}, term: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outcome: 'intermediate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'accepts a numeric string destination city ID',
|
||||||
|
destCityId: '70',
|
||||||
|
outcome: 'completed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'truncates a fractional destination city ID',
|
||||||
|
destCityId: 70.9,
|
||||||
|
outcome: 'completed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'accepts supply state two for both source and destination',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: {
|
||||||
|
cities: {
|
||||||
|
3: { supplyState: 2 },
|
||||||
|
70: { supplyState: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outcome: 'completed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ignores a reverse-only war relation',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: { diplomacy: { '2:1': { state: 0 } } },
|
||||||
|
outcome: 'completed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'uses ten-percent floors and raises low trust on a level-eight city',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: {
|
||||||
|
cities: {
|
||||||
|
70: {
|
||||||
|
level: 8,
|
||||||
|
population: 1_000,
|
||||||
|
populationMax: 100_000,
|
||||||
|
agriculture: 10,
|
||||||
|
agricultureMax: 10_000,
|
||||||
|
commerce: 20,
|
||||||
|
commerceMax: 20_000,
|
||||||
|
security: 30,
|
||||||
|
securityMax: 30_000,
|
||||||
|
defence: 40,
|
||||||
|
defenceMax: 40_000,
|
||||||
|
wall: 50,
|
||||||
|
wallMax: 50_000,
|
||||||
|
trust: 20,
|
||||||
|
conflict: { 2: 30 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outcome: 'completed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'applies high-value ratios, recovered resources, officer penalties, and city resets',
|
||||||
|
destCityId: 70,
|
||||||
|
fixturePatches: {
|
||||||
|
generals: {
|
||||||
|
1: { experience: 1_235 },
|
||||||
|
},
|
||||||
|
cities: {
|
||||||
|
23: { nationId: 1, frontState: 1, conflict: { 2: 10 } },
|
||||||
|
70: {
|
||||||
|
level: 7,
|
||||||
|
population: 80_003,
|
||||||
|
populationMax: 100_000,
|
||||||
|
agriculture: 8_003,
|
||||||
|
agricultureMax: 10_000,
|
||||||
|
commerce: 16_003,
|
||||||
|
commerceMax: 20_000,
|
||||||
|
security: 24_003,
|
||||||
|
securityMax: 30_000,
|
||||||
|
defence: 32_003,
|
||||||
|
defenceMax: 40_000,
|
||||||
|
wall: 40_001,
|
||||||
|
wallMax: 50_000,
|
||||||
|
trust: 80,
|
||||||
|
conflict: { 2: 30 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outcome: 'completed',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const calcScorchedEarthReturnAmount = (city: Record<string, unknown>): number => {
|
||||||
|
let amount = readNumericField(city, 'population') / 5;
|
||||||
|
for (const [currentKey, maxKey] of [
|
||||||
|
['agriculture', 'agricultureMax'],
|
||||||
|
['commerce', 'commerceMax'],
|
||||||
|
['security', 'securityMax'],
|
||||||
|
] as const) {
|
||||||
|
const current = readNumericField(city, currentKey);
|
||||||
|
const max = readNumericField(city, maxKey);
|
||||||
|
amount *= (current - max * 0.5) / max + 0.8;
|
||||||
|
}
|
||||||
|
return Math.trunc(amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
integration('nation scorched-earth constraints, multistep, city values, officers, and diplomacy boundaries', () => {
|
||||||
|
it.each(scorchedEarthBoundaryCases)(
|
||||||
|
'$name matches legacy progress, destruction, resources, officers, logs, RNG, and semantic delta',
|
||||||
|
async ({ destCityId, fixturePatches, outcome, coreResolution = true }) => {
|
||||||
|
const normalizedDestCityId = Math.trunc(Number(destCityId));
|
||||||
|
const completed = outcome === 'completed';
|
||||||
|
const fallback = outcome === 'fallback';
|
||||||
|
const completionNationPatch = completed
|
||||||
|
? {
|
||||||
|
turnLastByOfficerLevel: {
|
||||||
|
12: { command: '초토화', arg: { destCityID: destCityId }, term: 2 },
|
||||||
|
},
|
||||||
|
coreTurnLastByOfficerLevel: {
|
||||||
|
12: { command: '초토화', arg: { destCityId: normalizedDestCityId }, term: 2 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
const request = buildRequest(
|
||||||
|
'che_초토화',
|
||||||
|
{ destCityID: destCityId },
|
||||||
|
{
|
||||||
|
...fixturePatches,
|
||||||
|
nations: {
|
||||||
|
...fixturePatches?.nations,
|
||||||
|
1: {
|
||||||
|
...fixturePatches?.nations?.[1],
|
||||||
|
...completionNationPatch,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cities: {
|
||||||
|
...fixturePatches?.cities,
|
||||||
|
70: {
|
||||||
|
nationId: 1,
|
||||||
|
...fixturePatches?.cities?.[70],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
generals: {
|
||||||
|
3: { betray: 1, ...fixturePatches?.generals?.[3] },
|
||||||
|
4: {
|
||||||
|
nationId: 1,
|
||||||
|
cityId: 3,
|
||||||
|
officerLevel: 5,
|
||||||
|
officerCityId: 3,
|
||||||
|
experience: 1_235,
|
||||||
|
betray: 2,
|
||||||
|
...fixturePatches?.generals?.[4],
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
nationId: 1,
|
||||||
|
cityId: 3,
|
||||||
|
officerLevel: 4,
|
||||||
|
officerCityId: 0,
|
||||||
|
experience: 1_235,
|
||||||
|
betray: 3,
|
||||||
|
...fixturePatches?.generals?.[5],
|
||||||
|
},
|
||||||
|
6: {
|
||||||
|
nationId: 2,
|
||||||
|
cityId: 70,
|
||||||
|
officerLevel: 5,
|
||||||
|
officerCityId: 70,
|
||||||
|
experience: 1_235,
|
||||||
|
betray: 4,
|
||||||
|
...fixturePatches?.generals?.[6],
|
||||||
|
},
|
||||||
|
...fixturePatches?.generals,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const reference = runReferenceTurnCommandTraceRequest(
|
||||||
|
workspaceRoot!,
|
||||||
|
request as unknown as Record<string, unknown>
|
||||||
|
);
|
||||||
|
const core = await runCoreTurnCommandTrace(request, reference.before);
|
||||||
|
|
||||||
|
expect(reference.execution.outcome).toMatchObject({ completed });
|
||||||
|
if (coreResolution) {
|
||||||
|
expect(core.execution.outcome).toMatchObject({
|
||||||
|
requestedAction: 'che_초토화',
|
||||||
|
actionKey: fallback ? '휴식' : 'che_초토화',
|
||||||
|
usedFallback: fallback,
|
||||||
|
...(fallback ? {} : { completed }),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
expect(core.execution.outcome).toBeUndefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outcome === 'intermediate') {
|
||||||
|
const expectedTerm =
|
||||||
|
fixturePatches?.nations?.[1]?.turnLastByOfficerLevel &&
|
||||||
|
(fixturePatches.nations[1].turnLastByOfficerLevel as Record<number, { command?: string }>)[12]
|
||||||
|
?.command === '초토화'
|
||||||
|
? 2
|
||||||
|
: 1;
|
||||||
|
expect(reference.execution.outcome).toMatchObject({
|
||||||
|
lastTurn: {
|
||||||
|
command: '초토화',
|
||||||
|
term: expectedTerm,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(readNationMeta(core.after.nations.find((entry) => entry.id === 1)).turn_last_12).toMatchObject({
|
||||||
|
command: '초토화',
|
||||||
|
term: expectedTerm,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const snapshot of [reference, core]) {
|
||||||
|
const nationBefore = snapshot.before.nations.find((entry) => entry.id === 1);
|
||||||
|
const nationAfter = snapshot.after.nations.find((entry) => entry.id === 1);
|
||||||
|
const cityBefore = snapshot.before.cities.find((entry) => entry.id === normalizedDestCityId);
|
||||||
|
const cityAfter = snapshot.after.cities.find((entry) => entry.id === normalizedDestCityId);
|
||||||
|
const actorBefore = snapshot.before.generals.find((entry) => entry.id === 1);
|
||||||
|
const actorAfter = snapshot.after.generals.find((entry) => entry.id === 1);
|
||||||
|
|
||||||
|
if (!completed) {
|
||||||
|
expect(readNationResource(nationAfter, 'gold')).toBe(readNationResource(nationBefore, 'gold'));
|
||||||
|
expect(readNationResource(nationAfter, 'rice')).toBe(readNationResource(nationBefore, 'rice'));
|
||||||
|
expect(readNumericField(actorAfter, 'experience')).toBe(
|
||||||
|
readNumericField(actorBefore, 'experience')
|
||||||
|
);
|
||||||
|
expect(readNumericField(actorAfter, 'dedication')).toBe(
|
||||||
|
readNumericField(actorBefore, 'dedication')
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rewardAmount = calcScorchedEarthReturnAmount(cityBefore!);
|
||||||
|
expect(readNationResource(nationAfter, 'gold') - readNationResource(nationBefore, 'gold')).toBe(
|
||||||
|
rewardAmount
|
||||||
|
);
|
||||||
|
expect(readNationResource(nationAfter, 'rice') - readNationResource(nationBefore, 'rice')).toBe(
|
||||||
|
rewardAmount
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
readNumericField(nationAfter, 'diplomacyLimit') - readNumericField(nationBefore, 'diplomacyLimit')
|
||||||
|
).toBe(24);
|
||||||
|
expect(readNumericField(actorAfter, 'experience')).toBe(
|
||||||
|
Math.round(readNumericField(actorBefore, 'experience') * 0.9 + 15)
|
||||||
|
);
|
||||||
|
expect(readNumericField(actorAfter, 'dedication') - readNumericField(actorBefore, 'dedication')).toBe(
|
||||||
|
15
|
||||||
|
);
|
||||||
|
expect(readNumericField(actorAfter, 'betray') - readNumericField(actorBefore, 'betray')).toBe(1);
|
||||||
|
|
||||||
|
for (const generalId of [3, 4, 5, 6]) {
|
||||||
|
const before = snapshot.before.generals.find((entry) => entry.id === generalId);
|
||||||
|
const after = snapshot.after.generals.find((entry) => entry.id === generalId);
|
||||||
|
const friendly = [3, 4, 5].includes(generalId);
|
||||||
|
const officer = readNumericField(before, 'officerLevel') >= 5;
|
||||||
|
expect(readNumericField(after, 'experience')).toBe(
|
||||||
|
friendly && officer
|
||||||
|
? Math.round(readNumericField(before, 'experience') * 0.9)
|
||||||
|
: readNumericField(before, 'experience')
|
||||||
|
);
|
||||||
|
expect(readNumericField(after, 'betray') - readNumericField(before, 'betray')).toBe(
|
||||||
|
friendly ? 1 : 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(cityAfter).toMatchObject({
|
||||||
|
nationId: 0,
|
||||||
|
frontState: 0,
|
||||||
|
trust: Math.max(50, readNumericField(cityBefore, 'trust')),
|
||||||
|
population: Math.max(
|
||||||
|
Math.round(readNumericField(cityBefore, 'populationMax') * 0.1),
|
||||||
|
Math.round(readNumericField(cityBefore, 'population') * 0.2)
|
||||||
|
),
|
||||||
|
agriculture: Math.max(
|
||||||
|
Math.round(readNumericField(cityBefore, 'agricultureMax') * 0.1),
|
||||||
|
Math.round(readNumericField(cityBefore, 'agriculture') * 0.2)
|
||||||
|
),
|
||||||
|
commerce: Math.max(
|
||||||
|
Math.round(readNumericField(cityBefore, 'commerceMax') * 0.1),
|
||||||
|
Math.round(readNumericField(cityBefore, 'commerce') * 0.2)
|
||||||
|
),
|
||||||
|
security: Math.max(
|
||||||
|
Math.round(readNumericField(cityBefore, 'securityMax') * 0.1),
|
||||||
|
Math.round(readNumericField(cityBefore, 'security') * 0.2)
|
||||||
|
),
|
||||||
|
defence: Math.max(
|
||||||
|
Math.round(readNumericField(cityBefore, 'defenceMax') * 0.1),
|
||||||
|
Math.round(readNumericField(cityBefore, 'defence') * 0.2)
|
||||||
|
),
|
||||||
|
wall: Math.max(
|
||||||
|
Math.round(readNumericField(cityBefore, 'wallMax') * 0.1),
|
||||||
|
Math.round(readNumericField(cityBefore, 'wall') * 0.5)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
expect(Object.keys((cityAfter?.conflict ?? {}) as object)).toHaveLength(0);
|
||||||
|
const auxBefore = readNationMeta(nationBefore);
|
||||||
|
const auxAfter = readNationMeta(nationAfter);
|
||||||
|
expect(
|
||||||
|
readNumericField(auxAfter, 'did_특성초토화') - readNumericField(auxBefore, 'did_특성초토화')
|
||||||
|
).toBe(readNumericField(cityBefore, 'level') >= 8 ? 1 : 0);
|
||||||
|
expect(snapshot.after.logs.slice(snapshot.before.logs.length).map((entry) => entry.text)).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.stringContaining('초토화했습니다'),
|
||||||
|
expect.stringContaining('초토화</> 명령'),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(reference.rng).toEqual([]);
|
||||||
|
expect(core.rng).toEqual(reference.rng);
|
||||||
|
expect(
|
||||||
|
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
||||||
|
ignoredPathPatterns: ignoredLifecyclePaths,
|
||||||
|
})
|
||||||
|
).toEqual([]);
|
||||||
|
},
|
||||||
|
120_000
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user