Spaces:
Running
Running
| // Pure progression math + event helpers — the single source of truth for the XP curve | |
| // so no UI re-derives it. See docs/rpg-progression-plan.md (Phase 0/1). | |
| // | |
| // Curve: a gentle quadratic so early levels come fast and later ones slow down. | |
| // xpToNext(L) = BASE + GROWTH * (L-1)^1.5, rounded to a tidy step. | |
| const BASE = 50 | |
| const GROWTH = 40 | |
| const ATTR_PER_LEVEL = 0 // level-up reward is now the roguelike perk draft (levelUpCards.js), not points | |
| export function xpToNext(level) { | |
| const l = Math.max(1, level | 0) | |
| return Math.round((BASE + GROWTH * Math.pow(l - 1, 1.5)) / 10) * 10 | |
| } | |
| // Total XP needed to have *reached* a given level from level 1 (handy for bars/among UIs). | |
| export function xpForLevel(level) { | |
| let sum = 0 | |
| for (let l = 1; l < Math.max(1, level | 0); l++) sum += xpToNext(l) | |
| return sum | |
| } | |
| // Apply `amount` XP to a progression object, rolling over as many levels as the XP covers. | |
| // Returns a NEW progression object plus what happened, so callers can react (toast, points). | |
| export function applyXp(progression, amount) { | |
| const p = { level: 1, xp: 0, kills: 0, attributePoints: 0, ...(progression || {}) } | |
| let { level, xp, attributePoints } = p | |
| xp += Math.max(0, amount | 0) | |
| let leveledUp = false, levelsGained = 0 | |
| let need = xpToNext(level) | |
| while (xp >= need) { | |
| xp -= need | |
| level += 1 | |
| levelsGained += 1 | |
| attributePoints += ATTR_PER_LEVEL | |
| leveledUp = true | |
| need = xpToNext(level) | |
| } | |
| return { | |
| progression: { ...p, level, xp, attributePoints }, | |
| leveledUp, | |
| levelsGained, | |
| xpToNext: need, | |
| } | |
| } | |
| // Fraction [0,1] toward the next level — for the XP bar. | |
| export function levelProgress(progression) { | |
| const p = progression || {} | |
| const need = xpToNext(p.level || 1) | |
| return need > 0 ? Math.min(1, Math.max(0, (p.xp || 0) / need)) : 0 | |
| } | |
| // XP a defeated foe is worth. Small table keyed by a coarse tier/class string; unknown | |
| // enemies fall back to a sane default. Tunable in one place. | |
| const ENEMY_XP = { weak: 8, normal: 12, tough: 20, elite: 40, boss: 100 } | |
| export function enemyXpValue(enemy) { | |
| if (!enemy) return ENEMY_XP.normal | |
| if (typeof enemy.xp === 'number') return enemy.xp | |
| const tier = (enemy.tier || enemy.rank || '').toLowerCase() | |
| if (ENEMY_XP[tier]) return ENEMY_XP[tier] | |
| return ENEMY_XP.normal | |
| } | |
| // Combat stat scaling from level + spent attribute points. rank feeds the engine's skill | |
| // scaling; hpMul/dmgMul scale survivability + damage so leveling actually makes you stronger. | |
| export function statBonuses(progression) { | |
| const p = progression || {} | |
| const lvl = p.level || 1 | |
| const spent = p.spent || { hp: 0, dmg: 0 } | |
| return { | |
| rank: Math.min(20, 7 + lvl), | |
| hpMul: 1 + (lvl - 1) * 0.07 + (spent.hp || 0) * 0.05, | |
| dmgMul: 1 + (lvl - 1) * 0.05 + (spent.dmg || 0) * 0.05, | |
| speedMul: 1 + (spent.spd || 0) * 0.06, | |
| } | |
| } | |
| // Append a capped, structured event to a persona's log (newest last). Used for the chat | |
| // "what's happening to you" context (woid-style perception log) and the level-up feed. | |
| const MAX_EVENTS = 60 | |
| export function appendEvent(events, type, data = {}) { | |
| const next = Array.isArray(events) ? events.slice() : [] | |
| next.push({ type, ...data }) | |
| return next.length > MAX_EVENTS ? next.slice(next.length - MAX_EVENTS) : next | |
| } | |