격노와 계략 효과의 PHP 호환 보정

This commit is contained in:
2026-09-19 10:09:18 +00:00
parent e542a9beea
commit e34c24c74d
7 changed files with 541 additions and 11 deletions
@@ -29,6 +29,8 @@ import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/c
import type { City, General, GeneralMeta, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
import { LogFormat } from '@sammo-ts/logic/logging/types.js';
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName } from '@sammo-ts/logic/actionModules/types.js';
import { LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic/scenario/constants.js';
import { searchDistance } from '@sammo-ts/logic/world/distance.js';
import { clamp } from 'es-toolkit';
import { z } from 'zod';
@@ -64,8 +66,6 @@ export interface StrategyEnvironment {
sabotageDamageMax: number;
maxSuccessProbability?: number;
getDistance?: (sourceCityId: number, destCityId: number) => number | null;
getDefenceCorrection?: (context: StrategyContext, defender: General) => number;
getInjuryProbability?: (context: StrategyContext, defender: General) => number;
}
export interface StrategyContext<
@@ -119,7 +119,65 @@ const INJURY_MAX = 80;
const randomRangeInt = (rng: RandomGenerator, min: number, max: number): number => rng.nextInt(min, max + 1);
const getStatValue = (general: General, statKey: StrategyStatKey): number => general.stats[statKey];
const createTargetActionContext = <TriggerState extends GeneralTriggerState>(
context: StrategyContext<TriggerState>,
general: General<TriggerState>,
nation: Nation | null | undefined
): GeneralActionContext<TriggerState> => ({
...context,
general,
nation: nation ?? null,
});
interface EffectiveStatOptions {
withInjury?: boolean;
withStatAdjust?: boolean;
withActions?: boolean;
truncate?: boolean;
}
// Ref General::getStatValue()의 순서와 정수화 경계를 일반 장수 커맨드에서도 사용한다.
const getEffectiveStatValue = <TriggerState extends GeneralTriggerState>(
pipeline: GeneralActionPipeline<TriggerState>,
context: GeneralActionContext<TriggerState>,
statKey: StrategyStatKey,
options: EffectiveStatOptions = {}
): number => {
const withInjury = options.withInjury ?? true;
const withStatAdjust = options.withStatAdjust ?? true;
const withActions = options.withActions ?? true;
const truncate = options.truncate ?? true;
const general = context.general;
const injuryRatio = withInjury ? (100 - general.injury) / 100 : 1;
let value = general.stats[statKey] * injuryRatio;
if (withStatAdjust && statKey === 'strength') {
value += Math.round(
getEffectiveStatValue(pipeline, context, 'intelligence', {
withInjury,
withStatAdjust: false,
withActions,
truncate: false,
}) / 4
);
} else if (withStatAdjust && statKey === 'intelligence') {
value += Math.round(
getEffectiveStatValue(pipeline, context, 'strength', {
withInjury,
withStatAdjust: false,
withActions,
truncate: false,
}) / 4
);
}
value = clamp(value, 0, LEGACY_DEFAULT_MAX_LEVEL);
if (withActions) {
value = pipeline.onCalcStat(context, statKey as GeneralStatName, value);
}
value = clamp(value, 0, LEGACY_DEFAULT_MAX_LEVEL);
return truncate ? Math.trunc(value) : value;
};
const addMetaNumber = (meta: GeneralMeta, key: string, delta: number): GeneralMeta => {
const current = typeof meta[key] === 'number' ? (meta[key] as number) : 0;
@@ -144,7 +202,8 @@ export class StrategyCommandResolver<TriggerState extends GeneralTriggerState =
}
getProbability(context: StrategyContext<TriggerState>): StrategyProbability {
const attackBase = getStatValue(context.general, this.config.statKey) / this.env.sabotageProbCoefByStat;
const attackBase =
getEffectiveStatValue(this.pipeline, context, this.config.statKey) / this.env.sabotageProbCoefByStat;
const attack = this.pipeline.onCalcDomestic(context, '계략', 'success', attackBase);
const destNationId = context.destCity.nationId;
@@ -156,8 +215,16 @@ export class StrategyCommandResolver<TriggerState extends GeneralTriggerState =
continue;
}
affectCount += 1;
maxStat = Math.max(maxStat, getStatValue(defender, this.config.statKey));
defenceCorrection += this.env.getDefenceCorrection?.(context, defender) ?? 0;
const defenderContext = createTargetActionContext(context, defender, context.destNation);
maxStat = Math.max(
maxStat,
getEffectiveStatValue(this.pipeline, defenderContext, this.config.statKey)
);
defenceCorrection = this.pipeline.onCalcStat(
defenderContext,
'sabotageDefence',
defenceCorrection
);
}
let defence = maxStat / this.env.sabotageProbCoefByStat;
@@ -203,7 +270,8 @@ export class StrategyCommandResolver<TriggerState extends GeneralTriggerState =
if (defender.nationId !== context.destCity.nationId) {
continue;
}
const injuryProbability = this.env.getInjuryProbability?.(context, defender) ?? 0.3;
const defenderContext = createTargetActionContext(context, defender, context.destNation);
const injuryProbability = this.pipeline.onCalcStat(defenderContext, 'injuryProb', 0.3);
if (!rng.nextBool(injuryProbability)) {
continue;
}
@@ -23,7 +23,7 @@ export const itemModule: ItemModule = {
if (!context.unit) return null;
return new WarTriggerCaller(
new che_격노시도(context.unit, BaseWarUnitTrigger.TYPE_ITEM),
new che_격노발동(context.unit, BaseWarUnitTrigger.TYPE_ITEM)
new che_격노발동(context.unit)
);
},
};
@@ -23,7 +23,7 @@ export const itemModule: ItemModule = {
return null;
}
const general = unit.getGeneral();
const leadership = general.stats.leadership;
const leadership = unit.getComputedStat('leadership', general.stats.leadership);
const crew = general.crew;
const crewRatio = clamp(crew / (leadership * 100), 0, 1);
return new WarTriggerCaller(new 전투력보정(unit, 1 + 0.6 * (1 - crewRatio)));
@@ -8,7 +8,7 @@ const ITEM_KEY = 'che_저지_삼황내문';
class ActivateItemHalt extends BaseWarUnitTrigger {
constructor(unit: WarUnit) {
super(unit, TriggerPriority.Pre);
super(unit, TriggerPriority.Begin);
}
protected actionWar(self: WarUnit): boolean {
@@ -3,6 +3,7 @@ import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/trigge
import { che_의술발동, che_의술시도 } from '@sammo-ts/logic/war/triggers/che_의술.js';
import { che_저격발동, che_저격시도 } from '@sammo-ts/logic/war/triggers/che_저격.js';
import { che_반계발동, che_반계시도 } from '@sammo-ts/logic/war/triggers/che_반계.js';
import { che_격노발동, che_격노시도 } from '@sammo-ts/logic/war/triggers/che_격노.js';
import type { ItemModule } from './types.js';
export const createEventBattleTraitItemModule = (
@@ -73,6 +74,14 @@ export const createEventBattleTraitItemModule = (
new che_의술발동(context.unit)
)
: null;
} else if (traitModule.key === 'che_격노') {
itemModule.getBattlePhaseTriggerList = (context) =>
context.unit
? new WarTriggerCaller(
new che_격노시도(context.unit, BaseWarUnitTrigger.TYPE_ITEM),
new che_격노발동(context.unit)
)
: null;
} else {
itemModule.getBattlePhaseTriggerList = traitModule.getBattlePhaseTriggerList;
}