From d7cb8c77e334fcfdb500c82633b5ee5c2204b9c7 Mon Sep 17 00:00:00 2001 From: Hide_D Date: Fri, 6 Feb 2026 18:26:21 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=97=AC=EB=9F=AC=20=EC=BB=A4=EB=A7=A8?= =?UTF-8?q?=EB=93=9C=EC=9D=98=20=EC=A0=9C=EC=95=BD=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=EC=A0=9C=EC=95=BD=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/actions/turn/general/che_강행.ts | 2 - .../src/actions/turn/general/che_거병.ts | 4 +- .../src/actions/turn/general/che_군량매매.ts | 12 ++- .../src/actions/turn/general/che_기술연구.ts | 10 +- .../turn/general/che_내정특기초기화.ts | 10 +- .../src/actions/turn/general/che_단련.ts | 8 +- .../src/actions/turn/general/che_사기진작.ts | 16 +++- .../src/actions/turn/general/che_숙련전환.ts | 6 ++ .../src/actions/turn/general/che_요양.ts | 6 +- .../src/actions/turn/general/che_은퇴.ts | 6 +- .../turn/general/che_전투특기초기화.ts | 10 +- .../src/actions/turn/general/che_정착장려.ts | 6 +- .../src/actions/turn/general/che_주민선정.ts | 2 + .../src/actions/turn/general/che_하야.ts | 4 +- .../src/actions/turn/general/che_헌납.ts | 5 +- .../src/actions/turn/general/che_화계.ts | 2 - .../src/actions/turn/general/che_훈련.ts | 17 +++- .../src/actions/turn/nation/eventResearch.ts | 18 +++- packages/logic/src/constraints/city.ts | 36 ++++++++ packages/logic/src/constraints/general.ts | 92 +++++++++++++++++++ 20 files changed, 230 insertions(+), 42 deletions(-) diff --git a/packages/logic/src/actions/turn/general/che_강행.ts b/packages/logic/src/actions/turn/general/che_강행.ts index de69aee..45202ff 100644 --- a/packages/logic/src/actions/turn/general/che_강행.ts +++ b/packages/logic/src/actions/turn/general/che_강행.ts @@ -5,7 +5,6 @@ import { nearCity, reqGeneralGold, reqGeneralRice, - existsDestCity, } from '@sammo-ts/logic/constraints/presets.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { @@ -173,7 +172,6 @@ export class ActionDefinition< buildConstraints(ctx: ConstraintContext, _args: ForcedMoveArgs): Constraint[] { return [ - existsDestCity(), notSameDestCity(), nearCity(3), reqGeneralGold((_c, _v) => { diff --git a/packages/logic/src/actions/turn/general/che_거병.ts b/packages/logic/src/actions/turn/general/che_거병.ts index d7226bd..327feef 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, 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 { allowJoinAction, beNeutral, beOpeningPart, 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'; import { @@ -33,7 +33,7 @@ export class ActionDefinition< } buildConstraints(_ctx: ConstraintContext, _args: UprisingArgs): Constraint[] { - return [beNeutral()]; + return [beNeutral(), beOpeningPart(), allowJoinAction(), noPenalty('noFoundNation')]; } resolve( diff --git a/packages/logic/src/actions/turn/general/che_군량매매.ts b/packages/logic/src/actions/turn/general/che_군량매매.ts index cfa2f09..8e5972f 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 } from '@sammo-ts/logic/constraints/types.js'; -import { occupiedCity, reqGeneralGold, reqGeneralRice, suppliedCity } from '@sammo-ts/logic/constraints/presets.js'; +import { + occupiedCity, + reqCityTrader, + 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'; @@ -39,11 +45,11 @@ export class ActionDefinition< } buildMinConstraints(_ctx: ConstraintContext, _args: TradeArgs): Constraint[] { - return [occupiedCity(), suppliedCity()]; + return [reqCityTrader(), occupiedCity({ allowNeutral: true }), suppliedCity()]; } buildConstraints(_ctx: ConstraintContext, args: TradeArgs): Constraint[] { - const constraints: Constraint[] = [occupiedCity(), suppliedCity()]; + const constraints: Constraint[] = [reqCityTrader(), occupiedCity({ allowNeutral: true }), 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 2c38fb4..e9bc098 100644 --- a/packages/logic/src/actions/turn/general/che_기술연구.ts +++ b/packages/logic/src/actions/turn/general/che_기술연구.ts @@ -5,6 +5,7 @@ import { notWanderingNation, occupiedCity, reqGeneralGold, + reqGeneralRice, suppliedCity, } from '@sammo-ts/logic/constraints/presets.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; @@ -52,7 +53,14 @@ export class ActionDefinition< buildConstraints(_ctx: ConstraintContext, _args: TechResearchArgs): Constraint[] { const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0; - return [notBeNeutral(), notWanderingNation(), occupiedCity(), suppliedCity(), reqGeneralGold(getRequiredGold)]; + return [ + notBeNeutral(), + notWanderingNation(), + occupiedCity(), + suppliedCity(), + reqGeneralGold(getRequiredGold), + reqGeneralRice(() => 0), + ]; } resolve( diff --git a/packages/logic/src/actions/turn/general/che_내정특기초기화.ts b/packages/logic/src/actions/turn/general/che_내정특기초기화.ts index 3482924..dcb21fb 100644 --- a/packages/logic/src/actions/turn/general/che_내정특기초기화.ts +++ b/packages/logic/src/actions/turn/general/che_내정특기초기화.ts @@ -15,8 +15,8 @@ const ACTION_NAME = '내정 특기 초기화'; const hasSpecial = (value: string | null | undefined): boolean => value !== null && value !== undefined && value !== 'None'; -const reqDomesticSpecial = (): Constraint => ({ - name: 'ReqGeneralDomesticSpecial', +const reqGeneralValue = (): Constraint => ({ + name: 'reqGeneralValue', requires: (ctx) => [{ kind: 'general', id: ctx.actorId }], test: (ctx, view) => { const general = readGeneral(ctx, view); @@ -42,8 +42,12 @@ export class ActionDefinition< return {}; } + buildMinConstraints(_ctx: ConstraintContext, _args: ResetSpecialDomesticArgs): Constraint[] { + return [reqGeneralValue()]; + } + buildConstraints(_ctx: ConstraintContext, _args: ResetSpecialDomesticArgs): Constraint[] { - return [reqDomesticSpecial()]; + return [reqGeneralValue()]; } resolve( diff --git a/packages/logic/src/actions/turn/general/che_단련.ts b/packages/logic/src/actions/turn/general/che_단련.ts index 5d70489..bcc253d 100644 --- a/packages/logic/src/actions/turn/general/che_단련.ts +++ b/packages/logic/src/actions/turn/general/che_단련.ts @@ -62,8 +62,8 @@ const pickByWeight = (rng: DrillContext['rng'], weights: Recor return last ? last[0] : first[0]; }; -const reqGeneralStat = (key: 'train' | 'atmos', label: string, minValue: number): Constraint => ({ - name: `ReqGeneral${label}`, +const reqGeneralValue = (key: 'train' | 'atmos', label: string, minValue: number): Constraint => ({ + name: 'reqGeneralValue', requires: (ctx) => [{ kind: 'general', id: ctx.actorId }], test: (ctx, view) => { const general = readGeneral(ctx, view); @@ -103,8 +103,8 @@ export class ActionDefinition< return [ notBeNeutral(), reqGeneralCrew(), - reqGeneralStat('train', '훈련', trainLow), - reqGeneralStat('atmos', '사기', atmosLow), + reqGeneralValue('train', '훈련', trainLow), + reqGeneralValue('atmos', '사기', atmosLow), reqGeneralGold(getRequiredGold), reqGeneralRice(getRequiredRice), ]; diff --git a/packages/logic/src/actions/turn/general/che_사기진작.ts b/packages/logic/src/actions/turn/general/che_사기진작.ts index f4b55ce..9cb3138 100644 --- a/packages/logic/src/actions/turn/general/che_사기진작.ts +++ b/packages/logic/src/actions/turn/general/che_사기진작.ts @@ -4,8 +4,10 @@ import { notBeNeutral, notWanderingNation, occupiedCity, + reqGeneralAtmosMargin, reqGeneralCrew, reqGeneralGold, + reqGeneralRice, } from '@sammo-ts/logic/constraints/presets.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js'; @@ -49,7 +51,19 @@ export class ActionDefinition< buildConstraints(_ctx: ConstraintContext, _args: BoostMoraleArgs): Constraint[] { const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0; - return [notBeNeutral(), notWanderingNation(), occupiedCity(), reqGeneralCrew(), reqGeneralGold(getRequiredGold)]; + const maxAtmos = + this.env.maxAtmosByCommand && this.env.maxAtmosByCommand > 0 + ? this.env.maxAtmosByCommand + : DEFAULT_MAX_ATMOS; + return [ + notBeNeutral(), + notWanderingNation(), + occupiedCity(), + reqGeneralCrew(), + reqGeneralGold(getRequiredGold), + reqGeneralRice(() => 0), + reqGeneralAtmosMargin(maxAtmos), + ]; } resolve( diff --git a/packages/logic/src/actions/turn/general/che_숙련전환.ts b/packages/logic/src/actions/turn/general/che_숙련전환.ts index 70eb299..98d9bfc 100644 --- a/packages/logic/src/actions/turn/general/che_숙련전환.ts +++ b/packages/logic/src/actions/turn/general/che_숙련전환.ts @@ -52,6 +52,12 @@ export class ActionDefinition< return parseArgsWithSchema(ARGS_SCHEMA, raw); } + buildMinConstraints(_ctx: ConstraintContext, _args: DexTransferArgs): Constraint[] { + const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0; + const getRequiredRice = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0; + return [notBeNeutral(), occupiedCity(), reqGeneralGold(getRequiredGold), reqGeneralRice(getRequiredRice)]; + } + buildConstraints(_ctx: ConstraintContext, _args: DexTransferArgs): Constraint[] { const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0; const getRequiredRice = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0; diff --git a/packages/logic/src/actions/turn/general/che_요양.ts b/packages/logic/src/actions/turn/general/che_요양.ts index bd58376..2e2f1e2 100644 --- a/packages/logic/src/actions/turn/general/che_요양.ts +++ b/packages/logic/src/actions/turn/general/che_요양.ts @@ -1,6 +1,5 @@ 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 type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js'; import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'; @@ -34,8 +33,7 @@ export class ActionDefinition< } buildConstraints(_ctx: ConstraintContext, _args: RecoveryArgs): Constraint[] { - const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0; - return [notBeNeutral(), reqGeneralGold(getRequiredGold)]; + return []; } resolve( diff --git a/packages/logic/src/actions/turn/general/che_은퇴.ts b/packages/logic/src/actions/turn/general/che_은퇴.ts index eaf905b..2499eeb 100644 --- a/packages/logic/src/actions/turn/general/che_은퇴.ts +++ b/packages/logic/src/actions/turn/general/che_은퇴.ts @@ -22,8 +22,8 @@ const ACTION_KEY = 'che_은퇴'; const REQ_AGE = 60; -const reqAge = (): Constraint => ({ - name: 'ReqAge', +const reqGeneralValue = (): Constraint => ({ + name: 'reqGeneralValue', requires: (ctx) => [{ kind: 'general', id: ctx.actorId }], test: (ctx, view) => { const generalKey: RequirementKey = { kind: 'general', id: ctx.actorId }; @@ -118,7 +118,7 @@ export class ActionDefinition< } buildConstraints(_ctx: ConstraintContext, _args: RetireArgs): Constraint[] { - return [reqAge()]; + return [reqGeneralValue()]; } resolve(context: GeneralActionResolveContext, args: RetireArgs): GeneralActionOutcome { diff --git a/packages/logic/src/actions/turn/general/che_전투특기초기화.ts b/packages/logic/src/actions/turn/general/che_전투특기초기화.ts index 44e9d5e..eb21ade 100644 --- a/packages/logic/src/actions/turn/general/che_전투특기초기화.ts +++ b/packages/logic/src/actions/turn/general/che_전투특기초기화.ts @@ -16,8 +16,8 @@ const ACTION_NAME = '전투 특기 초기화'; const hasSpecial = (value: string | null | undefined): boolean => value !== null && value !== undefined && value !== 'None'; -const reqWarSpecial = (): Constraint => ({ - name: 'ReqGeneralWarSpecial', +const reqGeneralValue = (): Constraint => ({ + name: 'reqGeneralValue', requires: (ctx) => [{ kind: 'general', id: ctx.actorId }], test: (ctx, view) => { const general = readGeneral(ctx, view); @@ -43,8 +43,12 @@ export class ActionDefinition< return {}; } + buildMinConstraints(_ctx: ConstraintContext, _args: ResetSpecialWarArgs): Constraint[] { + return [reqGeneralValue()]; + } + buildConstraints(_ctx: ConstraintContext, _args: ResetSpecialWarArgs): Constraint[] { - return [reqWarSpecial()]; + return [reqGeneralValue()]; } resolve( diff --git a/packages/logic/src/actions/turn/general/che_정착장려.ts b/packages/logic/src/actions/turn/general/che_정착장려.ts index 39059e1..6679f98 100644 --- a/packages/logic/src/actions/turn/general/che_정착장려.ts +++ b/packages/logic/src/actions/turn/general/che_정착장려.ts @@ -4,7 +4,8 @@ import { notBeNeutral, notWanderingNation, occupiedCity, - remainCityCapacityByMax, + remainCityCapacity, + reqGeneralGold, reqGeneralRice, suppliedCity, } from '@sammo-ts/logic/constraints/presets.js'; @@ -76,7 +77,8 @@ export class ActionDefinition< notWanderingNation(), occupiedCity(), suppliedCity(), - remainCityCapacityByMax('population', 'populationMax', '인구'), + reqGeneralGold(() => 0), + remainCityCapacity('population', '인구'), reqGeneralRice(getRequiredRice), ]; } diff --git a/packages/logic/src/actions/turn/general/che_주민선정.ts b/packages/logic/src/actions/turn/general/che_주민선정.ts index ec4ea8d..0e4bc1b 100644 --- a/packages/logic/src/actions/turn/general/che_주민선정.ts +++ b/packages/logic/src/actions/turn/general/che_주민선정.ts @@ -6,6 +6,7 @@ import { notBeNeutral, notWanderingNation, occupiedCity, + reqGeneralGold, reqGeneralRice, suppliedCity, } from '@sammo-ts/logic/constraints/presets.js'; @@ -134,6 +135,7 @@ export class ActionDefinition< notWanderingNation(), occupiedCity(), suppliedCity(), + reqGeneralGold(() => 0, requirements), reqGeneralRice(getRiceCost, requirements), remainCityTrust(), ]; diff --git a/packages/logic/src/actions/turn/general/che_하야.ts b/packages/logic/src/actions/turn/general/che_하야.ts index c51cfa1..c2470f6 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 { notBeNeutral } from '@sammo-ts/logic/constraints/presets.js'; +import { notBeNeutral, notLord } from '@sammo-ts/logic/constraints/presets.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionOutcome, @@ -106,7 +106,7 @@ export class ActionDefinition< } buildConstraints(_ctx: ConstraintContext, _args: ResignArgs): Constraint[] { - return [notBeNeutral()]; + return [notBeNeutral(), notLord()]; } resolve(context: GeneralActionResolveContext, args: ResignArgs): GeneralActionOutcome { diff --git a/packages/logic/src/actions/turn/general/che_헌납.ts b/packages/logic/src/actions/turn/general/che_헌납.ts index 4d65735..5b872f3 100644 --- a/packages/logic/src/actions/turn/general/che_헌납.ts +++ b/packages/logic/src/actions/turn/general/che_헌납.ts @@ -2,7 +2,6 @@ import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js'; import { notBeNeutral, - notWanderingNation, occupiedCity, reqGeneralGold, reqGeneralRice, @@ -107,9 +106,9 @@ export class ActionDefinition< buildConstraints(_ctx: ConstraintContext, args: DonateArgs): Constraint[] { if (args.isGold) { - return [notBeNeutral(), notWanderingNation(), occupiedCity(), suppliedCity(), reqGeneralGold(() => args.amount)]; + return [notBeNeutral(), occupiedCity(), suppliedCity(), reqGeneralGold(() => args.amount)]; } - return [notBeNeutral(), notWanderingNation(), occupiedCity(), suppliedCity(), reqGeneralRice(() => args.amount)]; + return [notBeNeutral(), 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 1ba695e..128e26e 100644 --- a/packages/logic/src/actions/turn/general/che_화계.ts +++ b/packages/logic/src/actions/turn/general/che_화계.ts @@ -10,7 +10,6 @@ import type { import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js'; import { disallowDiplomacyBetweenStatus, - existsDestCity, notBeNeutral, notNeutralDestCity, notOccupiedDestCity, @@ -393,7 +392,6 @@ export class ActionDefinition< notBeNeutral(), occupiedCity(), suppliedCity(), - existsDestCity(), notOccupiedDestCity(), notNeutralDestCity(), reqGeneralGold(() => gold), diff --git a/packages/logic/src/actions/turn/general/che_훈련.ts b/packages/logic/src/actions/turn/general/che_훈련.ts index dc138d4..18eeca5 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, notWanderingNation, occupiedCity, reqGeneralCrew, reqGeneralGold } from '@sammo-ts/logic/constraints/presets.js'; +import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js'; +import { + notBeNeutral, + notWanderingNation, + occupiedCity, + reqGeneralCrew, + reqGeneralTrainMargin, +} 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'; @@ -42,8 +48,11 @@ export class ActionDefinition< } buildConstraints(_ctx: ConstraintContext, _args: TrainingArgs): Constraint[] { - const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0; - return [notBeNeutral(), notWanderingNation(), occupiedCity(), reqGeneralCrew(), reqGeneralGold(getRequiredGold)]; + const maxTrain = + this.env.maxTrainByCommand && this.env.maxTrainByCommand > 0 + ? this.env.maxTrainByCommand + : DEFAULT_MAX_TRAIN; + return [notBeNeutral(), notWanderingNation(), occupiedCity(), reqGeneralCrew(), reqGeneralTrainMargin(maxTrain)]; } resolve( diff --git a/packages/logic/src/actions/turn/nation/eventResearch.ts b/packages/logic/src/actions/turn/nation/eventResearch.ts index ee5717c..bdbcde8 100644 --- a/packages/logic/src/actions/turn/nation/eventResearch.ts +++ b/packages/logic/src/actions/turn/nation/eventResearch.ts @@ -24,8 +24,8 @@ export interface EventResearchConfig { category?: string; } -const requireNationAux = (auxKey: string, actionName: string): Constraint => ({ - name: 'requireNationAux', +const reqNationAuxValue = (auxKey: string, actionName: string): Constraint => ({ + name: 'reqNationAuxValue', requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []), test: (ctx: ConstraintContext, view: StateView) => { if (ctx.nationId === undefined) { @@ -69,13 +69,25 @@ export const createEventResearchCommand = (config: EventResearchConfig): { return {}; } + buildMinConstraints(_ctx: ConstraintContext, _args: Record): Constraint[] { + void _ctx; + void _args; + return [ + occupiedCity(), + beChief(), + reqNationAuxValue(config.auxKey, ACTION_NAME), + reqNationGold(() => this.env.baseGold + COST), + reqNationRice(() => this.env.baseRice + COST), + ]; + } + buildConstraints(_ctx: ConstraintContext, _args: Record): Constraint[] { void _ctx; void _args; return [ occupiedCity(), beChief(), - requireNationAux(config.auxKey, ACTION_NAME), + reqNationAuxValue(config.auxKey, ACTION_NAME), reqNationGold(() => this.env.baseGold + COST), reqNationRice(() => this.env.baseRice + COST), ]; diff --git a/packages/logic/src/constraints/city.ts b/packages/logic/src/constraints/city.ts index 93c6098..47ee1ba 100644 --- a/packages/logic/src/constraints/city.ts +++ b/packages/logic/src/constraints/city.ts @@ -109,6 +109,42 @@ export const suppliedCity = (): Constraint => ({ }, }); +export const reqCityTrader = (): Constraint => ({ + name: 'reqCityTrader', + requires: (ctx) => { + const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }]; + if (ctx.cityId !== undefined) { + reqs.push({ kind: 'city', id: ctx.cityId }); + } + 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 city = readCity(view, ctx.cityId); + if (!city) { + if (ctx.cityId === undefined) { + return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); + } + const req: RequirementKey = { kind: 'city', id: ctx.cityId }; + return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); + } + + const trade = readMetaNumberFromUnknown(city.meta, 'trade'); + if (trade !== null || general.npcState >= 2) { + return allow(); + } + return { kind: 'deny', reason: '도시에 상인이 없습니다.' }; + }, +}); + export const suppliedDestCity = (): Constraint => ({ name: 'suppliedDestCity', requires: (ctx) => diff --git a/packages/logic/src/constraints/general.ts b/packages/logic/src/constraints/general.ts index 2697795..c424383 100644 --- a/packages/logic/src/constraints/general.ts +++ b/packages/logic/src/constraints/general.ts @@ -145,6 +145,98 @@ export const reqGeneralCrew = (): Constraint => ({ }, }); +export const reqGeneralTrainMargin = (maxTrain: number): Constraint => ({ + name: 'reqGeneralTrainMargin', + requires: (ctx) => [{ kind: 'general', id: ctx.actorId }], + 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], '장수 정보가 없습니다.'); + } + if (general.train < maxTrain) { + return allow(); + } + return { kind: 'deny', reason: '병사들은 이미 정예병사들입니다.' }; + }, +}); + +export const reqGeneralAtmosMargin = (maxAtmos: number): Constraint => ({ + name: 'reqGeneralAtmosMargin', + requires: (ctx) => [{ kind: 'general', id: ctx.actorId }], + 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], '장수 정보가 없습니다.'); + } + if (general.atmos < maxAtmos) { + return allow(); + } + return { kind: 'deny', reason: '이미 사기는 하늘을 찌를듯 합니다.' }; + }, +}); + +export const allowJoinAction = (): Constraint => ({ + name: 'allowJoinAction', + requires: (ctx) => [{ kind: 'general', id: ctx.actorId }], + 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 makelimit = typeof general.meta.makelimit === 'number' ? general.meta.makelimit : 0; + if (makelimit === 0) { + return allow(); + } + + const joinActionLimit = typeof ctx.env.joinActionLimit === 'number' ? ctx.env.joinActionLimit : 12; + return { kind: 'deny', reason: `재야가 된지 ${joinActionLimit}턴이 지나야 합니다.` }; + }, +}); + +export const noPenalty = (penaltyKey: string): Constraint => ({ + name: 'noPenalty', + requires: (ctx) => [{ kind: 'general', id: ctx.actorId }], + 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 penalty = general.meta.penalty; + if (!penalty || typeof penalty !== 'object' || Array.isArray(penalty)) { + return allow(); + } + + const penaltyMap = penalty as Record; + if (!Object.prototype.hasOwnProperty.call(penaltyMap, penaltyKey)) { + return allow(); + } + + const reason = penaltyMap[penaltyKey]; + return { + kind: 'deny', + reason: `징계 사유: ${typeof reason === 'string' ? reason : String(reason)}`, + }; + }, +}); + export const reqGeneralCrewMargin = ( getCrewTypeId: (ctx: ConstraintContext, view: StateView) => number | null, requirements: RequirementKey[] = []