From 30531bcd528d6de0a3c3a3f1dd79fab311e90648 Mon Sep 17 00:00:00 2001 From: Pabloader Date: Wed, 29 Apr 2026 12:24:29 +0000 Subject: [PATCH] XP --- src/common/rpg/components/experience.ts | 69 +++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/common/rpg/components/experience.ts diff --git a/src/common/rpg/components/experience.ts b/src/common/rpg/components/experience.ts new file mode 100644 index 0000000..8c1b6e7 --- /dev/null +++ b/src/common/rpg/components/experience.ts @@ -0,0 +1,69 @@ +import { component } from "../core/registry"; +import { Component } from "../core/world"; +import { action, variable } from "../utils/decorators"; + +/** + * How much XP is required to advance from level N to N+1. + * + * - `number[]` — explicit table; `values[0]` = XP for level 1→2. + * Max level = `values.length + 1`. Past the end the entity is at max level. + * - `{ base, factor }` — geometric: XP[N] = floor(base * factor^(N-1)). + * No max level. + */ +export type ThresholdSpec = number[] | { base: number; factor: number }; + +function xpForStep(spec: ThresholdSpec, level: number): number | null { + if (Array.isArray(spec)) { + const idx = level - 1; + return idx < spec.length ? spec[idx] : null; + } + return Math.floor(spec.base * Math.pow(spec.factor, level - 1)); +} + +@component +export class Experience extends Component<{ + xp: number; + level: number; + xpAtLevel: number; // total XP accumulated when current level was reached + spec: ThresholdSpec; +}> { + constructor(spec: ThresholdSpec) { + super({ xp: 0, level: 1, xpAtLevel: 0, spec }); + } + + /** Current level, starting at 1. Primary variable for conditions: `experience >= 5`. */ + @variable get level(): number { return this.state.level; } + + /** Total accumulated XP (never resets). */ + @variable('.') get xp(): number { return this.state.xp; } + + /** XP accumulated within the current level. */ + @variable get xpInLevel(): number { return this.state.xp - this.state.xpAtLevel; } + + /** XP remaining until the next level, or `null` at max level. */ + get xpToNext(): number | null { + const needed = xpForStep(this.state.spec, this.state.level); + return needed === null ? null : needed - this.xpInLevel; + } + + /** Progress toward the next level as a 0–1 fraction. `1` at max level. */ + @variable get progress(): number { + const needed = xpForStep(this.state.spec, this.state.level); + if (needed === null) return 1; + return Math.min(this.xpInLevel / needed, 1); + } + + /** Add XP and emit `'levelup'` with `{ level, prev }` for each level gained. */ + @action + award(xp: number): void { + this.state.xp += xp; + while (true) { + const needed = xpForStep(this.state.spec, this.state.level); + if (needed === null) break; + if (this.xpInLevel < needed) break; + this.state.xpAtLevel += needed; + const prev = this.state.level++; + this.emit('levelup', { level: this.state.level, prev }); + } + } +}