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.
Files changed (35) hide show
  1. package/README.md +14 -0
  2. package/bin/cli.cjs +85 -0
  3. package/dist/gateway-CDMPqFEH.js +1320 -0
  4. package/dist/gateway-DKa45Uz6.cjs +6 -0
  5. package/dist/gateway.d.ts +1 -0
  6. package/dist/gateway.d.ts.map +1 -1
  7. package/dist/gateway.js +1 -1
  8. package/dist/gateway.mjs +22 -19
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/playlist-data-engine.js +4 -4
  12. package/dist/playlist-data-engine.mjs +30 -27
  13. package/dist/utils/engineDocs.d.ts +33 -0
  14. package/dist/utils/engineDocs.d.ts.map +1 -0
  15. package/docs/DATA_ENGINE_REFERENCE.md +6660 -0
  16. package/docs/USAGE_IN_OTHER_PROJECTS.md +587 -0
  17. package/docs/features/AUDIO_ANALYSIS.md +610 -0
  18. package/docs/features/BEAT_DETECTION.md +5250 -0
  19. package/docs/features/COMBAT_SYSTEM.md +1632 -0
  20. package/docs/features/CONTENT_PACKS.md +464 -0
  21. package/docs/features/CUSTOM_CONTENT.md +603 -0
  22. package/docs/features/ENEMY_GENERATION.md +1711 -0
  23. package/docs/features/EQUIPMENT_SYSTEM.md +2279 -0
  24. package/docs/features/EXTENSIBILITY_GUIDE.md +1106 -0
  25. package/docs/features/GATEWAY_RESOLUTION.md +725 -0
  26. package/docs/features/IRL_SENSORS.md +360 -0
  27. package/docs/features/PLAYLIST_PARSING.md +446 -0
  28. package/docs/features/PREREQUISITES.md +571 -0
  29. package/docs/features/ROLLS_AND_SEEDS.md +687 -0
  30. package/docs/features/XP_AND_STATS.md +1221 -0
  31. package/llms.txt +33 -0
  32. package/package.json +9 -2
  33. package/skills/playlist-data-engine/SKILL.md +69 -0
  34. package/dist/gateway-C_p9Ku3O.js +0 -1211
  35. package/dist/gateway-Ceg-5xug.cjs +0 -1
