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:
2026-01-03 09:28:46 +00:00
parent e10bcba49b
commit d1522ff450
21 changed files with 1248 additions and 1 deletions
+356
View File
@@ -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';