feat(items): add new item modules and enhance item functionality
- Introduced various item modules including '흑색마', '사륜거', '단궁', '동호비궁', '도기', '변도론', '전론', and '환약'. - Implemented stat item modules to enhance general and war action contexts. - Added triggers for item healing and sniping actions during battles. - Enhanced item management with new utility functions for inventory handling. - Updated the item registry and loader to support new items and their functionalities. - Improved the overall structure of item-related code for better maintainability and scalability.
This commit is contained in:
@@ -8,11 +8,16 @@ import {
|
||||
LogScope,
|
||||
resolveDefenderOrder,
|
||||
resolveWarBattle,
|
||||
createItemActionModules,
|
||||
createItemModuleRegistry,
|
||||
ITEM_KEYS,
|
||||
loadItemModules,
|
||||
type City,
|
||||
type General,
|
||||
type Nation,
|
||||
type UnitSetDefinition,
|
||||
type WarBattleOutcome,
|
||||
type WarActionModule,
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
import {
|
||||
@@ -24,6 +29,10 @@ import { convertLog } from './logFormatter.js';
|
||||
|
||||
const DEFAULT_GENERAL_AGE = 20;
|
||||
|
||||
const itemWarModules: WarActionModule[] = createItemActionModules(
|
||||
createItemModuleRegistry(await loadItemModules([...ITEM_KEYS]))
|
||||
).war;
|
||||
|
||||
const normalizeItemCode = (value: string | null): string | null =>
|
||||
value === 'None' ? null : value;
|
||||
|
||||
@@ -324,11 +333,13 @@ export const processBattleSimJob = (payload: BattleSimJobPayload): BattleSimResu
|
||||
general: attackerGeneral,
|
||||
city: attackerCity,
|
||||
nation: attackerNation,
|
||||
modules: itemWarModules,
|
||||
},
|
||||
defenders: defenderGenerals.map((general) => ({
|
||||
general,
|
||||
city: defenderCity,
|
||||
nation: defenderNation,
|
||||
modules: itemWarModules,
|
||||
})),
|
||||
defenderCity,
|
||||
defenderNation,
|
||||
|
||||
@@ -10,6 +10,10 @@ import type {
|
||||
import {
|
||||
loadGeneralTurnCommandSpecs,
|
||||
loadNationTurnCommandSpecs,
|
||||
createItemActionModules,
|
||||
createItemModuleRegistry,
|
||||
ITEM_KEYS,
|
||||
loadItemModules,
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
const DEFAULT_GENERAL_GOLD = 1000;
|
||||
@@ -158,6 +162,18 @@ export const buildReservedTurnDefinitions = async (options: {
|
||||
general: Map<string, GeneralActionDefinition>;
|
||||
nation: Map<string, GeneralActionDefinition>;
|
||||
}> => {
|
||||
const itemModules = await loadItemModules([...ITEM_KEYS]);
|
||||
const itemRegistry = createItemModuleRegistry(itemModules);
|
||||
const itemActionModules = createItemActionModules(itemRegistry);
|
||||
options.env.generalActionModules = [
|
||||
...(options.env.generalActionModules ?? []),
|
||||
...itemActionModules.general,
|
||||
];
|
||||
options.env.warActionModules = [
|
||||
...(options.env.warActionModules ?? []),
|
||||
...itemActionModules.war,
|
||||
];
|
||||
|
||||
const generalSpecs = await loadGeneralTurnCommandSpecs(
|
||||
options.commandProfile.general
|
||||
);
|
||||
|
||||
@@ -40,6 +40,7 @@ Move items into the main docs once they are finalized.
|
||||
## Trigger System
|
||||
|
||||
- Example trigger sets per scenario or rule pack
|
||||
- [AI suggestion] Define item/equipment effect modeling for war triggers (equip vs carry, stacking, consumable/breakable) and hook into WarActionPipeline/trigger registry.
|
||||
|
||||
## Data and Profiles (Lower Priority)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
|
||||
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
|
||||
|
||||
export interface TurnCommandEnv {
|
||||
develCost: number;
|
||||
@@ -24,4 +25,5 @@ export interface TurnCommandEnv {
|
||||
baseRice: number;
|
||||
maxResourceActionAmount: number;
|
||||
generalActionModules?: Array<GeneralActionModule>;
|
||||
warActionModules?: Array<WarActionModule>;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import type {
|
||||
} from '@sammo-ts/logic/war/types.js';
|
||||
import { resolveWarAftermath } from '@sammo-ts/logic/war/aftermath.js';
|
||||
import { resolveWarBattle } from '@sammo-ts/logic/war/engine.js';
|
||||
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
|
||||
import {
|
||||
increaseMetaNumber,
|
||||
simpleSerialize,
|
||||
@@ -268,6 +269,11 @@ export class ActionDefinition<
|
||||
> {
|
||||
public readonly key = 'che_출병';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly warModules: Array<WarActionModule<TriggerState>>;
|
||||
|
||||
constructor(modules: Array<WarActionModule<TriggerState> | null | undefined> = []) {
|
||||
this.warModules = modules.filter(Boolean) as Array<WarActionModule<TriggerState>>;
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): DispatchArgs | null {
|
||||
const data = raw as { destCityId?: unknown };
|
||||
@@ -439,11 +445,13 @@ export class ActionDefinition<
|
||||
general: context.general,
|
||||
city: attackerCity,
|
||||
nation: attackerNation,
|
||||
modules: this.warModules,
|
||||
},
|
||||
defenders: defenderGenerals.map((general) => ({
|
||||
general,
|
||||
city: defenderCity,
|
||||
nation: defenderNation,
|
||||
modules: this.warModules,
|
||||
})),
|
||||
defenderCity,
|
||||
defenderNation,
|
||||
@@ -580,5 +588,6 @@ export const commandSpec: GeneralTurnCommandSpec = {
|
||||
category: '군사',
|
||||
reqArg: true,
|
||||
args: { destCityId: 0 },
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.warActionModules ?? []),
|
||||
};
|
||||
|
||||
@@ -4,6 +4,13 @@ export * from './actions/index.js';
|
||||
export * from './constraints/index.js';
|
||||
export * from './logging/index.js';
|
||||
export * from './messages/index.js';
|
||||
export * from './items/index.js';
|
||||
export {
|
||||
ITEM_KEYS,
|
||||
createItemActionModules,
|
||||
createItemModuleRegistry,
|
||||
loadItemModules,
|
||||
} from './items/index.js';
|
||||
export * from './ports/world.js';
|
||||
export * from './ports/worldSnapshot.js';
|
||||
export * from './scenario/index.js';
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { ItemModule, ItemSlot } from './types.js';
|
||||
|
||||
const resolveStatLabel = (statName: string): string => {
|
||||
switch (statName) {
|
||||
case 'leadership':
|
||||
return '통솔';
|
||||
case 'strength':
|
||||
return '무력';
|
||||
case 'intelligence':
|
||||
return '지력';
|
||||
default:
|
||||
return statName;
|
||||
}
|
||||
};
|
||||
|
||||
export interface StatItemOptions {
|
||||
key: string;
|
||||
rawName: string;
|
||||
slot: ItemSlot;
|
||||
statName: 'leadership' | 'strength' | 'intelligence';
|
||||
statValue: number;
|
||||
cost: number | null;
|
||||
buyable: boolean;
|
||||
reqSecu: number;
|
||||
unique?: boolean;
|
||||
extraInfo?: string;
|
||||
}
|
||||
|
||||
export const createStatItemModule = (
|
||||
options: StatItemOptions
|
||||
): ItemModule => {
|
||||
const statLabel = resolveStatLabel(options.statName);
|
||||
const name = `${options.rawName}(+${options.statValue})`;
|
||||
const baseInfo = `${statLabel} +${options.statValue}`;
|
||||
const info = options.extraInfo ? `${baseInfo}<br>${options.extraInfo}` : baseInfo;
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext,
|
||||
statName: string,
|
||||
value: number,
|
||||
_aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(
|
||||
_context: WarActionContext,
|
||||
statName: string,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: string,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
if (statName !== options.statName) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
return value + options.statValue;
|
||||
}
|
||||
|
||||
return {
|
||||
key: options.key,
|
||||
rawName: options.rawName,
|
||||
name,
|
||||
info,
|
||||
slot: options.slot,
|
||||
cost: options.cost,
|
||||
buyable: options.buyable,
|
||||
consumable: false,
|
||||
reqSecu: options.reqSecu,
|
||||
unique: options.unique ?? false,
|
||||
onCalcStat,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createStatItemModule } from './base.js';
|
||||
import type { ItemModule } from './types.js';
|
||||
|
||||
export const itemModule: ItemModule = createStatItemModule({
|
||||
key: 'che_명마_06_흑색마',
|
||||
rawName: '흑색마',
|
||||
slot: 'horse',
|
||||
statName: 'leadership',
|
||||
statValue: 6,
|
||||
cost: 21000,
|
||||
buyable: true,
|
||||
reqSecu: 6000,
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import { CheRetreatNoWoundTrigger } from '@sammo-ts/logic/war/triggers/che_퇴각부상무효.js';
|
||||
import { createStatItemModule } from './base.js';
|
||||
import type { ItemModule } from './types.js';
|
||||
|
||||
const baseModule = createStatItemModule({
|
||||
key: 'che_명마_12_사륜거',
|
||||
rawName: '사륜거',
|
||||
slot: 'horse',
|
||||
statName: 'leadership',
|
||||
statValue: 12,
|
||||
cost: 200,
|
||||
buyable: false,
|
||||
reqSecu: 0,
|
||||
unique: true,
|
||||
extraInfo: '[전투] 전투 종료로 인한 부상 없음',
|
||||
});
|
||||
|
||||
export const itemModule: ItemModule = {
|
||||
...baseModule,
|
||||
getBattleInitTriggerList: (context) => {
|
||||
if (!context.unit) {
|
||||
return null;
|
||||
}
|
||||
return new WarTriggerCaller(new CheRetreatNoWoundTrigger(context.unit));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import {
|
||||
CheSnipingActivateTrigger,
|
||||
CheSnipingAttemptTrigger,
|
||||
} from '@sammo-ts/logic/war/triggers/che_저격.js';
|
||||
import { createStatItemModule } from './base.js';
|
||||
import type { ItemModule } from './types.js';
|
||||
|
||||
const raiseType =
|
||||
BaseWarUnitTrigger.TYPE_ITEM +
|
||||
BaseWarUnitTrigger.TYPE_DEDUP_TYPE_BASE * 102;
|
||||
|
||||
const baseModule = createStatItemModule({
|
||||
key: 'che_무기_02_단궁',
|
||||
rawName: '단궁',
|
||||
slot: 'weapon',
|
||||
statName: 'strength',
|
||||
statValue: 2,
|
||||
cost: 3000,
|
||||
buyable: true,
|
||||
reqSecu: 2000,
|
||||
extraInfo: '[전투] 새로운 상대와 전투 시 1% 확률로 저격 발동, 성공 시 사기+20',
|
||||
});
|
||||
|
||||
export const itemModule: ItemModule = {
|
||||
...baseModule,
|
||||
getBattlePhaseTriggerList: (context) => {
|
||||
if (!context.unit) {
|
||||
return null;
|
||||
}
|
||||
return new WarTriggerCaller(
|
||||
new CheSnipingAttemptTrigger(
|
||||
context.unit,
|
||||
raiseType,
|
||||
0.01,
|
||||
10,
|
||||
30
|
||||
),
|
||||
new CheSnipingActivateTrigger(context.unit, raiseType)
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import {
|
||||
CheSnipingActivateTrigger,
|
||||
CheSnipingAttemptTrigger,
|
||||
} from '@sammo-ts/logic/war/triggers/che_저격.js';
|
||||
import { createStatItemModule } from './base.js';
|
||||
import type { ItemModule } from './types.js';
|
||||
|
||||
const raiseType =
|
||||
BaseWarUnitTrigger.TYPE_ITEM +
|
||||
BaseWarUnitTrigger.TYPE_DEDUP_TYPE_BASE * 109;
|
||||
|
||||
const baseModule = createStatItemModule({
|
||||
key: 'che_무기_09_동호비궁',
|
||||
rawName: '동호비궁',
|
||||
slot: 'weapon',
|
||||
statName: 'strength',
|
||||
statValue: 9,
|
||||
cost: 200,
|
||||
buyable: false,
|
||||
reqSecu: 0,
|
||||
unique: true,
|
||||
extraInfo: '[전투] 새로운 상대와 전투 시 20% 확률로 저격 발동, 성공 시 사기+20',
|
||||
});
|
||||
|
||||
export const itemModule: ItemModule = {
|
||||
...baseModule,
|
||||
getBattlePhaseTriggerList: (context) => {
|
||||
if (!context.unit) {
|
||||
return null;
|
||||
}
|
||||
return new WarTriggerCaller(
|
||||
new CheSnipingAttemptTrigger(
|
||||
context.unit,
|
||||
raiseType,
|
||||
0.2,
|
||||
20,
|
||||
40,
|
||||
20
|
||||
),
|
||||
new CheSnipingActivateTrigger(context.unit, raiseType)
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { ItemModule } from './types.js';
|
||||
|
||||
const ITEM_KEY = 'che_보물_도기';
|
||||
|
||||
const resolveNumber = (value: unknown, fallback = 0): number =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
||||
|
||||
export const itemModule: ItemModule = {
|
||||
key: ITEM_KEY,
|
||||
rawName: '도기',
|
||||
name: '도기(보물)',
|
||||
info:
|
||||
'[개인] 판매 시 장수 소지금과 국고에 금, 쌀 중 하나를 추가 (총 +10,000, 2년마다 +5,000)',
|
||||
slot: 'item',
|
||||
cost: 200,
|
||||
buyable: false,
|
||||
consumable: false,
|
||||
reqSecu: 0,
|
||||
unique: true,
|
||||
onArbitraryAction: (context, actionType, phase, aux) => {
|
||||
if (!aux || actionType !== '장비매매' || phase !== '판매') {
|
||||
return aux ?? null;
|
||||
}
|
||||
if (aux['itemKey'] !== ITEM_KEY && aux['itemCode'] !== ITEM_KEY) {
|
||||
return aux;
|
||||
}
|
||||
|
||||
const year = resolveNumber(aux['year']);
|
||||
const startYear = resolveNumber(aux['startYear']);
|
||||
const relYear = Math.max(0, year - startYear);
|
||||
const score = Math.round(10000 + 5000 * Math.floor(relYear / 2));
|
||||
|
||||
const rng = context.rng;
|
||||
const pick = rng?.nextBool(0.5) ?? true;
|
||||
const resName = pick ? '금' : '쌀';
|
||||
const resKey = pick ? 'gold' : 'rice';
|
||||
|
||||
const nation = aux['nation'];
|
||||
if (nation && typeof nation === 'object') {
|
||||
const cast = nation as { gold?: number; rice?: number };
|
||||
const half = Math.floor(score / 2);
|
||||
if (resKey === 'gold') {
|
||||
cast.gold = (cast.gold ?? 0) + half;
|
||||
} else {
|
||||
cast.rice = (cast.rice ?? 0) + half;
|
||||
}
|
||||
}
|
||||
|
||||
const selfGain = score - Math.floor(score / 2);
|
||||
if (resKey === 'gold') {
|
||||
context.general.gold += selfGain;
|
||||
} else {
|
||||
context.general.rice += selfGain;
|
||||
}
|
||||
|
||||
const josa = JosaUtil.pick('도기', '을');
|
||||
context.log?.push(
|
||||
`<C>${itemModule.name}</>${josa} 판매하여 ${resName} <C>${score.toLocaleString(
|
||||
'en-US'
|
||||
)}</>을 보충합니다.`
|
||||
);
|
||||
return aux;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { createStatItemModule } from './base.js';
|
||||
import type { ItemModule } from './types.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
|
||||
const STAT_VALUE = 3;
|
||||
|
||||
const baseModule = createStatItemModule({
|
||||
key: 'che_서적_03_변도론',
|
||||
rawName: '변도론',
|
||||
slot: 'book',
|
||||
statName: 'intelligence',
|
||||
statValue: STAT_VALUE,
|
||||
cost: 6000,
|
||||
buyable: true,
|
||||
reqSecu: 3000,
|
||||
extraInfo: '[전투] 계략 시도 확률 +2%p',
|
||||
});
|
||||
|
||||
export const itemModule: ItemModule = {
|
||||
...baseModule,
|
||||
onCalcStat: (() => {
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext,
|
||||
statName: string,
|
||||
value: number,
|
||||
_aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(
|
||||
_context: WarActionContext,
|
||||
statName: string,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: string,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
let base = value;
|
||||
if (statName === 'intelligence' && !Array.isArray(base)) {
|
||||
base = base + STAT_VALUE;
|
||||
}
|
||||
if (statName === 'warMagicTrialProb') {
|
||||
return Array.isArray(base) ? base : base + 0.02;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<ItemModule['onCalcStat'], undefined>,
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { createStatItemModule } from './base.js';
|
||||
import type { ItemModule } from './types.js';
|
||||
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
|
||||
const STAT_VALUE = 8;
|
||||
|
||||
const baseModule = createStatItemModule({
|
||||
key: 'che_서적_08_전론',
|
||||
rawName: '전론',
|
||||
slot: 'book',
|
||||
statName: 'intelligence',
|
||||
statValue: STAT_VALUE,
|
||||
cost: 200,
|
||||
buyable: false,
|
||||
reqSecu: 0,
|
||||
unique: true,
|
||||
extraInfo: '[전투] 계략 성공 시 대미지 +20%',
|
||||
});
|
||||
|
||||
export const itemModule: ItemModule = {
|
||||
...baseModule,
|
||||
onCalcStat: (() => {
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext,
|
||||
statName: string,
|
||||
value: number,
|
||||
_aux?: unknown
|
||||
): number;
|
||||
function onCalcStat(
|
||||
_context: WarActionContext,
|
||||
statName: string,
|
||||
value: number | [number, number],
|
||||
_aux?: unknown
|
||||
): number | [number, number];
|
||||
function onCalcStat(
|
||||
_context: GeneralActionContext | WarActionContext,
|
||||
statName: string,
|
||||
value: number | [number, number]
|
||||
): number | [number, number] {
|
||||
let base = value;
|
||||
if (statName === 'intelligence' && !Array.isArray(base)) {
|
||||
base = base + STAT_VALUE;
|
||||
}
|
||||
if (statName === 'warMagicSuccessDamage') {
|
||||
return Array.isArray(base) ? base : base * 1.2;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
return onCalcStat;
|
||||
})() as Exclude<ItemModule['onCalcStat'], undefined>,
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { GeneralTriggerCaller } from '@sammo-ts/logic/triggers/general.js';
|
||||
import { CheItemHealTrigger } from '@sammo-ts/logic/triggers/generalTriggers/che_아이템치료.js';
|
||||
import { consumeItemRemain, setItemRemain } from './utils.js';
|
||||
import type { ItemModule } from './types.js';
|
||||
|
||||
const ITEM_KEY = 'che_치료_환약';
|
||||
|
||||
export const itemModule: ItemModule = {
|
||||
key: ITEM_KEY,
|
||||
rawName: '환약',
|
||||
name: '환약(치료)',
|
||||
info: '[군사] 턴 실행 전 부상 회복. 3회용',
|
||||
slot: 'item',
|
||||
cost: 200,
|
||||
buyable: true,
|
||||
consumable: true,
|
||||
reqSecu: 0,
|
||||
unique: false,
|
||||
getPreTurnExecuteTriggerList: (context) => {
|
||||
const target = context.general.triggerState.meta['use_treatment'];
|
||||
const injuryTarget =
|
||||
typeof target === 'number' && Number.isFinite(target) ? target : 10;
|
||||
return new GeneralTriggerCaller(
|
||||
new CheItemHealTrigger(context.general, {
|
||||
injuryTarget,
|
||||
itemKey: ITEM_KEY,
|
||||
itemName: '환약(치료)',
|
||||
itemRawName: '환약',
|
||||
consume: () => consumeItemRemain(context.general, ITEM_KEY, 1),
|
||||
})
|
||||
);
|
||||
},
|
||||
onArbitraryAction: (context, actionType, phase, aux) => {
|
||||
if (actionType !== '장비매매' || phase !== '구매') {
|
||||
return aux ?? null;
|
||||
}
|
||||
setItemRemain(context.general, ITEM_KEY, 3);
|
||||
return aux ?? null;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,356 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
|
||||
import {
|
||||
GeneralTriggerCaller,
|
||||
type GeneralActionContext,
|
||||
} from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext, WarActionModule } from '@sammo-ts/logic/war/actions.js';
|
||||
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';
|
||||
|
||||
export const ITEM_KEYS = [
|
||||
'che_명마_06_흑색마',
|
||||
'che_무기_02_단궁',
|
||||
'che_서적_03_변도론',
|
||||
'che_치료_환약',
|
||||
'che_명마_12_사륜거',
|
||||
'che_무기_09_동호비궁',
|
||||
'che_서적_08_전론',
|
||||
'che_보물_도기',
|
||||
] as const;
|
||||
|
||||
export type ItemKey = (typeof ITEM_KEYS)[number];
|
||||
|
||||
export type ItemImporter = () => Promise<ItemModuleExport>;
|
||||
|
||||
const defaultImporters: Record<ItemKey, ItemImporter> = {
|
||||
che_명마_06_흑색마: async () => import('./che_명마_06_흑색마.js'),
|
||||
che_무기_02_단궁: async () => import('./che_무기_02_단궁.js'),
|
||||
che_서적_03_변도론: async () => import('./che_서적_03_변도론.js'),
|
||||
che_치료_환약: async () => import('./che_치료_환약.js'),
|
||||
che_명마_12_사륜거: async () => import('./che_명마_12_사륜거.js'),
|
||||
che_무기_09_동호비궁: async () => import('./che_무기_09_동호비궁.js'),
|
||||
che_서적_08_전론: async () => import('./che_서적_08_전론.js'),
|
||||
che_보물_도기: async () => import('./che_보물_도기.js'),
|
||||
};
|
||||
|
||||
export const isItemKey = (value: string): value is ItemKey =>
|
||||
ITEM_KEYS.includes(value as ItemKey);
|
||||
|
||||
export class ItemLoader {
|
||||
private readonly cache = new Map<ItemKey, Promise<ItemModule>>();
|
||||
|
||||
constructor(
|
||||
private readonly importers: Record<ItemKey, ItemImporter> = defaultImporters
|
||||
) {}
|
||||
|
||||
async load(key: ItemKey): Promise<ItemModule> {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const importer = this.importers[key];
|
||||
if (!importer) {
|
||||
throw new Error(`Unknown item key: ${key}`);
|
||||
}
|
||||
const loading = importer().then((module) => {
|
||||
if (!('itemModule' in module)) {
|
||||
throw new Error(`Missing itemModule for item: ${key}`);
|
||||
}
|
||||
const resolved = module.itemModule;
|
||||
if (resolved.key !== key) {
|
||||
throw new Error(
|
||||
`Item key mismatch: expected ${key}, got ${resolved.key}`
|
||||
);
|
||||
}
|
||||
return resolved;
|
||||
});
|
||||
this.cache.set(key, loading);
|
||||
return loading;
|
||||
}
|
||||
}
|
||||
|
||||
export const loadItemModules = async (
|
||||
keys: ItemKey[],
|
||||
loader: ItemLoader = new ItemLoader()
|
||||
): Promise<ItemModule[]> => {
|
||||
const modules: ItemModule[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const key of keys) {
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
modules.push(await loader.load(key));
|
||||
}
|
||||
return modules;
|
||||
};
|
||||
|
||||
export type ItemModuleRegistry<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> = Map<string, ItemModule<TriggerState>>;
|
||||
|
||||
export const createItemModuleRegistry = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
>(
|
||||
modules: ItemModule<TriggerState>[]
|
||||
): ItemModuleRegistry<TriggerState> => {
|
||||
const registry: ItemModuleRegistry<TriggerState> = new Map();
|
||||
for (const module of modules) {
|
||||
registry.set(module.key, module);
|
||||
}
|
||||
return registry;
|
||||
};
|
||||
|
||||
class ItemGeneralActionRouter<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionModule<TriggerState> {
|
||||
constructor(private readonly registry: ItemModuleRegistry<TriggerState>) {}
|
||||
|
||||
private resolveModules(
|
||||
context: GeneralActionContext<TriggerState>
|
||||
): Array<ItemModule<TriggerState>> {
|
||||
const keys = listEquippedItemKeys(context.general);
|
||||
const modules: Array<ItemModule<TriggerState>> = [];
|
||||
for (const key of keys) {
|
||||
const module = this.registry.get(key);
|
||||
if (module) {
|
||||
modules.push(module);
|
||||
}
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
getPreTurnExecuteTriggerList(
|
||||
context: GeneralActionContext<TriggerState>
|
||||
): GeneralTriggerCaller<TriggerState> | null {
|
||||
const caller = new GeneralTriggerCaller<TriggerState>();
|
||||
for (const module of this.resolveModules(context)) {
|
||||
const triggers = module.getPreTurnExecuteTriggerList?.(context);
|
||||
if (triggers) {
|
||||
caller.merge(triggers);
|
||||
}
|
||||
}
|
||||
return caller.isEmpty() ? null : caller;
|
||||
}
|
||||
|
||||
onCalcDomestic(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
turnType: string,
|
||||
varType: string,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number {
|
||||
let current = value;
|
||||
for (const module of this.resolveModules(context)) {
|
||||
if (!module.onCalcDomestic) {
|
||||
continue;
|
||||
}
|
||||
current = module.onCalcDomestic(context, turnType, varType, current, aux);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
onCalcStat(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
statName: string,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number {
|
||||
let current = value;
|
||||
for (const module of this.resolveModules(context)) {
|
||||
if (!module.onCalcStat) {
|
||||
continue;
|
||||
}
|
||||
current = module.onCalcStat(context, statName, current, aux);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
onCalcOpposeStat(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
statName: string,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number {
|
||||
let current = value;
|
||||
for (const module of this.resolveModules(context)) {
|
||||
if (!module.onCalcOpposeStat) {
|
||||
continue;
|
||||
}
|
||||
current = module.onCalcOpposeStat(context, statName, current, aux);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
onCalcStrategic(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
turnType: string,
|
||||
varType: string,
|
||||
value: number
|
||||
): number {
|
||||
let current = value;
|
||||
for (const module of this.resolveModules(context)) {
|
||||
if (!module.onCalcStrategic) {
|
||||
continue;
|
||||
}
|
||||
current = module.onCalcStrategic(context, turnType, varType, current);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
onCalcNationalIncome(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
type: string,
|
||||
amount: number
|
||||
): number {
|
||||
let current = amount;
|
||||
for (const module of this.resolveModules(context)) {
|
||||
if (!module.onCalcNationalIncome) {
|
||||
continue;
|
||||
}
|
||||
current = module.onCalcNationalIncome(context, type, current);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
onArbitraryAction(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
actionType: string,
|
||||
phase?: string | null,
|
||||
aux?: Record<string, unknown> | null
|
||||
): Record<string, unknown> | null {
|
||||
let current = aux ?? null;
|
||||
for (const module of this.resolveModules(context)) {
|
||||
if (!module.onArbitraryAction) {
|
||||
continue;
|
||||
}
|
||||
const result = module.onArbitraryAction(context, actionType, phase, current);
|
||||
if (result !== undefined) {
|
||||
current = result;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
class ItemWarActionRouter<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements WarActionModule<TriggerState> {
|
||||
constructor(private readonly registry: ItemModuleRegistry<TriggerState>) {}
|
||||
|
||||
private resolveModules(
|
||||
context: WarActionContext<TriggerState>
|
||||
): Array<ItemModule<TriggerState>> {
|
||||
const keys = listEquippedItemKeys(context.general);
|
||||
const modules: Array<ItemModule<TriggerState>> = [];
|
||||
for (const key of keys) {
|
||||
const module = this.registry.get(key);
|
||||
if (module) {
|
||||
modules.push(module);
|
||||
}
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
getBattleInitTriggerList(
|
||||
context: WarActionContext<TriggerState>
|
||||
): WarTriggerCaller | null {
|
||||
const caller = new WarTriggerCaller();
|
||||
for (const module of this.resolveModules(context)) {
|
||||
const triggers = module.getBattleInitTriggerList?.(context);
|
||||
if (triggers) {
|
||||
caller.merge(triggers);
|
||||
}
|
||||
}
|
||||
return caller.isEmpty() ? null : caller;
|
||||
}
|
||||
|
||||
getBattlePhaseTriggerList(
|
||||
context: WarActionContext<TriggerState>
|
||||
): WarTriggerCaller | null {
|
||||
const caller = new WarTriggerCaller();
|
||||
for (const module of this.resolveModules(context)) {
|
||||
const triggers = module.getBattlePhaseTriggerList?.(context);
|
||||
if (triggers) {
|
||||
caller.merge(triggers);
|
||||
}
|
||||
}
|
||||
return caller.isEmpty() ? null : caller;
|
||||
}
|
||||
|
||||
onCalcStat(
|
||||
context: WarActionContext<TriggerState>,
|
||||
statName: string,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number] {
|
||||
let current: number | [number, number] = value;
|
||||
for (const module of this.resolveModules(context)) {
|
||||
if (!module.onCalcStat) {
|
||||
continue;
|
||||
}
|
||||
current = module.onCalcStat(context, statName, current, aux);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
onCalcOpposeStat(
|
||||
context: WarActionContext<TriggerState>,
|
||||
statName: string,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number] {
|
||||
let current: number | [number, number] = value;
|
||||
for (const module of this.resolveModules(context)) {
|
||||
if (!module.onCalcOpposeStat) {
|
||||
continue;
|
||||
}
|
||||
current = module.onCalcOpposeStat(context, statName, current, aux);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
getWarPowerMultiplier(
|
||||
context: WarActionContext<TriggerState>,
|
||||
unit: WarUnit<TriggerState>,
|
||||
oppose: WarUnit<TriggerState>
|
||||
): [number, number] {
|
||||
let attack = 1;
|
||||
let defence = 1;
|
||||
for (const module of this.resolveModules(context)) {
|
||||
if (!module.getWarPowerMultiplier) {
|
||||
continue;
|
||||
}
|
||||
const [attMul, defMul] = module.getWarPowerMultiplier(
|
||||
context,
|
||||
unit,
|
||||
oppose
|
||||
);
|
||||
attack *= attMul;
|
||||
defence *= defMul;
|
||||
}
|
||||
return [attack, defence];
|
||||
}
|
||||
}
|
||||
|
||||
export const createItemActionModules = <
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
>(
|
||||
registry: ItemModuleRegistry<TriggerState>
|
||||
): { general: GeneralActionModule<TriggerState>[]; war: WarActionModule<TriggerState>[] } => ({
|
||||
general: [new ItemGeneralActionRouter(registry)],
|
||||
war: [new ItemWarActionRouter(registry)],
|
||||
});
|
||||
|
||||
export type { ItemModule, ItemModuleExport, ItemSlot } from './types.js';
|
||||
export {
|
||||
canAcquireItem,
|
||||
isInventoryEnabled,
|
||||
listEquippedItemKeys,
|
||||
consumeItemRemain,
|
||||
getItemRemain,
|
||||
setItemRemain,
|
||||
} from './utils.js';
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type {
|
||||
GeneralActionContext,
|
||||
GeneralTriggerCaller,
|
||||
} from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||
import type { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
|
||||
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
|
||||
|
||||
export type ItemSlot = 'horse' | 'weapon' | 'book' | 'item';
|
||||
|
||||
export interface ItemModule<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
key: string;
|
||||
name: string;
|
||||
rawName: string;
|
||||
info: string;
|
||||
slot: ItemSlot;
|
||||
cost: number | null;
|
||||
buyable: boolean;
|
||||
consumable: boolean;
|
||||
reqSecu: number;
|
||||
unique: boolean;
|
||||
|
||||
getPreTurnExecuteTriggerList?(
|
||||
context: GeneralActionContext<TriggerState>
|
||||
): GeneralTriggerCaller<TriggerState> | null;
|
||||
|
||||
onCalcDomestic?(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
turnType: string,
|
||||
varType: string,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number;
|
||||
|
||||
onCalcStat?: {
|
||||
(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
statName: string,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number;
|
||||
(
|
||||
context: WarActionContext<TriggerState>,
|
||||
statName: string,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
};
|
||||
|
||||
onCalcOpposeStat?: {
|
||||
(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
statName: string,
|
||||
value: number,
|
||||
aux?: unknown
|
||||
): number;
|
||||
(
|
||||
context: WarActionContext<TriggerState>,
|
||||
statName: string,
|
||||
value: number | [number, number],
|
||||
aux?: unknown
|
||||
): number | [number, number];
|
||||
};
|
||||
|
||||
onCalcStrategic?(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
turnType: string,
|
||||
varType: string,
|
||||
value: number
|
||||
): number;
|
||||
|
||||
onCalcNationalIncome?(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
type: string,
|
||||
amount: number
|
||||
): number;
|
||||
|
||||
onArbitraryAction?(
|
||||
context: GeneralActionContext<TriggerState>,
|
||||
actionType: string,
|
||||
phase?: string | null,
|
||||
aux?: Record<string, unknown> | null
|
||||
): Record<string, unknown> | null;
|
||||
|
||||
getBattleInitTriggerList?(
|
||||
context: WarActionContext<TriggerState>
|
||||
): WarTriggerCaller | null;
|
||||
|
||||
getBattlePhaseTriggerList?(
|
||||
context: WarActionContext<TriggerState>
|
||||
): WarTriggerCaller | null;
|
||||
|
||||
getWarPowerMultiplier?(
|
||||
context: WarActionContext<TriggerState>,
|
||||
unit: WarUnit<TriggerState>,
|
||||
oppose: WarUnit<TriggerState>
|
||||
): [number, number];
|
||||
}
|
||||
|
||||
export interface ItemModuleExport<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
itemModule: ItemModule<TriggerState>;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
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';
|
||||
|
||||
const ITEM_REMAIN_PREFIX = 'itemRemain:';
|
||||
|
||||
const toBoolean = (value: unknown): boolean => {
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return value > 0;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized === 'true' || normalized === 'yes' || normalized === '1';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isInventoryEnabled = (config: ScenarioConfig): boolean => {
|
||||
const constConfig = config.const ?? {};
|
||||
return toBoolean(
|
||||
constConfig['allowInventory'] ??
|
||||
constConfig['inventoryEnabled'] ??
|
||||
constConfig['enableInventory']
|
||||
);
|
||||
};
|
||||
|
||||
export const listEquippedItemKeys = <
|
||||
TriggerState extends GeneralTriggerState
|
||||
>(
|
||||
general: General<TriggerState>
|
||||
): string[] => {
|
||||
const items = [
|
||||
general.role.items.horse,
|
||||
general.role.items.weapon,
|
||||
general.role.items.book,
|
||||
general.role.items.item,
|
||||
];
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const key of items) {
|
||||
if (!key || seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
result.push(key);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getItemRemain = <
|
||||
TriggerState extends GeneralTriggerState
|
||||
>(
|
||||
general: General<TriggerState>,
|
||||
itemKey: string
|
||||
): number | null => {
|
||||
const value = general.triggerState.counters[
|
||||
`${ITEM_REMAIN_PREFIX}${itemKey}`
|
||||
];
|
||||
return typeof value === 'number' && value > 0 ? value : null;
|
||||
};
|
||||
|
||||
export const setItemRemain = <
|
||||
TriggerState extends GeneralTriggerState
|
||||
>(
|
||||
general: General<TriggerState>,
|
||||
itemKey: string,
|
||||
remain: number | null
|
||||
): void => {
|
||||
const key = `${ITEM_REMAIN_PREFIX}${itemKey}`;
|
||||
if (remain === null || remain <= 0) {
|
||||
delete general.triggerState.counters[key];
|
||||
return;
|
||||
}
|
||||
general.triggerState.counters[key] = remain;
|
||||
};
|
||||
|
||||
export const consumeItemRemain = <
|
||||
TriggerState extends GeneralTriggerState
|
||||
>(
|
||||
general: General<TriggerState>,
|
||||
itemKey: string,
|
||||
fallbackRemain = 1
|
||||
): boolean => {
|
||||
const remain = getItemRemain(general, itemKey) ?? fallbackRemain;
|
||||
if (remain > 1) {
|
||||
setItemRemain(general, itemKey, remain - 1);
|
||||
return false;
|
||||
}
|
||||
setItemRemain(general, itemKey, null);
|
||||
return true;
|
||||
};
|
||||
|
||||
export const canAcquireItem = <
|
||||
TriggerState extends GeneralTriggerState
|
||||
>(options: {
|
||||
general: General<TriggerState>;
|
||||
item: ItemModule;
|
||||
config: ScenarioConfig;
|
||||
registry: Map<string, ItemModule>;
|
||||
}): boolean => {
|
||||
const { general, item, config, registry } = options;
|
||||
if (!item.unique) {
|
||||
return true;
|
||||
}
|
||||
if (isInventoryEnabled(config)) {
|
||||
return true;
|
||||
}
|
||||
const slotItemKey = general.role.items[item.slot];
|
||||
if (!slotItemKey) {
|
||||
return true;
|
||||
}
|
||||
const slotItem = registry.get(slotItemKey);
|
||||
if (!slotItem) {
|
||||
return true;
|
||||
}
|
||||
return !slotItem.unique;
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import { TriggerPriority } from '@sammo-ts/logic/triggers/core.js';
|
||||
import {
|
||||
BaseGeneralTrigger,
|
||||
type GeneralTriggerContext,
|
||||
} from '@sammo-ts/logic/triggers/general.js';
|
||||
import type { General } from '@sammo-ts/logic/domain/entities.js';
|
||||
|
||||
interface ItemHealOptions {
|
||||
itemKey: string;
|
||||
itemName: string;
|
||||
itemRawName: string;
|
||||
injuryTarget: number;
|
||||
consume: () => boolean;
|
||||
}
|
||||
|
||||
// 아이템(환약 등)으로 턴 실행 전 부상을 회복하는 트리거.
|
||||
export class CheItemHealTrigger extends BaseGeneralTrigger {
|
||||
public readonly priority = TriggerPriority.Begin - 10;
|
||||
private readonly options: ItemHealOptions;
|
||||
|
||||
constructor(general: General, options: ItemHealOptions) {
|
||||
super(general);
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
action(
|
||||
context: GeneralTriggerContext,
|
||||
env: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const { general } = context;
|
||||
if (general.role.items.item !== this.options.itemKey) {
|
||||
return env;
|
||||
}
|
||||
if (general.injury < this.options.injuryTarget) {
|
||||
return env;
|
||||
}
|
||||
|
||||
general.injury = 0;
|
||||
context.skill.activate('pre.부상경감', 'pre.치료');
|
||||
|
||||
const josa = JosaUtil.pick(this.options.itemRawName, '을');
|
||||
context.log?.push(
|
||||
`<C>${this.options.itemName}</>${josa} 사용하여 치료합니다!`
|
||||
);
|
||||
|
||||
if (this.options.consume()) {
|
||||
general.role.items.item = null;
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { LogFormat } from '@sammo-ts/logic/logging/types.js';
|
||||
import { TriggerPriority } from '@sammo-ts/logic/triggers/core.js';
|
||||
import { BaseWarUnitTrigger } from '@sammo-ts/logic/war/triggers.js';
|
||||
import { WarUnitGeneral, type WarUnit } from '@sammo-ts/logic/war/units.js';
|
||||
import { clamp } from '@sammo-ts/logic/war/utils.js';
|
||||
|
||||
// 저격 시도 트리거: 전투 시작 타이밍에 확률 체크.
|
||||
export class CheSnipingAttemptTrigger extends BaseWarUnitTrigger {
|
||||
private readonly ratio: number;
|
||||
private readonly woundMin: number;
|
||||
private readonly woundMax: number;
|
||||
private readonly addAtmos: number;
|
||||
|
||||
constructor(
|
||||
unit: WarUnit,
|
||||
raiseType: number,
|
||||
ratio: number,
|
||||
woundMin: number,
|
||||
woundMax: number,
|
||||
addAtmos = 20
|
||||
) {
|
||||
super(unit, TriggerPriority.Pre + 100, raiseType);
|
||||
this.ratio = ratio;
|
||||
this.woundMin = woundMin;
|
||||
this.woundMax = woundMax;
|
||||
this.addAtmos = addAtmos;
|
||||
}
|
||||
|
||||
protected actionWar(
|
||||
self: WarUnit,
|
||||
oppose: WarUnit,
|
||||
selfEnv: Record<string, unknown>,
|
||||
_opposeEnv: Record<string, unknown>
|
||||
): boolean {
|
||||
if (!(self instanceof WarUnitGeneral)) {
|
||||
return true;
|
||||
}
|
||||
if (self.getPhase() !== 0 && oppose.getPhase() !== 0) {
|
||||
return true;
|
||||
}
|
||||
if (oppose.getPhase() < 0) {
|
||||
return true;
|
||||
}
|
||||
if (self.hasActivatedSkill('저격')) {
|
||||
return true;
|
||||
}
|
||||
if (self.hasActivatedSkill('저격불가')) {
|
||||
return true;
|
||||
}
|
||||
if (!self.rng.nextBool(this.ratio)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.activateSkill('저격');
|
||||
selfEnv['저격발동자'] = this.raiseType;
|
||||
selfEnv['woundMin'] = this.woundMin;
|
||||
selfEnv['woundMax'] = this.woundMax;
|
||||
selfEnv['addAtmos'] = this.addAtmos;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 저격 발동 트리거: 성공 시 사기 보정과 부상 판정.
|
||||
export class CheSnipingActivateTrigger extends BaseWarUnitTrigger {
|
||||
constructor(unit: WarUnit, raiseType: number) {
|
||||
super(unit, TriggerPriority.Post + 100, raiseType);
|
||||
}
|
||||
|
||||
protected actionWar(
|
||||
self: WarUnit,
|
||||
oppose: WarUnit,
|
||||
selfEnv: Record<string, unknown>,
|
||||
_opposeEnv: Record<string, unknown>
|
||||
): boolean {
|
||||
if (!self.hasActivatedSkill('저격')) {
|
||||
return true;
|
||||
}
|
||||
if ((selfEnv['저격발동자'] ?? -1) !== this.raiseType) {
|
||||
return true;
|
||||
}
|
||||
if (selfEnv['저격발동']) {
|
||||
return true;
|
||||
}
|
||||
selfEnv['저격발동'] = true;
|
||||
|
||||
if (oppose instanceof WarUnitGeneral) {
|
||||
self.getLogger().pushGeneralActionLog('상대를 <C>저격</>했다!', LogFormat.PLAIN);
|
||||
self.getLogger().pushGeneralBattleDetailLog(
|
||||
'상대를 <C>저격</>했다!',
|
||||
LogFormat.PLAIN
|
||||
);
|
||||
oppose.getLogger().pushGeneralActionLog(
|
||||
'상대에게 <R>저격</>당했다!',
|
||||
LogFormat.PLAIN
|
||||
);
|
||||
oppose.getLogger().pushGeneralBattleDetailLog(
|
||||
'상대에게 <R>저격</>당했다!',
|
||||
LogFormat.PLAIN
|
||||
);
|
||||
} else {
|
||||
self.getLogger().pushGeneralActionLog(
|
||||
'성벽 수비대장을 <C>저격</>했다!',
|
||||
LogFormat.PLAIN
|
||||
);
|
||||
self.getLogger().pushGeneralBattleDetailLog(
|
||||
'성벽 수비대장을 <C>저격</>했다!',
|
||||
LogFormat.PLAIN
|
||||
);
|
||||
}
|
||||
|
||||
const addAtmos = Number(selfEnv['addAtmos'] ?? 0);
|
||||
self.addAtmos(addAtmos);
|
||||
|
||||
if (!oppose.hasActivatedSkill('부상무효') && oppose instanceof WarUnitGeneral) {
|
||||
const woundMin = Number(selfEnv['woundMin'] ?? 10);
|
||||
const woundMax = Number(selfEnv['woundMax'] ?? 80);
|
||||
oppose.getGeneral().injury = clamp(
|
||||
oppose.getGeneral().injury +
|
||||
self.rng.nextRangeInt(woundMin, woundMax),
|
||||
0,
|
||||
80
|
||||
);
|
||||
}
|
||||
|
||||
this.processConsumableItem();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { TriggerPriority } from '@sammo-ts/logic/triggers/core.js';
|
||||
import { BaseWarUnitTrigger } from '@sammo-ts/logic/war/triggers.js';
|
||||
import { WarUnitGeneral, type WarUnit } from '@sammo-ts/logic/war/units.js';
|
||||
|
||||
// 전투 종료 부상을 무효화하는 상태를 부여한다.
|
||||
export class CheRetreatNoWoundTrigger extends BaseWarUnitTrigger {
|
||||
constructor(unit: WarUnit) {
|
||||
super(unit, TriggerPriority.Begin + 300);
|
||||
}
|
||||
|
||||
protected actionWar(
|
||||
self: WarUnit,
|
||||
_oppose: WarUnit,
|
||||
_selfEnv: Record<string, unknown>,
|
||||
_opposeEnv: Record<string, unknown>
|
||||
): boolean {
|
||||
if (!(self instanceof WarUnitGeneral)) {
|
||||
return true;
|
||||
}
|
||||
self.activateSkill('퇴각부상무효');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user