feat: 추가된 전투 및 내정 특기 초기화, 단련, 숙련 전환 및 관광 명령

This commit is contained in:
2026-01-04 06:58:43 +00:00
parent 436a55297e
commit 4fe4af868a
7 changed files with 837 additions and 0 deletions
@@ -0,0 +1,269 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
} from '@sammo-ts/logic/constraints/types.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 type { GeneralTurnCommandSpec } from './index.js';
import { increaseMetaNumber } from '@sammo-ts/logic/war/utils.js';
export interface SightseeingArgs {}
const ACTION_NAME = '견문';
const IncExp = 0x1;
const IncHeavyExp = 0x2;
const IncLeadership = 0x10;
const IncStrength = 0x20;
const IncIntel = 0x40;
const IncGold = 0x100;
const IncRice = 0x200;
const DecGold = 0x400;
const DecRice = 0x800;
const Wounded = 0x1000;
const HeavyWounded = 0x2000;
const SIGHTSEEING_MESSAGES: Array<{
flags: number;
texts: string[];
weight: number;
}> = [
{
flags: IncExp,
texts: [
'아무일도 일어나지 않았습니다.',
'명사와 설전을 벌였으나 망신만 당했습니다.',
'동네 장사와 힘겨루기를 했지만 망신만 당했습니다.',
],
weight: 1,
},
{
flags: IncHeavyExp,
texts: [
'주점에서 사람들과 어울려 술을 마셨습니다.',
'위기에 빠진 사람을 구해주었습니다.',
],
weight: 1,
},
{
flags: IncHeavyExp | IncLeadership,
texts: [
'백성들에게 현인의 가르침을 설파했습니다.',
'어느 집의 도망친 가축을 되찾아 주었습니다.',
],
weight: 2,
},
{
flags: IncHeavyExp | IncStrength,
texts: [
'동네 장사와 힘겨루기를 하여 멋지게 이겼습니다.',
'어느 집의 무너진 울타리를 고쳐주었습니다.',
],
weight: 2,
},
{
flags: IncHeavyExp | IncIntel,
texts: [
'어느 명사와 설전을 벌여 멋지게 이겼습니다.',
'거리에서 글 모르는 아이들을 모아 글을 가르쳤습니다.',
],
weight: 2,
},
{
flags: IncExp | IncGold,
texts: ['지나가는 행인에게서 금을 :goldAmount: 받았습니다.'],
weight: 1,
},
{
flags: IncExp | IncRice,
texts: ['지나가는 행인에게서 쌀을 :riceAmount: 받았습니다.'],
weight: 1,
},
{
flags: IncExp | DecGold,
texts: [
'산적을 만나 금 :goldAmount:을 빼앗겼습니다.',
'돈을 :goldAmount: 빌려주었다가 떼어먹혔습니다.',
],
weight: 1,
},
{
flags: IncExp | DecRice,
texts: ['쌀을 :riceAmount: 빌려주었다가 떼어먹혔습니다.'],
weight: 1,
},
{
flags: IncExp | Wounded,
texts: ['호랑이에게 물려 다쳤습니다.', '곰에게 할퀴어 다쳤습니다.'],
weight: 1,
},
{
flags: IncHeavyExp | Wounded,
texts: ['위기에 빠진 사람을 구해주다가 다쳤습니다.'],
weight: 1,
},
{
flags: IncExp | HeavyWounded,
texts: ['호랑이에게 물려 크게 다쳤습니다.', '곰에게 할퀴어 크게 다쳤습니다.'],
weight: 1,
},
{
flags: IncHeavyExp | Wounded | HeavyWounded,
texts: ['위기에 빠진 사람을 구하다가 죽을뻔 했습니다.'],
weight: 1,
},
{
flags: IncHeavyExp | IncStrength | IncGold,
texts: ['산적과 싸워 금 :goldAmount:을 빼앗았습니다.'],
weight: 1,
},
{
flags: IncHeavyExp | IncStrength | IncRice,
texts: [
'호랑이를 잡아 고기 :riceAmount:을 얻었습니다.',
'곰을 잡아 고기 :riceAmount:을 얻었습니다.',
],
weight: 1,
},
{
flags: IncHeavyExp | IncIntel | IncGold,
texts: ['돈을 빌려주었다가 이자 :goldAmount:을 받았습니다.'],
weight: 1,
},
{
flags: IncHeavyExp | IncIntel | IncRice,
texts: ['쌀을 빌려주었다가 이자 :riceAmount:을 받았습니다.'],
weight: 1,
},
];
const pickByWeight = (
rng: GeneralActionResolveContext['rng']
): { flags: number; text: string } => {
if (SIGHTSEEING_MESSAGES.length === 0) {
return { flags: 0, text: '' };
}
const total = SIGHTSEEING_MESSAGES.reduce(
(sum, entry) => sum + Math.max(entry.weight, 0),
0
);
const base = SIGHTSEEING_MESSAGES[0];
if (!base) {
return { flags: 0, text: '' };
}
if (total <= 0) {
const text = base.texts[0] ?? '';
return { flags: base.flags, text };
}
let cursor = rng.nextFloat() * total;
for (const entry of SIGHTSEEING_MESSAGES) {
const weight = Math.max(entry.weight, 0);
cursor -= weight;
if (cursor <= 0) {
const index = rng.nextInt(0, entry.texts.length);
const text = entry.texts[index] ?? entry.texts[0] ?? '';
return { flags: entry.flags, text };
}
}
const fallback = SIGHTSEEING_MESSAGES[SIGHTSEEING_MESSAGES.length - 1];
if (!fallback) {
return { flags: 0, text: '' };
}
const index = rng.nextInt(0, fallback.texts.length);
const text = fallback.texts[index] ?? fallback.texts[0] ?? '';
return { flags: fallback.flags, text };
};
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<TriggerState, SightseeingArgs> {
public readonly key = 'che_견문';
public readonly name = ACTION_NAME;
parseArgs(_raw: unknown): SightseeingArgs | null {
void _raw;
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: SightseeingArgs
): Constraint[] {
return [];
}
resolve(
context: GeneralActionResolveContext<TriggerState>,
_args: SightseeingArgs
): GeneralActionOutcome<TriggerState> {
const general = context.general;
const picked = pickByWeight(context.rng);
let message = picked.text;
let exp = 0;
if (picked.flags & IncExp) {
exp += 30;
}
if (picked.flags & IncHeavyExp) {
exp += 60;
}
if (picked.flags & IncLeadership) {
increaseMetaNumber(general.meta, 'leadership_exp', 2);
}
if (picked.flags & IncStrength) {
increaseMetaNumber(general.meta, 'strength_exp', 2);
}
if (picked.flags & IncIntel) {
increaseMetaNumber(general.meta, 'intel_exp', 2);
}
if (picked.flags & IncGold) {
general.gold += 300;
message = message.replace(':goldAmount:', '300');
}
if (picked.flags & IncRice) {
general.rice += 300;
message = message.replace(':riceAmount:', '300');
}
if (picked.flags & DecGold) {
general.gold = Math.max(0, general.gold - 200);
message = message.replace(':goldAmount:', '200');
}
if (picked.flags & DecRice) {
general.rice = Math.max(0, general.rice - 200);
message = message.replace(':riceAmount:', '200');
}
if (picked.flags & Wounded) {
const delta = context.rng.nextInt(10, 21);
general.injury = Math.min(80, general.injury + delta);
}
if (picked.flags & HeavyWounded) {
const delta = context.rng.nextInt(20, 51);
general.injury = Math.min(80, general.injury + delta);
}
general.experience += exp;
context.addLog(message);
return { effects: [] };
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_견문',
category: '개인',
reqArg: false,
args: {},
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
};
@@ -0,0 +1,81 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
} from '@sammo-ts/logic/constraints/types.js';
import { allow, unknownOrDeny, readGeneral } from '@sammo-ts/logic/constraints/helpers.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 type { GeneralTurnCommandSpec } from './index.js';
import { setMetaNumber } from '@sammo-ts/logic/war/utils.js';
export interface ResetSpecialDomesticArgs {}
const ACTION_NAME = '내정 특기 초기화';
const hasSpecial = (value: string | null | undefined): boolean =>
value !== null && value !== undefined && value !== 'None';
const reqDomesticSpecial = (): Constraint => ({
name: 'ReqGeneralDomesticSpecial',
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
test: (ctx, view) => {
const general = readGeneral(ctx, view);
if (!general) {
const req = { kind: 'general', id: ctx.actorId } as const;
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
}
if (hasSpecial(general.role.specialDomestic)) {
return allow();
}
return { kind: 'deny', reason: '특기가 없습니다.' };
},
});
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<TriggerState, ResetSpecialDomesticArgs> {
public readonly key = 'che_내정특기초기화';
public readonly name = ACTION_NAME;
parseArgs(_raw: unknown): ResetSpecialDomesticArgs | null {
void _raw;
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: ResetSpecialDomesticArgs
): Constraint[] {
return [reqDomesticSpecial()];
}
resolve(
context: GeneralActionResolveContext<TriggerState>,
_args: ResetSpecialDomesticArgs
): GeneralActionOutcome<TriggerState> {
const general = context.general;
general.role.specialDomestic = null;
setMetaNumber(general.meta, 'specAge', general.age + 1);
context.addLog('새로운 내정 특기를 가질 준비가 되었습니다.');
return { effects: [] };
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_내정특기초기화',
category: '개인',
reqArg: false,
args: {},
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
};
@@ -0,0 +1,228 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
StateView,
} from '@sammo-ts/logic/constraints/types.js';
import {
notBeNeutral,
reqGeneralCrew,
reqGeneralGold,
reqGeneralRice,
} from '@sammo-ts/logic/constraints/presets.js';
import { allow, unknownOrDeny, readGeneral } from '@sammo-ts/logic/constraints/helpers.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type {
ActionContextBuilder,
} from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
import { JosaUtil } from '@sammo-ts/common';
import { getMetaNumber, setMetaNumber, increaseMetaNumber } from '@sammo-ts/logic/war/utils.js';
export interface DrillArgs {}
export interface DrillContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> extends GeneralActionResolveContext<TriggerState> {
unitSet?: UnitSetDefinition | null;
}
export interface DrillEnvironment {
develCost?: number;
defaultTrainLow?: number;
defaultAtmosLow?: number;
}
type DrillPick = 'success' | 'normal' | 'fail';
const ACTION_NAME = '단련';
const resolveArmTypeName = (
unitSet: UnitSetDefinition,
armType: number
): string =>
unitSet.armTypes?.[String(armType)] ?? `병종${armType}`;
const pickByWeight = <T extends string>(
rng: DrillContext['rng'],
weights: Record<T, number>
): T => {
const entries = Object.entries(weights) as Array<[T, number]>;
const first = entries[0];
if (!first) {
throw new Error('Empty weights');
}
let total = 0;
for (const [, weight] of entries) {
if (weight > 0) {
total += weight;
}
}
if (total <= 0) {
return first[0];
}
let cursor = rng.nextFloat() * total;
for (const [key, weight] of entries) {
if (weight <= 0) {
continue;
}
cursor -= weight;
if (cursor <= 0) {
return key;
}
}
const last = entries[entries.length - 1];
return last ? last[0] : first[0];
};
const reqGeneralStat = (
key: 'train' | 'atmos',
label: string,
minValue: number
): Constraint => ({
name: `ReqGeneral${label}`,
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
test: (ctx, view) => {
const general = readGeneral(ctx, view);
if (!general) {
const req = { kind: 'general', id: ctx.actorId } as const;
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
}
if (general[key] >= minValue) {
return allow();
}
const josa = JosaUtil.pick(label, '이');
return { kind: 'deny', reason: `${label}${josa} 부족합니다.` };
},
});
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
DrillArgs,
DrillContext<TriggerState>
> {
public readonly key = 'che_단련';
public readonly name = ACTION_NAME;
private readonly env: DrillEnvironment;
constructor(env: DrillEnvironment = {}) {
this.env = env;
}
parseArgs(_raw: unknown): DrillArgs | null {
void _raw;
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: DrillArgs
): Constraint[] {
const trainLow = this.env.defaultTrainLow ?? 40;
const atmosLow = this.env.defaultAtmosLow ?? 40;
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
this.env.develCost ?? 0;
const getRequiredRice = (_context: ConstraintContext, _view: StateView): number =>
this.env.develCost ?? 0;
return [
notBeNeutral(),
reqGeneralCrew(),
reqGeneralStat('train', '훈련', trainLow),
reqGeneralStat('atmos', '사기', atmosLow),
reqGeneralGold(getRequiredGold),
reqGeneralRice(getRequiredRice),
];
}
resolve(
context: DrillContext<TriggerState>,
_args: DrillArgs
): GeneralActionOutcome<TriggerState> {
if (!context.unitSet) {
context.addLog('병종 정보를 확인할 수 없어 단련을 진행할 수 없습니다.');
return { effects: [] };
}
const general = context.general;
const crewType = context.unitSet.crewTypes?.find(
(entry) => entry.id === general.crewTypeId
);
if (!crewType) {
context.addLog('병종 정보를 확인할 수 없어 단련을 진행할 수 없습니다.');
return { effects: [] };
}
const pick = pickByWeight<DrillPick>(context.rng, {
success: 0.34,
normal: 0.33,
fail: 0.33,
});
const multiplier = pick === 'success' ? 3 : pick === 'normal' ? 2 : 1;
const baseScore = Math.round(
(general.crew * general.train * general.atmos) / 20 / 10000
);
const score = baseScore * multiplier;
const armTypeName = resolveArmTypeName(
context.unitSet,
crewType.armType
);
const logPrefix =
pick === 'success'
? '단련이 일취월장하여'
: pick === 'fail'
? '단련이 지지부진하여'
: '';
const logText = logPrefix
? `${logPrefix} ${armTypeName} 숙련도가 ${score} 향상되었습니다.`
: `${armTypeName} 숙련도가 ${score} 향상되었습니다.`;
const dexKey = `dex${crewType.armType}`;
const nextDex = getMetaNumber(general.meta, dexKey, 0) + score;
setMetaNumber(general.meta, dexKey, nextDex);
const expGain = general.crew / 400;
general.experience += expGain;
const statKey = pickByWeight(context.rng, {
leadership_exp: general.stats.leadership,
strength_exp: general.stats.strength,
intel_exp: general.stats.intelligence,
});
increaseMetaNumber(general.meta, statKey, 1);
const cost = this.env.develCost ?? 0;
general.gold = Math.max(0, general.gold - cost);
general.rice = Math.max(0, general.rice - cost);
context.addLog(logText);
return { effects: [] };
}
}
export const actionContextBuilder: ActionContextBuilder = (base, options) => ({
...base,
unitSet: options.unitSet ?? null,
});
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_단련',
category: '군사',
reqArg: false,
args: {},
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition({
develCost: env.develCost,
}),
};
@@ -0,0 +1,163 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
StateView,
} from '@sammo-ts/logic/constraints/types.js';
import {
notBeNeutral,
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 type {
ActionContextBuilder,
} from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
import { JosaUtil } from '@sammo-ts/common';
import { getMetaNumber, setMetaNumber, increaseMetaNumber } from '@sammo-ts/logic/war/utils.js';
export interface DexTransferArgs {
srcArmType: number;
destArmType: number;
}
export interface DexTransferContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> extends GeneralActionResolveContext<TriggerState> {
unitSet?: UnitSetDefinition | null;
}
export interface DexTransferEnvironment {
develCost?: number;
}
const ACTION_NAME = '숙련전환';
const DECREASE_COEFF = 0.4;
const CONVERT_COEFF = 0.9;
const isRecord = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === 'object' && !Array.isArray(value);
const resolveArmType = (value: unknown): number | null => {
if (typeof value !== 'number' || !Number.isInteger(value)) {
return null;
}
return value > 0 ? value : null;
};
const resolveArmTypeName = (
unitSet: UnitSetDefinition | null | undefined,
armType: number
): string =>
unitSet?.armTypes?.[String(armType)] ?? `병종${armType}`;
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
DexTransferArgs,
DexTransferContext<TriggerState>
> {
public readonly key = 'che_숙련전환';
public readonly name = ACTION_NAME;
private readonly env: DexTransferEnvironment;
constructor(env: DexTransferEnvironment = {}) {
this.env = env;
}
parseArgs(raw: unknown): DexTransferArgs | null {
const data = isRecord(raw) ? raw : {};
const srcArmType = resolveArmType(data.srcArmType);
const destArmType = resolveArmType(data.destArmType);
if (srcArmType === null || destArmType === null) {
return null;
}
if (srcArmType === destArmType) {
return null;
}
return { srcArmType, destArmType };
}
buildConstraints(
_ctx: ConstraintContext,
_args: DexTransferArgs
): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
this.env.develCost ?? 0;
const getRequiredRice = (_context: ConstraintContext, _view: StateView): number =>
this.env.develCost ?? 0;
return [
notBeNeutral(),
occupiedCity(),
reqGeneralGold(getRequiredGold),
reqGeneralRice(getRequiredRice),
];
}
resolve(
context: DexTransferContext<TriggerState>,
args: DexTransferArgs
): GeneralActionOutcome<TriggerState> {
const general = context.general;
const srcKey = `dex${args.srcArmType}`;
const destKey = `dex${args.destArmType}`;
const srcDex = getMetaNumber(general.meta, srcKey, 0);
const cutDex = Math.trunc(srcDex * DECREASE_COEFF);
const addDex = Math.trunc(cutDex * CONVERT_COEFF);
setMetaNumber(general.meta, srcKey, srcDex - cutDex);
setMetaNumber(
general.meta,
destKey,
getMetaNumber(general.meta, destKey, 0) + addDex
);
const srcName = resolveArmTypeName(
context.unitSet,
args.srcArmType
);
const destName = resolveArmTypeName(
context.unitSet,
args.destArmType
);
const cutJosa = JosaUtil.pick(String(cutDex), '을');
const addJosa = JosaUtil.pick(String(addDex), '으로');
const cost = this.env.develCost ?? 0;
general.gold = Math.max(0, general.gold - cost);
general.rice = Math.max(0, general.rice - cost);
general.experience += 10;
increaseMetaNumber(general.meta, 'leadership_exp', 2);
context.addLog(
`${srcName} 숙련 ${cutDex}${cutJosa} ${destName} 숙련 ${addDex}${addJosa} 전환했습니다.`
);
return { effects: [] };
}
}
export const actionContextBuilder: ActionContextBuilder = (base, options) => ({
...base,
unitSet: options.unitSet ?? null,
});
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_숙련전환',
category: '군사',
reqArg: true,
args: { srcArmType: 0, destArmType: 0 },
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition({ develCost: env.develCost }),
};
@@ -0,0 +1,81 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
} from '@sammo-ts/logic/constraints/types.js';
import { allow, unknownOrDeny, readGeneral } from '@sammo-ts/logic/constraints/helpers.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 type { GeneralTurnCommandSpec } from './index.js';
import { setMetaNumber } from '@sammo-ts/logic/war/utils.js';
export interface ResetSpecialWarArgs {}
const ACTION_NAME = '전투 특기 초기화';
const hasSpecial = (value: string | null | undefined): boolean =>
value !== null && value !== undefined && value !== 'None';
const reqWarSpecial = (): Constraint => ({
name: 'ReqGeneralWarSpecial',
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
test: (ctx, view) => {
const general = readGeneral(ctx, view);
if (!general) {
const req = { kind: 'general', id: ctx.actorId } as const;
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
}
if (hasSpecial(general.role.specialWar)) {
return allow();
}
return { kind: 'deny', reason: '특기가 없습니다.' };
},
});
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<TriggerState, ResetSpecialWarArgs> {
public readonly key = 'che_전투특기초기화';
public readonly name = ACTION_NAME;
parseArgs(_raw: unknown): ResetSpecialWarArgs | null {
void _raw;
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: ResetSpecialWarArgs
): Constraint[] {
return [reqWarSpecial()];
}
resolve(
context: GeneralActionResolveContext<TriggerState>,
_args: ResetSpecialWarArgs
): GeneralActionOutcome<TriggerState> {
const general = context.general;
general.role.specialWar = null;
setMetaNumber(general.meta, 'specAge2', general.age + 1);
context.addLog('새로운 전투 특기를 가질 준비가 되었습니다.');
return { effects: [] };
}
}
// 예약 턴 실행은 기본 컨텍스트만 사용한다.
export const actionContextBuilder = defaultActionContextBuilder;
export const commandSpec: GeneralTurnCommandSpec = {
key: 'che_전투특기초기화',
category: '개인',
reqArg: false,
args: {},
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
};
@@ -5,8 +5,13 @@ export const GENERAL_TURN_COMMAND_KEYS = [
'che_임관',
'che_건국',
'che_훈련',
'che_단련',
'che_숙련전환',
'che_사기진작',
'che_요양',
'che_견문',
'che_내정특기초기화',
'che_전투특기초기화',
'che_출병',
'che_주민선정',
'che_농지개간',
@@ -40,8 +45,13 @@ const defaultImporters: Record<
che_임관: async () => import('./che_임관.js'),
che_건국: async () => import('./che_건국.js'),
che_훈련: async () => import('./che_훈련.js'),
che_단련: async () => import('./che_단련.js'),
che_숙련전환: async () => import('./che_숙련전환.js'),
che_사기진작: async () => import('./che_사기진작.js'),
che_요양: async () => import('./che_요양.js'),
che_견문: async () => import('./che_견문.js'),
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'),
+5
View File
@@ -4,8 +4,13 @@
"che_임관",
"che_건국",
"che_훈련",
"che_단련",
"che_숙련전환",
"che_사기진작",
"che_요양",
"che_견문",
"che_내정특기초기화",
"che_전투특기초기화",
"che_출병",
"che_주민선정",
"che_농지개간",