From 41339176ee92e7960e5deb6bb974c8db18bd63eb Mon Sep 17 00:00:00 2001 From: Hide_D Date: Mon, 5 Jan 2026 16:43:28 +0000 Subject: [PATCH] feat: add new war traits for various unit types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implemented '궁병' trait with bonuses for archer units, including reduced recruitment costs and increased evasion. - Added '기병' trait providing cavalry units with damage bonuses and reduced recruitment costs. - Introduced '돌격' trait enhancing attack phases and damage against equal or weaker unit types. - Created '무쌍' trait that increases damage and reduces damage taken based on the number of victories. - Developed '반계' trait to reduce enemy strategy success rates and reflect damage back. - Added '보병' trait with reduced recruitment costs and damage penalties for allies during attacks. - Implemented '신산' trait enhancing magic success rates for various strategies. - Created '신중' trait guaranteeing strategy success. - Added '위압' trait to activate pressure effects on the first phase of battle. - Implemented '저격' trait allowing for a chance to activate a sniping effect against new opponents. - Developed '집중' trait to increase damage on successful strategies. - Created '척사' trait providing damage bonuses against regional and city units while reducing ally damage. - Implemented '필살' trait enhancing critical hit chances and disabling enemy evasion on critical hits. - Added '환술' trait to boost magic success rates and damage on successful magic attacks. --- .../src/actions/turn/general/che_군량매매.ts | 131 ++++++++++++++++++ .../src/actions/turn/general/che_모병.ts | 34 +++++ .../src/actions/turn/general/che_소집해제.ts | 62 +++++++++ .../src/actions/turn/general/che_정착장려.ts | 84 +++++++++++ .../src/actions/turn/general/che_징병.ts | 4 +- .../logic/src/actions/turn/general/index.ts | 8 ++ packages/logic/src/items/che_명마_01_노기.ts | 13 ++ packages/logic/src/items/index.ts | 2 + .../src/triggers/special/domestic/che_경작.ts | 25 ++++ .../src/triggers/special/domestic/che_귀모.ts | 19 +++ .../src/triggers/special/domestic/che_상재.ts | 25 ++++ .../src/triggers/special/domestic/che_수비.ts | 25 ++++ .../src/triggers/special/domestic/che_축성.ts | 25 ++++ .../src/triggers/special/domestic/che_통찰.ts | 25 ++++ .../src/triggers/special/domestic/index.ts | 17 ++- .../src/triggers/special/war/che_격노.ts | 108 +++++++++++++++ .../src/triggers/special/war/che_견고.ts | 68 +++++++++ .../src/triggers/special/war/che_공성.ts | 72 ++++++++++ .../src/triggers/special/war/che_궁병.ts | 71 ++++++++++ .../src/triggers/special/war/che_기병.ts | 71 ++++++++++ .../src/triggers/special/war/che_돌격.ts | 77 ++++++++++ .../src/triggers/special/war/che_무쌍.ts | 48 +++++++ .../src/triggers/special/war/che_반계.ts | 109 +++++++++++++++ .../src/triggers/special/war/che_보병.ts | 73 ++++++++++ .../src/triggers/special/war/che_신산.ts | 42 ++++++ .../src/triggers/special/war/che_신중.ts | 33 +++++ .../src/triggers/special/war/che_위압.ts | 87 ++++++++++++ .../src/triggers/special/war/che_저격.ts | 128 +++++++++++++++++ .../src/triggers/special/war/che_집중.ts | 33 +++++ .../src/triggers/special/war/che_척사.ts | 37 +++++ .../src/triggers/special/war/che_필살.ts | 63 +++++++++ .../src/triggers/special/war/che_환술.ts | 36 +++++ .../logic/src/triggers/special/war/index.ts | 6 +- packages/logic/src/triggers/types.ts | 4 + packages/logic/src/war/crewType.ts | 8 ++ packages/logic/src/war/units.ts | 4 + 36 files changed, 1673 insertions(+), 4 deletions(-) create mode 100644 packages/logic/src/actions/turn/general/che_군량매매.ts create mode 100644 packages/logic/src/actions/turn/general/che_모병.ts create mode 100644 packages/logic/src/actions/turn/general/che_소집해제.ts create mode 100644 packages/logic/src/actions/turn/general/che_정착장려.ts create mode 100644 packages/logic/src/items/che_명마_01_노기.ts create mode 100644 packages/logic/src/triggers/special/domestic/che_경작.ts create mode 100644 packages/logic/src/triggers/special/domestic/che_귀모.ts create mode 100644 packages/logic/src/triggers/special/domestic/che_상재.ts create mode 100644 packages/logic/src/triggers/special/domestic/che_수비.ts create mode 100644 packages/logic/src/triggers/special/domestic/che_축성.ts create mode 100644 packages/logic/src/triggers/special/domestic/che_통찰.ts create mode 100644 packages/logic/src/triggers/special/war/che_격노.ts create mode 100644 packages/logic/src/triggers/special/war/che_견고.ts create mode 100644 packages/logic/src/triggers/special/war/che_공성.ts create mode 100644 packages/logic/src/triggers/special/war/che_궁병.ts create mode 100644 packages/logic/src/triggers/special/war/che_기병.ts create mode 100644 packages/logic/src/triggers/special/war/che_돌격.ts create mode 100644 packages/logic/src/triggers/special/war/che_무쌍.ts create mode 100644 packages/logic/src/triggers/special/war/che_반계.ts create mode 100644 packages/logic/src/triggers/special/war/che_보병.ts create mode 100644 packages/logic/src/triggers/special/war/che_신산.ts create mode 100644 packages/logic/src/triggers/special/war/che_신중.ts create mode 100644 packages/logic/src/triggers/special/war/che_위압.ts create mode 100644 packages/logic/src/triggers/special/war/che_저격.ts create mode 100644 packages/logic/src/triggers/special/war/che_집중.ts create mode 100644 packages/logic/src/triggers/special/war/che_척사.ts create mode 100644 packages/logic/src/triggers/special/war/che_필살.ts create mode 100644 packages/logic/src/triggers/special/war/che_환술.ts diff --git a/packages/logic/src/actions/turn/general/che_군량매매.ts b/packages/logic/src/actions/turn/general/che_군량매매.ts new file mode 100644 index 0000000..f390183 --- /dev/null +++ b/packages/logic/src/actions/turn/general/che_군량매매.ts @@ -0,0 +1,131 @@ +import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; +import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js'; +import { occupiedCity, reqGeneralGold, reqGeneralRice } from '@sammo-ts/logic/constraints/presets.js'; +import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; +import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js'; +import { LogFormat } from '@sammo-ts/logic/logging/types.js'; +import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'; +import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js'; +import type { GeneralTurnCommandSpec } from './index.js'; + +export interface TradeArgs { + buyRice: boolean; + amount: number; +} + +export interface TradeEnvironment { + exchangeFee?: number; +} + +const ACTION_NAME = '군량매매'; +const DEFAULT_EXCHANGE_FEE = 0.01; + +export class ActionDefinition< + TriggerState extends GeneralTriggerState = GeneralTriggerState, +> implements GeneralActionDefinition { + public readonly key = 'che_군량매매'; + public readonly name = ACTION_NAME; + private readonly env: TradeEnvironment; + + constructor(env: TradeEnvironment = {}) { + this.env = env; + } + + parseArgs(raw: unknown): TradeArgs | null { + if (typeof raw !== 'object' || raw === null) { + return null; + } + const { buyRice, amount } = raw as any; + if (typeof buyRice !== 'boolean' || typeof amount !== 'number') { + return null; + } + return { buyRice, amount }; + } + + buildConstraints(_ctx: ConstraintContext, args: TradeArgs): Constraint[] { + const constraints: Constraint[] = [occupiedCity()]; + if (args.buyRice) { + constraints.push(reqGeneralGold(() => 1)); + } else { + constraints.push(reqGeneralRice(() => 1)); + } + return constraints; + } + + resolve(context: GeneralActionResolveContext, args: TradeArgs): GeneralActionOutcome { + const general = context.general; + const city = context.city; + if (!city) { + context.addLog('도시 정보가 없습니다.'); + return { effects: [] }; + } + const tradeRate = (city.meta.trade as number | undefined) ?? 100; + const rate = tradeRate / 100; + const fee = this.env.exchangeFee ?? DEFAULT_EXCHANGE_FEE; + + let buyAmount = 0; + let sellAmount = 0; + let tax = 0; + + if (args.buyRice) { + const requestedSell = Math.min(args.amount * rate, general.gold); + tax = requestedSell * fee; + if (requestedSell + tax > general.gold) { + sellAmount = general.gold; + tax = sellAmount * (fee / (1 + fee)); + const actualSell = sellAmount - tax; + buyAmount = actualSell / rate; + } else { + sellAmount = requestedSell + tax; + buyAmount = args.amount; + } + general.gold = Math.max(0, general.gold - sellAmount); + general.rice += buyAmount; + context.addLog( + `군량 ${Math.round(buyAmount).toLocaleString()}을 사서 자금 ${Math.round( + sellAmount + ).toLocaleString()}을 썼습니다.`, + { format: LogFormat.PLAIN } + ); + } else { + sellAmount = Math.min(args.amount, general.rice); + const grossBuy = sellAmount * rate; + tax = grossBuy * fee; + buyAmount = grossBuy - tax; + general.rice = Math.max(0, general.rice - sellAmount); + general.gold += buyAmount; + context.addLog( + `군량 ${Math.round(sellAmount).toLocaleString()}을 팔아 자금 ${Math.round( + buyAmount + ).toLocaleString()}을 얻었습니다.`, + { format: LogFormat.PLAIN } + ); + } + + // 국고 증가 (세금) + if (context.nation) { + const nation = context.nation; + const currentGold = (nation.gold as number) ?? 0; + nation.gold = currentGold + tax; + } + + // 경험치 및 명성 증가 + general.experience += 30; + general.dedication += 50; + + return { effects: [] }; + } +} + +export const actionContextBuilder = defaultActionContextBuilder; + +export const commandSpec: GeneralTurnCommandSpec = { + key: 'che_군량매매', + category: '개인', + reqArg: true, + args: { + buyRice: 'boolean', + amount: 'number', + }, + createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(), +}; diff --git a/packages/logic/src/actions/turn/general/che_모병.ts b/packages/logic/src/actions/turn/general/che_모병.ts new file mode 100644 index 0000000..48a6edd --- /dev/null +++ b/packages/logic/src/actions/turn/general/che_모병.ts @@ -0,0 +1,34 @@ +import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; +import { ActionDefinition as RecruitActionDefinition } from './che_징병.js'; +import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'; +import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js'; +import type { GeneralTurnCommandSpec } from './index.js'; +import type { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js'; + +export class ActionDefinition< + TriggerState extends GeneralTriggerState = GeneralTriggerState, +> extends RecruitActionDefinition { + public override readonly key = 'che_모병'; + public override readonly name = '모병'; + + constructor(modules: GeneralActionModule[]) { + super(modules, { + costOffset: 2, + defaultTrain: 70, // GameConst::$defaultTrainHigh + defaultAtmos: 70, // GameConst::$defaultAtmosHigh + }); + } +} + +export const actionContextBuilder = defaultActionContextBuilder; + +export const commandSpec: GeneralTurnCommandSpec = { + key: 'che_모병', + category: '군사', + reqArg: true, + args: { + crewType: 'number', + amount: 'number', + }, + createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []), +}; diff --git a/packages/logic/src/actions/turn/general/che_소집해제.ts b/packages/logic/src/actions/turn/general/che_소집해제.ts new file mode 100644 index 0000000..4ff84f8 --- /dev/null +++ b/packages/logic/src/actions/turn/general/che_소집해제.ts @@ -0,0 +1,62 @@ +import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; +import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js'; +import { reqGeneralCrew } from '@sammo-ts/logic/constraints/presets.js'; +import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; +import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js'; +import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'; +import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js'; +import { clamp } from 'es-toolkit'; +import type { GeneralTurnCommandSpec } from './index.js'; + +export interface DisbandArgs {} + +export class ActionDefinition< + TriggerState extends GeneralTriggerState = GeneralTriggerState, +> implements GeneralActionDefinition { + public readonly key = 'che_소집해제'; + public readonly name = '소집해제'; + + parseArgs(_raw: unknown): DisbandArgs | null { + return {}; + } + + buildConstraints(_ctx: ConstraintContext, _args: DisbandArgs): Constraint[] { + return [reqGeneralCrew()]; + } + + resolve( + context: GeneralActionResolveContext, + _args: DisbandArgs + ): GeneralActionOutcome { + const general = context.general; + const city = context.city; + if (!city) { + context.addLog('도시 정보를 찾지 못했습니다.'); + return { effects: [] }; + } + + const crew = general.crew; + const currentPop = city.population; + const maxPop = city.populationMax; + + const nextPop = clamp(currentPop + crew, 0, maxPop); + const addedPop = nextPop - currentPop; + + general.crew = 0; + city.population = nextPop; + + context.addLog(`병사들을 소집해제하여 인구가 ${addedPop} 증가했습니다.`); + + return { effects: [] }; + } +} + +export const actionContextBuilder = defaultActionContextBuilder; + +export const commandSpec: GeneralTurnCommandSpec = { + key: 'che_소집해제', + category: '군사', + reqArg: false, + args: {}, + createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(), +}; diff --git a/packages/logic/src/actions/turn/general/che_정착장려.ts b/packages/logic/src/actions/turn/general/che_정착장려.ts new file mode 100644 index 0000000..501128b --- /dev/null +++ b/packages/logic/src/actions/turn/general/che_정착장려.ts @@ -0,0 +1,84 @@ +import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; +import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js'; +import { + notBeNeutral, + notWanderingNation, + occupiedCity, + remainCityCapacityByMax, + reqGeneralRice, + suppliedCity, +} from '@sammo-ts/logic/constraints/presets.js'; +import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; +import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js'; +import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'; +import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js'; +import { clamp } from 'es-toolkit'; +import type { GeneralTurnCommandSpec } from './index.js'; + +export interface SettlementArgs {} + +export class ActionDefinition< + TriggerState extends GeneralTriggerState = GeneralTriggerState, +> implements GeneralActionDefinition { + public readonly key = 'che_정착장려'; + public readonly name = '정착 장려'; + private readonly env: { develCost?: number }; + + constructor(env: { develCost?: number } = {}) { + this.env = env; + } + + parseArgs(_raw: unknown): SettlementArgs | null { + return {}; + } + + buildConstraints(_ctx: ConstraintContext, _args: SettlementArgs): Constraint[] { + const getRequiredRice = (_context: ConstraintContext, _view: StateView): number => + (this.env.develCost ?? 0) * 2; + + return [ + notBeNeutral(), + notWanderingNation(), + occupiedCity(), + suppliedCity(), + remainCityCapacityByMax('population', 'populationMax', '인구'), + reqGeneralRice(getRequiredRice), + ]; + } + + resolve( + context: GeneralActionResolveContext, + _args: SettlementArgs + ): GeneralActionOutcome { + const general = context.general; + const city = context.city; + if (!city) { + context.addLog('도시 정보를 찾지 못했습니다.'); + return { effects: [] }; + } + + const baseAmount = 1000; + const current = city.population; + const max = city.populationMax; + + const nextValue = clamp(current + baseAmount, 0, max); + const costRice = (this.env.develCost ?? 0) * 2; + + city.population = nextValue; + general.rice = Math.max(0, general.rice - costRice); + + context.addLog(`인구가 ${nextValue - current} 증가했습니다.`); + + return { effects: [] }; + } +} + +export const actionContextBuilder = defaultActionContextBuilder; + +export const commandSpec: GeneralTurnCommandSpec = { + key: 'che_정착장려', + category: '내정', + reqArg: false, + args: {}, + createDefinition: (env: TurnCommandEnv) => new ActionDefinition({ develCost: env.develCost }), +}; diff --git a/packages/logic/src/actions/turn/general/che_징병.ts b/packages/logic/src/actions/turn/general/che_징병.ts index f65c0ce..a0390c6 100644 --- a/packages/logic/src/actions/turn/general/che_징병.ts +++ b/packages/logic/src/actions/turn/general/che_징병.ts @@ -388,8 +388,8 @@ export class ActionResolver< export class ActionDefinition< TriggerState extends GeneralTriggerState = GeneralTriggerState, > implements GeneralActionDefinition> { - public readonly key = 'che_징병'; - public readonly name = ACTION_NAME; + public readonly key: string = 'che_징병'; + public readonly name: string = ACTION_NAME; private readonly command: CommandResolver; private readonly resolver: ActionResolver; private readonly env: RecruitEnvironment; diff --git a/packages/logic/src/actions/turn/general/index.ts b/packages/logic/src/actions/turn/general/index.ts index a0dc35b..bab29a4 100644 --- a/packages/logic/src/actions/turn/general/index.ts +++ b/packages/logic/src/actions/turn/general/index.ts @@ -14,6 +14,7 @@ export const GENERAL_TURN_COMMAND_KEYS = [ 'che_전투특기초기화', 'che_출병', 'che_주민선정', + 'che_정착장려', 'che_농지개간', 'che_상업투자', 'che_기술연구', @@ -24,6 +25,9 @@ export const GENERAL_TURN_COMMAND_KEYS = [ 'che_집합', 'che_인재탐색', 'che_징병', + 'che_모병', + 'che_소집해제', + 'che_군량매매', '휴식', ] as const; @@ -49,6 +53,7 @@ const defaultImporters: Record import('./che_전투특기초기화.js'), che_출병: async () => import('./che_출병.js'), che_주민선정: async () => import('./che_주민선정.js'), + che_정착장려: async () => import('./che_정착장려.js'), che_농지개간: async () => import('./che_농지개간.js'), che_상업투자: async () => import('./che_상업투자.js'), che_기술연구: async () => import('./che_기술연구.js'), @@ -59,6 +64,9 @@ const defaultImporters: Record import('./che_집합.js'), che_인재탐색: async () => import('./che_인재탐색.js'), che_징병: async () => import('./che_징병.js'), + che_모병: async () => import('./che_모병.js'), + che_소집해제: async () => import('./che_소집해제.js'), + che_군량매매: async () => import('./che_군량매매.js'), 휴식: async () => import('./휴식.js'), }; diff --git a/packages/logic/src/items/che_명마_01_노기.ts b/packages/logic/src/items/che_명마_01_노기.ts new file mode 100644 index 0000000..ca48d18 --- /dev/null +++ b/packages/logic/src/items/che_명마_01_노기.ts @@ -0,0 +1,13 @@ +import { createStatItemModule } from './base.js'; +import type { ItemModule } from './types.js'; + +export const itemModule: ItemModule = createStatItemModule({ + key: 'che_명마_01_노기', + rawName: '노기', + slot: 'horse', + statName: 'leadership', + statValue: 1, + cost: 1000, + buyable: true, + reqSecu: 1000, +}); diff --git a/packages/logic/src/items/index.ts b/packages/logic/src/items/index.ts index da02a3f..b53f5fe 100644 --- a/packages/logic/src/items/index.ts +++ b/packages/logic/src/items/index.ts @@ -27,6 +27,7 @@ export const ITEM_KEYS = [ 'che_무기_09_동호비궁', 'che_서적_08_전론', 'che_보물_도기', + 'che_명마_01_노기', ] as const; export type ItemKey = (typeof ITEM_KEYS)[number]; @@ -42,6 +43,7 @@ const defaultImporters: Record = { che_무기_09_동호비궁: async () => import('./che_무기_09_동호비궁.js'), che_서적_08_전론: async () => import('./che_서적_08_전론.js'), che_보물_도기: async () => import('./che_보물_도기.js'), + che_명마_01_노기: async () => import('./che_명마_01_노기.js'), }; export const isItemKey = (value: string): value is ItemKey => ITEM_KEYS.includes(value as ItemKey); diff --git a/packages/logic/src/triggers/special/domestic/che_경작.ts b/packages/logic/src/triggers/special/domestic/che_경작.ts new file mode 100644 index 0000000..5cc2179 --- /dev/null +++ b/packages/logic/src/triggers/special/domestic/che_경작.ts @@ -0,0 +1,25 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; + +// 내정 특기: 경작 +export const traitModule: TraitModule = { + key: 'che_경작', + name: '경작', + info: '[내정] 농지 개간 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%', + kind: 'domestic', + getName: () => '경작', + getInfo: () => '[내정] 농지 개간 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%', + onCalcDomestic: (_context, turnType, varType, value) => { + if (turnType === '농업') { + if (varType === 'score') { + return value * 1.1; + } + if (varType === 'cost') { + return value * 0.8; + } + if (varType === 'success') { + return value + 0.1; + } + } + return value; + }, +}; diff --git a/packages/logic/src/triggers/special/domestic/che_귀모.ts b/packages/logic/src/triggers/special/domestic/che_귀모.ts new file mode 100644 index 0000000..63ae228 --- /dev/null +++ b/packages/logic/src/triggers/special/domestic/che_귀모.ts @@ -0,0 +1,19 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; + +// 내정 특기: 귀모 +export const traitModule: TraitModule = { + key: 'che_귀모', + name: '귀모', + info: '[계략] 화계·탈취·파괴·선동 : 성공률 +20%p', + kind: 'domestic', + getName: () => '귀모', + getInfo: () => '[계략] 화계·탈취·파괴·선동 : 성공률 +20%p', + onCalcDomestic: (_context, turnType, varType, value) => { + if (turnType === '계략') { + if (varType === 'success') { + return value + 0.2; + } + } + return value; + }, +}; diff --git a/packages/logic/src/triggers/special/domestic/che_상재.ts b/packages/logic/src/triggers/special/domestic/che_상재.ts new file mode 100644 index 0000000..1ee3235 --- /dev/null +++ b/packages/logic/src/triggers/special/domestic/che_상재.ts @@ -0,0 +1,25 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; + +// 내정 특기: 상재 +export const traitModule: TraitModule = { + key: 'che_상재', + name: '상재', + info: '[내정] 상업 투자 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%', + kind: 'domestic', + getName: () => '상재', + getInfo: () => '[내정] 상업 투자 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%', + onCalcDomestic: (_context, turnType, varType, value) => { + if (turnType === '상업') { + if (varType === 'score') { + return value * 1.1; + } + if (varType === 'cost') { + return value * 0.8; + } + if (varType === 'success') { + return value + 0.1; + } + } + return value; + }, +}; diff --git a/packages/logic/src/triggers/special/domestic/che_수비.ts b/packages/logic/src/triggers/special/domestic/che_수비.ts new file mode 100644 index 0000000..4525478 --- /dev/null +++ b/packages/logic/src/triggers/special/domestic/che_수비.ts @@ -0,0 +1,25 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; + +// 내정 특기: 수비 +export const traitModule: TraitModule = { + key: 'che_수비', + name: '수비', + info: '[내정] 수비 강화 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%', + kind: 'domestic', + getName: () => '수비', + getInfo: () => '[내정] 수비 강화 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%', + onCalcDomestic: (_context, turnType, varType, value) => { + if (turnType === '수비') { + if (varType === 'score') { + return value * 1.1; + } + if (varType === 'cost') { + return value * 0.8; + } + if (varType === 'success') { + return value + 0.1; + } + } + return value; + }, +}; diff --git a/packages/logic/src/triggers/special/domestic/che_축성.ts b/packages/logic/src/triggers/special/domestic/che_축성.ts new file mode 100644 index 0000000..9d0a158 --- /dev/null +++ b/packages/logic/src/triggers/special/domestic/che_축성.ts @@ -0,0 +1,25 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; + +// 내정 특기: 축성 +export const traitModule: TraitModule = { + key: 'che_축성', + name: '축성', + info: '[내정] 성벽 보수 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%', + kind: 'domestic', + getName: () => '축성', + getInfo: () => '[내정] 성벽 보수 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%', + onCalcDomestic: (_context, turnType, varType, value) => { + if (turnType === '성벽') { + if (varType === 'score') { + return value * 1.1; + } + if (varType === 'cost') { + return value * 0.8; + } + if (varType === 'success') { + return value + 0.1; + } + } + return value; + }, +}; diff --git a/packages/logic/src/triggers/special/domestic/che_통찰.ts b/packages/logic/src/triggers/special/domestic/che_통찰.ts new file mode 100644 index 0000000..e8b66ad --- /dev/null +++ b/packages/logic/src/triggers/special/domestic/che_통찰.ts @@ -0,0 +1,25 @@ +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; + +// 내정 특기: 통찰 +export const traitModule: TraitModule = { + key: 'che_통찰', + name: '통찰', + info: '[내정] 치안 강화 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%', + kind: 'domestic', + getName: () => '통찰', + getInfo: () => '[내정] 치안 강화 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%', + onCalcDomestic: (_context, turnType, varType, value) => { + if (turnType === '치안') { + if (varType === 'score') { + return value * 1.1; + } + if (varType === 'cost') { + return value * 0.8; + } + if (varType === 'success') { + return value + 0.1; + } + } + return value; + }, +}; diff --git a/packages/logic/src/triggers/special/domestic/index.ts b/packages/logic/src/triggers/special/domestic/index.ts index 925de37..941bae9 100644 --- a/packages/logic/src/triggers/special/domestic/index.ts +++ b/packages/logic/src/triggers/special/domestic/index.ts @@ -1,6 +1,15 @@ import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/triggers/special/types.js'; -export const DOMESTIC_TRAIT_KEYS = ['che_인덕', 'che_발명'] as const; +export const DOMESTIC_TRAIT_KEYS = [ + 'che_인덕', + 'che_발명', + 'che_경작', + 'che_상재', + 'che_축성', + 'che_수비', + 'che_통찰', + 'che_귀모', +] as const; export type DomesticTraitKey = (typeof DOMESTIC_TRAIT_KEYS)[number]; @@ -11,6 +20,12 @@ export type DomesticTraitImporter = () => Promise; const defaultImporters: Record = { che_인덕: async () => import('./che_인덕.js'), che_발명: async () => import('./che_발명.js'), + che_경작: async () => import('./che_경작.js'), + che_상재: async () => import('./che_상재.js'), + che_축성: async () => import('./che_축성.js'), + che_수비: async () => import('./che_수비.js'), + che_통찰: async () => import('./che_통찰.js'), + che_귀모: async () => import('./che_귀모.js'), }; export const isDomesticTraitKey = (value: string): value is DomesticTraitKey => diff --git a/packages/logic/src/triggers/special/war/che_격노.ts b/packages/logic/src/triggers/special/war/che_격노.ts new file mode 100644 index 0000000..7dc000a --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_격노.ts @@ -0,0 +1,108 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; +import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js'; +import type { WarUnit } from '@sammo-ts/logic/war/units.js'; +import { LogFormat } from '@sammo-ts/logic/logging/types.js'; + +class che_격노시도 extends BaseWarUnitTrigger { + constructor(unit: WarUnit) { + super(unit, 30400); + } + + protected actionWar( + self: WarUnit, + oppose: WarUnit, + _selfEnv: Record, + _opposeEnv: Record + ): boolean { + if (!oppose.hasActivatedSkill('필살') && !oppose.hasActivatedSkill('회피')) { + return true; + } + if (self.hasActivatedSkill('격노불가')) { + return true; + } + + if (oppose.hasActivatedSkill('필살')) { + self.activateSkill('격노'); + oppose.deactivateSkill('회피'); + if (self.isAttacker() && self.rng.nextBool(1 / 2)) { + self.activateSkill('진노'); + } + } else if (self.rng.nextBool(1 / 4)) { + self.activateSkill('격노'); + oppose.deactivateSkill('회피'); + if (self.isAttacker() && self.rng.nextBool(1 / 2)) { + self.activateSkill('진노'); + } + } + return true; + } +} + +class che_격노발동 extends BaseWarUnitTrigger { + constructor(unit: WarUnit) { + super(unit, 40600); + } + + protected actionWar( + self: WarUnit, + oppose: WarUnit, + _selfEnv: Record, + _opposeEnv: Record + ): boolean { + if (!self.hasActivatedSkill('격노')) { + return true; + } + + const targetAct = oppose.hasActivatedSkill('필살') ? '필살 공격' : '회피 시도'; + const is진노 = self.hasActivatedSkill('진노'); + const reaction = is진노 ? '진노' : '격노'; + + self.getLogger().pushGeneralBattleDetailLog(`상대의 ${targetAct}에 ${reaction}했다!`, LogFormat.PLAIN); + oppose.getLogger().pushGeneralBattleDetailLog(`${targetAct}에 상대가 ${reaction}했다!`, LogFormat.PLAIN); + + if (is진노) { + self.addBonusPhase(1); + } + self.multiplyWarPowerMultiply(self.criticalDamage()); + + return true; + } +} + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + return value; +} + +export const traitModule: TraitModule = { + key: 'che_격노', + name: '격노', + info: '[전투] 상대방 필살 시 격노(필살) 발동, 회피 시도시 25% 확률로 격노 발동, 공격 시 일정 확률로 진노(1페이즈 추가), 격노마다 대미지 20% 추가 중첩', + kind: 'war', + getName: () => '격노', + getInfo: () => + '[전투] 상대방 필살 시 격노(필살) 발동, 회피 시도시 25% 확률로 격노 발동, 공격 시 일정 확률로 진노(1페이즈 추가), 격노마다 대미지 20% 추가 중첩', + getWarPowerMultiplier: (_context, unit, _oppose) => { + const activatedCnt = unit.hasActivatedSkillOnLog('격노'); + return [1 + 0.2 * activatedCnt, 1]; + }, + getBattlePhaseTriggerList: (_context) => { + if (!_context.unit) return null; + return new WarTriggerCaller(new che_격노시도(_context.unit), new che_격노발동(_context.unit)); + }, + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_견고.ts b/packages/logic/src/triggers/special/war/che_견고.ts new file mode 100644 index 0000000..6f02d1b --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_견고.ts @@ -0,0 +1,68 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; +import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js'; +import type { WarUnit } from '@sammo-ts/logic/war/units.js'; + +class che_부상무효 extends BaseWarUnitTrigger { + constructor(unit: WarUnit) { + super(unit, 10200); + } + + protected actionWar( + self: WarUnit, + _oppose: WarUnit, + _selfEnv: Record, + _opposeEnv: Record + ): boolean { + self.activateSkill('부상무효'); + return true; + } +} + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + return value; +} + +export const traitModule: TraitModule = { + key: 'che_견고', + name: '견고', + info: '[전투] 상대 필살 확률 -20%p, 상대 계략 시도시 성공 확률 -10%p, 부상 없음, 아군 피해 -10%', + kind: 'war', + getName: () => '견고', + getInfo: () => '[전투] 상대 필살 확률 -20%p, 상대 계략 시도시 성공 확률 -10%p, 부상 없음, 아군 피해 -10%', + onCalcOpposeStat: (_context, statName, value, _aux) => { + if (statName === 'warMagicSuccessProb') { + return (value as number) - 0.1; + } + if (statName === 'warCriticalRatio') { + return (value as number) - 0.2; + } + return value; + }, + getBattleInitTriggerList: (_context) => { + if (!_context.unit) return null; + return new WarTriggerCaller(new che_부상무효(_context.unit)); + }, + getBattlePhaseTriggerList: (_context) => { + if (!_context.unit) return null; + return new WarTriggerCaller(new che_부상무효(_context.unit)); + }, + getWarPowerMultiplier: (_context, _unit, _oppose) => { + return [1, 0.9]; + }, + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_공성.ts b/packages/logic/src/triggers/special/war/che_공성.ts new file mode 100644 index 0000000..a1ad260 --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_공성.ts @@ -0,0 +1,72 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; +import { getMetaNumber } from '@sammo-ts/logic/war/utils.js'; +import { WarUnitCity } from '@sammo-ts/logic/war/units.js'; + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + if (!('unit' in context) || !context.unit) { + return value; + } + const unit = context.unit; + + const siegeType = unit.getGameConfig().armTypes.siege; + if (siegeType === undefined) { + return value; + } + + if (statName.startsWith('dex')) { + const myDex = getMetaNumber(context.general.meta, `dex${siegeType}`); + const isAttacker = (aux as any)?.isAttacker; + const opposeType = (aux as any)?.opposeType; + + if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) { + return (value as number) + myDex; + } + if (!isAttacker && statName === `dex${siegeType}`) { + return (value as number) + myDex; + } + } + return value; +} + +// 전투 특기: 공성 +export const traitModule: TraitModule = { + key: 'che_공성', + name: '공성', + info: '[군사] 차병 계통 징·모병비 -10%
[전투] 성벽 공격 시 대미지 +100%,
공격시 상대 병종에/수비시 자신 병종 숙련에 차병 숙련을 가산', + kind: 'war', + getName: () => '공성', + getInfo: () => + '[군사] 차병 계통 징·모병비 -10%
[전투] 성벽 공격 시 대미지 +100%,
공격시 상대 병종에/수비시 자신 병종 숙련에 차병 숙련을 가산', + onCalcDomestic: (_context, turnType, varType, value, aux) => { + if (turnType === '징병' || turnType === '모병') { + if (varType === 'cost' && aux && typeof aux === 'object' && 'armType' in aux) { + if ((aux as any).armType === 4) { + return value * 0.9; + } + } + } + return value; + }, + getWarPowerMultiplier: (_context, _unit, oppose) => { + if (oppose instanceof WarUnitCity) { + return [2, 1]; + } + return [1, 1]; + }, + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_궁병.ts b/packages/logic/src/triggers/special/war/che_궁병.ts new file mode 100644 index 0000000..2b5d87f --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_궁병.ts @@ -0,0 +1,71 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; +import { getMetaNumber } from '@sammo-ts/logic/war/utils.js'; + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + if (statName === 'warAvoidRatio') { + return (value as number) + 0.2; + } + + if (!('unit' in context) || !context.unit) { + return value; + } + const unit = context.unit; + + const archerType = unit.getGameConfig().armTypes.archer; + if (archerType === undefined) { + return value; + } + + if (statName.startsWith('dex')) { + const myDex = getMetaNumber(context.general.meta, `dex${archerType}`); + const isAttacker = (aux as any)?.isAttacker; + const opposeType = (aux as any)?.opposeType; + + if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) { + return (value as number) + myDex; + } + if (!isAttacker && statName === `dex${archerType}`) { + return (value as number) + myDex; + } + } + return value; +} + +// 전투 특기: 궁병 +export const traitModule: TraitModule = { + key: 'che_궁병', + name: '궁병', + info: '[군사] 궁병 계통 징·모병비 -10%
[전투] 회피 확률 +20%p,
공격시 상대 병종에/수비시 자신 병종 숙련에 궁병 숙련을 가산', + kind: 'war', + getName: () => '궁병', + getInfo: () => + '[군사] 궁병 계통 징·모병비 -10%
[전투] 회피 확률 +20%p,
공격시 상대 병종에/수비시 자신 병종 숙련에 궁병 숙련을 가산', + onCalcDomestic: (_context, turnType, varType, value, aux) => { + if (turnType === '징병' || turnType === '모병') { + if (varType === 'cost' && aux && typeof aux === 'object' && 'armType' in aux) { + // Note: In a real scenario, we should check if aux.armType is archer. + // Since we don't have easy access to config here, we might need to assume legacy ID 2 or similar. + if ((aux as any).armType === 2) { + return value * 0.9; + } + } + } + return value; + }, + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_기병.ts b/packages/logic/src/triggers/special/war/che_기병.ts new file mode 100644 index 0000000..c26d81e --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_기병.ts @@ -0,0 +1,71 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; +import { getMetaNumber } from '@sammo-ts/logic/war/utils.js'; + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + if (!('unit' in context) || !context.unit) { + return value; + } + const unit = context.unit; + + const cavalryType = unit.getGameConfig().armTypes.cavalry; + if (cavalryType === undefined) { + return value; + } + + if (statName.startsWith('dex')) { + const myDex = getMetaNumber(context.general.meta, `dex${cavalryType}`); + const isAttacker = (aux as any)?.isAttacker; + const opposeType = (aux as any)?.opposeType; + + if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) { + return (value as number) + myDex; + } + if (!isAttacker && statName === `dex${cavalryType}`) { + return (value as number) + myDex; + } + } + return value; +} + +// 전투 특기: 기병 +export const traitModule: TraitModule = { + key: 'che_기병', + name: '기병', + info: '[군사] 기병 계통 징·모병비 -10%
[전투] 수비 시 대미지 +10%, 공격 시 대미지 +20%,
공격시 상대 병종에/수비시 자신 병종 숙련에 기병 숙련을 가산', + kind: 'war', + getName: () => '기병', + getInfo: () => + '[군사] 기병 계통 징·모병비 -10%
[전투] 수비 시 대미지 +10%, 공격 시 대미지 +20%,
공격시 상대 병종에/수비시 자신 병종 숙련에 기병 숙련을 가산', + onCalcDomestic: (_context, turnType, varType, value, aux) => { + if (turnType === '징병' || turnType === '모병') { + if (varType === 'cost' && aux && typeof aux === 'object' && 'armType' in aux) { + if ((aux as any).armType === 3) { + return value * 0.9; + } + } + } + return value; + }, + getWarPowerMultiplier: (_context, unit, _oppose) => { + if (unit.isAttacker()) { + return [1.2, 1]; + } + return [1.1, 1]; + }, + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_돌격.ts b/packages/logic/src/triggers/special/war/che_돌격.ts new file mode 100644 index 0000000..b40b035 --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_돌격.ts @@ -0,0 +1,77 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; +import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js'; +import type { WarUnit } from '@sammo-ts/logic/war/units.js'; + +class che_돌격지속 extends BaseWarUnitTrigger { + constructor(unit: WarUnit) { + super(unit, 40900); + } + + protected actionWar( + self: WarUnit, + oppose: WarUnit, + _selfEnv: Record, + _opposeEnv: Record + ): boolean { + if (oppose.constructor.name === 'WarUnitCity') { + return true; + } + if (!self.isAttacker()) { + return true; + } + const attackCoef = self.getCrewType().getAttackCoef(oppose.getCrewType()); + if (attackCoef < 1) { + if (oppose.hasActivatedSkill('선제') && self.getPhase() >= self.getMaxPhase() - 2) { + self.addBonusPhase(-1); + } + return true; + } + if (self.getPhase() < self.getMaxPhase() - 1) { + return true; + } + self.addBonusPhase(1); + return true; + } +} + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + if (statName === 'initWarPhase') { + return (value as number) + 2; + } + return value; +} + +export const traitModule: TraitModule = { + key: 'che_돌격', + name: '돌격', + info: '[전투] 공격 시 대등/유리한 병종에게는 퇴각 전까지 전투, 공격 시 페이즈 + 2, 공격 시 대미지 +5%', + kind: 'war', + getName: () => '돌격', + getInfo: () => '[전투] 공격 시 대등/유리한 병종에게는 퇴각 전까지 전투, 공격 시 페이즈 + 2, 공격 시 대미지 +5%', + getWarPowerMultiplier: (_context, unit, _oppose) => { + if (unit.isAttacker()) { + return [1.05, 1]; + } + return [1, 1]; + }, + getBattlePhaseTriggerList: (_context) => { + if (!_context.unit) return null; + return new WarTriggerCaller(new che_돌격지속(_context.unit)); + }, + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_무쌍.ts b/packages/logic/src/triggers/special/war/che_무쌍.ts new file mode 100644 index 0000000..d6d7e8a --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_무쌍.ts @@ -0,0 +1,48 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; +import { getMetaNumber } from '@sammo-ts/logic/war/utils.js'; + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + if (statName === 'warCriticalRatio' && (aux as any)?.isAttacker) { + return (value as number) + 0.1; + } + return value; +} + +export const traitModule: TraitModule = { + key: 'che_무쌍', + name: '무쌍', + info: '[전투] 대미지 +5%, 피해 -2%, 공격 시 필살 확률 +10%p,
승리 수의 로그 비례로 대미지 상승(10회 ⇒ +5%, 40회 ⇒ +15%)
승리 수의 로그 비례로 피해 감소(10회 ⇒ -2%, 40회 ⇒ -6%)', + kind: 'war', + getName: () => '무쌍', + getInfo: () => + '[전투] 대미지 +5%, 피해 -2%, 공격 시 필살 확률 +10%p,
승리 수의 로그 비례로 대미지 상승(10회 ⇒ +5%, 40회 ⇒ +15%)
승리 수의 로그 비례로 피해 감소(10회 ⇒ -2%, 40회 ⇒ -6%)', + getWarPowerMultiplier: (_context, unit, _oppose) => { + let attackMultiplier = 1.05; + let defenceMultiplier = 0.98; + // Note: unit.getGeneral() is only available for WarUnitGeneral. + // In a real scenario, we should check if unit is WarUnitGeneral. + if ('getGeneral' in unit) { + const killnum = getMetaNumber((unit as any).getGeneral().meta, 'rank_killnum', 0); + const logVal = Math.log2(Math.max(1, killnum / 5)); + attackMultiplier += logVal / 20; + defenceMultiplier -= logVal / 50; + } + return [attackMultiplier, defenceMultiplier]; + }, + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_반계.ts b/packages/logic/src/triggers/special/war/che_반계.ts new file mode 100644 index 0000000..0ab7d51 --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_반계.ts @@ -0,0 +1,109 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; +import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js'; +import type { WarUnit } from '@sammo-ts/logic/war/units.js'; +import { LogFormat } from '@sammo-ts/logic/logging/types.js'; + +class che_반계시도 extends BaseWarUnitTrigger { + private readonly prob: number; + + constructor(unit: WarUnit, prob = 0.4) { + super(unit, 30300); + this.prob = prob; + } + + protected actionWar( + self: WarUnit, + oppose: WarUnit, + _selfEnv: Record, + _opposeEnv: Record + ): boolean { + if (!oppose.hasActivatedSkill('계략')) { + return true; + } + if (self.hasActivatedSkill('반계불가')) { + return true; + } + + if (!self.rng.nextBool(this.prob)) { + return true; + } + + self.activateSkill('반계'); + oppose.deactivateSkill('계략'); + + return true; + } +} + +class che_반계발동 extends BaseWarUnitTrigger { + constructor(unit: WarUnit) { + super(unit, 40250); + } + + protected actionWar( + self: WarUnit, + oppose: WarUnit, + _selfEnv: Record, + opposeEnv: Record + ): boolean { + if (!self.hasActivatedSkill('반계')) { + return true; + } + + const magicData = opposeEnv.magic as [string, number] | undefined; + if (!magicData) { + return true; + } + + const [opposeMagic, damage] = magicData; + + self.getLogger().pushGeneralBattleDetailLog(`반계로 상대의 ${opposeMagic}을 되돌렸다!`, LogFormat.PLAIN); + oppose.getLogger().pushGeneralBattleDetailLog(`${opposeMagic}을 역으로 당했다!`, LogFormat.PLAIN); + + self.multiplyWarPowerMultiply(damage); + + return true; + } +} + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + if (statName === 'warMagicSuccessDamage' && aux === '반목') { + return (value as number) + 0.9; + } + return value; +} + +export const traitModule: TraitModule = { + key: 'che_반계', + name: '반계', + info: '[전투] 상대의 계략 성공 확률 -10%p, 상대의 계략을 40% 확률로 되돌림, 반목 성공시 대미지 추가(+60% → +150%)', + kind: 'war', + getName: () => '반계', + getInfo: () => '[전투] 상대의 계략 성공 확률 -10%p, 상대의 계략을 40% 확률로 되돌림, 반목 성공시 대미지 추가(+60% → +150%)', + onCalcOpposeStat: (_context, statName, value, _aux) => { + if (statName === 'warMagicSuccessProb') { + return (value as number) - 0.1; + } + return value; + }, + getBattlePhaseTriggerList: (_context) => { + if (!_context.unit) return null; + return new WarTriggerCaller(new che_반계시도(_context.unit), new che_반계발동(_context.unit)); + }, + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_보병.ts b/packages/logic/src/triggers/special/war/che_보병.ts new file mode 100644 index 0000000..449d7b1 --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_보병.ts @@ -0,0 +1,73 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; +import { getMetaNumber } from '@sammo-ts/logic/war/utils.js'; + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + if (!('unit' in context) || !context.unit) { + return value; + } + const unit = context.unit; + + const footmanType = unit.getGameConfig().armTypes.footman; + if (footmanType === undefined) { + return value; + } + + if (statName.startsWith('dex')) { + const myDex = getMetaNumber(context.general.meta, `dex${footmanType}`); + const isAttacker = (aux as any)?.isAttacker; + const opposeType = (aux as any)?.opposeType; + + if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) { + return (value as number) + myDex; + } + if (!isAttacker && statName === `dex${footmanType}`) { + return (value as number) + myDex; + } + } + return value; +} + +// 전투 특기: 보병 +export const traitModule: TraitModule = { + key: 'che_보병', + name: '보병', + info: '[군사] 보병 계통 징·모병비 -10%
[전투] 공격 시 아군 피해 -10%, 수비 시 아군 피해 -20%,
공격시 상대 병종에/수비시 자신 병종 숙련에 보병 숙련을 가산', + kind: 'war', + getName: () => '보병', + getInfo: () => + '[군사] 보병 계통 징·모병비 -10%
[전투] 공격 시 아군 피해 -10%, 수비 시 아군 피해 -20%,
공격시 상대 병종에/수비시 자신 병종 숙련에 보병 숙련을 가산', + onCalcDomestic: (_context, turnType, varType, value, aux) => { + if (turnType === '징병' || turnType === '모병') { + if (varType === 'cost' && aux && typeof aux === 'object' && 'armType' in aux) { + // Note: In a real scenario, we should check if aux.armType is footman. + // Since we don't have easy access to config here, we might need to assume legacy ID 1 or similar. + if ((aux as any).armType === 1) { + return value * 0.9; + } + } + } + return value; + }, + getWarPowerMultiplier: (_context, unit, _oppose) => { + if (unit.isAttacker()) { + return [1, 0.9]; + } + return [1, 0.8]; + }, + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_신산.ts b/packages/logic/src/triggers/special/war/che_신산.ts new file mode 100644 index 0000000..9cbae75 --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_신산.ts @@ -0,0 +1,42 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + if (statName === 'warMagicTrialProb') { + return (value as number) + 0.2; + } + if (statName === 'warMagicSuccessProb') { + return (value as number) + 0.2; + } + return value; +} + +export const traitModule: TraitModule = { + key: 'che_신산', + name: '신산', + info: '[계략] 화계·탈취·파괴·선동 : 성공률 +10%p
[전투] 계략 시도 확률 +20%p, 계략 성공 확률 +20%p', + kind: 'war', + getName: () => '신산', + getInfo: () => '[계략] 화계·탈취·파괴·선동 : 성공률 +10%p
[전투] 계략 시도 확률 +20%p, 계략 성공 확률 +20%p', + onCalcDomestic: (_context, turnType, varType, value, _aux) => { + if (turnType === '계략') { + if (varType === 'success') return value + 0.1; + } + return value; + }, + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_신중.ts b/packages/logic/src/triggers/special/war/che_신중.ts new file mode 100644 index 0000000..0df10aa --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_신중.ts @@ -0,0 +1,33 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + if (statName === 'warMagicSuccessProb') { + return (value as number) + 1; + } + return value; +} + +export const traitModule: TraitModule = { + key: 'che_신중', + name: '신중', + info: '[전투] 계략 성공 확률 100%', + kind: 'war', + getName: () => '신중', + getInfo: () => '[전투] 계략 성공 확률 100%', + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_위압.ts b/packages/logic/src/triggers/special/war/che_위압.ts new file mode 100644 index 0000000..49149ad --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_위압.ts @@ -0,0 +1,87 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; +import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js'; +import type { WarUnit } from '@sammo-ts/logic/war/units.js'; +import { LogFormat } from '@sammo-ts/logic/logging/types.js'; + +class che_위압시도 extends BaseWarUnitTrigger { + constructor(unit: WarUnit) { + super(unit, 10100); + } + + protected actionWar( + self: WarUnit, + oppose: WarUnit, + _selfEnv: Record, + _opposeEnv: Record + ): boolean { + if (self.getPhase() !== 0 && oppose.getPhase() !== 0) { + return true; + } + if (self.hasActivatedSkill('위압불가')) { + return true; + } + + self.activateSkill('위압'); + oppose.activateSkill('회피불가', '필살불가', '계략불가'); + return true; + } +} + +class che_위압발동 extends BaseWarUnitTrigger { + constructor(unit: WarUnit) { + super(unit, 40700); + } + + protected actionWar( + self: WarUnit, + oppose: WarUnit, + _selfEnv: Record, + _opposeEnv: Record + ): boolean { + if (!self.hasActivatedSkill('위압')) { + return true; + } + + oppose.getLogger().pushGeneralBattleDetailLog('상대에게 위압받았다!', LogFormat.PLAIN); + self.getLogger().pushGeneralBattleDetailLog('상대에게 위압을 줬다!', LogFormat.PLAIN); + oppose.setWarPowerMultiply(0); + if ('addAtmos' in oppose) { + (oppose as any).addAtmos(-5); + } + + return true; + } +} + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + return value; +} + +export const traitModule: TraitModule = { + key: 'che_위압', + name: '위압', + info: '[전투] 첫 페이즈 위압 발동(적 공격, 회피 불가, 사기 5 감소)', + kind: 'war', + getName: () => '위압', + getInfo: () => '[전투] 첫 페이즈 위압 발동(적 공격, 회피 불가, 사기 5 감소)', + getBattlePhaseTriggerList: (_context) => { + if (!_context.unit) return null; + return new WarTriggerCaller(new che_위압시도(_context.unit), new che_위압발동(_context.unit)); + }, + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_저격.ts b/packages/logic/src/triggers/special/war/che_저격.ts new file mode 100644 index 0000000..673039a --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_저격.ts @@ -0,0 +1,128 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; +import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js'; +import type { WarUnit } from '@sammo-ts/logic/war/units.js'; +import { LogFormat } from '@sammo-ts/logic/logging/types.js'; + +class che_저격시도 extends BaseWarUnitTrigger { + private readonly ratio: number; + private readonly woundMin: number; + private readonly woundMax: number; + private readonly addAtmos: number; + + constructor(unit: WarUnit, ratio: number, woundMin: number, woundMax: number, addAtmos = 20) { + super(unit, 20100); + this.ratio = ratio; + this.woundMin = woundMin; + this.woundMax = woundMax; + this.addAtmos = addAtmos; + } + + protected actionWar( + self: WarUnit, + oppose: WarUnit, + selfEnv: Record, + _opposeEnv: Record + ): boolean { + 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; + } +} + +class che_저격발동 extends BaseWarUnitTrigger { + constructor(unit: WarUnit) { + super(unit, 40100); + } + + protected actionWar( + self: WarUnit, + oppose: WarUnit, + selfEnv: Record, + _opposeEnv: Record + ): boolean { + if (!self.hasActivatedSkill('저격')) { + return true; + } + + if ((selfEnv['저격발동자'] as number ?? -1) !== this.raiseType) { + return true; + } + + if (selfEnv['저격발동'] as boolean ?? false) { + return true; + } + selfEnv['저격발동'] = true; + + if (oppose.constructor.name === 'WarUnitGeneral') { + self.getLogger().pushGeneralActionLog('상대를 저격했다!', LogFormat.PLAIN); + self.getLogger().pushGeneralBattleDetailLog('상대를 저격했다!', LogFormat.PLAIN); + oppose.getLogger().pushGeneralActionLog('상대에게 저격당했다!', LogFormat.PLAIN); + oppose.getLogger().pushGeneralBattleDetailLog('상대에게 저격당했다!', LogFormat.PLAIN); + } else { + self.getLogger().pushGeneralActionLog('성벽 수비대장을 저격했다!', LogFormat.PLAIN); + self.getLogger().pushGeneralBattleDetailLog('성벽 수비대장을 저격했다!', LogFormat.PLAIN); + } + + if ('addAtmos' in self) { + (self as any).addAtmos(selfEnv['addAtmos'] as number); + } + + return true; + } +} + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + return value; +} + +export const traitModule: TraitModule = { + key: 'che_저격', + name: '저격', + info: '[전투] 새로운 상대와 전투 시 50% 확률로 저격 발동, 성공 시 사기+20', + kind: 'war', + getName: () => '저격', + getInfo: () => '[전투] 새로운 상대와 전투 시 50% 확률로 저격 발동, 성공 시 사기+20', + getBattlePhaseTriggerList: (_context) => { + if (!_context.unit) return null; + return new WarTriggerCaller( + new che_저격시도(_context.unit, 0.5, 20, 40, 20), + new che_저격발동(_context.unit) + ); + }, + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_집중.ts b/packages/logic/src/triggers/special/war/che_집중.ts new file mode 100644 index 0000000..beaf9a7 --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_집중.ts @@ -0,0 +1,33 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + if (statName === 'warMagicSuccessDamage') { + return (value as number) * 1.5; + } + return value; +} + +export const traitModule: TraitModule = { + key: 'che_집중', + name: '집중', + info: '[전투] 계략 성공 시 대미지 +50%', + kind: 'war', + getName: () => '집중', + getInfo: () => '[전투] 계략 성공 시 대미지 +50%', + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_척사.ts b/packages/logic/src/triggers/special/war/che_척사.ts new file mode 100644 index 0000000..e620df1 --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_척사.ts @@ -0,0 +1,37 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + return value; +} + +export const traitModule: TraitModule = { + key: 'che_척사', + name: '척사', + info: '[전투] 지역·도시 병종 상대로 대미지 +20%, 아군 피해 -20%', + kind: 'war', + getName: () => '척사', + getInfo: () => '[전투] 지역·도시 병종 상대로 대미지 +20%, 아군 피해 -20%', + getWarPowerMultiplier: (_context, _unit, oppose) => { + const opposeCrewType = oppose.getCrewType(); + if (opposeCrewType.reqCities() || opposeCrewType.reqRegions()) { + return [1.2, 0.8]; + } + return [1, 1]; + }, + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_필살.ts b/packages/logic/src/triggers/special/war/che_필살.ts new file mode 100644 index 0000000..7c0a0af --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_필살.ts @@ -0,0 +1,63 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; +import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js'; +import type { WarUnit } from '@sammo-ts/logic/war/units.js'; + +class che_필살강화_회피불가 extends BaseWarUnitTrigger { + constructor(unit: WarUnit) { + super(unit, 20150); + } + + protected actionWar( + self: WarUnit, + oppose: WarUnit, + _selfEnv: Record, + _opposeEnv: Record + ): boolean { + if (!self.hasActivatedSkill('필살')) { + return true; + } + + oppose.activateSkill('회피불가'); + return true; + } +} + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + if (statName === 'warCriticalRatio') { + return (value as number) + 0.3; + } + if (statName === 'criticalDamageRange') { + const [rangeMin, rangeMax] = value as [number, number]; + return [(rangeMin + rangeMax) / 2, rangeMax]; + } + return value; +} + +export const traitModule: TraitModule = { + key: 'che_필살', + name: '필살', + info: '[전투] 필살 확률 +30%p, 필살 발동시 대상 회피 불가, 필살 계수 향상', + kind: 'war', + getName: () => '필살', + getInfo: () => '[전투] 필살 확률 +30%p, 필살 발동시 대상 회피 불가, 필살 계수 향상', + getBattlePhaseTriggerList: (_context) => { + if (!_context.unit) return null; + return new WarTriggerCaller(new che_필살강화_회피불가(_context.unit)); + }, + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/che_환술.ts b/packages/logic/src/triggers/special/war/che_환술.ts new file mode 100644 index 0000000..1339106 --- /dev/null +++ b/packages/logic/src/triggers/special/war/che_환술.ts @@ -0,0 +1,36 @@ +import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js'; +import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js'; +import type { WarActionContext } from '@sammo-ts/logic/war/actions.js'; +import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js'; + +function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number; +function onCalcStat( + context: WarActionContext, + statName: WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number]; +function onCalcStat( + context: GeneralActionContext | WarActionContext, + statName: GeneralStatName | WarStatName, + value: number | [number, number], + aux?: unknown +): number | [number, number] { + if (statName === 'warMagicSuccessProb') { + return (value as number) + 0.1; + } + if (statName === 'warMagicSuccessDamage') { + return (value as number) * 1.3; + } + return value; +} + +export const traitModule: TraitModule = { + key: 'che_환술', + name: '환술', + info: '[전투] 계략 성공 확률 +10%p, 계략 성공 시 대미지 +30%', + kind: 'war', + getName: () => '환술', + getInfo: () => '[전투] 계략 성공 확률 +10%p, 계략 성공 시 대미지 +30%', + onCalcStat, +}; diff --git a/packages/logic/src/triggers/special/war/index.ts b/packages/logic/src/triggers/special/war/index.ts index b2fca5b..a8d2243 100644 --- a/packages/logic/src/triggers/special/war/index.ts +++ b/packages/logic/src/triggers/special/war/index.ts @@ -1,6 +1,6 @@ import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/triggers/special/types.js'; -export const WAR_TRAIT_KEYS = ['che_의술', 'che_징병'] as const; +export const WAR_TRAIT_KEYS = ['che_의술', 'che_징병', 'che_보병', 'che_궁병', 'che_기병', 'che_공성'] as const; export type WarTraitKey = (typeof WAR_TRAIT_KEYS)[number]; @@ -11,6 +11,10 @@ export type WarTraitImporter = () => Promise; const defaultImporters: Record = { che_의술: async () => import('./che_의술.js'), che_징병: async () => import('./che_징병.js'), + che_보병: async () => import('./che_보병.js'), + che_궁병: async () => import('./che_궁병.js'), + che_기병: async () => import('./che_기병.js'), + che_공성: async () => import('./che_공성.js'), }; export const isWarTraitKey = (value: string): value is WarTraitKey => WAR_TRAIT_KEYS.includes(value as WarTraitKey); diff --git a/packages/logic/src/triggers/types.ts b/packages/logic/src/triggers/types.ts index 5b52675..ee45cf6 100644 --- a/packages/logic/src/triggers/types.ts +++ b/packages/logic/src/triggers/types.ts @@ -4,6 +4,10 @@ export type TriggerActionPhase = '판매' | '구매'; export type TriggerDomesticActionType = | '상업' + | '농업' + | '성벽' + | '수비' + | '치안' | '인재탐색' | '징병' | '징집인구' diff --git a/packages/logic/src/war/crewType.ts b/packages/logic/src/war/crewType.ts index c7a76af..566f673 100644 --- a/packages/logic/src/war/crewType.ts +++ b/packages/logic/src/war/crewType.ts @@ -36,6 +36,14 @@ export class WarCrewType { return this.definition.rice; } + public reqCities(): boolean { + return this.definition.requirements.some((req) => req.type === 'ReqCities'); + } + + public reqRegions(): boolean { + return this.definition.requirements.some((req) => req.type === 'ReqRegions'); + } + get initSkillTrigger(): string[] { return this.definition.initSkillTrigger ?? []; } diff --git a/packages/logic/src/war/units.ts b/packages/logic/src/war/units.ts index 943765e..6e4e128 100644 --- a/packages/logic/src/war/units.ts +++ b/packages/logic/src/war/units.ts @@ -118,6 +118,10 @@ export abstract class WarUnit