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 {
|
||||
beChief,
|
||||
@@ -15,20 +15,22 @@ import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} 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 type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
import { normalizeLegacyIntegerArg, parseArgsWithSchema } from '../parseArgs.js';
|
||||
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destCityId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
destCityId: z.preprocess(normalizeLegacyIntegerArg, z.number().int().positive()),
|
||||
});
|
||||
export type ScorchedEarthArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
@@ -37,6 +39,7 @@ export interface ScorchedEarthResolveContext<
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destCity: City;
|
||||
destNation: Nation | null;
|
||||
friendlyGenerals: Array<General<TriggerState>>;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '초토화';
|
||||
@@ -61,9 +64,9 @@ const calcReturnAmount = (destCity: City): number => {
|
||||
|
||||
const calcReducedValue = (current: number, max: number, ratio: number): number => {
|
||||
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 reducedSecu = calcReducedValue(destCity.security, destCity.securityMax, 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 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.meta = {
|
||||
...general.meta,
|
||||
betray: Number(general.meta.betray ?? 0) + 1,
|
||||
};
|
||||
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||
createCityPatchEffect(
|
||||
@@ -159,6 +166,7 @@ export class ActionDefinition<
|
||||
security: reducedSecu,
|
||||
defence: reducedDef,
|
||||
wall: reducedWall,
|
||||
conflict: {},
|
||||
meta: {
|
||||
...destCity.meta,
|
||||
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 };
|
||||
}
|
||||
}
|
||||
@@ -229,10 +257,12 @@ export const actionContextBuilder: ActionContextBuilder<ScorchedEarthArgs> = (ba
|
||||
return null;
|
||||
}
|
||||
const destNation = worldRef.getNationById(destCity.nationId);
|
||||
const friendlyGenerals = worldRef.listGenerals().filter((general) => general.nationId === base.general.nationId);
|
||||
return {
|
||||
...base,
|
||||
destCity,
|
||||
destNation: destNation ?? null,
|
||||
friendlyGenerals,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -469,6 +469,7 @@ describe('Nation Missing Actions', () => {
|
||||
nation,
|
||||
destCity,
|
||||
destNation,
|
||||
friendlyGenerals: [general],
|
||||
rng: {} as any,
|
||||
addLog: () => {},
|
||||
} as any,
|
||||
|
||||
@@ -116,6 +116,7 @@ export const projectCoreDatabaseSnapshot = (rows: {
|
||||
experience: row.experience,
|
||||
dedication: row.dedication,
|
||||
officerLevel: row.officerLevel,
|
||||
betray: row.betray,
|
||||
personality: row.personality ?? null,
|
||||
specialDomestic: row.specialDomestic ?? 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')),
|
||||
dex5: readNumber(row, 'dex5', readNumber(meta, 'dex5')),
|
||||
explevel: readNumber(row, 'expLevel', readNumber(meta, 'explevel')),
|
||||
betray: readNumber(row, 'betray', readNumber(meta, 'betray')),
|
||||
officerCityId: readNumber(row, 'officerCityId', readNumber(meta, 'officerCityId')),
|
||||
block: readNumber(row, 'blockState', readNumber(meta, 'block')),
|
||||
},
|
||||
@@ -500,6 +501,7 @@ const projectWorld = (
|
||||
dedication: toDatabaseInt(general.dedication),
|
||||
expLevel: readNumber(general.meta, 'explevel'),
|
||||
officerLevel: general.officerLevel,
|
||||
betray: readNumber(general.meta, 'betray'),
|
||||
personality: general.role.personality,
|
||||
specialDomestic: general.role.specialDomestic,
|
||||
specialWar: general.role.specialWar,
|
||||
|
||||
@@ -4231,3 +4231,427 @@ integration('nation deception target, multistep, movement, RNG, and cooldown bou
|
||||
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