From f3eaab1fa9f2ad060890decd1ec0a4aedc6b6c8d Mon Sep 17 00:00:00 2001 From: hided62 Date: Sun, 30 Aug 2026 23:35:23 +0000 Subject: [PATCH] =?UTF-8?q?=EC=98=88=EC=95=BD=20=EC=BB=A4=EB=A7=A8?= =?UTF-8?q?=EB=93=9C=20=EB=B9=84=EC=9A=A9=20=ED=91=9C=EC=8B=9C=20=EB=B3=B5?= =?UTF-8?q?=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/game-api/src/router/turns/index.ts | 1 + app/game-api/src/turns/commandTable.ts | 77 +++++++++++++++++-- app/game-api/test/commandTable.test.ts | 53 +++++++++++++ .../e2e/commandArguments.spec.ts | 19 +++-- .../src/components/command/types.ts | 1 + .../src/components/main/CommandSelectForm.vue | 11 +++ packages/logic/src/actions/definition.ts | 8 ++ .../src/actions/turn/general/che_기술연구.ts | 7 +- .../src/actions/turn/general/che_상업투자.ts | 6 ++ .../src/actions/turn/general/che_정착장려.ts | 8 +- .../src/actions/turn/general/che_주민선정.ts | 8 +- .../actions/turn/general/strategyCommand.ts | 5 ++ .../src/actions/turn/nation/eventResearch.ts | 6 +- 13 files changed, 193 insertions(+), 17 deletions(-) diff --git a/app/game-api/src/router/turns/index.ts b/app/game-api/src/router/turns/index.ts index 8d47ccbf..c2fe795c 100644 --- a/app/game-api/src/router/turns/index.ts +++ b/app/game-api/src/router/turns/index.ts @@ -446,6 +446,7 @@ export const getTurnCommandTable = async (ctx: GameApiContext, generalId: number nationGenerals, realNationCount: nations.filter((entry) => entry.id > 0).length, inputOptions, + generalActionModules: moduleBundle.general, }); }; diff --git a/app/game-api/src/turns/commandTable.ts b/app/game-api/src/turns/commandTable.ts index fc419b21..9c1ca284 100644 --- a/app/game-api/src/turns/commandTable.ts +++ b/app/game-api/src/turns/commandTable.ts @@ -40,6 +40,7 @@ export interface TurnCommandAvailability { key: string; name: string; turnDurationText?: string; + costText?: string; reqArg: boolean; possible: boolean; status: AvailabilityStatus; @@ -326,9 +327,12 @@ const resolveMaxNation = (worldState: WorldStateRow): number => { const buildCommandEnv = (worldState: WorldStateRow): CommandEnv => { const config = asRecord(worldState.config); const constValues = asRecord(config.const); + const meta = asRecord(worldState.meta); + const configuredDevelCost = resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0); return { - develCost: resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0), + // daemon은 월간 변동이 반영된 world.meta.develcost를 실행 직전에 우선한다. + develCost: resolveNumber(meta, ['develcost', 'develCost'], configuredDevelCost), trainDelta: resolveNumber(constValues, ['trainDelta'], 0), atmosDelta: resolveNumber(constValues, ['atmosDelta'], 0), trainSideEffectByAtmosTurn: resolveNumber(constValues, ['trainSideEffectByAtmosTurn'], 1), @@ -726,11 +730,65 @@ const getTurnDurationText = (definition: GeneralActionDefinition): string | unde return preReqTurn > 0 ? `${preReqTurn + 1}턴` : undefined; }; +const formatCost = (gold: number | undefined, rice: number | undefined): string | undefined => { + const parts = [ + gold && gold > 0 ? `금 ${gold.toLocaleString('ko-KR')}` : null, + rice && rice > 0 ? `쌀 ${rice.toLocaleString('ko-KR')}` : null, + ].filter((part): part is string => part !== null); + return parts.length > 0 ? parts.join(' · ') : undefined; +}; + +const getCommandCostText = ( + entry: CommandEntry, + ctx: ConstraintContext, + view: StateView, + env: CommandEnv +): string | undefined => { + const hint = entry.definition.getCostHint?.(ctx, view); + if (hint?.formula) return hint.formula; + const hintedCost = hint ? formatCost(hint.gold, hint.rice) : undefined; + if (hintedCost) return hintedCost; + + const develCost = env.develCost; + const general = view.get({ kind: 'general', id: ctx.actorId }) as General | null; + switch (entry.definition.key) { + case 'che_단련': + case 'che_숙련전환': + return formatCost(develCost, develCost); + case 'che_첩보': + return formatCost(develCost * 3, develCost * 3); + case 'che_이동': + case 'che_인재탐색': + return formatCost(develCost, 0); + case 'che_강행': + return formatCost(develCost * 5, 0); + case 'che_사기진작': + return formatCost(Math.round((general?.crew ?? 0) / 100), 0); + case 'che_출병': + return formatCost(0, Math.round((general?.crew ?? 0) / 100)); + case 'che_천도': + return `금·쌀 ${(develCost * 5).toLocaleString('ko-KR')} × 2^거리`; + case 'cr_인구이동': + return `금·쌀 ${develCost.toLocaleString('ko-KR')} × 인구[만]`; + case 'che_증축': { + const cost = develCost * 500 + 60_000; + return formatCost(cost, cost); + } + case 'che_감축': { + const recovery = develCost * 500 + 30_000; + return `금 ${recovery.toLocaleString('ko-KR')} · 쌀 ${recovery.toLocaleString('ko-KR')} 회수`; + } + default: + return undefined; + } +}; + const buildGroups = ( entries: CommandEntry[], ctx: ConstraintContext, view: StateView, - includeTurnDuration = false + includeTurnDuration = false, + env: CommandEnv ): TurnCommandGroup[] => { const groups = new Map(); @@ -739,10 +797,12 @@ const buildGroups = ( ? entry.evaluate(ctx, view) : evaluateDefinition(entry.definition, ctx, view, entry.reqArg, entry.availabilityArgs); const turnDurationText = includeTurnDuration ? getTurnDurationText(entry.definition) : undefined; + const costText = getCommandCostText(entry, ctx, view, env); const value: TurnCommandAvailability = { key: entry.definition.key, name: entry.definition.name, ...(turnDurationText ? { turnDurationText } : {}), + ...(costText ? { costText } : {}), reqArg: entry.reqArg, inputFields: entry.inputFields, ...availability, @@ -800,6 +860,7 @@ export const buildTurnCommandTable = async (options: { /** Ref's nation-table row count. Core callers must exclude synthetic id=0. */ realNationCount?: number; inputOptions?: TurnCommandInputOptions; + generalActionModules?: TurnCommandEnv['generalActionModules']; }): Promise => { // 턴 입력 화면에서 쓰는 사전 판단이므로 최소 정보로 가능/불가만 계산한다. const general = mapGeneralRow(options.general); @@ -818,7 +879,10 @@ export const buildTurnCommandTable = async (options: { mode: 'precheck', }; - const env = buildCommandEnv(options.worldState); + const env = { + ...buildCommandEnv(options.worldState), + ...(options.generalActionModules ? { generalActionModules: options.generalActionModules } : {}), + }; const scenarioConst = asRecord(options.worldState.config).const; const { general: generalSpecs, @@ -841,13 +905,16 @@ export const buildTurnCommandTable = async (options: { general: buildGroups( projectCommandGroups(generalEntries, generalGroups ?? REF_GENERAL_COMMAND_GROUPS), ctx, - view + view, + false, + env ), nation: buildGroups( projectCommandGroups(nationEntries, nationGroups ?? REF_NATION_COMMAND_GROUPS), ctx, view, - true + true, + env ), inputOptions: options.inputOptions ?? { cities: [], diff --git a/app/game-api/test/commandTable.test.ts b/app/game-api/test/commandTable.test.ts index 0236c799..45d3fe25 100644 --- a/app/game-api/test/commandTable.test.ts +++ b/app/game-api/test/commandTable.test.ts @@ -211,6 +211,29 @@ describe('buildTurnCommandTable', () => { expect(table.general.flatMap(({ values }) => values)).not.toContainEqual( expect.objectContaining({ turnDurationText: expect.any(String) }) ); + const generalCosts = Object.fromEntries( + table.general.flatMap(({ values }) => values.map(({ key, costText }) => [key, costText])) + ); + expect(generalCosts).toMatchObject({ + che_농지개간: '금 100', + che_기술연구: '금 100', + che_정착장려: '쌀 200', + che_주민선정: '쌀 200', + che_화계: '금 500 · 쌀 500', + che_첩보: '금 300 · 쌀 300', + che_사기진작: '금 1', + che_출병: '쌀 1', + che_이동: '금 100', + che_강행: '금 500', + }); + const nationCosts = Object.fromEntries( + table.nation.flatMap(({ values }) => values.map(({ key, costText }) => [key, costText])) + ); + expect(nationCosts).toMatchObject({ + che_천도: '금·쌀 500 × 2^거리', + che_증축: '금 110,000 · 쌀 110,000', + che_감축: '금 80,000 · 쌀 80,000 회수', + }); }); it('projects scenario-specific command categories instead of the default profile', async () => { @@ -239,6 +262,13 @@ describe('buildTurnCommandTable', () => { 'che_물자조달', 'cr_맹훈련', ]); + expect( + Object.fromEntries(table.nation.flatMap(({ values }) => values.map(({ key, costText }) => [key, costText]))) + ).toMatchObject({ + cr_인구이동: '금·쌀 100 × 인구[만]', + event_대검병연구: '금 50,000 · 쌀 50,000', + event_화륜차연구: '금 100,000 · 쌀 100,000', + }); expect(table.nation.map(({ category }) => category)).toEqual(['휴식', '특수', '연구']); expect(table.nation.flatMap(({ values }) => values.map(({ key }) => key))).toEqual([ '휴식', @@ -256,6 +286,29 @@ describe('buildTurnCommandTable', () => { }); }); + it('projects costs from the current world develcost used by command execution', async () => { + const worldState = buildWorldState(); + (worldState as unknown as { meta: Record }).meta.develcost = 120; + const table = await buildTurnCommandTable({ + worldState, + general: buildGeneral(), + city: buildCity(), + nation: buildNation(), + nationGenerals: null, + }); + const costs = Object.fromEntries( + [...table.general, ...table.nation].flatMap(({ values }) => + values.map(({ key, costText }) => [key, costText]) + ) + ); + expect(costs).toMatchObject({ + che_농지개간: '금 120', + che_화계: '금 600 · 쌀 600', + che_천도: '금·쌀 600 × 2^거리', + che_증축: '금 120,000 · 쌀 120,000', + }); + }); + it('projects the real 904/905/910/912 world command profiles into the API table', async () => { const buildScenarioTable = async (scenarioId: number) => { const scenario = await loadScenarioDefinitionById(scenarioId); diff --git a/app/game-frontend/e2e/commandArguments.spec.ts b/app/game-frontend/e2e/commandArguments.spec.ts index 4eac1e70..8f6f8aab 100644 --- a/app/game-frontend/e2e/commandArguments.spec.ts +++ b/app/game-frontend/e2e/commandArguments.spec.ts @@ -320,10 +320,11 @@ const buildGeneralCommand = (key: string, name: string) => ({ { key: 'destGeneralId', label: '대상 장수', kind: 'select', required: true, optionSource: 'generals' }, ], }); -const buildSimpleCommand = (key: string, name: string, turnDurationText?: string) => ({ +const buildSimpleCommand = (key: string, name: string, turnDurationText?: string, costText?: string) => ({ key, name, ...(turnDurationText ? { turnDurationText } : {}), + ...(costText ? { costText } : {}), reqArg: false, possible: true, status: 'available', @@ -662,9 +663,9 @@ const refChiefCommandTable = { category: '특수', values: [ buildSimpleCommand('che_초토화', '초토화', '3턴'), - buildSimpleCommand('che_천도', '천도', '1+거리×2턴'), - buildSimpleCommand('che_증축', '증축', '6턴'), - buildSimpleCommand('che_감축', '감축', '6턴'), + buildSimpleCommand('che_천도', '천도', '1+거리×2턴', '금·쌀 500 × 2^거리'), + buildSimpleCommand('che_증축', '증축', '6턴', '금 110,000 · 쌀 110,000'), + buildSimpleCommand('che_감축', '감축', '6턴', '금 80,000 · 쌀 80,000 회수'), ], }, { @@ -1342,9 +1343,9 @@ test('shows every Ref chief command in the exact category and command order', as await picker.getByRole('button', { name: '특수', exact: true }).click(); await expect(picker.locator('.command-grid .command-item')).toHaveText([ '초토화 /3턴', - '천도 /1+거리×2턴', - '증축 /6턴', - '감축 /6턴', + '천도 /1+거리×2턴금·쌀 500 × 2^거리', + '증축 /6턴금 110,000 · 쌀 110,000', + '감축 /6턴금 80,000 · 쌀 80,000 회수', ]); const durationGeometry = await picker.locator('.command-grid .command-item').evaluateAll((buttons) => buttons.map((button) => ({ @@ -1354,6 +1355,7 @@ test('shows every Ref chief command in the exact category and command order', as })) ); expect(durationGeometry.every(({ height, overflow }) => height >= 35 && overflow <= 0)).toBe(true); + await picker.screenshot({ path: test.info().outputPath('chief-command-costs-desktop-1200.png') }); await picker.getByRole('button', { name: '기타', exact: true }).click(); const rename = picker.getByRole('button', { name: '국호변경', exact: true }); await rename.hover(); @@ -1376,13 +1378,14 @@ test('shows every Ref chief command in the exact category and command order', as expect(mobileGeometry.horizontalOverflow).toBeLessThanOrEqual(0); expect(mobileGeometry.categoryColumns.split(' ')).toHaveLength(3); await mobilePicker.getByRole('button', { name: '특수', exact: true }).click(); - const mobileExpand = mobilePicker.getByRole('button', { name: '증축 /6턴', exact: true }); + const mobileExpand = mobilePicker.getByRole('button', { name: '증축 /6턴 금 110,000 · 쌀 110,000', exact: true }); await expect(mobileExpand).toBeVisible(); await mobileExpand.hover(); await mobileExpand.focus(); await expect(mobileExpand).toBeFocused(); await mobileExpand.dispatchEvent('pointerdown'); await expect(mobileExpand.locator('.command-duration')).toHaveText('/6턴'); + await expect(mobileExpand.locator('.command-cost')).toHaveText('금 110,000 · 쌀 110,000'); await mobileExpand.dispatchEvent('pointerup'); expect(await mobilePicker.evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(0); await mobilePicker.screenshot({ path: test.info().outputPath('ref-chief-command-list-mobile-500.png') }); diff --git a/app/game-frontend/src/components/command/types.ts b/app/game-frontend/src/components/command/types.ts index 343a09fb..555f0780 100644 --- a/app/game-frontend/src/components/command/types.ts +++ b/app/game-frontend/src/components/command/types.ts @@ -65,6 +65,7 @@ export type CommandAvailability = { key: string; name: string; turnDurationText?: string; + costText?: string; reqArg: boolean; status: 'available' | 'blocked' | 'needsInput' | 'unknown'; possible: boolean; diff --git a/app/game-frontend/src/components/main/CommandSelectForm.vue b/app/game-frontend/src/components/main/CommandSelectForm.vue index 86ee6df5..03e4654e 100644 --- a/app/game-frontend/src/components/main/CommandSelectForm.vue +++ b/app/game-frontend/src/components/main/CommandSelectForm.vue @@ -6,6 +6,7 @@ type CommandAvailability = { key: string; name: string; turnDurationText?: string; + costText?: string; reqArg: boolean; status: 'available' | 'blocked' | 'needsInput' | 'unknown'; possible: boolean; @@ -165,6 +166,7 @@ const commandTitle = (command: CommandAvailability) => /{{ command.turnDurationText }} + {{ command.costText }} @@ -255,6 +257,15 @@ const commandTitle = (command: CommandAvailability) => white-space: nowrap; } +.command-cost { + display: block; + color: #f3d58b; + font-size: 0.875em; + font-weight: 400; + line-height: 1.1; + overflow-wrap: anywhere; +} + .empty { color: rgba(232, 221, 196, 0.6); } diff --git a/packages/logic/src/actions/definition.ts b/packages/logic/src/actions/definition.ts index 5e773675..c6f3cc44 100644 --- a/packages/logic/src/actions/definition.ts +++ b/packages/logic/src/actions/definition.ts @@ -21,6 +21,8 @@ export interface GeneralActionDefinition< getPreReqTurn?(context: Context, args: Args): number; // 입력 시점에 대상이 정해져야 소요 턴을 계산할 수 있는 명령의 Ref 표시 문구. getTurnDurationHint?(): string; + // Ref getCommandDetailTitle()처럼 예약 목록에 현재 비용 또는 계산식을 투영한다. + getCostHint?(ctx: ConstraintContext, view: StateView): ActionCostHint | null; getPostReqTurn?(context: Context, args: Args): number; getStackSequence?(context: Context, args: Args): number | null; getProgressText?(context: Context, args: Args, term: number, termMax: number): string; @@ -28,3 +30,9 @@ export interface GeneralActionDefinition< getInheritanceActiveActionAmount?(context: Context, args: Args): number; resolve(context: Context, args: Args): GeneralActionOutcome; } + +export interface ActionCostHint { + gold?: number; + rice?: number; + formula?: string; +} diff --git a/packages/logic/src/actions/turn/general/che_기술연구.ts b/packages/logic/src/actions/turn/general/che_기술연구.ts index f6142803..4df6f1d7 100644 --- a/packages/logic/src/actions/turn/general/che_기술연구.ts +++ b/packages/logic/src/actions/turn/general/che_기술연구.ts @@ -8,7 +8,7 @@ import { reqGeneralRice, suppliedCity, } from '@sammo-ts/logic/constraints/presets.js'; -import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; +import type { ActionCostHint, GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionOutcome } from '@sammo-ts/logic/actions/engine.js'; import type { ActionContextBase, @@ -89,6 +89,11 @@ export class ActionDefinition< ]; } + getCostHint(ctx: ConstraintContext, view: StateView): ActionCostHint | null { + const context = buildDomesticContextFromView(ctx, view); + return context ? this.command.getCost(context) : null; + } + resolve(context: TechResearchContext, _args: TechResearchArgs): GeneralActionOutcome { const result = this.command.resolve(context, context.rng); let techScore = result.score; diff --git a/packages/logic/src/actions/turn/general/che_상업투자.ts b/packages/logic/src/actions/turn/general/che_상업투자.ts index 10f434d0..fdad4d5d 100644 --- a/packages/logic/src/actions/turn/general/che_상업투자.ts +++ b/packages/logic/src/actions/turn/general/che_상업투자.ts @@ -14,6 +14,7 @@ import { import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js'; import type { TriggerDomesticActionType } from '@sammo-ts/logic/actionModules/types.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; +import type { ActionCostHint } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionOutcome, GeneralActionResolver, @@ -448,6 +449,11 @@ export class ActionDefinition< ]; } + getCostHint(ctx: ConstraintContext, view: StateView): ActionCostHint | null { + const context = buildDomesticContextFromView(ctx, view); + return context ? this.command.getCost(context) : null; + } + resolve( context: GeneralActionResolveContext, args: CommerceInvestmentArgs diff --git a/packages/logic/src/actions/turn/general/che_정착장려.ts b/packages/logic/src/actions/turn/general/che_정착장려.ts index 708fb957..5abf1246 100644 --- a/packages/logic/src/actions/turn/general/che_정착장려.ts +++ b/packages/logic/src/actions/turn/general/che_정착장려.ts @@ -9,7 +9,7 @@ import { reqGeneralRice, suppliedCity, } from '@sammo-ts/logic/constraints/presets.js'; -import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; +import type { ActionCostHint, GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js'; import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'; import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js'; @@ -76,6 +76,12 @@ export class ActionDefinition< ]; } + getCostHint(ctx: ConstraintContext, view: StateView): ActionCostHint | null { + const context = buildDomesticContextFromView(ctx, view); + const cost = context ? this.command.getCost(context).gold : 0; + return context ? { rice: cost } : null; + } + resolve( context: GeneralActionResolveContext, _args: SettlementArgs diff --git a/packages/logic/src/actions/turn/general/che_주민선정.ts b/packages/logic/src/actions/turn/general/che_주민선정.ts index f00c75f2..2bf4c2f2 100644 --- a/packages/logic/src/actions/turn/general/che_주민선정.ts +++ b/packages/logic/src/actions/turn/general/che_주민선정.ts @@ -8,7 +8,7 @@ import { reqGeneralRice, suppliedCity, } from '@sammo-ts/logic/constraints/presets.js'; -import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; +import type { ActionCostHint, GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js'; import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'; import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js'; @@ -91,6 +91,12 @@ export class ActionDefinition< ]; } + getCostHint(ctx: ConstraintContext, view: StateView): ActionCostHint | null { + const context = buildDomesticContextFromView(ctx, view); + const cost = context ? this.command.getCost(context).gold : 0; + return context ? { rice: cost } : null; + } + resolve( context: GeneralActionResolveContext, _args: TrustActionArgs diff --git a/packages/logic/src/actions/turn/general/strategyCommand.ts b/packages/logic/src/actions/turn/general/strategyCommand.ts index 0c34928e..7eed48cf 100644 --- a/packages/logic/src/actions/turn/general/strategyCommand.ts +++ b/packages/logic/src/actions/turn/general/strategyCommand.ts @@ -1,6 +1,7 @@ import { JosaUtil, type RandomGenerator } from '@sammo-ts/common'; import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; +import type { ActionCostHint } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionEffect, GeneralActionOutcome, @@ -391,6 +392,10 @@ export class StrategyActionDefinition< return [notBeNeutral(), occupiedCity(), suppliedCity(), reqGeneralGold(() => gold), reqGeneralRice(() => rice)]; } + getCostHint(): ActionCostHint { + return this.command.getCost(); + } + buildConstraints(_ctx: ConstraintContext, _args: StrategyArgs): Constraint[] { const { gold, rice } = this.command.getCost(); return [ diff --git a/packages/logic/src/actions/turn/nation/eventResearch.ts b/packages/logic/src/actions/turn/nation/eventResearch.ts index 6c52e8ed..4a25c93a 100644 --- a/packages/logic/src/actions/turn/nation/eventResearch.ts +++ b/packages/logic/src/actions/turn/nation/eventResearch.ts @@ -2,7 +2,7 @@ import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entitie import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js'; import { allow, compareValues, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js'; import { beChief, occupiedCity, reqNationGold, reqNationRice } from '@sammo-ts/logic/constraints/presets.js'; -import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; +import type { ActionCostHint, GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js'; import { createLogEffect, createNationPatchEffect } from '@sammo-ts/logic/actions/engine.js'; import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js'; @@ -104,6 +104,10 @@ export const createEventResearchCommand = ( return PRE_REQ_TURN; } + getCostHint(): ActionCostHint { + return { gold: COST, rice: COST }; + } + resolve( context: GeneralActionResolveContext, _args: Record