diff --git a/src/common/rpg/types.ts b/src/common/rpg/types.ts index 0dfc3d9..b428eaa 100644 --- a/src/common/rpg/types.ts +++ b/src/common/rpg/types.ts @@ -1,101 +1,79 @@ +import { Type, type Static } from '../typebox'; + +// ── Shared ──────────────────────────────────────────────────────────────────── + +const RPGActionScheme = Type.Object({ + type: Type.String(), + arg: Type.Optional(Type.Union([Type.String(), Type.Number(), Type.Boolean()])), +}); + export type RPGCondition = string; export type RPGVariables = Record; - -export interface RPGAction { - type: string; - arg?: string | number | boolean | null; -} - export type RPGActions = Record unknown>; +export type RPGAction = Static; -export interface DialogChoice { - text: string; - nextNodeId?: string; -} +// ── Dialog ──────────────────────────────────────────────────────────────────── -export interface DialogNode { - id: string; - speaker: string; - text: string; - nextNodeId?: string; - choices?: DialogChoice[]; - conditions?: RPGCondition[]; - actions?: RPGAction[]; -} +const DialogChoiceScheme = Type.Object({ + text: Type.String(), + nextNodeId: Type.Optional(Type.String()), +}); -export interface Dialog { - nodes: DialogNode[]; - startNodeId: string; -} +const DialogNodeScheme = Type.Object({ + id: Type.String(), + speaker: Type.String(), + text: Type.String(), + nextNodeId: Type.Optional(Type.String()), + choices: Type.Optional(Type.Array(DialogChoiceScheme)), + conditions: Type.Optional(Type.Array(Type.String())), + actions: Type.Optional(Type.Array(RPGActionScheme)), +}); -export interface QuestObjective { - id: string; - description: string; - condition: RPGCondition; -} +const DialogScheme = Type.Object({ + nodes: Type.Array(DialogNodeScheme), + startNodeId: Type.String(), +}); -export interface QuestStage { - id: string; - description: string; - objectives: QuestObjective[]; - actions: RPGAction[]; -} - -export interface Quest { - id: string; - title: string; - description: string; - conditions?: RPGCondition[]; - stages: QuestStage[]; -} - -function isRPGAction(v: unknown): v is RPGAction { - if (typeof v !== 'object' || v === null) return false; - return typeof (v as Record).type === 'string'; -} - -function isDialogChoice(v: unknown): v is DialogChoice { - if (typeof v !== 'object' || v === null) return false; - const c = v as Record; - return typeof c.text === 'string' - && (c.nextNodeId === undefined || typeof c.nextNodeId === 'string'); -} - -function isDialogNode(v: unknown): v is DialogNode { - if (typeof v !== 'object' || v === null) return false; - const n = v as Record; - return typeof n.id === 'string' - && typeof n.speaker === 'string' - && typeof n.text === 'string' - && (n.nextNodeId === undefined || typeof n.nextNodeId === 'string') - && (n.choices === undefined || (Array.isArray(n.choices) && n.choices.every(isDialogChoice))) - && (n.conditions === undefined || (Array.isArray(n.conditions) && n.conditions.every(c => typeof c === 'string'))) - && (n.actions === undefined || (Array.isArray(n.actions) && n.actions.every(isRPGAction))); -} +export type DialogChoice = Static; +export type DialogNode = Static; +export type Dialog = Static; export function isDialog(v: unknown): v is Dialog { - if (typeof v !== 'object' || v === null) return false; - const d = v as Record; - return Array.isArray(d.nodes) && d.nodes.every(isDialogNode) - && typeof d.startNodeId === 'string'; + return Type.Is(DialogScheme, v); } -function isQuestObjective(v: unknown): v is QuestObjective { - if (typeof v !== 'object' || v === null) return false; - const o = v as Record; - return typeof o.id === 'string' - && typeof o.description === 'string' - && typeof o.condition === 'string'; +// ── Quest ───────────────────────────────────────────────────────────────────── + +const QuestObjectiveScheme = Type.Object({ + id: Type.String(), + description: Type.String(), + condition: Type.String(), +}); + +const QuestStageScheme = Type.Object({ + id: Type.String(), + description: Type.String(), + objectives: Type.Array(QuestObjectiveScheme), + actions: Type.Array(RPGActionScheme), +}); + +const QuestScheme = Type.Object({ + id: Type.String(), + title: Type.String(), + description: Type.String(), + conditions: Type.Optional(Type.Array(Type.String())), + stages: Type.Array(QuestStageScheme), +}); + +export type QuestObjective = Static; +export type QuestStage = Static; +export type Quest = Static; + +export function isQuest(v: unknown): v is Quest { + return Type.Is(QuestScheme, v); } -function isQuestStage(v: unknown): v is QuestStage { - if (typeof v !== 'object' || v === null) return false; - const s = v as Record; - return typeof s.id === 'string' - && typeof s.description === 'string' - && Array.isArray(s.objectives) && s.objectives.every(isQuestObjective) - && Array.isArray(s.actions) && s.actions.every(isRPGAction); -} +// ── Inventory ───────────────────────────────────────────────────────────────── export type SlotId = string | number; @@ -109,13 +87,3 @@ export type InventorySlotInput = SlotId | InventorySlotDefinition; export interface InventoryOptions { maxAmountPerItem?: Record; } - -export function isQuest(v: unknown): v is Quest { - if (typeof v !== 'object' || v === null) return false; - const q = v as Record; - return typeof q.id === 'string' - && typeof q.title === 'string' - && typeof q.description === 'string' - && Array.isArray(q.stages) && q.stages.every(isQuestStage) - && (q.conditions === undefined || (Array.isArray(q.conditions) && q.conditions.every(c => typeof c === 'string'))); -} diff --git a/src/common/typebox.ts b/src/common/typebox.ts index 60aa677..3be0aa0 100644 --- a/src/common/typebox.ts +++ b/src/common/typebox.ts @@ -64,6 +64,14 @@ export namespace Type { return result; } + export function Union(anyOf: [...T], args: { description?: string } = {}): TUnion { + const result: TUnion = { type: 'union', anyOf }; + if (args.description) { + result.description = args.description; + } + return result; + } + export function Optional(scheme: T): TOptional { const result = { ...scheme }; GlobalObject.defineProperty(result, optional, { value: true, enumerable: false, writable: false, configurable: false }); @@ -158,6 +166,13 @@ export namespace Type { } return errors; } + case 'union': { + const s = scheme as TUnion; + for (const branch of s.anyOf) { + if (check(branch, value, path).length === 0) return []; + } + return [{ path, message: `Expected union type at ${path}, got ${typeof value}` }]; + } default: return []; } @@ -224,17 +239,28 @@ export interface TOptionalObject extends TO [optional]: true; } +export interface TUnion { + type: 'union'; + anyOf: T; + description?: string; +} + +export interface TOptionalUnion extends TUnion { + [optional]: true; +} + export type TOptional = T extends TString ? TOptionalString : T extends TNumber ? TOptionalNumber : T extends TBoolean ? TOptionalBoolean : T extends TArray ? TOptionalArray : T extends TObject ? TOptionalObject

