playlist-data-engine 1.7.3 → 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.
- package/README.md +14 -0
- package/bin/cli.cjs +85 -0
- package/dist/gateway-CDMPqFEH.js +1320 -0
- package/dist/gateway-DKa45Uz6.cjs +6 -0
- package/dist/gateway.d.ts +1 -0
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +1 -1
- package/dist/gateway.mjs +22 -19
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/playlist-data-engine.js +4 -4
- package/dist/playlist-data-engine.mjs +30 -27
- package/dist/utils/engineDocs.d.ts +33 -0
- package/dist/utils/engineDocs.d.ts.map +1 -0
- package/docs/DATA_ENGINE_REFERENCE.md +6660 -0
- package/docs/USAGE_IN_OTHER_PROJECTS.md +587 -0
- package/docs/features/AUDIO_ANALYSIS.md +610 -0
- package/docs/features/BEAT_DETECTION.md +5250 -0
- package/docs/features/COMBAT_SYSTEM.md +1632 -0
- package/docs/features/CONTENT_PACKS.md +464 -0
- package/docs/features/CUSTOM_CONTENT.md +603 -0
- package/docs/features/ENEMY_GENERATION.md +1711 -0
- package/docs/features/EQUIPMENT_SYSTEM.md +2279 -0
- package/docs/features/EXTENSIBILITY_GUIDE.md +1106 -0
- package/docs/features/GATEWAY_RESOLUTION.md +725 -0
- package/docs/features/IRL_SENSORS.md +360 -0
- package/docs/features/PLAYLIST_PARSING.md +446 -0
- package/docs/features/PREREQUISITES.md +571 -0
- package/docs/features/ROLLS_AND_SEEDS.md +687 -0
- package/docs/features/XP_AND_STATS.md +1221 -0
- package/llms.txt +33 -0
- package/package.json +9 -2
- package/skills/playlist-data-engine/SKILL.md +69 -0
- package/dist/gateway-C_p9Ku3O.js +0 -1211
- package/dist/gateway-Ceg-5xug.cjs +0 -1
|
@@ -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
|