File size: 3,278 Bytes
d0eb71c
 
 
 
 
 
 
 
f3c5314
d0eb71c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f92b745
 
 
 
 
 
 
 
 
 
f3c5314
f92b745
 
 
d0eb71c
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// 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
}