@@ -0,0 +1,1711 @@
1
+ # Enemy Generation System
2
+
3
+ Complete guide to generating enemies and encounters in the Playlist Data Engine.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Overview](#overview)
10
+ 2. [Quick Start](#quick-start)
11
+ 3. [Generation Modes](#generation-modes)
12
+ 4. [Rarity Tiers](#rarity-tiers)
13
+ 5. [Leader Promotion](#leader-promotion)
14
+ 6. [Mixed Enemy Types](#mixed-enemy-types)
15
+ 7. [Template System](#template-system)
16
+ 8. [Audio Integration](#audio-integration)
17
+ 9. [Encounter Balance](#encounter-balance)
18
+ 10. [Simulation-Based Balance Validation](#simulation-based-balance-validation)
19
+ 11. [Parameter Sweep](#parameter-sweep)
20
+ 12. [Comparative Analysis](#comparative-analysis)
21
+ 13. [Difficulty Calculator](#difficulty-calculator)
22
+ 14. [Recommended Simulation Counts](#recommended-simulation-counts)
23
+ 15. [API Reference](#api-reference)
24
+
25
+ ---
26
+
27
+ ## Overview
28
+
29
+ The Enemy Generation System creates balanced combat encounters through:
30
+
31
+ - **Deterministic Generation**: Seeded RNG ensures reproducible enemies
32
+ - **Rarity Scaling**: Four tiers (Common → Boss) with stat/ability scaling
33
+ - **Template-Based**: Predefined enemy types with signature abilities
34
+ - **Audio-Influenced**: Music profiles affect template selection
35
+ - **Party-Balanced**: D&D 5e XP budgets for fair encounters
36
+
37
+ ### Design Philosophy
38
+
39
+ - **Elegant over complex**: Reuses FeatureQuery for abilities rather than parallel systems
40
+ - **Infinite scaling**: Any template scales from Common to Boss tier
41
+ - **Signature + extras**: Every enemy has ONE signature ability (scaled by rarity) plus extra abilities from the feature pool
42
+ - **Audio-influenced**: Audio affects both template selection AND stat distribution (V2)
43
+ - **CR vs Rarity independence**: CR determines power (level, stats), Rarity determines complexity (abilities, resistances). Any combination is valid.
44
+
45
+ ---
46
+
47
+ ## Quick Start
48
+
49
+ ```typescript
50
+ import { EnemyGenerator } from 'playlist-data-engine';
51
+ import { AudioAnalyzer } from 'playlist-data-engine/analysis';
52
+
53
+ // ═══════════════════════════════════════════════════════════════
54
+ // SINGLE ENEMY
55
+ // ═══════════════════════════════════════════════════════════════
56
+
57
+ // By template ID
58
+ const orc = EnemyGenerator.generate({
59
+ seed: 'dungeon-entrance',
60
+ templateId: 'orc',
61
+ cr: 5, // Power level (determines level/stats)
62
+ rarity: 'elite' // Complexity (determines abilities)
63
+ });
64
+
65
+ // By category/archetype (random from matching templates)
66
+ const enemy = EnemyGenerator.generate({
67
+ seed: 'random',
68
+ category: 'humanoid',
69
+ archetype: 'brute',
70
+ cr: 3,
71
+ rarity: 'uncommon'
72
+ });
73
+
74
+ // ═══════════════════════════════════════════════════════════════
75
+ // ENCOUNTERS
76
+ // ═══════════════════════════════════════════════════════════════
77
+
78
+ // Party-balanced (uses D&D 5e XP budgets)
79
+ const party = [player1, player2, player3, player4];
80
+ const encounter = EnemyGenerator.generateEncounter(party, {
81
+ seed: 'room-3',
82
+ difficulty: 'medium',
83
+ count: 5
84
+ });
85
+
86
+ // CR-based (no party needed)
87
+ const crEncounter = EnemyGenerator.generateEncounterByCR({
88
+ seed: 'cr5-group',
89
+ targetCR: 5,
90
+ count: 3
91
+ });
92
+
93
+ // Custom composition
94
+ const customMix = EnemyGenerator.generateEncounterByCR({
95
+ seed: 'patrol',
96
+ targetCR: 3,
97
+ enemyMix: 'custom',
98
+ templates: ['orc', 'orc', 'goblin-archer', 'shaman']
99
+ });
100
+
101
+ // ═══════════════════════════════════════════════════════════════
102
+ // AUDIO-INFLUENCED
103
+ // ═══════════════════════════════════════════════════════════════
104
+
105
+ const analyzer = new AudioAnalyzer();
106
+ const audioProfile = await analyzer.extractSonicFingerprint(track.audio_url);
107
+
108
+ const audioEncounter = EnemyGenerator.generateEncounter(party, {
109
+ seed: 'audio-room',
110
+ audioProfile,
111
+ track,
112
+ difficulty: 'hard',
113
+ count: 4
114
+ });
115
+ // Bass-heavy → more brutes (Orc, Bear)
116
+ // Treble-heavy → more archers (Hunter, Goblin Archer)
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Generation Modes
122
+
123
+ The system supports two distinct generation modes:
124
+
125
+ ### Party-Based Mode
126
+
127
+ Analyzes party strength → calculates balanced encounter → generates appropriate enemies.
128
+
129
+ ```typescript
130
+ const enemies = EnemyGenerator.generateEncounter(
131
+ party: CharacterSheet[], // Required - party members
132
+ options: {
133
+ seed: string, // Required - for determinism
134
+ count: number, // Required - number of enemies
135
+ difficulty?: 'easy' | 'medium' | 'hard' | 'deadly',
136
+ difficultyMultiplier?: number, // Fine-tune difficulty (default: 1.0)
137
+ category?: 'humanoid' | 'beast', // Filter by category
138
+ archetype?: 'brute' | 'archer' | 'support', // Filter by archetype
139
+ templateId?: string, // Force specific template
140
+ enemyMix?: 'uniform' | 'custom',
141
+ templates?: string[], // For custom mix
142
+ audioProfile?: AudioProfile,
143
+ track?: PlaylistTrack, // Required if audioProfile provided
144
+ enableLeaderPromotion?: boolean // Default: true for groups > 3
145
+ }
146
+ ): CharacterSheet[]
147
+ ```
148
+
149
+ **How it works:**
150
+ 1. Analyzes party levels and calculates XP budget using D&D 5e tables
151
+ 2. Applies encounter multiplier for group fights
152
+ 3. Divides budget across requested enemy count
153
+ 4. Generates enemies at calculated CR
154
+ 5. Applies leader promotion if enabled
155
+
156
+ ### CR-Based Mode
157
+
158
+ Specify target Challenge Rating directly → no party analysis needed.
159
+
160
+ ```typescript
161
+ const enemies = EnemyGenerator.generateEncounterByCR({
162
+ seed: string, // Required
163
+ count: number, // Required - number of enemies
164
+ targetCR: number, // Target Challenge Rating per enemy
165
+ baseRarity?: 'common', // Starting rarity before promotion
166
+ difficultyMultiplier?: number, // Fine-tune (default: 1.0)
167
+ category?: EnemyCategory,
168
+ archetype?: EnemyArchetype,
169
+ templateId?: string,
170
+ enemyMix?: 'uniform' | 'custom',
171
+ templates?: string[],
172
+ audioProfile?: AudioProfile,
173
+ track?: PlaylistTrack,
174
+ enableLeaderPromotion?: boolean
175
+ }): CharacterSheet[]
176
+ ```
177
+
178
+ **How it works:**
179
+ 1. Uses targetCR directly instead of party analysis
180
+ 2. Applies encounter multiplier for group adjustments
181
+ 3. Generates enemies at calculated rarity
182
+ 4. Applies leader promotion if enabled
183
+
184
+ ---
185
+
186
+ ## CR vs Rarity: Two Independent Axes
187
+
188
+ The enemy generation system uses **two independent axes** to create diverse enemies:
189
+
190
+ | Concept | Determines | Examples |
191
+ |---------|------------|----------|
192
+ | **Challenge Rating (CR)** | Power level (stats, HP, level, proficiency) | Weak beast vs. ancient dragon |
193
+ | **Rarity** | Complexity (abilities, resistances, legendary actions) | Simple guard vs. complex spellcaster |
194
+
195
+ ### Key Design Principle
196
+
197
+ **Any CR can combine with any rarity:**
198
+
199
+ | CR | Rarity | Result | Example |
200
+ |----|--------|--------|---------|
201
+ | 0.25 | Common | Weak, simple | Goblin grunt |
202
+ | 0.25 | Boss | Weak, complex | Goblin chieftain |
203
+ | 5 | Common | Strong, simple | Dire wolf |
204
+ | 5 | Boss | Strong, complex | Werewolf alpha |
205
+ | 20 | Common | Epic, simple | Ancient purple worm |
206
+ | 20 | Boss | Epic, complex | Ancient red dragon |
207
+
208
+ ### How It Works
209
+
210
+ 1. **CR determines power**: Level is derived from CR using `CRLevelConverter.crToLevel()`. Higher CR = higher level = stronger stats.
211
+ 2. **Rarity determines complexity**: Rarity controls ability count, signature die size, and special features. Higher rarity = more complex = more abilities.
212
+ 3. **Fractional CRs** (0.25, 0.5) get reduced base stats (75-85%) to represent "sub-level" enemies.
213
+
214
+ ### Example: CR vs Rarity Independence
215
+
216
+ ```typescript
217
+ // SAME rarity, DIFFERENT power (CR determines stats/level)
218
+ const grunt = EnemyGenerator.generate({ seed: 'a', templateId: 'goblin', cr: 0.25, rarity: 'common' });
219
+ const beast = EnemyGenerator.generate({ seed: 'b', templateId: 'purple-worm', cr: 20, rarity: 'common' });
220
+ // Both have 1d6 signature, 0 extras — but beast has 20x the stats
221
+
222
+ // SAME power, DIFFERENT complexity (rarity determines abilities)
223
+ const simple = EnemyGenerator.generate({ seed: 'c', templateId: 'dire-wolf', cr: 5, rarity: 'common' });
224
+ const complex = EnemyGenerator.generate({ seed: 'd', templateId: 'werewolf', cr: 5, rarity: 'boss' });
225
+ // Both are level 5 — but boss has 1d12 signature, 3 extras, legendary actions
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Rarity Tiers
231
+
232
+ Every enemy template can be generated at four rarity tiers. Higher rarities have meaningfully stronger ability scores and dramatically more HP, making them feel distinctly more powerful.
233
+
234
+ | Rarity | Stat Multiplier | HP Multiplier | Signature Die | Extra Abilities | Resistances |
235
+ |--------|-----------------|---------------|----------------|-----------------|-------------|
236
+ | **Common** | 1.0× (base) | 1.0× | 1d6 | 0 | None |
237
+ | **Uncommon** | 1.08× (+8%) | 1.3× | 1d8 | 1 | None |
238
+ | **Elite** | 1.15× (+15%) | 1.7× | d10 | 2 | Type-based |
239
+ | **Boss** | 1.25× (+25%) | 2.2× | d12 | 3 | Type-based |
240
+
241
+ **Stat vs HP multipliers:** `statMultiplier` scales base ability scores, while `hpMultiplier` provides a separate, more dramatic HP boost. A boss goblin (baseHP 15) at CR 30 would reach ~1056 HP thanks to the combined effect of power-curve level scaling and the 2.2× HP multiplier.
242
+
243
+ ### Signature Ability Scaling
244
+
245
+ The signature ability is the core ability that defines an enemy type. It scales by rarity:
246
+
247
+ | Rarity | Die Damage | Example (Orc Savage Strike) |
248
+ |--------|-------------|----------------------------|
249
+ | Common | 1d6 + 2 | 1d6 + 2 slashing damage |
250
+ | Uncommon | 1d8 + 3 | 1d8 + 3 slashing damage |
251
+ | Elite | 1d10 + 4 | 1d10 + 4 slashing damage |
252
+ | Boss | 1d12 + 6 | 1d12 + 6 slashing damage |
253
+
254
+ The damage bonus (+2/+3/+4/+6) represents the increasing ability modifier based on rarity complexity. Note that the die size increases with rarity, while CR determines the underlying power level.
255
+
256
+ ### Extra Abilities
257
+
258
+ Higher rarity enemies draw additional abilities from the FeatureQuery pool:
259
+
260
+ | Rarity | Extra Abilities | Source |
261
+ |---------|----------------|---------|
262
+ | Common | 0 | None (signature only) |
263
+ | Uncommon | 1 | FeatureQuery (archetype-filtered) |
264
+ | Elite | 2 | FeatureQuery (archetype-filtered) |
265
+ | Boss | 3 | FeatureQuery (archetype-filtered) |
266
+
267
+ Abilities are selected based on archetype tags:
268
+ - **Brute**: combat, damage, defense, melee, durability
269
+ - **Archer**: combat, ranged, accuracy, mobility, stealth
270
+ - **Support**: support, healing, buff, control, utility
271
+
272
+ ### Resistances
273
+
274
+ Elite and Boss enemies gain type-appropriate resistances:
275
+
276
+ | Template | Elite+ Resistances |
277
+ |-----------|-------------------|
278
+ | Orc | poison |
279
+ | Bandit | none |
280
+ | Hunter | none |
281
+ | Goblin Archer | none |
282
+ | Shaman | necrotic |
283
+ | Cultist | necrotic |
284
+ | Bear | cold |
285
+ | Boar | none |
286
+ | Giant Spider | poison |
287
+ | Stirge | none |
288
+
289
+ ---
290
+
291
+ ## Leader Promotion
292
+
293
+ When generating encounters with **more than 3 enemies**, the system automatically promotes one or more enemies to higher rarity tiers as "leaders":
294
+
295
+ | Enemy Count | Leader Rule |
296
+ |-------------|-------------|
297
+ | 1-3 | No leader, all same rarity |
298
+ | 4-6 | 1 enemy promoted to next rarity tier |
299
+ | 7-9 | 1 enemy promoted two tiers up |
300
+ | 10+ | 2 enemies promoted (1 one tier, 1 two tiers) |
301
+
302
+ **Example:**
303
+ ```typescript
304
+ // Generate 5 common orcs
305
+ const enemies = EnemyGenerator.generateEncounter(party, {
306
+ seed: 'goblin-camp',
307
+ count: 5,
308
+ baseRarity: 'common'
309
+ });
310
+
311
+ // Result:
312
+ // - 4 Common Orcs (15 HP, 1d6 signature)
313
+ // - 1 Uncommon Orc leader (17 HP, 1d8 signature, +1 extra ability)
314
+ ```
315
+
316
+ **Promotion is capped at Boss rarity** - if promotion would exceed boss, the enemy becomes a boss.
317
+
318
+ **Disable leader promotion:**
319
+ ```typescript
320
+ const enemies = EnemyGenerator.generateEncounter(party, {
321
+ seed: 'no-leaders',
322
+ count: 5,
323
+ enableLeaderPromotion: false
324
+ });
325
+ // All 5 enemies remain at base rarity
326
+ ```
327
+
328
+ ---
329
+
330
+ ## CR-Based Gradual Rarity Scaling (Opt-In)
331
+
332
+ By default, rarity is independent of CR. However, you can enable automatic rarity scaling based on CR by setting `scaleRarityWithCR: true`.
333
+
334
+ ### How It Works
335
+
336
+ When enabled, the system distributes rarity upgrades across enemies based on the target CR:
337
+
338
+ | CR Tier | CR Range | Upgrade Points | Party of 3 Result |
339
+ |---------|----------|----------------|-------------------|
340
+ | Low | 0-2 | 0 | [common, common, common] |
341
+ | Low-Medium | 3-5 | 1 | [uncommon, common, common] |
342
+ | Medium | 6-10 | 2 | [uncommon, uncommon, common] |
343
+ | Medium-High | 11-15 | 3 | [uncommon, uncommon, uncommon] |
344
+ | High | 16-20 | 4 | [elite, uncommon, uncommon] |
345
+ | Very High | 21-30 | 5 | [elite, elite, uncommon] |
346
+ | Epic | 30+ | 6 | [elite, elite, elite] |
347
+
348
+ **Upgrade Path:** common → uncommon → elite (per enemy)
349
+
350
+ ### Example Usage
351
+
352
+ ```typescript
353
+ // Default behavior: rarity is independent of CR
354
+ const lowCR = EnemyGenerator.generateEncounterByCR({
355
+ seed: 'low-cr',
356
+ targetCR: 2,
357
+ count: 3,
358
+ baseRarity: 'common' // All enemies are common
359
+ });
360
+
361
+ // With scaling enabled: rarity increases with CR
362
+ const highCR = EnemyGenerator.generateEncounterByCR({
363
+ seed: 'high-cr',
364
+ targetCR: 18,
365
+ count: 3,
366
+ scaleRarityWithCR: true // Results in [elite, uncommon, uncommon]
367
+ });
368
+ ```
369
+
370
+ ### Boss Rule
371
+
372
+ When rarity is 'boss', count is automatically enforced to 1. Bosses are always 1vparty encounters.
373
+
374
+ ```typescript
375
+ // This will generate only 1 enemy, not 3
376
+ const boss = EnemyGenerator.generateEncounterByCR({
377
+ seed: 'boss-encounter',
378
+ targetCR: 10,
379
+ count: 3, // Will be overridden to 1
380
+ baseRarity: 'boss' // Forces count = 1
381
+ });
382
+ ```
383
+
384
+ ---
385
+
386
+ ## Mixed Enemy Types
387
+
388
+ Encounters can contain different enemy types using the `enemyMix` option:
389
+
390
+ ### Uniform Mode (Default)
391
+
392
+ All enemies use the same randomly-selected template.
393
+
394
+ ```typescript
395
+ const enemies = EnemyGenerator.generateEncounter(party, {
396
+ seed: 'uniform-group',
397
+ count: 5,
398
+ category: 'humanoid',
399
+ archetype: 'brute'
400
+ // enemyMix defaults to 'uniform'
401
+ });
402
+
403
+ // Result: 5 enemies, all the same type (e.g., 5 Orcs or 5 Bandits)
404
+ ```
405
+
406
+ ### Custom Mode
407
+
408
+ Specify the exact template mix for the encounter.
409
+
410
+ ```typescript
411
+ const enemies = EnemyGenerator.generateEncounter(party, {
412
+ seed: 'custom-composition',
413
+ count: 6,
414
+ enemyMix: 'custom',
415
+ templates: ['orc', 'orc', 'goblin-archer', 'goblin-archer', 'shaman', 'cultist']
416
+ });
417
+
418
+ // Result: 2 Orcs, 2 Goblin Archers, 1 Shaman, 1 Cultist
419
+ // Templates cycle if count exceeds array length
420
+ ```
421
+
422
+ ### Category Mode
423
+
424
+ Random mix from templates within the same category.
425
+
426
+ ```typescript
427
+ const enemies = EnemyGenerator.generateEncounter(party, {
428
+ seed: 'undead-crypt',
429
+ count: 6,
430
+ category: 'undead',
431
+ enemyMix: 'category'
432
+ });
433
+
434
+ // Result: Random mix of undead enemies (Skeleton, Zombie, Wight, Ghost)
435
+ // Could produce: 2 Skeletons, 2 Zombies, 1 Wight, 1 Ghost
436
+ ```
437
+
438
+ **Rules:**
439
+ - All enemies come from the specified category
440
+ - Each enemy is independently randomized from templates in that category
441
+ - Audio weighting applies if `audioProfile` provided
442
+ - Requires `category` option to be specified
443
+
444
+ ### Random Mode
445
+
446
+ Completely random mix from all available templates.
447
+
448
+ ```typescript
449
+ const enemies = EnemyGenerator.generateEncounter(party, {
450
+ seed: 'chaos-encounter',
451
+ count: 4,
452
+ enemyMix: 'random'
453
+ });
454
+
455
+ // Result: Completely random enemies
456
+ // Could produce: 1 Orc, 1 Fire Elemental, 1 Imp, 1 Basilisk
457
+ ```
458
+
459
+ **Warning:** This can create thematically disjoint encounters. Use with intent.
460
+
461
+ ---
462
+
463
+ ## Template System
464
+
465
+ Templates are the foundation of enemy generation. Each template defines:
466
+
467
+ | Property | Description |
468
+ |-----------|-------------|
469
+ | `id` | Unique identifier (e.g., `'orc'`, `'goblin-archer'`) |
470
+ | `name` | Display name (used as enemy name when generated) |
471
+ | `category` | Type classification: `'humanoid'` or `'beast'` |
472
+ | `archetype` | Combat role: `'brute'`, `'archer'`, or `'support'` |
473
+ | `signatureAbility` | Core ability shared across all rarities |
474
+ | `baseStats` | Ability scores before rarity scaling |
475
+ | `baseHP` | Hit points before rarity scaling |
476
+ | `baseAC` | Armor class before DEX modifier |
477
+ | `baseSpeed` | Movement speed in feet |
478
+ | `audioPreference` | Weights for audio-influenced selection |
479
+ | `resistances` | Damage resistances/immunities for Elite+ tier |
480
+
481
+ ### Available Templates
482
+
483
+ The system includes 40+ templates across 8 categories. Full template definitions are in the source:
484
+
485
+ | Category | Source File | Templates |
486
+ |----------|-------------|-----------|
487
+ | **Humanoid, Beast** | [DefaultEnemies.ts](src/constants/DefaultEnemies.ts) | Orc, Bandit, Hunter, Goblin Archer, Shaman, Cultist, Bear, Boar, Giant Spider, Stirge |
488
+ | **Undead** | [EnemyTemplates/Undead.ts](src/constants/EnemyTemplates/Undead.ts) | Skeleton, Ghost, Zombie, Wight |
489
+ | **Fiend** | [EnemyTemplates/Fiend.ts](src/constants/EnemyTemplates/Fiend.ts) | Imp, Lemure, Demon, Quasit |
490
+ | **Elemental** | [EnemyTemplates/Elemental.ts](src/constants/EnemyTemplates/Elemental.ts) | Fire, Earth, Air, Water Elementals |
491
+ | **Construct** | [EnemyTemplates/Construct.ts](src/constants/EnemyTemplates/Construct.ts) | Animated Armor, Golem, Flying Sword, Shield Guardian |
492
+ | **Dragon** | [EnemyTemplates/Dragon.ts](src/constants/EnemyTemplates/Dragon.ts) | Young Red Dragon, Young Blue Dragon, Wyrmling, Drake |
493
+ | **Monstrosity** | [EnemyTemplates/Monstrosity.ts](src/constants/EnemyTemplates/Monstrosity.ts) | Owlbear, Mimic, Griffin, Basilisk |
494
+
495
+ Each template includes: `id`, `name`, `category`, `archetype`, `signatureAbility`, `baseStats`, `baseHP`, `baseAC`, `baseSpeed`, `audioPreference`, and `resistances`.
496
+
497
+ ### Get Template by ID
498
+
499
+ ```typescript
500
+ import { EnemyGenerator } from 'playlist-data-engine';
501
+
502
+ const template = EnemyGenerator.getTemplateById('orc');
503
+ if (template) {
504
+ console.log(template.name); // 'Orc'
505
+ console.log(template.baseStats); // { STR: 16, DEX: 12, ... }
506
+ }
507
+ ```
508
+
509
+ ---
510
+
511
+ ## Audio Integration
512
+
513
+ Audio profiles influence enemy generation in two ways:
514
+
515
+ ### 1. Template Selection
516
+
517
+ When generating random enemies, the system weights template selection based on audio characteristics:
518
+
519
+ | Audio Characteristic | Favors Templates |
520
+ |---------------------|------------------|
521
+ | High bass dominance | Brute templates (Orc, Bear) |
522
+ | High treble dominance | Archer templates (Hunter, Goblin Archer) |
523
+ | Balanced (mid-heavy) | Support templates (Shaman, Cultist) |
524
+
525
+ **How it works:**
526
+ ```typescript
527
+ // Dot product: audio values × template weights = selection score
528
+ const score =
529
+ audioProfile.bass_dominance * template.audioPreference.bass +
530
+ audioProfile.mid_dominance * template.audioPreference.mid +
531
+ audioProfile.treble_dominance * template.audioPreference.treble;
532
+ ```
533
+
534
+ Higher scores = more likely to be selected.
535
+
536
+ ### 2. Stat Distribution (V2)
537
+
538
+ Audio profiles now also influence individual ability scores:
539
+
540
+ | Audio Characteristic | Stat Bonuses |
541
+ |---------------------|---------------|
542
+ | High bass dominance | +1 to STR and CON |
543
+ | High treble dominance | +1 to DEX |
544
+ | High mid dominance | +1 to WIS and CHA |
545
+ | Balanced (all similar) | +1 to all stats (smaller bonus) |
546
+
547
+ **Maximum bonus:** +2 to any single stat from audio influence
548
+
549
+ This is additive to rarity scaling, not multiplicative.
550
+
551
+ ---
552
+
553
+ ## V2 Features
554
+
555
+ ### Equipment Generation (V2)
556
+
557
+ Enemies are now equipped with actual weapons and armor based on their archetype and rarity.
558
+
559
+ **EquipmentGenerator** provides:
560
+ - Weapons selected by archetype (brute/archer/support)
561
+ - Armor scaled by rarity (common→boss gets better gear)
562
+ - Shields for applicable archetypes (archers don't get shields)
563
+
564
+ #### Equipment by Archetype
565
+
566
+ | Archetype | Weapons | Armor |
567
+ |-----------|---------|-------|
568
+ | **Brute** | Greataxe, Longsword, Handaxe, Mace | Scale Mail, Chain Mail, Plate Armor (boss only) |
569
+ | **Archer** | Longbow, Light Crossbow, Shortsword, Dagger | Leather Armor (light for mobility) |
570
+ | **Support** | Quarterstaff, Mace, Dagger | Scale Mail, Chain Mail, Shield available |
571
+
572
+ #### Shield Probability by Rarity
573
+
574
+ | Rarity | Shield Chance |
575
+ |--------|---------------|
576
+ | Common | 25% |
577
+ | Uncommon | 50% |
578
+ | Elite | 75% |
579
+ | Boss | 100% |
580
+
581
+ **Example:**
582
+ ```typescript
583
+ // Generate an elite brute with equipment
584
+ const enemy = EnemyGenerator.generate({
585
+ seed: 'elite-orc-1',
586
+ templateId: 'orc',
587
+ rarity: 'elite'
588
+ });
589
+
590
+ // Equipment is stored on the enemy
591
+ console.log(enemy.equipment_config?.weapon?.name); // 'Greataxe'
592
+ console.log(enemy.equipment_config?.armor?.name); // 'Chain Mail'
593
+ console.log(enemy.equipment_config?.shield?.name); // 'Shield' (75% chance)
594
+ ```
595
+
596
+ ---
597
+
598
+ ### Spellcasting System (V2)
599
+
600
+ Some enemies gain innate spellcasting abilities based on archetype and rarity.
601
+
602
+ #### Spell Availability
603
+
604
+ | Archetype | Common | Uncommon | Elite | Boss |
605
+ |-----------|--------|----------|-------|------|
606
+ | **Support** | ✅ Always | ✅ Always | ✅ Always | ✅ Always |
607
+ | **Archer** | ❌ No | ❌ No | ✅ Elite+ | ✅ Elite+ |
608
+ | **Brute** | ❌ No | ❌ No | ✅ Elite+ | ✅ Elite+ |
609
+
610
+ #### Spell Slots by CR
611
+
612
+ | CR | Level 1 | Level 2 | Level 3 | Level 4 |
613
+ |----|----------|----------|----------|----------|
614
+ | 0-0.5 | 0 | 0 | 0 | 0 |
615
+ | 1 | 3 | 0 | 0 | 0 |
616
+ | 2 | 4 | 0 | 0 | 0 |
617
+ | 3 | 4 | 2 | 0 | 0 |
618
+ | 4 | 4 | 3 | 0 | 0 |
619
+ | 5 | 4 | 3 | 2 | 0 |
620
+ | 6-7 | 4 | 3 | 3 | 0 |
621
+ | 8+ | 4 | 3 | 3 | 1-2 |
622
+
623
+ #### Spell List Examples
624
+
625
+ **Support Spells:**
626
+ - Cantrips: Sacred Flame, Guidance, Resistance
627
+ - Level 1: Bless, Bane, Cure Wounds, Healing Word, Command
628
+ - Level 2: Aid, Lesser Restoration, Spiritual Weapon, Shatter
629
+ - Level 3: Spirit Guardians, Revivify, Mass Healing Word
630
+
631
+ **Archer Spells:**
632
+ - Cantrips: Ray of Frost, Shocking Grasp, True Strike
633
+ - Level 1: Misty Step, Hold Person, Ray of Sickness, Thunderwave
634
+ - Level 2: Web, Invisibility, Melf's Acid Arrow, Scorching Ray
635
+ - Level 3: Fly, Lightning Bolt, Gaseous Form
636
+
637
+ **Brute Spells:**
638
+ - Cantrips: Fire Bolt, Shillelagh, Thorn Whip
639
+ - Level 1: Burning Hands, Divine Favor, Magic Stone, Zephyr Strike
640
+ - Level 2: Shatter, Branding Smite, Spiritual Weapon, Flame Blade
641
+ - Level 3: Call Lightning, Elemental Weapon, Blur
642
+
643
+ **Example:**
644
+ ```typescript
645
+ // Generate an elite shaman with spellcasting
646
+ const enemy = EnemyGenerator.generate({
647
+ seed: 'elite-shaman',
648
+ templateId: 'shaman',
649
+ rarity: 'elite'
650
+ });
651
+
652
+ // Spells are converted to Features with isSpell: true
653
+ console.log(enemy.class_features.filter(f => f.isSpell)); // 2 cantrips + 3 spells
654
+ ```
655
+
656
+ ---
657
+
658
+ ### Legendary System (V2)
659
+
660
+ Boss-tier enemies gain legendary actions and resistances.
661
+
662
+ #### Legendary Actions
663
+
664
+ Bosses receive **3 legendary actions** selected from their archetype pool:
665
+
666
+ | Archetype | Sample Actions |
667
+ |-----------|----------------|
668
+ | **Brute** | Tail Attack (1), Devour (3), Trample (2), Charge (1) |
669
+ | **Archer** | Snipe (1), Volley Shot (2), Shadow Step (2), Multi-Shot (3) |
670
+ | **Support** | Rally (1), Frightful Presence (1), Healing Aura (2), Command Ally (2) |
671
+ | **Universal** | Teleport (2), Detect (1) |
672
+
673
+ *Costs are in legendary action points (typically 3 per round)*
674
+
675
+ #### Legendary Resistances
676
+
677
+ Bosses gain legendary resistances per day based on CR:
678
+
679
+ | CR Range | Resistances/Day |
680
+ |----------|-----------------|
681
+ | CR 1-4 | 3 |
682
+ | CR 5-10 | 3 |
683
+ | CR 11-15 | 4 |
684
+ | CR 16-20 | 5 |
685
+ | CR 21+ | 6 |
686
+
687
+ #### Boss Enhancements
688
+
689
+ Boss enemies also receive:
690
+ - **Enhanced Signature Ability:** 2x damage dice (d12 → 2d12)
691
+ - **Ultimate Ability:** One special ability usable once per encounter
692
+ - **Epic Name:** Title added to name (e.g., "Grognak the Destroyer")
693
+
694
+ **Example:**
695
+ ```typescript
696
+ // Generate a boss with legendary system
697
+ const enemy = EnemyGenerator.generate({
698
+ seed: 'boss-dragon',
699
+ templateId: 'young-red-dragon',
700
+ rarity: 'boss'
701
+ });
702
+
703
+ console.log(enemy.legendary_config?.resistances); // 3 per day
704
+ console.log(enemy.legendary_config?.actions); // 3 legendary actions
705
+ console.log(enemy.name.includes('the')); // true (epic title added)
706
+ ```
707
+
708
+ ---
709
+
710
+ ### Fractional CR Stat Reduction
711
+
712
+ When generating enemies with fractional CR values (0.25, 0.5), the system applies automatic stat reduction to represent "sub-level" enemies. The reduction uses a continuous linear ramp from 0.5× at CR 0 to 1.0× at CR 1:
713
+
714
+ | CR | Stat Multiplier | Description |
715
+ |----|-----------------|-------------|
716
+ | 0.125 | ~56% | Sub-level enemy |
717
+ | 0.25 | ~63% | Sub-level enemy (e.g., goblin grunt) |
718
+ | 0.5 | ~75% | Sub-level enemy (e.g., giant rat) |
719
+ | 1+ | 100% | Full stats (standard enemy) |
720
+
721
+ **Formula:** `0.5 + 0.5 * CR` (for CR < 1)
722
+
723
+ ```typescript
724
+ const grunt = EnemyGenerator.generate({ seed: 'weak', templateId: 'goblin', cr: 0.25, rarity: 'common' });
725
+ const warrior = EnemyGenerator.generate({ seed: 'strong', templateId: 'goblin', cr: 5, rarity: 'common' });
726
+ // Same template, same rarity — grunt has ~63% stats, warrior has 100%
727
+ ```
728
+
729
+ ---
730
+
731
+ ### CR/Level Conversion (V2)
732
+
733
+ Dedicated functions for converting between Challenge Rating and character level. **The EnemyGenerator now uses `CRLevelConverter.crToLevel()` for all CR → level conversions.**
734
+
735
+ #### CR → Level Mapping
736
+
737
+ The enemy generation system uses the following mapping:
738
+
739
+ | CR | Level | Stat Multiplier | Notes |
740
+ |----|-------|-----------------|-------|
741
+ | 0.25 | 0.25 | ~63% | Sub-level enemy (0.5 + 0.5 × CR) |
742
+ | 0.5 | 0.5 | ~75% | Sub-level enemy |
743
+ | 1 | 1 | 100% | Standard enemy |
744
+ | 5 | 5 | 100% | Standard enemy |
745
+ | 10 | 10 | 100% | Standard enemy |
746
+ | 20 | 20 | 100% | Standard enemy |
747
+
748
+ **Key insight:** CR ≈ level in D&D 5e. A CR 5 enemy is roughly equivalent to a level 5 character.
749
+
750
+ #### Conversion Functions
751
+
752
+ ```typescript
753
+ import { crToLevel, levelToCR, roundLevel, roundCR } from 'playlist-data-engine';
754
+
755
+ // CR to Level
756
+ crToLevel(1); // 1
757
+ crToLevel(0.25); // 0.25
758
+ crToLevel(5); // 5
759
+
760
+ // Level to CR (inverse)
761
+ levelToCR(5); // 5
762
+
763
+ // Round to valid values
764
+ roundLevel(0.7); // 1 (nearest integer)
765
+ roundCR(0.3); // 0.25 (nearest CR step)
766
+
767
+ // Format for display
768
+ formatLevel(0.25); // "0 (1/4)"
769
+ formatCR(0.25); // "1/4"
770
+ ```
771
+
772
+ #### Tuning Configuration
773
+
774
+ Customize conversion with tuning parameters:
775
+
776
+ ```typescript
777
+ import { createCRTuning } from 'playlist-data-engine';
778
+
779
+ // Adjust difficulty by changing how CR maps to level
780
+ const tuning = createCRTuning({
781
+ baseMultiplier: 1.2 // CR 5 → Level 6 (harder enemies)
782
+ // baseMultiplier: 0.8 // CR 5 → Level 4 (easier enemies)
783
+ // customCurve: new Map([[5, 7], [10, 15]]) // Specific breakpoints
784
+ });
785
+ ```
786
+
787
+ ---
788
+
789
+ ## Encounter Balance
790
+
791
+ The system uses D&D 5e official encounter building tables for balance.
792
+
793
+ ### XP Budget by Level and Difficulty
794
+
795
+ Each character level has XP thresholds for each difficulty:
796
+
797
+ | Level | Easy | Medium | Hard | Deadly |
798
+ |--------|--------|---------|-------|---------|
799
+ | 1 | 25 | 50 | 75 | 100 |
800
+ | 3 | 75 | 150 | 225 | 300 |
801
+ | 5 | 250 | 500 | 750 | 1,000 |
802
+ | 10 | 600 | 1,200 | 1,800 | 2,400 |
803
+ | 15 | 1,600 | 3,200 | 4,800 | 6,400 |
804
+ | 20 | 5,000 | 10,000 | 15,000 | 20,000 |
805
+
806
+ **Party budget = Sum of individual character budgets**
807
+
808
+ ### Encounter Multipliers
809
+
810
+ Groups of enemies are more dangerous due to action economy:
811
+
812
+ | Enemy Count | Multiplier |
813
+ |-------------|-------------|
814
+ | 1 | 1.0× |
815
+ | 2 | 1.5× |
816
+ | 3-6 | 2.0× |
817
+ | 7-10 | 1.5× |
818
+ | 11-14 | 1.0× |
819
+ | 15+ | 1.0× |
820
+
821
+ **Applied to adjusted XP total** - accounts for crowd control effectiveness.
822
+
823
+ ### CR to XP Conversion
824
+
825
+ Challenge Rating maps to XP for encounter calculations:
826
+
827
+ | CR | XP | CR | XP | CR | XP |
828
+ |-----|-----|-----|-----|-----|
829
+ | 0 | 10 | 1 | 200 | 5 | 1,800 |
830
+ | 1/8 | 25 | 2 | 450 | 10 | 5,900 |
831
+ | 1/4 | 50 | 3 | 700 | 15 | 13,000 |
832
+ | 1/2 | 100 | 4 | 1,100 | 20+ | 25,000+ |
833
+
834
+ ### Difficulty Multiplier
835
+
836
+ Fine-tune encounter difficulty with `difficultyMultiplier`:
837
+
838
+ ```typescript
839
+ const enemies = EnemyGenerator.generateEncounter(party, {
840
+ seed: 'harder-encounter',
841
+ difficulty: 'medium',
842
+ difficultyMultiplier: 1.2, // +20% difficulty
843
+ count: 4
844
+ });
845
+ ```
846
+
847
+ - **1.0** = Standard difficulty
848
+ - **1.1-1.2** = Harder (for experienced players)
849
+ - **0.8-0.9** = Easier (for newer players)
850
+
851
+ ---
852
+
853
+ ## Simulation-Based Balance Validation
854
+
855
+ > **For the core simulation engine, Combat AI, and Monte Carlo simulation details, see [Monte Carlo Simulation](COMBAT_SYSTEM.md#monte-carlo-simulation) in COMBAT_SYSTEM.md.** This section covers balance validation, parameter sweeps, comparative analysis, and the difficulty calculator — tools built on top of the simulator.
856
+
857
+ XP budgets (above) are **theoretical** — they use D&D 5e tables to estimate encounter difficulty. Simulation-based validation **tests actual difficulty** by running hundreds of AI-controlled combats and measuring real outcomes.
858
+
859
+ ### Why Simulation?
860
+
861
+ | Aspect | XP Budget (Theoretical) | Simulation (Empirical) |
862
+ |--------|------------------------|----------------------|
863
+ | **What it measures** | Expected threat level | Actual win/loss outcomes |
864
+ | **Accounts for** | CR, enemy count, party level | AI decisions, dice variance, action economy, abilities |
865
+ | **Accuracy** | Approximate (designed for tabletop) | Precise (matches this game's combat engine) |
866
+ | **Speed** | Instant (table lookup) | Seconds (hundreds of simulated combats) |
867
+ | **Output** | "This is a Medium encounter" | "Players win 73% of the time in ~4 rounds" |
868
+
869
+ **Use XP budgets for fast encounter generation, then validate with simulation when balance matters.**
870
+
871
+ ### Expected Win Rates
872
+
873
+ The `BalanceValidator` uses these D&D 5e-inspired targets:
874
+
875
+ | Difficulty | Player Win Rate | Meaning |
876
+ |------------|----------------|---------|
877
+ | **Easy** | 90–100% | Party almost always wins. Low risk, resource-conservative. |
878
+ | **Medium** | 70–80% | Comfortable but not trivial. Occasional resource drain. |
879
+ | **Hard** | 50–60% | Challenging. Real risk of character death. |
880
+ | **Deadly** | 30–40% | Likely TPK. Major achievement to win. |
881
+
882
+ These values are tunable via the `EXPECTED_WIN_RATES` constant.
883
+
884
+ ### BalanceValidator
885
+
886
+ Validates an encounter by comparing simulated win rate against the intended difficulty tier.
887
+
888
+ ```typescript
889
+ import {
890
+ BalanceValidator,
891
+ CombatSimulator,
892
+ AIPlayStyle
893
+ } from 'playlist-data-engine';
894
+
895
+ // ═══════════════════════════════════════════════════════════════
896
+ // Option A: Validate from scratch (runs simulations internally)
897
+ // ═══════════════════════════════════════════════════════════════
898
+ const validator = new BalanceValidator();
899
+ const report = validator.validate(
900
+ party, // Player CharacterSheet[]
901
+ enemies, // Enemy CharacterSheet[]
902
+ 'medium', // Intended difficulty
903
+ {
904
+ runCount: 500,
905
+ baseSeed: 'validation-seed',
906
+ aiConfig: {
907
+ playerStyle: 'normal',
908
+ enemyStyle: 'aggressive'
909
+ }
910
+ }
911
+ );
912
+
913
+ // ═══════════════════════════════════════════════════════════════
914
+ // Option B: Analyze existing simulation results
915
+ // ═══════════════════════════════════════════════════════════════
916
+ const simulator = new CombatSimulator();
917
+ const results = simulator.run(party, enemies, {
918
+ runCount: 500,
919
+ baseSeed: 'validation-seed',
920
+ aiConfig: {
921
+ playerStyle: 'normal',
922
+ enemyStyle: 'aggressive'
923
+ }
924
+ });
925
+
926
+ const report2 = validator.analyze(results, 'medium');
927
+ ```
928
+
929
+ ### BalanceReport Output
930
+
931
+ The `validate()` and `analyze()` methods return a `BalanceReport`:
932
+
933
+ ```typescript
934
+ interface BalanceReport {
935
+ intendedDifficulty: 'easy' | 'medium' | 'hard' | 'deadly';
936
+ actualDifficulty: 'easy' | 'medium' | 'hard' | 'deadly';
937
+ balanceScore: number; // 0–100 (100 = perfect match)
938
+ playerWinRate: number; // e.g., 0.73
939
+ expectedWinRate: { min: number; max: number }; // e.g., { min: 0.70, max: 0.80 }
940
+ difficultyVariance: 'underpowered' | 'balanced' | 'overpowered';
941
+ confidence: number; // 0–1 (based on run count)
942
+ recommendations: BalanceRecommendation[];
943
+ averagePlayerHPPercentRemaining: number; // 0–100
944
+ totalRuns: number;
945
+ }
946
+ ```
947
+
948
+ | Field | Description |
949
+ |-------|-------------|
950
+ | `balanceScore` | 100 = win rate hits the midpoint of expected range. Decreases with deviation. |
951
+ | `difficultyVariance` | `'underpowered'` = encounter too easy (win rate above expected), `'overpowered'` = too hard (below expected), `'balanced'` = within range. |
952
+ | `confidence` | Statistical confidence based on run count: `1 - 1/√n`. 100 runs ≈ 0.90, 500 runs ≈ 0.96. |
953
+ | `recommendations` | Actionable suggestions for adjusting difficulty (see below). |
954
+
955
+ ### Interpreting Results
956
+
957
+ ```typescript
958
+ // Check if encounter is balanced for its intended difficulty
959
+ if (report.difficultyVariance === 'balanced') {
960
+ console.log(`Well-balanced! Score: ${report.balanceScore}/100`);
961
+ }
962
+
963
+ // Check recommendations
964
+ for (const rec of report.recommendations) {
965
+ console.log(`${rec.description} (${rec.expectedImpact})`);
966
+ }
967
+ // Example outputs:
968
+ // "Reduce enemy CR by 1 level" (+8-12% player win rate)
969
+ // "Add 1-2 additional enemies" (-6-10% player win rate)
970
+ // "Encounter is well-balanced. No changes needed." (None — encounter is within target range)
971
+ ```
972
+
973
+ ### Recommendations
974
+
975
+ The validator generates context-aware recommendations based on how far the win rate deviates:
976
+
977
+ | Situation | Gap | Recommendations |
978
+ |-----------|-----|----------------|
979
+ | Way too hard | >30% below target | Reduce CR by 1-2, reduce enemy count |
980
+ | Moderately too hard | 15-30% below | Reduce CR by 1 |
981
+ | Slightly too hard | <15% below | Reduce CR by 1 or remove one ability |
982
+ | Slightly too easy | <15% above | Increase CR by 1 or add one enemy |
983
+ | Way too easy | >15% above | Increase CR by 1-2, add 1-2 enemies |
984
+ | Balanced + high HP | Within range | Consider increasing difficulty slightly |
985
+ | Balanced + low HP | Within range | Consider reducing enemy damage slightly |
986
+
987
+ Each recommendation includes `expectedImpact` (estimated win rate change) and `confidence` (0-1).
988
+
989
+ ### AI Strategy Impact
990
+
991
+ The AI style used during simulation significantly affects results:
992
+
993
+ ```typescript
994
+ // Normal vs Normal — baseline difficulty measurement
995
+ const normalReport = validator.validate(party, enemies, 'medium', {
996
+ runCount: 500,
997
+ aiConfig: { playerStyle: 'normal', enemyStyle: 'normal' }
998
+ });
999
+
1000
+ // Normal players vs Aggressive enemies — maximum threat ceiling
1001
+ const aggressiveReport = validator.validate(party, enemies, 'medium', {
1002
+ runCount: 500,
1003
+ aiConfig: { playerStyle: 'normal', enemyStyle: 'aggressive' }
1004
+ });
1005
+ ```
1006
+
1007
+ - **Normal enemies** — balanced combat, basic attacks, conservative resources. Measures baseline difficulty.
1008
+ - **Aggressive enemies** — maximum effort, burns all spell slots and abilities. Measures difficulty ceiling.
1009
+
1010
+ Comparing Normal vs Aggressive enemy results reveals how much enemy resource usage affects the encounter.
1011
+
1012
+ ### Parameter Sweep
1013
+
1014
+ A **parameter sweep** systematically varies a single encounter parameter across a range and runs simulations at each data point. This answers questions like *"What CR makes this a Medium encounter?"* or *"How does adding more enemies change the difficulty curve?"*
1015
+
1016
+ ```typescript
1017
+ import { ParameterSweep } from 'playlist-data-engine';
1018
+
1019
+ const sweeper = new ParameterSweep();
1020
+
1021
+ // ═══════════════════════════════════════════════════════════════
1022
+ // Sweep CR from 1 to 10 to find the sweet spot for Medium
1023
+ // ═══════════════════════════════════════════════════════════════
1024
+ const results = sweeper.sweep(
1025
+ party, // Player CharacterSheet[]
1026
+ { cr: 3, rarity: 'elite', category: 'humanoid', archetype: 'brute' },
1027
+ {
1028
+ variable: 'cr',
1029
+ range: { min: 1, max: 10, step: 1 }, // 10 data points
1030
+ simulationsPerPoint: 200,
1031
+ aiConfig: {
1032
+ playerStyle: 'normal',
1033
+ enemyStyle: 'aggressive'
1034
+ },
1035
+ baseSeed: 'cr-sweep',
1036
+ },
1037
+ (completed, total) => console.log(`Sweep ${completed}/${total}`)
1038
+ );
1039
+
1040
+ // Each data point has a parameter value and simulation summary
1041
+ for (const point of results.dataPoints) {
1042
+ console.log(
1043
+ `CR ${point.parameterValue}: ` +
1044
+ `${(point.playerWinRate * 100).toFixed(1)}% win rate, ` +
1045
+ `${point.averageRounds.toFixed(1)} avg rounds`
1046
+ );
1047
+ }
1048
+ ```
1049
+
1050
+ #### SweepParams
1051
+
1052
+ | Field | Type | Description |
1053
+ |-------|------|-------------|
1054
+ | `variable` | `SweepVariable` | Which parameter to vary (see table below) |
1055
+ | `range` | `{ min, max, step }` | Range of values to sweep across |
1056
+ | `simulationsPerPoint` | `number` | Number of simulations at each data point |
1057
+ | `aiConfig` | `AIConfig` | AI strategy for all simulations in the sweep |
1058
+ | `combatConfig?` | `CombatConfig` | Optional combat engine overrides |
1059
+ | `baseSeed?` | `string` | Seed prefix — each point gets `baseSeed-value` |
1060
+ | `abortSignal?` | `AbortSignal` | Cancel the sweep mid-execution |
1061
+
1062
+ #### SweepVariable — What You Can Sweep
1063
+
1064
+ | Variable | Effect | Example Range |
1065
+ |----------|--------|---------------|
1066
+ | `'cr'` | Varies enemy Challenge Rating | `{ min: 1, max: 10, step: 1 }` |
1067
+ | `'enemyCount'` | Varies number of enemies generated | `{ min: 1, max: 8, step: 1 }` |
1068
+ | `'partyLevel'` | Scales all player levels (simplified) | `{ min: 1, max: 20, step: 1 }` |
1069
+ | `'difficultyMultiplier'` | Scales enemy stats proportionally | `{ min: 0.5, max: 2.0, step: 0.1 }` |
1070
+ | `'rarity'` | Maps 0–3 to common/uncommon/elite/boss | `{ min: 0, max: 3, step: 1 }` |
1071
+ | `'hpLevel'` | Overrides enemy HP to a different effective level | `{ min: 1, max: 20, step: 1 }` |
1072
+ | `'attackLevel'` | Overrides enemy attack to a different effective level | `{ min: 1, max: 20, step: 1 }` |
1073
+ | `'defenseLevel'` | Overrides enemy defense to a different effective level | `{ min: 1, max: 20, step: 1 }` |
1074
+
1075
+ #### SweepResults
1076
+
1077
+ The `sweep()` method returns a `SweepResults` object with one `SweepDataPoint` per value in the range, ordered from lowest to highest:
1078
+
1079
+ ```typescript
1080
+ interface SweepResults {
1081
+ variable: SweepVariable; // Which parameter was swept
1082
+ range: SweepRange; // The range that was swept
1083
+ simulationsPerPoint: number; // Sims per data point
1084
+ dataPoints: SweepDataPoint[]; // One per value in the range
1085
+ wasCancelled: boolean; // True if cancelled before completion
1086
+ }
1087
+ ```
1088
+
1089
+ Each `SweepDataPoint` contains:
1090
+
1091
+ | Field | Type | Description |
1092
+ |-------|------|-------------|
1093
+ | `parameterValue` | `number` | The value of the sweep parameter at this point |
1094
+ | `playerWinRate` | `number` | Player win rate (0.0–1.0) |
1095
+ | `averageRounds` | `number` | Average rounds to combat resolution |
1096
+ | `medianRounds` | `number` | Median rounds to combat resolution |
1097
+ | `averageHPRemaining` | `number` | Average player HP remaining % on wins |
1098
+ | `totalPlayerDeaths` | `number` | Total player deaths across all sims |
1099
+ | `totalEnemyDeaths` | `number` | Total enemy deaths across all sims |
1100
+
1101
+ #### Interpreting Sweep Results
1102
+
1103
+ Plot `playerWinRate` against `parameterValue` to see the difficulty curve. Key patterns:
1104
+
1105
+ - **CR sweep**: Win rate should generally decrease as CR increases. The "sweet spot" for a given difficulty is where the win rate falls in the expected range.
1106
+ - **Enemy count sweep**: Similar to CR — more enemies means lower win rate. Watch for steep drop-offs (action economy tipping points).
1107
+ - **Difficulty multiplier sweep**: Produces the smoothest curves because it scales all enemy stats proportionally. Best for fine-tuning.
1108
+ - **Stat level sweeps** (`hpLevel`, `attackLevel`, `defenseLevel`): Reveal which stat axis has the most impact on difficulty. Useful for creating specialized enemies (tanks, glass cannons, brutes).
1109
+
1110
+ ```typescript
1111
+ // Find the CR range that produces Medium difficulty (70-80% win rate)
1112
+ const mediumPoints = results.dataPoints.filter(
1113
+ p => p.playerWinRate >= 0.70 && p.playerWinRate <= 0.80
1114
+ );
1115
+ if (mediumPoints.length > 0) {
1116
+ console.log(
1117
+ `Medium difficulty CR range: ${mediumPoints[0].parameterValue}` +
1118
+ `–${mediumPoints[mediumPoints.length - 1].parameterValue}`
1119
+ );
1120
+ }
1121
+
1122
+ // Find the exact CR closest to 75% win rate
1123
+ const closest = results.dataPoints.reduce((best, p) =>
1124
+ Math.abs(p.playerWinRate - 0.75) < Math.abs(best.playerWinRate - 0.75)
1125
+ ? p : best
1126
+ );
1127
+ console.log(`Best CR for Medium: ${closest.parameterValue} (${(closest.playerWinRate * 100).toFixed(1)}%)`);
1128
+ ```
1129
+
1130
+ #### Cancellation
1131
+
1132
+ Like `CombatSimulator`, parameter sweeps support `AbortSignal` for cancellation. Partial results are returned:
1133
+
1134
+ ```typescript
1135
+ const controller = new AbortController();
1136
+
1137
+ // Cancel after 3 seconds
1138
+ setTimeout(() => controller.abort(), 3000);
1139
+
1140
+ const partialResults = sweeper.sweep(party, encounter, {
1141
+ variable: 'cr',
1142
+ range: { min: 1, max: 20, step: 1 },
1143
+ simulationsPerPoint: 500,
1144
+ aiConfig: { playerStyle: 'normal', enemyStyle: 'aggressive' },
1145
+ abortSignal: controller.signal,
1146
+ });
1147
+
1148
+ console.log(`Completed ${partialResults.dataPoints.length} of 20 points`);
1149
+ ```
1150
+
1151
+ ### Comparative Analysis
1152
+
1153
+ **Comparative analysis** runs two encounter configurations with identical seed sequences, isolating the effect of a single variable change. This answers questions like *"How much does +2 AC improve win rate?"* or *"Is adding a 5th party member statistically significant?"*
1154
+
1155
+ Because both configurations use the same dice rolls (via deterministic seeding), any difference in outcomes is attributable to the configuration change itself — not random variance.
1156
+
1157
+ ```typescript
1158
+ import { ComparativeAnalyzer, EnemyGenerator } from 'playlist-data-engine';
1159
+
1160
+ const analyzer = new ComparativeAnalyzer();
1161
+
1162
+ // ═══════════════════════════════════════════════════════════════
1163
+ // Compare: "+2 AC" vs "No AC bonus" for a level 5 party
1164
+ // ═══════════════════════════════════════════════════════════════
1165
+ const enemiesA = [
1166
+ EnemyGenerator.generate({ seed: 'base-enemy', cr: 3, rarity: 'elite' }),
1167
+ ];
1168
+ const enemiesB = [
1169
+ EnemyGenerator.generate({ seed: 'base-enemy', cr: 3, rarity: 'elite' }),
1170
+ ];
1171
+
1172
+ // Modify one config — e.g., boost enemy AC in config B
1173
+ enemiesB[0].ac! += 2;
1174
+
1175
+ const comparison = analyzer.compare(
1176
+ { players: party, enemies: enemiesA, label: 'Base' },
1177
+ { players: party, enemies: enemiesB, label: '+2 AC' },
1178
+ {
1179
+ runCount: 500,
1180
+ baseSeed: 'ac-comparison',
1181
+ aiConfig: {
1182
+ playerStyle: 'normal',
1183
+ enemyStyle: 'aggressive',
1184
+ },
1185
+ },
1186
+ );
1187
+
1188
+ console.log(`Win rate delta: ${(comparison.deltas.winRateDelta * 100).toFixed(1)}%`);
1189
+ console.log(`Significant: ${comparison.winRateSignificance.isSignificant}`);
1190
+ console.log(comparison.winRateSignificance.interpretation);
1191
+ ```
1192
+
1193
+ #### Identical-Seed Methodology
1194
+
1195
+ Both configurations are simulated using the same seed sequence (`baseSeed-A` for config A, `baseSeed-B` for config B). This means:
1196
+
1197
+ - **Same dice rolls**: Each run index uses deterministic seeds derived from the same base, so both configs experience the same attack rolls, damage rolls, saving throws, and initiative order.
1198
+ - **Isolated variable**: The only difference in outcomes comes from the configuration change (e.g., +2 AC, different party size, different CR).
1199
+ - **Pair-wise comparison**: Results are comparable run-by-run, not just in aggregate.
1200
+
1201
+ This is more statistically powerful than running two independent simulations and comparing — the paired design eliminates dice variance as a confounding factor.
1202
+
1203
+ #### ComparisonConfig
1204
+
1205
+ Each side of the comparison is defined by a `ComparisonConfig`:
1206
+
1207
+ | Field | Type | Description |
1208
+ |-------|------|-------------|
1209
+ | `players` | `CharacterSheet[]` | Player characters for this config |
1210
+ | `enemies` | `CharacterSheet[]` | Enemy characters for this config |
1211
+ | `label?` | `string` | Display name (e.g., `"Base"`, `"+2 AC"`). Default: `"Config A"` / `"Config B"` |
1212
+ | `combatConfig?` | `CombatConfig` | Optional combat engine overrides |
1213
+
1214
+ #### ComparisonOptions
1215
+
1216
+ | Field | Type | Description |
1217
+ |-------|------|-------------|
1218
+ | `runCount` | `number` | Simulations per configuration (500+ recommended) |
1219
+ | `baseSeed` | `string` | Base seed — both configs use derived seeds from this |
1220
+ | `aiConfig` | `AIConfig` | AI strategy for all simulations |
1221
+ | `combatConfig?` | `CombatConfig` | Optional combat engine overrides (used if not set per-config) |
1222
+ | `significanceThreshold?` | `number` | Alpha level for significance test (default: `0.05`) |
1223
+ | `abortSignal?` | `AbortSignal` | Cancel the comparison mid-execution |
1224
+ | `onProgress?` | `(completed, total, side) => void` | Progress callback per side |
1225
+
1226
+ #### ComparisonResult
1227
+
1228
+ The `compare()` method returns a `ComparisonResult` with full data for both sides:
1229
+
1230
+ | Field | Type | Description |
1231
+ |-------|------|-------------|
1232
+ | `labelA` | `string` | Label for configuration A |
1233
+ | `labelB` | `string` | Label for configuration B |
1234
+ | `resultsA` | `SimulationResults` | Full simulation results for config A |
1235
+ | `resultsB` | `SimulationResults` | Full simulation results for config B |
1236
+ | `summaryA` | `SimulationSummary` | Summary for config A |
1237
+ | `summaryB` | `SimulationSummary` | Summary for config B |
1238
+ | `deltas` | `DeltaMetrics` | Aggregate difference metrics |
1239
+ | `combatantDeltas` | `CombatantDelta[]` | Per-combatant differences |
1240
+ | `winRateSignificance` | `SignificanceResult` | Statistical significance of win rate difference |
1241
+ | `wasCancelled` | `boolean` | Whether comparison was cancelled |
1242
+
1243
+ #### DeltaMetrics
1244
+
1245
+ Aggregate differences between configurations. **Positive values favor config A** (A is better for players):
1246
+
1247
+ | Field | Description |
1248
+ |-------|-------------|
1249
+ | `winRateDelta` | Win rate difference (e.g., `+0.15` = A wins 15% more) |
1250
+ | `averageRoundsDelta` | Average rounds difference |
1251
+ | `averageHPRemainingDelta` | Average player HP remaining % difference |
1252
+ | `totalPlayerDeathsDelta` | Player death count difference (negative = fewer deaths in A) |
1253
+ | `totalEnemyDeathsDelta` | Enemy death count difference |
1254
+ | `medianRoundsDelta` | Median rounds difference |
1255
+
1256
+ #### CombatantDelta
1257
+
1258
+ Per-combatant differences, matched by side and index position:
1259
+
1260
+ | Field | Description |
1261
+ |-------|-------------|
1262
+ | `name` | Combatant name (from config A) |
1263
+ | `side` | `'player'` or `'enemy'` |
1264
+ | `dprDelta` | Damage per round difference |
1265
+ | `damageDealtDelta` | Average total damage dealt difference |
1266
+ | `damageTakenDelta` | Average total damage taken difference |
1267
+ | `survivalRateDelta` | Survival rate difference |
1268
+ | `killRateDelta` | Kill rate difference |
1269
+ | `criticalHitRateDelta` | Critical hit rate difference |
1270
+ | `healingDoneDelta` | Average healing done difference |
1271
+
1272
+ Unmatched combatants (different party sizes) are marked with `(only in A)` or `(only in B)`.
1273
+
1274
+ #### SignificanceResult
1275
+
1276
+ | Field | Type | Description |
1277
+ |-------|------|-------------|
1278
+ | `isSignificant` | `boolean` | Whether the difference is statistically significant |
1279
+ | `pValue` | `number` | Approximate p-value from the test |
1280
+ | `threshold` | `number` | The significance threshold used (alpha) |
1281
+ | `interpretation` | `string` | Human-readable explanation of the result |
1282
+
1283
+ Significance is tested using a **normal approximation for the difference of proportions** (two-tailed test). For small samples (n < 30), a conservative minimum detectable effect threshold is used instead.
1284
+
1285
+ #### Common Use Cases
1286
+
1287
+ **Comparing stat changes:**
1288
+ ```typescript
1289
+ // Does +2 AC on enemies meaningfully increase difficulty?
1290
+ const baseEnemy = EnemyGenerator.generate({ seed: 'goblin', cr: 2 });
1291
+ const tankyEnemy = EnemyGenerator.generate({ seed: 'goblin', cr: 2 });
1292
+ tankyEnemy.ac! += 2;
1293
+
1294
+ const result = analyzer.compare(
1295
+ { players: party, enemies: [baseEnemy], label: 'Base AC' },
1296
+ { players: party, enemies: [tankyEnemy], label: '+2 AC' },
1297
+ { runCount: 500, baseSeed: 'ac-test', aiConfig: { playerStyle: 'normal', enemyStyle: 'aggressive' } },
1298
+ );
1299
+
1300
+ // Negative winRateDelta means B is harder (AC increase hurt players)
1301
+ console.log(result.deltas.winRateDelta); // e.g., -0.12 (12% lower win rate)
1302
+ ```
1303
+
1304
+ **Comparing party sizes:**
1305
+ ```typescript
1306
+ // Is a 5th party member significantly impactful?
1307
+ const result = analyzer.compare(
1308
+ { players: party4, enemies: encounter, label: '4 Players' },
1309
+ { players: party5, enemies: encounter, label: '5 Players' },
1310
+ { runCount: 500, baseSeed: 'party-size', aiConfig: { playerStyle: 'normal', enemyStyle: 'aggressive' } },
1311
+ );
1312
+
1313
+ console.log(result.winRateSignificance.interpretation);
1314
+ // "Config 5 Players has a statistically significant 18.2% higher win rate (p=0.0012, n=500)"
1315
+ ```
1316
+
1317
+ **Comparing enemy CR:**
1318
+ ```typescript
1319
+ // Is CR 5 meaningfully harder than CR 3?
1320
+ const enemies3 = Array.from({ length: 3 }, (_, i) =>
1321
+ EnemyGenerator.generate({ seed: `cr3-${i}`, cr: 3, rarity: 'uncommon' })
1322
+ );
1323
+ const enemies5 = Array.from({ length: 3 }, (_, i) =>
1324
+ EnemyGenerator.generate({ seed: `cr5-${i}`, cr: 5, rarity: 'uncommon' })
1325
+ );
1326
+
1327
+ const result = analyzer.compare(
1328
+ { players: party, enemies: enemies3, label: 'CR 3' },
1329
+ { players: party, enemies: enemies5, label: 'CR 5' },
1330
+ { runCount: 500, baseSeed: 'cr-compare', aiConfig: { playerStyle: 'normal', enemyStyle: 'aggressive' } },
1331
+ );
1332
+ ```
1333
+
1334
+ ---
1335
+
1336
+ ## Difficulty Calculator
1337
+
1338
+ > **"What CR should I use for a Hard encounter with this level 5 party?"**
1339
+
1340
+ The `DifficultyCalculator` answers this question by combining **XP-budget theory** (D&D 5e encounter building math) with **simulation-driven refinement**. It runs actual combats at different CR values and uses binary search to find the CR that produces the target win rate for your desired difficulty.
1341
+
1342
+ ### How It Works
1343
+
1344
+ The search uses a two-phase approach:
1345
+
1346
+ 1. **XP Budget Estimate** — `getXPBudgetForParty()` calculates the theoretical XP budget for the party at the target difficulty, adjusts for enemy count via encounter multiplier, then converts to a CR estimate via `getCRFromXP()`. This gives a reasonable starting point.
1347
+
1348
+ 2. **Simulation-Driven Refinement** — binary search over CR values. At each step, the calculator:
1349
+ - Generates enemies from the template at the current CR
1350
+ - Runs N simulations (default: 200 per probe)
1351
+ - Checks if the player win rate falls within the target range
1352
+ - Adjusts CR up (win rate too high → encounter too easy) or down (win rate too low → encounter too hard)
1353
+
1354
+ The search converges when the win rate is within the expected range for the target difficulty, or after 10 iterations (whichever comes first).
1355
+
1356
+ ### CR Rounding
1357
+
1358
+ CR values are rounded to standard D&D 5e steps: `0.125`, `0.25`, `0.5`, or integers. The search uses fractional CRs internally but reports rounded values in the final suggestion.
1359
+
1360
+ ### Confidence Intervals
1361
+
1362
+ Each suggestion includes a **margin of error** calculated via normal approximation for proportions (z = 1.96, 95% confidence). This is expressed as a human-readable string:
1363
+
1364
+ ```
1365
+ "72% ± 5%"
1366
+ ```
1367
+
1368
+ The margin of error decreases with more simulations per probe. For higher confidence, increase `simulationsPerProbe`.
1369
+
1370
+ ### Usage
1371
+
1372
+ ```typescript
1373
+ import { DifficultyCalculator } from 'playlist-data-engine';
1374
+
1375
+ const calculator = new DifficultyCalculator();
1376
+
1377
+ const suggestion = calculator.suggest(
1378
+ party, // Player CharacterSheet[]
1379
+ { rarity: 'elite', category: 'humanoid', archetype: 'brute' },
1380
+ 'hard', // Target difficulty
1381
+ {
1382
+ aiConfig: {
1383
+ playerStyle: 'normal',
1384
+ enemyStyle: 'aggressive',
1385
+ },
1386
+ baseSeed: 'my-search',
1387
+ simulationsPerProbe: 200,
1388
+ enemyCount: 1,
1389
+ onProgress: (iteration, maxIterations, currentCR) => {
1390
+ console.log(`Probe ${iteration}/${maxIterations} — testing CR ${currentCR}`);
1391
+ },
1392
+ },
1393
+ );
1394
+
1395
+ console.log(suggestion.recommendedCR); // e.g., 3
1396
+ console.log(suggestion.winRate); // e.g., 0.73
1397
+ console.log(suggestion.confidenceInterval); // e.g., "73% ± 5%"
1398
+ console.log(suggestion.converged); // true
1399
+ console.log(suggestion.suggestedEnemy); // Generated CharacterSheet at CR 3
1400
+ ```
1401
+
1402
+ ### DifficultyCalculatorOptions
1403
+
1404
+ | Field | Type | Default | Description |
1405
+ |-------|------|---------|-------------|
1406
+ | `aiConfig` | `AIConfig` | *(required)* | AI configuration for simulations |
1407
+ | `combatConfig` | `CombatConfig` | `undefined` | Optional combat engine overrides |
1408
+ | `baseSeed` | `string` | `'difficulty-calc'` | Base seed — each probe gets a derived seed |
1409
+ | `simulationsPerProbe` | `number` | `200` | Simulation runs per CR probe |
1410
+ | `maxIterations` | `number` | `10` | Maximum binary search iterations |
1411
+ | `enemyCount` | `number` | `1` | Number of enemies in the encounter |
1412
+ | `abortSignal` | `AbortSignal` | `undefined` | Cancellation support |
1413
+ | `onProgress` | `function` | `undefined` | Callback: `(iteration, maxIterations, currentCR)` |
1414
+
1415
+ ### DifficultyEnemyTemplate
1416
+
1417
+ Defines the "shape" of the enemy. CR is set automatically by the search — all other fields define the template.
1418
+
1419
+ | Field | Type | Default | Description |
1420
+ |-------|------|---------|-------------|
1421
+ | `rarity` | `EnemyRarity` | `'elite'` | Rarity tier |
1422
+ | `category` | `EnemyCategory` | `'humanoid'` | Enemy category |
1423
+ | `archetype` | `EnemyArchetype` | `'brute'` | Combat archetype |
1424
+ | `templateId` | `string` | `undefined` | Force a specific template |
1425
+ | `statLevels` | `StatLevelOverrides` | `undefined` | HP/attack/defense level overrides |
1426
+ | `difficultyMultiplier` | `number` | `undefined` | Fine-tune enemy difficulty |
1427
+
1428
+ ### DifficultySuggestion
1429
+
1430
+ | Field | Type | Description |
1431
+ |-------|------|-------------|
1432
+ | `targetDifficulty` | `EncounterDifficulty` | The requested difficulty tier |
1433
+ | `recommendedCR` | `number` | Suggested CR (rounded to standard D&D step) |
1434
+ | `winRate` | `number` | Player win rate at the recommended CR (0–1) |
1435
+ | `expectedWinRateRange` | `{ min, max }` | Target win rate range for the difficulty |
1436
+ | `confidenceInterval` | `string` | Human-readable, e.g., `"73% ± 5%"` |
1437
+ | `marginOfError` | `number` | Statistical margin of error (±) |
1438
+ | `converged` | `boolean` | Whether the search found a CR within the target range |
1439
+ | `totalSimulationsRun` | `number` | Total simulations across all probes |
1440
+ | `iterationsUsed` | `number` | Number of probes actually performed |
1441
+ | `probes` | `DifficultyProbe[]` | Full search history (CR, win rate, etc. per probe) |
1442
+ | `initialCREstimate` | `number` | XP-budget-based CR estimate (before simulation) |
1443
+ | `suggestedEnemy` | `CharacterSheet` | Generated enemy at the recommended CR |
1444
+ | `wasCancelled` | `boolean` | Whether the search was cancelled |
1445
+
1446
+ ### DifficultyProbe
1447
+
1448
+ Each entry in the `probes` array records one step of the binary search.
1449
+
1450
+ | Field | Type | Description |
1451
+ |-------|------|-------------|
1452
+ | `cr` | `number` | CR value tested |
1453
+ | `winRate` | `number` | Player win rate from simulation |
1454
+ | `totalRuns` | `number` | Number of simulation runs |
1455
+ | `averageRounds` | `number` | Average rounds to resolution |
1456
+ | `averageHPRemaining` | `number` | Average player HP % remaining on wins |
1457
+
1458
+ ### Multi-Enemy Encounters
1459
+
1460
+ For encounters with multiple enemies, set `enemyCount` to the desired number. The calculator adjusts the XP budget using the encounter multiplier (1 enemy = 1×, 2 enemies = 1.5×, 3–6 = 2×, 7–10 = 2.5×, 11–14 = 3×) and searches for a per-enemy CR that produces the target difficulty.
1461
+
1462
+ ```typescript
1463
+ const suggestion = calculator.suggest(
1464
+ party,
1465
+ { rarity: 'common', archetype: 'archer' },
1466
+ 'medium',
1467
+ { aiConfig, enemyCount: 3, baseSeed: 'mob-search' },
1468
+ );
1469
+
1470
+ // Recommended CR is per-enemy — all 3 enemies use this CR
1471
+ console.log(suggestion.recommendedCR); // e.g., 0.5
1472
+ ```
1473
+
1474
+ ### Cancellation
1475
+
1476
+ Like other long-running tools, the calculator supports `AbortController` for cancellation. Partial results are returned with `wasCancelled: true`.
1477
+
1478
+ ```typescript
1479
+ const controller = new AbortController();
1480
+
1481
+ // Cancel after 3 seconds
1482
+ setTimeout(() => controller.abort(), 3000);
1483
+
1484
+ const suggestion = calculator.suggest(
1485
+ party, template, 'hard',
1486
+ { aiConfig, baseSeed: 'quick', abortSignal: controller.signal },
1487
+ );
1488
+
1489
+ console.log(suggestion.wasCancelled); // true
1490
+ console.log(suggestion.probes.length); // 1–2 (partial results)
1491
+ ```
1492
+
1493
+ ### When to Use
1494
+
1495
+ | Scenario | Tool |
1496
+ |----------|------|
1497
+ | "Is this encounter balanced?" | `BalanceValidator` (validate existing config) |
1498
+ | "How does difficulty change across CR 1–10?" | `ParameterSweep` (curve exploration) |
1499
+ | "What CR should I use for Medium difficulty?" | **`DifficultyCalculator`** (inverse problem) |
1500
+ | "Is +2 AC meaningfully different?" | `ComparativeAnalyzer` (A/B testing) |
1501
+
1502
+ ---
1503
+
1504
+ ## Recommended Simulation Counts
1505
+
1506
+ Choosing the right number of simulation runs balances confidence against speed. More runs = tighter confidence intervals, but diminishing returns set in quickly.
1507
+
1508
+ ### Quick Reference
1509
+
1510
+ | Purpose | Runs | Speed* | Confidence | Margin of Error | Use When |
1511
+ |---------|------|--------|------------|-----------------|----------|
1512
+ | Quick exploration | 100 | <0.1s | Rough estimate | ~±10% | Prototyping, sanity checks, early iteration |
1513
+ | Standard analysis | 500 | <0.5s | Reasonable | ~±4% | Most balance decisions, parameter sweeps |
1514
+ | Thorough validation | 2,000 | ~1s | High | ~±2% | Final balance passes, shipping decisions |
1515
+ | Publication-quality | 5,000+ | ~2s | Very high | ~±1.5% | Documentation, balance patches, release notes |
1516
+
1517
+ *\*Speed estimates based on standard 4v1 party-vs-encounter composition. Actual speed depends on encounter size and combat duration.*
1518
+
1519
+ ### How Margin of Error Works
1520
+
1521
+ The simulator reports results as percentages (e.g., "72% player win rate"). With fewer runs, that percentage has more uncertainty:
1522
+
1523
+ - **100 runs**: A reported 72% win rate means the true value is likely between 62–82% (±10%)
1524
+ - **500 runs**: A reported 72% win rate means the true value is likely between 68–76% (±4%)
1525
+ - **2,000 runs**: A reported 72% win rate means the true value is likely between 70–74% (±2%)
1526
+
1527
+ The formula: `margin of error ≈ 1 / √(totalRuns)` (at 95% confidence).
1528
+
1529
+ ### Recommendations by Tool
1530
+
1531
+ | Tool | Recommended Runs | Why |
1532
+ |------|-----------------|-----|
1533
+ | `BalanceValidator` | 500 | Single data point — 500 gives ±4% which is enough to classify difficulty |
1534
+ | `ParameterSweep` | 100–250 per point | Sweeps generate many data points — lower per-point counts are acceptable for curve shape |
1535
+ | `ComparativeAnalyzer` | 500–1,000 per config | Statistical significance testing needs sufficient sample size per config |
1536
+ | `DifficultyCalculator` | 250 per probe | Binary search makes multiple probes — lower per-probe counts with convergence checking |
1537
+
1538
+ ### When to Use More Runs
1539
+
1540
+ - **Close to a difficulty boundary**: If win rate is near a threshold (e.g., 68–72% for Medium), use more runs to determine which side it falls on
1541
+ - **Comparing similar configurations**: Small differences need larger samples to detect as statistically significant
1542
+ - **Boss encounters**: Boss fights have higher variance (legendary actions, more abilities) — use 2× the normal count
1543
+ - **Final ship decisions**: Always validate with 2,000+ runs before committing balance changes
1544
+
1545
+ ### When Fewer Runs Are Fine
1546
+
1547
+ - **Early iteration**: While adjusting CR, enemy count, or templates, 100 runs gives enough signal to guide direction
1548
+ - **Obviously broken balance**: A 5% or 95% win rate won't change meaningfully with more runs
1549
+ - **Sweep exploration**: A 20-point CR sweep at 100 runs/point completes in under a second and shows the trend clearly
1550
+
1551
+ ---
1552
+
1553
+ ## API Reference
1554
+
1555
+ ### EnemyGenerator
1556
+
1557
+ Static class - no instantiation required.
1558
+
1559
+ #### generate()
1560
+
1561
+ Generate a single enemy.
1562
+
1563
+ ```typescript
1564
+ static generate(options: EnemyGenerationOptions): CharacterSheet
1565
+ ```
1566
+
1567
+ **Parameters:**
1568
+ - `seed` (required): Seed for deterministic generation
1569
+ - `templateId` (optional): Force specific template by ID
1570
+ - `rarity` (optional): Rarity tier (default: `'common'`)
1571
+ - `difficultyMultiplier` (optional): Fine-tune HP/damage (default: 1.0)
1572
+ - `audioProfile` (optional): Audio profile for template selection
1573
+ - `track` (optional): Track data (required if audioProfile provided)
1574
+
1575
+ **Returns:** `CharacterSheet` representing the enemy
1576
+
1577
+ **Throws:**
1578
+ - `Error` if templateId not found
1579
+ - `Error` if audioProfile provided without track
1580
+
1581
+ #### generateEncounter()
1582
+
1583
+ Generate balanced encounter for a party.
1584
+
1585
+ ```typescript
1586
+ static generateEncounter(
1587
+ party: CharacterSheet[],
1588
+ options: EncounterGenerationOptions
1589
+ ): CharacterSheet[]
1590
+ ```
1591
+
1592
+ **Parameters:** All options from `EncounterGenerationOptions` interface
1593
+
1594
+ **Returns:** Array of generated enemies
1595
+
1596
+ #### generateEncounterByCR()
1597
+
1598
+ Generate encounter by target CR (no party analysis).
1599
+
1600
+ ```typescript
1601
+ static generateEncounterByCR(
1602
+ options: EncounterGenerationOptions
1603
+ ): CharacterSheet[]
1604
+ ```
1605
+
1606
+ **Returns:** Array of generated enemies
1607
+
1608
+ **Note:** Must include `targetCR` in options
1609
+
1610
+ #### getTemplateById()
1611
+
1612
+ Look up a template by ID.
1613
+
1614
+ ```typescript
1615
+ static getTemplateById(id: string): EnemyTemplate | undefined
1616
+ ```
1617
+
1618
+ **Returns:** Template object or `undefined` if not found
1619
+
1620
+ ### Type Reference
1621
+
1622
+ #### EnemyGenerationOptions
1623
+
1624
+ ```typescript
1625
+ interface EnemyGenerationOptions {
1626
+ seed: string; // Required
1627
+ cr?: number; // Recommended - target Challenge Rating (determines level/stats)
1628
+ templateId?: string; // Optional - force template
1629
+ rarity?: EnemyRarity; // Optional - default 'common' (determines complexity)
1630
+ difficultyMultiplier?: number; // Optional - default 1.0
1631
+ audioProfile?: AudioProfile; // Optional
1632
+ track?: PlaylistTrack; // Required if audioProfile
1633
+ category?: EnemyCategory; // Optional
1634
+ archetype?: EnemyArchetype; // Optional
1635
+ level?: number; // Optional - overrides CR-based level (rarely needed)
1636
+ }
1637
+ ```
1638
+
1639
+ **Important:** The `cr` parameter determines the enemy's power level (level and base stats). The `rarity` parameter determines complexity (abilities, signature die, resistances). These are independent - any CR can combine with any rarity.
1640
+
1641
+ #### EncounterGenerationOptions
1642
+
1643
+ ```typescript
1644
+ interface EncounterGenerationOptions {
1645
+ seed: string; // Required
1646
+ count: number; // Required
1647
+ difficulty?: EncounterDifficulty; // Party-based mode
1648
+ targetCR?: number; // CR-based mode - determines power level
1649
+ baseRarity?: EnemyRarity; // Optional - default 'common'
1650
+ scaleRarityWithCR?: boolean; // Optional - default false (opt-in CR-based rarity scaling)
1651
+ difficultyMultiplier?: number; // Optional - default 1.0
1652
+ category?: EnemyCategory; // Optional
1653
+ archetype?: EnemyArchetype; // Optional
1654
+ templateId?: string; // Optional
1655
+ enemyMix?: 'uniform' | 'custom' | 'category' | 'random'; // V2: added category, random
1656
+ templates?: string[]; // For custom mix
1657
+ audioProfile?: AudioProfile; // Optional
1658
+ track?: PlaylistTrack; // Required if audioProfile
1659
+ enableLeaderPromotion?: boolean; // Optional - default true
1660
+ // V2 additions:
1661
+ allowMixedCategories?: boolean; // For 'random' mode validation
1662
+ lairFeatures?: boolean; // Include lair actions for bosses
1663
+ minRarity?: EnemyRarity; // Force minimum rarity
1664
+ maxRarity?: EnemyRarity; // Cap maximum rarity
1665
+ statLevels?: StatLevelOverrides; // HP/attack/defense level overrides
1666
+ }
1667
+ ```
1668
+
1669
+ **CR vs Rarity:** By default, `targetCR` and `baseRarity` are independent. Set `scaleRarityWithCR: true` to opt-in to automatic rarity scaling based on CR (higher CR = higher average rarity).
1670
+
1671
+ **Boss Encounters:** When rarity is 'boss', count is automatically enforced to 1 (bosses are always 1vparty).
1672
+
1673
+ #### EnemyRarity
1674
+
1675
+ ```typescript
1676
+ type EnemyRarity = 'common' | 'uncommon' | 'elite' | 'boss';
1677
+ ```
1678
+
1679
+ #### EnemyCategory
1680
+
1681
+ ```typescript
1682
+ type EnemyCategory =
1683
+ | 'humanoid'
1684
+ | 'beast'
1685
+ | 'undead'
1686
+ | 'dragon'
1687
+ | 'fiend'
1688
+ | 'construct'
1689
+ | 'elemental'
1690
+ | 'monstrosity';
1691
+ ```
1692
+
1693
+ #### EnemyArchetype
1694
+
1695
+ ```typescript
1696
+ type EnemyArchetype = 'brute' | 'archer' | 'support';
1697
+ ```
1698
+
1699
+ #### EncounterDifficulty
1700
+
1701
+ ```typescript
1702
+ type EncounterDifficulty = 'easy' | 'medium' | 'hard' | 'deadly';
1703
+ ```
1704
+
1705
+ ---
1706
+
1707
+ ## See Also
1708
+
1709
+ - [COMBAT_SYSTEM.md](COMBAT_SYSTEM.md) - Combat system reference
1710
+ - [DATA_ENGINE_REFERENCE.md](DATA_ENGINE_REFERENCE.md) - Complete API reference
1711
+ - [specs/001-core-engine/SPEC.md](specs/001-core-engine/SPEC.md) - Core engine specification