merge: expand general command differential parity

This commit is contained in:
2026-07-26 00:31:44 +00:00
5 changed files with 47 additions and 24 deletions
@@ -56,9 +56,10 @@ normalized to the core `*Id` spelling before command identity comparison.
canonical projection.
- `trace.ts`: before/execute/after capture boundary.
- `compare.ts`: exact snapshot and delta comparison.
- `turnCommandGeneralMatrix.integration.test.ts`: 29 successful general
- `turnCommandGeneralMatrix.integration.test.ts`: 37 successful general
command paths, including the four-call `전투태세` completion path and
movement/return/gift/spy/sabotage commands.
movement/return/gift/spy/sabotage, recruitment, proficiency transfer,
trait reset and equipment-trade commands.
- `turnCommandNationMatrix.integration.test.ts`: 8 successful nation
command paths.
- `turnCommandCoreReference.integration.test.ts`: declaration and live
@@ -163,9 +164,9 @@ Compatibility is established per case only when:
4. live sortie also passes the battle trace comparison;
5. any ignored path is documented in the case evidence.
As of 2026-07-25, 29 general cases, 8 nation cases, declaration and live sortie
As of 2026-07-25, 37 general cases, 8 nation cases, declaration and live sortie
pass this boundary. Live sortie covers battle entry, conquest, defeated-general
neutralization and last-city nation collapse. This is 39 executable comparison
neutralization and last-city nation collapse. This is 47 executable comparison
cases, not a claim that all 55 general and 38 nation command classes have been
dynamically compared.
@@ -1,7 +1,10 @@
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import { ARGS_SCHEMA, ActionDefinition as RecruitActionDefinition } from './che_징병.js';
import {
ARGS_SCHEMA,
ActionDefinition as RecruitActionDefinition,
actionContextBuilder as recruitActionContextBuilder,
} from './che_징병.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
import type { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
@@ -13,6 +16,7 @@ export class ActionDefinition<
constructor(modules: GeneralActionModule<TriggerState>[]) {
super(modules, {
actionName: '모병',
costOffset: 2,
defaultTrain: 70, // GameConst::$defaultTrainHigh
defaultAtmos: 70, // GameConst::$defaultAtmosHigh
@@ -20,7 +24,8 @@ export class ActionDefinition<
}
}
export const actionContextBuilder = defaultActionContextBuilder;
// 모병도 징병과 동일하게 병종/지도/연도 컨텍스트가 필요하다.
export const actionContextBuilder = recruitActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_모병',
@@ -418,7 +418,7 @@ export class ActionResolver<
general.meta = addMetaNumber(general.meta, dexGain.key, dexGain.amount);
}
tryApplyUniqueLottery(context, { acquireType: '아이템', reason: ACTION_NAME });
tryApplyUniqueLottery(context, { acquireType: '아이템', reason: actionName });
return { effects: [] };
}
@@ -101,8 +101,11 @@ export class ActionResolver<
const rawGold = result.agriDamage * destCity.level * yearCoef * (0.25 + commRatio / 4);
const rawRice = result.commDamage * destCity.level * yearCoef * (0.25 + agriRatio / 4);
let stolenGold = Math.floor(rawGold);
let stolenRice = Math.floor(rawRice);
// 레거시는 탈취량을 부동소수점으로 유지한 채 국가/장수 DB 정수 필드에
// 기록할 때 반올림한다. 여기서 미리 내림하면 국고와 본국 몫이 1씩
// 달라질 수 있다.
let stolenGold = rawGold;
let stolenRice = rawRice;
const isSupplied = destCity.supplyState === 1;
@@ -119,9 +122,8 @@ export class ActionResolver<
effects.push(
createNationPatchEffect(
{
...destNation,
gold: destNation.gold - stolenGold,
rice: destNation.rice - stolenRice,
gold: Math.round(destNation.gold - stolenGold),
rice: Math.round(destNation.rice - stolenRice),
},
destNation.id
)
@@ -130,23 +132,23 @@ export class ActionResolver<
effects.push(
createCityPatchEffect(
{
...destCity,
state: 34,
// 레거시는 같은 명령 안에서 잠시 34로 쓴 뒤 최종 32로
// 덮어쓴다. 관찰 가능한 최종 상태는 32다.
state: 32,
},
args.destCityId
)
);
} else {
const commDmg = Math.floor(stolenGold / 12);
const agriDmg = Math.floor(stolenRice / 12);
const commDmg = stolenGold / 12;
const agriDmg = stolenRice / 12;
effects.push(
createCityPatchEffect(
{
...destCity,
commerce: Math.max(0, destCity.commerce - commDmg),
agriculture: Math.max(0, destCity.agriculture - agriDmg),
state: 34,
commerce: Math.round(Math.max(0, destCity.commerce - commDmg)),
agriculture: Math.round(Math.max(0, destCity.agriculture - agriDmg)),
state: 32,
},
args.destCityId
)
@@ -165,7 +167,6 @@ export class ActionResolver<
effects.push(
createNationPatchEffect(
{
...nation,
gold: nation.gold + nationShareGold,
rice: nation.rice + nationShareRice,
},
@@ -186,8 +187,8 @@ export class ActionResolver<
});
consumeSuccessfulStrategyItem(this.pipeline, context);
general.gold += myShareGold;
general.rice += myShareRice;
general.gold = Math.round(general.gold + myShareGold);
general.rice = Math.round(general.rice + myShareRice);
return { effects };
}
@@ -190,6 +190,22 @@ const cases: Array<[string, Record<string, unknown> | undefined, Record<string,
['che_첩보', { destCityID: 70 }, undefined],
['che_화계', { destCityID: 70 }, undefined],
['che_파괴', { destCityID: 70 }, undefined],
['che_선동', { destCityID: 70 }, undefined],
['che_탈취', { destCityID: 70 }, undefined],
['che_모병', { crewType: 1100, amount: 100 }, undefined],
['che_징병', { crewType: 1100, amount: 100 }, undefined],
['che_숙련전환', { srcArmType: 1, destArmType: 2 }, { dex1: 100 }],
[
'che_내정특기초기화',
undefined,
{ specialDomestic: 'che_인덕', lastTurn: { command: '내정 특기 초기화', term: 1 } },
],
[
'che_전투특기초기화',
undefined,
{ specialWar: 'che_귀병', lastTurn: { command: '전투 특기 초기화', term: 1 } },
],
['che_장비매매', { itemType: 'weapon', itemCode: 'che_무기_01_단도' }, undefined],
];
integration('general command success matrix', () => {