69 lines
2.5 KiB
TypeScript
69 lines
2.5 KiB
TypeScript
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 });
|
||
}
|
||
}
|
||
}
|