: + T extends TUnion ? TOptionalUnion : never; export type IsOptional = T extends { [optional]: true } ? true : false; -export type TScheme = TString | TNumber | TBoolean | TArray | TObject | TOptionalString | TOptionalNumber | TOptionalBoolean | TOptionalArray | TOptionalObject; +export type TScheme = TString | TNumber | TBoolean | TArray | TObject | TUnion | TOptionalString | TOptionalNumber | TOptionalBoolean | TOptionalArray | TOptionalObject | TOptionalUnion; type Prettify = { [K in keyof T]: T[K] } & {}; @@ -251,10 +277,16 @@ type StaticObject = Prettify< { [K in OptionalKeys]?: Static } >; +type StaticUnion = + T extends [infer First extends TScheme, ...infer Rest extends TScheme[]] + ? Static | StaticUnion + : never; + export type Static = T extends TString ? S : T extends TNumber ? N : T extends TBoolean ? boolean : T extends TArray ? Static[] : T extends TObject ? StaticObject

: + T extends TUnion ? StaticUnion : never; diff --git a/src/games/playground/index.tsx b/src/games/playground/index.tsx index d32904b..402d7a4 100644 --- a/src/games/playground/index.tsx +++ b/src/games/playground/index.tsx @@ -4,7 +4,6 @@ import { Stat } from "@common/rpg/components/stat"; import { Variables } from "@common/rpg/components/variables"; import { QuestManager } from "@common/rpg/quest"; - export default async function main() { const game = new RPGEntity(); const player = new RPGEntity(); @@ -29,7 +28,7 @@ export default async function main() { amount: 1, slotId: 'head', }); - inventory.addItem({ + game.getActions()['player.inventory.addItem']({ itemId: 'boots', amount: 2, });