playlist-data-engine 1.7.2 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +20 -3
  2. package/bin/cli.cjs +85 -0
  3. package/dist/core/parser/TrackExtras.d.ts +150 -1
  4. package/dist/core/parser/TrackExtras.d.ts.map +1 -1
  5. package/dist/gateway-CDMPqFEH.js +1320 -0
  6. package/dist/gateway-DKa45Uz6.cjs +6 -0
  7. package/dist/gateway.d.ts +3 -1
  8. package/dist/gateway.d.ts.map +1 -1
  9. package/dist/gateway.js +1 -1
  10. package/dist/gateway.mjs +22 -14
  11. package/dist/index.d.ts +4 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/playlist-data-engine.js +34 -34
  14. package/dist/playlist-data-engine.mjs +964 -1240
  15. package/dist/utils/engineDocs.d.ts +33 -0
  16. package/dist/utils/engineDocs.d.ts.map +1 -0
  17. package/dist/utils/playlistUtils.d.ts +37 -0
  18. package/dist/utils/playlistUtils.d.ts.map +1 -1
  19. package/dist/utils/validators.d.ts +27 -0
  20. package/dist/utils/validators.d.ts.map +1 -1
  21. package/docs/DATA_ENGINE_REFERENCE.md +6660 -0
  22. package/docs/USAGE_IN_OTHER_PROJECTS.md +587 -0
  23. package/docs/features/AUDIO_ANALYSIS.md +610 -0
  24. package/docs/features/BEAT_DETECTION.md +5250 -0
  25. package/docs/features/COMBAT_SYSTEM.md +1632 -0
  26. package/docs/features/CONTENT_PACKS.md +464 -0
  27. package/docs/features/CUSTOM_CONTENT.md +603 -0
  28. package/docs/features/ENEMY_GENERATION.md +1711 -0
  29. package/docs/features/EQUIPMENT_SYSTEM.md +2279 -0
  30. package/docs/features/EXTENSIBILITY_GUIDE.md +1106 -0
  31. package/docs/features/GATEWAY_RESOLUTION.md +725 -0
  32. package/docs/features/IRL_SENSORS.md +360 -0
  33. package/docs/features/PLAYLIST_PARSING.md +446 -0
  34. package/docs/features/PREREQUISITES.md +571 -0
  35. package/docs/features/ROLLS_AND_SEEDS.md +687 -0
  36. package/docs/features/XP_AND_STATS.md +1221 -0
  37. package/llms.txt +33 -0
  38. package/package.json +9 -2
  39. package/skills/playlist-data-engine/SKILL.md +69 -0
  40. package/dist/gateway-DUk4nCao.cjs +0 -1
  41. package/dist/gateway-DyR4M-uH.js +0 -681
