Move component decorator to all decorators
This commit is contained in:
parent
b549193a3b
commit
6568e1e8d9
|
|
@ -1,5 +1,5 @@
|
|||
import { component } from "../core/registry";
|
||||
import { Component } from "../core/world";
|
||||
import { component } from "../utils/decorators";
|
||||
import { Stat } from "./stat";
|
||||
|
||||
@component
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { component } from "../core/registry";
|
||||
import { Component } from "../core/world";
|
||||
import { action, variable } from "../utils/decorators";
|
||||
import { action, component, variable } from "../utils/decorators";
|
||||
|
||||
@component
|
||||
export class Cooldown extends Component<{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { component } from "../core/registry";
|
||||
import { Component, type EvalContext } from "../core/world";
|
||||
import { evaluateCondition } from "../utils/conditions";
|
||||
import { action, variable } from "../utils/decorators";
|
||||
import { action, component, variable } from "../utils/decorators";
|
||||
import { Stat } from "./stat";
|
||||
|
||||
@component
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { component } from "../core/registry";
|
||||
import { Component } from "../core/world";
|
||||
import { action } from "../utils/decorators";
|
||||
import type { RPGVariables } from "../types";
|
||||
import { action, component } from "../utils/decorators";
|
||||
import { Effect } from "./effect";
|
||||
import { Damage } from "./combat";
|
||||
|
||||
// ── Equippable ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { component } from "../core/registry";
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { component } from "../core/registry";
|
||||
import { Component, type Entity } from "../core/world";
|
||||
import type { RPGVariables } from "../types";
|
||||
import { action } from "../utils/decorators";
|
||||
import { action, component } from "../utils/decorators";
|
||||
|
||||
// ── FactionMember ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -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 } from "../core/registry";
|
||||
import { Stackable, Usable } from "./item";
|
||||
import { Equipment, Equippable } from "./equipment";
|
||||
import type { InventorySlotInput, RPGVariables, SlotId } from "../types";
|
||||
import { action, component } from "../utils/decorators";
|
||||
import { resolveVariables } from "../utils/variables";
|
||||
import { Equipment, Equippable } from "./equipment";
|
||||
import { Stackable, Usable } from "./item";
|
||||
|
||||
interface SlotRecord {
|
||||
slotId: SlotId;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Component, type EvalContext, type World } from "../core/world";
|
||||
import { component } from "../core/registry";
|
||||
import type { RPGAction } from "../types";
|
||||
import { action, variable } from "../utils/decorators";
|
||||
import { action, component, variable } from "../utils/decorators";
|
||||
import { executeAction } from "../utils/variables";
|
||||
import { Equippable } from "./equipment";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, type EvalContext } from "../core/world";
|
||||
import { component } from "../core/registry";
|
||||
import { isQuest, type Quest, type QuestStage, type RPGVariables } from "../types";
|
||||
import { evaluateCondition } from "../utils/conditions";
|
||||
import { component } from "../utils/decorators";
|
||||
|
||||
export type QuestStatus = 'inactive' | 'active' | 'completed' | 'failed';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { action, variable } from "../utils/decorators";
|
||||
import { Component } from "../core/world";
|
||||
import { component } from "../core/registry";
|
||||
import { action, component, variable } from "../utils/decorators";
|
||||
|
||||
interface StatState {
|
||||
base: number;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import type { RPGVariables } from "../types";
|
||||
import { action } from "../utils/decorators";
|
||||
import { Component } from "../core/world";
|
||||
import { component } from "../core/registry";
|
||||
import type { RPGVariables } from "../types";
|
||||
import { action, component } from "../utils/decorators";
|
||||
|
||||
interface Var {
|
||||
key: string;
|
||||
|
|
|
|||
|
|
@ -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 0→1 and 1→2 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);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
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. */
|
||||
const SCHEMA_VERSION = 1;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { ACTION_KEYS, VARIABLE_KEYS } from '../utils/decorators';
|
||||
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> {
|
||||
target: Entity;
|
||||
|
|
@ -59,14 +58,24 @@ export abstract class Component<TState = Record<string, unknown>> {
|
|||
getVariables(): RPGVariables {
|
||||
const meta = (this.constructor as Function)[Symbol.metadata];
|
||||
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 = {};
|
||||
if (keys) {
|
||||
for (const [methodKey, exportName] of keys) {
|
||||
const v = (this as Record<string, unknown>)[String(methodKey)];
|
||||
if (typeof v === 'number' || typeof v === 'string' || typeof v === 'boolean') {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { Component } from "../core/world";
|
||||
import type { RPGVariables } from "../types";
|
||||
|
||||
// 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 VARIABLE_KEYS = Symbol('rpg.variables');
|
||||
export const STATE_KEYS = Symbol('rpg.state_variables');
|
||||
|
||||
export function action<T extends (arg?: any, ctx?: any) => unknown>(
|
||||
target: T,
|
||||
|
|
@ -12,18 +14,23 @@ export function action<T extends (arg?: any, ctx?: any) => unknown>(
|
|||
): T {
|
||||
const prev = context.metadata[ACTION_KEYS] as Set<string | symbol> | undefined;
|
||||
context.metadata[ACTION_KEYS] = new Set(prev).add(context.name);
|
||||
return function(this: any, arg?: any, ctx?: any) {
|
||||
return function (this: any, arg?: any, ctx?: any) {
|
||||
return target.call(this, arg, ctx ?? this.context);
|
||||
} as unknown as T;
|
||||
}
|
||||
|
||||
type VariableContext<T extends RPGVariables[string]> =
|
||||
| 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;
|
||||
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;
|
||||
|
|
@ -41,3 +48,107 @@ export function variable(
|
|||
}
|
||||
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 0→1 and 1→2 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Entity, EvalContext } from "../core/world";
|
||||
import { World } from "../core/world";
|
||||
import type { RPGAction, RPGActions, RPGVariables } from "../types";
|
||||
import { isEvalContext, World } from "../core/world";
|
||||
import type { EvalContext, Entity } from "../core/world";
|
||||
import { getComponentName } from "../core/registry";
|
||||
import { getComponentName } from "./decorators";
|
||||
|
||||
export function resolveVariables(target: Entity | World): RPGVariables {
|
||||
const result: RPGVariables = {};
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Serialization } from "@common/rpg/core/serialization";
|
|||
import { Factions } from "@common/rpg/components/faction";
|
||||
import { Effect } from "@common/rpg/components/effect";
|
||||
import { Equipment } from "@common/rpg/components/equipment";
|
||||
import { Position } from "@common/rpg/components/position";
|
||||
|
||||
export default async function main() {
|
||||
const world = new World();
|
||||
|
|
@ -35,6 +36,7 @@ export default async function main() {
|
|||
}]));
|
||||
player.add('str', new Stat({ value: 100 }));
|
||||
player.add('agl', new Stat({ value: 100 }));
|
||||
player.add(new Position(42, 69));
|
||||
|
||||
Factions.join(player, 'boobs');
|
||||
Factions.adjustReputation(player, 'guards', 10);
|
||||
|
|
|
|||
Loading…
Reference in New Issue