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
@@ -25,6 +25,8 @@ import type { ActionContextBase, ActionContextOptions } from '@sammo-ts/logic/ac
import type { GeneralTurnCommandSpec } from './index.js';
import { JosaUtil } from '@sammo-ts/common';
import { parseArgsWithSchema } from '../parseArgs.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import { consumeSuccessfulStrategyItem } from './strategyItemConsumption.js';
export interface AgitateResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState,
@@ -44,6 +46,11 @@ export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, AgitateArgs> {
readonly key = ACTION_KEY;
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined> = []) {
this.pipeline = new GeneralActionPipeline(modules);
}
resolve(context: GeneralActionResolveContext<TriggerState>, args: AgitateArgs): GeneralActionOutcome<TriggerState> {
const ctx = context as AgitateResolveContext<TriggerState>;
@@ -86,8 +93,8 @@ export class ActionResolver<
1
)}</>만큼 감소하고, 장수 <C>${injuryCount}</>명이 부상 당했습니다.`,
{
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
}
);
@@ -107,6 +114,8 @@ export class ActionResolver<
)
);
consumeSuccessfulStrategyItem(this.pipeline, context);
// General Update (Cost + Exp + LeaderExp)
effects.push(
createGeneralPatchEffect(
@@ -137,8 +146,10 @@ export class ActionDefinition<
public readonly name = ACTION_NAME;
private readonly resolver: ActionResolver<TriggerState>;
constructor(_env: TurnCommandEnv) {
this.resolver = new ActionResolver();
constructor(env: TurnCommandEnv) {
this.resolver = new ActionResolver<TriggerState>(
(env.generalActionModules ?? []) as GeneralActionModule<TriggerState>[]
);
}
parseArgs(raw: unknown): AgitateArgs | null {
@@ -29,6 +29,8 @@ import type { ActionContextBase, ActionContextOptions } from '@sammo-ts/logic/ac
import type { GeneralTurnCommandSpec } from './index.js';
import { JosaUtil } from '@sammo-ts/common';
import { parseArgsWithSchema } from '../parseArgs.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import { consumeSuccessfulStrategyItem } from './strategyItemConsumption.js';
export interface SeizeResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState,
@@ -50,6 +52,11 @@ export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, SeizeArgs> {
readonly key = ACTION_KEY;
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined> = []) {
this.pipeline = new GeneralActionPipeline(modules);
}
resolve(context: GeneralActionResolveContext<TriggerState>, args: SeizeArgs): GeneralActionOutcome<TriggerState> {
const ctx = context as SeizeResolveContext<TriggerState>;
@@ -166,6 +173,8 @@ export class ActionResolver<
format: LogFormat.PLAIN,
});
consumeSuccessfulStrategyItem(this.pipeline, context);
effects.push(
createGeneralPatchEffect(
{
@@ -195,8 +204,10 @@ export class ActionDefinition<
public readonly name = ACTION_NAME;
private readonly resolver: ActionResolver<TriggerState>;
constructor(_env: TurnCommandEnv) {
this.resolver = new ActionResolver();
constructor(env: TurnCommandEnv) {
this.resolver = new ActionResolver<TriggerState>(
(env.generalActionModules ?? []) as GeneralActionModule<TriggerState>[]
);
}
parseArgs(raw: unknown): SeizeArgs | null {
@@ -25,6 +25,8 @@ import type { ActionContextBase, ActionContextOptions } from '@sammo-ts/logic/ac
import type { GeneralTurnCommandSpec } from './index.js';
import { JosaUtil } from '@sammo-ts/common';
import { parseArgsWithSchema } from '../parseArgs.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import { consumeSuccessfulStrategyItem } from './strategyItemConsumption.js';
export interface DestroyResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState,
@@ -44,6 +46,11 @@ export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, DestroyArgs> {
readonly key = ACTION_KEY;
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined> = []) {
this.pipeline = new GeneralActionPipeline(modules);
}
resolve(context: GeneralActionResolveContext<TriggerState>, args: DestroyArgs): GeneralActionOutcome<TriggerState> {
const ctx = context as DestroyResolveContext<TriggerState>;
@@ -102,6 +109,8 @@ export class ActionResolver<
)
);
consumeSuccessfulStrategyItem(this.pipeline, context);
// General Update (Cost + Exp)
effects.push(
createGeneralPatchEffect(
@@ -132,8 +141,10 @@ export class ActionDefinition<
public readonly name = ACTION_NAME;
private readonly resolver: ActionResolver<TriggerState>;
constructor(_env: TurnCommandEnv) {
this.resolver = new ActionResolver();
constructor(env: TurnCommandEnv) {
this.resolver = new ActionResolver<TriggerState>(
(env.generalActionModules ?? []) as GeneralActionModule<TriggerState>[]
);
}
parseArgs(raw: unknown): DestroyArgs | null {
@@ -36,6 +36,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
import type { GeneralTurnCommandSpec } from './index.js';
import { clamp } from 'es-toolkit';
import { parseArgsWithSchema } from '../parseArgs.js';
import { consumeSuccessfulStrategyItem } from './strategyItemConsumption.js';
export interface FireAttackEnvironment {
develCost: number;
@@ -251,9 +252,11 @@ export class ActionResolver<
> implements GeneralActionResolver<TriggerState, FireAttackArgs> {
readonly key = 'che_화계';
private readonly command: CommandResolver<TriggerState>;
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: FireAttackEnvironment) {
this.command = new CommandResolver(modules, env);
this.pipeline = new GeneralActionPipeline(modules);
}
resolve(
@@ -334,7 +337,7 @@ export class ActionResolver<
context.addLog(
`<G><b>${context.destCity.name}</b></>에 ${commandName}${JosaUtil.pick(commandName, '이')} 성공했습니다.`,
{
format: LogFormat.MONTH,
format: LogFormat.MONTH,
}
);
context.addLog(
@@ -345,7 +348,9 @@ export class ActionResolver<
);
const itemCode = general.role.items.item;
if (typeof itemCode === 'string' && itemCode.length > 0) {
const actionResult = consumeSuccessfulStrategyItem(this.pipeline, context);
const consumedItems = Array.isArray(actionResult?.['consumedItems']) ? actionResult['consumedItems'] : [];
if (typeof itemCode === 'string' && consumedItems.includes(itemCode)) {
context.addLog(`<C>${itemCode}</>${JosaUtil.pick(itemCode, '을')} 사용!`, {
format: LogFormat.PLAIN,
});
@@ -383,13 +388,7 @@ export class ActionDefinition<
buildMinConstraints(_ctx: ConstraintContext, _args: FireAttackArgs): Constraint[] {
const { gold, rice } = this.command.getCost();
return [
notBeNeutral(),
occupiedCity(),
suppliedCity(),
reqGeneralGold(() => gold),
reqGeneralRice(() => rice),
];
return [notBeNeutral(), occupiedCity(), suppliedCity(), reqGeneralGold(() => gold), reqGeneralRice(() => rice)];
}
buildConstraints(_ctx: ConstraintContext, _args: FireAttackArgs): Constraint[] {
@@ -0,0 +1,12 @@
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionPipeline } from '@sammo-ts/logic/triggers/general-action.js';
export const consumeSuccessfulStrategyItem = <TriggerState extends GeneralTriggerState>(
pipeline: GeneralActionPipeline<TriggerState>,
context: GeneralActionResolveContext<TriggerState>
): Record<string, unknown> | null =>
pipeline.onArbitraryAction(context, 'GeneralCommand', null, {
command: '계략',
success: true,
});
@@ -13,6 +13,7 @@ export const itemModule: ItemModule = {
consumable: true,
reqSecu: 1000,
unique: false,
consumeOn: [{ actionType: 'GeneralCommand', command: '계략', successOnly: true }],
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '계략' && varType === 'success') {
return value + 0.2;
@@ -13,6 +13,7 @@ export const itemModule: ItemModule = {
consumable: true,
reqSecu: 2000,
unique: false,
consumeOn: [{ actionType: 'GeneralCommand', command: '계략', successOnly: true }],
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '계략' && varType === 'success') {
return value + 0.5;
@@ -7,7 +7,7 @@ export const itemModule: ItemModule = createStatItemModule({
slot: 'horse',
statName: 'leadership',
statValue: 3,
cost: 4500,
cost: 6000,
buyable: true,
reqSecu: 2500,
reqSecu: 3000,
});
@@ -7,7 +7,7 @@ export const itemModule: ItemModule = createStatItemModule({
slot: 'horse',
statName: 'leadership',
statValue: 4,
cost: 6000,
cost: 10000,
buyable: true,
reqSecu: 3000,
reqSecu: 4000,
});
@@ -7,7 +7,7 @@ export const itemModule: ItemModule = createStatItemModule({
slot: 'horse',
statName: 'leadership',
statValue: 5,
cost: 7500,
cost: 15000,
buyable: true,
reqSecu: 3500,
reqSecu: 5000,
});
@@ -7,7 +7,7 @@ export const itemModule: ItemModule = createStatItemModule({
slot: 'weapon',
statName: 'strength',
statValue: 3,
cost: 4500,
cost: 6000,
buyable: true,
reqSecu: 2500,
reqSecu: 3000,
});
@@ -7,7 +7,7 @@ export const itemModule: ItemModule = createStatItemModule({
slot: 'weapon',
statName: 'strength',
statValue: 4,
cost: 6000,
cost: 10000,
buyable: true,
reqSecu: 3000,
reqSecu: 4000,
});
@@ -7,7 +7,7 @@ export const itemModule: ItemModule = createStatItemModule({
slot: 'weapon',
statName: 'strength',
statValue: 5,
cost: 7500,
cost: 15000,
buyable: true,
reqSecu: 3500,
reqSecu: 5000,
});
@@ -7,7 +7,7 @@ export const itemModule: ItemModule = createStatItemModule({
slot: 'weapon',
statName: 'strength',
statValue: 6,
cost: 9000,
cost: 21000,
buyable: true,
reqSecu: 4000,
reqSecu: 6000,
});
@@ -1,7 +1,9 @@
import { createStatItemModule } from './base.js';
import type { ItemModule } from './types.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { che_저격발동, che_저격시도 } from '@sammo-ts/logic/war/triggers/che_저격.js';
export const itemModule: ItemModule = createStatItemModule({
const baseModule = createStatItemModule({
key: 'che_무기_07_맥궁',
rawName: '맥궁',
slot: 'weapon',
@@ -11,4 +13,18 @@ export const itemModule: ItemModule = createStatItemModule({
buyable: false,
reqSecu: 0,
unique: true,
extraInfo: '[전투] 새로운 상대와 전투 시 20% 확률로 저격 발동, 성공 시 사기+20',
});
const raiseType = BaseWarUnitTrigger.TYPE_ITEM + BaseWarUnitTrigger.TYPE_DEDUP_TYPE_BASE * 107;
export const itemModule: ItemModule = {
...baseModule,
getBattlePhaseTriggerList: (context) =>
context.unit
? new WarTriggerCaller(
new che_저격시도(context.unit, raiseType, 0.2, 20, 40),
new che_저격발동(context.unit, raiseType)
)
: null,
};
@@ -1,7 +1,9 @@
import { createStatItemModule } from './base.js';
import type { ItemModule } from './types.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { che_저격발동, che_저격시도 } from '@sammo-ts/logic/war/triggers/che_저격.js';
export const itemModule: ItemModule = createStatItemModule({
const baseModule = createStatItemModule({
key: 'che_무기_11_이광궁',
rawName: '이광궁',
slot: 'weapon',
@@ -11,4 +13,18 @@ export const itemModule: ItemModule = createStatItemModule({
buyable: false,
reqSecu: 0,
unique: true,
extraInfo: '[전투] 새로운 상대와 전투 시 10% 확률로 저격 발동, 성공 시 사기+10',
});
const raiseType = BaseWarUnitTrigger.TYPE_ITEM + BaseWarUnitTrigger.TYPE_DEDUP_TYPE_BASE * 111;
export const itemModule: ItemModule = {
...baseModule,
getBattlePhaseTriggerList: (context) =>
context.unit
? new WarTriggerCaller(
new che_저격시도(context.unit, raiseType, 0.1, 20, 40, 10),
new che_저격발동(context.unit, raiseType)
)
: null,
};
@@ -1,7 +1,9 @@
import { createStatItemModule } from './base.js';
import type { ItemModule } from './types.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { che_저격발동, che_저격시도 } from '@sammo-ts/logic/war/triggers/che_저격.js';
export const itemModule: ItemModule = createStatItemModule({
const baseModule = createStatItemModule({
key: 'che_무기_13_양유기궁',
rawName: '양유기궁',
slot: 'weapon',
@@ -11,4 +13,18 @@ export const itemModule: ItemModule = createStatItemModule({
buyable: false,
reqSecu: 0,
unique: true,
extraInfo: '[전투] 새로운 상대와 전투 시 10% 확률로 저격 발동, 성공 시 사기+10',
});
const raiseType = BaseWarUnitTrigger.TYPE_ITEM + BaseWarUnitTrigger.TYPE_DEDUP_TYPE_BASE * 113;
export const itemModule: ItemModule = {
...baseModule,
getBattlePhaseTriggerList: (context) =>
context.unit
? new WarTriggerCaller(
new che_저격시도(context.unit, raiseType, 0.1, 20, 40, 10),
new che_저격발동(context.unit, raiseType)
)
: null,
};
@@ -1,5 +1,7 @@
import { clamp } from '@sammo-ts/logic/war/utils.js';
import { WarUnitGeneral } from '@sammo-ts/logic/war/units.js';
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { } from '@sammo-ts/logic/war/triggers/전투력보정.js';
import type { ItemModule } from './types.js';
const ITEM_KEY = 'che_불굴_상편';
@@ -15,14 +17,15 @@ export const itemModule: ItemModule = {
consumable: false,
reqSecu: 0,
unique: false,
getWarPowerMultiplier: (_context, unit, _oppose) => {
getBattlePhaseTriggerList: (context) => {
const unit = context.unit;
if (!(unit instanceof WarUnitGeneral)) {
return [1, 1];
return null;
}
const general = unit.getGeneral();
const leadership = general.stats.leadership;
const crew = general.crew;
const crewRatio = clamp(crew / (leadership * 100), 0, 1);
return [1 + 0.6 * (1 - crewRatio), 1];
return new WarTriggerCaller(new (unit, 1 + 0.6 * (1 - crewRatio)));
},
};
@@ -1,4 +1,6 @@
import type { ItemModule } from './types.js';
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { } from '@sammo-ts/logic/war/triggers/전투력보정.js';
const ITEM_KEY = 'che_상성보정_과실주';
@@ -13,11 +15,16 @@ export const itemModule: ItemModule = {
consumable: false,
reqSecu: 0,
unique: false,
getWarPowerMultiplier: (_context, unit, oppose) => {
getBattlePhaseTriggerList: (context) => {
const unit = context.unit;
const oppose = unit?.getOppose();
if (!unit || !oppose) {
return null;
}
const attackCoef = unit.getCrewType().getAttackCoef(oppose.getCrewType());
if (attackCoef < 1) {
return [1, 1];
return null;
}
return [1.1, 0.9];
return new WarTriggerCaller(new (unit, 1.1, 0.9));
},
};
@@ -12,9 +12,9 @@ const baseModule = createStatItemModule({
slot: 'book',
statName: 'intelligence',
statValue: STAT_VALUE,
cost: 6000,
cost: 10000,
buyable: true,
reqSecu: 3000,
reqSecu: 4000,
extraInfo: '[전투] 계략 시도 확률 +2%p',
});
@@ -12,9 +12,9 @@ const baseModule = createStatItemModule({
slot: 'book',
statName: 'intelligence',
statValue: STAT_VALUE,
cost: 7500,
cost: 15000,
buyable: true,
reqSecu: 3500,
reqSecu: 5000,
extraInfo: '[전투] 계략 시도 확률 +3%p',
});
@@ -12,9 +12,9 @@ const baseModule = createStatItemModule({
slot: 'book',
statName: 'intelligence',
statValue: STAT_VALUE,
cost: 9000,
cost: 21000,
buyable: true,
reqSecu: 4000,
reqSecu: 6000,
extraInfo: '[전투] 계략 시도 확률 +3%p',
});
@@ -1,7 +1,9 @@
import { createStatItemModule } from './base.js';
import type { ItemModule } from './types.js';
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { che_반계발동, che_반계시도 } from '@sammo-ts/logic/war/triggers/che_반계.js';
export const itemModule: ItemModule = createStatItemModule({
const baseModule = createStatItemModule({
key: 'che_서적_12_산해경',
rawName: '산해경',
slot: 'book',
@@ -13,3 +15,9 @@ export const itemModule: ItemModule = createStatItemModule({
unique: true,
extraInfo: '[전투] 상대의 계략을 10% 확률로 되돌림',
});
export const itemModule: ItemModule = {
...baseModule,
getBattlePhaseTriggerList: (context) =>
context.unit ? new WarTriggerCaller(new che_반계시도(context.unit, 0.1), new che_반계발동(context.unit)) : null,
};
@@ -1,19 +1,3 @@
import { GeneralTriggerCaller } from '@sammo-ts/logic/triggers/general.js';
import { CheUisulCityHealTrigger } from '@sammo-ts/logic/triggers/generalTriggers/che_도시치료.js';
import type { ItemModule } from './types.js';
import { createMedicalItem } from './createMedicalItem.js';
export const itemModule: ItemModule = {
key: 'che_의술_상한잡병론',
rawName: '상한잡병론',
name: '상한잡병론(의술)',
info: '[의술] 매달 도시의 부상병을 치료. 치료 효율 50% 향상',
slot: 'book',
cost: 500,
buyable: false,
consumable: false,
reqSecu: 0,
unique: false,
getPreTurnExecuteTriggerList: (context) => {
return new GeneralTriggerCaller(new CheUisulCityHealTrigger(context.general, 1.5));
},
};
export const itemModule = createMedicalItem('che_의술_상한잡병론', '상한잡병론');
@@ -1,19 +1,3 @@
import { GeneralTriggerCaller } from '@sammo-ts/logic/triggers/general.js';
import { CheUisulCityHealTrigger } from '@sammo-ts/logic/triggers/generalTriggers/che_도시치료.js';
import type { ItemModule } from './types.js';
import { createMedicalItem } from './createMedicalItem.js';
export const itemModule: ItemModule = {
key: 'che_의술_정력견혈산',
rawName: '정력견혈산',
name: '정력견혈산(의술)',
info: '[의술] 매달 도시의 부상병을 치료. 치료 효율 100% 향상',
slot: 'book',
cost: 500,
buyable: false,
consumable: false,
reqSecu: 0,
unique: false,
getPreTurnExecuteTriggerList: (context) => {
return new GeneralTriggerCaller(new CheUisulCityHealTrigger(context.general, 2));
},
};
export const itemModule = createMedicalItem('che_의술_정력견혈산', '정력견혈산');
@@ -1,19 +1,3 @@
import { GeneralTriggerCaller } from '@sammo-ts/logic/triggers/general.js';
import { CheUisulCityHealTrigger } from '@sammo-ts/logic/triggers/generalTriggers/che_도시치료.js';
import type { ItemModule } from './types.js';
import { createMedicalItem } from './createMedicalItem.js';
export const itemModule: ItemModule = {
key: 'che_의술_청낭서',
rawName: '청낭서',
name: '청낭서(의술)',
info: '[의술] 매달 도시의 부상병을 치료. 치료 효율 50% 향상',
slot: 'book',
cost: 500,
buyable: false,
consumable: false,
reqSecu: 0,
unique: false,
getPreTurnExecuteTriggerList: (context) => {
return new GeneralTriggerCaller(new CheUisulCityHealTrigger(context.general, 1.5));
},
};
export const itemModule = createMedicalItem('che_의술_청낭서', '청낭서');
@@ -1,19 +1,3 @@
import { GeneralTriggerCaller } from '@sammo-ts/logic/triggers/general.js';
import { CheUisulCityHealTrigger } from '@sammo-ts/logic/triggers/generalTriggers/che_도시치료.js';
import type { ItemModule } from './types.js';
import { createMedicalItem } from './createMedicalItem.js';
export const itemModule: ItemModule = {
key: 'che_의술_태평청령',
rawName: '태평청령',
name: '태평청령(의술)',
info: '[의술] 매달 도시의 부상병을 치료. 치료 효율 50% 향상',
slot: 'book',
cost: 500,
buyable: false,
consumable: false,
reqSecu: 0,
unique: false,
getPreTurnExecuteTriggerList: (context) => {
return new GeneralTriggerCaller(new CheUisulCityHealTrigger(context.general, 1.5));
},
};
export const itemModule = createMedicalItem('che_의술_태평청령', '태평청령');
@@ -9,7 +9,7 @@ export const itemModule: ItemModule = {
key: 'che_저격_매화수전',
rawName: '매화수전',
name: '매화수전(저격)',
info: '[전투] 계략 시도 단계에서 저격 확률 50%',
info: '[전투] 새로운 상대와 전투 시 50% 확률로 저격 발동, 성공 시 사기+20',
slot: 'item',
cost: 200,
buyable: false,
@@ -9,7 +9,7 @@ export const itemModule: ItemModule = {
key: 'che_저격_비도',
rawName: '비도',
name: '비도(저격)',
info: '[전투] 계략 시도 단계에서 저격 확률 50%',
info: '[전투] 새로운 상대와 전투 시 50% 확률로 저격 발동, 성공 시 사기+20',
slot: 'item',
cost: 200,
buyable: false,
@@ -15,12 +15,7 @@ export const itemModule: ItemModule = {
unique: false,
getWarPowerMultiplier: (_context, _unit, oppose) => {
const opposeCrewType = oppose.getCrewType();
// In Sammo, region/city units usually have reqCities or reqRegions in crewType.
// We'll check if those properties exist and have at least one element.
if (
(opposeCrewType.reqCities && opposeCrewType.reqCities.length > 0) ||
(opposeCrewType.reqRegions && opposeCrewType.reqRegions.length > 0)
) {
if (opposeCrewType.reqCities() || opposeCrewType.reqRegions()) {
return [1.15, 0.85];
}
return [1, 1];
@@ -0,0 +1,23 @@
import { GeneralTriggerCaller } from '@sammo-ts/logic/triggers/general.js';
import { CheUisulCityHealTrigger } from '@sammo-ts/logic/triggers/generalTriggers/che_도시치료.js';
import { triggerModule as medicalWarTriggerModule } from '@sammo-ts/logic/war/triggers/che_의술.js';
import type { ItemModule } from './types.js';
const INFO =
'[군사] 매 턴마다 자신(100%)과 소속 도시 장수(적 포함 50%) 부상 회복<br>[전투] 페이즈마다 40% 확률로 치료 발동(아군 피해 30% 감소, 부상 회복)';
export const createMedicalItem = (key: string, rawName: string): ItemModule => ({
key,
rawName,
name: `${rawName}(의술)`,
info: INFO,
slot: 'item',
cost: 200,
buyable: false,
consumable: false,
reqSecu: 0,
unique: true,
getPreTurnExecuteTriggerList: (context) => new GeneralTriggerCaller(new CheUisulCityHealTrigger(context.general)),
getBattlePhaseTriggerList: (context) =>
context.unit ? medicalWarTriggerModule.createTriggerList(context.unit) : null,
});
@@ -0,0 +1,69 @@
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import type { ItemModule } from './types.js';
export const createEventBattleTraitItemModule = (
key: string,
traitModule: TraitModule,
overrides: Partial<
Pick<ItemModule, 'rawName' | 'name' | 'info' | 'cost' | 'buyable' | 'consumable' | 'reqSecu'>
> = {}
): ItemModule => {
const itemModule: ItemModule = {
key,
rawName: overrides.rawName ?? '비급',
name: overrides.name ?? `비급(${traitModule.name})`,
info: overrides.info ?? traitModule.info,
slot: 'item',
cost: overrides.cost ?? 100,
buyable: overrides.buyable ?? true,
consumable: overrides.consumable ?? false,
reqSecu: overrides.reqSecu ?? 3000,
unique: false,
};
if (traitModule.getPreTurnExecuteTriggerList) {
itemModule.getPreTurnExecuteTriggerList = traitModule.getPreTurnExecuteTriggerList;
}
if (traitModule.onCalcDomestic) {
itemModule.onCalcDomestic = traitModule.onCalcDomestic;
}
if (traitModule.onCalcStat) {
itemModule.onCalcStat = traitModule.onCalcStat;
}
if (traitModule.onCalcOpposeStat) {
itemModule.onCalcOpposeStat = traitModule.onCalcOpposeStat;
}
if (traitModule.onCalcStrategic) {
itemModule.onCalcStrategic = traitModule.onCalcStrategic;
}
if (traitModule.onCalcNationalIncome) {
itemModule.onCalcNationalIncome = traitModule.onCalcNationalIncome;
}
if (traitModule.onArbitraryAction) {
itemModule.onArbitraryAction = traitModule.onArbitraryAction;
}
if (traitModule.getBattleInitTriggerList) {
itemModule.getBattleInitTriggerList = traitModule.getBattleInitTriggerList;
}
if (traitModule.getBattlePhaseTriggerList) {
itemModule.getBattlePhaseTriggerList = traitModule.getBattlePhaseTriggerList;
}
if (traitModule.getWarPowerMultiplier) {
itemModule.getWarPowerMultiplier =
traitModule.key === 'che_무쌍'
? (context, unit, oppose) => {
const general =
'getGeneral' in unit && typeof (unit as { getGeneral?: unknown }).getGeneral === 'function'
? (
unit as typeof unit & {
getGeneral: () => { role: { specialWar: string | null } };
}
).getGeneral()
: null;
return general?.role.specialWar === traitModule.key
? [1, 1]
: traitModule.getWarPowerMultiplier!(context, unit, oppose);
}
: traitModule.getWarPowerMultiplier;
}
return itemModule;
};
+53
View File
@@ -0,0 +1,53 @@
import { TriggerPriority } from '@sammo-ts/logic/triggers/core.js';
import { consumeEquippedItemCharge, getEquippedItemInstance } from './inventory.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { WarUnitCity, WarUnitGeneral, type WarUnit } from '@sammo-ts/logic/war/units.js';
import type { ItemModule } from './types.js';
const ITEM_KEY = 'event_충차';
class EventRamConsumptionTrigger extends BaseWarUnitTrigger {
constructor(unit: WarUnit) {
super(unit, TriggerPriority.Pre + 200, BaseWarUnitTrigger.TYPE_CONSUMABLE_ITEM);
}
protected actionWar(
self: WarUnit,
oppose: WarUnit,
_selfEnv: Record<string, unknown>,
_opposeEnv: Record<string, unknown>
): boolean {
if (!(self instanceof WarUnitGeneral) || !(oppose instanceof WarUnitCity)) {
return true;
}
if (self.hasActivatedSkillOnLog('충차공격') > 0) {
return true;
}
const general = self.getGeneral();
if (getEquippedItemInstance(general, 'item')?.itemKey !== ITEM_KEY) {
return true;
}
self.activateSkill('충차공격', '아이템사용');
self.getLogger().pushGeneralBattleDetailLog('<C>충차</>로 성벽을 공격합니다.');
consumeEquippedItemCharge(general, 'item', ITEM_KEY, 2);
return true;
}
}
export const itemModule: ItemModule = {
key: ITEM_KEY,
rawName: '충차',
name: '충차',
info: '[전투] 성벽 공격 시 대미지 +50%, 2회용',
slot: 'item',
cost: 2000,
buyable: true,
consumable: true,
initialCharges: 2,
reqSecu: 3000,
unique: false,
getWarPowerMultiplier: (_context, _unit, oppose) => (oppose instanceof WarUnitCity ? [1.5, 1] : [1, 1]),
getBattlePhaseTriggerList: (context) =>
context.unit ? new WarTriggerCaller(new EventRamConsumptionTrigger(context.unit)) : null,
};
+210 -5
View File
@@ -17,6 +17,7 @@ import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
import type { ItemModule, ItemModuleExport } from './types.js';
import { listEquippedItemKeys } from './utils.js';
import { removeEquippedItem } from './inventory.js';
export const ITEM_KEYS = [
'che_간파_노군입산부',
@@ -120,6 +121,7 @@ export const ITEM_KEYS = [
'che_서적_15_손자병법',
'che_숙련_동작',
'che_약탈_옥벽',
'che_위압_조목삭',
'che_의술_상한잡병론',
'che_의술_정력견혈산',
'che_의술_청낭서',
@@ -142,6 +144,27 @@ export const ITEM_KEYS = [
'che_훈련_단결도',
'che_훈련_철벽서',
'che_훈련_청주',
'event_전투특기_격노',
'event_전투특기_견고',
'event_전투특기_공성',
'event_전투특기_궁병',
'event_전투특기_귀병',
'event_전투특기_기병',
'event_전투특기_돌격',
'event_전투특기_무쌍',
'event_전투특기_반계',
'event_전투특기_보병',
'event_전투특기_신산',
'event_전투특기_신중',
'event_전투특기_위압',
'event_전투특기_의술',
'event_전투특기_저격',
'event_전투특기_집중',
'event_전투특기_징병',
'event_전투특기_척사',
'event_전투특기_필살',
'event_전투특기_환술',
'event_충차',
] as const;
export type ItemKey = (typeof ITEM_KEYS)[number];
@@ -250,6 +273,21 @@ const defaultImporters: Record<ItemKey, ItemImporter> = {
che_서적_15_손자병법: async () => import('./che_서적_15_손자병법.js'),
che_숙련_동작: async () => import('./che_숙련_동작.js'),
che_약탈_옥벽: async () => import('./che_약탈_옥벽.js'),
che_위압_조목삭: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_위압.js'),
import('./eventBattleTrait.js'),
]);
return {
itemModule: createEventBattleTraitItemModule('che_위압_조목삭', traitModule, {
rawName: '조목삭',
name: '조목삭(위압)',
cost: 200,
buyable: false,
reqSecu: 0,
}),
};
},
che_의술_상한잡병론: async () => import('./che_의술_상한잡병론.js'),
che_의술_정력견혈산: async () => import('./che_의술_정력견혈산.js'),
che_의술_청낭서: async () => import('./che_의술_청낭서.js'),
@@ -272,6 +310,147 @@ const defaultImporters: Record<ItemKey, ItemImporter> = {
che_훈련_단결도: async () => import('./che_훈련_단결도.js'),
che_훈련_철벽서: async () => import('./che_훈련_철벽서.js'),
che_훈련_청주: async () => import('./che_훈련_청주.js'),
event_전투특기_격노: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_격노.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_격노', traitModule) };
},
event_전투특기_견고: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_견고.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_견고', traitModule) };
},
event_전투특기_공성: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_공성.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_공성', traitModule) };
},
event_전투특기_궁병: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_궁병.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_궁병', traitModule) };
},
event_전투특기_귀병: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_귀병.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_귀병', traitModule) };
},
event_전투특기_기병: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_기병.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_기병', traitModule) };
},
event_전투특기_돌격: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_돌격.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_돌격', traitModule) };
},
event_전투특기_무쌍: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_무쌍.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_무쌍', traitModule) };
},
event_전투특기_반계: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_반계.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_반계', traitModule) };
},
event_전투특기_보병: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_보병.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_보병', traitModule) };
},
event_전투특기_신산: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_신산.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_신산', traitModule) };
},
event_전투특기_신중: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_신중.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_신중', traitModule) };
},
event_전투특기_위압: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_위압.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_위압', traitModule) };
},
event_전투특기_의술: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_의술.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_의술', traitModule) };
},
event_전투특기_저격: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_저격.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_저격', traitModule) };
},
event_전투특기_집중: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_집중.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_집중', traitModule) };
},
event_전투특기_징병: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_징병.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_징병', traitModule) };
},
event_전투특기_척사: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_척사.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_척사', traitModule) };
},
event_전투특기_필살: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_필살.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_필살', traitModule) };
},
event_전투특기_환술: async () => {
const [{ traitModule }, { createEventBattleTraitItemModule }] = await Promise.all([
import('../triggers/special/war/che_환술.js'),
import('./eventBattleTrait.js'),
]);
return { itemModule: createEventBattleTraitItemModule('event_전투특기_환술', traitModule) };
},
event_충차: async () => import('./event_충차.js'),
};
export const isItemKey = (value: string): value is ItemKey => ITEM_KEYS.includes(value as ItemKey);
@@ -294,7 +473,13 @@ export class ItemLoader {
if (!('itemModule' in module)) {
throw new Error(`Missing itemModule for item: ${key}`);
}
const resolved = module.itemModule;
const resolved: ItemModule = {
...module.itemModule,
// Legacy defines the finite unique pool as every non-buyable
// scenario item; keep that invariant authoritative instead of
// relying on duplicated per-module flags.
unique: !module.itemModule.buyable,
};
if (resolved.key !== key) {
throw new Error(`Item key mismatch: expected ${key}, got ${resolved.key}`);
}
@@ -455,11 +640,30 @@ class ItemGeneralActionRouter<
let current = aux ?? null;
for (const module of this.resolveModules(context)) {
if (!module.onArbitraryAction) {
continue;
// Declarative consumption rules are evaluated below even when
// the item has no custom arbitrary-action hook.
} else {
const result = module.onArbitraryAction(context, actionType, phase, current);
if (result !== undefined) {
current = result;
}
}
const result = module.onArbitraryAction(context, actionType, phase, current);
if (result !== undefined) {
current = result;
const shouldConsume = module.consumeOn?.some(
(rule) =>
rule.actionType === actionType &&
(rule.phase === undefined || rule.phase === phase) &&
(rule.command === undefined || current?.['command'] === rule.command) &&
(!rule.successOnly || current?.['success'] === true)
);
if (shouldConsume) {
const removed = removeEquippedItem(context.general, module.slot);
if (removed?.itemKey === module.key) {
const consumedItems = Array.isArray(current?.['consumedItems'])
? (current['consumedItems'] as unknown[])
: [];
current = { ...(current ?? {}), consumedItems: [...consumedItems, module.key] };
}
}
}
return current;
@@ -581,6 +785,7 @@ export {
getEquippedItemInstance,
parseItemInventory,
projectItemSlots,
readItemInventory,
readItemInventoryFromMeta,
removeEquippedItem,
serializeItemInventory,
+8 -2
View File
@@ -154,6 +154,10 @@ export const ensureItemInventory = <TriggerState extends GeneralTriggerState>(
return general.itemInventory;
};
export const readItemInventory = <TriggerState extends GeneralTriggerState>(
general: General<TriggerState>
): GeneralItemInventory => general.itemInventory ?? createItemInventoryFromSlots(general.role.items);
export const projectItemSlots = (inventory: GeneralItemInventory): GeneralItemSlots => {
const slots: GeneralItemSlots = { horse: null, weapon: null, book: null, item: null };
for (const slot of ITEM_SLOTS) {
@@ -167,7 +171,7 @@ export const getEquippedItemInstance = <TriggerState extends GeneralTriggerState
general: General<TriggerState>,
slot: GeneralItemSlot
): GeneralItemInstance | null => {
const inventory = ensureItemInventory(general);
const inventory = readItemInventory(general);
const instanceId = inventory.equipped[slot];
return instanceId ? (inventory.instances[instanceId] ?? null) : null;
};
@@ -220,7 +224,9 @@ export const consumeEquippedItemCharge = <TriggerState extends GeneralTriggerSta
itemKey: string,
fallbackCharges = 1
): boolean => {
const instance = getEquippedItemInstance(general, slot);
const inventory = ensureItemInventory(general);
const instanceId = inventory.equipped[slot];
const instance = instanceId ? inventory.instances[instanceId] : undefined;
if (!instance || instance.itemKey !== itemKey) {
return false;
}
+8
View File
@@ -17,6 +17,13 @@ import type { WarUnit } from '@sammo-ts/logic/war/units.js';
export type ItemSlot = 'horse' | 'weapon' | 'book' | 'item';
export interface ItemActionConsumptionRule {
actionType: TriggerActionType;
phase?: TriggerActionPhase | null;
command?: string;
successOnly?: boolean;
}
export interface ItemModule<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
key: string;
name: string;
@@ -27,6 +34,7 @@ export interface ItemModule<TriggerState extends GeneralTriggerState = GeneralTr
buyable: boolean;
consumable: boolean;
initialCharges?: number;
consumeOn?: ItemActionConsumptionRule[];
reqSecu: number;
unique: boolean;
+8 -2
View File
@@ -1,7 +1,12 @@
import type { ScenarioConfig } from '@sammo-ts/logic/scenario/types.js';
import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { ItemModule } from './types.js';
import { consumeEquippedItemCharge, ensureItemInventory, getEquippedItemInstance } from './inventory.js';
import {
consumeEquippedItemCharge,
ensureItemInventory,
getEquippedItemInstance,
readItemInventory,
} from './inventory.js';
const toBoolean = (value: unknown): boolean => {
if (typeof value === 'boolean') {
@@ -27,7 +32,7 @@ export const isInventoryEnabled = (config: ScenarioConfig): boolean => {
export const listEquippedItemKeys = <TriggerState extends GeneralTriggerState>(
general: General<TriggerState>
): string[] => {
const inventory = ensureItemInventory(general);
const inventory = readItemInventory(general);
const items = (['horse', 'weapon', 'book', 'item'] as const).map((slot) => {
const instanceId = inventory.equipped[slot];
return instanceId ? (inventory.instances[instanceId]?.itemKey ?? null) : null;
@@ -58,6 +63,7 @@ export const setItemRemain = <TriggerState extends GeneralTriggerState>(
itemKey: string,
remain: number | null
): void => {
ensureItemInventory(general);
const instance = getEquippedItemInstance(general, 'item');
if (!instance || instance.itemKey !== itemKey) {
return;
+1 -1
View File
@@ -1,4 +1,4 @@
export type TriggerActionType = '장비매매';
export type TriggerActionType = '장비매매' | 'GeneralCommand';
export type TriggerActionPhase = '판매' | '구매';
+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 {