@@ -0,0 +1,1221 @@
1
+ # XP Leveling and Stats Reference
2
+
3
+ Complete guide to XP, leveling, and stats in the Playlist Data Engine.
4
+
5
+ **For API details, see [DATA_ENGINE_REFERENCE.md](../DATA_ENGINE_REFERENCE.md)**
6
+ **For other usage examples, see [USAGE_IN_OTHER_PROJECTS.md](../USAGE_IN_OTHER_PROJECTS.md)**
7
+
8
+ ---
9
+
10
+ ## Table of Contents
11
+
12
+ 1. [XP and Leveling](#xp-and-leveling)
13
+ 2. [Rhythm Game XP](#rhythm-game-xp)
14
+ 3. [Track Mastery Prestige System](#track-mastery-prestige-system)
15
+ 4. [Stat Strategies](#stat-strategies)
16
+ 5. [XP Scaling](#xp-scaling)
17
+ 6. [Progression Configuration](#progression-configuration)
18
+
19
+ ---
20
+
21
+ ## XP and Leveling
22
+
23
+
24
+ ### Earning XP from Listening to Music
25
+
26
+ Track a listening session, calculate XP earned with modifiers, and apply it to your character. Level-ups happen automatically when XP thresholds are reached.
27
+
28
+ ```typescript
29
+ import {
30
+ SessionTracker,
31
+ XPCalculator,
32
+ CharacterUpdater,
33
+ MasterySystem
34
+ } from 'playlist-data-engine';
35
+
36
+ // ===== STEP 1: TRACK LISTENING SESSIONS =====
37
+ const tracker = new SessionTracker();
38
+
39
+ // Start a session - returns a sessionId (required for ending the session)
40
+ const sessionId = tracker.startSession(track.id, track);
41
+
42
+ // ... user listens to a track for 300 seconds (5 minutes) ...
43
+
44
+ // End the session - requires the sessionId returned from startSession()
45
+ const session = tracker.endSession(sessionId);
46
+
47
+ if (session) {
48
+ // ===== STEP 2: CALCULATE XP WITH MODIFIERS =====
49
+ const xpCalc = new XPCalculator();
50
+
51
+ // Base XP: 1 XP per second of listening
52
+ const baseXP = session.duration; // 300 seconds = 300 base XP
53
+
54
+ // Environmental modifiers (examples):
55
+ // - Running: 1.5x | Walking: 1.2x | Night time: 1.25x
56
+ // - Extreme weather: 1.4x | High altitude (≥2000m): 1.3x
57
+
58
+ // Gaming modifiers (examples):
59
+ // - Base gaming bonus: +0.25x
60
+ // - RPG game: +0.20x | Action/FPS: +0.15x | Multiplayer: +0.15x
61
+ // - Long session (4+ hours): up to +0.20x
62
+
63
+ // Total calculation is capped at 3.0x multiplier
64
+ // Example: Running (1.5x) + Playing RPG (1.75x) = 2.625x total
65
+ const totalXP = xpCalc.calculateSessionXP(session, track);
66
+
67
+ // ===== STEP 3: APPLY SESSION TO CHARACTER =====
68
+ const updater = new CharacterUpdater();
69
+ const previousListenCount = tracker.getTrackListenCount(track.id) - 1;
70
+ const result = updater.updateCharacterFromSession(character, session, track, previousListenCount);
71
+
72
+ // ===== STEP 4: BASIC LEVEL-UP HANDLING =====
73
+ if (result.leveledUp) {
74
+ console.log(`Level up! Now level ${result.newLevel}`);
75
+ }
76
+
77
+ // Check for track mastery
78
+ if (result.masteredTrack) {
79
+ console.log(`Track mastered! ${result.masteryBonusXP} bonus XP unlocked!`);
80
+ }
81
+ }
82
+ ```
83
+
84
+ **Note**: By default, `CharacterUpdater` uses automatic stat increases (`dnD5e_smart` strategy). Stats are intelligently selected based on the character's class and current stats - **no manual intervention required**. This ensures the simple example above works perfectly, with stats increasing automatically on level-up (at levels 4, 8, 12, 16, 19).
85
+
86
+ To use manual D&D 5e rules (player must choose stats), pass a custom `StatManager`:
87
+
88
+ ```typescript
89
+ import { StatManager } from 'playlist-data-engine';
90
+
91
+ const statManager = new StatManager({ strategy: 'dnD5e' });
92
+ const updater = new CharacterUpdater(statManager);
93
+ ```
94
+
95
+ #### Comprehensive Level-Up Celebration & Multi-Source XP System
96
+
97
+ The `levelUpDetails` returned by both `updateCharacterFromSession()` and `addXP()` contains everything you need for that "LEVELED UP!" celebration experience. All XP sources work identically.
98
+
99
+ ```typescript
100
+ import { CharacterUpdater } from 'playlist-data-engine';
101
+
102
+ const updater = new CharacterUpdater();
103
+
104
+ // ===== REUSABLE CELEBRATION FUNCTION =====
105
+ function celebrateLevelUp(result: LevelUpResult, source: string) {
106
+ if (!result.leveledUp) return;
107
+
108
+ console.log(`🎉 LEVELED UP from ${source}!`);
109
+ console.log(`Level ${result.levelUpDetails![0].fromLevel} → ${result.newLevel}!`);
110
+ console.log(`Gained ${result.levelUpDetails?.length} level(s) at once!`);
111
+
112
+ for (const detail of result.levelUpDetails!) {
113
+ console.log(`=== Level ${detail.fromLevel} → ${detail.toLevel} ===`);
114
+ console.log(`HP: +${detail.hpIncrease} (new max: ${detail.newMaxHP})`);
115
+
116
+ if (detail.proficiencyIncrease > 0) {
117
+ console.log(`Proficiency: +${detail.proficiencyIncrease} (new: ${detail.newProficiency})`);
118
+ }
119
+ if (detail.statIncreases?.length) {
120
+ console.log(`Stats: ${detail.statIncreases.map(s => `${s.ability} ${s.oldValue}→${s.newValue}`).join(', ')}`);
121
+ }
122
+ if (detail.featuresGained?.length) {
123
+ console.log(`Features: ${detail.featuresGained.join(', ')}`);
124
+ }
125
+ if (detail.newSpellSlots) {
126
+ console.log(`Spell Slots:`, detail.newSpellSlots);
127
+ }
128
+ }
129
+ }
130
+
131
+ // ===== USAGE: ALL XP SOURCES USE THE SAME PATTERN =====
132
+ // Music listening
133
+ const musicResult = updater.updateCharacterFromSession(character, session, track, listenCount);
134
+ celebrateLevelUp(musicResult, 'Music Session');
135
+
136
+ // Combat, quests, custom activities - same pattern
137
+ const combatResult = updater.addXP(character, 500, 'combat');
138
+ celebrateLevelUp(combatResult, 'Combat');
139
+
140
+ const questResult = updater.addXP(character, 1000, 'quest');
141
+ celebrateLevelUp(questResult, 'Quest Complete');
142
+
143
+ // Boss rewards can trigger multiple level-ups at once!
144
+ const bossResult = updater.addXP(character, 10000, 'boss_defeat');
145
+ celebrateLevelUp(bossResult, 'Boss Defeat');
146
+
147
+ // ===== OPTIONAL: XP SOURCE TRACKING =====
148
+ // Track XP sources for analytics (application-level implementation)
149
+ const xpHistory: Array<{ source: string; amount: number; timestamp: number }> = [];
150
+
151
+ function addXPWithTracking(character: CharacterSheet, amount: number, source: string) {
152
+ const result = updater.addXP(character, amount, source);
153
+ xpHistory.push({ source, amount, timestamp: Date.now() });
154
+ celebrateLevelUp(result, source);
155
+ return result;
156
+ }
157
+
158
+ // Analyze: const combatXP = xpHistory.filter(h => h.source.startsWith('combat')).reduce((sum, h) => sum + h.amount, 0);
159
+ ```
160
+
161
+ **Type Reference:**
162
+ - `CharacterUpdateResult` - Return type from `updateCharacterFromSession()` and `addXP()` - [*src/core/progression/CharacterUpdater.ts*](src/core/progression/CharacterUpdater.ts)
163
+ - `LevelUpDetail` - Contains `fromLevel`, `toLevel`, `hpIncrease`, `statIncreases`, etc. - [*src/core/types/Progression.ts*](src/core/types/Progression.ts)
164
+
165
+ **All XP Sources Return the Same Detailed Breakdown:**
166
+
167
+ | Source | Method | XP Calculation | Level-Up Details |
168
+ |--------|--------|----------------|------------------|
169
+ | Music Listening | `updateCharacterFromSession()` | Duration × modifiers | ✅ Full breakdown |
170
+ | Combat | `addXP()` | Direct amount | ✅ Full breakdown |
171
+ | Quests | `addXP()` | Direct amount | ✅ Full breakdown |
172
+ | Custom Activities | `addXP()` | Direct amount | ✅ Full breakdown |
173
+ | Rhythm Game | `addRhythmXP()` | Accuracy × combo × groove | ✅ Full breakdown |
174
+
175
+ **Level-Up Result Properties:**
176
+ - `leveledUp` - Whether character leveled up
177
+ - `newLevel` - New level (if leveled up)
178
+ - `levelUpDetails` - Array of HP, stat, feature, and spell slot changes
179
+ - `xpEarned` - Amount of XP earned
180
+ - `masteredTrack` - (Music only) Whether track was mastered
181
+ - `masteryBonusXP` - (Music only) Bonus XP from mastery
182
+
183
+
184
+ ## Rhythm Game XP
185
+
186
+ The rhythm game XP system rewards players for timing accuracy, combo streaks, and groove meter performance. It integrates with the beat detection system to provide character progression from rhythm gameplay.
187
+
188
+ **Key Concept: Score vs XP**
189
+ The system separates "score points" (for in-game display/leaderboards) from "character XP" (for progression) via the `xpRatio` parameter:
190
+ - **Score Points**: Raw values from accuracy (perfect = 10, great = 7, etc.) - for display
191
+ - **Character XP**: Score converted via `xpRatio` (default: 0.1, so 10 score = 1 XP) - for progression
192
+
193
+ ### Basic Usage with BeatStream and GrooveAnalyzer
194
+
195
+ ```typescript
196
+ import {
197
+ BeatMapGenerator,
198
+ BeatStream,
199
+ GrooveAnalyzer,
200
+ RhythmXPCalculator,
201
+ CharacterUpdater,
202
+ shouldAccuracyBreakCombo
203
+ } from 'playlist-data-engine';
204
+
205
+ // ===== SETUP =====
206
+ const generator = new BeatMapGenerator();
207
+ const beatMap = await generator.generateBeatMap('song.mp3', 'track-1');
208
+ const beatStream = new BeatStream(beatMap, audioContext);
209
+ const grooveAnalyzer = new GrooveAnalyzer();
210
+ const rhythmXP = new RhythmXPCalculator(); // Uses defaults
211
+ const updater = new CharacterUpdater();
212
+
213
+ // Start session tracking
214
+ rhythmXP.startSession();
215
+ let comboCount = 0;
216
+
217
+ // ===== ON BUTTON PRESS =====
218
+ function onButtonPress(timestamp: number) {
219
+ // 1. Check button press accuracy
220
+ const buttonResult = beatStream.checkButtonPress(timestamp);
221
+
222
+ // 2. Record hit for groove analysis
223
+ const grooveResult = grooveAnalyzer.recordHit(
224
+ buttonResult.offset,
225
+ beatStream.getCurrentBpm(),
226
+ buttonResult.matchedBeat?.time,
227
+ buttonResult.accuracy // 'miss' or 'wrongKey' will hurt groove
228
+ );
229
+
230
+ // 3. Check if combo is about to break (before updating)
231
+ // Use shouldAccuracyBreakCombo helper - respects okBreaksCombo config setting
232
+ const comboBeforeHit = comboCount;
233
+ const config = rhythmXP.getConfig();
234
+ const isComboBreaker = shouldAccuracyBreakCombo(buttonResult.accuracy, config.combo.okBreaksCombo);
235
+
236
+ // 4. Update combo
237
+ if (isComboBreaker) {
238
+ comboCount = 0;
239
+ } else {
240
+ comboCount++;
241
+ }
242
+
243
+ // 5. Calculate XP for this hit AND update session totals
244
+ const xpResult = rhythmXP.recordHit(buttonResult.accuracy, {
245
+ comboLength: comboCount,
246
+ grooveHotness: grooveResult.hotness
247
+ });
248
+
249
+ console.log(`Accuracy: ${buttonResult.accuracy}`);
250
+ console.log(`Score: ${xpResult.finalScore.toFixed(1)} points (for leaderboards)`);
251
+ console.log(`XP: ${xpResult.finalXP.toFixed(2)} (added to character)`);
252
+
253
+ // 6. Add XP to character (triggers level-ups!)
254
+ const updateResult = updater.addRhythmXP(character, xpResult, 'rhythm_game');
255
+
256
+ if (updateResult.leveledUp) {
257
+ console.log(`🎉 LEVELED UP to ${updateResult.newLevel}!`);
258
+ if (updateResult.levelUpDetails) {
259
+ for (const detail of updateResult.levelUpDetails) {
260
+ console.log(` HP: +${detail.hpIncrease}`);
261
+ }
262
+ }
263
+ }
264
+
265
+ // 7. If combo broke, award combo end bonus
266
+ if (isComboBreaker && comboBeforeHit > 0) {
267
+ const comboBonus = rhythmXP.calculateComboEndBonus(comboBeforeHit);
268
+ console.log(`Combo ended! ${comboBeforeHit} hit streak → +${comboBonus.bonusXP.toFixed(2)} XP bonus`);
269
+ updater.addXP(character, comboBonus.bonusXP, 'combo_bonus');
270
+ }
271
+
272
+ // 8. Check if groove ended (endedGrooveStats is present when groove just ended)
273
+ // This is returned directly in the result - no need for separate game loop!
274
+ if (grooveResult.endedGrooveStats) {
275
+ const grooveBonus = rhythmXP.calculateGrooveEndBonus(grooveResult.endedGrooveStats);
276
+ console.log(`🔥 Groove ended! Duration: ${grooveResult.endedGrooveStats.duration.toFixed(1)}s`);
277
+ console.log(` Avg Hotness: ${grooveResult.endedGrooveStats.avgHotness.toFixed(1)}%`);
278
+ console.log(` Bonus: +${grooveBonus.bonusXP.toFixed(2)} XP`);
279
+ updater.addXP(character, grooveBonus.bonusXP, 'groove_bonus');
280
+ }
281
+ }
282
+
283
+ // ===== ON SESSION END =====
284
+ function onSessionEnd() {
285
+ const finalStats = rhythmXP.endSession();
286
+
287
+ console.log('=== Session Complete ===');
288
+ console.log(`Total Score: ${finalStats?.totalScore.toFixed(0)}`);
289
+ console.log(`Total XP: ${finalStats?.totalXP.toFixed(2)}`);
290
+ console.log(`Max Combo: ${finalStats?.maxCombo}`);
291
+ console.log(`Duration: ${finalStats?.duration.toFixed(1)}s`);
292
+ console.log(`Accuracy: ${finalStats?.accuracyPercentage.toFixed(1)}%`);
293
+ console.log('Accuracy Distribution:', finalStats?.accuracyDistribution);
294
+
295
+ // Check for any active groove at session end
296
+ const grooveState = grooveAnalyzer.getState();
297
+ if (grooveState.avgHotness > 0 && grooveState.grooveHitCount > 0) {
298
+ const grooveBonus = rhythmXP.calculateGrooveEndBonus({
299
+ maxStreak: grooveState.streakLength,
300
+ maxHotness: grooveState.maxHotness,
301
+ avgHotness: grooveState.avgHotness,
302
+ duration: grooveState.grooveDuration,
303
+ totalHits: grooveState.grooveHitCount,
304
+ startTime: grooveState.grooveStartTime ?? 0,
305
+ endTime: Date.now() / 1000,
306
+ });
307
+ console.log(`Final groove bonus: +${grooveBonus.bonusXP.toFixed(2)} XP`);
308
+ updater.addXP(character, grooveBonus.bonusXP, 'groove_bonus');
309
+ grooveAnalyzer.resetGrooveStats();
310
+ }
311
+ }
312
+ ```
313
+
314
+ ### Configuration Options
315
+
316
+ ```typescript
317
+ import { RhythmXPCalculator, mergeRhythmXPConfig, type RhythmXPConfig } from 'playlist-data-engine';
318
+
319
+ // ===== DEFAULT CONFIGURATION =====
320
+ // Default values are tuned for D&D 5e progression:
321
+ // - xpRatio: 0.1 (10 score points = 1 character XP)
322
+ // - Combo cap: 5.0x at 200 combo
323
+ // - okBreaksCombo: true ("ok" accuracy breaks combo streaks)
324
+ // - Groove end bonus: enabled
325
+
326
+ // ===== CUSTOM CONFIGURATION =====
327
+ const customConfig: Partial<RhythmXPConfig> = {
328
+ // Base XP values (score points) for each accuracy level
329
+ baseXP: {
330
+ perfect: 10,
331
+ great: 7,
332
+ good: 5,
333
+ ok: 2,
334
+ miss: 0,
335
+ wrongKey: 0, // Can be negative for score penalty (XP floored at 0)
336
+ },
337
+
338
+ // Score-to-XP conversion ratio
339
+ xpRatio: 0.1, // 10 score = 1 XP (tuned for D&D 5e progression)
340
+
341
+ // Combo multiplier settings
342
+ combo: {
343
+ enabled: true,
344
+ cap: 5.0, // Max 5x multiplier
345
+ okBreaksCombo: true, // "ok" accuracy breaks combo (default: true). Set to false for easier gameplay.
346
+ // Custom formula (optional)
347
+ // formula: (combo) => 1 + Math.log10(combo + 1),
348
+ endBonus: {
349
+ enabled: true,
350
+ // Custom formula (optional): comboLength * 2 is default
351
+ // formula: (combo) => Math.floor(combo * 1.5),
352
+ },
353
+ },
354
+
355
+ // Groove XP settings
356
+ groove: {
357
+ perHitMultiplier: false, // If true: multiplier += (hotness/100) * perHitScale
358
+ perHitScale: 1.0,
359
+ endBonus: {
360
+ enabled: true,
361
+ maxStreakWeight: 0.5, // How much max streak matters
362
+ avgHotnessWeight: 0.5, // How much average hotness matters
363
+ durationWeight: 0.25, // How long groove lasted
364
+ },
365
+ },
366
+
367
+ maxMultiplier: 5.0, // Total multiplier cap
368
+ };
369
+
370
+ const rhythmXP = new RhythmXPCalculator(customConfig);
371
+ ```
372
+
373
+ ### Custom Combo Formulas
374
+
375
+ ```typescript
376
+ import { RhythmXPCalculator } from 'playlist-data-engine';
377
+
378
+ // ===== EXPONENTIAL GROWTH (Uncapped feel) =====
379
+ // Slower scaling at high combos, faster at low combos
380
+ const exponentialXP = new RhythmXPCalculator({
381
+ combo: {
382
+ enabled: true,
383
+ cap: 10.0,
384
+ formula: (combo) => 1 + Math.log10(combo + 1),
385
+ // At 10 combo = 2.0x, at 100 combo = 3.0x, at 1000 combo = 4.0x
386
+ endBonus: { enabled: true },
387
+ },
388
+ });
389
+
390
+ // ===== STEP-BASED (Tiered progression) =====
391
+ // Every 10 hits = +0.1x, predictable milestones
392
+ const stepXP = new RhythmXPCalculator({
393
+ combo: {
394
+ enabled: true,
395
+ cap: 5.0,
396
+ formula: (combo) => 1 + Math.floor(combo / 10) * 0.1,
397
+ // At 10 combo = 1.1x, at 50 combo = 1.5x, at 100 combo = 2.0x
398
+ endBonus: { enabled: true },
399
+ },
400
+ });
401
+
402
+ // ===== OTHER VARIATIONS =====
403
+ // Aggressive: formula: (combo) => 1 + (combo / 25) // 2x at 25 combo
404
+ // Custom end bonus: endBonus: { formula: (combo) => Math.floor(combo * 1.5) }
405
+ ```
406
+
407
+ ### Groove End Bonus
408
+
409
+ When groove ends (hotness drops to 0 or direction changes), stats are returned directly in `grooveResult.endedGrooveStats` - no separate game loop needed!
410
+
411
+ **Groove ends when:**
412
+ 1. Hotness drops to 0 (player broke their pocket too many times)
413
+ 2. Direction changes from push ↔ pull (player shifted from ahead to behind beat)
414
+ 3. Session ends (manually check `grooveState` for any active groove)
415
+
416
+ **What resets when groove ends:**
417
+ - `streakLength`, `grooveStartTime`, `maxHotness`, `hotnessSamples`, `grooveHitCount` → all reset
418
+ - **Note:** `establishedOffset` and `pocketDirection` are NOT reset - only groove statistics
419
+
420
+ ### Per-Hit Groove Multiplier Mode
421
+
422
+ ```typescript
423
+ import { RhythmXPCalculator } from 'playlist-data-engine';
424
+
425
+ // By default, groove only affects the end bonus.
426
+ // Enable per-hit mode to add groove to every hit's multiplier:
427
+
428
+ const perHitGrooveXP = new RhythmXPCalculator({
429
+ groove: {
430
+ perHitMultiplier: true, // Enable per-hit groove bonus
431
+ perHitScale: 1.0, // At 100% hotness = +1.0x to multiplier
432
+ endBonus: { enabled: true }, // End bonus still works too!
433
+ },
434
+ });
435
+
436
+ // Example: At 80% hotness with 50 combo:
437
+ // - Combo multiplier: 2.0x (1 + 50/50)
438
+ // - Groove multiplier: 0.8x (80/100 * 1.0)
439
+ // - Total: 2.8x (capped at maxMultiplier)
440
+ ```
441
+
442
+ ### Session Tracking for UI Display
443
+
444
+ ```typescript
445
+ import { RhythmXPCalculator } from 'playlist-data-engine';
446
+
447
+ const rhythmXP = new RhythmXPCalculator();
448
+
449
+ // Start tracking
450
+ rhythmXP.startSession();
451
+
452
+ // On each hit, recordHit() updates totals automatically
453
+ rhythmXP.recordHit('perfect', { comboLength: 10 });
454
+ rhythmXP.recordHit('great', { comboLength: 11 });
455
+ rhythmXP.recordHit('miss', { comboLength: 0 });
456
+
457
+ // Get running totals for UI
458
+ const stats = rhythmXP.getSessionTotals();
459
+ if (stats) {
460
+ console.log(`Score: ${stats.totalScore}`);
461
+ console.log(`XP: ${stats.totalXP}`);
462
+ console.log(`Accuracy: ${stats.accuracyPercentage.toFixed(1)}%`);
463
+ console.log(`Perfect: ${stats.accuracyDistribution.perfect}`);
464
+ console.log(`Great: ${stats.accuracyDistribution.great}`);
465
+ console.log(`Max Combo: ${stats.maxCombo}`);
466
+ }
467
+
468
+ // End session and get final stats
469
+ const finalStats = rhythmXP.endSession();
470
+ ```
471
+
472
+ ### Stateless Usage (Frontend Tracks Combo)
473
+
474
+ ```typescript
475
+ import { RhythmXPCalculator, shouldAccuracyBreakCombo } from 'playlist-data-engine';
476
+
477
+ const rhythmXP = new RhythmXPCalculator({ xpRatio: 0.1 });
478
+
479
+ // Get config for combo breaking behavior
480
+ const config = rhythmXP.getConfig();
481
+
482
+ // Frontend manages combo tracking
483
+ let currentCombo = 0;
484
+
485
+ function onHit(accuracy: 'perfect' | 'great' | 'good' | 'ok' | 'miss' | 'wrongKey', grooveHotness: number) {
486
+ // Calculate XP without internal session tracking
487
+ const result = rhythmXP.calculateButtonPressXP(accuracy, {
488
+ comboLength: currentCombo,
489
+ grooveHotness,
490
+ });
491
+
492
+ // Frontend updates combo using helper function
493
+ if (shouldAccuracyBreakCombo(accuracy, config.combo.okBreaksCombo)) {
494
+ // Get end bonus before resetting
495
+ if (currentCombo > 0) {
496
+ const bonus = rhythmXP.calculateComboEndBonus(currentCombo);
497
+ console.log(`Combo bonus: +${bonus.bonusXP} XP`);
498
+ }
499
+ currentCombo = 0;
500
+ } else {
501
+ currentCombo++;
502
+ }
503
+
504
+ return result;
505
+ }
506
+ ```
507
+
508
+ ### Combo Breaking Behavior
509
+
510
+ The `okBreaksCombo` config option controls whether "ok" accuracy breaks the combo streak:
511
+
512
+ | Setting | Behavior | Use Case |
513
+ |---------|----------|----------|
514
+ | `true` (default) | "ok" resets combo to 0 | Stricter gameplay, rewards precision |
515
+ | `false` | "ok" keeps combo going | More forgiving, easier for beginners |
516
+
517
+ **Important:** The groove meter is NOT affected by this setting. "ok" accuracy always contributes positively to the groove meter regardless of `okBreaksCombo`. This keeps the two systems independent:
518
+ - **Combo**: Rewards consecutive precise hits (affected by `okBreaksCombo`)
519
+ - **Groove**: Rewards consistent timing feel (always counts "ok" as valid)
520
+
521
+ ```typescript
522
+ import { RhythmXPCalculator, shouldAccuracyBreakCombo } from 'playlist-data-engine';
523
+
524
+ // Stricter mode (default): "ok" breaks combo
525
+ const strictRhythmXP = new RhythmXPCalculator();
526
+ const strictConfig = strictRhythmXP.getConfig();
527
+ console.log(shouldAccuracyBreakCombo('ok', strictConfig.combo.okBreaksCombo)); // true
528
+
529
+ // Easier mode: "ok" keeps combo going
530
+ const easyRhythmXP = new RhythmXPCalculator({
531
+ combo: { okBreaksCombo: false }
532
+ });
533
+ const easyConfig = easyRhythmXP.getConfig();
534
+ console.log(shouldAccuracyBreakCombo('ok', easyConfig.combo.okBreaksCombo)); // false
535
+ ```
536
+
537
+ ### Expected XP Rates (Default Config)
538
+
539
+ With `xpRatio: 0.1` and default multipliers:
540
+
541
+ **Per-Hit XP (no multipliers):**
542
+ | Accuracy | Score | XP |
543
+ |----------|-------|-----|
544
+ | Perfect | 10 | 1.0 |
545
+ | Great | 7 | 0.7 |
546
+ | Good | 5 | 0.5 |
547
+ | Ok | 2 | 0.2 |
548
+ | Miss | 0 | 0 |
549
+ | Wrong Key | 0 | 0 |
550
+
551
+ **Combo Multiplier Scaling:**
552
+ | Combo | Multiplier |
553
+ |-------|------------|
554
+ | 0 | 1.0x |
555
+ | 25 | 1.5x |
556
+ | 50 | 2.0x |
557
+ | 100 | 3.0x |
558
+ | 200+ | 5.0x (cap) |
559
+
560
+ **Typical 3-minute song at 120 BPM (~360 beats):**
561
+ - 80% perfect, 15% great, 5% good = ~320 base XP
562
+ - With average 50-combo (2x multiplier) = ~640 XP effective
563
+ - With groove end bonus = additional ~20-50 XP
564
+
565
+ **Level progression estimate:**
566
+ | Level | XP Required | Songs (good performance) |
567
+ |-------|-------------|--------------------------|
568
+ | 1→2 | 300 | ~1 song |
569
+ | 2→3 | 600 | ~2 songs |
570
+ | 3→4 | 1,800 | ~4-5 songs |
571
+ | 4→5 | 3,800 | ~8-10 songs |
572
+
573
+ **Tuning tips:**
574
+ - Faster leveling: Set `xpRatio: 0.2` (doubles XP rate)
575
+ - Slower leveling: Set `xpRatio: 0.05` (halves XP rate)
576
+ - Emphasize combos: Increase `combo.cap` to 10.0
577
+ - Emphasize groove: Set `groove.perHitMultiplier: true`
578
+
579
+ ### Listening XP Boost While Playing Rhythm Game
580
+
581
+ In addition to per-button-press XP, the system boosts background listening XP when rhythm game mode is active. Configure via `ProgressionConfig`:
582
+
583
+ ```typescript
584
+ import { mergeProgressionConfig } from 'playlist-data-engine';
585
+
586
+ mergeProgressionConfig({
587
+ xp: {
588
+ activity_bonuses: {
589
+ // Rhythm game bonuses (apply to listening XP)
590
+ rhythm_game_base: 1.25, // +25% base when rhythm game active
591
+ rhythm_game_combo: 0.5, // Up to +50% at max combo
592
+ rhythm_game_groove: 0.5, // Up to +50% at 100% hotness
593
+ },
594
+ },
595
+ });
596
+ ```
597
+
598
+ See [Progression Configuration](#progression-configuration) for more details on activity bonuses.
599
+
600
+ **Type Reference:**
601
+ - `RhythmXPConfig` - Configuration interface - [*src/core/types/RhythmXP.ts*](src/core/types/RhythmXP.ts)
602
+ - `RhythmXPResult` - Result from `calculateButtonPressXP()` - [*src/core/types/RhythmXP.ts*](src/core/types/RhythmXP.ts)
603
+ - `RhythmSessionTotals` - Session statistics - [*src/core/types/RhythmXP.ts*](src/core/types/RhythmXP.ts)
604
+ - `GrooveStats` - Groove end bonus stats - [*src/core/types/RhythmXP.ts*](src/core/types/RhythmXP.ts)
605
+ - `RhythmXPCalculator` - Main calculator class - [*src/core/progression/RhythmXPCalculator.ts*](src/core/progression/RhythmXPCalculator.ts)
606
+
607
+
608
+ ## Track Mastery Prestige System
609
+
610
+ The prestige system allows players to reset their character after mastering a track (meeting BOTH plays AND XP thresholds) in exchange for a visual badge upgrade. Higher prestige levels require more plays and XP.
611
+
612
+ **Key Features:**
613
+ - 10 prestige levels (Roman numerals I-X)
614
+ - Dual requirements: plays AND XP (prevents "cheesing" via play/pause spam)
615
+ - 1.5x scaling per level (10 plays + 1,000 XP at base, up to 584 plays + 57,666 XP at max)
616
+ - Character resets to level 1, but equipment is preserved
617
+
618
+ ### Checking Prestige Eligibility
619
+
620
+ ```typescript
621
+ import {
622
+ SessionTracker,
623
+ CharacterUpdater,
624
+ PrestigeSystem,
625
+ type PrestigeLevel
626
+ } from 'playlist-data-engine';
627
+
628
+ const tracker = new SessionTracker();
629
+ const updater = new CharacterUpdater();
630
+
631
+ // Get character's current prestige level
632
+ const prestigeLevel: PrestigeLevel = character.prestige_level ?? 0;
633
+
634
+ // Get track progress
635
+ const listenCount = tracker.getTrackListenCount(track.id);
636
+ const totalXP = tracker.getTrackXPTotal(track.id);
637
+
638
+ // Check if character can prestige
639
+ const canPrestige = PrestigeSystem.canPrestige(prestigeLevel, listenCount, totalXP);
640
+
641
+ if (canPrestige) {
642
+ console.log(`✅ Character can prestige!`);
643
+ }
644
+
645
+ // Get full prestige info for UI display
646
+ const info = PrestigeSystem.getPrestigeInfo(prestigeLevel, listenCount, totalXP);
647
+ console.log(`Plays: ${info.currentPlays}/${info.playsThreshold} (${Math.round(info.playsProgress * 100)}%)`);
648
+ console.log(`XP: ${info.currentXP}/${info.xpThreshold} (${Math.round(info.xpProgress * 100)}%)`);
649
+ console.log(`Mastered: ${info.isMastered}`);
650
+ console.log(`Can Prestige: ${info.canPrestige}`);
651
+ console.log(`Max Prestige: ${info.isMaxPrestige}`);
652
+ ```
653
+
654
+ ### Executing Prestige
655
+
656
+ ```typescript
657
+ import {
658
+ SessionTracker,
659
+ CharacterUpdater,
660
+ CharacterGenerator
661
+ } from 'playlist-data-engine';
662
+ import { AudioAnalyzer } from 'playlist-data-engine/analysis';
663
+
664
+ const analyzer = new AudioAnalyzer();
665
+ const tracker = new SessionTracker();
666
+ const updater = new CharacterUpdater();
667
+
668
+ // Get audio profile for regeneration
669
+ const audioProfile = await analyzer.extractSonicFingerprint(track.audio_url);
670
+
671
+ // Execute prestige (resets character to level 1, preserves equipment)
672
+ const result = updater.resetCharacterForPrestige(
673
+ character,
674
+ tracker, // SessionTracker to clear track sessions
675
+ track.id, // Track UUID
676
+ audioProfile, // For character regeneration
677
+ track // Track metadata
678
+ );
679
+
680
+ if (result.success) {
681
+ // Update your character state with the regenerated character
682
+ character = (result as any).character;
683
+
684
+ console.log(result.message);
685
+ // "Successfully prestiged to level I! New mastery requirements: 15 plays, 1,500 XP"
686
+
687
+ console.log(`New prestige level: ${PrestigeSystem.toRomanNumeral(result.newPrestigeLevel)}`);
688
+ console.log(`Previous level: ${PrestigeSystem.toRomanNumeral(result.previousPrestigeLevel)}`);
689
+ } else {
690
+ console.log(`Prestige failed: ${result.message}`);
691
+ }
692
+ ```
693
+
694
+ ### Displaying Prestige-Aware Mastery Progress
695
+
696
+ ```typescript
697
+ import { PrestigeSystem, type PrestigeLevel } from 'playlist-data-engine';
698
+
699
+ // Get character's prestige level (defaults to 0)
700
+ const prestigeLevel: PrestigeLevel = character.prestige_level ?? 0;
701
+
702
+ // Get progress info
703
+ const listenCount = tracker.getTrackListenCount(track.id);
704
+ const totalXP = tracker.getTrackXPTotal(track.id);
705
+ const info = PrestigeSystem.getPrestigeInfo(prestigeLevel, listenCount, totalXP);
706
+
707
+ // Display in UI
708
+ const prestigeRoman = PrestigeSystem.toRomanNumeral(prestigeLevel);
709
+ const playsPercent = Math.round(info.playsProgress * 100);
710
+ const xpPercent = Math.round(info.xpProgress * 100);
711
+
712
+ console.log(`Prestige ${prestigeRoman || 'None'}`);
713
+ console.log(`Plays: ${info.currentPlays}/${info.playsThreshold} (${playsPercent}%)`);
714
+ console.log(`XP: ${info.currentXP.toLocaleString()}/${info.xpThreshold.toLocaleString()} (${xpPercent}%)`);
715
+
716
+ // Check if both requirements are met (mastered)
717
+ if (info.isMastered) {
718
+ console.log(`🎉 Track Mastered!`);
719
+
720
+ // Can they prestige?
721
+ if (info.canPrestige) {
722
+ const nextLevel = PrestigeSystem.getNextPrestigeLevel(prestigeLevel);
723
+ console.log(`Ready to prestige to ${PrestigeSystem.toRomanNumeral(nextLevel!)}!`);
724
+ }
725
+
726
+ // Are they at max prestige?
727
+ if (info.isMaxPrestige) {
728
+ console.log(`⭐ Maximum Prestige Achieved!`);
729
+ }
730
+ }
731
+ ```
732
+
733
+ ### Setting Custom Thresholds
734
+
735
+ For special cases (events, difficulty adjustments), you can override the default thresholds:
736
+
737
+ ```typescript
738
+ import { PrestigeSystem } from 'playlist-data-engine';
739
+
740
+ // Set custom thresholds for prestige level 5
741
+ PrestigeSystem.setCustomThresholds(5, {
742
+ playsThreshold: 100, // Override calculated 77
743
+ xpThreshold: 10000 // Override calculated 7,594
744
+ });
745
+
746
+ // Set only plays threshold (XP uses calculated value)
747
+ PrestigeSystem.setCustomThresholds(3, {
748
+ playsThreshold: 50
749
+ });
750
+
751
+ // Reset plays to calculated value
752
+ PrestigeSystem.setCustomThresholds(3, {
753
+ playsThreshold: null
754
+ });
755
+
756
+ // Check if custom thresholds exist
757
+ const hasCustom = PrestigeSystem.hasCustomThresholds(5);
758
+
759
+ // Get custom thresholds
760
+ const custom = PrestigeSystem.getCustomThresholds(5);
761
+
762
+ // Clear custom thresholds for specific level
763
+ PrestigeSystem.clearCustomThresholds(5);
764
+
765
+ // Clear all custom thresholds
766
+ PrestigeSystem.clearCustomThresholds();
767
+
768
+ // View all threshold values (for debugging/display)
769
+ const allThresholds = PrestigeSystem.getAllThresholds();
770
+ allThresholds.forEach(t => {
771
+ console.log(`Prestige ${t.level}: ${t.plays} plays, ${t.xp} XP`);
772
+ });
773
+ ```
774
+
775
+ ### Threshold Reference
776
+
777
+ | Prestige | Plays | XP |
778
+ |----------|-------|-----|
779
+ | 0 | 10 | 1,000 |
780
+ | I | 15 | 1,500 |
781
+ | II | 23 | 2,250 |
782
+ | III | 34 | 3,375 |
783
+ | IV | 51 | 5,063 |
784
+ | V | 77 | 7,594 |
785
+ | VI | 115 | 11,391 |
786
+ | VII | 173 | 17,086 |
787
+ | VIII | 259 | 25,629 |
788
+ | IX | 389 | 38,444 |
789
+ | X (max) | 584 | 57,666 |
790
+
791
+ ### ISessionTracker Adapter {#isessiontracker-adapter}
792
+
793
+ If you're using a custom state management solution (Zustand, Redux, etc.) instead of the built-in `SessionTracker`, implement the `ISessionTracker` interface for the prestige system's `resetCharacterForPrestige()` method.
794
+
795
+ **Using with Zustand:**
796
+
797
+ ```typescript
798
+ import { type ISessionTracker, CharacterUpdater } from 'playlist-data-engine';
799
+ import { useSessionStore } from './stores/sessionStore';
800
+
801
+ const zustandAdapter: ISessionTracker = {
802
+ getTrackListenCount: (id) => useSessionStore.getState().getTrackListenCount(id),
803
+ getTrackXPTotal: (id) => useSessionStore.getState().getTrackXPTotal(id),
804
+ clearTrackSessions: (id) => useSessionStore.getState().clearTrackSessions(id),
805
+ };
806
+
807
+ const result = updater.resetCharacterForPrestige(character, zustandAdapter, trackUuid, audioProfile, track);
808
+ ```
809
+
810
+ **Using with a mock for testing:**
811
+
812
+ ```typescript
813
+ const mockTracker: ISessionTracker = {
814
+ getTrackListenCount: () => 15,
815
+ getTrackXPTotal: () => 2000,
816
+ clearTrackSessions: () => 10,
817
+ };
818
+ ```
819
+
820
+
821
+ ## Stat Strategies
822
+
823
+ ### Level-Up with Stat Increases
824
+
825
+ Stats increase on level-up at levels 4, 8, 12, 16, and 19 (standard mode) or every level (uncapped mode) following D&D 5e rules.
826
+
827
+ ```typescript
828
+ import {
829
+ StatManager,
830
+ CharacterUpdater,
831
+ CharacterGenerator,
832
+ LevelUpProcessor
833
+ } from 'playlist-data-engine';
834
+
835
+ // ===== GAME MODE SELECTION =====
836
+ // Standard mode (default): D&D 5e rules - stats capped at 20, increases at levels 4, 8, 12, 16, 19
837
+ // Uses MANUAL stat selection (2-step level-up process)
838
+ const standardCharacter = CharacterGenerator.generate(
839
+ seed,
840
+ audioProfile,
841
+ track,
842
+ { gameMode: 'standard' } // Optional, this is the default
843
+ );
844
+
845
+ // Uncapped mode: No stat limits, stat increases EVERY level (unlimited)
846
+ // Uses AUTOMATIC stat selection (1-step level-up process)
847
+ const uncappedCharacter = CharacterGenerator.generate(
848
+ seed,
849
+ audioProfile,
850
+ track,
851
+ { gameMode: 'uncapped' }
852
+ );
853
+
854
+ // ===== STRATEGY QUICK REFERENCE =====
855
+ // All stat strategies use the same pattern - just change the strategy parameter:
856
+ //
857
+ // const statManager = new StatManager({ strategy: /* your choice here */ });
858
+ // const updater = new CharacterUpdater(statManager);
859
+ //
860
+ // Available strategies:
861
+ // • 'dnD5e' (manual) - Player chooses stats at level-up (2-step process)
862
+ // • 'dnD5e_smart' (auto) - Intelligently selects based on class primary ability
863
+ // • 'balanced' (auto) - Distributes evenly across all stats
864
+ // • Custom function - Your own formula (see OPTION 6 below)
865
+ //
866
+ // Then use updater.addXP() - level-ups will use your chosen strategy automatically
867
+
868
+ // ===== OPTION 1: Auto-Detected Strategy (Recommended) =====
869
+ // CharacterUpdater automatically detects strategy based on gameMode - no StatManager needed!
870
+ const updater = new CharacterUpdater();
871
+
872
+ // Standard mode: 2-step level-up (manual stat selection required)
873
+ const standardResult = updater.addXP(standardCharacter, 6500, 'quest');
874
+ console.log(`Leveled up to ${standardResult.newLevel}!`);
875
+
876
+ if (updater.hasPendingStatIncreases(standardCharacter)) {
877
+ const count = updater.getPendingStatIncreaseCount(standardCharacter);
878
+ console.log(`${count} stat increases pending! Player must choose.`);
879
+
880
+ // Player chooses +2 to STR
881
+ const completeResult = updater.applyPendingStatIncrease(standardCharacter, 'STR');
882
+ console.log(`STR: ${completeResult.statIncreases[0].oldValue} → ${completeResult.statIncreases[0].newValue}`);
883
+
884
+ // Or player chooses +1 to STR and +1 to DEX
885
+ const result2 = updater.applyPendingStatIncrease(standardCharacter, 'STR', ['DEX']);
886
+ }
887
+
888
+ // Uncapped mode: 1-step level-up (automatic stat selection)
889
+ const uncappedResult = updater.addXP(uncappedCharacter, 6500, 'quest');
890
+ console.log(`Leveled up to ${uncappedResult.newLevel}!`);
891
+ console.log(`Stats auto-increased: ${JSON.stringify(uncappedResult.levelUpDetails?.[0].statIncreases)}`);
892
+
893
+ // ===== OPTION 2: Manual Stat Selection =====
894
+ const manualStatManager = new StatManager({ strategy: 'dnD5e' });
895
+ const manualUpdater = new CharacterUpdater(manualStatManager);
896
+
897
+ const manualResult = manualUpdater.addXP(character, 6500, 'quest');
898
+
899
+ if (manualUpdater.hasPendingStatIncreases(character)) {
900
+ const playerChoice = await showStatSelectionUI(); // Returns 'STR' or ['DEX', 'CON']
901
+ const completeResult = manualUpdater.applyPendingStatIncrease(
902
+ character, playerChoice.primary, playerChoice.secondary
903
+ );
904
+ }
905
+
906
+ // Alternative: Use LevelUpProcessor directly for lower-level control over the two-phase process
907
+
908
+ // ===== OPTION 3: Smart Auto-Selection (Force Auto Mode) =====
909
+ // Automatically picks best stats based on class and current scores - no player input needed
910
+ // Intelligently selects based on:
911
+ // - Class primary ability (Fighter → STR/DEX, Wizard → INT, etc.)
912
+ // - Current stat values (boosts lowest relevant stat if primary is high)
913
+ // - D&D 5e rules (+2 to one, or +1 to two)
914
+ const smartStatManager = new StatManager({
915
+ strategy: 'dnD5e_smart'
916
+ });
917
+ const smartUpdater = new CharacterUpdater(smartStatManager);
918
+
919
+ const smartResult = smartUpdater.addXP(character, 6500, 'quest');
920
+ console.log(`Leveled up to ${smartResult.newLevel}! Stats auto-increased.`);
921
+
922
+ // ===== OPTION 4: Item-Based Stat Changes (Potions, Curses, Restorations) =====
923
+ const itemStatManager = new StatManager();
924
+
925
+ // Potion of Strength: +4 STR (temporary or permanent based on your game logic)
926
+ const potionResult = itemStatManager.increaseStats(
927
+ character,
928
+ [{ ability: 'STR', amount: 4 }],
929
+ 'item'
930
+ );
931
+
932
+ character = potionResult.character;
933
+
934
+ // Check if stat was capped at 20 (standard mode)
935
+ if (potionResult.capped.length > 0) {
936
+ console.log('Stat was capped at 20!');
937
+ }
938
+
939
+ // Check what actually increased
940
+ for (const inc of potionResult.increases) {
941
+ console.log(`${inc.ability}: ${inc.oldValue} → ${inc.newValue} (+${inc.delta})`);
942
+ }
943
+
944
+ // Curse of Weakness: -2 STR penalty
945
+ const curseResult = itemStatManager.decreaseStats(
946
+ character,
947
+ [{ ability: 'STR', amount: 2 }],
948
+ 'event'
949
+ );
950
+
951
+ character = curseResult.character;
952
+
953
+ // Poison: -1 DEX, -1 CON
954
+ const poisonResult = itemStatManager.decreaseStats(
955
+ character,
956
+ [
957
+ { ability: 'DEX', amount: 1 },
958
+ { ability: 'CON', amount: 1 }
959
+ ],
960
+ 'event'
961
+ );
962
+
963
+ // Remove curse with restoration potion
964
+ const restoreResult = itemStatManager.increaseStats(
965
+ character,
966
+ [{ ability: 'STR', amount: 2 }],
967
+ 'item'
968
+ );
969
+
970
+ // ===== OPTION 5: Change Strategy Mid-Game =====
971
+ // Start with manual selection (early game), switch to auto later
972
+ const flexibleManager = new StatManager();
973
+
974
+ // Early game: Player chooses manually
975
+ const earlyGame = flexibleManager.processLevelUp(character, 4, {
976
+ forcedAbilities: ['STR']
977
+ });
978
+
979
+ // Mid-game: Switch to smart auto-selection (e.g., after level 10)
980
+ flexibleManager.updateConfig({
981
+ strategy: 'dnD5e_smart'
982
+ });
983
+
984
+ // Level-ups are now automatic - no manual input needed
985
+ const midGame = flexibleManager.processLevelUp(character, 11);
986
+
987
+ // Late-game: Switch to balanced strategy
988
+ flexibleManager.updateConfig({
989
+ strategy: 'balanced'
990
+ });
991
+
992
+ // ===== OPTION 6: Custom Level-Up Formula =====
993
+ // Provide your own formula for stat selection (perfect for custom game mechanics!)
994
+ // Example: Tank build that always prioritizes CON first, then DEX
995
+ const tankStrategy = (character, amount, options) => {
996
+ if (character.ability_scores.CON < 18) {
997
+ return [{ ability: 'CON', amount }];
998
+ }
999
+ return [{ ability: 'DEX', amount }];
1000
+ };
1001
+
1002
+ // Use the pattern from Strategy Quick Reference above
1003
+ const customStatManager = new StatManager({ strategy: tankStrategy });
1004
+ const customUpdater = new CharacterUpdater(customStatManager);
1005
+ const customResult = customUpdater.addXP(character, 6500, 'quest');
1006
+ ```
1007
+
1008
+ ### HP Increases on Every Level
1009
+
1010
+ HP increases on **every** level using class hit die + CON modifier. Standard mode caps stats at 20; uncapped mode has no cap.
1011
+
1012
+ ### Optional Features - Developer Implementation
1013
+
1014
+ The engine provides core stat manipulation but does NOT include:
1015
+
1016
+ 1. **Banked Stat Points**: Stat increases must be applied immediately. If your game needs a "spend points later" system, implement it yourself using `StatManager` as the building block.
1017
+
1018
+ 2. **Respec System**: There's no built-in stat respec system. Track the history of stat increases yourself and implement respec logic using `increaseStats` and `decreaseStats`.
1019
+
1020
+ **Example: Implementing Banked Points**
1021
+
1022
+ ```typescript
1023
+ // Your game's custom banked points system
1024
+ // Note: These are custom application-level types (not from the engine)
1025
+ type BankedPoints = {
1026
+ available: number;
1027
+ history: Array<{ timestamp: number; source: string; amount: number }>;
1028
+ };
1029
+
1030
+ class CharacterWithBankedPoints {
1031
+ character: CharacterSheet;
1032
+ banked: BankedPoints;
1033
+
1034
+ applyBankedPoints(ability: Ability, amount: number): void {
1035
+ if (this.banked.available < amount) {
1036
+ throw new Error('Not enough banked points');
1037
+ }
1038
+
1039
+ const statManager = new StatManager();
1040
+ const result = statManager.increaseStats(
1041
+ this.character,
1042
+ [{ ability, amount }],
1043
+ 'manual'
1044
+ );
1045
+
1046
+ this.character = result.character;
1047
+ this.banked.available -= amount;
1048
+ }
1049
+ }
1050
+ ```
1051
+
1052
+ ## XP Scaling
1053
+
1054
+ ### Custom XP Scaling for Uncapped Mode
1055
+
1056
+ Uncapped mode supports two options for XP progression (unlimited levels):
1057
+
1058
+ **Option 1: Default D&D 5e Pattern (Continues Naturally)**
1059
+
1060
+ ```typescript
1061
+ import { CharacterGenerator, LevelUpProcessor } from 'playlist-data-engine';
1062
+
1063
+ // Just generate a character in uncapped mode - no additional config needed!
1064
+ const character = CharacterGenerator.generate(
1065
+ seed,
1066
+ audioProfile,
1067
+ track,
1068
+ { gameMode: 'uncapped' }
1069
+ );
1070
+
1071
+ // XP automatically continues the D&D 5e formula: XP(n) = XP(n-1) + (n-1) × n × 500
1072
+ // Level 21: 565,000 XP (355000 + 20*21*500)
1073
+ // Level 25: ~735,000 XP
1074
+ // Level 30: ~1,120,000 XP
1075
+ // Proficiency bonus continues: +1 every 4 levels (21-24: 6, 25-28: 7, etc.)
1076
+ ```
1077
+
1078
+ **Option 2: Provide Your Own XP Formula**
1079
+
1080
+ ```typescript
1081
+ import { CharacterGenerator, LevelUpProcessor, type UncappedProgressionConfig } from 'playlist-data-engine';
1082
+
1083
+ // Set custom formulas BEFORE generating characters
1084
+ LevelUpProcessor.setUncappedConfig({
1085
+ // Your formula is used for EVERY level (1-∞)
1086
+ xpFormula: (level) => {
1087
+ // Example: Linear 50,000 XP per level
1088
+ return (level - 1) * 50000;
1089
+ },
1090
+ proficiencyBonusFormula: (level) => {
1091
+ // Example: +1 every 2 levels
1092
+ return 2 + Math.floor((level - 1) / 2);
1093
+ }
1094
+ });
1095
+
1096
+ // Now generate a character in uncapped mode
1097
+ const character = CharacterGenerator.generate(
1098
+ seed,
1099
+ audioProfile,
1100
+ track,
1101
+ { gameMode: 'uncapped' }
1102
+ );
1103
+
1104
+ // Uses YOUR formulas:
1105
+ // Level 1: 0 XP
1106
+ // Level 2: 50,000 XP
1107
+ // Level 3: 100,000 XP
1108
+ // Level 10: 450,000 XP
1109
+ // Proficiency: Level 1-2: 2, Level 3-4: 3, Level 5-6: 4, etc.
1110
+ ```
1111
+
1112
+ **Type Reference:** `UncappedProgressionConfig` interface - [*src/core/progression/LevelUpProcessor.ts*](src/core/progression/LevelUpProcessor.ts)
1113
+
1114
+ **Example Formulas:**
1115
+
1116
+ ```typescript
1117
+ // Exponential: faster at low levels, slower at high levels
1118
+ LevelUpProcessor.setUncappedConfig({
1119
+ xpFormula: (level) => Math.floor(1000 * Math.pow(1.5, level - 1)),
1120
+ proficiencyBonusFormula: (level) => 2 + Math.floor(Math.sqrt(level))
1121
+ });
1122
+ // Level 1: 1,000 XP | Level 5: ~5,062 XP | Level 10: ~38,443 XP
1123
+
1124
+ // OSRS-style: cubic XP curve
1125
+ LevelUpProcessor.setUncappedConfig({
1126
+ xpFormula: (level) => Math.floor(Math.pow(level, 3) * 100),
1127
+ proficiencyBonusFormula: (level) => 2 + Math.floor(level / 10)
1128
+ });
1129
+ // Level 1: 100 XP | Level 5: 12,500 XP | Level 10: 100,000 XP
1130
+ ```
1131
+
1132
+ **Reset to Default:**
1133
+
1134
+ ```typescript
1135
+ // Clear custom formulas and return to D&D 5e pattern
1136
+ LevelUpProcessor.setUncappedConfig({});
1137
+ ```
1138
+
1139
+ **Important Notes:**
1140
+
1141
+ 1. Formulas apply to ALL levels (1-infinity), not just beyond 20
1142
+ 2. Your `xpFormula` receives the level number and returns the TOTAL XP required to reach that level
1143
+ 3. Your `proficiencyBonusFormula` receives the level number and returns the proficiency bonus
1144
+ 4. Set config BEFORE generating characters or processing level-ups
1145
+ 5. Config is global and affects ALL uncapped mode characters
1146
+
1147
+ ---
1148
+
1149
+ ## Progression Configuration
1150
+
1151
+ Customize XP calculation, stat increases, and level-up behavior globally across all characters.
1152
+
1153
+ ```typescript
1154
+ import {
1155
+ DEFAULT_PROGRESSION_CONFIG,
1156
+ mergeProgressionConfig,
1157
+ type ProgressionConfig
1158
+ } from 'playlist-data-engine';
1159
+
1160
+ // ===== VIEW DEFAULT CONFIGURATION =====
1161
+ const defaultProgression = DEFAULT_PROGRESSION_CONFIG;
1162
+ console.log(defaultProgression.xp.level_thresholds); // D&D 5e XP thresholds
1163
+
1164
+ // ===== CUSTOMIZE PROGRESSION SETTINGS =====
1165
+ const customProgression = mergeProgressionConfig({
1166
+ xp: {
1167
+ // Base XP rate (default: 1 XP per second)
1168
+ xp_per_second: 2, // Double XP rate
1169
+
1170
+ // Environmental activity bonuses (multipliers applied to base XP)
1171
+ activity_bonuses: {
1172
+ running: 2.0, // 2x XP while running (default: 1.5)
1173
+ walking: 1.2, // 1.2x XP while walking
1174
+ night_time: 1.5, // 1.5x XP at night (default: 1.25)
1175
+ // Weather bonuses
1176
+ rain: 1.2,
1177
+ snow: 1.3,
1178
+ storm: 1.4,
1179
+ // Gaming bonuses
1180
+ gaming_base: 1.25, // Base gaming bonus
1181
+ rpg_game: 0.2, // Additional for RPG games
1182
+ action_fps: 0.15, // Additional for action/FPS
1183
+ multiplayer: 0.15, // Additional for multiplayer
1184
+ // Max multiplier cap
1185
+ max_multiplier: 3.0 // Total cap on all modifiers (default: 3.0)
1186
+ }
1187
+ },
1188
+ statIncrease: {
1189
+ // Strategy: 'dnD5e' (manual), 'dnD5e_smart' (auto), 'balanced', or custom
1190
+ strategy: 'balanced',
1191
+ autoApply: true // Automatically apply stat increases
1192
+ },
1193
+ levelUp: {
1194
+ useAverageHP: true, // Use average HP instead of rolling
1195
+ allowManualStatSelection: false // Disable manual selection UI
1196
+ }
1197
+ });
1198
+
1199
+ // For the complete `ProgressionConfig` interface definition, see:
1200
+ // [*src/core/config/progressionConfig.ts*](src/core/config/progressionConfig.ts)
1201
+ ```
1202
+
1203
+ **Important Notes:**
1204
+ - Configuration is **global** - affects all characters and all XP calculations
1205
+ - Set configuration **before** generating characters or processing level-ups
1206
+ - `mergeProgressionConfig()` merges your settings with defaults - unset properties remain default
1207
+
1208
+ **Available Exports:** [*src/core/config/progressionConfig.ts*](src/core/config/progressionConfig.ts)
1209
+ - `DEFAULT_PROGRESSION_CONFIG` - Default D&D 5e progression values
1210
+ - `mergeProgressionConfig(userConfig?)` - Merge progression config with defaults
1211
+ - `type ProgressionConfig` - Progression system configuration interface
1212
+
1213
+ ---
1214
+
1215
+ ## See Also
1216
+
1217
+ - [DATA_ENGINE_REFERENCE.md](../DATA_ENGINE_REFERENCE.md) - Complete API reference
1218
+ - [USAGE_IN_OTHER_PROJECTS.md](../USAGE_IN_OTHER_PROJECTS.md) - Usage examples
1219
+ - [EQUIPMENT_SYSTEM.md](EQUIPMENT_SYSTEM.md) - Equipment properties, enchanting, and effects
1220
+ - [PREREQUISITES.md](PREREQUISITES.md) - Level and ability requirements
1221
+ - [EXTENSIBILITY_GUIDE.md](EXTENSIBILITY_GUIDE.md) - Custom content registration