fix: 전투 이펙트와 차등 하네스 호환성을 복원한다
Ref 기준의 판정 순서, 난수 소비, 아이템·특기 이펙트와 로그 형식을 맞춘다. 전체 전투 결과와 이벤트·상태·난수 trace를 엄격 비교하고 fixture 및 출병 예약 회귀를 보강한다.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { JosaUtil, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
||||
|
||||
import type { City, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { City, General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import { compileCrewTypeCatalog, isCrewTypeWarActionRouter } from '@sammo-ts/logic/crewType/catalog.js';
|
||||
import { ActionLogger } from '@sammo-ts/logic/logging/actionLogger.js';
|
||||
import { LogFormat, type LogEntryDraft } from '@sammo-ts/logic/logging/types.js';
|
||||
@@ -110,6 +110,17 @@ const isSupplyCity = (city: City): boolean => {
|
||||
return city.supplyState > 0;
|
||||
};
|
||||
|
||||
const resolveLegacyTurnHourMinute = (general: General): string => {
|
||||
const raw = general.meta['turnTime'];
|
||||
if (typeof raw === 'string' && raw.length >= 16) {
|
||||
return raw.slice(11, 16);
|
||||
}
|
||||
if (general.turnTime instanceof Date && Number.isFinite(general.turnTime.getTime())) {
|
||||
return general.turnTime.toISOString().slice(11, 16);
|
||||
}
|
||||
return '00:00';
|
||||
};
|
||||
|
||||
export const computeBattleOrder = <TriggerState extends GeneralTriggerState>(
|
||||
defender: WarUnit<TriggerState>,
|
||||
attacker: WarUnitGeneral<TriggerState>
|
||||
@@ -390,7 +401,8 @@ export const resolveWarBattle = <TriggerState extends GeneralTriggerState = Gene
|
||||
const attackerNationName = (attackerUnit.getNationVar('name') as string | null) ?? 'UNKNOWN';
|
||||
const attackerName = attackerUnit.getName();
|
||||
const cityName = cityUnit.getName();
|
||||
const seedText = input.seed ? `<span class="hidden_but_copyable">(전투시드: ${input.seed})</span>` : '';
|
||||
const seedText = input.seed ? `<span class='hidden_but_copyable'>(전투시드: ${input.seed})</span>` : '';
|
||||
const turnHourMinute = resolveLegacyTurnHourMinute(attackerUnit.getGeneral());
|
||||
|
||||
const josaRo = JosaUtil.pick(cityName, '로');
|
||||
const josaYi = JosaUtil.pick(attackerName, '이');
|
||||
@@ -400,7 +412,7 @@ export const resolveWarBattle = <TriggerState extends GeneralTriggerState = Gene
|
||||
LogFormat.MONTH
|
||||
);
|
||||
attackerLogger.pushGeneralActionLog(
|
||||
`<G><b>${cityName}</b></>${josaRo} <M>진격</>합니다.${seedText}`,
|
||||
`<G><b>${cityName}</b></>${josaRo} <M>진격</>합니다.${seedText} <1>${turnHourMinute}</>`,
|
||||
LogFormat.MONTH
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { RandUtil } from '@sammo-ts/common';
|
||||
import { JosaUtil, type RandUtil } from '@sammo-ts/common';
|
||||
|
||||
import type { General } from '@sammo-ts/logic/domain/entities.js';
|
||||
import { getLegacyBattleItemIdentity, removeEquippedItem } from '@sammo-ts/logic/items/inventory.js';
|
||||
import { LogFormat } from '@sammo-ts/logic/logging/types.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;
|
||||
@@ -97,12 +98,6 @@ export abstract class BaseWarUnitTrigger implements WarTrigger {
|
||||
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;
|
||||
};
|
||||
@@ -110,7 +105,18 @@ export abstract class BaseWarUnitTrigger implements WarTrigger {
|
||||
if (!general) {
|
||||
return false;
|
||||
}
|
||||
const item = getLegacyBattleItemIdentity(general);
|
||||
this.unit.activateSkill(item.name);
|
||||
if (this.raiseType !== BaseWarUnitTrigger.TYPE_CONSUMABLE_ITEM) {
|
||||
return false;
|
||||
}
|
||||
if (this.unit.hasActivatedSkill('아이템소모')) {
|
||||
return false;
|
||||
}
|
||||
this.unit.activateSkill('아이템소모');
|
||||
return removeEquippedItem(general, 'item') !== null;
|
||||
const josaUl = JosaUtil.pick(item.rawName, '을');
|
||||
this.unit.getLogger().pushGeneralActionLog(`<C>${item.name}</>${josaUl} 사용!`, LogFormat.PLAIN);
|
||||
removeEquippedItem(general, 'item');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { TriggerPriority } from '@sammo-ts/logic/triggers/core.js';
|
||||
import { BaseWarUnitTrigger } from '../triggers.js';
|
||||
import type { WarUnit } from '../units.js';
|
||||
|
||||
export class che_부적 extends BaseWarUnitTrigger {
|
||||
constructor(unit: WarUnit, raiseType: number = 0) {
|
||||
super(unit, 0, raiseType);
|
||||
super(unit, TriggerPriority.Begin, raiseType);
|
||||
}
|
||||
|
||||
protected actionWar(self: WarUnit): boolean {
|
||||
self.activateSkill('저격불가', '부상무효');
|
||||
protected actionWar(_self: WarUnit, oppose: WarUnit): boolean {
|
||||
// Ref WarActivateSkills(..., isSelf=false): the talisman's owner is
|
||||
// injury-proof, while the opposing unit is prevented from sniping.
|
||||
oppose.activateSkill('저격불가');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import type { WarTriggerModule } from './types.js';
|
||||
|
||||
// 의술: 치료 시도
|
||||
export class che_의술시도 extends BaseWarUnitTrigger {
|
||||
constructor(unit: WarUnit) {
|
||||
super(unit, TriggerPriority.Pre + 350);
|
||||
constructor(unit: WarUnit, raiseType = BaseWarUnitTrigger.TYPE_NONE) {
|
||||
super(unit, TriggerPriority.Pre + 350, raiseType);
|
||||
}
|
||||
|
||||
protected actionWar(
|
||||
@@ -36,8 +36,8 @@ export class che_의술시도 extends BaseWarUnitTrigger {
|
||||
|
||||
// 의술: 치료 발동
|
||||
export class che_의술발동 extends BaseWarUnitTrigger {
|
||||
constructor(unit: WarUnit) {
|
||||
super(unit, TriggerPriority.Post + 550);
|
||||
constructor(unit: WarUnit, raiseType = BaseWarUnitTrigger.TYPE_NONE) {
|
||||
super(unit, TriggerPriority.Post + 550, raiseType);
|
||||
}
|
||||
|
||||
protected actionWar(
|
||||
|
||||
@@ -58,7 +58,9 @@ export class che_저지 extends BaseWarUnitTrigger {
|
||||
self.addDex(self.getCrewType(), calcDamage);
|
||||
|
||||
self.addLevelExp(calcDamage / 50);
|
||||
let rice = self.calcRiceConsumption(calcDamage);
|
||||
// Ref calcRiceConsumption() declares an int parameter, so the
|
||||
// fractional 90% counter-damage is truncated before rice cost.
|
||||
let rice = self.calcRiceConsumption(Math.trunc(calcDamage));
|
||||
rice *= 0.25;
|
||||
const general = self.getGeneral();
|
||||
general.rice = Math.max(0, general.rice - rice);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { TriggerPriority } from '@sammo-ts/logic/triggers/core.js';
|
||||
import { BaseWarUnitTrigger } from '../triggers.js';
|
||||
import type { WarUnit } from '../units.js';
|
||||
|
||||
export class che_진압 extends BaseWarUnitTrigger {
|
||||
constructor(unit: WarUnit, raiseType: number = 0) {
|
||||
super(unit, 0, raiseType);
|
||||
super(unit, TriggerPriority.Begin, raiseType);
|
||||
}
|
||||
|
||||
protected actionWar(self: WarUnit): boolean {
|
||||
self.activateSkill('반계불가', '격노불가');
|
||||
protected actionWar(_self: WarUnit, oppose: WarUnit): boolean {
|
||||
// Ref's 진압 is an opposing-unit restriction, not a self debuff.
|
||||
oppose.activateSkill('반계불가', '격노불가');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,10 +192,16 @@ export class WarUnitGeneral<
|
||||
return truncate ? Math.trunc(clamped) : clamped;
|
||||
}
|
||||
|
||||
private resolveMainStat(armType: number, withInjury = true): number {
|
||||
const leadership = this.getComputedStat('leadership', this.general.stats.leadership, { withInjury });
|
||||
const strength = this.getComputedStat('strength', this.general.stats.strength, { withInjury });
|
||||
const intelligence = this.getComputedStat('intelligence', this.general.stats.intelligence, { withInjury });
|
||||
private resolveMainStat(armType: number, withInjury = true, truncate = true): number {
|
||||
const leadership = this.getComputedStat('leadership', this.general.stats.leadership, {
|
||||
withInjury,
|
||||
truncate,
|
||||
});
|
||||
const strength = this.getComputedStat('strength', this.general.stats.strength, { withInjury, truncate });
|
||||
const intelligence = this.getComputedStat('intelligence', this.general.stats.intelligence, {
|
||||
withInjury,
|
||||
truncate,
|
||||
});
|
||||
|
||||
if (armType === this.config.armTypes.wizard) {
|
||||
return intelligence;
|
||||
@@ -279,7 +285,10 @@ export class WarUnitGeneral<
|
||||
return 0;
|
||||
}
|
||||
|
||||
const mainStat = this.resolveMainStat(armType, false);
|
||||
// GameUnitDetail::getCriticalRatio requests each Ref stat with
|
||||
// useFloor=false, so action bonuses such as 징병's +25% leadership must
|
||||
// retain their fractional part until after the probability is formed.
|
||||
const mainStat = this.resolveMainStat(armType, false, false);
|
||||
const coef =
|
||||
armType === this.config.armTypes.wizard ||
|
||||
armType === this.config.armTypes.siege ||
|
||||
|
||||
Reference in New Issue
Block a user