From 828dceae13fd71e45f04beca90cd671cbb42621e Mon Sep 17 00:00:00 2001 From: hided62 Date: Sat, 25 Jul 2026 07:04:47 +0000 Subject: [PATCH] fix: align item battle progression with legacy --- .../logic/src/items/che_명마_12_옥란백용구.ts | 10 +++- packages/logic/src/items/che_저지_삼황내문.ts | 21 ++++++-- packages/logic/src/war/units/general.ts | 22 ++++++-- packages/logic/test/warEngine.test.ts | 52 +++++++++++++++++++ .../test/battleDifferential.test.ts | 22 ++++++-- 5 files changed, 112 insertions(+), 15 deletions(-) diff --git a/packages/logic/src/items/che_명마_12_옥란백용구.ts b/packages/logic/src/items/che_명마_12_옥란백용구.ts index 0bbb548..8045b7a 100644 --- a/packages/logic/src/items/che_명마_12_옥란백용구.ts +++ b/packages/logic/src/items/che_명마_12_옥란백용구.ts @@ -25,14 +25,20 @@ export const itemModule: ItemModule = { context: GeneralActionContext | WarActionContext, statName: GeneralStatName | WarStatName, value: number | [number, number], - _aux?: unknown + aux?: unknown ): number | [number, number] { let newValue: number | [number, number] = value; if (statName === 'leadership' && typeof value === 'number') { newValue = value + STAT_VALUE; } if (statName === 'warAvoidRatio' && typeof newValue === 'number') { - const { leadership } = context.general.stats; + const leadership = + typeof aux === 'object' && + aux !== null && + 'leadership' in aux && + typeof aux.leadership === 'number' + ? aux.leadership + : context.general.stats.leadership + STAT_VALUE; const crewL = context.general.crew / 100; const boost = (1 - crewL / leadership) * 0.5; return newValue + Math.min(Math.max(boost, 0), 0.5); diff --git a/packages/logic/src/items/che_저지_삼황내문.ts b/packages/logic/src/items/che_저지_삼황내문.ts index 197b32e..8494eea 100644 --- a/packages/logic/src/items/che_저지_삼황내문.ts +++ b/packages/logic/src/items/che_저지_삼황내문.ts @@ -1,9 +1,22 @@ -import { che_저지, che_저지_시도 } from '@sammo-ts/logic/war/triggers/che_저지.js'; -import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js'; +import { TriggerPriority } from '@sammo-ts/logic/triggers/core.js'; +import { che_저지 } from '@sammo-ts/logic/war/triggers/che_저지.js'; +import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js'; +import type { WarUnit } from '@sammo-ts/logic/war/units.js'; import type { ItemModule } from './types.js'; const ITEM_KEY = 'che_저지_삼황내문'; +class ActivateItemHalt extends BaseWarUnitTrigger { + constructor(unit: WarUnit) { + super(unit, TriggerPriority.Pre); + } + + protected actionWar(self: WarUnit): boolean { + self.activateSkill('특수', '저지'); + return true; + } +} + export const itemModule: ItemModule = { key: ITEM_KEY, rawName: '삼황내문', @@ -28,10 +41,10 @@ export const itemModule: ItemModule = { if (haltCount >= 2) { return null; } - if (haltCount === 1 && unit.getPhase() === 0 && !unit.rng.nextBool(0.5)) { + if (haltCount === 1 && unit.getPhase() === 0 && unit.rng.nextBool(0.5)) { return null; } - return new WarTriggerCaller(new che_저지_시도(unit), new che_저지(unit)); + return new WarTriggerCaller(new ActivateItemHalt(unit), new che_저지(unit)); }, }; diff --git a/packages/logic/src/war/units/general.ts b/packages/logic/src/war/units/general.ts index 9c21017..053cfab 100644 --- a/packages/logic/src/war/units/general.ts +++ b/packages/logic/src/war/units/general.ts @@ -30,6 +30,7 @@ const META_RANK_PREFIX = 'rank_'; const META_INTEL_EXP = 'intelExp'; const META_STRENGTH_EXP = 'strengthExp'; const META_LEADERSHIP_EXP = 'leadershipExp'; +const MAX_EXP_LEVEL = 255; const RANK_WARNUM = `${META_RANK_PREFIX}warnum`; const RANK_KILLNUM = `${META_RANK_PREFIX}killnum`; @@ -295,7 +296,10 @@ export class WarUnitGeneral< let avoidRatio = this.getCrewType().avoid / 100; avoidRatio *= this.getComputedTrain() / 100; - const aux = { isAttacker: this.isAttacker() }; + const aux = { + isAttacker: this.isAttacker(), + leadership: this.getComputedStat('leadership', this.general.stats.leadership), + }; avoidRatio = this.actionPipeline.onCalcStat(this.getActionContext(), 'warAvoidRatio', avoidRatio, aux); avoidRatio = this.resolveOpposeStatValue('warAvoidRatio', avoidRatio, aux); @@ -346,8 +350,14 @@ export class WarUnitGeneral< } public addLevelExp(value: number): void { - const adjust = this.isAttacker() ? value : value * 0.8; - this.general.experience += adjust; + const sideAdjusted = this.isAttacker() ? value : value * 0.8; + const adjusted = this.actionPipeline.onCalcStat(this.getActionContext(), 'experience', sideAdjusted); + this.general.experience += adjusted; + const nextExpLevel = + this.general.experience < 1000 + ? Math.trunc(this.general.experience / 100) + : Math.trunc(Math.sqrt(this.general.experience / 10)); + this.general.meta[META_EXP_LEVEL] = clamp(nextExpLevel, 0, MAX_EXP_LEVEL); } public addDedication(value: number): void { @@ -361,7 +371,11 @@ export class WarUnitGeneral< : crewType.armType; const key = `${META_DEX_PREFIX}${armType}`; const base = getMetaNumber(this.general.meta, key); - const adjustedExp = this.actionPipeline.onCalcStat(this.getActionContext(), 'addDex', exp, { armType }); + let nextExp = exp; + if (armType === this.config.armTypes.wizard || armType === this.config.armTypes.siege) { + nextExp *= 0.9; + } + const adjustedExp = this.actionPipeline.onCalcStat(this.getActionContext(), 'addDex', nextExp, { armType }); this.general.meta[key] = base + adjustedExp; } diff --git a/packages/logic/test/warEngine.test.ts b/packages/logic/test/warEngine.test.ts index 552429b..185e045 100644 --- a/packages/logic/test/warEngine.test.ts +++ b/packages/logic/test/warEngine.test.ts @@ -166,6 +166,58 @@ const buildGeneral = (strength: number): General => ({ }); describe('war triggers', () => { + it('updates the legacy experience level and applies item experience modifiers immediately', () => { + const general = buildGeneral(80); + general.experience = 90; + general.meta.explevel = 0; + const crewType = new WarCrewType(buildUnitSet().crewTypes![0]!); + const unit = new WarUnitGeneral( + new RandUtil(new ConstantRNG(0)), + buildConfig(), + general, + buildCity(), + buildNation(), + true, + crewType, + new ActionLogger({ generalId: 1, nationId: 1 }), + new WarActionPipeline([ + { + onCalcStat: (_context, statName, value) => + statName === 'experience' ? (value as number) * 1.2 : value, + }, + ]) + ); + + unit.addLevelExp(10); + expect(general.experience).toBe(102); + expect(general.meta.explevel).toBe(1); + }); + + it('applies the legacy 90% dexterity gain for wizard and siege arms', () => { + const general = buildGeneral(80); + general.meta.dex5 = 5000; + const siegeCrew = new WarCrewType({ + ...buildUnitSet().crewTypes![0]!, + id: 101, + armType: 5, + name: '공성병', + }); + const unit = new WarUnitGeneral( + new RandUtil(new ConstantRNG(0)), + buildConfig(), + general, + buildCity(), + buildNation(), + true, + siegeCrew, + new ActionLogger({ generalId: 1, nationId: 1 }), + new WarActionPipeline([]) + ); + + unit.addDex(siegeCrew, 100); + expect(general.meta.dex5).toBe(5090); + }); + it('activates and applies critical damage', async () => { const rng = new RandUtil(new ConstantRNG(0)); const config = buildConfig(); diff --git a/tools/integration-tests/test/battleDifferential.test.ts b/tools/integration-tests/test/battleDifferential.test.ts index 03a9575..f92d9e1 100644 --- a/tools/integration-tests/test/battleDifferential.test.ts +++ b/tools/integration-tests/test/battleDifferential.test.ts @@ -691,12 +691,12 @@ describeWithReference('ref ↔ core2026 battle differential', () => { process.env['ITEM_PARITY_DEBUG'] === '1' ? `\ncore=${JSON.stringify( coreEvents - .map((event, index) => ({ index, event })) - .filter(({ event }) => event.event === 'phase_damage') + .map((event, index) => ({ index, event })) + .filter(({ event }) => event.event === 'phase_power' || event.event === 'phase_damage') )}\nref=${JSON.stringify( reference.events - .map((event, index) => ({ index, event })) - .filter(({ event }) => event.event === 'phase_damage') + .map((event, index) => ({ index, event })) + .filter(({ event }) => event.event === 'phase_power' || event.event === 'phase_damage') )}` : ''; failures.push(`${itemKey}: ${error instanceof Error ? error.message : String(error)}${debug}`); @@ -791,7 +791,19 @@ describeWithReference('ref ↔ core2026 battle differential', () => { try { assertTraceParity(coreEvents, reference, coreRng); } catch (error) { - failures.push(`${itemKey}: ${error instanceof Error ? error.message : String(error)}`); + const debug = + process.env['ITEM_PARITY_DEBUG'] === '1' + ? `\ncore=${JSON.stringify( + coreEvents + .map((event, index) => ({ index, event })) + .filter(({ event }) => event.event === 'phase_power' || event.event === 'phase_damage') + )}\nref=${JSON.stringify( + reference.events + .map((event, index) => ({ index, event })) + .filter(({ event }) => event.event === 'phase_power' || event.event === 'phase_damage') + )}` + : ''; + failures.push(`${itemKey}: ${error instanceof Error ? error.message : String(error)}${debug}`); } } expect(failures, failures.join('\n')).toEqual([]);