1
0
Fork 0

Rewrite types using typebox

This commit is contained in:
Pabloader 2026-04-28 12:28:54 +00:00
parent 87837754f6
commit 2d4a59f2b3
3 changed files with 96 additions and 97 deletions

View File

@ -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<string, string | number | boolean | undefined>;
export interface RPGAction {
type: string;
arg?: string | number | boolean | null;
}
export type RPGActions = Record<string, (arg?: any) => unknown>;
export type RPGAction = Static<typeof RPGActionScheme>;
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<string, unknown>).type === 'string';
}
function isDialogChoice(v: unknown): v is DialogChoice {
if (typeof v !== 'object' || v === null) return false;
const c = v as Record<string, unknown>;
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<string, unknown>;
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<typeof DialogChoiceScheme>;
export type DialogNode = Static<typeof DialogNodeScheme>;
export type Dialog = Static<typeof DialogScheme>;
export function isDialog(v: unknown): v is Dialog {
if (typeof v !== 'object' || v === null) return false;
const d = v as Record<string, unknown>;
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<string, unknown>;
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<typeof QuestObjectiveScheme>;
export type QuestStage = Static<typeof QuestStageScheme>;
export type Quest = Static<typeof QuestScheme>;
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<string, unknown>;
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<string, number>;
}
export function isQuest(v: unknown): v is Quest {
if (typeof v !== 'object' || v === null) return false;
const q = v as Record<string, unknown>;
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')));
}

View File

@ -64,6 +64,14 @@ export namespace Type {
return result;
}
export function Union<T extends TScheme[]>(anyOf: [...T], args: { description?: string } = {}): TUnion<T> {
const result: TUnion<T> = { type: 'union', anyOf };
if (args.description) {
result.description = args.description;
}
return result;
}
export function Optional<T extends TScheme = TScheme>(scheme: T): TOptional<T> {
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<T extends TProperties = TProperties> extends TO
[optional]: true;
}
export interface TUnion<T extends TScheme[] = TScheme[]> {
type: 'union';
anyOf: T;
description?: string;
}
export interface TOptionalUnion<T extends TScheme[] = TScheme[]> extends TUnion<T> {
[optional]: true;
}
export type TOptional<T extends TScheme = TScheme> =
T extends TString<infer S> ? TOptionalString<S> :
T extends TNumber<infer N> ? TOptionalNumber<N> :
T extends TBoolean ? TOptionalBoolean :
T extends TArray<infer I> ? TOptionalArray<I> :
T extends TObject<infer P> ? TOptionalObject<P> :
T extends TUnion<infer U> ? TOptionalUnion<U> :
never;
export type IsOptional<T> = 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<T> = { [K in keyof T]: T[K] } & {};
@ -251,10 +277,16 @@ type StaticObject<T extends TProperties> = Prettify<
{ [K in OptionalKeys<T>]?: Static<T[K]> }
>;
type StaticUnion<T extends TScheme[]> =
T extends [infer First extends TScheme, ...infer Rest extends TScheme[]]
? Static<First> | StaticUnion<Rest>
: never;
export type Static<T extends TScheme> =
T extends TString<infer S> ? S :
T extends TNumber<infer N> ? N :
T extends TBoolean ? boolean :
T extends TArray<infer I> ? Static<I>[] :
T extends TObject<infer P> ? StaticObject<P> :
T extends TUnion<infer U> ? StaticUnion<U> :
never;

View File

@ -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,
});