1
0
Fork 0

Move component decorator to all decorators

This commit is contained in:
Pabloader 2026-04-30 16:27:42 +00:00
parent b549193a3b
commit 6568e1e8d9
18 changed files with 161 additions and 140 deletions

View File

@ -1,5 +1,5 @@
import { component } from "../core/registry";
import { Component } from "../core/world"; import { Component } from "../core/world";
import { component } from "../utils/decorators";
import { Stat } from "./stat"; import { Stat } from "./stat";
@component @component

View File

@ -1,6 +1,5 @@
import { component } from "../core/registry";
import { Component } from "../core/world"; import { Component } from "../core/world";
import { action, variable } from "../utils/decorators"; import { action, component, variable } from "../utils/decorators";
@component @component
export class Cooldown extends Component<{ export class Cooldown extends Component<{

View File

@ -1,7 +1,6 @@
import { component } from "../core/registry";
import { Component, type EvalContext } from "../core/world"; import { Component, type EvalContext } from "../core/world";
import { evaluateCondition } from "../utils/conditions"; import { evaluateCondition } from "../utils/conditions";
import { action, variable } from "../utils/decorators"; import { action, component, variable } from "../utils/decorators";
import { Stat } from "./stat"; import { Stat } from "./stat";
@component @component

View File

@ -1,9 +1,7 @@
import { component } from "../core/registry";
import { Component } from "../core/world"; import { Component } from "../core/world";
import { action } from "../utils/decorators";
import type { RPGVariables } from "../types"; import type { RPGVariables } from "../types";
import { action, component } from "../utils/decorators";
import { Effect } from "./effect"; import { Effect } from "./effect";
import { Damage } from "./combat";
// ── Equippable ──────────────────────────────────────────────────────────────── // ── Equippable ────────────────────────────────────────────────────────────────

View File

@ -1,6 +1,5 @@
import { component } from "../core/registry";
import { Component } from "../core/world"; import { Component } from "../core/world";
import { action, variable } from "../utils/decorators"; import { action, component, variable } from "../utils/decorators";
/** /**
* How much XP is required to advance from level N to N+1. * How much XP is required to advance from level N to N+1.

View File

@ -1,7 +1,6 @@
import { component } from "../core/registry";
import { Component, type Entity } from "../core/world"; import { Component, type Entity } from "../core/world";
import type { RPGVariables } from "../types"; import type { RPGVariables } from "../types";
import { action } from "../utils/decorators"; import { action, component } from "../utils/decorators";
// ── FactionMember ───────────────────────────────────────────────────────────── // ── FactionMember ─────────────────────────────────────────────────────────────

View File

@ -1,10 +1,9 @@
import type { InventorySlotInput, RPGVariables, SlotId } from "../types";
import { action } from "../utils/decorators";
import { Component, type EvalContext } from "../core/world"; import { Component, type EvalContext } from "../core/world";
import { component } from "../core/registry"; import type { InventorySlotInput, RPGVariables, SlotId } from "../types";
import { Stackable, Usable } from "./item"; import { action, component } from "../utils/decorators";
import { Equipment, Equippable } from "./equipment";
import { resolveVariables } from "../utils/variables"; import { resolveVariables } from "../utils/variables";
import { Equipment, Equippable } from "./equipment";
import { Stackable, Usable } from "./item";
interface SlotRecord { interface SlotRecord {
slotId: SlotId; slotId: SlotId;

View File

@ -1,7 +1,6 @@
import { Component, type EvalContext, type World } from "../core/world"; import { Component, type EvalContext, type World } from "../core/world";
import { component } from "../core/registry";
import type { RPGAction } from "../types"; import type { RPGAction } from "../types";
import { action, variable } from "../utils/decorators"; import { action, component, variable } from "../utils/decorators";
import { executeAction } from "../utils/variables"; import { executeAction } from "../utils/variables";
import { Equippable } from "./equipment"; import { Equippable } from "./equipment";

View File

@ -0,0 +1,9 @@
import { Component } from "../core/world";
import { component } from "../utils/decorators";
@component({ variables: ['x', 'y', 'z'] })
export class Position extends Component<{ x: number, y: number, z: number }> {
constructor(x = 0, y = 0, z = 0) {
super({ x, y, z });
}
}

View File

@ -1,7 +1,7 @@
import { Component, type EvalContext } from "../core/world"; import { Component, type EvalContext } from "../core/world";
import { component } from "../core/registry";
import { isQuest, type Quest, type QuestStage, type RPGVariables } from "../types"; import { isQuest, type Quest, type QuestStage, type RPGVariables } from "../types";
import { evaluateCondition } from "../utils/conditions"; import { evaluateCondition } from "../utils/conditions";
import { component } from "../utils/decorators";
export type QuestStatus = 'inactive' | 'active' | 'completed' | 'failed'; export type QuestStatus = 'inactive' | 'active' | 'completed' | 'failed';

View File

@ -1,6 +1,5 @@
import { action, variable } from "../utils/decorators";
import { Component } from "../core/world"; import { Component } from "../core/world";
import { component } from "../core/registry"; import { action, component, variable } from "../utils/decorators";
interface StatState { interface StatState {
base: number; base: number;

View File

@ -1,7 +1,6 @@
import type { RPGVariables } from "../types";
import { action } from "../utils/decorators";
import { Component } from "../core/world"; import { Component } from "../core/world";
import { component } from "../core/registry"; import type { RPGVariables } from "../types";
import { action, component } from "../utils/decorators";
interface Var { interface Var {
key: string; key: string;

View File

@ -1,100 +0,0 @@
import type { Component } from './world';
type ComponentConstructor = abstract new (...args: any[]) => Component<any>;
type MigrationFn = (state: Record<string, unknown>) => Record<string, unknown>;
interface ComponentMeta {
ctor: ComponentConstructor;
version: number;
}
interface MigrationEntry {
toVersion: number;
fn: MigrationFn;
}
const registry = new Map<string, ComponentMeta>();
const reverseRegistry = new Map<ComponentConstructor, string>();
/** migrations[name][fromVersion] → { toVersion, fn } */
const migrations = new Map<string, Map<number, MigrationEntry>>();
function register(name: string, ctor: ComponentConstructor, version: number): void {
registry.set(name, { ctor, version });
reverseRegistry.set(ctor, name);
}
export function getComponentMeta(name: string): ComponentMeta | undefined {
return registry.get(name);
}
export function getComponentName(ctor: Function): string | undefined {
return reverseRegistry.get(ctor as ComponentConstructor);
}
/**
* Register a migration that upgrades component state from `fromVersion` to `toVersion`.
* Migrations are chained automatically: registering 01 and 12 handles saves at version 0.
*/
export function registerMigration(
name: string,
fromVersion: number,
toVersion: number,
fn: MigrationFn,
): void {
if (!migrations.has(name)) migrations.set(name, new Map());
migrations.get(name)!.set(fromVersion, { toVersion, fn });
}
/**
* Apply all registered migrations to bring `state` from `fromVersion` up to the current
* registered version. Returns the migrated state (may be the same object if no migrations ran).
*/
export function migrateState(
name: string,
state: Record<string, unknown>,
fromVersion: number,
): Record<string, unknown> {
const meta = registry.get(name);
if (!meta) return state;
const chain = migrations.get(name);
if (!chain) return state;
let current = fromVersion;
let s = state;
while (current < meta.version) {
const entry = chain.get(current);
if (!entry) throw new Error(
`[registry] No migration for '${name}' from version ${current} to ${meta.version}. ` +
`Register one with registerMigration('${name}', ${current}, ...).`
);
s = entry.fn(s);
current = entry.toVersion;
}
return s;
}
interface ComponentOptions {
name?: string;
version?: number;
}
type ComponentDecorator = (target: ComponentConstructor, ctx: ClassDecoratorContext) => void;
export function component(target: ComponentConstructor, ctx: ClassDecoratorContext): void;
export function component(name: string): ComponentDecorator;
export function component(options: ComponentOptions): ComponentDecorator;
export function component(
nameOrTargetOrOptions: string | ComponentConstructor | ComponentOptions,
ctx?: ClassDecoratorContext,
): void | ComponentDecorator {
if (typeof nameOrTargetOrOptions === 'string') {
const name = nameOrTargetOrOptions;
return (target: ComponentConstructor) => register(name, target, 0);
}
if (typeof nameOrTargetOrOptions === 'object') {
const { name, version = 0 } = nameOrTargetOrOptions;
return (target: ComponentConstructor, ctx: ClassDecoratorContext) =>
register(name ?? String(ctx.name), target, version);
}
// Used as bare @component
register(String(ctx!.name), nameOrTargetOrOptions, 0);
}

View File

@ -1,5 +1,5 @@
import { World, Entity, Component, WORLD_ENTITY_COUNTER } from './world'; import { World, Entity, Component, WORLD_ENTITY_COUNTER } from './world';
import { getComponentMeta, getComponentName, migrateState } from './registry'; import { getComponentMeta, getComponentName, migrateState } from '../utils/decorators';
/** Increment this when the WorldData/EntityData structure itself changes incompatibly. */ /** Increment this when the WorldData/EntityData structure itself changes incompatibly. */
const SCHEMA_VERSION = 1; const SCHEMA_VERSION = 1;

View File

@ -1,6 +1,5 @@
import { ACTION_KEYS, VARIABLE_KEYS } from '../utils/decorators';
import type { RPGActions, RPGVariables } from '../types'; import type { RPGActions, RPGVariables } from '../types';
import { getComponentName } from './registry'; import { ACTION_KEYS, getComponentName, STATE_KEYS, VARIABLE_KEYS } from '../utils/decorators';
interface EntityEvent<T = unknown> { interface EntityEvent<T = unknown> {
target: Entity; target: Entity;
@ -59,14 +58,24 @@ export abstract class Component<TState = Record<string, unknown>> {
getVariables(): RPGVariables { getVariables(): RPGVariables {
const meta = (this.constructor as Function)[Symbol.metadata]; const meta = (this.constructor as Function)[Symbol.metadata];
const keys = meta?.[VARIABLE_KEYS] as Map<string | symbol, string> | undefined; const keys = meta?.[VARIABLE_KEYS] as Map<string | symbol, string> | undefined;
if (!keys) return {}; const stateKeys = meta?.[STATE_KEYS] as Set<string> | undefined;
const vars: RPGVariables = {}; const vars: RPGVariables = {};
if (keys) {
for (const [methodKey, exportName] of keys) { for (const [methodKey, exportName] of keys) {
const v = (this as Record<string, unknown>)[String(methodKey)]; const v = (this as Record<string, unknown>)[String(methodKey)];
if (typeof v === 'number' || typeof v === 'string' || typeof v === 'boolean') { if (typeof v === 'number' || typeof v === 'string' || typeof v === 'boolean') {
vars[exportName] = v; vars[exportName] = v;
} }
} }
}
if (stateKeys) {
for (const key of stateKeys) {
const v = (this._state as Record<string, unknown>)[String(key)];
if (typeof v === 'number' || typeof v === 'string' || typeof v === 'boolean') {
vars[key] = v;
}
}
}
return vars; return vars;
} }

View File

@ -1,3 +1,4 @@
import type { Component } from "../core/world";
import type { RPGVariables } from "../types"; import type { RPGVariables } from "../types";
// TC39 stage-3 decorators use Symbol.metadata; polyfill for runtimes that lack it (e.g. Bun). // TC39 stage-3 decorators use Symbol.metadata; polyfill for runtimes that lack it (e.g. Bun).
@ -5,6 +6,7 @@ import type { RPGVariables } from "../types";
export const ACTION_KEYS = Symbol('rpg.actions'); export const ACTION_KEYS = Symbol('rpg.actions');
export const VARIABLE_KEYS = Symbol('rpg.variables'); export const VARIABLE_KEYS = Symbol('rpg.variables');
export const STATE_KEYS = Symbol('rpg.state_variables');
export function action<T extends (arg?: any, ctx?: any) => unknown>( export function action<T extends (arg?: any, ctx?: any) => unknown>(
target: T, target: T,
@ -19,11 +21,16 @@ export function action<T extends (arg?: any, ctx?: any) => unknown>(
type VariableContext<T extends RPGVariables[string]> = type VariableContext<T extends RPGVariables[string]> =
| ClassFieldDecoratorContext<unknown, T> | ClassFieldDecoratorContext<unknown, T>
| ClassGetterDecoratorContext<unknown, T>; | ClassGetterDecoratorContext<unknown, T>
| ClassDecoratorContext;
function registerVariable(context: VariableContext<RPGVariables[string]>, exportName: string): void { function registerVariable(context: VariableContext<RPGVariables[string]>, exportName: string, name?: string): void {
const prev = context.metadata[VARIABLE_KEYS] as Map<string | symbol, string> | undefined; const prev = context.metadata[VARIABLE_KEYS] as Map<string | symbol, string> | undefined;
context.metadata[VARIABLE_KEYS] = new Map(prev).set(context.name, exportName); context.metadata[VARIABLE_KEYS] = new Map(prev).set(name ?? context.name ?? exportName, exportName);
}
function registerStateVariable(context: VariableContext<RPGVariables[string]>, name: string): void {
const prev = context.metadata[STATE_KEYS] as Set<string> | undefined;
context.metadata[STATE_KEYS] = new Set(prev).add(name);
} }
export function variable(name: string): <T extends RPGVariables[string]>(target: undefined | (() => T), context: VariableContext<T>) => void; export function variable(name: string): <T extends RPGVariables[string]>(target: undefined | (() => T), context: VariableContext<T>) => void;
@ -41,3 +48,107 @@ export function variable(
} }
registerVariable(context!, String(context!.name)); registerVariable(context!, String(context!.name));
} }
type ComponentConstructor<T> = abstract new (...args: any[]) => Component<T>;
type MigrationFn = (state: Record<string, unknown>) => Record<string, unknown>;
interface ComponentMeta<T> {
ctor: ComponentConstructor<T>;
version: number;
}
interface MigrationEntry {
toVersion: number;
fn: MigrationFn;
}
const registry = new Map<string, ComponentMeta<any>>();
const reverseRegistry = new Map<ComponentConstructor<any>, string>();
/** migrations[name][fromVersion] → { toVersion, fn } */
const migrations = new Map<string, Map<number, MigrationEntry>>();
function register<T>(name: string, ctor: ComponentConstructor<T>, version: number): void {
registry.set(name, { ctor, version });
reverseRegistry.set(ctor, name);
}
export function getComponentMeta<T>(name: string): ComponentMeta<T> | undefined {
return registry.get(name);
}
export function getComponentName(ctor: Function): string | undefined {
return reverseRegistry.get(ctor as ComponentConstructor<any>);
}
/**
* Register a migration that upgrades component state from `fromVersion` to `toVersion`.
* Migrations are chained automatically: registering 01 and 12 handles saves at version 0.
*/
export function registerMigration(
name: string,
fromVersion: number,
toVersion: number,
fn: MigrationFn,
): void {
if (!migrations.has(name)) migrations.set(name, new Map());
migrations.get(name)!.set(fromVersion, { toVersion, fn });
}
/**
* Apply all registered migrations to bring `state` from `fromVersion` up to the current
* registered version. Returns the migrated state (may be the same object if no migrations ran).
*/
export function migrateState(
name: string,
state: Record<string, unknown>,
fromVersion: number,
): Record<string, unknown> {
const meta = registry.get(name);
if (!meta) return state;
const chain = migrations.get(name);
if (!chain) return state;
let current = fromVersion;
let s = state;
while (current < meta.version) {
const entry = chain.get(current);
if (!entry) throw new Error(
`[registry] No migration for '${name}' from version ${current} to ${meta.version}. ` +
`Register one with registerMigration('${name}', ${current}, ...).`
);
s = entry.fn(s);
current = entry.toVersion;
}
return s;
}
interface ComponentOptions<T> {
name?: string;
version?: number;
variables?: (keyof T)[];
}
type ComponentDecorator<T> = (target: ComponentConstructor<T>, ctx: ClassDecoratorContext) => void;
export function component<T>(target: ComponentConstructor<T>, ctx: ClassDecoratorContext): void;
export function component<T>(name: string): ComponentDecorator<T>;
export function component<T>(options: ComponentOptions<T>): ComponentDecorator<T>;
export function component<T>(
nameOrTargetOrOptions: string | ComponentConstructor<T> | ComponentOptions<T>,
ctx?: ClassDecoratorContext,
): void | ComponentDecorator<T> {
if (typeof nameOrTargetOrOptions === 'string') {
const name = nameOrTargetOrOptions;
return (target: ComponentConstructor<T>) => register(name, target, 0);
}
if (typeof nameOrTargetOrOptions === 'object') {
const { name, version = 0, variables = [] } = nameOrTargetOrOptions;
return (target: ComponentConstructor<T>, ctx: ClassDecoratorContext) => {
register(name ?? String(ctx.name), target, version);
for (const key of variables) {
registerStateVariable(ctx, String(key));
}
}
}
// Used as bare @component
register(String(ctx!.name), nameOrTargetOrOptions, 0);
}

View File

@ -1,7 +1,7 @@
import type { Entity, EvalContext } from "../core/world";
import { World } from "../core/world";
import type { RPGAction, RPGActions, RPGVariables } from "../types"; import type { RPGAction, RPGActions, RPGVariables } from "../types";
import { isEvalContext, World } from "../core/world"; import { getComponentName } from "./decorators";
import type { EvalContext, Entity } from "../core/world";
import { getComponentName } from "../core/registry";
export function resolveVariables(target: Entity | World): RPGVariables { export function resolveVariables(target: Entity | World): RPGVariables {
const result: RPGVariables = {}; const result: RPGVariables = {};

View File

@ -10,6 +10,7 @@ import { Serialization } from "@common/rpg/core/serialization";
import { Factions } from "@common/rpg/components/faction"; import { Factions } from "@common/rpg/components/faction";
import { Effect } from "@common/rpg/components/effect"; import { Effect } from "@common/rpg/components/effect";
import { Equipment } from "@common/rpg/components/equipment"; import { Equipment } from "@common/rpg/components/equipment";
import { Position } from "@common/rpg/components/position";
export default async function main() { export default async function main() {
const world = new World(); const world = new World();
@ -35,6 +36,7 @@ export default async function main() {
}])); }]));
player.add('str', new Stat({ value: 100 })); player.add('str', new Stat({ value: 100 }));
player.add('agl', new Stat({ value: 100 })); player.add('agl', new Stat({ value: 100 }));
player.add(new Position(42, 69));
Factions.join(player, 'boobs'); Factions.join(player, 'boobs');
Factions.adjustReputation(player, 'guards', 10); Factions.adjustReputation(player, 'guards', 10);