예약 커맨드 비용 표시 복원

This commit is contained in:
2026-08-30 23:35:23 +00:00
parent 71b9449903
commit f3eaab1fa9
13 changed files with 193 additions and 17 deletions
+1
View File
@@ -446,6 +446,7 @@ export const getTurnCommandTable = async (ctx: GameApiContext, generalId: number
nationGenerals, nationGenerals,
realNationCount: nations.filter((entry) => entry.id > 0).length, realNationCount: nations.filter((entry) => entry.id > 0).length,
inputOptions, inputOptions,
generalActionModules: moduleBundle.general,
}); });
}; };
+72 -5
View File
@@ -40,6 +40,7 @@ export interface TurnCommandAvailability {
key: string; key: string;
name: string; name: string;
turnDurationText?: string; turnDurationText?: string;
costText?: string;
reqArg: boolean; reqArg: boolean;
possible: boolean; possible: boolean;
status: AvailabilityStatus; status: AvailabilityStatus;
@@ -326,9 +327,12 @@ const resolveMaxNation = (worldState: WorldStateRow): number => {
const buildCommandEnv = (worldState: WorldStateRow): CommandEnv => { const buildCommandEnv = (worldState: WorldStateRow): CommandEnv => {
const config = asRecord(worldState.config); const config = asRecord(worldState.config);
const constValues = asRecord(config.const); const constValues = asRecord(config.const);
const meta = asRecord(worldState.meta);
const configuredDevelCost = resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0);
return { return {
develCost: resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0), // daemon은 월간 변동이 반영된 world.meta.develcost를 실행 직전에 우선한다.
develCost: resolveNumber(meta, ['develcost', 'develCost'], configuredDevelCost),
trainDelta: resolveNumber(constValues, ['trainDelta'], 0), trainDelta: resolveNumber(constValues, ['trainDelta'], 0),
atmosDelta: resolveNumber(constValues, ['atmosDelta'], 0), atmosDelta: resolveNumber(constValues, ['atmosDelta'], 0),
trainSideEffectByAtmosTurn: resolveNumber(constValues, ['trainSideEffectByAtmosTurn'], 1), trainSideEffectByAtmosTurn: resolveNumber(constValues, ['trainSideEffectByAtmosTurn'], 1),
@@ -726,11 +730,65 @@ const getTurnDurationText = (definition: GeneralActionDefinition): string | unde
return preReqTurn > 0 ? `${preReqTurn + 1}` : undefined; 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 = ( const buildGroups = (
entries: CommandEntry[], entries: CommandEntry[],
ctx: ConstraintContext, ctx: ConstraintContext,
view: StateView, view: StateView,
includeTurnDuration = false includeTurnDuration = false,
env: CommandEnv
): TurnCommandGroup[] => { ): TurnCommandGroup[] => {
const groups = new Map<string, TurnCommandAvailability[]>(); const groups = new Map<string, TurnCommandAvailability[]>();
@@ -739,10 +797,12 @@ const buildGroups = (
? entry.evaluate(ctx, view) ? entry.evaluate(ctx, view)
: evaluateDefinition(entry.definition, ctx, view, entry.reqArg, entry.availabilityArgs); : evaluateDefinition(entry.definition, ctx, view, entry.reqArg, entry.availabilityArgs);
const turnDurationText = includeTurnDuration ? getTurnDurationText(entry.definition) : undefined; const turnDurationText = includeTurnDuration ? getTurnDurationText(entry.definition) : undefined;
const costText = getCommandCostText(entry, ctx, view, env);
const value: TurnCommandAvailability = { const value: TurnCommandAvailability = {
key: entry.definition.key, key: entry.definition.key,
name: entry.definition.name, name: entry.definition.name,
...(turnDurationText ? { turnDurationText } : {}), ...(turnDurationText ? { turnDurationText } : {}),
...(costText ? { costText } : {}),
reqArg: entry.reqArg, reqArg: entry.reqArg,
inputFields: entry.inputFields, inputFields: entry.inputFields,
...availability, ...availability,
@@ -800,6 +860,7 @@ export const buildTurnCommandTable = async (options: {
/** Ref's nation-table row count. Core callers must exclude synthetic id=0. */ /** Ref's nation-table row count. Core callers must exclude synthetic id=0. */
realNationCount?: number; realNationCount?: number;
inputOptions?: TurnCommandInputOptions; inputOptions?: TurnCommandInputOptions;
generalActionModules?: TurnCommandEnv['generalActionModules'];
}): Promise<TurnCommandTable> => { }): Promise<TurnCommandTable> => {
// 턴 입력 화면에서 쓰는 사전 판단이므로 최소 정보로 가능/불가만 계산한다. // 턴 입력 화면에서 쓰는 사전 판단이므로 최소 정보로 가능/불가만 계산한다.
const general = mapGeneralRow(options.general); const general = mapGeneralRow(options.general);
@@ -818,7 +879,10 @@ export const buildTurnCommandTable = async (options: {
mode: 'precheck', 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 scenarioConst = asRecord(options.worldState.config).const;
const { const {
general: generalSpecs, general: generalSpecs,
@@ -841,13 +905,16 @@ export const buildTurnCommandTable = async (options: {
general: buildGroups( general: buildGroups(
projectCommandGroups(generalEntries, generalGroups ?? REF_GENERAL_COMMAND_GROUPS), projectCommandGroups(generalEntries, generalGroups ?? REF_GENERAL_COMMAND_GROUPS),
ctx, ctx,
view view,
false,
env
), ),
nation: buildGroups( nation: buildGroups(
projectCommandGroups(nationEntries, nationGroups ?? REF_NATION_COMMAND_GROUPS), projectCommandGroups(nationEntries, nationGroups ?? REF_NATION_COMMAND_GROUPS),
ctx, ctx,
view, view,
true true,
env
), ),
inputOptions: options.inputOptions ?? { inputOptions: options.inputOptions ?? {
cities: [], cities: [],
+53
View File
@@ -211,6 +211,29 @@ describe('buildTurnCommandTable', () => {
expect(table.general.flatMap(({ values }) => values)).not.toContainEqual( expect(table.general.flatMap(({ values }) => values)).not.toContainEqual(
expect.objectContaining({ turnDurationText: expect.any(String) }) 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 () => { it('projects scenario-specific command categories instead of the default profile', async () => {
@@ -239,6 +262,13 @@ describe('buildTurnCommandTable', () => {
'che_물자조달', 'che_물자조달',
'cr_맹훈련', '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.map(({ category }) => category)).toEqual(['휴식', '특수', '연구']);
expect(table.nation.flatMap(({ values }) => values.map(({ key }) => key))).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<string, unknown> }).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 () => { it('projects the real 904/905/910/912 world command profiles into the API table', async () => {
const buildScenarioTable = async (scenarioId: number) => { const buildScenarioTable = async (scenarioId: number) => {
const scenario = await loadScenarioDefinitionById(scenarioId); const scenario = await loadScenarioDefinitionById(scenarioId);
+11 -8
View File
@@ -320,10 +320,11 @@ const buildGeneralCommand = (key: string, name: string) => ({
{ key: 'destGeneralId', label: '대상 장수', kind: 'select', required: true, optionSource: 'generals' }, { 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, key,
name, name,
...(turnDurationText ? { turnDurationText } : {}), ...(turnDurationText ? { turnDurationText } : {}),
...(costText ? { costText } : {}),
reqArg: false, reqArg: false,
possible: true, possible: true,
status: 'available', status: 'available',
@@ -662,9 +663,9 @@ const refChiefCommandTable = {
category: '특수', category: '특수',
values: [ values: [
buildSimpleCommand('che_초토화', '초토화', '3턴'), buildSimpleCommand('che_초토화', '초토화', '3턴'),
buildSimpleCommand('che_천도', '천도', '1+거리×2턴'), buildSimpleCommand('che_천도', '천도', '1+거리×2턴', '금·쌀 500 × 2^거리'),
buildSimpleCommand('che_증축', '증축', '6턴'), buildSimpleCommand('che_증축', '증축', '6턴', '금 110,000 · 쌀 110,000'),
buildSimpleCommand('che_감축', '감축', '6턴'), 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 picker.getByRole('button', { name: '특수', exact: true }).click();
await expect(picker.locator('.command-grid .command-item')).toHaveText([ await expect(picker.locator('.command-grid .command-item')).toHaveText([
'초토화 /3턴', '초토화 /3턴',
'천도 /1+거리×2턴', '천도 /1+거리×2턴금·쌀 500 × 2^거리',
'증축 /6턴', '증축 /6턴금 110,000 · 쌀 110,000',
'감축 /6턴', '감축 /6턴금 80,000 · 쌀 80,000 회수',
]); ]);
const durationGeometry = await picker.locator('.command-grid .command-item').evaluateAll((buttons) => const durationGeometry = await picker.locator('.command-grid .command-item').evaluateAll((buttons) =>
buttons.map((button) => ({ 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); 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(); await picker.getByRole('button', { name: '기타', exact: true }).click();
const rename = picker.getByRole('button', { name: '국호변경', exact: true }); const rename = picker.getByRole('button', { name: '국호변경', exact: true });
await rename.hover(); 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.horizontalOverflow).toBeLessThanOrEqual(0);
expect(mobileGeometry.categoryColumns.split(' ')).toHaveLength(3); expect(mobileGeometry.categoryColumns.split(' ')).toHaveLength(3);
await mobilePicker.getByRole('button', { name: '특수', exact: true }).click(); 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 expect(mobileExpand).toBeVisible();
await mobileExpand.hover(); await mobileExpand.hover();
await mobileExpand.focus(); await mobileExpand.focus();
await expect(mobileExpand).toBeFocused(); await expect(mobileExpand).toBeFocused();
await mobileExpand.dispatchEvent('pointerdown'); await mobileExpand.dispatchEvent('pointerdown');
await expect(mobileExpand.locator('.command-duration')).toHaveText('/6턴'); await expect(mobileExpand.locator('.command-duration')).toHaveText('/6턴');
await expect(mobileExpand.locator('.command-cost')).toHaveText('금 110,000 · 쌀 110,000');
await mobileExpand.dispatchEvent('pointerup'); await mobileExpand.dispatchEvent('pointerup');
expect(await mobilePicker.evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(0); 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') }); await mobilePicker.screenshot({ path: test.info().outputPath('ref-chief-command-list-mobile-500.png') });
@@ -65,6 +65,7 @@ export type CommandAvailability = {
key: string; key: string;
name: string; name: string;
turnDurationText?: string; turnDurationText?: string;
costText?: string;
reqArg: boolean; reqArg: boolean;
status: 'available' | 'blocked' | 'needsInput' | 'unknown'; status: 'available' | 'blocked' | 'needsInput' | 'unknown';
possible: boolean; possible: boolean;
@@ -6,6 +6,7 @@ type CommandAvailability = {
key: string; key: string;
name: string; name: string;
turnDurationText?: string; turnDurationText?: string;
costText?: string;
reqArg: boolean; reqArg: boolean;
status: 'available' | 'blocked' | 'needsInput' | 'unknown'; status: 'available' | 'blocked' | 'needsInput' | 'unknown';
possible: boolean; possible: boolean;
@@ -165,6 +166,7 @@ const commandTitle = (command: CommandAvailability) =>
<small v-if="command.turnDurationText" class="command-duration"> <small v-if="command.turnDurationText" class="command-duration">
/{{ command.turnDurationText }} /{{ command.turnDurationText }}
</small> </small>
<small v-if="command.costText" class="command-cost">{{ command.costText }}</small>
</button> </button>
</div> </div>
</div> </div>
@@ -255,6 +257,15 @@ const commandTitle = (command: CommandAvailability) =>
white-space: nowrap; white-space: nowrap;
} }
.command-cost {
display: block;
color: #f3d58b;
font-size: 0.875em;
font-weight: 400;
line-height: 1.1;
overflow-wrap: anywhere;
}
.empty { .empty {
color: rgba(232, 221, 196, 0.6); color: rgba(232, 221, 196, 0.6);
} }
+8
View File
@@ -21,6 +21,8 @@ export interface GeneralActionDefinition<
getPreReqTurn?(context: Context, args: Args): number; getPreReqTurn?(context: Context, args: Args): number;
// 입력 시점에 대상이 정해져야 소요 턴을 계산할 수 있는 명령의 Ref 표시 문구. // 입력 시점에 대상이 정해져야 소요 턴을 계산할 수 있는 명령의 Ref 표시 문구.
getTurnDurationHint?(): string; getTurnDurationHint?(): string;
// Ref getCommandDetailTitle()처럼 예약 목록에 현재 비용 또는 계산식을 투영한다.
getCostHint?(ctx: ConstraintContext, view: StateView): ActionCostHint | null;
getPostReqTurn?(context: Context, args: Args): number; getPostReqTurn?(context: Context, args: Args): number;
getStackSequence?(context: Context, args: Args): number | null; getStackSequence?(context: Context, args: Args): number | null;
getProgressText?(context: Context, args: Args, term: number, termMax: number): string; getProgressText?(context: Context, args: Args, term: number, termMax: number): string;
@@ -28,3 +30,9 @@ export interface GeneralActionDefinition<
getInheritanceActiveActionAmount?(context: Context, args: Args): number; getInheritanceActiveActionAmount?(context: Context, args: Args): number;
resolve(context: Context, args: Args): GeneralActionOutcome<TriggerState>; resolve(context: Context, args: Args): GeneralActionOutcome<TriggerState>;
} }
export interface ActionCostHint {
gold?: number;
rice?: number;
formula?: string;
}
@@ -8,7 +8,7 @@ import {
reqGeneralRice, reqGeneralRice,
suppliedCity, suppliedCity,
} from '@sammo-ts/logic/constraints/presets.js'; } 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 { GeneralActionOutcome } from '@sammo-ts/logic/actions/engine.js';
import type { import type {
ActionContextBase, ActionContextBase,
@@ -89,6 +89,11 @@ export class ActionDefinition<
]; ];
} }
getCostHint(ctx: ConstraintContext, view: StateView): ActionCostHint | null {
const context = buildDomesticContextFromView<TriggerState>(ctx, view);
return context ? this.command.getCost(context) : null;
}
resolve(context: TechResearchContext<TriggerState>, _args: TechResearchArgs): GeneralActionOutcome<TriggerState> { resolve(context: TechResearchContext<TriggerState>, _args: TechResearchArgs): GeneralActionOutcome<TriggerState> {
const result = this.command.resolve(context, context.rng); const result = this.command.resolve(context, context.rng);
let techScore = result.score; let techScore = result.score;
@@ -14,6 +14,7 @@ import {
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js'; import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
import type { TriggerDomesticActionType } from '@sammo-ts/logic/actionModules/types.js'; import type { TriggerDomesticActionType } from '@sammo-ts/logic/actionModules/types.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type { ActionCostHint } from '@sammo-ts/logic/actions/definition.js';
import type { import type {
GeneralActionOutcome, GeneralActionOutcome,
GeneralActionResolver, GeneralActionResolver,
@@ -448,6 +449,11 @@ export class ActionDefinition<
]; ];
} }
getCostHint(ctx: ConstraintContext, view: StateView): ActionCostHint | null {
const context = buildDomesticContextFromView<TriggerState>(ctx, view);
return context ? this.command.getCost(context) : null;
}
resolve( resolve(
context: GeneralActionResolveContext<TriggerState>, context: GeneralActionResolveContext<TriggerState>,
args: CommerceInvestmentArgs args: CommerceInvestmentArgs
@@ -9,7 +9,7 @@ import {
reqGeneralRice, reqGeneralRice,
suppliedCity, suppliedCity,
} from '@sammo-ts/logic/constraints/presets.js'; } 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 { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'; import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.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<TriggerState>(ctx, view);
const cost = context ? this.command.getCost(context).gold : 0;
return context ? { rice: cost } : null;
}
resolve( resolve(
context: GeneralActionResolveContext<TriggerState>, context: GeneralActionResolveContext<TriggerState>,
_args: SettlementArgs _args: SettlementArgs
@@ -8,7 +8,7 @@ import {
reqGeneralRice, reqGeneralRice,
suppliedCity, suppliedCity,
} from '@sammo-ts/logic/constraints/presets.js'; } 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 { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'; import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.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<TriggerState>(ctx, view);
const cost = context ? this.command.getCost(context).gold : 0;
return context ? { rice: cost } : null;
}
resolve( resolve(
context: GeneralActionResolveContext<TriggerState>, context: GeneralActionResolveContext<TriggerState>,
_args: TrustActionArgs _args: TrustActionArgs
@@ -1,6 +1,7 @@
import { JosaUtil, type RandomGenerator } from '@sammo-ts/common'; import { JosaUtil, type RandomGenerator } from '@sammo-ts/common';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js'; import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type { ActionCostHint } from '@sammo-ts/logic/actions/definition.js';
import type { import type {
GeneralActionEffect, GeneralActionEffect,
GeneralActionOutcome, GeneralActionOutcome,
@@ -391,6 +392,10 @@ export class StrategyActionDefinition<
return [notBeNeutral(), occupiedCity(), suppliedCity(), reqGeneralGold(() => gold), reqGeneralRice(() => rice)]; return [notBeNeutral(), occupiedCity(), suppliedCity(), reqGeneralGold(() => gold), reqGeneralRice(() => rice)];
} }
getCostHint(): ActionCostHint {
return this.command.getCost();
}
buildConstraints(_ctx: ConstraintContext, _args: StrategyArgs): Constraint[] { buildConstraints(_ctx: ConstraintContext, _args: StrategyArgs): Constraint[] {
const { gold, rice } = this.command.getCost(); const { gold, rice } = this.command.getCost();
return [ return [
@@ -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 type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
import { allow, compareValues, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.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 { 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 type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import { createLogEffect, createNationPatchEffect } 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'; import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
@@ -104,6 +104,10 @@ export const createEventResearchCommand = (
return PRE_REQ_TURN; return PRE_REQ_TURN;
} }
getCostHint(): ActionCostHint {
return { gold: COST, rice: COST };
}
resolve( resolve(
context: GeneralActionResolveContext<TriggerState>, context: GeneralActionResolveContext<TriggerState>,
_args: Record<string, never> _args: Record<string, never>