feat: complete legacy unique item battle parity

This commit is contained in:
2026-07-25 06:47:56 +00:00
parent 35935c9eca
commit 0d7ede3d93
45 changed files with 1143 additions and 174 deletions
+21 -2
View File
@@ -1,7 +1,9 @@
import type { RandUtil } from '@sammo-ts/common';
import type { General } from '@sammo-ts/logic/domain/entities.js';
import { TriggerCaller, type Trigger } from '@sammo-ts/logic/triggers/core.js';
import type { WarUnit } from './units.js';
import { removeEquippedItem } from '@sammo-ts/logic/items/inventory.js';
export interface WarTriggerContext {
rng: RandUtil;
@@ -87,11 +89,28 @@ export abstract class BaseWarUnitTrigger implements WarTrigger {
opposeEnv: Record<string, unknown>
): boolean;
// 아이템 소모 트리거는 아직 미구현. 필요 시 raiseType을 활용해 확장 가능하다.
public processConsumableItem(): boolean {
if (!(this.raiseType & BaseWarUnitTrigger.TYPE_ITEM)) {
return false;
}
return false;
if (this.unit.hasActivatedSkill('아이템사용')) {
return false;
}
this.unit.activateSkill('아이템사용');
if (this.raiseType !== BaseWarUnitTrigger.TYPE_CONSUMABLE_ITEM) {
return false;
}
if (this.unit.hasActivatedSkill('아이템소모')) {
return false;
}
const unit = this.unit as WarUnit & {
getGeneral?: () => General;
};
const general = unit.getGeneral?.();
if (!general) {
return false;
}
this.unit.activateSkill('아이템소모');
return removeEquippedItem(general, 'item') !== null;
}
}
@@ -0,0 +1,24 @@
import { TriggerPriority } from '@sammo-ts/logic/triggers/core.js';
import { BaseWarUnitTrigger } from '@sammo-ts/logic/war/triggers.js';
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
export class extends BaseWarUnitTrigger {
constructor(
unit: WarUnit,
private readonly attackMultiplier: number,
private readonly defenceMultiplier = 1
) {
super(unit, TriggerPriority.Begin + 20);
}
protected actionWar(
self: WarUnit,
oppose: WarUnit,
_selfEnv: Record<string, unknown>,
_opposeEnv: Record<string, unknown>
): boolean {
self.multiplyWarPowerMultiply(this.attackMultiplier);
oppose.multiplyWarPowerMultiply(this.defenceMultiplier);
return true;
}
}
+30 -13
View File
@@ -15,14 +15,7 @@ import { getTechAbility, getTechCost } from '@sammo-ts/logic/world/unitSet.js';
import type { WarActionPipeline, WarActionContext } from '../actions.js';
import type { WarEngineConfig } from '../types.js';
import type { WarCrewType } from '../crewType.js';
import {
clamp,
clampMin,
getMetaNumber,
getMetaString,
increaseMetaNumber,
round,
} from '../utils.js';
import { clamp, clampMin, getMetaNumber, getMetaString, increaseMetaNumber, round } from '../utils.js';
import { WAR_CRITICAL_RANGE, WarUnit, resolveNationTech } from './base.js';
type CityUnit = WarUnit & { getCityId: () => number };
@@ -150,24 +143,47 @@ export class WarUnitGeneral<
public getComputedStat(
statName: keyof StatBlock,
base: number,
options: { withInjury?: boolean; withStatAdjust?: boolean; withActions?: boolean } = {}
options: {
withInjury?: boolean;
withStatAdjust?: boolean;
withActions?: boolean;
truncate?: boolean;
} = {}
): number {
const withInjury = options.withInjury ?? true;
const withStatAdjust = options.withStatAdjust ?? true;
const withActions = options.withActions ?? true;
const truncate = options.truncate ?? true;
const injuryRatio = withInjury ? (100 - this.general.injury) / 100 : 1;
let value = base * injuryRatio;
if (withStatAdjust && statName === 'strength') {
value += round((this.general.stats.intelligence * injuryRatio) / 4);
value += round(
this.getComputedStat('intelligence', this.general.stats.intelligence, {
withInjury,
withStatAdjust: false,
withActions,
truncate: false,
}) / 4
);
} else if (withStatAdjust && statName === 'intelligence') {
value += round((this.general.stats.strength * injuryRatio) / 4);
value += round(
this.getComputedStat('strength', this.general.stats.strength, {
withInjury,
withStatAdjust: false,
withActions,
truncate: false,
}) / 4
);
}
const maxGeneralStat = this.config.maxGeneralStat ?? 255;
value = clamp(value, 0, maxGeneralStat);
if (withActions) {
value = this.actionPipeline.onCalcStat(this.getActionContext(), statName, value);
}
return clamp(value, 0, maxGeneralStat);
// Legacy General::getStatValue() applies every action and then returns
// Util::toInt(), which truncates toward zero on every stat read.
const clamped = clamp(value, 0, maxGeneralStat);
return truncate ? Math.trunc(clamped) : clamped;
}
private resolveMainStat(armType: number, withInjury = true): number {
@@ -345,7 +361,8 @@ export class WarUnitGeneral<
: crewType.armType;
const key = `${META_DEX_PREFIX}${armType}`;
const base = getMetaNumber(this.general.meta, key);
this.general.meta[key] = round(base + exp);
const adjustedExp = this.actionPipeline.onCalcStat(this.getActionContext(), 'addDex', exp, { armType });
this.general.meta[key] = base + adjustedExp;
}
public calcRiceConsumption(damage: number): number {