feat: add 발령 (Assignment) and 포상 (Award) actions for generals, and implement 휴식 (Rest) action
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
TriggerValue,
|
||||
} from '../../../domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
} from '../../../constraints/types.js';
|
||||
import {
|
||||
alwaysFail,
|
||||
beChief,
|
||||
existsDestGeneral,
|
||||
friendlyDestGeneral,
|
||||
notBeNeutral,
|
||||
occupiedCity,
|
||||
occupiedDestCity,
|
||||
suppliedCity,
|
||||
suppliedDestCity,
|
||||
} from '../../../constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '../../definition.js';
|
||||
import type {
|
||||
GeneralActionEffect,
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '../../engine.js';
|
||||
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||
|
||||
export interface AssignmentArgs {
|
||||
destGeneralId: number;
|
||||
destCityId: number;
|
||||
}
|
||||
|
||||
export interface AssignmentResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destGeneral: General<TriggerState>;
|
||||
destCity: City;
|
||||
currentYear: number;
|
||||
currentMonth: number;
|
||||
turnTermMinutes?: number;
|
||||
generalTurnTime?: Date;
|
||||
destGeneralTurnTime?: Date;
|
||||
}
|
||||
|
||||
export interface AssignmentEnvironment {
|
||||
formatCityName?: (city: City) => string;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '발령';
|
||||
|
||||
const joinYearMonth = (year: number, month: number): number =>
|
||||
year * 12 + month - 1;
|
||||
|
||||
const cutTurn = (time: Date, turnTermMinutes: number): number => {
|
||||
const turnMs = turnTermMinutes * 60 * 1000;
|
||||
return Math.floor(time.getTime() / turnMs);
|
||||
};
|
||||
|
||||
const resolveLastAssignment = (
|
||||
context: AssignmentResolveContext
|
||||
): number => {
|
||||
let yearMonth = joinYearMonth(context.currentYear, context.currentMonth);
|
||||
const term = context.turnTermMinutes;
|
||||
const srcTime = context.generalTurnTime;
|
||||
const destTime = context.destGeneralTurnTime;
|
||||
if (term && srcTime && destTime) {
|
||||
if (cutTurn(srcTime, term) !== cutTurn(destTime, term)) {
|
||||
yearMonth += 1;
|
||||
}
|
||||
}
|
||||
return yearMonth;
|
||||
};
|
||||
|
||||
const addMetaValue = (
|
||||
meta: Record<string, TriggerValue>,
|
||||
key: string,
|
||||
value: TriggerValue
|
||||
): Record<string, TriggerValue> => ({
|
||||
...meta,
|
||||
[key]: value,
|
||||
});
|
||||
|
||||
// 발령 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
private readonly env: AssignmentEnvironment;
|
||||
|
||||
constructor(env: AssignmentEnvironment) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: AssignmentResolveContext<TriggerState>,
|
||||
_args: AssignmentArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
void _args;
|
||||
const destGeneral = context.destGeneral;
|
||||
const destCity = context.destCity;
|
||||
const cityName = this.env.formatCityName
|
||||
? this.env.formatCityName(destCity)
|
||||
: destCity.name;
|
||||
const yearMonth = resolveLastAssignment(context);
|
||||
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||
createGeneralPatchEffect(
|
||||
{
|
||||
cityId: destCity.id,
|
||||
meta: addMetaValue(destGeneral.meta, 'last발령', yearMonth),
|
||||
},
|
||||
destGeneral.id
|
||||
),
|
||||
];
|
||||
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`<Y>${context.general.name}</>에 의해 <G><b>${cityName}</b></>로 발령됐습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: destGeneral.id,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
)
|
||||
);
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`<Y>${destGeneral.name}</>을 <G><b>${cityName}</b></>로 발령했습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return { effects };
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
AssignmentArgs,
|
||||
AssignmentResolveContext<TriggerState>
|
||||
> {
|
||||
public readonly key = 'che_발령';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(env: AssignmentEnvironment) {
|
||||
this.resolver = new ActionResolver(env);
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): AssignmentArgs | null {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return null;
|
||||
}
|
||||
const data = raw as {
|
||||
destGeneralId?: unknown;
|
||||
destCityId?: unknown;
|
||||
};
|
||||
if (typeof data.destGeneralId !== 'number') {
|
||||
return null;
|
||||
}
|
||||
if (typeof data.destCityId !== 'number') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
destGeneralId: data.destGeneralId,
|
||||
destCityId: data.destCityId,
|
||||
};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
ctx: ConstraintContext,
|
||||
_args: AssignmentArgs
|
||||
): Constraint[] {
|
||||
void _args;
|
||||
if (ctx.destGeneralId === ctx.actorId) {
|
||||
return [alwaysFail('본인입니다')];
|
||||
}
|
||||
return [
|
||||
beChief(),
|
||||
notBeNeutral(),
|
||||
occupiedCity(),
|
||||
suppliedCity(),
|
||||
existsDestGeneral(),
|
||||
friendlyDestGeneral(),
|
||||
occupiedDestCity(),
|
||||
suppliedDestCity(),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: AssignmentResolveContext<TriggerState>,
|
||||
args: AssignmentArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import type {
|
||||
General,
|
||||
GeneralTriggerState,
|
||||
Nation,
|
||||
} from '../../../domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
RequirementKey,
|
||||
StateView,
|
||||
} from '../../../constraints/types.js';
|
||||
import {
|
||||
alwaysFail,
|
||||
beChief,
|
||||
existsDestGeneral,
|
||||
friendlyDestGeneral,
|
||||
notBeNeutral,
|
||||
occupiedCity,
|
||||
reqNationGold,
|
||||
reqNationRice,
|
||||
suppliedCity,
|
||||
} from '../../../constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '../../definition.js';
|
||||
import type {
|
||||
GeneralActionEffect,
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '../../engine.js';
|
||||
import {
|
||||
createGeneralPatchEffect,
|
||||
createLogEffect,
|
||||
createNationPatchEffect,
|
||||
} from '../../engine.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||
|
||||
export interface AwardArgs {
|
||||
isGold: boolean;
|
||||
amount: number;
|
||||
destGeneralId: number;
|
||||
}
|
||||
|
||||
export interface AwardResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
destGeneral: General<TriggerState>;
|
||||
}
|
||||
|
||||
export interface AwardEnvironment {
|
||||
baseGold: number;
|
||||
baseRice: number;
|
||||
minAmount?: number;
|
||||
maxAmount: number;
|
||||
amountUnit?: number;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '포상';
|
||||
const DEFAULT_MIN_AMOUNT = 100;
|
||||
const DEFAULT_AMOUNT_UNIT = 100;
|
||||
|
||||
const roundToUnit = (value: number, unit: number): number =>
|
||||
Math.round(value / unit) * unit;
|
||||
|
||||
const formatNumber = (value: number): string =>
|
||||
value.toLocaleString('en-US');
|
||||
|
||||
const clamp = (value: number, min: number, max: number): number =>
|
||||
Math.min(Math.max(value, min), max);
|
||||
|
||||
const normalizeAmount = (
|
||||
amount: number,
|
||||
env: AwardEnvironment
|
||||
): number => {
|
||||
const unit = env.amountUnit ?? DEFAULT_AMOUNT_UNIT;
|
||||
const min = env.minAmount ?? DEFAULT_MIN_AMOUNT;
|
||||
const max = env.maxAmount;
|
||||
return clamp(roundToUnit(amount, unit), min, max);
|
||||
};
|
||||
|
||||
const resolveNationResource = (
|
||||
nation: Nation,
|
||||
isGold: boolean
|
||||
): { current: number; base: number; key: 'gold' | 'rice'; label: string } => ({
|
||||
current: isGold ? nation.gold : nation.rice,
|
||||
base: isGold ? 0 : 0,
|
||||
key: isGold ? 'gold' : 'rice',
|
||||
label: isGold ? '금' : '쌀',
|
||||
});
|
||||
|
||||
// 포상 비용 및 유효 범위를 계산한다.
|
||||
export class CommandResolver {
|
||||
private readonly env: AwardEnvironment;
|
||||
|
||||
constructor(env: AwardEnvironment) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
getRequiredResource(isGold: boolean): number {
|
||||
return 1 + (isGold ? this.env.baseGold : this.env.baseRice);
|
||||
}
|
||||
|
||||
normalizeAmount(amount: number): number {
|
||||
return normalizeAmount(amount, this.env);
|
||||
}
|
||||
}
|
||||
|
||||
// 포상 결과를 계산한다.
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
private readonly env: AwardEnvironment;
|
||||
private readonly command: CommandResolver;
|
||||
|
||||
constructor(env: AwardEnvironment) {
|
||||
this.env = env;
|
||||
this.command = new CommandResolver(env);
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: AwardResolveContext<TriggerState>,
|
||||
args: AwardArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
const nation = context.nation;
|
||||
if (!nation) {
|
||||
return { effects: [] };
|
||||
}
|
||||
const { key, label } = resolveNationResource(nation, args.isGold);
|
||||
const base = args.isGold ? this.env.baseGold : this.env.baseRice;
|
||||
const available = Math.max(nation[key] - base, 0);
|
||||
const amount = clamp(
|
||||
this.command.normalizeAmount(args.amount),
|
||||
0,
|
||||
available
|
||||
);
|
||||
if (amount <= 0) {
|
||||
return { effects: [] };
|
||||
}
|
||||
|
||||
const amountText = formatNumber(amount);
|
||||
const effects: Array<GeneralActionEffect<TriggerState>> = [
|
||||
createGeneralPatchEffect(
|
||||
{ [key]: context.destGeneral[key] + amount } as Partial<
|
||||
General<TriggerState>
|
||||
>,
|
||||
context.destGeneral.id
|
||||
),
|
||||
createNationPatchEffect({
|
||||
[key]: nation[key] - amount,
|
||||
} as Partial<Nation>, nation.id),
|
||||
];
|
||||
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`${label} ${amountText} 포상으로 받았습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId: context.destGeneral.id,
|
||||
format: LogFormat.PLAIN,
|
||||
}
|
||||
)
|
||||
);
|
||||
effects.push(
|
||||
createLogEffect(
|
||||
`<Y>${context.destGeneral.name}</>에게 ${label} ${amountText} 수여했습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return { effects };
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
AwardArgs,
|
||||
AwardResolveContext<TriggerState>
|
||||
> {
|
||||
public readonly key = 'che_포상';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly command: CommandResolver;
|
||||
private readonly resolver: ActionResolver<TriggerState>;
|
||||
|
||||
constructor(env: AwardEnvironment) {
|
||||
this.command = new CommandResolver(env);
|
||||
this.resolver = new ActionResolver(env);
|
||||
}
|
||||
|
||||
parseArgs(raw: unknown): AwardArgs | null {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return null;
|
||||
}
|
||||
const data = raw as {
|
||||
isGold?: unknown;
|
||||
amount?: unknown;
|
||||
destGeneralId?: unknown;
|
||||
};
|
||||
if (typeof data.isGold !== 'boolean') {
|
||||
return null;
|
||||
}
|
||||
if (typeof data.amount !== 'number' || Number.isNaN(data.amount)) {
|
||||
return null;
|
||||
}
|
||||
if (typeof data.destGeneralId !== 'number') {
|
||||
return null;
|
||||
}
|
||||
const amount = this.command.normalizeAmount(data.amount);
|
||||
if (amount <= 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
isGold: data.isGold,
|
||||
amount,
|
||||
destGeneralId: data.destGeneralId,
|
||||
};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
ctx: ConstraintContext,
|
||||
args: AwardArgs
|
||||
): Constraint[] {
|
||||
const requirements: RequirementKey[] = [];
|
||||
if (ctx.cityId !== undefined) {
|
||||
requirements.push({ kind: 'city', id: ctx.cityId });
|
||||
}
|
||||
if (ctx.nationId !== undefined) {
|
||||
requirements.push({ kind: 'nation', id: ctx.nationId });
|
||||
}
|
||||
if (ctx.destGeneralId !== undefined) {
|
||||
requirements.push({ kind: 'destGeneral', id: ctx.destGeneralId });
|
||||
}
|
||||
|
||||
if (ctx.destGeneralId === ctx.actorId) {
|
||||
return [alwaysFail('본인입니다')];
|
||||
}
|
||||
|
||||
const getRequired = (_ctx: ConstraintContext, _view: StateView): number =>
|
||||
this.command.getRequiredResource(args.isGold);
|
||||
|
||||
const resourceConstraint = args.isGold
|
||||
? reqNationGold(getRequired, requirements)
|
||||
: reqNationRice(getRequired, requirements);
|
||||
|
||||
return [
|
||||
notBeNeutral(),
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
suppliedCity(),
|
||||
existsDestGeneral(),
|
||||
friendlyDestGeneral(),
|
||||
resourceConstraint,
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: AwardResolveContext<TriggerState>,
|
||||
args: AwardArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
@@ -1 +1,50 @@
|
||||
export {};
|
||||
export type NationTurnCommandKey = '휴식' | 'che_포상' | 'che_발령';
|
||||
|
||||
export type NationTurnCommandModule =
|
||||
| typeof import('./휴식.js')
|
||||
| typeof import('./che_포상.js')
|
||||
| typeof import('./che_발령.js');
|
||||
|
||||
export type NationTurnCommandImporter = () => Promise<NationTurnCommandModule>;
|
||||
|
||||
const defaultImporters: Record<
|
||||
NationTurnCommandKey,
|
||||
NationTurnCommandImporter
|
||||
> = {
|
||||
휴식: async () => import('./휴식.js'),
|
||||
che_포상: async () => import('./che_포상.js'),
|
||||
che_발령: async () => import('./che_발령.js'),
|
||||
};
|
||||
|
||||
export class NationTurnCommandLoader {
|
||||
constructor(
|
||||
private readonly importers: Record<
|
||||
NationTurnCommandKey,
|
||||
NationTurnCommandImporter
|
||||
> = defaultImporters
|
||||
) {}
|
||||
|
||||
async load(
|
||||
key: NationTurnCommandKey
|
||||
): Promise<NationTurnCommandModule> {
|
||||
const importer = this.importers[key];
|
||||
if (!importer) {
|
||||
throw new Error(`Unknown nation turn command key: ${key}`);
|
||||
}
|
||||
return importer();
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
ActionDefinition as NationRestActionDefinition,
|
||||
ActionResolver as NationRestActionResolver,
|
||||
} from './휴식.js';
|
||||
export {
|
||||
ActionDefinition as AwardActionDefinition,
|
||||
ActionResolver as AwardActionResolver,
|
||||
CommandResolver as AwardCommandResolver,
|
||||
} from './che_포상.js';
|
||||
export {
|
||||
ActionDefinition as AssignmentActionDefinition,
|
||||
ActionResolver as AssignmentActionResolver,
|
||||
} from './che_발령.js';
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type {
|
||||
GeneralTriggerState,
|
||||
} from '../../../domain/entities.js';
|
||||
import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
} from '../../../constraints/types.js';
|
||||
import type { GeneralActionDefinition } from '../../definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '../../engine.js';
|
||||
|
||||
export interface NationRestArgs {}
|
||||
|
||||
const ACTION_NAME = '휴식';
|
||||
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> {
|
||||
resolve(
|
||||
_context: GeneralActionResolveContext<TriggerState>,
|
||||
_args: NationRestArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
void _context;
|
||||
void _args;
|
||||
return { effects: [] };
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<TriggerState, NationRestArgs> {
|
||||
public readonly key = '휴식';
|
||||
public readonly name = ACTION_NAME;
|
||||
private readonly resolver = new ActionResolver<TriggerState>();
|
||||
|
||||
parseArgs(_raw: unknown): NationRestArgs | null {
|
||||
void _raw;
|
||||
return {};
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: NationRestArgs
|
||||
): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
return [];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: GeneralActionResolveContext<TriggerState>,
|
||||
args: NationRestArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,21 @@ const readCity = (view: StateView, id?: number): City | null => {
|
||||
return view.get(req) as City | null;
|
||||
};
|
||||
|
||||
const readDestGeneral = (
|
||||
ctx: ConstraintContext,
|
||||
view: StateView
|
||||
): General | null => {
|
||||
const destGeneralId = resolveDestGeneralId(ctx);
|
||||
if (destGeneralId === undefined) {
|
||||
return null;
|
||||
}
|
||||
const req: RequirementKey = { kind: 'destGeneral', id: destGeneralId };
|
||||
if (!view.has(req)) {
|
||||
return null;
|
||||
}
|
||||
return view.get(req) as General | null;
|
||||
};
|
||||
|
||||
const resolveDestCityId = (ctx: ConstraintContext): number | undefined => {
|
||||
if (ctx.destCityId !== undefined) {
|
||||
return ctx.destCityId;
|
||||
@@ -48,6 +63,14 @@ const resolveDestCityId = (ctx: ConstraintContext): number | undefined => {
|
||||
return typeof raw === 'number' ? raw : undefined;
|
||||
};
|
||||
|
||||
const resolveDestGeneralId = (ctx: ConstraintContext): number | undefined => {
|
||||
if (ctx.destGeneralId !== undefined) {
|
||||
return ctx.destGeneralId;
|
||||
}
|
||||
const raw = ctx.args?.destGeneralId;
|
||||
return typeof raw === 'number' ? raw : undefined;
|
||||
};
|
||||
|
||||
const resolveDestNationId = (ctx: ConstraintContext): number | undefined => {
|
||||
if (ctx.destNationId !== undefined) {
|
||||
return ctx.destNationId;
|
||||
@@ -135,6 +158,12 @@ export const notBeNeutral = (): Constraint => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const alwaysFail = (reason: string): Constraint => ({
|
||||
name: 'AlwaysFail',
|
||||
requires: () => [],
|
||||
test: () => ({ kind: 'deny', reason }),
|
||||
});
|
||||
|
||||
export const beChief = (): Constraint => ({
|
||||
name: 'BeChief',
|
||||
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
|
||||
@@ -219,6 +248,78 @@ export const availableStrategicCommand = (
|
||||
},
|
||||
});
|
||||
|
||||
export const reqNationGold = (
|
||||
getRequiredGold: (ctx: ConstraintContext, view: StateView) => number,
|
||||
requirements: RequirementKey[] = []
|
||||
): Constraint => ({
|
||||
name: 'ReqNationGold',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [...requirements];
|
||||
if (ctx.nationId !== undefined) {
|
||||
reqs.push({ kind: 'nation', id: ctx.nationId });
|
||||
}
|
||||
return reqs;
|
||||
},
|
||||
test: (ctx, view) => {
|
||||
const nationId = ctx.nationId;
|
||||
if (nationId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||
}
|
||||
const nationReq: RequirementKey = { kind: 'nation', id: nationId };
|
||||
const missing = [nationReq, ...requirements].filter(
|
||||
(req) => !view.has(req)
|
||||
);
|
||||
if (missing.length > 0) {
|
||||
return unknownOrDeny(ctx, missing, '국가 정보가 없습니다.');
|
||||
}
|
||||
const nation = view.get(nationReq) as Nation | null;
|
||||
if (!nation) {
|
||||
return unknownOrDeny(ctx, [nationReq], '국가 정보가 없습니다.');
|
||||
}
|
||||
const required = getRequiredGold(ctx, view);
|
||||
if (nation.gold >= required) {
|
||||
return allow();
|
||||
}
|
||||
return { kind: 'deny', reason: '자금이 모자랍니다.' };
|
||||
},
|
||||
});
|
||||
|
||||
export const reqNationRice = (
|
||||
getRequiredRice: (ctx: ConstraintContext, view: StateView) => number,
|
||||
requirements: RequirementKey[] = []
|
||||
): Constraint => ({
|
||||
name: 'ReqNationRice',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [...requirements];
|
||||
if (ctx.nationId !== undefined) {
|
||||
reqs.push({ kind: 'nation', id: ctx.nationId });
|
||||
}
|
||||
return reqs;
|
||||
},
|
||||
test: (ctx, view) => {
|
||||
const nationId = ctx.nationId;
|
||||
if (nationId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||
}
|
||||
const nationReq: RequirementKey = { kind: 'nation', id: nationId };
|
||||
const missing = [nationReq, ...requirements].filter(
|
||||
(req) => !view.has(req)
|
||||
);
|
||||
if (missing.length > 0) {
|
||||
return unknownOrDeny(ctx, missing, '국가 정보가 없습니다.');
|
||||
}
|
||||
const nation = view.get(nationReq) as Nation | null;
|
||||
if (!nation) {
|
||||
return unknownOrDeny(ctx, [nationReq], '국가 정보가 없습니다.');
|
||||
}
|
||||
const required = getRequiredRice(ctx, view);
|
||||
if (nation.rice >= required) {
|
||||
return allow();
|
||||
}
|
||||
return { kind: 'deny', reason: '군량이 모자랍니다.' };
|
||||
},
|
||||
});
|
||||
|
||||
export const notOpeningPart = (
|
||||
relYear: number,
|
||||
openingPartYear: number
|
||||
@@ -272,6 +373,45 @@ export const occupiedCity = (
|
||||
},
|
||||
});
|
||||
|
||||
export const occupiedDestCity = (): Constraint => ({
|
||||
name: 'OccupiedDestCity',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
|
||||
const destCityId = resolveDestCityId(ctx);
|
||||
if (destCityId !== undefined) {
|
||||
reqs.push({ kind: 'destCity', id: destCityId });
|
||||
}
|
||||
return reqs;
|
||||
},
|
||||
test: (ctx, view) => {
|
||||
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||
if (!view.has(generalReq)) {
|
||||
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
|
||||
}
|
||||
const general = view.get(generalReq) as General | null;
|
||||
if (!general) {
|
||||
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
|
||||
}
|
||||
const destCity = readDestCity(ctx, view);
|
||||
if (!destCity) {
|
||||
const destCityId = resolveDestCityId(ctx);
|
||||
if (destCityId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||
}
|
||||
const req: RequirementKey = {
|
||||
kind: 'destCity',
|
||||
id: destCityId,
|
||||
};
|
||||
return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.');
|
||||
}
|
||||
const baseNationId = ctx.nationId ?? general.nationId;
|
||||
if (destCity.nationId === baseNationId) {
|
||||
return allow();
|
||||
}
|
||||
return { kind: 'deny', reason: '아국이 아닙니다.' };
|
||||
},
|
||||
});
|
||||
|
||||
export const suppliedCity = (): Constraint => ({
|
||||
name: 'SuppliedCity',
|
||||
requires: (ctx) =>
|
||||
@@ -292,6 +432,37 @@ export const suppliedCity = (): Constraint => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const suppliedDestCity = (): Constraint => ({
|
||||
name: 'SuppliedDestCity',
|
||||
requires: (ctx) =>
|
||||
resolveDestCityId(ctx) !== undefined
|
||||
? [
|
||||
{
|
||||
kind: 'destCity',
|
||||
id: resolveDestCityId(ctx) ?? 0,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
test: (ctx, view) => {
|
||||
const destCity = readDestCity(ctx, view);
|
||||
if (!destCity) {
|
||||
const destCityId = resolveDestCityId(ctx);
|
||||
if (destCityId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||
}
|
||||
const req: RequirementKey = {
|
||||
kind: 'destCity',
|
||||
id: destCityId,
|
||||
};
|
||||
return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.');
|
||||
}
|
||||
if (destCity.supplyState) {
|
||||
return allow();
|
||||
}
|
||||
return { kind: 'deny', reason: '고립된 도시입니다.' };
|
||||
},
|
||||
});
|
||||
|
||||
export const reqGeneralGold = (
|
||||
getRequiredGold: (ctx: ConstraintContext, view: StateView) => number,
|
||||
requirements: RequirementKey[] = []
|
||||
@@ -402,6 +573,72 @@ export const existsDestCity = (): Constraint => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const existsDestGeneral = (): Constraint => ({
|
||||
name: 'ExistsDestGeneral',
|
||||
requires: (ctx) =>
|
||||
resolveDestGeneralId(ctx) !== undefined
|
||||
? [
|
||||
{
|
||||
kind: 'destGeneral',
|
||||
id: resolveDestGeneralId(ctx) ?? 0,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
test: (ctx, view) => {
|
||||
const destGeneralId = resolveDestGeneralId(ctx);
|
||||
if (destGeneralId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '장수 정보가 없습니다.');
|
||||
}
|
||||
const req: RequirementKey = { kind: 'destGeneral', id: destGeneralId };
|
||||
if (!view.has(req)) {
|
||||
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
|
||||
}
|
||||
const general = view.get(req) as General | null;
|
||||
if (!general) {
|
||||
return { kind: 'deny', reason: '장수 정보가 없습니다.' };
|
||||
}
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
export const friendlyDestGeneral = (): Constraint => ({
|
||||
name: 'FriendlyDestGeneral',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
|
||||
const destGeneralId = resolveDestGeneralId(ctx);
|
||||
if (destGeneralId !== undefined) {
|
||||
reqs.push({ kind: 'destGeneral', id: destGeneralId });
|
||||
}
|
||||
return reqs;
|
||||
},
|
||||
test: (ctx, view) => {
|
||||
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||
if (!view.has(generalReq)) {
|
||||
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
|
||||
}
|
||||
const general = view.get(generalReq) as General | null;
|
||||
if (!general) {
|
||||
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
|
||||
}
|
||||
const destGeneral = readDestGeneral(ctx, view);
|
||||
if (!destGeneral) {
|
||||
const destGeneralId = resolveDestGeneralId(ctx);
|
||||
if (destGeneralId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '장수 정보가 없습니다.');
|
||||
}
|
||||
const req: RequirementKey = {
|
||||
kind: 'destGeneral',
|
||||
id: destGeneralId,
|
||||
};
|
||||
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
|
||||
}
|
||||
if (destGeneral.nationId === general.nationId) {
|
||||
return allow();
|
||||
}
|
||||
return { kind: 'deny', reason: '아군이 아닙니다.' };
|
||||
},
|
||||
});
|
||||
|
||||
export const notOccupiedDestCity = (): Constraint => ({
|
||||
name: 'NotOccupiedDestCity',
|
||||
requires: (ctx) => {
|
||||
|
||||
Reference in New Issue
Block a user