feat: add command table functionality and integrate logic for turn commands
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^10.0.1",
|
||||
"@sammo-ts/common": "workspace:*",
|
||||
"@sammo-ts/logic": "workspace:*",
|
||||
"@sammo-ts/infra": "workspace:*",
|
||||
"@trpc/server": "^11.4.4",
|
||||
"fastify": "^5.3.3",
|
||||
|
||||
@@ -18,10 +18,83 @@ export interface WorldStateRow {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface GeneralRow {
|
||||
id: number;
|
||||
name: string;
|
||||
nationId: number;
|
||||
cityId: number;
|
||||
troopId: number;
|
||||
leadership: number;
|
||||
strength: number;
|
||||
intel: number;
|
||||
experience: number;
|
||||
dedication: number;
|
||||
officerLevel: number;
|
||||
personalCode: string;
|
||||
specialCode: string;
|
||||
special2Code: string;
|
||||
horseCode: string;
|
||||
weaponCode: string;
|
||||
bookCode: string;
|
||||
itemCode: string;
|
||||
injury: number;
|
||||
gold: number;
|
||||
rice: number;
|
||||
crew: number;
|
||||
crewTypeId: number;
|
||||
train: number;
|
||||
age: number;
|
||||
npcState: number;
|
||||
meta: unknown;
|
||||
}
|
||||
|
||||
export interface CityRow {
|
||||
id: number;
|
||||
name: string;
|
||||
nationId: number;
|
||||
level: number;
|
||||
population: number;
|
||||
populationMax: number;
|
||||
agriculture: number;
|
||||
agricultureMax: number;
|
||||
commerce: number;
|
||||
commerceMax: number;
|
||||
security: number;
|
||||
securityMax: number;
|
||||
supplyState: number;
|
||||
frontState: number;
|
||||
defence: number;
|
||||
defenceMax: number;
|
||||
wall: number;
|
||||
wallMax: number;
|
||||
meta: unknown;
|
||||
}
|
||||
|
||||
export interface NationRow {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
capitalCityId: number | null;
|
||||
gold: number;
|
||||
rice: number;
|
||||
level: number;
|
||||
typeCode: string;
|
||||
meta: unknown;
|
||||
}
|
||||
|
||||
export interface DatabaseClient {
|
||||
worldState: {
|
||||
findFirst(args?: unknown): Promise<WorldStateRow | null>;
|
||||
};
|
||||
general: {
|
||||
findUnique(args: { where: { id: number } }): Promise<GeneralRow | null>;
|
||||
};
|
||||
city: {
|
||||
findUnique(args: { where: { id: number } }): Promise<CityRow | null>;
|
||||
};
|
||||
nation: {
|
||||
findUnique(args: { where: { id: number } }): Promise<NationRow | null>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GameApiContext {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { WorldStateRow } from './context.js';
|
||||
import { procedure, router } from './trpc.js';
|
||||
import { authedProcedure, procedure, router } from './trpc.js';
|
||||
import { buildTurnCommandTable } from './turns/commandTable.js';
|
||||
|
||||
const zRunReason = z.enum(['schedule', 'manual', 'poke']);
|
||||
|
||||
@@ -35,6 +37,54 @@ export const appRouter = router({
|
||||
return state ? toWorldStateSnapshot(state) : null;
|
||||
}),
|
||||
}),
|
||||
turns: router({
|
||||
getCommandTable: authedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
generalId: z.number().int().positive(),
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const [worldState, general] = await Promise.all([
|
||||
ctx.db.worldState.findFirst(),
|
||||
ctx.db.general.findUnique({ where: { id: input.generalId } }),
|
||||
]);
|
||||
|
||||
if (!worldState) {
|
||||
throw new TRPCError({
|
||||
code: 'PRECONDITION_FAILED',
|
||||
message: 'World state is not initialized.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!general) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'General not found.',
|
||||
});
|
||||
}
|
||||
|
||||
const [city, nation] = await Promise.all([
|
||||
general.cityId > 0
|
||||
? ctx.db.city.findUnique({
|
||||
where: { id: general.cityId },
|
||||
})
|
||||
: null,
|
||||
general.nationId > 0
|
||||
? ctx.db.nation.findUnique({
|
||||
where: { id: general.nationId },
|
||||
})
|
||||
: null,
|
||||
]);
|
||||
|
||||
return buildTurnCommandTable({
|
||||
worldState,
|
||||
general,
|
||||
city,
|
||||
nation,
|
||||
});
|
||||
}),
|
||||
}),
|
||||
turnDaemon: router({
|
||||
run: procedure
|
||||
.input(
|
||||
|
||||
@@ -0,0 +1,626 @@
|
||||
import type {
|
||||
City,
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
General,
|
||||
GeneralActionDefinition,
|
||||
Nation,
|
||||
RequirementKey,
|
||||
StateView,
|
||||
TriggerValue,
|
||||
} from '@sammo-ts/logic';
|
||||
import {
|
||||
AssignmentActionDefinition,
|
||||
AwardActionDefinition,
|
||||
CommerceInvestmentActionDefinition,
|
||||
evaluateConstraints,
|
||||
FireAttackActionDefinition,
|
||||
NationRestActionDefinition,
|
||||
RestActionDefinition,
|
||||
TalentScoutActionDefinition,
|
||||
VolunteerRecruitActionDefinition,
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
import type {
|
||||
CityRow,
|
||||
GeneralRow,
|
||||
NationRow,
|
||||
WorldStateRow,
|
||||
} from '../context.js';
|
||||
|
||||
type AvailabilityStatus = 'available' | 'blocked' | 'needsInput' | 'unknown';
|
||||
|
||||
export interface TurnCommandAvailability {
|
||||
key: string;
|
||||
name: string;
|
||||
reqArg: boolean;
|
||||
possible: boolean;
|
||||
status: AvailabilityStatus;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface TurnCommandGroup {
|
||||
category: string;
|
||||
values: TurnCommandAvailability[];
|
||||
}
|
||||
|
||||
export interface TurnCommandTable {
|
||||
general: TurnCommandGroup[];
|
||||
nation: TurnCommandGroup[];
|
||||
}
|
||||
|
||||
interface CommandEnv {
|
||||
develCost: number;
|
||||
sabotageDefaultProb: number;
|
||||
sabotageProbCoefByStat: number;
|
||||
sabotageDefenceCoefByGeneralCount: number;
|
||||
sabotageDamageMin: number;
|
||||
sabotageDamageMax: number;
|
||||
openingPartYear: number;
|
||||
maxGeneral: number;
|
||||
defaultNpcGold: number;
|
||||
defaultNpcRice: number;
|
||||
defaultCrewTypeId: number;
|
||||
defaultSpecialDomestic: string | null;
|
||||
defaultSpecialWar: string | null;
|
||||
initialNationGenLimit: number;
|
||||
baseGold: number;
|
||||
baseRice: number;
|
||||
maxResourceActionAmount: number;
|
||||
}
|
||||
|
||||
interface AvailabilityCore {
|
||||
possible: boolean;
|
||||
status: AvailabilityStatus;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
interface CommandEntry {
|
||||
category: string;
|
||||
definition: GeneralActionDefinition;
|
||||
reqArg: boolean;
|
||||
args: unknown;
|
||||
evaluate?: (ctx: ConstraintContext, view: StateView) => AvailabilityCore;
|
||||
}
|
||||
|
||||
const INPUT_REQUIREMENT_KINDS = new Set<RequirementKey['kind']>([
|
||||
'destGeneral',
|
||||
'destCity',
|
||||
'destNation',
|
||||
'diplomacy',
|
||||
'arg',
|
||||
]);
|
||||
|
||||
const AVAILABILITY_PRIORITY: Record<AvailabilityStatus, number> = {
|
||||
available: 3,
|
||||
needsInput: 2,
|
||||
unknown: 1,
|
||||
blocked: 0,
|
||||
};
|
||||
|
||||
const DEFAULT_GENERAL_GOLD = 1000;
|
||||
const DEFAULT_GENERAL_RICE = 1000;
|
||||
const DEFAULT_CREW_TYPE_ID = 1100;
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> =>
|
||||
isRecord(value) ? value : {};
|
||||
|
||||
const asTriggerRecord = (value: unknown): Record<string, TriggerValue> =>
|
||||
isRecord(value) ? (value as Record<string, TriggerValue>) : {};
|
||||
|
||||
const normalizeCode = (value: string | null | undefined): string | null => {
|
||||
if (!value || value === 'None') {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
class MemoryStateView implements StateView {
|
||||
private readonly store = new Map<string, unknown>();
|
||||
|
||||
has(req: RequirementKey): boolean {
|
||||
return this.store.has(this.getKey(req));
|
||||
}
|
||||
|
||||
get(req: RequirementKey): unknown | null {
|
||||
return this.store.get(this.getKey(req)) ?? null;
|
||||
}
|
||||
|
||||
set(req: RequirementKey, value: unknown): void {
|
||||
this.store.set(this.getKey(req), value);
|
||||
}
|
||||
|
||||
private getKey(req: RequirementKey): string {
|
||||
switch (req.kind) {
|
||||
case 'general':
|
||||
return `general:${req.id}`;
|
||||
case 'city':
|
||||
return `city:${req.id}`;
|
||||
case 'nation':
|
||||
return `nation:${req.id}`;
|
||||
case 'destGeneral':
|
||||
return `destGeneral:${req.id}`;
|
||||
case 'destCity':
|
||||
return `destCity:${req.id}`;
|
||||
case 'destNation':
|
||||
return `destNation:${req.id}`;
|
||||
case 'diplomacy':
|
||||
return `diplomacy:${req.srcNationId}:${req.destNationId}`;
|
||||
case 'arg':
|
||||
return `arg:${req.key}`;
|
||||
case 'env':
|
||||
return `env:${req.key}`;
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resolveNumber = (
|
||||
source: Record<string, unknown>,
|
||||
keys: string[],
|
||||
fallback: number
|
||||
): number => {
|
||||
for (const key of keys) {
|
||||
const value = source[key];
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const resolveOptionalString = (
|
||||
source: Record<string, unknown>,
|
||||
keys: string[]
|
||||
): string | null => {
|
||||
for (const key of keys) {
|
||||
const value = source[key];
|
||||
if (typeof value === 'string') {
|
||||
return normalizeCode(value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const buildCommandEnv = (worldState: WorldStateRow): CommandEnv => {
|
||||
const config = asRecord(worldState.config);
|
||||
const constValues = asRecord(config.const);
|
||||
|
||||
return {
|
||||
develCost: resolveNumber(
|
||||
constValues,
|
||||
['develCost', 'develcost', 'develrate'],
|
||||
0
|
||||
),
|
||||
sabotageDefaultProb: resolveNumber(
|
||||
constValues,
|
||||
['sabotageDefaultProb'],
|
||||
0
|
||||
),
|
||||
sabotageProbCoefByStat: resolveNumber(
|
||||
constValues,
|
||||
['sabotageProbCoefByStat'],
|
||||
0
|
||||
),
|
||||
sabotageDefenceCoefByGeneralCount: resolveNumber(
|
||||
constValues,
|
||||
['sabotageDefenceCoefByGeneralCount'],
|
||||
0
|
||||
),
|
||||
sabotageDamageMin: resolveNumber(
|
||||
constValues,
|
||||
['sabotageDamageMin'],
|
||||
0
|
||||
),
|
||||
sabotageDamageMax: resolveNumber(
|
||||
constValues,
|
||||
['sabotageDamageMax'],
|
||||
0
|
||||
),
|
||||
openingPartYear: resolveNumber(
|
||||
constValues,
|
||||
['openingPartYear'],
|
||||
0
|
||||
),
|
||||
maxGeneral: resolveNumber(
|
||||
constValues,
|
||||
['defaultMaxGeneral', 'maxGeneral'],
|
||||
0
|
||||
),
|
||||
defaultNpcGold: resolveNumber(
|
||||
constValues,
|
||||
['defaultNpcGold', 'defaultGold'],
|
||||
DEFAULT_GENERAL_GOLD
|
||||
),
|
||||
defaultNpcRice: resolveNumber(
|
||||
constValues,
|
||||
['defaultNpcRice', 'defaultRice'],
|
||||
DEFAULT_GENERAL_RICE
|
||||
),
|
||||
defaultCrewTypeId: resolveNumber(
|
||||
constValues,
|
||||
['defaultCrewTypeId'],
|
||||
DEFAULT_CREW_TYPE_ID
|
||||
),
|
||||
defaultSpecialDomestic: resolveOptionalString(
|
||||
constValues,
|
||||
['defaultSpecialDomestic']
|
||||
),
|
||||
defaultSpecialWar: resolveOptionalString(
|
||||
constValues,
|
||||
['defaultSpecialWar']
|
||||
),
|
||||
initialNationGenLimit: resolveNumber(
|
||||
constValues,
|
||||
['initialNationGenLimit'],
|
||||
0
|
||||
),
|
||||
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
|
||||
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], 0),
|
||||
maxResourceActionAmount: resolveNumber(
|
||||
constValues,
|
||||
['maxResourceActionAmount'],
|
||||
0
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const buildConstraintEnv = (
|
||||
worldState: WorldStateRow
|
||||
): Record<string, unknown> => {
|
||||
const meta = asRecord(worldState.meta);
|
||||
const scenarioMeta = asRecord(meta.scenarioMeta);
|
||||
const startYear =
|
||||
typeof scenarioMeta.startYear === 'number'
|
||||
? scenarioMeta.startYear
|
||||
: undefined;
|
||||
const relYear =
|
||||
typeof startYear === 'number'
|
||||
? worldState.currentYear - startYear
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
currentYear: worldState.currentYear,
|
||||
currentMonth: worldState.currentMonth,
|
||||
year: worldState.currentYear,
|
||||
month: worldState.currentMonth,
|
||||
startYear,
|
||||
relYear,
|
||||
};
|
||||
};
|
||||
|
||||
const mapGeneralRow = (row: GeneralRow): General => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
nationId: row.nationId,
|
||||
cityId: row.cityId,
|
||||
troopId: row.troopId,
|
||||
stats: {
|
||||
leadership: row.leadership,
|
||||
strength: row.strength,
|
||||
intelligence: row.intel,
|
||||
},
|
||||
experience: row.experience,
|
||||
dedication: row.dedication,
|
||||
officerLevel: row.officerLevel,
|
||||
role: {
|
||||
personality: normalizeCode(row.personalCode),
|
||||
specialDomestic: normalizeCode(row.specialCode),
|
||||
specialWar: normalizeCode(row.special2Code),
|
||||
items: {
|
||||
horse: normalizeCode(row.horseCode),
|
||||
weapon: normalizeCode(row.weaponCode),
|
||||
book: normalizeCode(row.bookCode),
|
||||
item: normalizeCode(row.itemCode),
|
||||
},
|
||||
},
|
||||
injury: row.injury,
|
||||
gold: row.gold,
|
||||
rice: row.rice,
|
||||
crew: row.crew,
|
||||
crewTypeId: row.crewTypeId,
|
||||
train: row.train,
|
||||
age: row.age,
|
||||
npcState: row.npcState,
|
||||
triggerState: {
|
||||
flags: {},
|
||||
counters: {},
|
||||
modifiers: {},
|
||||
meta: {},
|
||||
},
|
||||
meta: asTriggerRecord(row.meta),
|
||||
});
|
||||
|
||||
const mapCityRow = (row: CityRow): City => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
nationId: row.nationId,
|
||||
level: row.level,
|
||||
population: row.population,
|
||||
populationMax: row.populationMax,
|
||||
agriculture: row.agriculture,
|
||||
agricultureMax: row.agricultureMax,
|
||||
commerce: row.commerce,
|
||||
commerceMax: row.commerceMax,
|
||||
security: row.security,
|
||||
securityMax: row.securityMax,
|
||||
supplyState: row.supplyState,
|
||||
frontState: row.frontState,
|
||||
defence: row.defence,
|
||||
defenceMax: row.defenceMax,
|
||||
wall: row.wall,
|
||||
wallMax: row.wallMax,
|
||||
meta: asTriggerRecord(row.meta),
|
||||
});
|
||||
|
||||
const mapNationRow = (row: NationRow): Nation => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
color: row.color,
|
||||
capitalCityId: row.capitalCityId,
|
||||
chiefGeneralId: null,
|
||||
gold: row.gold,
|
||||
rice: row.rice,
|
||||
power: 0,
|
||||
level: row.level,
|
||||
typeCode: row.typeCode,
|
||||
meta: asTriggerRecord(row.meta),
|
||||
});
|
||||
|
||||
const buildStateView = (
|
||||
general: General,
|
||||
city: City | null,
|
||||
nation: Nation | null
|
||||
): StateView => {
|
||||
const view = new MemoryStateView();
|
||||
view.set({ kind: 'general', id: general.id }, general);
|
||||
if (city) {
|
||||
view.set({ kind: 'city', id: city.id }, city);
|
||||
}
|
||||
if (nation) {
|
||||
view.set({ kind: 'nation', id: nation.id }, nation);
|
||||
}
|
||||
return view;
|
||||
};
|
||||
|
||||
const evaluateAvailability = (
|
||||
constraints: Constraint[],
|
||||
ctx: ConstraintContext,
|
||||
view: StateView,
|
||||
reqArg: boolean
|
||||
): AvailabilityCore => {
|
||||
const result = evaluateConstraints(constraints, ctx, view);
|
||||
if (result.kind === 'allow') {
|
||||
return { possible: true, status: 'available' };
|
||||
}
|
||||
if (result.kind === 'deny') {
|
||||
return {
|
||||
possible: false,
|
||||
status: 'blocked',
|
||||
reason: result.reason,
|
||||
};
|
||||
}
|
||||
const missingKinds = new Set(result.missing.map((req) => req.kind));
|
||||
const inputOnlyMissing =
|
||||
missingKinds.size === 0
|
||||
? reqArg
|
||||
: Array.from(missingKinds).every((kind) =>
|
||||
INPUT_REQUIREMENT_KINDS.has(kind)
|
||||
);
|
||||
if (inputOnlyMissing) {
|
||||
return {
|
||||
possible: true,
|
||||
status: 'needsInput',
|
||||
reason: '대상 선택 필요',
|
||||
};
|
||||
}
|
||||
return {
|
||||
possible: false,
|
||||
status: 'unknown',
|
||||
reason: '정보 부족',
|
||||
};
|
||||
};
|
||||
|
||||
const evaluateDefinition = (
|
||||
definition: GeneralActionDefinition,
|
||||
ctx: ConstraintContext,
|
||||
view: StateView,
|
||||
reqArg: boolean,
|
||||
args: unknown
|
||||
): AvailabilityCore => {
|
||||
const constraints = definition.buildConstraints(ctx, args);
|
||||
return evaluateAvailability(constraints, ctx, view, reqArg);
|
||||
};
|
||||
|
||||
const pickAvailability = (
|
||||
lhs: AvailabilityCore,
|
||||
rhs: AvailabilityCore
|
||||
): AvailabilityCore =>
|
||||
AVAILABILITY_PRIORITY[lhs.status] >= AVAILABILITY_PRIORITY[rhs.status]
|
||||
? lhs
|
||||
: rhs;
|
||||
|
||||
const buildEntries = (env: CommandEnv): {
|
||||
general: CommandEntry[];
|
||||
nation: CommandEntry[];
|
||||
} => {
|
||||
const emptyModules: [] = [];
|
||||
|
||||
const generalEntries: CommandEntry[] = [
|
||||
{
|
||||
category: '개인',
|
||||
definition: new RestActionDefinition(),
|
||||
reqArg: false,
|
||||
args: {},
|
||||
},
|
||||
{
|
||||
category: '내정',
|
||||
definition: new CommerceInvestmentActionDefinition(emptyModules, {
|
||||
develCost: env.develCost,
|
||||
}),
|
||||
reqArg: false,
|
||||
args: {},
|
||||
},
|
||||
{
|
||||
category: '계략',
|
||||
definition: new FireAttackActionDefinition(emptyModules, {
|
||||
develCost: env.develCost,
|
||||
sabotageDefaultProb: env.sabotageDefaultProb,
|
||||
sabotageProbCoefByStat: env.sabotageProbCoefByStat,
|
||||
sabotageDefenceCoefByGeneralCount:
|
||||
env.sabotageDefenceCoefByGeneralCount,
|
||||
sabotageDamageMin: env.sabotageDamageMin,
|
||||
sabotageDamageMax: env.sabotageDamageMax,
|
||||
}),
|
||||
reqArg: true,
|
||||
args: { destCityId: 0 },
|
||||
},
|
||||
{
|
||||
category: '인사',
|
||||
definition: new TalentScoutActionDefinition(emptyModules, {
|
||||
develCost: env.develCost,
|
||||
maxGeneral: env.maxGeneral,
|
||||
defaultNpcGold: env.defaultNpcGold,
|
||||
defaultNpcRice: env.defaultNpcRice,
|
||||
defaultCrewTypeId: env.defaultCrewTypeId,
|
||||
defaultSpecialDomestic: env.defaultSpecialDomestic,
|
||||
defaultSpecialWar: env.defaultSpecialWar,
|
||||
}),
|
||||
reqArg: false,
|
||||
args: {},
|
||||
},
|
||||
{
|
||||
category: '전략',
|
||||
definition: new VolunteerRecruitActionDefinition(emptyModules, {
|
||||
openingPartYear: env.openingPartYear,
|
||||
initialNationGenLimit: env.initialNationGenLimit,
|
||||
defaultNpcGold: env.defaultNpcGold,
|
||||
defaultNpcRice: env.defaultNpcRice,
|
||||
defaultCrewTypeId: env.defaultCrewTypeId,
|
||||
defaultSpecialDomestic: env.defaultSpecialDomestic,
|
||||
defaultSpecialWar: env.defaultSpecialWar,
|
||||
}),
|
||||
reqArg: false,
|
||||
args: {},
|
||||
},
|
||||
];
|
||||
|
||||
const awardDefinition = new AwardActionDefinition({
|
||||
baseGold: env.baseGold,
|
||||
baseRice: env.baseRice,
|
||||
maxAmount: env.maxResourceActionAmount,
|
||||
});
|
||||
const assignmentDefinition = new AssignmentActionDefinition({});
|
||||
|
||||
const nationEntries: CommandEntry[] = [
|
||||
{
|
||||
category: '휴식',
|
||||
definition: new NationRestActionDefinition(),
|
||||
reqArg: false,
|
||||
args: {},
|
||||
},
|
||||
{
|
||||
category: '인사',
|
||||
definition: assignmentDefinition,
|
||||
reqArg: true,
|
||||
args: { destGeneralId: 0, destCityId: 0 },
|
||||
},
|
||||
{
|
||||
category: '인사',
|
||||
definition: awardDefinition,
|
||||
reqArg: true,
|
||||
args: { isGold: true, amount: 1, destGeneralId: 0 },
|
||||
evaluate: (ctx, view) => {
|
||||
const gold = evaluateDefinition(
|
||||
awardDefinition,
|
||||
ctx,
|
||||
view,
|
||||
true,
|
||||
{ isGold: true, amount: 1, destGeneralId: 0 }
|
||||
);
|
||||
const rice = evaluateDefinition(
|
||||
awardDefinition,
|
||||
ctx,
|
||||
view,
|
||||
true,
|
||||
{ isGold: false, amount: 1, destGeneralId: 0 }
|
||||
);
|
||||
return pickAvailability(gold, rice);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return { general: generalEntries, nation: nationEntries };
|
||||
};
|
||||
|
||||
const buildGroups = (
|
||||
entries: CommandEntry[],
|
||||
ctx: ConstraintContext,
|
||||
view: StateView
|
||||
): TurnCommandGroup[] => {
|
||||
const groups = new Map<string, TurnCommandAvailability[]>();
|
||||
|
||||
for (const entry of entries) {
|
||||
const availability = entry.evaluate
|
||||
? entry.evaluate(ctx, view)
|
||||
: evaluateDefinition(
|
||||
entry.definition,
|
||||
ctx,
|
||||
view,
|
||||
entry.reqArg,
|
||||
entry.args
|
||||
);
|
||||
const value: TurnCommandAvailability = {
|
||||
key: entry.definition.key,
|
||||
name: entry.definition.name,
|
||||
reqArg: entry.reqArg,
|
||||
...availability,
|
||||
};
|
||||
|
||||
const list = groups.get(entry.category);
|
||||
if (list) {
|
||||
list.push(value);
|
||||
} else {
|
||||
groups.set(entry.category, [value]);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(groups.entries()).map(([category, values]) => ({
|
||||
category,
|
||||
values,
|
||||
}));
|
||||
};
|
||||
|
||||
export const buildTurnCommandTable = (options: {
|
||||
worldState: WorldStateRow;
|
||||
general: GeneralRow;
|
||||
city: CityRow | null;
|
||||
nation: NationRow | null;
|
||||
}): TurnCommandTable => {
|
||||
// 턴 입력 화면에서 쓰는 사전 판단이므로 최소 정보로 가능/불가만 계산한다.
|
||||
const general = mapGeneralRow(options.general);
|
||||
const city = options.city ? mapCityRow(options.city) : null;
|
||||
const nation = options.nation ? mapNationRow(options.nation) : null;
|
||||
const view = buildStateView(general, city, nation);
|
||||
|
||||
const ctx: ConstraintContext = {
|
||||
actorId: general.id,
|
||||
cityId: city?.id,
|
||||
nationId: nation?.id,
|
||||
args: {},
|
||||
env: buildConstraintEnv(options.worldState),
|
||||
mode: 'precheck',
|
||||
};
|
||||
|
||||
const env = buildCommandEnv(options.worldState);
|
||||
const entries = buildEntries(env);
|
||||
|
||||
return {
|
||||
general: buildGroups(entries.general, ctx, view),
|
||||
nation: buildGroups(entries.nation, ctx, view),
|
||||
};
|
||||
};
|
||||
@@ -19,6 +19,15 @@ const buildContext = (options?: {
|
||||
worldState: {
|
||||
findFirst: async () => options?.state ?? null,
|
||||
},
|
||||
general: {
|
||||
findUnique: async () => null,
|
||||
},
|
||||
city: {
|
||||
findUnique: async () => null,
|
||||
},
|
||||
nation: {
|
||||
findUnique: async () => null,
|
||||
},
|
||||
};
|
||||
return {
|
||||
db,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../../packages/common" },
|
||||
{ "path": "../../packages/infra" }
|
||||
{ "path": "../../packages/infra" },
|
||||
{ "path": "../../packages/logic" }
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user