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'),
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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<ItemKey, ItemImporter> = {
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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<TraitModuleExport>;
|
||||
const defaultImporters: Record<DomesticTraitKey, DomesticTraitImporter> = {
|
||||
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 =>
|
||||
|
||||
@@ -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<string, unknown>,
|
||||
_opposeEnv: Record<string, unknown>
|
||||
): 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<string, unknown>,
|
||||
_opposeEnv: Record<string, unknown>
|
||||
): boolean {
|
||||
if (!self.hasActivatedSkill('격노')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const targetAct = oppose.hasActivatedSkill('필살') ? '필살 공격' : '회피 시도';
|
||||
const is진노 = self.hasActivatedSkill('진노');
|
||||
const reaction = is진노 ? '진노' : '격노';
|
||||
|
||||
self.getLogger().pushGeneralBattleDetailLog(`상대의 ${targetAct}에 <C>${reaction}</>했다!</>`, LogFormat.PLAIN);
|
||||
oppose.getLogger().pushGeneralBattleDetailLog(`${targetAct}에 상대가 <R>${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,
|
||||
};
|
||||
@@ -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<string, unknown>,
|
||||
_opposeEnv: Record<string, unknown>
|
||||
): 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,
|
||||
};
|
||||
@@ -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%<br>[전투] 성벽 공격 시 대미지 +100%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 차병 숙련을 가산',
|
||||
kind: 'war',
|
||||
getName: () => '공성',
|
||||
getInfo: () =>
|
||||
'[군사] 차병 계통 징·모병비 -10%<br>[전투] 성벽 공격 시 대미지 +100%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 차병 숙련을 가산',
|
||||
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,
|
||||
};
|
||||
@@ -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%<br>[전투] 회피 확률 +20%p,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 궁병 숙련을 가산',
|
||||
kind: 'war',
|
||||
getName: () => '궁병',
|
||||
getInfo: () =>
|
||||
'[군사] 궁병 계통 징·모병비 -10%<br>[전투] 회피 확률 +20%p,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 궁병 숙련을 가산',
|
||||
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,
|
||||
};
|
||||
@@ -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%<br>[전투] 수비 시 대미지 +10%, 공격 시 대미지 +20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 기병 숙련을 가산',
|
||||
kind: 'war',
|
||||
getName: () => '기병',
|
||||
getInfo: () =>
|
||||
'[군사] 기병 계통 징·모병비 -10%<br>[전투] 수비 시 대미지 +10%, 공격 시 대미지 +20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 기병 숙련을 가산',
|
||||
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,
|
||||
};
|
||||
@@ -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<string, unknown>,
|
||||
_opposeEnv: Record<string, unknown>
|
||||
): 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,
|
||||
};
|
||||
@@ -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, <br>승리 수의 로그 비례로 대미지 상승(10회 ⇒ +5%, 40회 ⇒ +15%)<br>승리 수의 로그 비례로 피해 감소(10회 ⇒ -2%, 40회 ⇒ -6%)',
|
||||
kind: 'war',
|
||||
getName: () => '무쌍',
|
||||
getInfo: () =>
|
||||
'[전투] 대미지 +5%, 피해 -2%, 공격 시 필살 확률 +10%p, <br>승리 수의 로그 비례로 대미지 상승(10회 ⇒ +5%, 40회 ⇒ +15%)<br>승리 수의 로그 비례로 피해 감소(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,
|
||||
};
|
||||
@@ -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<string, unknown>,
|
||||
_opposeEnv: Record<string, unknown>
|
||||
): 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<string, unknown>,
|
||||
opposeEnv: Record<string, unknown>
|
||||
): 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(`<C>반계</>로 상대의 <D>${opposeMagic}</>을 되돌렸다!`, LogFormat.PLAIN);
|
||||
oppose.getLogger().pushGeneralBattleDetailLog(`<D>${opposeMagic}</>을 <R>역으로</> 당했다!`, 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,
|
||||
};
|
||||
@@ -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%<br>[전투] 공격 시 아군 피해 -10%, 수비 시 아군 피해 -20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 보병 숙련을 가산',
|
||||
kind: 'war',
|
||||
getName: () => '보병',
|
||||
getInfo: () =>
|
||||
'[군사] 보병 계통 징·모병비 -10%<br>[전투] 공격 시 아군 피해 -10%, 수비 시 아군 피해 -20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 보병 숙련을 가산',
|
||||
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,
|
||||
};
|
||||
@@ -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<br>[전투] 계략 시도 확률 +20%p, 계략 성공 확률 +20%p',
|
||||
kind: 'war',
|
||||
getName: () => '신산',
|
||||
getInfo: () => '[계략] 화계·탈취·파괴·선동 : 성공률 +10%p<br>[전투] 계략 시도 확률 +20%p, 계략 성공 확률 +20%p',
|
||||
onCalcDomestic: (_context, turnType, varType, value, _aux) => {
|
||||
if (turnType === '계략') {
|
||||
if (varType === 'success') return value + 0.1;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onCalcStat,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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<string, unknown>,
|
||||
_opposeEnv: Record<string, unknown>
|
||||
): 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<string, unknown>,
|
||||
_opposeEnv: Record<string, unknown>
|
||||
): boolean {
|
||||
if (!self.hasActivatedSkill('위압')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
oppose.getLogger().pushGeneralBattleDetailLog('상대에게 <R>위압</>받았다!', LogFormat.PLAIN);
|
||||
self.getLogger().pushGeneralBattleDetailLog('상대에게 <C>위압</>을 줬다!', 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,
|
||||
};
|
||||
@@ -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<string, unknown>,
|
||||
_opposeEnv: Record<string, unknown>
|
||||
): 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<string, unknown>,
|
||||
_opposeEnv: Record<string, unknown>
|
||||
): 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('상대를 <C>저격</>했다!', LogFormat.PLAIN);
|
||||
self.getLogger().pushGeneralBattleDetailLog('상대를 <C>저격</>했다!', LogFormat.PLAIN);
|
||||
oppose.getLogger().pushGeneralActionLog('상대에게 <R>저격</>당했다!', LogFormat.PLAIN);
|
||||
oppose.getLogger().pushGeneralBattleDetailLog('상대에게 <R>저격</>당했다!', LogFormat.PLAIN);
|
||||
} else {
|
||||
self.getLogger().pushGeneralActionLog('성벽 수비대장을 <C>저격</>했다!', LogFormat.PLAIN);
|
||||
self.getLogger().pushGeneralBattleDetailLog('성벽 수비대장을 <C>저격</>했다!', LogFormat.PLAIN);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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<string, unknown>,
|
||||
_opposeEnv: Record<string, unknown>
|
||||
): 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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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<TraitModuleExport>;
|
||||
const defaultImporters: Record<WarTraitKey, WarTraitImporter> = {
|
||||
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);
|
||||
|
||||
@@ -4,6 +4,10 @@ export type TriggerActionPhase = '판매' | '구매';
|
||||
|
||||
export type TriggerDomesticActionType =
|
||||
| '상업'
|
||||
| '농업'
|
||||
| '성벽'
|
||||
| '수비'
|
||||
| '치안'
|
||||
| '인재탐색'
|
||||
| '징병'
|
||||
| '징집인구'
|
||||
|
||||
@@ -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 ?? [];
|
||||
}
|
||||
|
||||
@@ -118,6 +118,10 @@ export abstract class WarUnit<TriggerState extends GeneralTriggerState = General
|
||||
return this.unitId;
|
||||
}
|
||||
|
||||
public getGameConfig(): WarEngineConfig {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
public getNationVar(key: string): TriggerValue | null {
|
||||
return resolveNationVar(this.nation, key);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user