fix: align item battle progression with legacy
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
Reference in New Issue
Block a user