diff --git a/app/game-api/src/turns/commandTable.ts b/app/game-api/src/turns/commandTable.ts index 31e2211..36ff0e0 100644 --- a/app/game-api/src/turns/commandTable.ts +++ b/app/game-api/src/turns/commandTable.ts @@ -184,6 +184,8 @@ const buildCommandEnv = (worldState: WorldStateRow): CommandEnv => { }; const buildConstraintEnv = (worldState: WorldStateRow): Record => { + const config = asRecord(worldState.config); + const constValues = asRecord(config.const); const meta = asRecord(worldState.meta); const scenarioMeta = asRecord(meta.scenarioMeta); const startYear = typeof scenarioMeta.startYear === 'number' ? scenarioMeta.startYear : undefined; @@ -196,6 +198,7 @@ const buildConstraintEnv = (worldState: WorldStateRow): Record month: worldState.currentMonth, startYear, relYear, + openingPartYear: resolveNumber(constValues, ['openingPartYear'], 0), }; }; @@ -352,7 +355,10 @@ const evaluateDefinition = ( reqArg: boolean, args: unknown ): AvailabilityCore => { - const constraints = definition.buildConstraints(ctx, args); + const constraints = + ctx.mode === 'precheck' && definition.buildMinConstraints + ? definition.buildMinConstraints(ctx, args) + : definition.buildConstraints(ctx, args); return evaluateAvailability(constraints, ctx, view, reqArg); }; diff --git a/app/game-api/test/commandTable.test.ts b/app/game-api/test/commandTable.test.ts new file mode 100644 index 0000000..a59b120 --- /dev/null +++ b/app/game-api/test/commandTable.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it } from 'vitest'; + +import type { CityRow, GeneralRow, NationRow, WorldStateRow } from '../src/context.js'; +import { buildTurnCommandTable } from '../src/turns/commandTable.js'; + +const buildWorldState = (): WorldStateRow => + ({ + id: 1, + scenarioCode: 'default', + currentYear: 3, + currentMonth: 1, + tickSeconds: 600, + config: { + const: { + baseGold: 1000, + baseRice: 1000, + develCost: 100, + }, + }, + meta: { + scenarioMeta: { + startYear: 1, + }, + }, + updatedAt: new Date('2026-01-01T00:00:00Z'), + }) as unknown as WorldStateRow; + +const buildGeneral = (): GeneralRow => + ({ + id: 1, + name: 'TestGeneral', + nationId: 1, + cityId: 1, + troopId: 0, + leadership: 70, + strength: 60, + intel: 60, + experience: 0, + dedication: 0, + officerLevel: 6, + personalCode: null, + specialCode: null, + special2Code: null, + horseCode: null, + weaponCode: null, + bookCode: null, + itemCode: null, + injury: 0, + gold: 0, + rice: 0, + crew: 100, + crewTypeId: 1100, + train: 0, + atmos: 0, + age: 25, + npcState: 0, + meta: {}, + }) as unknown as GeneralRow; + +const buildCity = (): CityRow => + ({ + id: 1, + name: 'TestCity', + nationId: 1, + level: 5, + population: 100000, + populationMax: 200000, + agriculture: 1000, + agricultureMax: 2000, + commerce: 1000, + commerceMax: 2000, + security: 1000, + securityMax: 2000, + supplyState: 1, + frontState: 0, + defence: 0, + defenceMax: 0, + wall: 0, + wallMax: 0, + meta: {}, + trust: 50, + trade: 0, + region: 0, + }) as unknown as CityRow; + +const buildNation = (): NationRow => + ({ + id: 1, + name: 'TestNation', + color: '#000000', + capitalCityId: 1, + gold: 0, + rice: 0, + level: 1, + typeCode: 'default', + tech: 0, + meta: {}, + }) as unknown as NationRow; + +describe('buildTurnCommandTable', () => { + it('uses min-condition constraints for availability', async () => { + const table = await buildTurnCommandTable({ + worldState: buildWorldState(), + general: buildGeneral(), + city: buildCity(), + nation: buildNation(), + nationGenerals: null, + }); + + const nationCommand = table.nation + .flatMap((group) => group.values) + .find((command) => command.key === 'che_포상'); + + expect(nationCommand).toBeDefined(); + expect(nationCommand?.possible).toBe(true); + expect(nationCommand?.status).not.toBe('blocked'); + }); +}); diff --git a/packages/logic/src/actions/definition.ts b/packages/logic/src/actions/definition.ts index 1296b0f..1e8d05b 100644 --- a/packages/logic/src/actions/definition.ts +++ b/packages/logic/src/actions/definition.ts @@ -7,9 +7,12 @@ export interface GeneralActionDefinition< Args = unknown, Context extends GeneralActionResolveContext = GeneralActionResolveContext, > { + // TODO: legacy permissionConstraints(예약 권한) 모델링 필요. key: string; name: string; parseArgs(raw: unknown): Args | null; + // 커맨드 입력 단계에서 최소 조건만 평가할 때 사용한다. + buildMinConstraints?(ctx: ConstraintContext, args: Args): Constraint[]; buildConstraints(ctx: ConstraintContext, args: Args): Constraint[]; resolve(context: Context, args: Args): GeneralActionOutcome; } diff --git a/packages/logic/src/actions/turn/general/che_강행.ts b/packages/logic/src/actions/turn/general/che_강행.ts index 104dd9f..30b0736 100644 --- a/packages/logic/src/actions/turn/general/che_강행.ts +++ b/packages/logic/src/actions/turn/general/che_강행.ts @@ -164,6 +164,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: ForcedMoveArgs): Constraint[] { + return []; + } + buildConstraints(ctx: ConstraintContext, _args: ForcedMoveArgs): Constraint[] { return [ existsDestCity(), diff --git a/packages/logic/src/actions/turn/general/che_건국.ts b/packages/logic/src/actions/turn/general/che_건국.ts index c261231..9e1ee34 100644 --- a/packages/logic/src/actions/turn/general/che_건국.ts +++ b/packages/logic/src/actions/turn/general/che_건국.ts @@ -77,6 +77,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: FoundingArgs): Constraint[] { + return [beOpeningPart(), beWanderingNation()]; + } + buildConstraints(_ctx: ConstraintContext, args: FoundingArgs): Constraint[] { return [ beOpeningPart(), diff --git a/packages/logic/src/actions/turn/general/che_군량매매.ts b/packages/logic/src/actions/turn/general/che_군량매매.ts index a31fe4f..8671705 100644 --- a/packages/logic/src/actions/turn/general/che_군량매매.ts +++ b/packages/logic/src/actions/turn/general/che_군량매매.ts @@ -1,6 +1,6 @@ import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js'; -import { occupiedCity, reqGeneralGold, reqGeneralRice } from '@sammo-ts/logic/constraints/presets.js'; +import { occupiedCity, reqGeneralGold, reqGeneralRice, suppliedCity } from '@sammo-ts/logic/constraints/presets.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js'; import { LogFormat } from '@sammo-ts/logic/logging/types.js'; @@ -37,8 +37,12 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: TradeArgs): Constraint[] { + return [occupiedCity(), suppliedCity()]; + } + buildConstraints(_ctx: ConstraintContext, args: TradeArgs): Constraint[] { - const constraints: Constraint[] = [occupiedCity()]; + const constraints: Constraint[] = [occupiedCity(), suppliedCity()]; if (args.buyRice) { constraints.push(reqGeneralGold(() => 1)); } else { diff --git a/packages/logic/src/actions/turn/general/che_등용.ts b/packages/logic/src/actions/turn/general/che_등용.ts index 32bd350..f4813bc 100644 --- a/packages/logic/src/actions/turn/general/che_등용.ts +++ b/packages/logic/src/actions/turn/general/che_등용.ts @@ -150,6 +150,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: EmployArgs): Constraint[] { + return [notBeNeutral(), occupiedCity(), suppliedCity()]; + } + buildConstraints(_ctx: ConstraintContext, args: EmployArgs): Constraint[] { const develCost = this.env.develCost ?? 100; diff --git a/packages/logic/src/actions/turn/general/che_사기진작.ts b/packages/logic/src/actions/turn/general/che_사기진작.ts index e885057..b460363 100644 --- a/packages/logic/src/actions/turn/general/che_사기진작.ts +++ b/packages/logic/src/actions/turn/general/che_사기진작.ts @@ -1,6 +1,12 @@ import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js'; -import { notBeNeutral, reqGeneralGold } from '@sammo-ts/logic/constraints/presets.js'; +import { + notBeNeutral, + notWanderingNation, + occupiedCity, + reqGeneralCrew, + reqGeneralGold, +} from '@sammo-ts/logic/constraints/presets.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js'; import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'; @@ -36,9 +42,13 @@ export class ActionDefinition< return {}; } + buildMinConstraints(_ctx: ConstraintContext, _args: BoostMoraleArgs): Constraint[] { + return [notBeNeutral(), notWanderingNation(), occupiedCity()]; + } + buildConstraints(_ctx: ConstraintContext, _args: BoostMoraleArgs): Constraint[] { const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0; - return [notBeNeutral(), reqGeneralGold(getRequiredGold)]; + return [notBeNeutral(), notWanderingNation(), occupiedCity(), reqGeneralCrew(), reqGeneralGold(getRequiredGold)]; } resolve( diff --git a/packages/logic/src/actions/turn/general/che_이동.ts b/packages/logic/src/actions/turn/general/che_이동.ts index 8dde473..25eb54e 100644 --- a/packages/logic/src/actions/turn/general/che_이동.ts +++ b/packages/logic/src/actions/turn/general/che_이동.ts @@ -160,6 +160,15 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(ctx: ConstraintContext, _args: MoveArgs): Constraint[] { + return [ + reqGeneralGold(() => { + const develCost = ctx.env.develCost as number; + return develCost ?? 0; + }), + ]; + } + buildConstraints(ctx: ConstraintContext, _args: MoveArgs): Constraint[] { const constraints = [ existsDestCity(), diff --git a/packages/logic/src/actions/turn/general/che_임관.ts b/packages/logic/src/actions/turn/general/che_임관.ts index c6232f9..0e6c015 100644 --- a/packages/logic/src/actions/turn/general/che_임관.ts +++ b/packages/logic/src/actions/turn/general/che_임관.ts @@ -42,6 +42,10 @@ export class ActionDefinition< return { destNationId }; } + buildMinConstraints(_ctx: ConstraintContext, _args: AppointmentArgs): Constraint[] { + return [beNeutral()]; + } + buildConstraints(_ctx: ConstraintContext, _args: AppointmentArgs): Constraint[] { return [beNeutral(), existsDestNation()]; } diff --git a/packages/logic/src/actions/turn/general/che_징병.ts b/packages/logic/src/actions/turn/general/che_징병.ts index b2a9a3d..96a27f1 100644 --- a/packages/logic/src/actions/turn/general/che_징병.ts +++ b/packages/logic/src/actions/turn/general/che_징병.ts @@ -419,6 +419,16 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: RecruitArgs): Constraint[] { + const minPopBase = this.env.minAvailableRecruitPop ?? DEFAULT_MIN_POP; + return [ + notBeNeutral(), + occupiedCity(), + reqCityCapacity('population', '주민', minPopBase + MIN_CREW), + reqCityTrust(20), + ]; + } + buildConstraints(ctx: ConstraintContext, _args: RecruitArgs): Constraint[] { const requirements: RequirementKey[] = [ { kind: 'arg', key: 'crewType' }, diff --git a/packages/logic/src/actions/turn/general/che_첩보.ts b/packages/logic/src/actions/turn/general/che_첩보.ts index eef6fa2..832fe17 100644 --- a/packages/logic/src/actions/turn/general/che_첩보.ts +++ b/packages/logic/src/actions/turn/general/che_첩보.ts @@ -131,6 +131,12 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(ctx: ConstraintContext, _args: SpyArgs): Constraint[] { + const env = ctx.env; + const cost = ((env.develCost as number) ?? 100) * 3; + return [notBeNeutral(), reqGeneralGold(() => cost), reqGeneralRice(() => cost)]; + } + buildConstraints(ctx: ConstraintContext, _args: SpyArgs): Constraint[] { const env = ctx.env; const cost = ((env.develCost as number) ?? 100) * 3; diff --git a/packages/logic/src/actions/turn/general/che_출병.ts b/packages/logic/src/actions/turn/general/che_출병.ts index 8cac45f..ec2dc27 100644 --- a/packages/logic/src/actions/turn/general/che_출병.ts +++ b/packages/logic/src/actions/turn/general/che_출병.ts @@ -260,6 +260,19 @@ export class ActionDefinition< return { destCityId }; } + buildMinConstraints(ctx: ConstraintContext, _args: DispatchArgs): Constraint[] { + const relYear = typeof ctx.env.relYear === 'number' ? ctx.env.relYear : 0; + const openingPartYear = typeof ctx.env.openingPartYear === 'number' ? ctx.env.openingPartYear : 0; + return [ + notOpeningPart(relYear + 2, openingPartYear), + notBeNeutral(), + occupiedCity(), + suppliedCity(), + reqGeneralCrew(), + reqGeneralRice(getRequiredRice), + ]; + } + buildConstraints(_ctx: ConstraintContext, _args: DispatchArgs): Constraint[] { const relYear = typeof _ctx.env.relYear === 'number' ? _ctx.env.relYear : 0; const openingPartYear = typeof _ctx.env.openingPartYear === 'number' ? _ctx.env.openingPartYear : 0; diff --git a/packages/logic/src/actions/turn/general/che_헌납.ts b/packages/logic/src/actions/turn/general/che_헌납.ts index 34ae73e..af7666f 100644 --- a/packages/logic/src/actions/turn/general/che_헌납.ts +++ b/packages/logic/src/actions/turn/general/che_헌납.ts @@ -3,8 +3,10 @@ import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/ import { notBeNeutral, notWanderingNation, + occupiedCity, reqGeneralGold, reqGeneralRice, + suppliedCity, } from '@sammo-ts/logic/constraints/presets.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { @@ -96,11 +98,15 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: DonateArgs): Constraint[] { + return [notBeNeutral(), occupiedCity(), suppliedCity()]; + } + buildConstraints(_ctx: ConstraintContext, args: DonateArgs): Constraint[] { if (args.isGold) { - return [notBeNeutral(), notWanderingNation(), reqGeneralGold(() => args.amount)]; + return [notBeNeutral(), notWanderingNation(), occupiedCity(), suppliedCity(), reqGeneralGold(() => args.amount)]; } - return [notBeNeutral(), notWanderingNation(), reqGeneralRice(() => args.amount)]; + return [notBeNeutral(), notWanderingNation(), occupiedCity(), suppliedCity(), reqGeneralRice(() => args.amount)]; } resolve(context: GeneralActionResolveContext, args: DonateArgs): GeneralActionOutcome { diff --git a/packages/logic/src/actions/turn/general/che_화계.ts b/packages/logic/src/actions/turn/general/che_화계.ts index e3792cb..3a284bb 100644 --- a/packages/logic/src/actions/turn/general/che_화계.ts +++ b/packages/logic/src/actions/turn/general/che_화계.ts @@ -362,6 +362,17 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: FireAttackArgs): Constraint[] { + const { gold, rice } = this.command.getCost(); + return [ + notBeNeutral(), + occupiedCity(), + suppliedCity(), + reqGeneralGold(() => gold), + reqGeneralRice(() => rice), + ]; + } + buildConstraints(_ctx: ConstraintContext, _args: FireAttackArgs): Constraint[] { void _ctx; void _args; diff --git a/packages/logic/src/actions/turn/general/che_훈련.ts b/packages/logic/src/actions/turn/general/che_훈련.ts index d6ed324..0953251 100644 --- a/packages/logic/src/actions/turn/general/che_훈련.ts +++ b/packages/logic/src/actions/turn/general/che_훈련.ts @@ -1,6 +1,6 @@ import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js'; -import { notBeNeutral, reqGeneralGold } from '@sammo-ts/logic/constraints/presets.js'; +import { notBeNeutral, notWanderingNation, occupiedCity, reqGeneralCrew, reqGeneralGold } from '@sammo-ts/logic/constraints/presets.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js'; import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'; @@ -36,9 +36,13 @@ export class ActionDefinition< return {}; } + buildMinConstraints(_ctx: ConstraintContext, _args: TrainingArgs): Constraint[] { + return [notBeNeutral(), notWanderingNation(), occupiedCity()]; + } + buildConstraints(_ctx: ConstraintContext, _args: TrainingArgs): Constraint[] { const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0; - return [notBeNeutral(), reqGeneralGold(getRequiredGold)]; + return [notBeNeutral(), notWanderingNation(), occupiedCity(), reqGeneralCrew(), reqGeneralGold(getRequiredGold)]; } resolve( diff --git a/packages/logic/src/actions/turn/nation/che_감축.ts b/packages/logic/src/actions/turn/nation/che_감축.ts index af5d91d..53a97b1 100644 --- a/packages/logic/src/actions/turn/nation/che_감축.ts +++ b/packages/logic/src/actions/turn/nation/che_감축.ts @@ -43,6 +43,10 @@ export class ActionDefinition< return {}; } + buildMinConstraints(_ctx: ConstraintContext, _args: ReduceCityArgs): Constraint[] { + return [occupiedCity(), beChief(), suppliedCity()]; + } + private getRecoverAmount(): number { return this.env.develCost * COST_COEF + DEFAULT_COST / 2; } diff --git a/packages/logic/src/actions/turn/nation/che_국호변경.ts b/packages/logic/src/actions/turn/nation/che_국호변경.ts index 6bdd96f..04e9ddd 100644 --- a/packages/logic/src/actions/turn/nation/che_국호변경.ts +++ b/packages/logic/src/actions/turn/nation/che_국호변경.ts @@ -32,6 +32,25 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: ChangeNationNameArgs): Constraint[] { + return [ + 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' }; + }, + }, + ]; + } + buildConstraints(_ctx: ConstraintContext, args: ChangeNationNameArgs): Constraint[] { return [ occupiedCity(), diff --git a/packages/logic/src/actions/turn/nation/che_급습.ts b/packages/logic/src/actions/turn/nation/che_급습.ts index 3f1cd07..0742bef 100644 --- a/packages/logic/src/actions/turn/nation/che_급습.ts +++ b/packages/logic/src/actions/turn/nation/che_급습.ts @@ -174,6 +174,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: RaidArgs): Constraint[] { + return [occupiedCity(), beChief(), availableStrategicCommand()]; + } + buildConstraints(_ctx: ConstraintContext, _args: RaidArgs): Constraint[] { void _ctx; void _args; diff --git a/packages/logic/src/actions/turn/nation/che_몰수.ts b/packages/logic/src/actions/turn/nation/che_몰수.ts index b11a855..bd5099d 100644 --- a/packages/logic/src/actions/turn/nation/che_몰수.ts +++ b/packages/logic/src/actions/turn/nation/che_몰수.ts @@ -4,6 +4,8 @@ import { beChief, existsDestGeneral, friendlyDestGeneral, + notBeNeutral, + notOpeningPart, occupiedCity, suppliedCity, } from '@sammo-ts/logic/constraints/presets.js'; @@ -60,10 +62,20 @@ export class ActionDefinition< }; } + buildMinConstraints(ctx: ConstraintContext, _args: SeizureArgs): Constraint[] { + const relYear = typeof ctx.env.relYear === 'number' ? ctx.env.relYear : 0; + const openingPartYear = typeof ctx.env.openingPartYear === 'number' ? ctx.env.openingPartYear : 0; + return [notBeNeutral(), occupiedCity(), beChief(), notOpeningPart(relYear, openingPartYear), suppliedCity()]; + } + buildConstraints(_ctx: ConstraintContext, args: SeizureArgs): Constraint[] { + const relYear = typeof _ctx.env.relYear === 'number' ? _ctx.env.relYear : 0; + const openingPartYear = typeof _ctx.env.openingPartYear === 'number' ? _ctx.env.openingPartYear : 0; return [ + notBeNeutral(), occupiedCity(), beChief(), + notOpeningPart(relYear, openingPartYear), suppliedCity(), existsDestGeneral(), friendlyDestGeneral(), diff --git a/packages/logic/src/actions/turn/nation/che_물자원조.ts b/packages/logic/src/actions/turn/nation/che_물자원조.ts index 51b57e5..91d5d93 100644 --- a/packages/logic/src/actions/turn/nation/che_물자원조.ts +++ b/packages/logic/src/actions/turn/nation/che_물자원조.ts @@ -51,6 +51,25 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: MaterialAidArgs): Constraint[] { + return [ + occupiedCity(), + beChief(), + suppliedCity(), + { + name: 'nationSurlimit', + 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 surlimitRaw = nation?.meta.surlimit; + const surlimit = typeof surlimitRaw === 'number' ? surlimitRaw : 0; + if (surlimit > 0) return { kind: 'deny', reason: '외교제한중입니다.' }; + return { kind: 'allow' }; + }, + }, + ]; + } + buildConstraints(_ctx: ConstraintContext, args: MaterialAidArgs): Constraint[] { const [goldAmount, riceAmount] = args.amountList; return [ diff --git a/packages/logic/src/actions/turn/nation/che_발령.ts b/packages/logic/src/actions/turn/nation/che_발령.ts index 0d23695..f1349ee 100644 --- a/packages/logic/src/actions/turn/nation/che_발령.ts +++ b/packages/logic/src/actions/turn/nation/che_발령.ts @@ -152,6 +152,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: AssignmentArgs): Constraint[] { + return [beChief(), notBeNeutral(), occupiedCity(), suppliedCity()]; + } + buildConstraints(ctx: ConstraintContext, _args: AssignmentArgs): Constraint[] { void _args; if (ctx.destGeneralId === ctx.actorId) { diff --git a/packages/logic/src/actions/turn/nation/che_백성동원.ts b/packages/logic/src/actions/turn/nation/che_백성동원.ts index 0990d50..04de81b 100644 --- a/packages/logic/src/actions/turn/nation/che_백성동원.ts +++ b/packages/logic/src/actions/turn/nation/che_백성동원.ts @@ -152,6 +152,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: MobilizePeopleArgs): Constraint[] { + return [occupiedCity(), beChief(), availableStrategicCommand()]; + } + buildConstraints(_ctx: ConstraintContext, _args: MobilizePeopleArgs): Constraint[] { void _ctx; void _args; diff --git a/packages/logic/src/actions/turn/nation/che_부대탈퇴지시.ts b/packages/logic/src/actions/turn/nation/che_부대탈퇴지시.ts index a0c4cef..acd0f39 100644 --- a/packages/logic/src/actions/turn/nation/che_부대탈퇴지시.ts +++ b/packages/logic/src/actions/turn/nation/che_부대탈퇴지시.ts @@ -48,6 +48,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: TroopKickArgs): Constraint[] { + return [notBeNeutral(), beChief()]; + } + buildConstraints(ctx: ConstraintContext, _args: TroopKickArgs): Constraint[] { if (ctx.destGeneralId !== undefined && ctx.destGeneralId === ctx.actorId) { return [alwaysFail('본인입니다')]; diff --git a/packages/logic/src/actions/turn/nation/che_불가침제의.ts b/packages/logic/src/actions/turn/nation/che_불가침제의.ts index a91abcc..1e6da57 100644 --- a/packages/logic/src/actions/turn/nation/che_불가침제의.ts +++ b/packages/logic/src/actions/turn/nation/che_불가침제의.ts @@ -97,6 +97,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: NonAggressionProposalArgs): Constraint[] { + return [beChief(), notBeNeutral()]; + } + buildConstraints(_ctx: ConstraintContext, _args: NonAggressionProposalArgs): Constraint[] { return [ beChief(), diff --git a/packages/logic/src/actions/turn/nation/che_불가침파기제의.ts b/packages/logic/src/actions/turn/nation/che_불가침파기제의.ts index d3ff046..f04e0df 100644 --- a/packages/logic/src/actions/turn/nation/che_불가침파기제의.ts +++ b/packages/logic/src/actions/turn/nation/che_불가침파기제의.ts @@ -40,6 +40,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: NonAggressionCancelProposalArgs): Constraint[] { + return [beChief(), notBeNeutral(), occupiedCity(), suppliedCity()]; + } + buildConstraints(_ctx: ConstraintContext, _args: NonAggressionCancelProposalArgs): Constraint[] { return [ beChief(), diff --git a/packages/logic/src/actions/turn/nation/che_선전포고.ts b/packages/logic/src/actions/turn/nation/che_선전포고.ts index 71fbe99..f4e7f1f 100644 --- a/packages/logic/src/actions/turn/nation/che_선전포고.ts +++ b/packages/logic/src/actions/turn/nation/che_선전포고.ts @@ -53,6 +53,24 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: DeclareWarArgs): Constraint[] { + const relYear = typeof _ctx.env.relYear === 'number' ? _ctx.env.relYear : 0; + return [ + notBeNeutral(), + occupiedCity(), + suppliedCity(), + beChief(), + { + name: 'declareWarYearLimit', + requires: () => [], + test: () => { + if (relYear >= 1) return { kind: 'allow' }; + return { kind: 'deny', reason: '초반제한 해제 2년전부터 가능합니다.' }; + }, + }, + ]; + } + buildConstraints(_ctx: ConstraintContext, _args: DeclareWarArgs): Constraint[] { const relYear = typeof _ctx.env.relYear === 'number' ? _ctx.env.relYear : 0; diff --git a/packages/logic/src/actions/turn/nation/che_수몰.ts b/packages/logic/src/actions/turn/nation/che_수몰.ts index f802ab9..2f16cb3 100644 --- a/packages/logic/src/actions/turn/nation/che_수몰.ts +++ b/packages/logic/src/actions/turn/nation/che_수몰.ts @@ -177,6 +177,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: FloodArgs): Constraint[] { + return [occupiedCity(), beChief(), availableStrategicCommand()]; + } + buildConstraints(_ctx: ConstraintContext, _args: FloodArgs): Constraint[] { void _ctx; void _args; diff --git a/packages/logic/src/actions/turn/nation/che_이호경식.ts b/packages/logic/src/actions/turn/nation/che_이호경식.ts index 8293ac1..1cbde62 100644 --- a/packages/logic/src/actions/turn/nation/che_이호경식.ts +++ b/packages/logic/src/actions/turn/nation/che_이호경식.ts @@ -181,6 +181,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: DegradeRelationsArgs): Constraint[] { + return [occupiedCity(), beChief(), availableStrategicCommand()]; + } + buildConstraints(_ctx: ConstraintContext, _args: DegradeRelationsArgs): Constraint[] { void _ctx; void _args; diff --git a/packages/logic/src/actions/turn/nation/che_종전제의.ts b/packages/logic/src/actions/turn/nation/che_종전제의.ts index 26dd9cb..4904756 100644 --- a/packages/logic/src/actions/turn/nation/che_종전제의.ts +++ b/packages/logic/src/actions/turn/nation/che_종전제의.ts @@ -40,6 +40,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: StopWarProposalArgs): Constraint[] { + return [beChief(), notBeNeutral(), occupiedCity(), suppliedCity()]; + } + buildConstraints(_ctx: ConstraintContext, _args: StopWarProposalArgs): Constraint[] { return [ beChief(), diff --git a/packages/logic/src/actions/turn/nation/che_증축.ts b/packages/logic/src/actions/turn/nation/che_증축.ts index 693560f..15f6823 100644 --- a/packages/logic/src/actions/turn/nation/che_증축.ts +++ b/packages/logic/src/actions/turn/nation/che_증축.ts @@ -48,6 +48,10 @@ export class ActionDefinition< return {}; } + buildMinConstraints(_ctx: ConstraintContext, _args: ExpandCityArgs): Constraint[] { + return [occupiedCity(), beChief(), suppliedCity()]; + } + private getCost(): number { return this.env.develCost * COST_COEF + DEFAULT_COST; } diff --git a/packages/logic/src/actions/turn/nation/che_천도.ts b/packages/logic/src/actions/turn/nation/che_천도.ts index 8e09115..aa3ee4a 100644 --- a/packages/logic/src/actions/turn/nation/che_천도.ts +++ b/packages/logic/src/actions/turn/nation/che_천도.ts @@ -77,6 +77,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: MoveCapitalArgs): Constraint[] { + return [occupiedCity(), beChief(), suppliedCity()]; + } + buildConstraints(_ctx: ConstraintContext, args: MoveCapitalArgs): Constraint[] { const develcost = this.env.develCost; diff --git a/packages/logic/src/actions/turn/nation/che_초토화.ts b/packages/logic/src/actions/turn/nation/che_초토화.ts index bf41798..c381834 100644 --- a/packages/logic/src/actions/turn/nation/che_초토화.ts +++ b/packages/logic/src/actions/turn/nation/che_초토화.ts @@ -116,6 +116,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: ScorchedEarthArgs): Constraint[] { + return [occupiedCity(), beChief(), suppliedCity(), requireNoDiplomacyLimit()]; + } + buildConstraints(_ctx: ConstraintContext, args: ScorchedEarthArgs): Constraint[] { void _ctx; return [ diff --git a/packages/logic/src/actions/turn/nation/che_포상.ts b/packages/logic/src/actions/turn/nation/che_포상.ts index 6b2cd52..64f2241 100644 --- a/packages/logic/src/actions/turn/nation/che_포상.ts +++ b/packages/logic/src/actions/turn/nation/che_포상.ts @@ -180,6 +180,10 @@ export class ActionDefinition< }; } + buildMinConstraints(_ctx: ConstraintContext, _args: AwardArgs): Constraint[] { + return [notBeNeutral(), occupiedCity(), beChief(), suppliedCity()]; + } + buildConstraints(ctx: ConstraintContext, args: AwardArgs): Constraint[] { const requirements: RequirementKey[] = []; if (ctx.cityId !== undefined) { diff --git a/packages/logic/src/actions/turn/nation/che_피장파장.ts b/packages/logic/src/actions/turn/nation/che_피장파장.ts index 0148942..719abe4 100644 --- a/packages/logic/src/actions/turn/nation/che_피장파장.ts +++ b/packages/logic/src/actions/turn/nation/che_피장파장.ts @@ -211,6 +211,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: CounterStrategyArgs): Constraint[] { + return [occupiedCity(), beChief()]; + } + buildConstraints(_ctx: ConstraintContext, _args: CounterStrategyArgs): Constraint[] { void _ctx; void _args; diff --git a/packages/logic/src/actions/turn/nation/che_허보.ts b/packages/logic/src/actions/turn/nation/che_허보.ts index ec60f59..9b62eda 100644 --- a/packages/logic/src/actions/turn/nation/che_허보.ts +++ b/packages/logic/src/actions/turn/nation/che_허보.ts @@ -184,6 +184,10 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: DeceptionArgs): Constraint[] { + return [occupiedCity(), beChief(), availableStrategicCommand()]; + } + buildConstraints(_ctx: ConstraintContext, _args: DeceptionArgs): Constraint[] { void _ctx; void _args; diff --git a/packages/logic/src/actions/turn/nation/cr_인구이동.ts b/packages/logic/src/actions/turn/nation/cr_인구이동.ts index 0a722cc..1eea7e5 100644 --- a/packages/logic/src/actions/turn/nation/cr_인구이동.ts +++ b/packages/logic/src/actions/turn/nation/cr_인구이동.ts @@ -65,6 +65,15 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: PopulationMoveArgs): Constraint[] { + return [ + occupiedCity(), + beChief(), + suppliedCity(), + reqCityCapacity('population', '주민', MIN_AVAILABLE_RECRUIT_POP + 100), + ]; + } + buildConstraints(_ctx: ConstraintContext, args: PopulationMoveArgs): Constraint[] { const cost = calcCost(this.env.develCost, args.amount); return [