import { Component } from "../core/world"; import { action, component, 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 }); } } }