feat: add new war traits for various unit types

- 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.
This commit is contained in:
2026-01-05 16:43:28 +00:00
parent cf93c28aff
commit 41339176ee
36 changed files with 1673 additions and 4 deletions
@@ -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<TriggerState, TradeArgs> {
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<TriggerState>, args: TradeArgs): GeneralActionOutcome<TriggerState> {
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(),
};
@@ -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<TriggerState> {
public override readonly key = 'che_모병';
public override readonly name = '모병';
constructor(modules: GeneralActionModule<TriggerState>[]) {
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 ?? []),
};
@@ -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<TriggerState, DisbandArgs> {
public readonly key = 'che_소집해제';
public readonly name = '소집해제';
parseArgs(_raw: unknown): DisbandArgs | null {
return {};
}
buildConstraints(_ctx: ConstraintContext, _args: DisbandArgs): Constraint[] {
return [reqGeneralCrew()];
}
resolve(
context: GeneralActionResolveContext<TriggerState>,
_args: DisbandArgs
): GeneralActionOutcome<TriggerState> {
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(),
};
@@ -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<TriggerState, SettlementArgs> {
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<TriggerState>,
_args: SettlementArgs
): GeneralActionOutcome<TriggerState> {
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 }),
};
@@ -388,8 +388,8 @@ export class ActionResolver<
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, RecruitArgs, RecruitResolveContext<TriggerState>> {
public readonly key = 'che_징병';
public readonly name = ACTION_NAME;
public readonly key: string = 'che_징병';
public readonly name: string = ACTION_NAME;
private readonly command: CommandResolver<TriggerState>;
private readonly resolver: ActionResolver<TriggerState>;
private readonly env: RecruitEnvironment;
@@ -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<GeneralTurnCommandKey, GeneralTurnCommandImporter
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'),
@@ -59,6 +64,9 @@ const defaultImporters: Record<GeneralTurnCommandKey, GeneralTurnCommandImporter
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'),
휴식: async () => import('./휴식.js'),
};