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,1106 @@
|
|
|
1
|
+
# Playlist Data Engine - Extensibility Guide
|
|
2
|
+
|
|
3
|
+
This guide explains how to extend the Playlist Data Engine with custom content. The extensibility system allows you to add custom spells, equipment, races, classes, and appearance options at runtime, with full control over spawn rates and validation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Overview](#overview)
|
|
10
|
+
2. [ExtensionManager API](#extensionmanager-api)
|
|
11
|
+
3. [Helper Functions](#helper-functions)
|
|
12
|
+
4. [Spawn Rate System](#spawn-rate-system)
|
|
13
|
+
5. [Category-Specific Examples](#category-specific-examples)
|
|
14
|
+
6. [Batch Image Methods](#batch-image-methods)
|
|
15
|
+
7. [Content Packs](#content-packs)
|
|
16
|
+
8. [Best Practices](#best-practices)
|
|
17
|
+
9. [Validation](#validation)
|
|
18
|
+
10. [Troubleshooting](#troubleshooting)
|
|
19
|
+
11. [Reference](#reference)
|
|
20
|
+
12. [Support](#support)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Overview
|
|
25
|
+
|
|
26
|
+
The extensibility system allows you to:
|
|
27
|
+
|
|
28
|
+
- **Add custom content** to any procedural generation category
|
|
29
|
+
- **Control spawn rates** with relative or absolute weighting
|
|
30
|
+
- **Validate content** automatically with clear error messages
|
|
31
|
+
- **Create content packs** that can be loaded at runtime
|
|
32
|
+
|
|
33
|
+
**Location:** [src/core/extensions/ExtensionManager.ts](../src/core/extensions/ExtensionManager.ts)
|
|
34
|
+
|
|
35
|
+
### Supported Categories
|
|
36
|
+
|
|
37
|
+
| Category | Description | Example |
|
|
38
|
+
|----------|-------------|---------|
|
|
39
|
+
| `equipment` | Weapons, armor, items | Custom weapons, magic items |
|
|
40
|
+
| `equipment.templates` | Complete equipment templates | Pre-built items with properties |
|
|
41
|
+
| `appearance.bodyTypes` | Character body shapes | 'giant', 'diminutive', etc. |
|
|
42
|
+
| `appearance.skinTones` | Skin color options | Hex colors |
|
|
43
|
+
| `appearance.hairColors` | Hair color options | Hex colors |
|
|
44
|
+
| `appearance.hairStyles` | Hair style options | 'braided', 'mohawk', etc. |
|
|
45
|
+
| `appearance.eyeColors` | Eye color options | Hex colors |
|
|
46
|
+
| `appearance.facialFeatures` | Facial features | 'scar', 'tattoo', etc. |
|
|
47
|
+
| `spells` | Arcane and divine magic | Custom spells |
|
|
48
|
+
| `spells.{className}` | Class-specific spells | 'spells.Wizard' |
|
|
49
|
+
| `races` | Race names | Custom races |
|
|
50
|
+
| `races.data` | Race data | Ability bonuses, speed, traits, subraces |
|
|
51
|
+
| `classes` | Class names | Custom classes |
|
|
52
|
+
| `classes.data` | Class data | Hit die, saves, skills, spellcasting |
|
|
53
|
+
| `classFeatures` | All class features | Custom rage, metamagic, etc. |
|
|
54
|
+
| `classFeatures.{className}` | Class-specific features | 'classFeatures.Barbarian' |
|
|
55
|
+
| `racialTraits` | All racial traits | Custom darkvision, stonecunning |
|
|
56
|
+
| `racialTraits.{raceName}` | Race-specific traits | 'racialTraits.Elf' |
|
|
57
|
+
| `skills` | All skills (default + custom) | Custom survival, knowledge |
|
|
58
|
+
| `skills.{ability}` | Ability-specific skills | 'skills.STR', 'skills.DEX' |
|
|
59
|
+
| `skillLists` | All skill lists | Per-class skill selections |
|
|
60
|
+
| `skillLists.{className}` | Class-specific skill lists | 'skillLists.Barbarian' |
|
|
61
|
+
| `classSpellLists` | All class spell lists | Class-specific spell selections |
|
|
62
|
+
| `classSpellLists.{className}` | Class-specific spell list | Custom spell lists |
|
|
63
|
+
| `classSpellSlots` | Spell slot progressions | Custom slot progressions by level |
|
|
64
|
+
| `classStartingEquipment` | All class starting equipment | Default gear per class |
|
|
65
|
+
| `classStartingEquipment.{className}` | Class-specific equipment | Custom starting equipment |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## ExtensionManager API
|
|
70
|
+
|
|
71
|
+
The `ExtensionManager` is a singleton that manages all custom content.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { ExtensionManager } from 'playlist-data-engine';
|
|
75
|
+
|
|
76
|
+
const manager = ExtensionManager.getInstance();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Location:** [src/core/extensions/ExtensionManager.ts](../src/core/extensions/ExtensionManager.ts)
|
|
80
|
+
|
|
81
|
+
### Core Methods
|
|
82
|
+
|
|
83
|
+
| Method | Parameters | Return | Description |
|
|
84
|
+
|--------|------------|--------|-------------|
|
|
85
|
+
| `register()` | `category`, `items`, `options?` | `void` | Register custom content for a category |
|
|
86
|
+
| `registerMultiple()` | `registrations[]` | `void` | Register multiple categories at once |
|
|
87
|
+
| `get()` | `category` | `any[]` | Get all items (defaults + custom) |
|
|
88
|
+
| `getDefaults()` | `category` | `any[]` | Get default items only |
|
|
89
|
+
| `getCustom()` | `category` | `any[]` | Get custom items only |
|
|
90
|
+
| `setWeights()` | `category`, `weights` | `void` | Set spawn weights for items |
|
|
91
|
+
| `getWeights()` | `category` | `Record` | Get current weights (defaults + custom) |
|
|
92
|
+
| `getDefaultWeights()` | `category` | `Record` | Get default weights (all 1.0) |
|
|
93
|
+
| `setMode()` | `category`, `mode` | `void` | Change spawn mode after registration |
|
|
94
|
+
| `getMode()` | `category` | `SpawnMode \| undefined` | Get current spawn mode |
|
|
95
|
+
| `hasCustomData()` | `category` | `boolean` | Check if category has custom data |
|
|
96
|
+
| `validate()` | `category`, `items` | `ValidationResult` | Validate items without registering |
|
|
97
|
+
| `reset()` | `category` | `void` | Reset category to defaults |
|
|
98
|
+
| `resetAll()` | | `void` | Reset all categories to defaults |
|
|
99
|
+
| `getInfo()` | `category?` | `Record` | Get info about registered extensions |
|
|
100
|
+
| `getCurrentOptions()` | `category` | `ExtensionOptions \| undefined` | Get current registration options |
|
|
101
|
+
| `exportCustomData()` | | `Record` | Export all custom data |
|
|
102
|
+
| `exportCustomDataForCategory()` | `category` | `any[]` | Export custom data for single category |
|
|
103
|
+
| `getRegisteredCategories()` | | `ExtensionCategory[]` | Get all categories with defaults |
|
|
104
|
+
| `batchAddIcons()` | `category`, `iconMap`, `identifierKey?` | `number` | Add icons to items by name/ID. Returns count updated. |
|
|
105
|
+
| `batchAddImages()` | `category`, `imageMap`, `identifierKey?` | `number` | Add images to items by name/ID. Returns count updated. |
|
|
106
|
+
| `batchUpdateImages()` | `category`, `predicate`, `updates` | `number` | Update icon/image on items matching predicate. Returns count. |
|
|
107
|
+
| `batchByCategory()` | `category`, `property`, `valueMap` | `number` | Add icons/images by property value (e.g., school). Returns count. |
|
|
108
|
+
| `getImageOverrides()` | | `Map<Category, ImageOverride[]>` | Get all image overrides for all categories. |
|
|
109
|
+
| `getImageOverridesForCategory()` | `category` | `ImageOverride[]` | Get image overrides for a specific category. |
|
|
110
|
+
| `restoreImageOverrides()` | `category`, `overrides` | `void` | Restore saved image overrides (for persistence). |
|
|
111
|
+
| `clearImageOverrides()` | `category` | `void` | Clear all image overrides for a category. |
|
|
112
|
+
| `clearAllImageOverrides()` | | `void` | Clear all image overrides for all categories. |
|
|
113
|
+
|
|
114
|
+
### Registration Options
|
|
115
|
+
|
|
116
|
+
| Option | Type | Default | Description |
|
|
117
|
+
|--------|------|---------|-------------|
|
|
118
|
+
| `mode` | `'relative' \| 'absolute' \| 'default' \| 'replace'` | `'relative'` | Spawn mode for this extension |
|
|
119
|
+
| `weights` | `Record<string, number>` | `{}` | Custom spawn weights for items |
|
|
120
|
+
| `validate` | `boolean` | `true` | Whether to validate items before registering |
|
|
121
|
+
|
|
122
|
+
**Note:** Setting `mode` or `weights` during registration affects the entire category, not just the items being registered. This is a convenience equivalent to calling `setMode()` or `setWeights()` separately.
|
|
123
|
+
|
|
124
|
+
### Spawn Modes
|
|
125
|
+
|
|
126
|
+
| Mode | Behavior | Use Case |
|
|
127
|
+
|------|----------|----------|
|
|
128
|
+
| `relative` | Custom items added to default pool with custom weights | Add custom items to existing pool |
|
|
129
|
+
| `absolute` | Only custom items can spawn (defaults excluded) | Themed content packs, complete replacement |
|
|
130
|
+
| `default` | All items have equal weight (1.0) | Disable custom spawn weights |
|
|
131
|
+
| `replace` | Clear previous custom data before registering | Hot-reload content packs during development |
|
|
132
|
+
|
|
133
|
+
### Weight Values
|
|
134
|
+
|
|
135
|
+
| Value | Effect |
|
|
136
|
+
|-------|--------|
|
|
137
|
+
| `0` | Never spawns |
|
|
138
|
+
| `0.5` | Half as common as default |
|
|
139
|
+
| `1.0` | Default spawn rate |
|
|
140
|
+
| `2.0` | Twice as common as default |
|
|
141
|
+
| `10.0` | Very common |
|
|
142
|
+
|
|
143
|
+
### Usage Example
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// Register custom equipment with icon/image
|
|
147
|
+
manager.register('equipment', [
|
|
148
|
+
{
|
|
149
|
+
name: 'Dragon Sword',
|
|
150
|
+
type: 'weapon',
|
|
151
|
+
rarity: 'legendary',
|
|
152
|
+
weight: 5,
|
|
153
|
+
icon: '/icons/weapons/dragon-sword.png',
|
|
154
|
+
image: '/images/equipment/dragon-sword.png'
|
|
155
|
+
}
|
|
156
|
+
], { mode: 'relative', weights: { 'Dragon Sword': 0.5 } });
|
|
157
|
+
|
|
158
|
+
// Adjust weights
|
|
159
|
+
manager.setWeights('equipment', {
|
|
160
|
+
'Longsword': 2,
|
|
161
|
+
'Dagger': 0.5,
|
|
162
|
+
'Excalibur': 0.1
|
|
163
|
+
});
|
|
164
|
+
const weights = manager.getWeights('equipment');
|
|
165
|
+
|
|
166
|
+
// Inspect registered data
|
|
167
|
+
if (manager.hasCustomData('equipment')) {
|
|
168
|
+
const info = manager.getInfo('equipment');
|
|
169
|
+
const customItems = manager.getCustom('equipment');
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Helper Functions
|
|
176
|
+
|
|
177
|
+
The engine provides several helper functions for querying custom content. These complement `ExtensionManager` by providing read access to registered data.
|
|
178
|
+
|
|
179
|
+
### Quick Reference
|
|
180
|
+
|
|
181
|
+
| Function | Parameters | Returns | Description |
|
|
182
|
+
|----------|------------|---------|-------------|
|
|
183
|
+
| `getClassData` | `className: string` | `ClassDataEntry \| undefined` | Class data with hit die, abilities, features |
|
|
184
|
+
| `getRaceData` | `raceName: string` | `RaceDataEntry \| undefined` | Race data with speed, ability bonuses, traits |
|
|
185
|
+
| `getClassSpellList` | `className: string` | Spell list object \| undefined | `{ cantrips: string[], spells_by_level: Record<number, string[]> }` |
|
|
186
|
+
| `getSpellSlotsForClass` | `className, characterLevel` | Slot object \| undefined | `{ [level: number]: slots }` |
|
|
187
|
+
| `getClassStartingEquipment` | `className: string` | Equipment object \| undefined | `{ weapons: [...], armor: [...], items: [...] }` |
|
|
188
|
+
|
|
189
|
+
### Usage Examples
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import {
|
|
193
|
+
getClassData,
|
|
194
|
+
getRaceData,
|
|
195
|
+
getClassSpellList,
|
|
196
|
+
getSpellSlotsForClass,
|
|
197
|
+
getClassStartingEquipment
|
|
198
|
+
} from 'playlist-data-engine';
|
|
199
|
+
|
|
200
|
+
// Class data (default or custom)
|
|
201
|
+
const wizardData = getClassData('Wizard');
|
|
202
|
+
console.log(wizardData.hit_die); // 6
|
|
203
|
+
|
|
204
|
+
const necromancerData = getClassData('Necromancer');
|
|
205
|
+
if (necromancerData) {
|
|
206
|
+
console.log(necromancerData.baseClass); // 'Wizard'
|
|
207
|
+
console.log(necromancerData.primary_ability); // 'INT'
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Race data (default or custom)
|
|
211
|
+
const elfData = getRaceData('Elf');
|
|
212
|
+
console.log(elfData.speed); // 30
|
|
213
|
+
|
|
214
|
+
const dragonkinData = getRaceData('Dragonkin');
|
|
215
|
+
if (dragonkinData) {
|
|
216
|
+
console.log(dragonkinData.ability_bonuses);
|
|
217
|
+
// { STR: 2, CON: 1, CHA: 1 }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Additional class queries
|
|
221
|
+
const spellList = getClassSpellList('Necromancer');
|
|
222
|
+
const slots = getSpellSlotsForClass('Necromancer', 5);
|
|
223
|
+
const equipment = getClassStartingEquipment('Necromancer');
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Spawn Rate System
|
|
229
|
+
|
|
230
|
+
The spawn rate system controls how custom content is mixed with default content during procedural generation. See the [Spawn Modes table](#extensionmanager-api) for mode descriptions.
|
|
231
|
+
|
|
232
|
+
### Advanced Weight Configuration
|
|
233
|
+
|
|
234
|
+
You can use hierarchical weight configuration for fine-grained control:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
const manager = ExtensionManager.getInstance();
|
|
238
|
+
|
|
239
|
+
// Hierarchical weight system
|
|
240
|
+
// Category defaults with individual overrides
|
|
241
|
+
|
|
242
|
+
// Set default for all skills
|
|
243
|
+
manager.setWeights('skills', {
|
|
244
|
+
default: 1.0 // All skills have equal weight by default
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Override specific skills
|
|
248
|
+
manager.setWeights('skills', {
|
|
249
|
+
'athletics': 2.0, // Override: athletics is now 2x
|
|
250
|
+
'acrobatics': 0.5, // Override: acrobatics is now 0.5x
|
|
251
|
+
// All other skills remain at 1.0 (the default)
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Per-class skill spawn rates
|
|
255
|
+
manager.setWeights('skillLists.Barbarian', {
|
|
256
|
+
'athletics': 2.0, // Barbarians favor athletics
|
|
257
|
+
'survival': 1.5, // And survival
|
|
258
|
+
'arcana': 0.2 // But rarely get arcana
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
manager.setWeights('skillLists.Wizard', {
|
|
262
|
+
'arcana': 2.0, // Wizards favor arcana
|
|
263
|
+
'history': 1.5, // And history
|
|
264
|
+
'athletics': 0.2 // But rarely get athletics
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Zero weight = never spawn
|
|
268
|
+
manager.setWeights('classFeatures.Barbarian', {
|
|
269
|
+
'useless_feature': 0.0 // This feature will never spawn
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Reset to defaults
|
|
273
|
+
manager.reset('classFeatures.Barbarian');
|
|
274
|
+
// Now all Barbarian features are back to equal probability
|
|
275
|
+
|
|
276
|
+
// Reset all categories
|
|
277
|
+
manager.resetAll();
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Category-Specific Examples
|
|
283
|
+
|
|
284
|
+
### Equipment
|
|
285
|
+
|
|
286
|
+
Register custom equipment through ExtensionManager or via the `CharacterGenerator.generate()` convenience parameter. For complete examples including registration, spawn rates, and the CharacterGenerator convenience method, see [Custom Equipment](EQUIPMENT_SYSTEM.md#custom-equipment).
|
|
287
|
+
|
|
288
|
+
### Spells
|
|
289
|
+
|
|
290
|
+
Register custom spells through ExtensionManager:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { ExtensionManager } from 'playlist-data-engine';
|
|
294
|
+
|
|
295
|
+
const manager = ExtensionManager.getInstance();
|
|
296
|
+
|
|
297
|
+
const customSpells = [
|
|
298
|
+
{
|
|
299
|
+
name: 'Phoenix Fire',
|
|
300
|
+
level: 5,
|
|
301
|
+
school: 'Evocation',
|
|
302
|
+
casting_time: '1 action',
|
|
303
|
+
range: '60 feet',
|
|
304
|
+
duration: 'Instantaneous',
|
|
305
|
+
components: ['V', 'S'],
|
|
306
|
+
description: 'A burst of flame engulfs the target...',
|
|
307
|
+
icon: '/icons/spells/phoenix-fire.png',
|
|
308
|
+
image: '/images/spells/phoenix-fire.png'
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: 'Mind Shield',
|
|
312
|
+
level: 2,
|
|
313
|
+
school: 'Abjuration',
|
|
314
|
+
casting_time: '1 reaction',
|
|
315
|
+
range: 'Self',
|
|
316
|
+
duration: '1 minute',
|
|
317
|
+
components: ['S'],
|
|
318
|
+
description: 'You gain resistance to psychic damage...',
|
|
319
|
+
icon: '/icons/spells/mind-shield.png',
|
|
320
|
+
image: '/images/spells/mind-shield.png'
|
|
321
|
+
}
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
// Register custom spells
|
|
325
|
+
manager.register('spells', customSpells);
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
#### Spell Query
|
|
330
|
+
|
|
331
|
+
Query spells and check prerequisites using SpellQuery:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { SpellQuery } from 'playlist-data-engine';
|
|
335
|
+
|
|
336
|
+
const spellQuery = SpellQuery.getInstance();
|
|
337
|
+
|
|
338
|
+
// Query spells by level, school, or class
|
|
339
|
+
const fifthLevelSpells = spellQuery.getSpellsByLevel(5);
|
|
340
|
+
const evocationSpells = spellQuery.getSpellsBySchool('Evocation');
|
|
341
|
+
const sorcererSpells = spellQuery.getSpellsForClass('Sorcerer');
|
|
342
|
+
|
|
343
|
+
// Get spells available to a character (prerequisites met)
|
|
344
|
+
const availableSpells = spellQuery.getAvailableSpells(character);
|
|
345
|
+
console.log(`Available spells: ${availableSpells.map(s => s.name).join(', ')}`);
|
|
346
|
+
|
|
347
|
+
// Get a specific spell
|
|
348
|
+
const phoenixFire = spellQuery.getSpell('phoenix_fire');
|
|
349
|
+
|
|
350
|
+
// Validate spell prerequisites
|
|
351
|
+
if (phoenixFire) {
|
|
352
|
+
const validation = spellQuery.validatePrerequisites(phoenixFire, character);
|
|
353
|
+
if (!validation.valid) {
|
|
354
|
+
console.log(`Prerequisites not met: ${validation.errors.join(', ')}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Query statistics
|
|
359
|
+
const stats = spellQuery.getQueryStats();
|
|
360
|
+
console.log(`Total spells: ${stats.totalSpells} (${stats.customSpells} custom)`);
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
#### Spell Prerequisites
|
|
365
|
+
|
|
366
|
+
Spells can have prerequisites that must be met before a spellcaster can learn them (features, abilities, spells, skills, level, or class). For full examples and usage, see [Spells with Prerequisites](PREREQUISITES.md#spells-with-prerequisites).
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
### Races / Subraces
|
|
370
|
+
|
|
371
|
+
Add custom races and subraces, and control their spawn rates. For complete examples including type augmentation, race data registration, validation, subrace support, and spawn rate control, see [Custom Races](CUSTOM_CONTENT.md#custom-races).
|
|
372
|
+
|
|
373
|
+
### Classes
|
|
374
|
+
|
|
375
|
+
Adjust spawn rates for existing classes or create entirely new custom classes. For complete examples including template inheritance, audio preferences, spawn rate control, and full class registration, see [Custom Classes](CUSTOM_CONTENT.md#custom-classes).
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
### Class Features
|
|
379
|
+
|
|
380
|
+
Register custom class features through ExtensionManager:
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
import { ExtensionManager } from 'playlist-data-engine';
|
|
384
|
+
|
|
385
|
+
const manager = ExtensionManager.getInstance();
|
|
386
|
+
|
|
387
|
+
// Register custom class features (recommended approach)
|
|
388
|
+
manager.register('classFeatures', [
|
|
389
|
+
{
|
|
390
|
+
id: 'dragon_fury',
|
|
391
|
+
name: 'Dragon Fury',
|
|
392
|
+
description: 'Channel your draconic heritage to unleash devastating attacks',
|
|
393
|
+
type: 'active',
|
|
394
|
+
class: 'Barbarian',
|
|
395
|
+
level: 3,
|
|
396
|
+
prerequisites: {
|
|
397
|
+
level: 3
|
|
398
|
+
},
|
|
399
|
+
effects: [
|
|
400
|
+
{
|
|
401
|
+
type: 'passive_modifier',
|
|
402
|
+
target: 'damage',
|
|
403
|
+
value: 2,
|
|
404
|
+
condition: 'while raging'
|
|
405
|
+
}
|
|
406
|
+
],
|
|
407
|
+
source: 'custom',
|
|
408
|
+
icon: '/icons/features/dragon-fury.png',
|
|
409
|
+
image: '/images/features/dragon-fury.png'
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
id: 'arcane_shield',
|
|
413
|
+
name: 'Arcane Shield',
|
|
414
|
+
description: 'Create a protective barrier of magical energy',
|
|
415
|
+
type: 'active',
|
|
416
|
+
class: 'Wizard',
|
|
417
|
+
level: 2,
|
|
418
|
+
prerequisites: {
|
|
419
|
+
level: 2,
|
|
420
|
+
abilities: { INT: 14 }
|
|
421
|
+
},
|
|
422
|
+
effects: [
|
|
423
|
+
{
|
|
424
|
+
type: 'ability_unlock',
|
|
425
|
+
target: 'mage_armor',
|
|
426
|
+
value: true
|
|
427
|
+
}
|
|
428
|
+
],
|
|
429
|
+
source: 'custom',
|
|
430
|
+
icon: '/icons/features/arcane-shield.png',
|
|
431
|
+
image: '/images/features/arcane-shield.png'
|
|
432
|
+
}
|
|
433
|
+
]);
|
|
434
|
+
|
|
435
|
+
// Set spawn rates for features
|
|
436
|
+
manager.setWeights('classFeatures.Barbarian', {
|
|
437
|
+
'dragon_fury': 0.5, // Half as likely to spawn
|
|
438
|
+
'rage': 1.0 // Default spawn rate
|
|
439
|
+
});
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Feature Effect Types:**
|
|
443
|
+
|
|
444
|
+
| Type | Description | Example |
|
|
445
|
+
|------|-------------|---------|
|
|
446
|
+
| `stat_bonus` | Add to an ability score | +1 STR at level 4 |
|
|
447
|
+
| `skill_proficiency` | Grant proficiency or expertise | Expertise in Perception |
|
|
448
|
+
| `ability_unlock` | Unlock new abilities | Darkvision, flight |
|
|
449
|
+
| `passive_modifier` | Constant bonus to rolls | +2 damage while raging |
|
|
450
|
+
| `resource_grant` | Grant resource pools | Rage counts, ki points |
|
|
451
|
+
| `spell_slot_bonus` | Additional spell slots | +1 level 1 slot |
|
|
452
|
+
|
|
453
|
+
**Feature Prerequisites:**
|
|
454
|
+
|
|
455
|
+
Features can require levels, abilities, skills, spells, features, class, race, subrace, or custom conditions. For complete examples including skill prerequisites, spell prerequisites, racial traits with prerequisites, and validation, see [Feature Prerequisites](PREREQUISITES.md#feature-prerequisites).
|
|
456
|
+
|
|
457
|
+
### Racial Traits
|
|
458
|
+
|
|
459
|
+
Register custom racial traits through ExtensionManager:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { ExtensionManager } from 'playlist-data-engine';
|
|
463
|
+
|
|
464
|
+
const manager = ExtensionManager.getInstance();
|
|
465
|
+
|
|
466
|
+
// Register custom racial traits (recommended approach)
|
|
467
|
+
manager.register('racialTraits', [
|
|
468
|
+
{
|
|
469
|
+
id: 'dragon_born_fire_resistance',
|
|
470
|
+
name: 'Fire Resistance',
|
|
471
|
+
description: 'You have resistance to fire damage',
|
|
472
|
+
race: 'Dragonborn',
|
|
473
|
+
effects: [
|
|
474
|
+
{
|
|
475
|
+
type: 'ability_unlock',
|
|
476
|
+
target: 'damage_resistance',
|
|
477
|
+
value: 'fire'
|
|
478
|
+
}
|
|
479
|
+
],
|
|
480
|
+
source: 'default',
|
|
481
|
+
icon: '/icons/traits/fire-resistance.png',
|
|
482
|
+
image: '/images/traits/fire-resistance.png'
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
id: 'fairy_flight',
|
|
486
|
+
name: 'Fey Wings',
|
|
487
|
+
description: 'You can fly using your magical wings',
|
|
488
|
+
race: 'Fairy',
|
|
489
|
+
prerequisites: {
|
|
490
|
+
level: 5
|
|
491
|
+
},
|
|
492
|
+
effects: [
|
|
493
|
+
{
|
|
494
|
+
type: 'ability_unlock',
|
|
495
|
+
target: 'flight',
|
|
496
|
+
value: true,
|
|
497
|
+
condition: 'level 5+'
|
|
498
|
+
}
|
|
499
|
+
],
|
|
500
|
+
source: 'custom',
|
|
501
|
+
icon: '/icons/traits/fey-wings.png',
|
|
502
|
+
image: '/images/traits/fey-wings.png'
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
id: 'elemental_affinity',
|
|
506
|
+
name: 'Elemental Affinity',
|
|
507
|
+
description: 'You are attuned to a specific element',
|
|
508
|
+
race: 'Genasi',
|
|
509
|
+
effects: [
|
|
510
|
+
{
|
|
511
|
+
type: 'ability_unlock',
|
|
512
|
+
target: 'elemental_magic',
|
|
513
|
+
value: true
|
|
514
|
+
}
|
|
515
|
+
],
|
|
516
|
+
source: 'custom',
|
|
517
|
+
icon: '/icons/traits/elemental-affinity.png',
|
|
518
|
+
image: '/images/traits/elemental-affinity.png'
|
|
519
|
+
}
|
|
520
|
+
]);
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
**Get traits for a race:**
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
const query = FeatureQuery.getInstance();
|
|
528
|
+
|
|
529
|
+
// Get all traits for a race
|
|
530
|
+
const dragonbornTraits = query.getRacialTraits('Dragonborn');
|
|
531
|
+
|
|
532
|
+
// Get traits for a subrace
|
|
533
|
+
const hillDwarfTraits = query.getRacialTraitsForSubrace('Dwarf', 'Hill Dwarf');
|
|
534
|
+
|
|
535
|
+
// Get a specific trait
|
|
536
|
+
const fireResistance = query.getRacialTraitById('dragon_born_fire_resistance');
|
|
537
|
+
|
|
538
|
+
const featureStats = query.getQueryStats();
|
|
539
|
+
console.log(`Features: ${featureStats.totalFeatures} (${featureStats.customFeatures} custom)`);
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Skills
|
|
543
|
+
|
|
544
|
+
Register custom skills through ExtensionManager:
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
import { ExtensionManager } from 'playlist-data-engine';
|
|
548
|
+
|
|
549
|
+
const manager = ExtensionManager.getInstance();
|
|
550
|
+
|
|
551
|
+
// Register custom skills (recommended approach)
|
|
552
|
+
manager.register('skills', [
|
|
553
|
+
{
|
|
554
|
+
id: 'survival_cold',
|
|
555
|
+
name: 'Survival (Cold Environments)',
|
|
556
|
+
description: 'Expertise in surviving freezing conditions',
|
|
557
|
+
ability: 'WIS',
|
|
558
|
+
armorPenalty: false,
|
|
559
|
+
categories: ['exploration', 'environmental'],
|
|
560
|
+
source: 'custom',
|
|
561
|
+
icon: '/icons/skills/survival-cold.png',
|
|
562
|
+
image: '/images/skills/survival-cold.png'
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
id: 'arcana_crystal',
|
|
566
|
+
name: 'Arcana (Crystals)',
|
|
567
|
+
description: 'Knowledge of magical crystals and their uses',
|
|
568
|
+
ability: 'INT',
|
|
569
|
+
armorPenalty: false,
|
|
570
|
+
categories: ['knowledge', 'magical'],
|
|
571
|
+
source: 'custom',
|
|
572
|
+
icon: '/icons/skills/arcana-crystal.png',
|
|
573
|
+
image: '/images/skills/arcana-crystal.png'
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
id: 'intimidation_war',
|
|
577
|
+
name: 'Intimidation (War Cry)',
|
|
578
|
+
description: 'Terrifying shouts on the battlefield',
|
|
579
|
+
ability: 'CHA',
|
|
580
|
+
armorPenalty: false,
|
|
581
|
+
categories: ['combat', 'social'],
|
|
582
|
+
source: 'custom',
|
|
583
|
+
icon: '/icons/skills/intimidation-war.png',
|
|
584
|
+
image: '/images/skills/intimidation-war.png'
|
|
585
|
+
}
|
|
586
|
+
]);
|
|
587
|
+
|
|
588
|
+
// Set spawn rates for skills
|
|
589
|
+
manager.setWeights('skills', {
|
|
590
|
+
'survival_cold': 0.5, // Half as likely
|
|
591
|
+
'athletics': 2.0, // Twice as likely
|
|
592
|
+
'intimidation_war': 1.0 // Default rate
|
|
593
|
+
});
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**Register ability-specific skills:**
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
const manager = ExtensionManager.getInstance();
|
|
600
|
+
|
|
601
|
+
// Register skills for specific abilities
|
|
602
|
+
manager.register('skills.STR', [
|
|
603
|
+
{
|
|
604
|
+
id: 'climbing',
|
|
605
|
+
name: 'Climbing',
|
|
606
|
+
ability: 'STR',
|
|
607
|
+
armorPenalty: true,
|
|
608
|
+
categories: ['athletic'],
|
|
609
|
+
source: 'custom'
|
|
610
|
+
}
|
|
611
|
+
]);
|
|
612
|
+
|
|
613
|
+
manager.register('skills.DEX', [
|
|
614
|
+
{
|
|
615
|
+
id: 'balancing',
|
|
616
|
+
name: 'Balancing',
|
|
617
|
+
ability: 'DEX',
|
|
618
|
+
armorPenalty: true,
|
|
619
|
+
categories: ['athletic'],
|
|
620
|
+
source: 'custom'
|
|
621
|
+
}
|
|
622
|
+
]);
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**Query skills:**
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
const query = SkillQuery.getInstance();
|
|
629
|
+
|
|
630
|
+
// Get skill by ID
|
|
631
|
+
const survival = query.getSkill('survival_cold');
|
|
632
|
+
|
|
633
|
+
// Get all skills for an ability
|
|
634
|
+
const strSkills = query.getSkillsByAbility('STR');
|
|
635
|
+
|
|
636
|
+
// Get skills by category
|
|
637
|
+
const explorationSkills = query.getSkillsByCategory('exploration');
|
|
638
|
+
|
|
639
|
+
// Get custom skills only
|
|
640
|
+
const customSkills = query.getSkillsBySource('custom');
|
|
641
|
+
|
|
642
|
+
// Check if skill exists
|
|
643
|
+
const isValid = query.isValidSkill('survival_cold'); // true
|
|
644
|
+
|
|
645
|
+
// Get registry statistics
|
|
646
|
+
const skillStats = query.getQueryStats();
|
|
647
|
+
console.log(`Skills: ${skillStats.totalSkills} (${skillStats.customSkills} custom)`);
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
#### Skills with Prerequisites
|
|
651
|
+
|
|
652
|
+
Skills can have prerequisites that must be met before a character can gain proficiency in them. This allows for advanced skills that require base skills, specific features, spells, ability scores, level, class, or race. For complete examples including skill chains, ability prerequisites, spell prerequisites, race prerequisites, and validation, see [Skill Prerequisites](PREREQUISITES.md#skill-prerequisites).
|
|
653
|
+
|
|
654
|
+
### Skill Lists
|
|
655
|
+
|
|
656
|
+
Define custom skill lists for classes:
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
const manager = ExtensionManager.getInstance();
|
|
660
|
+
|
|
661
|
+
// Register custom skill list for a class
|
|
662
|
+
manager.register('skillLists', [
|
|
663
|
+
{
|
|
664
|
+
class: 'Barbarian',
|
|
665
|
+
skillCount: 2,
|
|
666
|
+
availableSkills: [
|
|
667
|
+
'athletics',
|
|
668
|
+
'survival',
|
|
669
|
+
'survival_cold', // Custom skill
|
|
670
|
+
'intimidation',
|
|
671
|
+
'intimidation_war', // Custom skill
|
|
672
|
+
'nature',
|
|
673
|
+
'perception'
|
|
674
|
+
],
|
|
675
|
+
selectionWeights: {
|
|
676
|
+
weights: {
|
|
677
|
+
'athletics': 2.0,
|
|
678
|
+
'survival_cold': 0.5
|
|
679
|
+
},
|
|
680
|
+
mode: 'relative'
|
|
681
|
+
},
|
|
682
|
+
hasExpertise: false,
|
|
683
|
+
expertiseCount: 0
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
class: 'Necromancer', // Custom class
|
|
687
|
+
skillCount: 3,
|
|
688
|
+
availableSkills: [
|
|
689
|
+
'arcana',
|
|
690
|
+
'arcana_crystal', // Custom skill
|
|
691
|
+
'history',
|
|
692
|
+
'religion',
|
|
693
|
+
'medicine',
|
|
694
|
+
'investigation'
|
|
695
|
+
],
|
|
696
|
+
hasExpertise: true,
|
|
697
|
+
expertiseCount: 1
|
|
698
|
+
}
|
|
699
|
+
]);
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
### Appearance
|
|
704
|
+
|
|
705
|
+
Customize physical appearance options using ExtensionManager. **Tip:** Set mode to `'absolute'` to use only your custom options:
|
|
706
|
+
|
|
707
|
+
```typescript
|
|
708
|
+
const manager = ExtensionManager.getInstance();
|
|
709
|
+
|
|
710
|
+
// Register custom appearance options
|
|
711
|
+
manager.register('appearance.bodyTypes', ['giant', 'diminutive', 'elongated']);
|
|
712
|
+
manager.register('appearance.hairStyles', ['mohawk', 'braided', 'pompadour', 'mullet']);
|
|
713
|
+
manager.register('appearance.facialFeatures', ['crystal tattoo', 'runes on cheek', 'glowing eyes', 'fangs']);
|
|
714
|
+
|
|
715
|
+
// Register custom colors (hex format)
|
|
716
|
+
manager.register('appearance.skinTones', ['#8B7355', '#F5DEB3', '#FFE4C4']);
|
|
717
|
+
manager.register('appearance.hairColors', ['#FF69B4', '#00CED1', '#9400D3']);
|
|
718
|
+
manager.register('appearance.eyeColors', ['#FF0000', '#800080', '#C0C0C0']);
|
|
719
|
+
|
|
720
|
+
// Use only custom options (exclude defaults)
|
|
721
|
+
manager.setMode('appearance.bodyTypes', 'absolute');
|
|
722
|
+
manager.setMode('appearance.hairStyles', 'absolute');
|
|
723
|
+
manager.setMode('appearance.facialFeatures', 'absolute');
|
|
724
|
+
manager.setMode('appearance.skinTones', 'absolute');
|
|
725
|
+
manager.setMode('appearance.hairColors', 'absolute');
|
|
726
|
+
manager.setMode('appearance.eyeColors', 'absolute');
|
|
727
|
+
|
|
728
|
+
// Optional: Weight specific options
|
|
729
|
+
manager.setWeights('appearance.bodyTypes', {
|
|
730
|
+
'giant': 0.2, // Very rare
|
|
731
|
+
'diminutive': 0.3, // Rare
|
|
732
|
+
'athletic': 1.5 // Common
|
|
733
|
+
});
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
**All appearance properties:** `bodyTypes`, `hairStyles`, `facialFeatures`, `skinTones`, `hairColors`, `eyeColors`
|
|
737
|
+
|
|
738
|
+
### Batch Image Methods
|
|
739
|
+
|
|
740
|
+
Use batch methods to add icons and images to multiple items at once. All methods validate URLs before applying changes.
|
|
741
|
+
|
|
742
|
+
**Supported categories:** `spells`, `skills`, `classFeatures`, `racialTraits`, `equipment`, `races.data`, `classes.data`
|
|
743
|
+
|
|
744
|
+
**Valid URL prefixes:** `http://`, `https://`, `/`, `assets/`
|
|
745
|
+
|
|
746
|
+
```typescript
|
|
747
|
+
import { ExtensionManager } from 'playlist-data-engine';
|
|
748
|
+
|
|
749
|
+
const manager = ExtensionManager.getInstance();
|
|
750
|
+
|
|
751
|
+
// --- Add icons to specific items by name ---
|
|
752
|
+
manager.batchAddIcons('spells', {
|
|
753
|
+
'Fireball': '/assets/spells/fireball.png',
|
|
754
|
+
'Magic Missile': '/assets/spells/magic-missile.png'
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
manager.batchAddIcons('equipment', {
|
|
758
|
+
'Longsword': '/assets/equipment/longsword.png'
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
// --- Add images to specific items ---
|
|
762
|
+
manager.batchAddImages('spells', {
|
|
763
|
+
'Fireball': '/assets/spells/fireball-full.png'
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// --- Update by predicate (matches items and applies updates) ---
|
|
767
|
+
// Add same icon to all cantrips
|
|
768
|
+
manager.batchUpdateImages('spells',
|
|
769
|
+
spell => spell.level === 0,
|
|
770
|
+
{ icon: '/assets/spells/cantrip-icon.png' }
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
// Add images to all rare equipment
|
|
774
|
+
manager.batchUpdateImages('equipment',
|
|
775
|
+
item => item.rarity === 'rare',
|
|
776
|
+
{ icon: '/assets/icons/rare.png', image: '/assets/images/rare-bg.png' }
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
// --- Update by category property ---
|
|
780
|
+
// Add icons by spell school
|
|
781
|
+
manager.batchByCategory('spells', 'school', {
|
|
782
|
+
'Evocation': '/assets/icons/fire.png',
|
|
783
|
+
'Necromancy': '/assets/icons/skull.png',
|
|
784
|
+
'Abjuration': '/assets/icons/shield.png'
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// Add icons by equipment rarity
|
|
788
|
+
manager.batchByCategory('equipment', 'rarity', {
|
|
789
|
+
'legendary': '/assets/icons/star-gold.png',
|
|
790
|
+
'very_rare': '/assets/icons/star-purple.png',
|
|
791
|
+
'rare': '/assets/icons/star-blue.png'
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// Add both icon and image by rarity
|
|
795
|
+
manager.batchByCategory('equipment', 'rarity', {
|
|
796
|
+
'legendary': {
|
|
797
|
+
icon: '/assets/icons/legendary.png',
|
|
798
|
+
image: '/assets/images/legendary-bg.png'
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
**Error handling:**
|
|
804
|
+
```typescript
|
|
805
|
+
try {
|
|
806
|
+
manager.batchAddIcons('spells', { 'Fireball': 'ftp://invalid.com/icon.png' });
|
|
807
|
+
} catch (error) {
|
|
808
|
+
console.error('Invalid URL format:', error.message);
|
|
809
|
+
// URLs must start with http://, https://, /, or assets/
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
---
|
|
814
|
+
|
|
815
|
+
### Image Overrides (Patch System)
|
|
816
|
+
|
|
817
|
+
Batch image methods use a **patch-based storage system** that stores only icon/image changes, not complete item copies. This prevents duplicates when applying images to default items.
|
|
818
|
+
|
|
819
|
+
**How it works:**
|
|
820
|
+
1. `batchUpdateImages()` and `batchByCategory()` store **patches** (identifier → {icon, image}) in a separate `imageOverrides` map
|
|
821
|
+
2. When `get()` retrieves items, patches are **applied on top** of the item data
|
|
822
|
+
3. This means default items remain in the default pool but appear with your custom images
|
|
823
|
+
|
|
824
|
+
**Benefits:**
|
|
825
|
+
- No duplicate items - you always have the same number of items
|
|
826
|
+
- Images persist across sessions via localStorage integration
|
|
827
|
+
- Works correctly in all view modes (default, relative, replace, absolute)
|
|
828
|
+
|
|
829
|
+
**API Methods:**
|
|
830
|
+
|
|
831
|
+
| Method | Parameters | Return | Description |
|
|
832
|
+
|--------|------------|--------|-------------|
|
|
833
|
+
| `getImageOverrides()` | | `Map<Category, ImageOverride[]>` | Get all image overrides for all categories |
|
|
834
|
+
| `getImageOverridesForCategory(category)` | `category` | `ImageOverride[]` | Get overrides for a specific category |
|
|
835
|
+
| `restoreImageOverrides(category, overrides)` | `category`, `overrides` | `void` | Restore saved overrides (for persistence) |
|
|
836
|
+
| `clearImageOverrides(category)` | `category` | `void` | Clear all overrides for a category |
|
|
837
|
+
| `clearAllImageOverrides()` | | `void` | Clear all overrides for all categories |
|
|
838
|
+
|
|
839
|
+
**ImageOverride interface:**
|
|
840
|
+
```typescript
|
|
841
|
+
interface ImageOverride {
|
|
842
|
+
identifier: string; // Item id (for spells) or name (for others)
|
|
843
|
+
icon?: string; // Icon URL
|
|
844
|
+
image?: string; // Image URL
|
|
845
|
+
appliedAt: number; // Timestamp when applied
|
|
846
|
+
}
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
**Example - Persistence integration:**
|
|
850
|
+
```typescript
|
|
851
|
+
import { ExtensionManager, type ImageOverride } from 'playlist-data-engine';
|
|
852
|
+
|
|
853
|
+
const manager = ExtensionManager.getInstance();
|
|
854
|
+
|
|
855
|
+
// After batch applying images, save overrides to localStorage
|
|
856
|
+
const overrides = manager.getImageOverridesForCategory('spells');
|
|
857
|
+
localStorage.setItem('spell_image_overrides', JSON.stringify(overrides));
|
|
858
|
+
|
|
859
|
+
// On app startup, restore overrides
|
|
860
|
+
const savedOverrides = JSON.parse(localStorage.getItem('spell_image_overrides') || '[]');
|
|
861
|
+
manager.restoreImageOverrides('spells', savedOverrides);
|
|
862
|
+
|
|
863
|
+
// Clear overrides to reset to defaults
|
|
864
|
+
manager.clearImageOverrides('spells');
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
---
|
|
868
|
+
|
|
869
|
+
## Content Packs
|
|
870
|
+
|
|
871
|
+
Content packs are reusable collections of custom content for multiple categories that can be saved to files and loaded at runtime. For complete examples including basic packs, themed packs, expansion packs with custom features and skills, prerequisite-based content, and saving/loading functionality, see [Content Packs](CONTENT_PACKS.md).
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
## Best Practices
|
|
876
|
+
|
|
877
|
+
### 1. Use Descriptive Names
|
|
878
|
+
|
|
879
|
+
```typescript
|
|
880
|
+
// Good
|
|
881
|
+
{ name: 'Sword of the Dawn', type: 'weapon', rarity: 'rare', weight: 3 }
|
|
882
|
+
|
|
883
|
+
// Bad
|
|
884
|
+
{ name: 'sword1', type: 'weapon', rarity: 'rare', weight: 3 }
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### 2. Set Appropriate Spawn Rates
|
|
888
|
+
|
|
889
|
+
```typescript
|
|
890
|
+
// Good balance
|
|
891
|
+
manager.setWeights('equipment', {
|
|
892
|
+
'Common Sword': 1.0, // Default
|
|
893
|
+
'Rare Sword': 0.5, // Half as common
|
|
894
|
+
'Legendary Sword': 0.1 // Very rare
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// Bad - everything is legendary
|
|
898
|
+
manager.setWeights('equipment', {
|
|
899
|
+
'Legendary Sword': 1.0,
|
|
900
|
+
'Legendary Armor': 1.0
|
|
901
|
+
});
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
### 3. Use Themed Content Packs
|
|
905
|
+
|
|
906
|
+
```typescript
|
|
907
|
+
// Good - organized by theme
|
|
908
|
+
loadDarkFantasyPack();
|
|
909
|
+
loadHighFantasyPack();
|
|
910
|
+
loadSciFiPack();
|
|
911
|
+
|
|
912
|
+
// Bad - random mix of content
|
|
913
|
+
register('equipment', [...darkFantasyItems, ...highFantasyItems, ...sciFiItems]);
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
### 4. Reset When Needed
|
|
917
|
+
|
|
918
|
+
```typescript
|
|
919
|
+
// Reset before loading new content
|
|
920
|
+
const manager = ExtensionManager.getInstance();
|
|
921
|
+
manager.resetAll();
|
|
922
|
+
|
|
923
|
+
// Load fresh content
|
|
924
|
+
loadMyContentPack();
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
### 5. Handle Validation Errors
|
|
928
|
+
|
|
929
|
+
```typescript
|
|
930
|
+
try {
|
|
931
|
+
manager.register('equipment', customItems);
|
|
932
|
+
} catch (error) {
|
|
933
|
+
console.error('Failed to register equipment:', error.message);
|
|
934
|
+
// Handle error gracefully
|
|
935
|
+
}
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
### 6. Use Absolute Mode for Themed Content
|
|
939
|
+
|
|
940
|
+
```typescript
|
|
941
|
+
// Good - absolute mode for themed content
|
|
942
|
+
manager.register('equipment', darkFantasyItems, { mode: 'absolute' });
|
|
943
|
+
|
|
944
|
+
// Bad - relative mode for themed content (default items will also spawn)
|
|
945
|
+
manager.register('equipment', darkFantasyItems, { mode: 'relative' });
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
### 7. Document Your Content Packs
|
|
949
|
+
|
|
950
|
+
```typescript
|
|
951
|
+
/**
|
|
952
|
+
* Dark Fantasy Content Pack
|
|
953
|
+
*
|
|
954
|
+
* Adds dark fantasy themed equipment, spells, and appearance options.
|
|
955
|
+
*
|
|
956
|
+
* Equipment: Soul Reaper (legendary), Shadow Cloak (very rare), etc.
|
|
957
|
+
* Spells: Soul Drain, Shadow Step, Death Coil
|
|
958
|
+
* Appearance: Dark skin tones, undead features
|
|
959
|
+
*
|
|
960
|
+
* @author Your Name
|
|
961
|
+
* @version 1.0.0
|
|
962
|
+
*/
|
|
963
|
+
export function loadDarkFantasyPack() {
|
|
964
|
+
// ...
|
|
965
|
+
}
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
---
|
|
969
|
+
|
|
970
|
+
## Validation
|
|
971
|
+
|
|
972
|
+
The extensibility system includes automatic validation. Invalid content is rejected with clear error messages.
|
|
973
|
+
|
|
974
|
+
**For complete type definitions, see [Reference](#reference).**
|
|
975
|
+
|
|
976
|
+
### Key Validation Rules
|
|
977
|
+
|
|
978
|
+
| Category | Required Fields | Validation Rules |
|
|
979
|
+
|----------|-----------------|------------------|
|
|
980
|
+
| **Equipment** | `name`, `type`, `rarity`, `weight` | Type: `weapon`/`armor`/`item`; valid rarity; weight ≥ 0 |
|
|
981
|
+
| **Spells** | `name`, `level`, `school` | Level 0-9; valid school (see `SpellSchool` type) |
|
|
982
|
+
| **Races/Classes** | String values | Must be valid name (default or registered custom) |
|
|
983
|
+
| **Appearance** | String values | Must be strings (not objects) |
|
|
984
|
+
| **Features** | `id`, `name`, `description`, `type`, `class`, `level`, `source` | ID: `lowercase_with_underscores`; must be unique |
|
|
985
|
+
| **Skills** | `id`, `name`, `ability`, `source` | ID: `lowercase_with_underscores`; valid ability |
|
|
986
|
+
| **Skill Lists** | `class`, `skillCount`, `availableSkills` | skillCount ≥ 0; skill IDs must exist |
|
|
987
|
+
|
|
988
|
+
### ID Format
|
|
989
|
+
|
|
990
|
+
All custom content IDs must use `lowercase_with_underscores` format.
|
|
991
|
+
|
|
992
|
+
| Valid | Invalid |
|
|
993
|
+
|-------|---------|
|
|
994
|
+
| `frost_rage` | `FrostRage` (use lowercase) |
|
|
995
|
+
| `necromancer_raise_dead` | `frost-rage` (use underscores) |
|
|
996
|
+
| `dragon_smithing` | `frost.rage` (use underscores) |
|
|
997
|
+
|
|
998
|
+
### Notes
|
|
999
|
+
|
|
1000
|
+
- **Duplicate IDs:** Automatically detected. Use `getCustom(category)` to check existing IDs before registering.
|
|
1001
|
+
- **Disable validation:** Use `{ validate: false }` to bypass (advanced use only—may cause runtime errors).
|
|
1002
|
+
|
|
1003
|
+
### Image/Icon URL Validation
|
|
1004
|
+
|
|
1005
|
+
Use `ImageValidator` to validate `icon` and `image` URL fields for custom content. Valid URL prefixes: `http://`, `https://`, `/`, `assets/`
|
|
1006
|
+
|
|
1007
|
+
```typescript
|
|
1008
|
+
import { validateImageFields } from 'playlist-data-engine';
|
|
1009
|
+
|
|
1010
|
+
const errors = validateImageFields({ icon: '/assets/icon.png', image: 'https://example.com/image.png' });
|
|
1011
|
+
if (errors.length > 0) {
|
|
1012
|
+
console.error('Invalid image URLs:', errors);
|
|
1013
|
+
}
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
---
|
|
1017
|
+
|
|
1018
|
+
## Troubleshooting
|
|
1019
|
+
|
|
1020
|
+
| Problem | Solution |
|
|
1021
|
+
|---------|----------|
|
|
1022
|
+
| **Content not appearing** | 1. Register before `CharacterGenerator.generate()` 2. Check console for validation errors 3. Verify weights ≠ 0 4. Use correct category name (see [Supported Categories](#supported-categories)) |
|
|
1023
|
+
| **Validation errors** | Error messages include category, item index, and specific issue. Use `validate: false` to bypass (not recommended). |
|
|
1024
|
+
| **Content not persisting** | System is **runtime only**. Re-register on app startup. See [Export/Import System](#exportimport-system) for persistence patterns. |
|
|
1025
|
+
| **Spawn rates not working** | Check spawn mode. See [Spawn Modes](#extensionmanager-api). Use `getMode()` to verify current mode. |
|
|
1026
|
+
| **Duplicate ID errors** | Custom IDs must be unique. Use `getCustom(category)` to check existing IDs. |
|
|
1027
|
+
|
|
1028
|
+
### Debugging
|
|
1029
|
+
|
|
1030
|
+
```typescript
|
|
1031
|
+
const manager = ExtensionManager.getInstance();
|
|
1032
|
+
|
|
1033
|
+
// Check registered content
|
|
1034
|
+
manager.getInfo('equipment'); // { hasCustomData, customCount, totalCount, mode, ... }
|
|
1035
|
+
|
|
1036
|
+
// Verify weights
|
|
1037
|
+
manager.getWeights('equipment'); // Current weights
|
|
1038
|
+
|
|
1039
|
+
// Check current mode
|
|
1040
|
+
manager.getMode('equipment'); // 'relative' | 'absolute' | 'default' | undefined
|
|
1041
|
+
|
|
1042
|
+
// Validate without registering
|
|
1043
|
+
manager.validate('equipment', items); // { valid: boolean, errors: string[] }
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
---
|
|
1047
|
+
|
|
1048
|
+
## Reference
|
|
1049
|
+
|
|
1050
|
+
### Type Definitions
|
|
1051
|
+
|
|
1052
|
+
**Location:** [src/core/extensions/ExtensionManager.ts](../src/core/extensions/ExtensionManager.ts)
|
|
1053
|
+
|
|
1054
|
+
**Import types from the package:**
|
|
1055
|
+
|
|
1056
|
+
```typescript
|
|
1057
|
+
import type {
|
|
1058
|
+
ClassFeature,
|
|
1059
|
+
RacialTrait,
|
|
1060
|
+
CustomSkill,
|
|
1061
|
+
ExtensionOptions,
|
|
1062
|
+
ExtensionCategory,
|
|
1063
|
+
ImageOverride,
|
|
1064
|
+
SpellSchool
|
|
1065
|
+
} from 'playlist-data-engine';
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
| Type | Source | Description |
|
|
1069
|
+
|------|--------|-------------|
|
|
1070
|
+
| `ExtensionOptions` | [src/core/extensions/ExtensionManager.ts](../src/core/extensions/ExtensionManager.ts) | Registration options: mode, weights, validate |
|
|
1071
|
+
| `ClassFeature` | [src/core/features/FeatureTypes.ts](../src/core/features/FeatureTypes.ts) | Class features with prerequisites and effects |
|
|
1072
|
+
| `RacialTrait` | [src/core/features/FeatureTypes.ts](../src/core/features/FeatureTypes.ts) | Racial traits with prerequisites and effects |
|
|
1073
|
+
| `CustomSkill` | [src/core/skills/SkillTypes.ts](../src/core/skills/SkillTypes.ts) | Skills with prerequisites, categories, armor penalty |
|
|
1074
|
+
| `SkillListDefinition` | [src/core/skills/SkillTypes.ts](../src/core/skills/SkillTypes.ts) | Skill lists for class character generation |
|
|
1075
|
+
| `SpellSchool` | [src/core/spells/SpellTypes.ts](../src/core/spells/SpellTypes.ts) | D&D 5e schools of magic: Abjuration, Conjuration, Divination, Enchantment, Evocation, Illusion, Necromancy, Transmutation |
|
|
1076
|
+
| `ExtensionCategory` | [src/core/extensions/ExtensionManager.ts](../src/core/extensions/ExtensionManager.ts) | All extensible category names |
|
|
1077
|
+
| `ImageOverride` | [src/core/extensions/ExtensionManager.ts](../src/core/extensions/ExtensionManager.ts) | Image patch: identifier, icon, image, appliedAt |
|
|
1078
|
+
|
|
1079
|
+
### Character Generator Extensions
|
|
1080
|
+
|
|
1081
|
+
The `CharacterGenerator.generate()` `extensions` option supports a limited set of categories.
|
|
1082
|
+
|
|
1083
|
+
| Property | Type | Description |
|
|
1084
|
+
|----------|------|-------------|
|
|
1085
|
+
| `spells` | `SpellExtension[]` | Custom spells to add |
|
|
1086
|
+
| `equipment` | `EquipmentExtension[]` | Custom equipment to add |
|
|
1087
|
+
| `races` | `string[]` | Custom race names (uses Race enum) |
|
|
1088
|
+
| `classes` | `string[]` | Custom class names (uses Class enum) |
|
|
1089
|
+
| `appearance` | `AppearanceExtension` | Custom appearance options |
|
|
1090
|
+
|
|
1091
|
+
**Important:** For `classFeatures`, `racialTraits`, `skills`, and `skillLists`, use `ExtensionManager.register()` directly instead of the `extensions` option.
|
|
1092
|
+
|
|
1093
|
+
For the complete list of supported categories, see [Supported Categories](#supported-categories) in the Overview.
|
|
1094
|
+
|
|
1095
|
+
---
|
|
1096
|
+
|
|
1097
|
+
## Support
|
|
1098
|
+
|
|
1099
|
+
For more information, see:
|
|
1100
|
+
- [DATA_ENGINE_REFERENCE.md](../DATA_ENGINE_REFERENCE.md) - Complete API reference
|
|
1101
|
+
- [USAGE_IN_OTHER_PROJECTS.md](../USAGE_IN_OTHER_PROJECTS.md) - Usage examples
|
|
1102
|
+
- [CONTENT_PACKS.md](CONTENT_PACKS.md) - Content pack creation and examples
|
|
1103
|
+
- [EQUIPMENT_SYSTEM.md](EQUIPMENT_SYSTEM.md) - Equipment properties and enchanting
|
|
1104
|
+
- [XP_AND_STATS.md](XP_AND_STATS.md) - Progression and stat strategies
|
|
1105
|
+
- [PREREQUISITES.md](PREREQUISITES.md) - Feature and skill requirements
|
|
1106
|
+
- [tests/integration/customGeneration.integration.test.ts](../tests/integration/customGeneration.integration.test.ts) - Integration test examples
|