Refactor constraints and action definitions for improved clarity and functionality
- Renamed `requireMinimumTerm` to `reqMinimumTreatyTerm` for consistency in naming conventions. - Introduced `battleGroundCity` constraint to encapsulate logic for checking if a city is in a war zone. - Added `hasRouteToDestCity` constraint to validate city accessibility based on distance and resource requirements. - Replaced `alwaysFail` with `denyWithReason` for better error handling in action definitions. - Implemented `reqEnvValue` for environment value checks with comparison operators. - Enhanced nation constraints with `reqNationValue` and `reqDestNationValue` for flexible value comparisons. - Added `wanderingNation` constraint to check if a nation is classified as wandering. - Improved general constraints with `reqGeneralValue` for dynamic value checks. - Updated various action definitions to utilize new constraints for better maintainability and readability.
This commit is contained in:
@@ -2,12 +2,12 @@ import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
beChief,
|
||||
destGeneralInDestNation,
|
||||
disallowDiplomacyBetweenStatus,
|
||||
existsDestGeneral,
|
||||
existsDestNation,
|
||||
notBeNeutral,
|
||||
occupiedCity,
|
||||
reqDestNationValue,
|
||||
suppliedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
|
||||
@@ -64,8 +64,8 @@ const parseMonth = (raw: unknown): number | null => {
|
||||
|
||||
const resolveMonthIndex = (year: number, month: number): number => year * 12 + month - 1;
|
||||
|
||||
const requireFutureTerm = (): Constraint => ({
|
||||
name: 'RequireNonAggressionFutureTerm',
|
||||
const reqFutureTreatyTerm = (): Constraint => ({
|
||||
name: 'reqFutureTreatyTerm',
|
||||
requires: () => [
|
||||
{ kind: 'arg', key: 'year' },
|
||||
{ kind: 'arg', key: 'month' },
|
||||
@@ -114,21 +114,6 @@ const requireFutureTerm = (): Constraint => ({
|
||||
},
|
||||
});
|
||||
|
||||
const notSameDestGeneral = (): Constraint => ({
|
||||
name: 'NotSameDestGeneral',
|
||||
requires: () => [{ kind: 'arg', key: 'destGeneralId' }],
|
||||
test: (ctx) => {
|
||||
const destGeneralId = ctx.args.destGeneralId;
|
||||
if (typeof destGeneralId !== 'number') {
|
||||
return unknownOrDeny(ctx, [{ kind: 'arg', key: 'destGeneralId' }], '장수 정보가 없습니다.');
|
||||
}
|
||||
if (destGeneralId === ctx.actorId) {
|
||||
return { kind: 'deny', reason: '대상이 올바르지 않습니다.' };
|
||||
}
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
// 불가침 수락은 메시지와 연결되는 즉시 국가 커맨드로 사용한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -161,9 +146,8 @@ export class ActionDefinition<
|
||||
suppliedCity(),
|
||||
existsDestNation(),
|
||||
existsDestGeneral(),
|
||||
destGeneralInDestNation(),
|
||||
notSameDestGeneral(),
|
||||
requireFutureTerm(),
|
||||
reqDestNationValue('level', '국가규모', '>', 0, '상대국 정보가 없습니다.'),
|
||||
reqFutureTreatyTerm(),
|
||||
disallowDiplomacyBetweenStatus({
|
||||
0: '아국과 이미 교전중입니다.',
|
||||
1: '아국과 이미 선포중입니다.',
|
||||
|
||||
@@ -3,12 +3,11 @@ import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/
|
||||
import {
|
||||
allowDiplomacyBetweenStatus,
|
||||
beChief,
|
||||
destGeneralInDestNation,
|
||||
existsDestGeneral,
|
||||
existsDestNation,
|
||||
notBeNeutral,
|
||||
reqDestNationValue,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import { allow, unknownOrDeny } 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 { createDiplomacyPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
@@ -37,21 +36,6 @@ const parseGeneralId = (raw: unknown): number | null => {
|
||||
return raw > 0 ? Math.floor(raw) : null;
|
||||
};
|
||||
|
||||
const notSameDestGeneral = (): Constraint => ({
|
||||
name: 'NotSameDestGeneral',
|
||||
requires: () => [{ kind: 'arg', key: 'destGeneralId' }],
|
||||
test: (ctx) => {
|
||||
const destGeneralId = ctx.args.destGeneralId;
|
||||
if (typeof destGeneralId !== 'number') {
|
||||
return unknownOrDeny(ctx, [{ kind: 'arg', key: 'destGeneralId' }], '장수 정보가 없습니다.');
|
||||
}
|
||||
if (destGeneralId === ctx.actorId) {
|
||||
return { kind: 'deny', reason: '대상이 올바르지 않습니다.' };
|
||||
}
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
// 불가침 파기 수락은 메시지와 연결되는 즉시 국가 커맨드로 사용한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -75,8 +59,7 @@ export class ActionDefinition<
|
||||
notBeNeutral(),
|
||||
existsDestNation(),
|
||||
existsDestGeneral(),
|
||||
destGeneralInDestNation(),
|
||||
notSameDestGeneral(),
|
||||
reqDestNationValue('level', '국가규모', '>', 0, '상대국 정보가 없습니다.'),
|
||||
allowDiplomacyBetweenStatus([DIPLOMACY_NON_AGGRESSION], '불가침 중인 상대국에게만 가능합니다.'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,12 +3,11 @@ import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/
|
||||
import {
|
||||
allowDiplomacyBetweenStatus,
|
||||
beChief,
|
||||
destGeneralInDestNation,
|
||||
existsDestGeneral,
|
||||
existsDestNation,
|
||||
notBeNeutral,
|
||||
reqDestNationValue,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import { allow, unknownOrDeny } 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 { createDiplomacyPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
@@ -37,21 +36,6 @@ const parseGeneralId = (raw: unknown): number | null => {
|
||||
return raw > 0 ? Math.floor(raw) : null;
|
||||
};
|
||||
|
||||
const notSameDestGeneral = (): Constraint => ({
|
||||
name: 'NotSameDestGeneral',
|
||||
requires: () => [{ kind: 'arg', key: 'destGeneralId' }],
|
||||
test: (ctx) => {
|
||||
const destGeneralId = ctx.args.destGeneralId;
|
||||
if (typeof destGeneralId !== 'number') {
|
||||
return unknownOrDeny(ctx, [{ kind: 'arg', key: 'destGeneralId' }], '장수 정보가 없습니다.');
|
||||
}
|
||||
if (destGeneralId === ctx.actorId) {
|
||||
return { kind: 'deny', reason: '대상이 올바르지 않습니다.' };
|
||||
}
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
// 종전 수락은 메시지와 연결되는 즉시 국가 커맨드로 사용한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -75,8 +59,7 @@ export class ActionDefinition<
|
||||
notBeNeutral(),
|
||||
existsDestNation(),
|
||||
existsDestGeneral(),
|
||||
destGeneralInDestNation(),
|
||||
notSameDestGeneral(),
|
||||
reqDestNationValue('level', '국가규모', '>', 0, '상대국 정보가 없습니다.'),
|
||||
allowDiplomacyBetweenStatus([0, 1], '상대국과 선포, 전쟁중이지 않습니다.'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { mustBeNPC, existsDestCity } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
@@ -40,6 +39,9 @@ export class ActionResolver<
|
||||
resolve(context: NPCSelfResolveContext<TriggerState>, args: NPCSelfArgs): GeneralActionOutcome<TriggerState> {
|
||||
const general = context.general;
|
||||
const effects: GeneralActionEffect<TriggerState>[] = [];
|
||||
if (general.npcState < 2) {
|
||||
throw new Error('NPC가 아닙니다.');
|
||||
}
|
||||
|
||||
if (args.optionText === '순간이동') {
|
||||
if (args.destCityId === undefined) {
|
||||
@@ -98,13 +100,9 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, args: NPCSelfArgs): Constraint[] {
|
||||
const constraints = [mustBeNPC()];
|
||||
|
||||
if (args.optionText === '순간이동') {
|
||||
constraints.push(existsDestCity());
|
||||
}
|
||||
|
||||
return constraints;
|
||||
void _ctx;
|
||||
void args;
|
||||
return [];
|
||||
}
|
||||
|
||||
resolve(context: NPCSelfResolveContext<TriggerState>, args: NPCSelfArgs): GeneralActionOutcome<TriggerState> {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
beMonarch,
|
||||
beWanderingNation,
|
||||
beNeutralCity,
|
||||
reqCityLevel,
|
||||
reqNationGeneralCount,
|
||||
beLord,
|
||||
wanderingNation,
|
||||
reqNationValue,
|
||||
checkNationNameDuplicate,
|
||||
beOpeningPart,
|
||||
constructableCity,
|
||||
allowJoinAction,
|
||||
noPenalty,
|
||||
} 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';
|
||||
@@ -80,18 +81,19 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
buildMinConstraints(_ctx: ConstraintContext, _args: FoundingArgs): Constraint[] {
|
||||
return [beOpeningPart(), beWanderingNation()];
|
||||
return [beOpeningPart(), reqNationValue('level', '국가규모', '==', 0, '정식 국가가 아니어야합니다.'), noPenalty('noFoundNation')];
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, args: FoundingArgs): Constraint[] {
|
||||
return [
|
||||
beOpeningPart(),
|
||||
beMonarch(),
|
||||
beWanderingNation(),
|
||||
reqNationGeneralCount(2),
|
||||
beNeutralCity(),
|
||||
reqCityLevel([5, 6]), // 소, 중 도시
|
||||
beLord(),
|
||||
wanderingNation(),
|
||||
reqNationValue('gennum', '수하 장수', '>=', 2),
|
||||
checkNationNameDuplicate(args.nationName),
|
||||
allowJoinAction(),
|
||||
constructableCity(),
|
||||
noPenalty('noFoundNation'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import {
|
||||
suppliedCity,
|
||||
existsDestGeneral,
|
||||
resolveDestGeneralId,
|
||||
reqGeneralGold,
|
||||
reqGeneralRice,
|
||||
reqEnvValue,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
@@ -61,8 +64,8 @@ const differentNationDestGeneral = (): Constraint => ({
|
||||
},
|
||||
});
|
||||
|
||||
const notDestGeneralLord = (): Constraint => ({
|
||||
name: 'NotDestGeneralLord',
|
||||
const targetMustNotBeLord = (): Constraint => ({
|
||||
name: 'targetMustNotBeLord',
|
||||
requires: (ctx) => {
|
||||
const destGeneralId = resolveDestGeneralId(ctx);
|
||||
if (destGeneralId === undefined) return [];
|
||||
@@ -73,8 +76,8 @@ const notDestGeneralLord = (): Constraint => ({
|
||||
const destGeneral =
|
||||
destId !== undefined ? (view.get({ kind: 'destGeneral', id: destId }) as General | null) : null;
|
||||
if (!destGeneral) return { kind: 'deny', reason: '장수 정보가 없습니다.' };
|
||||
if (destGeneral.officerLevel !== 12) return { kind: 'allow' };
|
||||
return { kind: 'deny', reason: '군주에게는 등용장을 보낼 수 없습니다.' };
|
||||
if (destGeneral.officerLevel === 12) return { kind: 'deny', reason: '군주에게는 등용장을 보낼 수 없습니다.' };
|
||||
return { kind: 'allow' };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -160,38 +163,37 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
buildMinConstraints(_ctx: ConstraintContext, _args: EmployArgs): Constraint[] {
|
||||
return [notBeNeutral(), occupiedCity(), suppliedCity()];
|
||||
return [
|
||||
reqEnvValue('join_mode', '!=', 'onlyRandom', '랜덤 임관만 가능합니다'),
|
||||
notBeNeutral(),
|
||||
occupiedCity(),
|
||||
suppliedCity(),
|
||||
];
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, args: EmployArgs): Constraint[] {
|
||||
const develCost = this.env.develCost ?? 100;
|
||||
|
||||
const reqCostConstraint: Constraint = {
|
||||
name: 'ReqScoutCost',
|
||||
requires: (c) => [
|
||||
{ kind: 'general', id: c.actorId },
|
||||
{ kind: 'destGeneral', id: args.destGeneralId },
|
||||
],
|
||||
test: (c, view) => {
|
||||
const gen = view.get({ kind: 'general', id: c.actorId }) as General | null;
|
||||
const dest = view.get({ kind: 'destGeneral', id: args.destGeneralId }) as General | null;
|
||||
if (!gen || !dest) return { kind: 'deny', reason: '정보 부족' };
|
||||
|
||||
const cost = develCost + Math.floor((dest.experience + dest.dedication) / 1000) * 10;
|
||||
if (gen.gold >= cost) return { kind: 'allow' };
|
||||
return { kind: 'deny', reason: `자금 ${cost}이 필요합니다.` };
|
||||
},
|
||||
};
|
||||
|
||||
return [
|
||||
const constraints: Constraint[] = [
|
||||
reqEnvValue('join_mode', '!=', 'onlyRandom', '랜덤 임관만 가능합니다'),
|
||||
notBeNeutral(),
|
||||
occupiedCity(),
|
||||
suppliedCity(),
|
||||
existsDestGeneral(),
|
||||
differentNationDestGeneral(),
|
||||
notDestGeneralLord(),
|
||||
reqCostConstraint,
|
||||
reqGeneralGold(
|
||||
(_c, view) => {
|
||||
const dest = view.get({ kind: 'destGeneral', id: args.destGeneralId }) as General | null;
|
||||
if (!dest) return develCost;
|
||||
return develCost + Math.floor((dest.experience + dest.dedication) / 1000) * 10;
|
||||
},
|
||||
[{ kind: 'destGeneral', id: args.destGeneralId }]
|
||||
),
|
||||
reqGeneralRice(() => 0),
|
||||
];
|
||||
|
||||
constraints.push(targetMustNotBeLord());
|
||||
return constraints;
|
||||
}
|
||||
|
||||
resolve(context: GeneralActionResolveContext<TriggerState>, args: EmployArgs): GeneralActionOutcome<TriggerState> {
|
||||
|
||||
@@ -2,10 +2,12 @@ import type { General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domai
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
existsDestNation,
|
||||
existsDestGeneral,
|
||||
notSameDestNation,
|
||||
destGeneralInDestNation,
|
||||
notLord,
|
||||
beNeutral,
|
||||
allowJoinDestNation,
|
||||
reqDestNationValue,
|
||||
differentDestNation,
|
||||
reqGeneralValue,
|
||||
reqEnvValue,
|
||||
readMetaNumberFromUnknown,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
@@ -186,7 +188,20 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: AcceptScoutArgs): Constraint[] {
|
||||
return [existsDestNation(), existsDestGeneral(), notSameDestNation(), destGeneralInDestNation(), notLord()];
|
||||
const env = _ctx.env;
|
||||
const year = typeof env.year === 'number' ? env.year : 0;
|
||||
const startYear = typeof env.startyear === 'number' ? env.startyear : 0;
|
||||
const relYear = year - startYear;
|
||||
|
||||
return [
|
||||
reqEnvValue('join_mode', '!=', 'onlyRandom', '랜덤 임관만 가능합니다'),
|
||||
existsDestNation(),
|
||||
beNeutral(),
|
||||
allowJoinDestNation(relYear),
|
||||
reqDestNationValue('level', '국가규모', '>', 0, '방랑군에는 임관할 수 없습니다.'),
|
||||
differentDestNation(),
|
||||
reqGeneralValue('officerLevel', '직위', '!=', 12, '군주는 등용장을 수락할 수 없습니다'),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { RandomGenerator } from '@sammo-ts/common';
|
||||
import { asRecord, JosaUtil } from '@sammo-ts/common';
|
||||
import type { General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { beNeutral } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import { beNeutral, allowJoinAction } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionEffect,
|
||||
@@ -134,11 +134,11 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
buildMinConstraints(_ctx: ConstraintContext, _args: RandomAppointmentArgs): Constraint[] {
|
||||
return [beNeutral()];
|
||||
return [];
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: RandomAppointmentArgs): Constraint[] {
|
||||
return [beNeutral()];
|
||||
return [beNeutral(), allowJoinAction()];
|
||||
}
|
||||
|
||||
resolve(
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { City, General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, RequirementKey } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { allow, notWanderingNation, unknownOrDeny } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
allow,
|
||||
notWanderingNation,
|
||||
unknownOrDeny,
|
||||
notOpeningPart,
|
||||
allowDiplomacyStatus,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
@@ -184,7 +190,14 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: WanderArgs): Constraint[] {
|
||||
return [beLord(), notWanderingNation()];
|
||||
const relYear = typeof _ctx.env.relYear === 'number' ? _ctx.env.relYear : 0;
|
||||
const openingPartYear = typeof _ctx.env.openingPartYear === 'number' ? _ctx.env.openingPartYear : 0;
|
||||
return [
|
||||
beLord(),
|
||||
notWanderingNation(),
|
||||
notOpeningPart(relYear, openingPartYear),
|
||||
allowDiplomacyStatus([2, 7], '방랑할 수 없는 외교상태입니다.'),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(context: WanderResolveContext<TriggerState>, args: WanderArgs): GeneralActionOutcome<TriggerState> {
|
||||
|
||||
@@ -2,10 +2,13 @@ import type { City, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
notBeNeutral,
|
||||
occupiedCity,
|
||||
suppliedCity,
|
||||
notOccupiedDestCity,
|
||||
notNeutralDestCity,
|
||||
reqGeneralGold,
|
||||
reqGeneralRice,
|
||||
existsDestCity,
|
||||
disallowDiplomacyBetweenStatus,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
@@ -136,15 +139,26 @@ export class ActionDefinition<
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildMinConstraints(ctx: ConstraintContext, _args: AgitateArgs): Constraint[] {
|
||||
const env = ctx.env;
|
||||
const cost = (env.develCost as number) ?? 100;
|
||||
return [notBeNeutral(), occupiedCity(), suppliedCity(), reqGeneralGold(() => cost), reqGeneralRice(() => cost)];
|
||||
}
|
||||
|
||||
buildConstraints(ctx: ConstraintContext, _args: AgitateArgs): Constraint[] {
|
||||
const env = ctx.env;
|
||||
const cost = (env.develCost as number) ?? 100;
|
||||
return [
|
||||
notBeNeutral(),
|
||||
existsDestCity(),
|
||||
occupiedCity(),
|
||||
suppliedCity(),
|
||||
notOccupiedDestCity(),
|
||||
notNeutralDestCity(),
|
||||
reqGeneralGold(() => cost),
|
||||
reqGeneralRice(() => cost),
|
||||
notOccupiedDestCity(),
|
||||
disallowDiplomacyBetweenStatus({
|
||||
7: '불가침국입니다.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, RequirementKey } from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
notSameDestCity,
|
||||
existsDestCity,
|
||||
nearCity,
|
||||
reqGeneralGold,
|
||||
unknownOrDeny,
|
||||
resolveDestCityId,
|
||||
reqGeneralRice,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
@@ -40,38 +39,6 @@ const ARGS_SCHEMA = z.object({
|
||||
});
|
||||
export type MoveArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
// Helper for map connectivity
|
||||
const connectedCity = (): Constraint => ({
|
||||
name: 'ConnectedCity',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [];
|
||||
if (ctx.cityId !== undefined) reqs.push({ kind: 'city', id: ctx.cityId });
|
||||
const destCityId = resolveDestCityId(ctx);
|
||||
if (destCityId !== undefined) reqs.push({ kind: 'destCity', id: destCityId });
|
||||
reqs.push({ kind: 'env', key: 'map' });
|
||||
return reqs;
|
||||
},
|
||||
test: (ctx, view) => {
|
||||
const map = view.get({ kind: 'env', key: 'map' }) as MapDefinition | null;
|
||||
if (!map) return unknownOrDeny(ctx, [], '지도 정보가 없습니다.');
|
||||
|
||||
const currentCityId = ctx.cityId;
|
||||
const destCityId = resolveDestCityId(ctx);
|
||||
|
||||
if (currentCityId === undefined || destCityId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||
}
|
||||
|
||||
const currentCityDef = map.cities.find((c) => c.id === currentCityId);
|
||||
if (!currentCityDef) return unknownOrDeny(ctx, [], '출발 도시 정보가 없습니다.');
|
||||
|
||||
if (currentCityDef.connections.includes(destCityId)) {
|
||||
return { kind: 'allow' };
|
||||
}
|
||||
return { kind: 'deny', reason: '인접하지 않은 도시입니다.' };
|
||||
},
|
||||
});
|
||||
|
||||
export class ActionResolver<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
> implements GeneralActionResolver<TriggerState, MoveArgs> {
|
||||
@@ -169,20 +136,20 @@ export class ActionDefinition<
|
||||
const develCost = ctx.env.develCost as number;
|
||||
return develCost ?? 0;
|
||||
}),
|
||||
reqGeneralRice(() => 0),
|
||||
];
|
||||
}
|
||||
|
||||
buildConstraints(ctx: ConstraintContext, _args: MoveArgs): Constraint[] {
|
||||
const constraints = [
|
||||
existsDestCity(),
|
||||
return [
|
||||
notSameDestCity(),
|
||||
connectedCity(),
|
||||
nearCity(1),
|
||||
reqGeneralGold(() => {
|
||||
const develCost = ctx.env.develCost as number;
|
||||
return develCost ?? 0;
|
||||
}),
|
||||
reqGeneralRice(() => 0),
|
||||
];
|
||||
return constraints;
|
||||
}
|
||||
|
||||
resolve(context: MoveResolveContext<TriggerState>, args: MoveArgs): GeneralActionOutcome<TriggerState> {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { beNeutral, existsDestNation } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
beNeutral,
|
||||
existsDestNation,
|
||||
reqEnvValue,
|
||||
allowJoinAction,
|
||||
noPenalty,
|
||||
allowJoinDestNation,
|
||||
} 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 { createGeneralPatchEffect } from '@sammo-ts/logic/actions/engine.js';
|
||||
@@ -44,11 +51,28 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
buildMinConstraints(_ctx: ConstraintContext, _args: AppointmentArgs): Constraint[] {
|
||||
return [beNeutral()];
|
||||
return [
|
||||
reqEnvValue('join_mode', '!=', 'onlyRandom', '랜덤 임관만 가능합니다'),
|
||||
beNeutral(),
|
||||
allowJoinAction(),
|
||||
noPenalty('noChosenAssignment'),
|
||||
];
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: AppointmentArgs): Constraint[] {
|
||||
return [beNeutral(), existsDestNation()];
|
||||
const env = _ctx.env;
|
||||
const year = typeof env.year === 'number' ? env.year : 0;
|
||||
const startYear = typeof env.startyear === 'number' ? env.startyear : 0;
|
||||
const relYear = year - startYear;
|
||||
|
||||
return [
|
||||
reqEnvValue('join_mode', '!=', 'onlyRandom', '랜덤 임관만 가능합니다'),
|
||||
beNeutral(),
|
||||
existsDestNation(),
|
||||
allowJoinDestNation(relYear),
|
||||
allowJoinAction(),
|
||||
noPenalty('noChosenAssignment'),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
notOccupiedDestCity,
|
||||
reqGeneralGold,
|
||||
reqGeneralRice,
|
||||
existsDestCity,
|
||||
notBeNeutral,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
@@ -297,11 +296,9 @@ export class ActionDefinition<
|
||||
const env = ctx.env;
|
||||
const cost = ((env.develCost as number) ?? 100) * 3;
|
||||
return [
|
||||
notBeNeutral(),
|
||||
existsDestCity(),
|
||||
notOccupiedDestCity(),
|
||||
reqGeneralGold(() => cost),
|
||||
reqGeneralRice(() => cost),
|
||||
notOccupiedDestCity(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
existsDestCity,
|
||||
hasRouteWithEnemy,
|
||||
notBeNeutral,
|
||||
notOccupiedDestCity,
|
||||
notOpeningPart,
|
||||
notSameDestCity,
|
||||
occupiedCity,
|
||||
reqGeneralCrew,
|
||||
reqGeneralRice,
|
||||
suppliedCity,
|
||||
allowWar,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import { readGeneral } from '@sammo-ts/logic/constraints/helpers.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
@@ -268,7 +266,6 @@ export class ActionDefinition<
|
||||
notOpeningPart(relYear + 2, openingPartYear),
|
||||
notBeNeutral(),
|
||||
occupiedCity(),
|
||||
suppliedCity(),
|
||||
reqGeneralCrew(),
|
||||
reqGeneralRice(getRequiredRice),
|
||||
];
|
||||
@@ -282,11 +279,9 @@ export class ActionDefinition<
|
||||
notSameDestCity(),
|
||||
notBeNeutral(),
|
||||
occupiedCity(),
|
||||
suppliedCity(),
|
||||
reqGeneralCrew(),
|
||||
reqGeneralRice(getRequiredRice),
|
||||
existsDestCity(),
|
||||
notOccupiedDestCity(),
|
||||
allowWar(),
|
||||
hasRouteWithEnemy(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ import type { City, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/e
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
notBeNeutral,
|
||||
occupiedCity,
|
||||
suppliedCity,
|
||||
notOccupiedDestCity,
|
||||
notNeutralDestCity,
|
||||
reqGeneralGold,
|
||||
reqGeneralRice,
|
||||
existsDestCity,
|
||||
disallowDiplomacyBetweenStatus,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
@@ -200,15 +203,26 @@ export class ActionDefinition<
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildMinConstraints(ctx: ConstraintContext, _args: SeizeArgs): Constraint[] {
|
||||
const env = ctx.env;
|
||||
const cost = (env.develCost as number) ?? 100;
|
||||
return [notBeNeutral(), occupiedCity(), suppliedCity(), reqGeneralGold(() => cost), reqGeneralRice(() => cost)];
|
||||
}
|
||||
|
||||
buildConstraints(ctx: ConstraintContext, _args: SeizeArgs): Constraint[] {
|
||||
const env = ctx.env;
|
||||
const cost = (env.develCost as number) ?? 100;
|
||||
return [
|
||||
notBeNeutral(),
|
||||
existsDestCity(),
|
||||
occupiedCity(),
|
||||
suppliedCity(),
|
||||
notOccupiedDestCity(),
|
||||
notNeutralDestCity(),
|
||||
reqGeneralGold(() => cost),
|
||||
reqGeneralRice(() => cost),
|
||||
notOccupiedDestCity(),
|
||||
disallowDiplomacyBetweenStatus({
|
||||
7: '불가침국입니다.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ import type { City, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
notBeNeutral,
|
||||
occupiedCity,
|
||||
suppliedCity,
|
||||
notOccupiedDestCity,
|
||||
notNeutralDestCity,
|
||||
reqGeneralGold,
|
||||
reqGeneralRice,
|
||||
existsDestCity,
|
||||
disallowDiplomacyBetweenStatus,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
@@ -133,15 +136,26 @@ export class ActionDefinition<
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildMinConstraints(ctx: ConstraintContext, _args: DestroyArgs): Constraint[] {
|
||||
const env = ctx.env;
|
||||
const cost = (env.develCost as number) ?? 100;
|
||||
return [notBeNeutral(), occupiedCity(), suppliedCity(), reqGeneralGold(() => cost), reqGeneralRice(() => cost)];
|
||||
}
|
||||
|
||||
buildConstraints(ctx: ConstraintContext, _args: DestroyArgs): Constraint[] {
|
||||
const env = ctx.env;
|
||||
const cost = (env.develCost as number) ?? 100;
|
||||
return [
|
||||
notBeNeutral(),
|
||||
existsDestCity(),
|
||||
occupiedCity(),
|
||||
suppliedCity(),
|
||||
notOccupiedDestCity(),
|
||||
notNeutralDestCity(),
|
||||
reqGeneralGold(() => cost),
|
||||
reqGeneralRice(() => cost),
|
||||
notOccupiedDestCity(),
|
||||
disallowDiplomacyBetweenStatus({
|
||||
7: '불가침국입니다.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ import {
|
||||
notBeNeutral,
|
||||
notWanderingNation,
|
||||
occupiedCity,
|
||||
remainCityCapacityByMax,
|
||||
remainCityCapacity,
|
||||
reqGeneralGold,
|
||||
reqGeneralRice,
|
||||
suppliedCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
@@ -61,8 +62,9 @@ export class CityDevelopmentActionDefinition<
|
||||
notWanderingNation(),
|
||||
occupiedCity(),
|
||||
suppliedCity(),
|
||||
remainCityCapacityByMax(this.config.statKey, this.config.maxKey, this.config.label),
|
||||
remainCityCapacity(this.config.statKey, this.config.label),
|
||||
reqGeneralGold(getRequiredGold),
|
||||
reqGeneralRice(() => 0),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { beChief, occupiedCity, suppliedCity } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { beChief, occupiedCity, suppliedCity, reqNationAuxValue } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionEffect,
|
||||
@@ -68,22 +68,21 @@ export class ActionDefinition<
|
||||
return parseArgsWithSchema(ARGS_SCHEMA, raw);
|
||||
}
|
||||
|
||||
buildMinConstraints(_ctx: ConstraintContext, _args: ChangeFlagArgs): Constraint[] {
|
||||
return [
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
suppliedCity(),
|
||||
reqNationAuxValue(`can_${ACTION_NAME}`, 0, '>', 0, '더이상 변경이 불가능합니다.'),
|
||||
];
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: ChangeFlagArgs): Constraint[] {
|
||||
return [
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
suppliedCity(),
|
||||
{
|
||||
name: 'canChangeFlag',
|
||||
requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }],
|
||||
test: (ctx: ConstraintContext, view: StateView) => {
|
||||
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||
const canChangeRaw = nation?.meta[`can_${ACTION_NAME}`];
|
||||
const canChange = (typeof canChangeRaw === 'number' ? canChangeRaw : 0) !== 0;
|
||||
if (!canChange) return { kind: 'deny', reason: '더이상 변경이 불가능합니다.' };
|
||||
return { kind: 'allow' };
|
||||
},
|
||||
},
|
||||
reqNationAuxValue(`can_${ACTION_NAME}`, 0, '>', 0, '더이상 변경이 불가능합니다.'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { beChief, occupiedCity, suppliedCity, checkNationNameDuplicate } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { beChief, occupiedCity, suppliedCity, reqNationAuxValue } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionEffect,
|
||||
@@ -37,37 +37,17 @@ export class ActionDefinition<
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
suppliedCity(),
|
||||
{
|
||||
name: 'canChangeNationName',
|
||||
requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }],
|
||||
test: (_ctx: ConstraintContext, view: StateView) => {
|
||||
const nation = view.get({ kind: 'nation', id: _ctx.nationId! }) as Nation | undefined;
|
||||
const canChangeRaw = nation?.meta[`can_${ACTION_NAME}`];
|
||||
const canChange = (typeof canChangeRaw === 'number' ? canChangeRaw : 0) !== 0;
|
||||
if (!canChange) return { kind: 'deny', reason: '더이상 변경이 불가능합니다.' };
|
||||
return { kind: 'allow' };
|
||||
},
|
||||
},
|
||||
reqNationAuxValue(`can_${ACTION_NAME}`, 0, '>', 0, '더이상 변경이 불가능합니다.'),
|
||||
];
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, args: ChangeNationNameArgs): Constraint[] {
|
||||
buildConstraints(_ctx: ConstraintContext, _args: ChangeNationNameArgs): Constraint[] {
|
||||
void _args;
|
||||
return [
|
||||
occupiedCity(),
|
||||
beChief(),
|
||||
suppliedCity(),
|
||||
checkNationNameDuplicate(args.nationName),
|
||||
{
|
||||
name: 'canChangeNationName',
|
||||
requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }],
|
||||
test: (_ctx: ConstraintContext, view: StateView) => {
|
||||
const nation = view.get({ kind: 'nation', id: _ctx.nationId! }) as Nation | undefined;
|
||||
const canChangeRaw = nation?.meta[`can_${ACTION_NAME}`];
|
||||
const canChange = (typeof canChangeRaw === 'number' ? canChangeRaw : 0) !== 0;
|
||||
if (!canChange) return { kind: 'deny', reason: '더이상 변경이 불가능합니다.' };
|
||||
return { kind: 'allow' };
|
||||
},
|
||||
},
|
||||
reqNationAuxValue(`can_${ACTION_NAME}`, 0, '>', 0, '더이상 변경이 불가능합니다.'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ export class ActionDefinition<
|
||||
existsDestGeneral(),
|
||||
friendlyDestGeneral(),
|
||||
{
|
||||
name: 'notSelf',
|
||||
name: 'notSelfDestGeneral',
|
||||
requires: () => [],
|
||||
test: (ctx: ConstraintContext) => {
|
||||
if (ctx.actorId === args.destGeneralID) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { GeneralTriggerState, Nation, City, General } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { beMonarch, occupiedCity, suppliedCity } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralTriggerState, City, General } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { beLord, occupiedCity, suppliedCity, beOpeningPart, reqNationAuxValue } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
GeneralActionEffect,
|
||||
@@ -45,21 +45,15 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: RandomMoveCapitalArgs): Constraint[] {
|
||||
void _ctx;
|
||||
void _args;
|
||||
|
||||
return [
|
||||
occupiedCity(),
|
||||
beMonarch(),
|
||||
beLord(),
|
||||
suppliedCity(),
|
||||
{
|
||||
name: 'canRandomMoveCapital',
|
||||
requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }],
|
||||
test: (ctx: ConstraintContext, view: StateView) => {
|
||||
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||
const canMoveRaw = nation?.meta[`can_무작위수도이전`];
|
||||
const canMove = (typeof canMoveRaw === 'number' ? canMoveRaw : 0) > 0;
|
||||
if (!canMove) return { kind: 'deny', reason: '더이상 변경이 불가능합니다.' };
|
||||
return { kind: 'allow' };
|
||||
},
|
||||
},
|
||||
beOpeningPart(),
|
||||
reqNationAuxValue('can_무작위수도이전', 0, '>', 0, '더이상 변경이 불가능합니다.'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
} from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
alwaysFail,
|
||||
denyWithReason,
|
||||
beChief,
|
||||
existsDestGeneral,
|
||||
friendlyDestGeneral,
|
||||
@@ -161,7 +161,7 @@ export class ActionDefinition<
|
||||
buildConstraints(ctx: ConstraintContext, _args: AssignmentArgs): Constraint[] {
|
||||
void _args;
|
||||
if (ctx.destGeneralId === ctx.actorId) {
|
||||
return [alwaysFail('본인입니다')];
|
||||
return [denyWithReason('본인입니다')];
|
||||
}
|
||||
return [
|
||||
beChief(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
alwaysFail,
|
||||
denyWithReason,
|
||||
beChief,
|
||||
existsDestGeneral,
|
||||
friendlyDestGeneral,
|
||||
@@ -54,7 +54,7 @@ export class ActionDefinition<
|
||||
|
||||
buildConstraints(ctx: ConstraintContext, _args: TroopKickArgs): Constraint[] {
|
||||
if (ctx.destGeneralId !== undefined && ctx.destGeneralId === ctx.actorId) {
|
||||
return [alwaysFail('본인입니다')];
|
||||
return [denyWithReason('본인입니다')];
|
||||
}
|
||||
return [notBeNeutral(), beChief(), existsDestGeneral(), friendlyDestGeneral()];
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ const MIN_TERM_MONTHS = 6;
|
||||
|
||||
const resolveMonthIndex = (year: number, month: number): number => year * 12 + month - 1;
|
||||
|
||||
const requireMinimumTerm = (minMonths: number): Constraint => ({
|
||||
name: 'RequireNonAggressionMinimumTerm',
|
||||
const reqMinimumTreatyTerm = (minMonths: number): Constraint => ({
|
||||
name: 'reqMinimumTreatyTerm',
|
||||
requires: () => [
|
||||
{ kind: 'arg', key: 'year' },
|
||||
{ kind: 'arg', key: 'month' },
|
||||
@@ -108,7 +108,7 @@ export class ActionDefinition<
|
||||
notBeNeutral(),
|
||||
existsDestNation(),
|
||||
differentDestNation(),
|
||||
requireMinimumTerm(MIN_TERM_MONTHS),
|
||||
reqMinimumTreatyTerm(MIN_TERM_MONTHS),
|
||||
disallowDiplomacyBetweenStatus({
|
||||
0: '아국과 이미 교전중입니다.',
|
||||
1: '아국과 이미 선포중입니다.',
|
||||
|
||||
@@ -61,7 +61,7 @@ export class ActionDefinition<
|
||||
suppliedCity(),
|
||||
beChief(),
|
||||
{
|
||||
name: 'declareWarYearLimit',
|
||||
name: 'reqEnvValue',
|
||||
requires: () => [],
|
||||
test: () => {
|
||||
if (relYear >= 1) return { kind: 'allow' };
|
||||
@@ -83,7 +83,7 @@ export class ActionDefinition<
|
||||
nearNation(),
|
||||
// legacy: startYear + 1. 0-indexed relYear로는 1
|
||||
{
|
||||
name: 'declareWarYearLimit',
|
||||
name: 'reqEnvValue',
|
||||
requires: () => [],
|
||||
test: () => {
|
||||
if (relYear >= 1) return { kind: 'allow' };
|
||||
|
||||
@@ -48,6 +48,12 @@ const PRE_REQ_TURN = 2;
|
||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||
const DAMAGE_RATE = 0.2;
|
||||
|
||||
const battleGroundCity = (): Constraint => ({
|
||||
name: 'battleGroundCity',
|
||||
requires: (ctx) => allowDiplomacyBetweenStatus([0], '교전중인 국가의 도시가 아닙니다.').requires(ctx),
|
||||
test: (ctx, view) => allowDiplomacyBetweenStatus([0], '교전중인 국가의 도시가 아닙니다.').test(ctx, view),
|
||||
});
|
||||
|
||||
// 수몰 쿨타임 계산을 담당한다.
|
||||
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
private readonly pipeline: GeneralActionPipeline<TriggerState>;
|
||||
@@ -189,7 +195,7 @@ export class ActionDefinition<
|
||||
beChief(),
|
||||
notNeutralDestCity(),
|
||||
notOccupiedDestCity(),
|
||||
allowDiplomacyBetweenStatus([0], '교전중인 국가의 도시가 아닙니다.'),
|
||||
battleGroundCity(),
|
||||
availableStrategicCommand(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import {
|
||||
beChief,
|
||||
occupiedCity,
|
||||
occupiedDestCity,
|
||||
reqNationGold,
|
||||
reqNationRice,
|
||||
reqNationValue,
|
||||
suppliedCity,
|
||||
suppliedDestCity,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
@@ -37,6 +40,30 @@ export interface MoveCapitalResolveContext<
|
||||
|
||||
const ACTION_NAME = '천도';
|
||||
|
||||
const hasRouteToDestCity = (destCityID: number, develCost: number): Constraint => ({
|
||||
name: 'hasRouteToDestCity',
|
||||
requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }, { kind: 'env', key: 'map' }],
|
||||
test: (ctx: ConstraintContext, view: StateView) => {
|
||||
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||
const map = view.get({ kind: 'env', key: 'map' }) as MapDefinition | undefined;
|
||||
if (!nation || !map || nation.capitalCityId === undefined || nation.capitalCityId === null) {
|
||||
return { kind: 'allow' };
|
||||
}
|
||||
const dist = calcDistance(nation.capitalCityId, destCityID, map);
|
||||
if (dist >= 50) {
|
||||
return { kind: 'deny', reason: '천도 대상으로 도달할 방법이 없습니다.' };
|
||||
}
|
||||
const cost = develCost * 5 * Math.pow(2, dist);
|
||||
if (nation.gold < cost + 1000) {
|
||||
return { kind: 'allow' };
|
||||
}
|
||||
if (nation.rice < cost + 1000) {
|
||||
return { kind: 'allow' };
|
||||
}
|
||||
return { kind: 'allow' };
|
||||
},
|
||||
});
|
||||
|
||||
const calcDistance = (fromCityId: number, toCityId: number, map: MapDefinition): number => {
|
||||
if (fromCityId === toCityId) return 0;
|
||||
|
||||
@@ -83,6 +110,21 @@ export class ActionDefinition<
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, args: MoveCapitalArgs): Constraint[] {
|
||||
const develcost = this.env.develCost;
|
||||
const baseGold = this.env.baseGold ?? 1000;
|
||||
const baseRice = this.env.baseRice ?? 1000;
|
||||
|
||||
const getRequiredCost = (ctx: ConstraintContext, view: StateView): number => {
|
||||
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||
const map = view.get({ kind: 'env', key: 'map' }) as MapDefinition | undefined;
|
||||
if (!nation || !map || nation.capitalCityId === undefined || nation.capitalCityId === null) {
|
||||
return 0;
|
||||
}
|
||||
const dist = calcDistance(nation.capitalCityId, args.destCityID, map);
|
||||
if (dist >= 50) {
|
||||
return 0;
|
||||
}
|
||||
return develcost * 5 * Math.pow(2, dist);
|
||||
};
|
||||
|
||||
return [
|
||||
occupiedCity(),
|
||||
@@ -90,40 +132,10 @@ export class ActionDefinition<
|
||||
beChief(),
|
||||
suppliedCity(),
|
||||
suppliedDestCity(),
|
||||
{
|
||||
name: 'notCurrentCapital',
|
||||
requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }],
|
||||
test: (ctx: ConstraintContext, view: StateView) => {
|
||||
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||
if (nation?.capitalCityId === args.destCityID) {
|
||||
return { kind: 'deny', reason: '이미 수도입니다.' };
|
||||
}
|
||||
return { kind: 'allow' };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'reqMoveCapitalCost',
|
||||
requires: (ctx) => [
|
||||
{ kind: 'nation', id: ctx.nationId! },
|
||||
{ kind: 'env', key: 'map' },
|
||||
],
|
||||
test: (ctx: ConstraintContext, view: StateView) => {
|
||||
const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined;
|
||||
const map = view.get({ kind: 'env', key: 'map' }) as MapDefinition | undefined;
|
||||
if (!map || !nation || nation.capitalCityId === undefined || nation.capitalCityId === null)
|
||||
return { kind: 'allow' };
|
||||
|
||||
const dist = calcDistance(nation.capitalCityId, args.destCityID, map);
|
||||
const cost = develcost * 5 * Math.pow(2, dist);
|
||||
|
||||
if (nation.gold < cost + 1000)
|
||||
return { kind: 'deny', reason: `금이 부족합니다. (필요: ${cost + 1000})` };
|
||||
if (nation.rice < cost + 1000)
|
||||
return { kind: 'deny', reason: `쌀이 부족합니다. (필요: ${cost + 1000})` };
|
||||
|
||||
return { kind: 'allow' };
|
||||
},
|
||||
},
|
||||
hasRouteToDestCity(args.destCityID, develcost),
|
||||
reqNationValue('capitalCityId', '수도', '!=', args.destCityID, '이미 수도입니다.'),
|
||||
reqNationGold((ctx, view) => baseGold + getRequiredCost(ctx, view), [{ kind: 'env', key: 'map' }]),
|
||||
reqNationRice((ctx, view) => baseRice + getRequiredCost(ctx, view), [{ kind: 'env', key: 'map' }]),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext, RequirementKey, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
alwaysFail,
|
||||
denyWithReason,
|
||||
beChief,
|
||||
existsDestGeneral,
|
||||
friendlyDestGeneral,
|
||||
@@ -200,7 +200,7 @@ export class ActionDefinition<
|
||||
}
|
||||
|
||||
if (ctx.destGeneralId === ctx.actorId) {
|
||||
return [alwaysFail('본인입니다')];
|
||||
return [denyWithReason('본인입니다')];
|
||||
}
|
||||
|
||||
const getRequired = (_ctx: ConstraintContext, _view: StateView): number =>
|
||||
|
||||
@@ -71,8 +71,8 @@ const ARGS_SCHEMA = z.object({
|
||||
.refine((value) => value !== 'che_피장파장', '같은 전략은 선택할 수 없습니다.'),
|
||||
});
|
||||
|
||||
const requireCommandType = (): Constraint => ({
|
||||
name: 'requireStrategicCommandType',
|
||||
const reqValidStrategicCommandType = (): Constraint => ({
|
||||
name: 'reqValidStrategicCommandType',
|
||||
requires: () => [{ kind: 'arg', key: 'commandType' }],
|
||||
test: (ctx) => {
|
||||
const commandType = ctx.args.commandType;
|
||||
@@ -225,7 +225,7 @@ export class ActionDefinition<
|
||||
existsDestNation(),
|
||||
allowDiplomacyBetweenStatus([0, 1], '선포, 전쟁중인 상대국에게만 가능합니다.'),
|
||||
availableStrategicCommand(),
|
||||
requireCommandType(),
|
||||
reqValidStrategicCommandType(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user