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:
@@ -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'),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user