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,2279 @@
|
|
|
1
|
+
# Equipment System Documentation
|
|
2
|
+
|
|
3
|
+
Complete reference for the Playlist Data Engine's Advanced Equipment System.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Overview](#overview)
|
|
8
|
+
2. [Enhanced Equipment](#enhanced-equipment)
|
|
9
|
+
3. [Equipment Properties](#equipment-properties)
|
|
10
|
+
4. [Equipment Effects](#equipment-effects)
|
|
11
|
+
5. [Equipment Modification](#equipment-modification)
|
|
12
|
+
6. [Spawn Weights](#spawn-weights)
|
|
13
|
+
7. [Enchantment Library](#enchantment-library)
|
|
14
|
+
8. [Magic Item System](#magic-item-system)
|
|
15
|
+
9. [Custom Equipment](#custom-equipment)
|
|
16
|
+
10. [Box Equipment Type](#box-equipment-type)
|
|
17
|
+
11. [API Reference](#api-reference)
|
|
18
|
+
12. [Examples](#examples)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
Get up and running with the equipment system in 5 minutes.
|
|
25
|
+
|
|
26
|
+
### 1. Register Custom Equipment
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { ExtensionManager } from 'playlist-data-engine';
|
|
30
|
+
|
|
31
|
+
const flamingSword = {
|
|
32
|
+
name: 'Flaming Sword',
|
|
33
|
+
type: 'weapon',
|
|
34
|
+
rarity: 'rare',
|
|
35
|
+
weight: 3,
|
|
36
|
+
damage: { dice: '1d8', damageType: 'slashing' },
|
|
37
|
+
icon: '/icons/weapons/flaming-sword.png',
|
|
38
|
+
properties: [{
|
|
39
|
+
type: 'damage_bonus',
|
|
40
|
+
target: 'fire',
|
|
41
|
+
value: '1d6',
|
|
42
|
+
description: '+1d6 fire damage'
|
|
43
|
+
}]
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const manager = ExtensionManager.getInstance();
|
|
47
|
+
manager.register('equipment', [flamingSword]);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Spawn Equipment
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { EquipmentSpawnHelper, SeededRNG } from 'playlist-data-engine';
|
|
54
|
+
|
|
55
|
+
const rng = new SeededRNG('loot_seed');
|
|
56
|
+
|
|
57
|
+
// Spawn by name
|
|
58
|
+
const item = EquipmentSpawnHelper.spawnFromList(['Flaming Sword']);
|
|
59
|
+
|
|
60
|
+
// Spawn by rarity
|
|
61
|
+
const rareItems = EquipmentSpawnHelper.spawnByRarity('rare', 3, rng);
|
|
62
|
+
|
|
63
|
+
// Spawn random (respects spawn weights)
|
|
64
|
+
const loot = EquipmentSpawnHelper.spawnRandom(5, rng, { excludeZeroWeight: true });
|
|
65
|
+
|
|
66
|
+
// Add to character
|
|
67
|
+
character = EquipmentSpawnHelper.addToCharacter(character, loot, false);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 3. Apply Equipment Effects
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { EquipmentEffectApplier } from 'playlist-data-engine';
|
|
74
|
+
|
|
75
|
+
// Equip item and apply effects
|
|
76
|
+
const result = EquipmentEffectApplier.equipItem(character, equipment);
|
|
77
|
+
|
|
78
|
+
// Check if equip succeeded (e.g., stat requirements met)
|
|
79
|
+
if (!result.applied) {
|
|
80
|
+
// Item was not equipped — requirements not satisfied
|
|
81
|
+
console.log('Equip failed:', result.reason);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Unequip and remove effects
|
|
85
|
+
EquipmentEffectApplier.unequipItem(character, 'Flaming Sword');
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Next Steps**: See [Enhanced Equipment](#enhanced-equipment) for the main equipment interface, [Equipment Properties](#equipment-properties) for all property types, or [API Reference](#api-reference) for complete class documentation.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Overview
|
|
93
|
+
|
|
94
|
+
The Advanced Equipment System transforms the basic equipment database into a comprehensive item system supporting:
|
|
95
|
+
|
|
96
|
+
- **Equipment Properties**: Stat bonuses, skill proficiencies, ability unlocks, passive modifiers, special properties, damage bonuses, spell grants
|
|
97
|
+
- **Equipment-Granted Features**: Items can provide unique features or reference existing registry features
|
|
98
|
+
- **D&D 5e Standard Stats**: All existing equipment populated with default damage dice, AC, and properties
|
|
99
|
+
- **Custom Equipment Support**: ExtensionManager integration for custom equipment with full property support
|
|
100
|
+
- **Runtime Equipment Modification**: Template-based items (Flaming Sword) + per-instance enchanting/upgrading
|
|
101
|
+
- **Helper Functions**: Batch equipment spawning utilities
|
|
102
|
+
|
|
103
|
+
### Design Principles
|
|
104
|
+
|
|
105
|
+
- **Backward Compatible**: Existing characters and equipment continue to work
|
|
106
|
+
- **Weight-Based Spawning**: Features have spawn weights (0 = never random, still available to game logic)
|
|
107
|
+
- **Template + Instance**: Support both equipment templates AND per-item unique modifications
|
|
108
|
+
- **D&D 5e Aligned**: Default equipment uses standard 5e stats
|
|
109
|
+
- **Feature-Aligned**: Follows existing FeatureEffect
|
|
110
|
+
- **Data Structure Focus**: Provides structures, not full gameplay systems
|
|
111
|
+
|
|
112
|
+
### System Architecture
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
ExtensionManager
|
|
116
|
+
| equipment (default + custom items)
|
|
117
|
+
| equipment.templates (template definitions)
|
|
118
|
+
|
|
|
119
|
+
v
|
|
120
|
+
EquipmentValidator
|
|
121
|
+
| Validates all equipment data
|
|
122
|
+
|
|
|
123
|
+
v
|
|
124
|
+
EquipmentEffectApplier
|
|
125
|
+
| Applies/removes equipment effects
|
|
126
|
+
|
|
|
127
|
+
v
|
|
128
|
+
CharacterSheet
|
|
129
|
+
| equipment_effects[] (tracks active effects)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Enhanced Equipment
|
|
135
|
+
|
|
136
|
+
The `EnhancedEquipment` interface extends the base equipment with advanced capabilities.
|
|
137
|
+
|
|
138
|
+
### EnhancedEquipment Interface
|
|
139
|
+
|
|
140
|
+
**Location:** [src/core/types/Equipment.ts](../src/core/types/Equipment.ts)
|
|
141
|
+
|
|
142
|
+
Primary equipment type with advanced properties support. Extends base equipment with properties, granted features/skills/spells, and spawn weights.
|
|
143
|
+
|
|
144
|
+
| Property | Type | Description |
|
|
145
|
+
|----------|------|-------------|
|
|
146
|
+
| `properties` | `EquipmentProperty[]` | Advanced properties (stat bonuses, damage, etc.) |
|
|
147
|
+
| `grantsFeatures` | Array<string \| EquipmentMiniFeature> | Features granted when equipped |
|
|
148
|
+
| `grantsSkills` | Array<{skillId, level}> | Skills granted when equipped |
|
|
149
|
+
| `grantsSpells` | Array<{spellId, level?, uses?, recharge?}> | Spells granted when equipped |
|
|
150
|
+
| `spawnWeight` | number | Spawn weight (0 = game-only) |
|
|
151
|
+
| `source` | `'default' \| 'custom'` | Source tracking |
|
|
152
|
+
| `tags` | string[] | Search/filter tags |
|
|
153
|
+
| `icon` | string | Optional icon URL for small UI display |
|
|
154
|
+
| `image` | string | Optional image URL for larger display |
|
|
155
|
+
|
|
156
|
+
For an example of the `EnhancedEquipment` interface, see [Example 1: Comprehensive Custom Item](#example-1-comprehensive-custom-item).
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Equipment Properties
|
|
161
|
+
|
|
162
|
+
Equipment properties define how items affect gameplay. Each property has a type, target, value, optional condition, and optional description.
|
|
163
|
+
|
|
164
|
+
### EquipmentPropertyType
|
|
165
|
+
|
|
166
|
+
**Location:** [src/core/types/Equipment.ts](../src/core/types/Equipment.ts)
|
|
167
|
+
|
|
168
|
+
Equipment properties define how items affect gameplay. Each property has a type, target, value, optional condition, and optional description.
|
|
169
|
+
|
|
170
|
+
**For complete reference**, see [DATA_ENGINE_REFERENCE.md - EquipmentPropertyType](../DATA_ENGINE_REFERENCE.md#equipmentpropertytype).
|
|
171
|
+
|
|
172
|
+
| Type | Description |
|
|
173
|
+
|------|-------------|
|
|
174
|
+
| `stat_bonus` | Increases ability scores (STR, DEX, CON, INT, WIS, CHA) |
|
|
175
|
+
| `skill_proficiency` | Grants skill proficiency or expertise |
|
|
176
|
+
| `ability_unlock` | Unlocks special abilities (darkvision, flight, etc.) |
|
|
177
|
+
| `passive_modifier` | Modifies passive values. For AC, supports formula strings (e.g., `"11 + DEX"`, `"14 + min(DEX, 2)"`, `"16"`) which are evaluated by `EquipmentEffectApplier.evaluateACFormula()`. Numeric values are treated as additive bonuses (shields, rings, etc.) |
|
|
178
|
+
| `special_property` | Game-specific properties (finesse, versatile, etc.) |
|
|
179
|
+
| `damage_bonus` | Adds extra damage (fire, cold, lightning, etc.) |
|
|
180
|
+
| `stat_requirement` | Minimum stat required to use item |
|
|
181
|
+
|
|
182
|
+
### EquipmentCondition
|
|
183
|
+
|
|
184
|
+
**Location:** [src/core/types/Equipment.ts](../src/core/types/Equipment.ts)
|
|
185
|
+
|
|
186
|
+
**For complete reference**, see [DATA_ENGINE_REFERENCE.md - EquipmentCondition](../DATA_ENGINE_REFERENCE.md#equipmentcondition).
|
|
187
|
+
|
|
188
|
+
| Type | Value Format | Description |
|
|
189
|
+
|------|--------------|-------------|
|
|
190
|
+
| `vs_creature_type` | string | Property applies vs specific creature |
|
|
191
|
+
| `at_time_of_day` | day/night/dawn/dusk | Property applies at specific time |
|
|
192
|
+
| `wielder_race` | string | Property applies only to specific race |
|
|
193
|
+
| `wielder_class` | string | Property applies only to specific class |
|
|
194
|
+
| `while_equipped` | boolean | Always true when equipped (default) |
|
|
195
|
+
| `on_hit` | boolean | Triggers when weapon hits |
|
|
196
|
+
| `on_damage_taken` | boolean | Triggers when wearer takes damage |
|
|
197
|
+
| `custom` | value + description | Game-defined condition |
|
|
198
|
+
|
|
199
|
+
### EquipmentProperty
|
|
200
|
+
|
|
201
|
+
**Location:** [src/core/types/Equipment.ts](../src/core/types/Equipment.ts)
|
|
202
|
+
|
|
203
|
+
Defines how equipment affects gameplay.
|
|
204
|
+
|
|
205
|
+
| Property | Type | Description |
|
|
206
|
+
|----------|------|-------------|
|
|
207
|
+
| `type` | `EquipmentPropertyType` | Property type (stat_bonus, skill_proficiency, etc.) |
|
|
208
|
+
| `target` | string | What the property affects (ability, skill, etc.) |
|
|
209
|
+
| `value` | number \| string \| boolean | Effect value |
|
|
210
|
+
| `condition` | `EquipmentCondition` | Optional condition for when property applies |
|
|
211
|
+
| `stackable` | boolean | Whether effects stack (default: true) |
|
|
212
|
+
|
|
213
|
+
For property examples, see [Examples - Property Examples](#examples).
|
|
214
|
+
|
|
215
|
+
### EquipmentMiniFeature
|
|
216
|
+
|
|
217
|
+
**Location:** [src/core/types/Equipment.ts](../src/core/types/Equipment.ts)
|
|
218
|
+
|
|
219
|
+
Equipment-specific features defined inline. Used in `EnhancedEquipment.grantsFeatures`.
|
|
220
|
+
|
|
221
|
+
| Property | Type | Description |
|
|
222
|
+
|----------|------|-------------|
|
|
223
|
+
| `id` | string | Unique feature ID |
|
|
224
|
+
| `name` | string | Feature name |
|
|
225
|
+
| `description` | string | Feature description |
|
|
226
|
+
| `effects` | `EquipmentProperty[]` | What this feature does |
|
|
227
|
+
| `source` | `'equipment_inline'` | Marks as equipment-specific |
|
|
228
|
+
|
|
229
|
+
For inline feature examples, see [Examples - Inline Mini-Features](#examples).
|
|
230
|
+
|
|
231
|
+
### Equipment-Granted Features
|
|
232
|
+
|
|
233
|
+
Equipment can grant features in two ways:
|
|
234
|
+
- **Registry Feature References**: String references to features in the FeatureQuery. For examples, see [Examples - Registry Feature References](#examples).
|
|
235
|
+
- **Inline Mini-Features**: Equipment-specific features defined inline (see `EquipmentMiniFeature` table above).
|
|
236
|
+
|
|
237
|
+
### Equipment-Granted Skills
|
|
238
|
+
|
|
239
|
+
Equipment can grant skill proficiencies or expertise. The `grantsSkills` property on `EnhancedEquipment` accepts an array of `{skillId, level}` objects.
|
|
240
|
+
|
|
241
|
+
**Skill Proficiency Hierarchy:** When equipment grants a skill proficiency:
|
|
242
|
+
- `none` < `proficient` < `expertise`
|
|
243
|
+
- Equipment always upgrades to at least the granted level
|
|
244
|
+
- Expertise overrides any lower level
|
|
245
|
+
- Multiple sources are tracked separately
|
|
246
|
+
|
|
247
|
+
For skill granting examples, see [Example 1: Comprehensive Custom Item](#example-1-comprehensive-custom-item) - Cloak of the Elder demonstrates `grantsSkills` with proficiency and expertise levels.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Equipment Effects
|
|
252
|
+
|
|
253
|
+
Equipment effects are applied when items are equipped and removed when unequipped. The `EquipmentEffectApplier` class handles all effect management.
|
|
254
|
+
|
|
255
|
+
### Stacking Behavior
|
|
256
|
+
|
|
257
|
+
**All equipment effects stack by default.** Multiple items with the same effect will combine:
|
|
258
|
+
- Two +1 STR items = +2 STR total
|
|
259
|
+
- Two +1 AC items = +2 AC total
|
|
260
|
+
- Stackable can be set to false for non-stacking effects
|
|
261
|
+
|
|
262
|
+
**Armor AC Formulas:** When a `passive_modifier` with `target: 'ac'` has a string value, it is treated as a full AC formula rather than an additive bonus. The formula replaces the character's unarmored AC entirely. Supported patterns:
|
|
263
|
+
- `"16"` — Flat AC (heavy armor, no DEX contribution)
|
|
264
|
+
- `"11 + DEX"` — Base + full DEX modifier (light armor)
|
|
265
|
+
- `"14 + min(DEX, 2)"` — Base + capped DEX modifier (medium armor)
|
|
266
|
+
|
|
267
|
+
Numeric AC values (e.g., `value: 2`) are additive bonuses that stack on top of armor, such as from shields or magic items.
|
|
268
|
+
|
|
269
|
+
### Effect Application Flow
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
Equip Item
|
|
273
|
+
|
|
|
274
|
+
v
|
|
275
|
+
EquipmentEffectApplier.equipItem()
|
|
276
|
+
|
|
|
277
|
+
+--> Check stat requirements (e.g., STR 13 for plate armor)
|
|
278
|
+
| |
|
|
279
|
+
| +--> Failed? Return { applied: false } — item not applied
|
|
280
|
+
|
|
|
281
|
+
+--> Apply properties (stat bonuses, AC formulas, skills, etc.)
|
|
282
|
+
+--> Apply granted features
|
|
283
|
+
+--> Apply granted skills
|
|
284
|
+
+--> Apply granted spells
|
|
285
|
+
|
|
|
286
|
+
v
|
|
287
|
+
Store in character.equipment_effects[]
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Effect Removal Flow
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
Unequip Item
|
|
294
|
+
|
|
|
295
|
+
v
|
|
296
|
+
EquipmentEffectApplier.unequipItem()
|
|
297
|
+
|
|
|
298
|
+
+--> Remove properties (reverse stat changes, etc.)
|
|
299
|
+
+--> Remove granted features
|
|
300
|
+
+--> Remove granted skills
|
|
301
|
+
+--> Remove granted spells
|
|
302
|
+
|
|
|
303
|
+
v
|
|
304
|
+
Remove from character.equipment_effects[]
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Character Equipment Effects Structure
|
|
308
|
+
|
|
309
|
+
**Location:** [CharacterSheet.equipment_effects](../src/core/types/Character.ts)
|
|
310
|
+
|
|
311
|
+
Equipment effects are tracked separately on the character for proper removal when unequipping.
|
|
312
|
+
|
|
313
|
+
| Property | Type | Description |
|
|
314
|
+
|----------|------|-------------|
|
|
315
|
+
| source | string | Equipment name |
|
|
316
|
+
| instanceId | string | Per-instance tracking ID |
|
|
317
|
+
| effects | EquipmentProperty[] | Properties from this item |
|
|
318
|
+
| features | EquipmentFeature[] | Features granted by this item |
|
|
319
|
+
| skills | EquipmentSkill[] | Skills granted by this item |
|
|
320
|
+
| spells | Array<{spellId, level?, uses?, recharge?}> | Spells granted by this item |
|
|
321
|
+
|
|
322
|
+
For examples of equipment effects in use, see [Example 2: Enchanting Equipment](#example-2-enchanting-equipment) or [Example 8: Progressive Enchantment Through Gameplay](#example-8-progressive-enchantment-through-gameplay).
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Equipment Modification
|
|
327
|
+
|
|
328
|
+
**Location:** [src/core/equipment/EquipmentModifier.ts](../src/core/equipment/EquipmentModifier.ts)
|
|
329
|
+
|
|
330
|
+
The `EquipmentModifier` class handles runtime equipment modifications including enchanting, cursing, and upgrading.
|
|
331
|
+
|
|
332
|
+
### Modification Types
|
|
333
|
+
|
|
334
|
+
| Type | Description | Source |
|
|
335
|
+
|------|-------------|--------|
|
|
336
|
+
| Enchantment | Adds positive properties | `enchantment` |
|
|
337
|
+
| Curse | Adds negative properties | `curse` |
|
|
338
|
+
| Upgrade | Improves existing properties | `upgrade` |
|
|
339
|
+
| Template | Applies template definition | `template` |
|
|
340
|
+
|
|
341
|
+
### EquipmentModification Interface
|
|
342
|
+
|
|
343
|
+
**Location:** [src/core/types/Equipment.ts](../src/core/types/Equipment.ts)
|
|
344
|
+
|
|
345
|
+
Runtime modification for enchanting, cursing, or upgrading equipment.
|
|
346
|
+
|
|
347
|
+
| Property | Type | Description |
|
|
348
|
+
|----------|------|-------------|
|
|
349
|
+
| `id` | string | Unique modification ID |
|
|
350
|
+
| `name` | string | Display name |
|
|
351
|
+
| `properties` | `EquipmentProperty[]` | Properties added by modification |
|
|
352
|
+
| `addsFeatures` | Array<string \| EquipmentMiniFeature> | Features granted |
|
|
353
|
+
| `addsSkills` | Array<{skillId, level}> | Skills granted |
|
|
354
|
+
| `addsSpells` | Array<{spellId, level?, uses?, recharge?}> | Spells granted |
|
|
355
|
+
| `source` | string | Source type ('enchantment', 'curse', 'upgrade', 'template') |
|
|
356
|
+
|
|
357
|
+
**For complete method reference**, see [DATA_ENGINE_REFERENCE.md - EquipmentModifier](../DATA_ENGINE_REFERENCE.md#equipmentmodifier) or [API Reference - EquipmentModifier](#api-reference).
|
|
358
|
+
|
|
359
|
+
### Templates vs Instances
|
|
360
|
+
|
|
361
|
+
The system supports both template-based and per-instance modifications.
|
|
362
|
+
|
|
363
|
+
**Template-based modifications** are predefined in the ExtensionManager and can be applied to any equipment. Templates define a set of properties that get added to base equipment.
|
|
364
|
+
|
|
365
|
+
**Per-instance modifications** are unique enchantments, curses, or upgrades applied to specific item instances. Each item tracks its own modifications separately.
|
|
366
|
+
|
|
367
|
+
For usage examples, see:
|
|
368
|
+
- [Example 4: Template-Based Items](#example-4-template-based-items) - Template registration and application
|
|
369
|
+
- [Examples - Enchantment Library](#examples) - `enchant`, `curse`
|
|
370
|
+
- [Example 2: Enchanting Equipment](#example-2-enchanting-equipment) - `enchant`
|
|
371
|
+
- [Example 8: Progressive Enchantment](#example-8-progressive-enchantment-through-gameplay) - `enchant`, `removeModification`
|
|
372
|
+
- [Example 9: Removing Debuffs from Cursed Items](#example-9-removing-debuffs-from-cursed-items) - `disenchant`, `liftCurse`, `removeModification`
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Spawn Weights
|
|
377
|
+
|
|
378
|
+
Spawn weights control item generation in random loot.
|
|
379
|
+
|
|
380
|
+
### Weight System
|
|
381
|
+
|
|
382
|
+
- **Weight > 0**: Item can spawn randomly (higher = more common)
|
|
383
|
+
- **Weight = 0**: Item never spawns randomly, but can be used by game logic
|
|
384
|
+
- **Default weight**: 1.0 for most items
|
|
385
|
+
|
|
386
|
+
### Rarity Levels
|
|
387
|
+
|
|
388
|
+
Equipment rarity controls spawn frequency and typical power level.
|
|
389
|
+
|
|
390
|
+
| Rarity | Spawn Weight (Typical) | Description |
|
|
391
|
+
|--------|----------------------|-------------|
|
|
392
|
+
| common | 1.0 | Standard items, spawn frequently |
|
|
393
|
+
| uncommon | 0.5 | Slightly magical, spawn occasionally |
|
|
394
|
+
| rare | 0.2 | Powerful magic items, spawn rarely |
|
|
395
|
+
| very_rare | 0.1 | Very powerful, spawn very rarely |
|
|
396
|
+
| legendary | 0.0 | Unique artifacts, never spawn randomly (game-only) |
|
|
397
|
+
|
|
398
|
+
**Note:** The spawnWeight property on equipment can override these defaults. Items with `spawnWeight: 0` will never appear in random loot but can still be used by game logic.
|
|
399
|
+
|
|
400
|
+
For spawning examples, see [Examples - Spawn Weights](#examples).
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Enchantment Library
|
|
405
|
+
|
|
406
|
+
The Enchantment Library provides a comprehensive collection of predefined enchantments and curses that can be applied to equipment at runtime. All enchantments are `EquipmentModification` objects designed to work with `EquipmentModifier`.
|
|
407
|
+
|
|
408
|
+
**For complete API documentation with all enchantment tables**, see [DATA_ENGINE_REFERENCE.md - Enchantment Library](../DATA_ENGINE_REFERENCE.md#enchantment-library).
|
|
409
|
+
|
|
410
|
+
**Collections:** `WEAPON_ENCHANTMENTS`, `ARMOR_ENCHANTMENTS`, `RESISTANCE_ENCHANTMENTS`, `CURSES`, `ALL_ENCHANTMENTS`
|
|
411
|
+
|
|
412
|
+
**Stat Boost Functions:** `createStrengthEnchantment`, `createDexterityEnchantment`, `createConstitutionEnchantment`, `createIntelligenceEnchantment`, `createWisdomEnchantment`, `createCharismaEnchantment` (each takes `bonus: 1 | 2 | 3 | 4`)
|
|
413
|
+
|
|
414
|
+
**Query Functions:** `getEnchantment`, `getCurse`, `getAllEnchantments`, `getAllCurses`, `getEnchantmentsByType`
|
|
415
|
+
|
|
416
|
+
For usage examples, see [Examples - Enchantment Library](#examples).
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## Magic Item System
|
|
421
|
+
|
|
422
|
+
The Magic Item Examples library provides 38 pre-built magic items that demonstrate all capabilities of the Advanced Equipment System. These include weapons, armor, wondrous items, cursed items, conditional items, and template-based items. They serve as reference implementations and test fixtures.
|
|
423
|
+
|
|
424
|
+
**For complete API documentation with all item tables**, see [DATA_ENGINE_REFERENCE.md - Magic Items and Equipment Templates](../DATA_ENGINE_REFERENCE.md#magic-items-and-equipment-templates).
|
|
425
|
+
|
|
426
|
+
**Collections:** `MAGIC_ITEMS` (34 items: weapons, armor, stat bonuses, skills, movement, defense, vision, spells, curses, conditional, template-based), `ITEM_CREATION_TEMPLATES` (9 templates)
|
|
427
|
+
|
|
428
|
+
**Query Functions:** `getMagicItem`, `getMagicItemsByType`, `getMagicItemsByRarity`, `getCursedItems`, `getItemsWithProperty`, `applyTemplate`
|
|
429
|
+
|
|
430
|
+
For usage examples, see [Examples - Magic Item Examples](#examples).
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Custom Equipment
|
|
435
|
+
|
|
436
|
+
Custom equipment is registered through the ExtensionManager. For examples, see [Examples - Custom Equipment](#examples).
|
|
437
|
+
|
|
438
|
+
### Validation
|
|
439
|
+
|
|
440
|
+
All custom equipment is automatically validated using `EquipmentValidator.validateEquipment()`. See [API Reference - EquipmentValidator](#api-reference) for validation methods.
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Box Equipment Type
|
|
445
|
+
|
|
446
|
+
The `'box'` equipment type represents containers that hold other items. Boxes support both guaranteed containers (backpacks, adventure packs) and probability-based loot boxes. Boxes are stored in the character's `items[]` inventory and are not automatically opened.
|
|
447
|
+
|
|
448
|
+
### Design Principles
|
|
449
|
+
|
|
450
|
+
- **Unopened by Default**: Boxes are added to inventory unopened; your game code decides when to open them
|
|
451
|
+
- **Deterministic**: Uses `SeededRNG` so the same seed always produces the same items
|
|
452
|
+
- **Nested Support**: Opening a box that contains another box adds the inner box to inventory unopened
|
|
453
|
+
- **Graceful Failure**: Items referenced in pools but not found in the registry are silently skipped with a warning
|
|
454
|
+
|
|
455
|
+
### Box Interfaces
|
|
456
|
+
|
|
457
|
+
**Location:** [src/core/types/Equipment.ts](../src/core/types/Equipment.ts)
|
|
458
|
+
|
|
459
|
+
#### BoxDropPool
|
|
460
|
+
|
|
461
|
+
A single entry in a drop pool — either an item or a gold reward.
|
|
462
|
+
|
|
463
|
+
| Property | Type | Required | Description |
|
|
464
|
+
|----------|------|----------|-------------|
|
|
465
|
+
| `weight` | number | Yes | Probability weight (higher = more likely; pool weights should sum to 100) |
|
|
466
|
+
| `itemName` | string | No | Item name to spawn (must exist in equipment registry) |
|
|
467
|
+
| `quantity` | number | No | How many copies to add (default: 1); e.g., `quantity: 10` for 10 torches |
|
|
468
|
+
| `gold` | number | No | Gold to award instead of an item (mutually exclusive with `itemName`) |
|
|
469
|
+
|
|
470
|
+
#### BoxDrop
|
|
471
|
+
|
|
472
|
+
Represents one "draw" from a pool — one item (or gold) selected from the array.
|
|
473
|
+
|
|
474
|
+
| Property | Type | Description |
|
|
475
|
+
|----------|------|-------------|
|
|
476
|
+
| `pool` | `BoxDropPool[]` | Pool of possible outcomes; one entry is selected per drop |
|
|
477
|
+
|
|
478
|
+
#### BoxContents
|
|
479
|
+
|
|
480
|
+
The full configuration for what a box contains.
|
|
481
|
+
|
|
482
|
+
| Property | Type | Required | Description |
|
|
483
|
+
|----------|------|----------|-------------|
|
|
484
|
+
| `drops` | `BoxDrop[]` | Yes | Each entry represents one draw; the box generates one result per drop |
|
|
485
|
+
| `consumeOnOpen` | boolean | No | Whether the box is removed from inventory after opening (default: `true`) |
|
|
486
|
+
| `openRequirements` | `BoxOpenRequirement[]` | No | Optional requirements that must be satisfied to open the box |
|
|
487
|
+
|
|
488
|
+
#### BoxOpenRequirement
|
|
489
|
+
|
|
490
|
+
A single requirement that must be met to open a box. Represents an item (and quantity) that must be consumed from inventory.
|
|
491
|
+
|
|
492
|
+
| Property | Type | Required | Description |
|
|
493
|
+
|----------|------|----------|-------------|
|
|
494
|
+
| `itemName` | string | Yes | Item name that must be consumed (must exist in character inventory) |
|
|
495
|
+
| `quantity` | number | No | Quantity of item required (default: 1) |
|
|
496
|
+
|
|
497
|
+
#### BoxOpenError
|
|
498
|
+
|
|
499
|
+
Error returned when box cannot be opened due to unmet requirements.
|
|
500
|
+
|
|
501
|
+
| Property | Type | Description |
|
|
502
|
+
|----------|------|-------------|
|
|
503
|
+
| `code` | `'MISSING_ITEM' \| 'INSUFFICIENT_QUANTITY' \| 'NO_BOX_CONTENTS'` | Error code for programmatic handling |
|
|
504
|
+
| `message` | string | Human-readable error message |
|
|
505
|
+
| `requirement` | `BoxOpenRequirement` | The requirement that was not met (if applicable) |
|
|
506
|
+
|
|
507
|
+
#### BoxOpenResult
|
|
508
|
+
|
|
509
|
+
The result returned by `BoxOpener.openBox()`.
|
|
510
|
+
|
|
511
|
+
| Property | Type | Description |
|
|
512
|
+
|----------|------|-------------|
|
|
513
|
+
| `success` | boolean | Whether the box was successfully opened |
|
|
514
|
+
| `items` | `Equipment[]` | All items generated from the box (empty if not opened) |
|
|
515
|
+
| `gold` | number | Total gold generated from gold drops |
|
|
516
|
+
| `consumeBox` | boolean | Whether the box should be removed from inventory |
|
|
517
|
+
| `error` | `BoxOpenError` | Error if box could not be opened (optional) |
|
|
518
|
+
| `consumedItems` | `{ name: string; quantity: number }[]` | Items consumed to open the box (optional) |
|
|
519
|
+
|
|
520
|
+
### BoxOpener Class
|
|
521
|
+
|
|
522
|
+
**Location:** [src/core/equipment/BoxOpener.ts](../src/core/equipment/BoxOpener.ts)
|
|
523
|
+
|
|
524
|
+
Static utility class for opening box-type equipment.
|
|
525
|
+
|
|
526
|
+
| Method | Signature | Description |
|
|
527
|
+
|--------|-----------|-------------|
|
|
528
|
+
| `openBox` | `(box: Equipment, rng: SeededRNG, inventory?: EnhancedInventoryItem[]): BoxOpenResult` | Open a box and generate all its contents; checks requirements if inventory provided |
|
|
529
|
+
| `isBox` | `(equipment: Equipment): boolean` | Check if equipment is a valid (openable) box |
|
|
530
|
+
| `checkRequirements` | `(box: Equipment, inventory: EnhancedInventoryItem[]): BoxOpenError \| null` | Check if box requirements are met; returns null if satisfied |
|
|
531
|
+
| `canOpen` | `(box: Equipment, inventory: EnhancedInventoryItem[]): boolean` | Simple boolean check if box can be opened |
|
|
532
|
+
| `getRequirementsDescription` | `(box: Equipment): string \| null` | Human-readable description of requirements |
|
|
533
|
+
| `previewContents` | `(box: Equipment): { possibleItems, possibleGold, totalDrops, openRequirements? }` | List all possible outcomes without opening |
|
|
534
|
+
|
|
535
|
+
#### openBox
|
|
536
|
+
|
|
537
|
+
Iterates over every `BoxDrop` in `boxContents.drops`, selects one `BoxDropPool` entry using weighted random selection, and returns the aggregated result. If `inventory` is provided and the box has `openRequirements`, requirements are checked before opening.
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
import { BoxOpener, SeededRNG } from 'playlist-data-engine';
|
|
541
|
+
|
|
542
|
+
const rng = new SeededRNG('my-seed');
|
|
543
|
+
const result = BoxOpener.openBox(explorersPack, rng);
|
|
544
|
+
|
|
545
|
+
console.log(result.success); // boolean — whether the box was opened
|
|
546
|
+
console.log(result.items); // Equipment[] — all generated items
|
|
547
|
+
console.log(result.gold); // number — total gold from gold drops
|
|
548
|
+
console.log(result.consumeBox); // boolean — whether to remove from inventory
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
#### isBox
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
if (BoxOpener.isBox(item)) {
|
|
555
|
+
// item.type === 'box' && item.boxContents !== undefined
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
#### previewContents
|
|
560
|
+
|
|
561
|
+
Returns all *possible* outcomes without consuming RNG state. Useful for tooltips or shop previews. Also returns `openRequirements` if the box has any.
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
const preview = BoxOpener.previewContents(goblinChest);
|
|
565
|
+
// preview.possibleItems → ['Shortsword', 'Leather Armor', "Thieves' Tools"]
|
|
566
|
+
// preview.possibleGold → { min: 0, max: 50 }
|
|
567
|
+
// preview.totalDrops → 1
|
|
568
|
+
// preview.openRequirements → [{ itemName: 'Iron Key' }] (if present)
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
#### checkRequirements
|
|
572
|
+
|
|
573
|
+
Check if all box requirements are met by the given inventory. Returns `null` if all requirements are satisfied, or a `BoxOpenError` if not.
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
const inventory = [{ name: 'Iron Key', quantity: 1, equipped: false }];
|
|
577
|
+
const error = BoxOpener.checkRequirements(lockedChest, inventory);
|
|
578
|
+
|
|
579
|
+
if (error) {
|
|
580
|
+
console.log(error.code); // 'MISSING_ITEM' or 'INSUFFICIENT_QUANTITY'
|
|
581
|
+
console.log(error.message); // Human-readable error
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
#### canOpen
|
|
586
|
+
|
|
587
|
+
Simple boolean check useful for UI purposes (showing lock icons, enabling/disabling buttons).
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
const inventory = [{ name: 'Iron Key', quantity: 1, equipped: false }];
|
|
591
|
+
|
|
592
|
+
if (BoxOpener.canOpen(lockedChest, inventory)) {
|
|
593
|
+
console.log('You can open this chest!');
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
#### getRequirementsDescription
|
|
598
|
+
|
|
599
|
+
Returns a human-readable string describing what items are needed to open the box, useful for tooltips.
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
const desc = BoxOpener.getRequirementsDescription(lockedChest);
|
|
603
|
+
// "Requires: Iron Key"
|
|
604
|
+
|
|
605
|
+
const multiDesc = BoxOpener.getRequirementsDescription(royalTreasuryBox);
|
|
606
|
+
// "Requires: Golden Key, 200 Gold Coins"
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Guaranteed Containers
|
|
610
|
+
|
|
611
|
+
Guaranteed containers have pools with exactly one entry (weight 100). Every item listed always drops. Adventure packs like Explorer's Pack are guaranteed containers.
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
const explorersPack = {
|
|
615
|
+
name: "Explorer's Pack",
|
|
616
|
+
type: 'box',
|
|
617
|
+
rarity: 'common',
|
|
618
|
+
weight: 59,
|
|
619
|
+
icon: '/icons/containers/backpack.png',
|
|
620
|
+
boxContents: {
|
|
621
|
+
drops: [
|
|
622
|
+
{ pool: [{ weight: 100, itemName: 'Backpack' }] },
|
|
623
|
+
{ pool: [{ weight: 100, itemName: 'Bedroll' }] },
|
|
624
|
+
{ pool: [{ weight: 100, itemName: 'Mess Kit' }] },
|
|
625
|
+
{ pool: [{ weight: 100, itemName: 'Tinderbox' }] },
|
|
626
|
+
{ pool: [{ weight: 100, itemName: 'Waterskin' }] },
|
|
627
|
+
{ pool: [{ weight: 100, itemName: 'Rope' }] },
|
|
628
|
+
{ pool: [{ weight: 100, itemName: 'Torch', quantity: 10 }] },
|
|
629
|
+
{ pool: [{ weight: 100, itemName: 'Rations', quantity: 10 }] },
|
|
630
|
+
]
|
|
631
|
+
},
|
|
632
|
+
tags: ['gear', 'pack', 'general'],
|
|
633
|
+
description: 'A backpack containing wilderness exploration gear.'
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// Opens to: Backpack, Bedroll, Mess Kit, Tinderbox, Waterskin, Rope, 10×Torch, 10×Rations
|
|
637
|
+
// Total: 26 items
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Loot Boxes (Probability-Based)
|
|
641
|
+
|
|
642
|
+
Loot boxes have pools with multiple entries. One entry is chosen per drop based on weights.
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
const goblinChest = {
|
|
646
|
+
name: 'Goblin Treasure Chest',
|
|
647
|
+
type: 'box',
|
|
648
|
+
rarity: 'uncommon',
|
|
649
|
+
weight: 5,
|
|
650
|
+
icon: '/icons/containers/chest.png',
|
|
651
|
+
boxContents: {
|
|
652
|
+
drops: [{
|
|
653
|
+
pool: [
|
|
654
|
+
{ weight: 40, itemName: 'Shortsword' },
|
|
655
|
+
{ weight: 30, itemName: 'Leather Armor' },
|
|
656
|
+
{ weight: 20, itemName: "Thieves' Tools" },
|
|
657
|
+
{ weight: 10, gold: 50 }
|
|
658
|
+
]
|
|
659
|
+
}]
|
|
660
|
+
},
|
|
661
|
+
tags: ['loot', 'treasure', 'goblin'],
|
|
662
|
+
spawnWeight: 0.3,
|
|
663
|
+
description: 'A small chest containing goblin treasure.'
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// Opens to one of: Shortsword (40%), Leather Armor (30%), Thieves' Tools (20%), or 50 gold (10%)
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### Mixed Boxes (Guaranteed + Random)
|
|
670
|
+
|
|
671
|
+
Combine guaranteed drops with random pools in the same box.
|
|
672
|
+
|
|
673
|
+
```typescript
|
|
674
|
+
const dragonHoard = {
|
|
675
|
+
name: 'Dragon Hoard Chest',
|
|
676
|
+
type: 'box',
|
|
677
|
+
rarity: 'rare',
|
|
678
|
+
weight: 10,
|
|
679
|
+
icon: '/icons/containers/treasure-chest.png',
|
|
680
|
+
image: '/images/equipment/dragon-hoard.png',
|
|
681
|
+
boxContents: {
|
|
682
|
+
drops: [
|
|
683
|
+
{ pool: [{ weight: 100, gold: 500 }] }, // Always 500 gold
|
|
684
|
+
{ pool: [{ weight: 100, itemName: 'Potion of Healing' }] }, // Always a potion
|
|
685
|
+
{
|
|
686
|
+
pool: [ // One random weapon/armor
|
|
687
|
+
{ weight: 35, itemName: 'Longsword +1' },
|
|
688
|
+
{ weight: 35, itemName: 'Chain Mail +1' },
|
|
689
|
+
{ weight: 20, itemName: 'Ring of Protection' },
|
|
690
|
+
{ weight: 10, itemName: 'Dragon Slayer Sword' }
|
|
691
|
+
]
|
|
692
|
+
}
|
|
693
|
+
]
|
|
694
|
+
},
|
|
695
|
+
tags: ['loot', 'treasure', 'dragon', 'boss'],
|
|
696
|
+
spawnWeight: 0.05,
|
|
697
|
+
description: "A chest from a dragon's hoard."
|
|
698
|
+
};
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### Quantity Parameter
|
|
702
|
+
|
|
703
|
+
Use `quantity` on a pool entry to add multiple copies of the same item in one drop.
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
const archersBox = {
|
|
707
|
+
name: "Archer's Supply Box",
|
|
708
|
+
type: 'box',
|
|
709
|
+
rarity: 'common',
|
|
710
|
+
weight: 2,
|
|
711
|
+
boxContents: {
|
|
712
|
+
drops: [
|
|
713
|
+
{ pool: [{ weight: 100, itemName: 'Arrow', quantity: 20 }] }, // 20 arrows
|
|
714
|
+
{ pool: [{ weight: 100, itemName: 'Bowstring', quantity: 3 }] }, // 3 bowstrings
|
|
715
|
+
]
|
|
716
|
+
},
|
|
717
|
+
description: 'A box of archery supplies.'
|
|
718
|
+
};
|
|
719
|
+
// Opens to: 20×Arrow, 3×Bowstring (23 items total)
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### Nested Box Behavior
|
|
723
|
+
|
|
724
|
+
When a drop resolves to another box, that box is added to inventory **unopened**. The inner box is never recursively opened. This allows treasure caches that contain other containers.
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
const treasureCache = {
|
|
728
|
+
name: 'Treasure Cache',
|
|
729
|
+
type: 'box',
|
|
730
|
+
rarity: 'uncommon',
|
|
731
|
+
weight: 8,
|
|
732
|
+
icon: '/icons/containers/cache.png',
|
|
733
|
+
boxContents: {
|
|
734
|
+
drops: [
|
|
735
|
+
{ pool: [{ weight: 100, itemName: 'Goblin Treasure Chest' }] }, // adds chest unopened
|
|
736
|
+
{ pool: [{ weight: 100, gold: 100 }] }
|
|
737
|
+
]
|
|
738
|
+
},
|
|
739
|
+
description: 'A hidden cache containing a treasure chest and gold.'
|
|
740
|
+
};
|
|
741
|
+
// Opens to: Goblin Treasure Chest (unopened, in items[]) + 100 gold
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
### Opening Boxes on Characters
|
|
745
|
+
|
|
746
|
+
Use `EquipmentSpawnHelper.openBoxForCharacter()` to open a box from a character's inventory. It removes the box (if `consumeOnOpen` is true) and adds all contents automatically.
|
|
747
|
+
|
|
748
|
+
```typescript
|
|
749
|
+
import { EquipmentSpawnHelper, SeededRNG } from 'playlist-data-engine';
|
|
750
|
+
|
|
751
|
+
const rng = new SeededRNG('open-pack-seed');
|
|
752
|
+
const outcome = EquipmentSpawnHelper.openBoxForCharacter(
|
|
753
|
+
character,
|
|
754
|
+
"Explorer's Pack",
|
|
755
|
+
rng
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
if (outcome) {
|
|
759
|
+
character = outcome.character; // Updated character with contents added
|
|
760
|
+
console.log(outcome.result.items.length); // Number of items added
|
|
761
|
+
console.log(outcome.result.gold); // Gold awarded
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
Returns `null` if the box is not found in the character's inventory.
|
|
766
|
+
|
|
767
|
+
### Non-Consuming Boxes
|
|
768
|
+
|
|
769
|
+
Set `consumeOnOpen: false` to keep the box in inventory after opening. Useful for containers like a Component Pouch that players open repeatedly.
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
const componentPouch = {
|
|
773
|
+
name: 'Component Pouch',
|
|
774
|
+
type: 'box',
|
|
775
|
+
rarity: 'common',
|
|
776
|
+
weight: 2,
|
|
777
|
+
icon: '/icons/items/pouch.png',
|
|
778
|
+
boxContents: {
|
|
779
|
+
drops: [], // Empty — no automatic contents
|
|
780
|
+
consumeOnOpen: false // Stays in inventory
|
|
781
|
+
},
|
|
782
|
+
tags: ['gear', 'magic', 'spellcasting'],
|
|
783
|
+
description: 'A pouch for holding spell components.'
|
|
784
|
+
};
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### Opening Requirements
|
|
788
|
+
|
|
789
|
+
Boxes can have optional **opening requirements** that must be satisfied before they can be opened. Requirements are items that get consumed from the character's inventory when the box is successfully opened.
|
|
790
|
+
|
|
791
|
+
#### Basic Item Requirement
|
|
792
|
+
|
|
793
|
+
A locked chest requiring a single key:
|
|
794
|
+
|
|
795
|
+
```typescript
|
|
796
|
+
const lockedChest = {
|
|
797
|
+
name: 'Locked Chest',
|
|
798
|
+
type: 'box',
|
|
799
|
+
rarity: 'uncommon',
|
|
800
|
+
weight: 10,
|
|
801
|
+
icon: '/icons/containers/locked-chest.png',
|
|
802
|
+
boxContents: {
|
|
803
|
+
openRequirements: [
|
|
804
|
+
{ itemName: 'Iron Key' } // Consumes 1 Iron Key when opened
|
|
805
|
+
],
|
|
806
|
+
drops: [
|
|
807
|
+
{ pool: [{ weight: 100, gold: 50 }] },
|
|
808
|
+
{ pool: [
|
|
809
|
+
{ weight: 50, itemName: 'Shortsword' },
|
|
810
|
+
{ weight: 30, itemName: 'Leather Armor' },
|
|
811
|
+
{ weight: 20, itemName: 'Medical Supply', quantity: 3 }
|
|
812
|
+
]}
|
|
813
|
+
]
|
|
814
|
+
},
|
|
815
|
+
description: 'A sturdy locked chest. Requires an Iron Key to open.'
|
|
816
|
+
};
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
#### Gold Coin Requirement
|
|
820
|
+
|
|
821
|
+
Gold requirements use `"Gold Coin"` as the item name with a quantity:
|
|
822
|
+
|
|
823
|
+
```typescript
|
|
824
|
+
const gildedStrongbox = {
|
|
825
|
+
name: 'Gilded Strongbox',
|
|
826
|
+
type: 'box',
|
|
827
|
+
rarity: 'rare',
|
|
828
|
+
weight: 15,
|
|
829
|
+
boxContents: {
|
|
830
|
+
openRequirements: [
|
|
831
|
+
{ itemName: 'Gold Coin', quantity: 100 } // Consumes 100 Gold Coins
|
|
832
|
+
],
|
|
833
|
+
drops: [
|
|
834
|
+
{ pool: [{ weight: 100, gold: 250 }] },
|
|
835
|
+
{ pool: [
|
|
836
|
+
{ weight: 40, itemName: 'Longsword' },
|
|
837
|
+
{ weight: 30, itemName: 'Chain Mail' },
|
|
838
|
+
{ weight: 20, itemName: 'Scale Mail' }
|
|
839
|
+
]}
|
|
840
|
+
]
|
|
841
|
+
},
|
|
842
|
+
description: 'A gilded strongbox with a magical lock. Consumes 100 Gold Coins to unlock.'
|
|
843
|
+
};
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
#### Quantity-Based Requirement
|
|
847
|
+
|
|
848
|
+
Some boxes require multiple of the same item:
|
|
849
|
+
|
|
850
|
+
```typescript
|
|
851
|
+
const thievesCache = {
|
|
852
|
+
name: "Thieves' Cache",
|
|
853
|
+
type: 'box',
|
|
854
|
+
rarity: 'uncommon',
|
|
855
|
+
weight: 5,
|
|
856
|
+
boxContents: {
|
|
857
|
+
openRequirements: [
|
|
858
|
+
{ itemName: 'Lockpick', quantity: 3 } // Consumes 3 Lockpicks
|
|
859
|
+
],
|
|
860
|
+
drops: [
|
|
861
|
+
{ pool: [{ weight: 100, gold: 75 }] },
|
|
862
|
+
{ pool: [
|
|
863
|
+
{ weight: 50, itemName: "Thieves' Tools" },
|
|
864
|
+
{ weight: 30, itemName: 'Dagger' },
|
|
865
|
+
{ weight: 20, itemName: 'Disguise Kit' }
|
|
866
|
+
]}
|
|
867
|
+
]
|
|
868
|
+
},
|
|
869
|
+
description: 'A hidden cache with a complex lock. Requires 3 lockpicks to crack.'
|
|
870
|
+
};
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
#### Multiple Requirements
|
|
874
|
+
|
|
875
|
+
Boxes can require multiple different items simultaneously. ALL requirements must be satisfied:
|
|
876
|
+
|
|
877
|
+
```typescript
|
|
878
|
+
const royalTreasuryBox = {
|
|
879
|
+
name: 'Royal Treasury Box',
|
|
880
|
+
type: 'box',
|
|
881
|
+
rarity: 'very_rare',
|
|
882
|
+
weight: 20,
|
|
883
|
+
boxContents: {
|
|
884
|
+
openRequirements: [
|
|
885
|
+
{ itemName: 'Golden Key' }, // Need 1 Golden Key
|
|
886
|
+
{ itemName: 'Gold Coin', quantity: 200 } // AND 200 Gold Coins
|
|
887
|
+
],
|
|
888
|
+
drops: [
|
|
889
|
+
{ pool: [{ weight: 100, gold: 1000 }] },
|
|
890
|
+
{ pool: [
|
|
891
|
+
{ weight: 30, itemName: 'Plate Armor' },
|
|
892
|
+
{ weight: 25, itemName: 'Chain Mail' },
|
|
893
|
+
{ weight: 25, itemName: 'Greataxe' },
|
|
894
|
+
{ weight: 20, itemName: 'Longsword' }
|
|
895
|
+
]}
|
|
896
|
+
]
|
|
897
|
+
},
|
|
898
|
+
description: 'A royal treasury box sealed with powerful magic. Requires a Golden Key and 200 Gold Coins.'
|
|
899
|
+
};
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
#### Checking Requirements Before Opening
|
|
903
|
+
|
|
904
|
+
Use `BoxOpener.checkRequirements()` to validate if a character can open a box:
|
|
905
|
+
|
|
906
|
+
```typescript
|
|
907
|
+
import { BoxOpener, SeededRNG } from 'playlist-data-engine';
|
|
908
|
+
|
|
909
|
+
const inventory = [
|
|
910
|
+
{ name: 'Iron Key', quantity: 1, equipped: false },
|
|
911
|
+
{ name: 'Gold Coin', quantity: 150, equipped: false }
|
|
912
|
+
];
|
|
913
|
+
|
|
914
|
+
// Check if requirements are met
|
|
915
|
+
const error = BoxOpener.checkRequirements(lockedChest, inventory);
|
|
916
|
+
|
|
917
|
+
if (error) {
|
|
918
|
+
console.log(`Cannot open: ${error.message}`);
|
|
919
|
+
// error.code → 'MISSING_ITEM' or 'INSUFFICIENT_QUANTITY'
|
|
920
|
+
// error.requirement → The specific unmet requirement
|
|
921
|
+
}
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
#### Opening with Requirements
|
|
925
|
+
|
|
926
|
+
When opening a box with `BoxOpener.openBox()`, provide the inventory to enable requirement checking:
|
|
927
|
+
|
|
928
|
+
```typescript
|
|
929
|
+
const rng = new SeededRNG('loot-seed');
|
|
930
|
+
|
|
931
|
+
// With inventory - requirements are checked
|
|
932
|
+
const result = BoxOpener.openBox(lockedChest, rng, inventory);
|
|
933
|
+
|
|
934
|
+
if (result.success) {
|
|
935
|
+
console.log('Box opened!');
|
|
936
|
+
console.log('Items received:', result.items);
|
|
937
|
+
console.log('Items consumed:', result.consumedItems);
|
|
938
|
+
// consumedItems → [{ name: 'Iron Key', quantity: 1 }]
|
|
939
|
+
} else {
|
|
940
|
+
console.log('Failed to open:', result.error?.message);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Without inventory - requirements are skipped (backward compatible)
|
|
944
|
+
const legacyResult = BoxOpener.openBox(lockedChest, rng);
|
|
945
|
+
// Always succeeds regardless of requirements
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
#### Character Integration with EquipmentSpawnHelper
|
|
949
|
+
|
|
950
|
+
When using `EquipmentSpawnHelper.openBoxForCharacter()`, requirements are automatically checked and items consumed from the character's inventory:
|
|
951
|
+
|
|
952
|
+
```typescript
|
|
953
|
+
import { EquipmentSpawnHelper, SeededRNG } from 'playlist-data-engine';
|
|
954
|
+
|
|
955
|
+
const rng = new SeededRNG('character-loot');
|
|
956
|
+
const outcome = EquipmentSpawnHelper.openBoxForCharacter(character, 'Locked Chest', rng);
|
|
957
|
+
|
|
958
|
+
if (outcome) {
|
|
959
|
+
character = outcome.character; // Updated with consumed requirements and new items
|
|
960
|
+
console.log('Items:', outcome.result.items);
|
|
961
|
+
console.log('Gold:', outcome.result.gold);
|
|
962
|
+
console.log('Consumed:', outcome.result.consumedItems);
|
|
963
|
+
} else {
|
|
964
|
+
console.log('Box not found in inventory');
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// If requirements not met, outcome.result.success will be false
|
|
968
|
+
// and outcome.result.error will contain the failure reason
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
#### Error Handling
|
|
972
|
+
|
|
973
|
+
When a box cannot be opened, the result includes an error object:
|
|
974
|
+
|
|
975
|
+
```typescript
|
|
976
|
+
interface BoxOpenError {
|
|
977
|
+
code: 'MISSING_ITEM' | 'INSUFFICIENT_QUANTITY' | 'NO_BOX_CONTENTS';
|
|
978
|
+
message: string;
|
|
979
|
+
requirement?: BoxOpenRequirement;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Example error handling
|
|
983
|
+
const result = BoxOpener.openBox(box, rng, inventory);
|
|
984
|
+
|
|
985
|
+
if (!result.success && result.error) {
|
|
986
|
+
switch (result.error.code) {
|
|
987
|
+
case 'MISSING_ITEM':
|
|
988
|
+
console.log(`You don't have any ${result.error.requirement?.itemName}`);
|
|
989
|
+
break;
|
|
990
|
+
case 'INSUFFICIENT_QUANTITY':
|
|
991
|
+
console.log(`Not enough ${result.error.requirement?.itemName}`);
|
|
992
|
+
break;
|
|
993
|
+
case 'NO_BOX_CONTENTS':
|
|
994
|
+
console.log('This box is empty');
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
#### UI Helper Methods
|
|
1001
|
+
|
|
1002
|
+
BoxOpener provides convenience methods for UI integration:
|
|
1003
|
+
|
|
1004
|
+
```typescript
|
|
1005
|
+
// Boolean check for enabling/disabling open button
|
|
1006
|
+
const canOpen = BoxOpener.canOpen(box, character.equipment.items);
|
|
1007
|
+
|
|
1008
|
+
// Human-readable requirement description for tooltips
|
|
1009
|
+
const desc = BoxOpener.getRequirementsDescription(box);
|
|
1010
|
+
// "Requires: Iron Key"
|
|
1011
|
+
// "Requires: Golden Key, 200 Gold Coins"
|
|
1012
|
+
// null (if no requirements)
|
|
1013
|
+
|
|
1014
|
+
// Preview contents with requirements included
|
|
1015
|
+
const preview = BoxOpener.previewContents(box);
|
|
1016
|
+
// preview.openRequirements → [{ itemName: 'Iron Key' }] or undefined
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
### Registering Custom Box Items
|
|
1020
|
+
|
|
1021
|
+
Register box items through `ExtensionManager` like any other equipment. Boxes with `spawnWeight > 0` can appear in random loot.
|
|
1022
|
+
|
|
1023
|
+
```typescript
|
|
1024
|
+
import { ExtensionManager } from 'playlist-data-engine';
|
|
1025
|
+
|
|
1026
|
+
const manager = ExtensionManager.getInstance();
|
|
1027
|
+
manager.register('equipment', [goblinChest, dragonHoard, treasureCache]);
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
---
|
|
1031
|
+
|
|
1032
|
+
## API Reference
|
|
1033
|
+
|
|
1034
|
+
**For complete API documentation with all method signatures**, see [DATA_ENGINE_REFERENCE.md - Equipment System](../DATA_ENGINE_REFERENCE.md#equipment-system).
|
|
1035
|
+
|
|
1036
|
+
### Quick Class Reference
|
|
1037
|
+
|
|
1038
|
+
| Class | Location | Description |
|
|
1039
|
+
|-------|----------|-------------|
|
|
1040
|
+
| `EquipmentEffectApplier` | [src/core/equipment/EquipmentEffectApplier.ts](../src/core/equipment/EquipmentEffectApplier.ts) | Apply/remove equipment effects when equipping/unequipping |
|
|
1041
|
+
| `EquipmentValidator` | [src/core/equipment/EquipmentValidator.ts](../src/core/equipment/EquipmentValidator.ts) | Validate equipment data structures |
|
|
1042
|
+
| `EquipmentModifier` | [src/core/equipment/EquipmentModifier.ts](../src/core/equipment/EquipmentModifier.ts) | Enchant, curse, upgrade, and modify equipment |
|
|
1043
|
+
| `EquipmentSpawnHelper` | [src/core/equipment/EquipmentSpawnHelper.ts](../src/core/equipment/EquipmentSpawnHelper.ts) | Batch spawn equipment by rarity, tags, or templates |
|
|
1044
|
+
| `EquipmentGenerator` | [src/core/generation/EquipmentGenerator.ts](../src/core/generation/EquipmentGenerator.ts) | Manage inventory and starting gear |
|
|
1045
|
+
| `BoxOpener` | [src/core/equipment/BoxOpener.ts](../src/core/equipment/BoxOpener.ts) | Open box-type items and generate their contents |
|
|
1046
|
+
|
|
1047
|
+
### EquipmentEffectApplier
|
|
1048
|
+
|
|
1049
|
+
**Location:** [src/core/equipment/EquipmentEffectApplier.ts](../src/core/equipment/EquipmentEffectApplier.ts)
|
|
1050
|
+
|
|
1051
|
+
**For complete method reference**, see [DATA_ENGINE_REFERENCE.md - EquipmentEffectApplier](../DATA_ENGINE_REFERENCE.md#equipmenteffectapplier).
|
|
1052
|
+
|
|
1053
|
+
**Key Methods:**
|
|
1054
|
+
- `equipItem(character, equipment, instanceId?)` - Apply all effects from equipping an item. Returns `EffectApplicationResult` with `applied: boolean` (false if stat requirements not met)
|
|
1055
|
+
- `evaluateACFormula(formula, dexMod)` - Evaluate an armor AC formula string (e.g., `"11 + DEX"`, `"14 + min(DEX, 2)"`, `"16"`)
|
|
1056
|
+
- `unequipItem(character, equipmentName, instanceId?)` - Remove all effects from unequipping an item
|
|
1057
|
+
- `reapplyEquipmentEffects(character)` - Re-apply all equipment effects (for updates/level-ups)
|
|
1058
|
+
- `getActiveEffects(character)` - Get all active equipment effects
|
|
1059
|
+
|
|
1060
|
+
### EquipmentValidator
|
|
1061
|
+
|
|
1062
|
+
**Location:** [src/core/equipment/EquipmentValidator.ts](../src/core/equipment/EquipmentValidator.ts)
|
|
1063
|
+
|
|
1064
|
+
**For complete method reference**, see [DATA_ENGINE_REFERENCE.md - EquipmentValidator](../DATA_ENGINE_REFERENCE.md#equipmentvalidator).
|
|
1065
|
+
|
|
1066
|
+
**Key Methods:**
|
|
1067
|
+
- `validateEquipment(equipment)` - Validate complete equipment object
|
|
1068
|
+
- `validateProperty(property)` - Validate single equipment property
|
|
1069
|
+
- `validateCondition(condition)` - Validate equipment condition
|
|
1070
|
+
- `validateModification(modification)` - Validate equipment modification
|
|
1071
|
+
- `validateEquipmentFeatureReference(featureId)` - Check if feature ID exists in FeatureQuery
|
|
1072
|
+
- `validateEquipmentSkillReference(skillId)` - Check if skill ID exists in SkillQuery
|
|
1073
|
+
|
|
1074
|
+
### EquipmentModifier
|
|
1075
|
+
|
|
1076
|
+
**Location:** [src/core/equipment/EquipmentModifier.ts](../src/core/equipment/EquipmentModifier.ts)
|
|
1077
|
+
|
|
1078
|
+
**For complete method reference**, see [DATA_ENGINE_REFERENCE.md - EquipmentModifier](../DATA_ENGINE_REFERENCE.md#equipmentmodifier).
|
|
1079
|
+
|
|
1080
|
+
**Modification Operations:**
|
|
1081
|
+
- `enchant(equipment, itemName, enchantment, character?)` - Enchant equipment with new properties
|
|
1082
|
+
- `curse(equipment, itemName, curse, character?)` - Curse equipment with negative effects
|
|
1083
|
+
- `upgrade(equipment, itemName, upgrade, character?)` - Upgrade equipment (improve properties)
|
|
1084
|
+
- `applyTemplate(equipment, itemName, templateId, character?)` - Apply a template modification
|
|
1085
|
+
- `removeModification(equipment, itemName, modificationId, character?)` - Remove a specific modification
|
|
1086
|
+
- `disenchant(equipment, itemName, character?)` - Remove enchantments, keep curses
|
|
1087
|
+
- `liftCurse(equipment, itemName, character?)` - Remove curses, keep enchantments
|
|
1088
|
+
|
|
1089
|
+
**Query Methods:**
|
|
1090
|
+
- `getCombinedEffects(equipment, itemName, instanceId?)` - Get all active effects (base + mods)
|
|
1091
|
+
- `isEnchanted(equipment, itemName)` - Check if item has any enchantments
|
|
1092
|
+
- `isCursed(equipment, itemName)` - Check if item has any curses
|
|
1093
|
+
- `getItemSummary(equipment, itemName)` - Get comprehensive item summary
|
|
1094
|
+
|
|
1095
|
+
### EquipmentSpawnHelper
|
|
1096
|
+
|
|
1097
|
+
**Location:** [src/core/equipment/EquipmentSpawnHelper.ts](../src/core/equipment/EquipmentSpawnHelper.ts)
|
|
1098
|
+
|
|
1099
|
+
**For complete method reference**, see [DATA_ENGINE_REFERENCE.md - EquipmentSpawnHelper](../DATA_ENGINE_REFERENCE.md#equipmentspawnhelper).
|
|
1100
|
+
|
|
1101
|
+
**Key Methods:**
|
|
1102
|
+
- `spawnFromList(itemNames, rng?)` - Spawn items from list of names
|
|
1103
|
+
- `spawnByRarity(rarity, count, rng?)` - Spawn items by rarity level
|
|
1104
|
+
- `spawnByTags(tags, count, rng?, options?)` - Spawn items matching tags
|
|
1105
|
+
- `spawnRandom(count, rng, options?)` - Spawn random equipment (respects weights)
|
|
1106
|
+
- `spawnTreasureHoard(cr, rng)` - Spawn treasure hoard by CR
|
|
1107
|
+
- `addToCharacter(character, items, equip?)` - Add spawned items to character
|
|
1108
|
+
- `openBoxForCharacter(character, boxName, rng)` - Open a box, remove from inventory, add contents
|
|
1109
|
+
|
|
1110
|
+
### BoxOpener
|
|
1111
|
+
|
|
1112
|
+
**Location:** [src/core/equipment/BoxOpener.ts](../src/core/equipment/BoxOpener.ts)
|
|
1113
|
+
|
|
1114
|
+
Static class for opening box-type equipment. See [Box Equipment Type](#box-equipment-type) for full documentation.
|
|
1115
|
+
|
|
1116
|
+
**Key Methods:**
|
|
1117
|
+
- `openBox(box, rng)` - Open a box and return `BoxOpenResult` (items + gold + consumeBox flag)
|
|
1118
|
+
- `isBox(equipment)` - Return `true` if `equipment.type === 'box'` and `boxContents` is set
|
|
1119
|
+
- `previewContents(box)` - Return all possible items and gold range without consuming RNG state
|
|
1120
|
+
|
|
1121
|
+
### FeatureQuery (Equipment-Related)
|
|
1122
|
+
|
|
1123
|
+
**Location:** [src/core/features/FeatureQuery.ts](../src/core/features/FeatureQuery.ts)
|
|
1124
|
+
|
|
1125
|
+
**For complete method reference**, see [DATA_ENGINE_REFERENCE.md - FeatureQuery](../DATA_ENGINE_REFERENCE.md#featurequery).
|
|
1126
|
+
|
|
1127
|
+
**Key Methods:**
|
|
1128
|
+
- `getEquipmentFeatures(equipmentName)` - Get features grantable by equipment
|
|
1129
|
+
- `isValidEquipmentFeature(featureId)` - Check if feature ID exists (spawnWeight: 0 is valid)
|
|
1130
|
+
- `registerEquipmentFeature(feature)` - Register feature for equipment use (adds 'equipment' tag)
|
|
1131
|
+
|
|
1132
|
+
---
|
|
1133
|
+
|
|
1134
|
+
## Examples
|
|
1135
|
+
|
|
1136
|
+
### Property Examples
|
|
1137
|
+
|
|
1138
|
+
```typescript
|
|
1139
|
+
// Stat Bonus
|
|
1140
|
+
{
|
|
1141
|
+
type: 'stat_bonus',
|
|
1142
|
+
target: 'STR',
|
|
1143
|
+
value: 2,
|
|
1144
|
+
description: '+2 Strength'
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Skill Proficiency
|
|
1148
|
+
{
|
|
1149
|
+
type: 'skill_proficiency',
|
|
1150
|
+
target: 'stealth',
|
|
1151
|
+
value: 'expertise',
|
|
1152
|
+
description: 'Stealth expertise'
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Conditional Damage
|
|
1156
|
+
{
|
|
1157
|
+
type: 'damage_bonus',
|
|
1158
|
+
target: 'fire',
|
|
1159
|
+
value: '2d6',
|
|
1160
|
+
description: '+2d6 fire damage',
|
|
1161
|
+
condition: { type: 'vs_creature_type', value: 'troll' }
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Passive Modifier (additive — shield, ring of protection, etc.)
|
|
1165
|
+
{
|
|
1166
|
+
type: 'passive_modifier',
|
|
1167
|
+
target: 'ac',
|
|
1168
|
+
value: 2,
|
|
1169
|
+
description: '+2 AC',
|
|
1170
|
+
stackable: true
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Passive Modifier (armor AC formula — replaces unarmored AC)
|
|
1174
|
+
// Light armor: full DEX
|
|
1175
|
+
{ type: 'passive_modifier', target: 'ac', value: '11 + DEX', description: 'Base AC: 11 + DEX' }
|
|
1176
|
+
// Medium armor: capped DEX
|
|
1177
|
+
{ type: 'passive_modifier', target: 'ac', value: '14 + min(DEX, 2)', description: 'Base AC: 14 + DEX (max 2)' }
|
|
1178
|
+
// Heavy armor: no DEX
|
|
1179
|
+
{ type: 'passive_modifier', target: 'ac', value: '16', description: 'Fixed AC: 16' }
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
### Registry Feature References
|
|
1183
|
+
|
|
1184
|
+
String references to features in the FeatureQuery:
|
|
1185
|
+
|
|
1186
|
+
```typescript
|
|
1187
|
+
{
|
|
1188
|
+
name: 'Ring of Free Action',
|
|
1189
|
+
type: 'item',
|
|
1190
|
+
rarity: 'rare',
|
|
1191
|
+
weight: 0.1,
|
|
1192
|
+
grantsFeatures: ['freedom_of_movement'],
|
|
1193
|
+
spawnWeight: 0.2,
|
|
1194
|
+
source: 'custom',
|
|
1195
|
+
tags: ['magic', 'ring', 'movement']
|
|
1196
|
+
}
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
### Inline Mini-Features
|
|
1200
|
+
|
|
1201
|
+
Equipment-specific features defined inline:
|
|
1202
|
+
|
|
1203
|
+
```typescript
|
|
1204
|
+
{
|
|
1205
|
+
name: 'Boots of Speed',
|
|
1206
|
+
type: 'item',
|
|
1207
|
+
rarity: 'rare',
|
|
1208
|
+
weight: 1,
|
|
1209
|
+
grantsFeatures: [
|
|
1210
|
+
{
|
|
1211
|
+
id: 'boots_of_speed_haste',
|
|
1212
|
+
name: 'Haste',
|
|
1213
|
+
description: 'While wearing these boots, you can use a bonus action to click them together. On your turn, you can increase your speed by 20 feet until the end of your turn.',
|
|
1214
|
+
effects: [
|
|
1215
|
+
{
|
|
1216
|
+
type: 'passive_modifier',
|
|
1217
|
+
target: 'speed',
|
|
1218
|
+
value: 20,
|
|
1219
|
+
description: '+20 speed'
|
|
1220
|
+
}
|
|
1221
|
+
],
|
|
1222
|
+
source: 'equipment_inline'
|
|
1223
|
+
}
|
|
1224
|
+
],
|
|
1225
|
+
spawnWeight: 0.15,
|
|
1226
|
+
source: 'custom',
|
|
1227
|
+
tags: ['magic', 'boots', 'speed']
|
|
1228
|
+
}
|
|
1229
|
+
```
|
|
1230
|
+
|
|
1231
|
+
### Templates vs Instances
|
|
1232
|
+
|
|
1233
|
+
The system supports both template-based and per-instance modifications. **Template-based modifications** are predefined in the ExtensionManager and can be applied to any equipment. **Per-instance modifications** are unique enchantments, curses, or upgrades applied to specific item instances. Each item tracks its own modifications separately.
|
|
1234
|
+
|
|
1235
|
+
For template registration and application code examples, see [Example 4: Template-Based Items](#example-4-template-based-items).
|
|
1236
|
+
|
|
1237
|
+
#### Per-Instance Modifications
|
|
1238
|
+
|
|
1239
|
+
Each item can have unique modifications:
|
|
1240
|
+
|
|
1241
|
+
```typescript
|
|
1242
|
+
// Enchant a specific sword instance
|
|
1243
|
+
const swordInstance = {
|
|
1244
|
+
name: 'Longsword',
|
|
1245
|
+
instanceId: 'sword_12345',
|
|
1246
|
+
modifications: []
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
const enchantment: EquipmentModification = {
|
|
1250
|
+
id: 'enchant_001',
|
|
1251
|
+
name: '+1 Longsword',
|
|
1252
|
+
properties: [
|
|
1253
|
+
{ type: 'passive_modifier', target: 'attack_roll', value: 1 }
|
|
1254
|
+
],
|
|
1255
|
+
appliedAt: new Date().toISOString(),
|
|
1256
|
+
source: 'enchantment'
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
EquipmentModifier.enchant(equipment, 'Longsword', enchantment, character);
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
#### Combined Effects
|
|
1263
|
+
|
|
1264
|
+
Final effects are the combination of:
|
|
1265
|
+
1. Base equipment properties
|
|
1266
|
+
2. Template properties
|
|
1267
|
+
3. Per-instance modifications
|
|
1268
|
+
|
|
1269
|
+
```typescript
|
|
1270
|
+
// Get all effects from an item
|
|
1271
|
+
const allEffects = EquipmentModifier.getCombinedEffects(
|
|
1272
|
+
equipment,
|
|
1273
|
+
'Longsword',
|
|
1274
|
+
'sword_12345'
|
|
1275
|
+
);
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
### Spawning with Weights
|
|
1279
|
+
|
|
1280
|
+
```typescript
|
|
1281
|
+
// Spawn random items (respects weights)
|
|
1282
|
+
const items = EquipmentSpawnHelper.spawnRandom(
|
|
1283
|
+
3,
|
|
1284
|
+
rng,
|
|
1285
|
+
{ excludeZeroWeight: true }
|
|
1286
|
+
);
|
|
1287
|
+
|
|
1288
|
+
// Spawn by rarity
|
|
1289
|
+
const rareItems = EquipmentSpawnHelper.spawnByRarity('rare', 2, rng);
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
### Enchantment Library
|
|
1293
|
+
|
|
1294
|
+
#### Using Predefined Enchantments
|
|
1295
|
+
|
|
1296
|
+
```typescript
|
|
1297
|
+
import { EquipmentModifier, WEAPON_ENCHANTMENTS, ARMOR_ENCHANTMENTS, RESISTANCE_ENCHANTMENTS } from 'playlist-data-engine';
|
|
1298
|
+
|
|
1299
|
+
// Apply a +1 enhancement to a weapon
|
|
1300
|
+
const plusOne = WEAPON_ENCHANTMENTS.plusOne;
|
|
1301
|
+
character.equipment = EquipmentModifier.enchant(
|
|
1302
|
+
character.equipment,
|
|
1303
|
+
'Longsword',
|
|
1304
|
+
plusOne,
|
|
1305
|
+
character
|
|
1306
|
+
);
|
|
1307
|
+
|
|
1308
|
+
// Add elemental damage
|
|
1309
|
+
const flaming = WEAPON_ENCHANTMENTS.flaming; // +1d6 fire damage
|
|
1310
|
+
character.equipment = EquipmentModifier.enchant(
|
|
1311
|
+
character.equipment,
|
|
1312
|
+
'Longsword',
|
|
1313
|
+
flaming,
|
|
1314
|
+
character
|
|
1315
|
+
);
|
|
1316
|
+
|
|
1317
|
+
// Improve armor
|
|
1318
|
+
character.equipment = EquipmentModifier.enchant(
|
|
1319
|
+
character.equipment,
|
|
1320
|
+
'Plate Armor',
|
|
1321
|
+
ARMOR_ENCHANTMENTS.plusTwo, // +2 AC
|
|
1322
|
+
character
|
|
1323
|
+
);
|
|
1324
|
+
|
|
1325
|
+
// Add resistance
|
|
1326
|
+
character.equipment = EquipmentModifier.enchant(
|
|
1327
|
+
character.equipment,
|
|
1328
|
+
'Cloak of Protection',
|
|
1329
|
+
RESISTANCE_ENCHANTMENTS.fire, // Fire resistance
|
|
1330
|
+
character
|
|
1331
|
+
);
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
#### Creating Stat-Boosting Enchantments
|
|
1335
|
+
|
|
1336
|
+
The `create*Enchantment` functions create stat bonuses with configurable levels (1-4):
|
|
1337
|
+
|
|
1338
|
+
```typescript
|
|
1339
|
+
import {
|
|
1340
|
+
createStrengthEnchantment,
|
|
1341
|
+
createDexterityEnchantment,
|
|
1342
|
+
createConstitutionEnchantment,
|
|
1343
|
+
createIntelligenceEnchantment,
|
|
1344
|
+
createWisdomEnchantment,
|
|
1345
|
+
createCharismaEnchantment
|
|
1346
|
+
} from 'playlist-data-engine';
|
|
1347
|
+
|
|
1348
|
+
// Create +2 Strength belt
|
|
1349
|
+
const beltOfStrength = createStrengthEnchantment(2); // Bonus: 1-4
|
|
1350
|
+
character.equipment = EquipmentModifier.enchant(
|
|
1351
|
+
character.equipment,
|
|
1352
|
+
'Belt of Giant Strength',
|
|
1353
|
+
beltOfStrength,
|
|
1354
|
+
character
|
|
1355
|
+
);
|
|
1356
|
+
|
|
1357
|
+
// Create +4 Intelligence circlet
|
|
1358
|
+
const circletOfIntellect = createIntelligenceEnchantment(4);
|
|
1359
|
+
character.equipment = EquipmentModifier.enchant(
|
|
1360
|
+
character.equipment,
|
|
1361
|
+
'Circlet of Intellect',
|
|
1362
|
+
circletOfIntellect,
|
|
1363
|
+
character
|
|
1364
|
+
);
|
|
1365
|
+
```
|
|
1366
|
+
|
|
1367
|
+
#### Applying Curses
|
|
1368
|
+
|
|
1369
|
+
```typescript
|
|
1370
|
+
import { EquipmentModifier, CURSES } from 'playlist-data-engine';
|
|
1371
|
+
|
|
1372
|
+
// Apply a cursed item
|
|
1373
|
+
const cursedItem = EquipmentModifier.curse(
|
|
1374
|
+
character.equipment,
|
|
1375
|
+
'Ring of Weakness',
|
|
1376
|
+
CURSES.weakness, // -4 Strength
|
|
1377
|
+
character
|
|
1378
|
+
);
|
|
1379
|
+
|
|
1380
|
+
// Apply attunement lock (cannot remove without remove curse)
|
|
1381
|
+
const lockedItem = EquipmentModifier.curse(
|
|
1382
|
+
character.equipment,
|
|
1383
|
+
'Cursed Helmet',
|
|
1384
|
+
CURSES.attunement,
|
|
1385
|
+
character
|
|
1386
|
+
);
|
|
1387
|
+
```
|
|
1388
|
+
|
|
1389
|
+
#### Combo Enchantments
|
|
1390
|
+
|
|
1391
|
+
Special multi-effect enchantments for powerful items:
|
|
1392
|
+
|
|
1393
|
+
```typescript
|
|
1394
|
+
import { ALL_ENCHANTMENTS } from 'playlist-data-engine';
|
|
1395
|
+
|
|
1396
|
+
// Holy Avenger: +3 enhancement, radiant damage vs fiends/undead, +5 saves vs spells
|
|
1397
|
+
const holyAvenger = ALL_ENCHANTMENTS.holyAvenger;
|
|
1398
|
+
character.equipment = EquipmentModifier.enchant(
|
|
1399
|
+
character.equipment,
|
|
1400
|
+
'Holy Avenger',
|
|
1401
|
+
holyAvenger,
|
|
1402
|
+
character
|
|
1403
|
+
);
|
|
1404
|
+
|
|
1405
|
+
// Dragon Slayer: +2 enhancement, extra damage vs dragons, fire resistance
|
|
1406
|
+
const dragonSlayer = ALL_ENCHANTMENTS.dragonSlayer;
|
|
1407
|
+
character.equipment = EquipmentModifier.enchant(
|
|
1408
|
+
character.equipment,
|
|
1409
|
+
'Dragon Slayer Sword',
|
|
1410
|
+
dragonSlayer,
|
|
1411
|
+
character
|
|
1412
|
+
);
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
#### Querying Enchantments
|
|
1416
|
+
|
|
1417
|
+
```typescript
|
|
1418
|
+
import { getEnchantment, getCurse, getAllEnchantments, getAllCurses, getEnchantmentsByType } from 'playlist-data-engine';
|
|
1419
|
+
|
|
1420
|
+
// Get specific enchantment by ID
|
|
1421
|
+
const ench = getEnchantment('flaming');
|
|
1422
|
+
if (ench) {
|
|
1423
|
+
console.log(ench.name); // 'Flaming'
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// Get all curses
|
|
1427
|
+
const allCurses = getAllCurses();
|
|
1428
|
+
console.log(`Available curses: ${allCurses.length}`); // 17 curses
|
|
1429
|
+
|
|
1430
|
+
// Get enchantments by type
|
|
1431
|
+
const weaponEnchants = getEnchantmentsByType('weapon');
|
|
1432
|
+
console.log(`Weapon enchantments: ${weaponEnchants.length}`); // 16 enchantments
|
|
1433
|
+
```
|
|
1434
|
+
|
|
1435
|
+
### Magic Item Examples
|
|
1436
|
+
|
|
1437
|
+
#### Getting Magic Items by Name
|
|
1438
|
+
|
|
1439
|
+
```typescript
|
|
1440
|
+
import { getMagicItem } from 'playlist-data-engine';
|
|
1441
|
+
|
|
1442
|
+
// Get a specific magic item
|
|
1443
|
+
const vorpalSword = getMagicItem('Vorpal Sword');
|
|
1444
|
+
if (vorpalSword) {
|
|
1445
|
+
console.log(vorpalSword.properties);
|
|
1446
|
+
// Output: Array of equipment properties for this legendary item
|
|
1447
|
+
console.log(vorpalSword.spawnWeight); // 0 (never spawns randomly, game-only)
|
|
1448
|
+
}
|
|
1449
|
+
```
|
|
1450
|
+
|
|
1451
|
+
#### Querying Magic Items
|
|
1452
|
+
|
|
1453
|
+
```typescript
|
|
1454
|
+
import {
|
|
1455
|
+
getMagicItemsByType,
|
|
1456
|
+
getMagicItemsByRarity,
|
|
1457
|
+
getCursedItems,
|
|
1458
|
+
getItemsWithProperty
|
|
1459
|
+
} from 'playlist-data-engine';
|
|
1460
|
+
|
|
1461
|
+
// Get all weapons
|
|
1462
|
+
const weapons = getMagicItemsByType('weapon');
|
|
1463
|
+
console.log(`Magic weapons: ${weapons.length}`); // 4 weapons
|
|
1464
|
+
|
|
1465
|
+
// Get all rare items
|
|
1466
|
+
const rareItems = getMagicItemsByRarity('rare');
|
|
1467
|
+
console.log(`Rare items: ${rareItems.length}`); // ~15 rare items
|
|
1468
|
+
|
|
1469
|
+
// Get cursed items
|
|
1470
|
+
const cursedItems = getCursedItems();
|
|
1471
|
+
cursedItems.forEach(item => {
|
|
1472
|
+
console.log(`Cursed: ${item.name}`);
|
|
1473
|
+
// Output: -1 Cursed Sword, Belt of Strength Drain, Helmet of Opposite Alignment
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
// Get all items with a specific property
|
|
1477
|
+
const statBonusItems = getItemsWithProperty('stat_bonus');
|
|
1478
|
+
console.log(`Items with stat bonuses: ${statBonusItems.length}`);
|
|
1479
|
+
```
|
|
1480
|
+
|
|
1481
|
+
> **Note:** For template application examples, see [Example 4: Template-Based Items](#example-4-template-based-items).
|
|
1482
|
+
|
|
1483
|
+
#### Registering Magic Items with ExtensionManager
|
|
1484
|
+
|
|
1485
|
+
Magic item examples can be registered as custom equipment for procedural generation:
|
|
1486
|
+
|
|
1487
|
+
```typescript
|
|
1488
|
+
import { ExtensionManager, MAGIC_ITEMS } from 'playlist-data-engine';
|
|
1489
|
+
|
|
1490
|
+
const manager = ExtensionManager.getInstance();
|
|
1491
|
+
|
|
1492
|
+
// Register all magic items as custom equipment
|
|
1493
|
+
manager.register('equipment', MAGIC_ITEMS, {
|
|
1494
|
+
mode: 'append',
|
|
1495
|
+
weights: MAGIC_ITEMS.reduce((acc, item) => {
|
|
1496
|
+
acc[item.name] = item.spawnWeight ?? 0;
|
|
1497
|
+
return acc;
|
|
1498
|
+
}, {} as Record<string, number>)
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
// Now items will appear in random generation (respecting spawnWeight)
|
|
1502
|
+
// Note: Vorpal Sword and other legendary items have spawnWeight: 0,
|
|
1503
|
+
// so they won't appear randomly but can still be spawned by name
|
|
1504
|
+
```
|
|
1505
|
+
|
|
1506
|
+
#### Direct Access to Magic Item Collections
|
|
1507
|
+
|
|
1508
|
+
```typescript
|
|
1509
|
+
import { MAGIC_ITEMS, ITEM_CREATION_TEMPLATES, ENCHANTMENT_LIBRARY } from 'playlist-data-engine';
|
|
1510
|
+
|
|
1511
|
+
// Iterate through all magic items
|
|
1512
|
+
MAGIC_ITEMS.forEach(item => {
|
|
1513
|
+
console.log(`${item.name} (${item.rarity}) - ${item.type}`);
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
// Access specific template
|
|
1517
|
+
const viciousTemplate = ITEM_CREATION_TEMPLATES.vicious_weapon_template;
|
|
1518
|
+
console.log(viciousTemplate.properties);
|
|
1519
|
+
```
|
|
1520
|
+
|
|
1521
|
+
### Custom Equipment
|
|
1522
|
+
|
|
1523
|
+
Custom equipment is registered through the ExtensionManager or passed directly to `CharacterGenerator.generate()`. All custom equipment is automatically validated using `EquipmentValidator.validateEquipment()`. See [API Reference - EquipmentValidator](#api-reference) for validation methods.
|
|
1524
|
+
|
|
1525
|
+
### Example 1: Comprehensive Custom Item
|
|
1526
|
+
|
|
1527
|
+
This example shows a single item that combines multiple equipment capabilities—stat bonuses, AC bonuses, and skill proficiencies. You can include any or all of these properties on your custom items.
|
|
1528
|
+
|
|
1529
|
+
```typescript
|
|
1530
|
+
import { ExtensionManager, CharacterGenerator, EquipmentValidator } from 'playlist-data-engine';
|
|
1531
|
+
import type { EnhancedEquipment } from './src/core/types/Equipment.js';
|
|
1532
|
+
|
|
1533
|
+
// ===== STEP 1: Define a comprehensive custom item =====
|
|
1534
|
+
const cloakOfTheElder: EnhancedEquipment = {
|
|
1535
|
+
name: 'Cloak of the Elder',
|
|
1536
|
+
type: 'item',
|
|
1537
|
+
rarity: 'very_rare',
|
|
1538
|
+
weight: 1,
|
|
1539
|
+
icon: '/icons/items/cloak-elder.png',
|
|
1540
|
+
image: '/images/equipment/cloak-of-the-elder.png',
|
|
1541
|
+
|
|
1542
|
+
// Stat bonuses: increases ability scores
|
|
1543
|
+
properties: [
|
|
1544
|
+
{
|
|
1545
|
+
type: 'stat_bonus',
|
|
1546
|
+
target: 'WIS',
|
|
1547
|
+
value: 2,
|
|
1548
|
+
description: '+2 Wisdom'
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
type: 'stat_bonus',
|
|
1552
|
+
target: 'INT',
|
|
1553
|
+
value: 1,
|
|
1554
|
+
description: '+1 Intelligence'
|
|
1555
|
+
},
|
|
1556
|
+
// AC bonus: increases armor class
|
|
1557
|
+
{
|
|
1558
|
+
type: 'passive_modifier',
|
|
1559
|
+
target: 'ac',
|
|
1560
|
+
value: 2,
|
|
1561
|
+
description: '+2 Armor Class',
|
|
1562
|
+
stackable: true
|
|
1563
|
+
},
|
|
1564
|
+
{
|
|
1565
|
+
type: 'passive_modifier',
|
|
1566
|
+
target: 'saving_throws',
|
|
1567
|
+
value: 1,
|
|
1568
|
+
description: '+1 to all saving throws',
|
|
1569
|
+
stackable: true
|
|
1570
|
+
}
|
|
1571
|
+
],
|
|
1572
|
+
|
|
1573
|
+
// Skill proficiencies: grants skills when equipped
|
|
1574
|
+
grantsSkills: [
|
|
1575
|
+
{ skillId: 'arcana', level: 'expertise' },
|
|
1576
|
+
{ skillId: 'history', level: 'proficient' },
|
|
1577
|
+
{ skillId: 'insight', level: 'proficient' }
|
|
1578
|
+
],
|
|
1579
|
+
|
|
1580
|
+
// Optional: grant features or spells
|
|
1581
|
+
grantsFeatures: ['darkvision'],
|
|
1582
|
+
grantsSpells: [
|
|
1583
|
+
{ spellId: 'detect_magic', level: 1, uses: null } // unlimited uses
|
|
1584
|
+
],
|
|
1585
|
+
|
|
1586
|
+
spawnWeight: 0.1,
|
|
1587
|
+
source: 'custom',
|
|
1588
|
+
tags: ['magic', 'cloak', 'wisdom', 'intelligence']
|
|
1589
|
+
};
|
|
1590
|
+
|
|
1591
|
+
// ===== STEP 2: Register via ExtensionManager =====
|
|
1592
|
+
const manager = ExtensionManager.getInstance();
|
|
1593
|
+
manager.register('equipment', [cloakOfTheElder], {
|
|
1594
|
+
weights: { 'Cloak of the Elder': 0.1 }
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
// ===== STEP 3: Register via CharacterGenerator (convenience method) =====
|
|
1598
|
+
const character = CharacterGenerator.generate(
|
|
1599
|
+
'my-seed',
|
|
1600
|
+
audioProfile,
|
|
1601
|
+
track,
|
|
1602
|
+
{ extensions: { equipment: [cloakOfTheElder] } }
|
|
1603
|
+
);
|
|
1604
|
+
|
|
1605
|
+
// ===== STEP 4: Adjust spawn rates after registration =====
|
|
1606
|
+
manager.setWeights('equipment', {
|
|
1607
|
+
'Cloak of the Elder': 0.05, // Make it rarer
|
|
1608
|
+
'Potion of Healing': 5.0 // Make potions more common
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
// ===== STEP 5: Validate equipment (optional, happens automatically) =====
|
|
1612
|
+
const validation = EquipmentValidator.validateEquipment(cloakOfTheElder);
|
|
1613
|
+
if (!validation.valid) {
|
|
1614
|
+
console.error('Invalid equipment:', validation.errors);
|
|
1615
|
+
}
|
|
1616
|
+
```
|
|
1617
|
+
|
|
1618
|
+
**Key Points:**
|
|
1619
|
+
- Items can grant **any combination** of stat bonuses, AC bonuses, and skills
|
|
1620
|
+
- `properties` array contains all modifiers (stats, AC, damage, etc.)
|
|
1621
|
+
- `grantsSkills` array uses `{skillId, level}` format (level: `proficient` | `expertise`)
|
|
1622
|
+
- `grantsFeatures` accepts string references to features in the FeatureQuery
|
|
1623
|
+
- `grantsSpells` accepts `{spellId, level?, uses?, recharge?}` for spell-granting items
|
|
1624
|
+
|
|
1625
|
+
### Example 2: Enchanting Equipment
|
|
1626
|
+
|
|
1627
|
+
```typescript
|
|
1628
|
+
import { EquipmentModifier } from './src/core/equipment/EquipmentModifier.js';
|
|
1629
|
+
|
|
1630
|
+
// Create enchantment
|
|
1631
|
+
const enchantment = EquipmentModifier.createModification(
|
|
1632
|
+
'plus_one_001',
|
|
1633
|
+
'+1 Longsword',
|
|
1634
|
+
[{
|
|
1635
|
+
type: 'passive_modifier',
|
|
1636
|
+
target: 'attack_roll',
|
|
1637
|
+
value: 1,
|
|
1638
|
+
description: '+1 to attack rolls'
|
|
1639
|
+
}],
|
|
1640
|
+
'enchantment'
|
|
1641
|
+
);
|
|
1642
|
+
|
|
1643
|
+
// Apply to equipment
|
|
1644
|
+
const updatedEquipment = EquipmentModifier.enchant(
|
|
1645
|
+
character.equipment,
|
|
1646
|
+
'Longsword',
|
|
1647
|
+
enchantment,
|
|
1648
|
+
character
|
|
1649
|
+
);
|
|
1650
|
+
|
|
1651
|
+
// Check if enchanted
|
|
1652
|
+
if (EquipmentModifier.isEnchanted(character.equipment, 'Longsword')) {
|
|
1653
|
+
console.log('Item is enchanted!');
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// Get item summary
|
|
1657
|
+
const summary = EquipmentModifier.getItemSummary(character.equipment, 'Longsword');
|
|
1658
|
+
console.log(summary);
|
|
1659
|
+
// { name: 'Longsword', modifications: [...], isCursed: false, isEnchanted: true }
|
|
1660
|
+
|
|
1661
|
+
// For predefined enchantments (stat boosts, elemental damage, curses), see [Enchantment Library](#enchantment-library)
|
|
1662
|
+
```
|
|
1663
|
+
|
|
1664
|
+
### Example 3: Batch Spawning
|
|
1665
|
+
|
|
1666
|
+
```typescript
|
|
1667
|
+
import { EquipmentSpawnHelper } from './src/core/equipment/EquipmentSpawnHelper.js';
|
|
1668
|
+
import { SeededRNG } from './src/utils/random.js';
|
|
1669
|
+
|
|
1670
|
+
// Spawn treasure hoard
|
|
1671
|
+
const rng = new SeededRNG('dragon_hoard_123');
|
|
1672
|
+
const hoard = EquipmentSpawnHelper.spawnTreasureHoard(15, rng);
|
|
1673
|
+
|
|
1674
|
+
console.log(`Generated ${hoard.items.length} items worth ~${hoard.totalValue} gp`);
|
|
1675
|
+
|
|
1676
|
+
// Add to character
|
|
1677
|
+
character = EquipmentSpawnHelper.addToCharacter(character, hoard.items, false);
|
|
1678
|
+
```
|
|
1679
|
+
|
|
1680
|
+
### Example 4: Template-Based Items
|
|
1681
|
+
|
|
1682
|
+
Templates define reusable property sets that can be applied to any equipment. Two application methods:
|
|
1683
|
+
|
|
1684
|
+
```typescript
|
|
1685
|
+
import { ExtensionManager, EquipmentSpawnHelper, applyTemplate, type EnhancedEquipment } from 'playlist-data-engine';
|
|
1686
|
+
|
|
1687
|
+
// Step 1: Register template (once)
|
|
1688
|
+
ExtensionManager.getInstance().register('equipment.templates', [{
|
|
1689
|
+
id: 'flaming_weapon',
|
|
1690
|
+
name: 'Flaming Weapon',
|
|
1691
|
+
properties: [{ type: 'damage_bonus', target: 'fire', value: '1d6', description: '+1d6 fire' }]
|
|
1692
|
+
}]);
|
|
1693
|
+
|
|
1694
|
+
// Method 1: spawnFromTemplate - looks up item by name + template by ID
|
|
1695
|
+
const sword1 = EquipmentSpawnHelper.spawnFromTemplate('flaming_weapon', 'Longsword');
|
|
1696
|
+
|
|
1697
|
+
// Method 2: applyTemplate - takes equipment object directly, returns modified equipment or null
|
|
1698
|
+
const baseSword: EnhancedEquipment = {
|
|
1699
|
+
name: 'Longsword', type: 'weapon', rarity: 'common', weight: 3,
|
|
1700
|
+
damage: { dice: '1d8', damageType: 'slashing', versatile: '1d10' },
|
|
1701
|
+
weaponProperties: ['finesse', 'versatile'], source: 'base', tags: ['martial', 'melee']
|
|
1702
|
+
};
|
|
1703
|
+
const sword2 = applyTemplate(baseSword, 'flaming_weapon_template');
|
|
1704
|
+
const sword3 = applyTemplate(baseSword, 'plus_one_weapon'); // Template chaining
|
|
1705
|
+
```
|
|
1706
|
+
|
|
1707
|
+
| Method | Input | Use Case |
|
|
1708
|
+
|--------|-------|----------|
|
|
1709
|
+
| `spawnFromTemplate(templateId, itemName)` | Template ID + item name | Spawning from templates |
|
|
1710
|
+
| `applyTemplate(equipment, templateId)` | Equipment object + template ID | Modifying existing equipment |
|
|
1711
|
+
|
|
1712
|
+
**Note**: For property type reference, see [EquipmentPropertyType](#equipmentpropertytype) in the Equipment Properties section. For equipment-granted features, see [Equipment-Granted Features](#equipment-granted-features).
|
|
1713
|
+
|
|
1714
|
+
### Example 5: Items That Grant Spells
|
|
1715
|
+
|
|
1716
|
+
```typescript
|
|
1717
|
+
// ===== Ring of Spell Storing - Store and cast spells =====
|
|
1718
|
+
const ringOfSpellStoring: EnhancedEquipment = {
|
|
1719
|
+
name: 'Ring of Spell Storing',
|
|
1720
|
+
type: 'item',
|
|
1721
|
+
rarity: 'rare',
|
|
1722
|
+
weight: 0.1,
|
|
1723
|
+
properties: [
|
|
1724
|
+
{
|
|
1725
|
+
type: 'special_property',
|
|
1726
|
+
target: 'spell_storing',
|
|
1727
|
+
value: 5,
|
|
1728
|
+
description: 'Can store up to 5 levels of spells'
|
|
1729
|
+
}
|
|
1730
|
+
],
|
|
1731
|
+
grantsSpells: [
|
|
1732
|
+
{ spellId: 'fireball', level: 3, uses: 1, recharge: 'dawn' },
|
|
1733
|
+
{ spellId: 'shield', level: 1, uses: 1, recharge: 'dawn' }
|
|
1734
|
+
],
|
|
1735
|
+
spawnWeight: 0.2,
|
|
1736
|
+
source: 'custom',
|
|
1737
|
+
tags: ['magic', 'ring', 'spell']
|
|
1738
|
+
};
|
|
1739
|
+
|
|
1740
|
+
// ===== Scroll of Fireball - One-time use spell =====
|
|
1741
|
+
const scrollOfFireball: EnhancedEquipment = {
|
|
1742
|
+
name: 'Scroll of Fireball',
|
|
1743
|
+
type: 'item',
|
|
1744
|
+
rarity: 'uncommon',
|
|
1745
|
+
weight: 0.1,
|
|
1746
|
+
grantsSpells: [
|
|
1747
|
+
{ spellId: 'fireball', level: 3, uses: 1 }
|
|
1748
|
+
// No recharge means one-time use
|
|
1749
|
+
],
|
|
1750
|
+
source: 'custom',
|
|
1751
|
+
tags: ['magic', 'scroll', 'consumable', 'fire']
|
|
1752
|
+
};
|
|
1753
|
+
|
|
1754
|
+
// ===== Wand of Magic Missiles - Cast at will (unlimited) =====
|
|
1755
|
+
const wandOfMagicMissiles: EnhancedEquipment = {
|
|
1756
|
+
name: 'Wand of Magic Missiles',
|
|
1757
|
+
type: 'item',
|
|
1758
|
+
rarity: 'uncommon',
|
|
1759
|
+
weight: 1,
|
|
1760
|
+
grantsSpells: [
|
|
1761
|
+
{ spellId: 'magic_missile', level: 1, uses: null }
|
|
1762
|
+
// uses: null means unlimited uses
|
|
1763
|
+
],
|
|
1764
|
+
source: 'custom',
|
|
1765
|
+
tags: ['magic', 'wand', 'evocation']
|
|
1766
|
+
};
|
|
1767
|
+
|
|
1768
|
+
// ===== Understanding grantsSpells Properties =====
|
|
1769
|
+
/*
|
|
1770
|
+
| Property | Type | Description |
|
|
1771
|
+
|----------|------|-------------|
|
|
1772
|
+
| spellId | string | The spell identifier (must exist in spell database) |
|
|
1773
|
+
| level | number | Spell level (0 for cantrips, 1-9 for spell levels) |
|
|
1774
|
+
| uses | number or null | Number of uses, or null for unlimited |
|
|
1775
|
+
| recharge | string | When uses reset: 'dawn', 'short_rest', 'long_rest', or undefined (one-time) |
|
|
1776
|
+
|
|
1777
|
+
Recharge Options:
|
|
1778
|
+
- undefined or omitted - One-time use (consumable like scrolls)
|
|
1779
|
+
- 'dawn' - Uses reset at dawn (daily items like most magic items)
|
|
1780
|
+
- 'short_rest' - Uses reset on short rest (powerful items)
|
|
1781
|
+
- 'long_rest' - Uses reset on long rest (very powerful items)
|
|
1782
|
+
- uses: null - Unlimited uses (cantrips, wands, at-will items)
|
|
1783
|
+
*/
|
|
1784
|
+
```
|
|
1785
|
+
|
|
1786
|
+
### Example 6: Fire Damage (Two Methods)
|
|
1787
|
+
|
|
1788
|
+
**Method 1: Using Properties**
|
|
1789
|
+
|
|
1790
|
+
```typescript
|
|
1791
|
+
const flameTongueWeapon: EnhancedEquipment = {
|
|
1792
|
+
name: 'Flame Tongue',
|
|
1793
|
+
type: 'weapon',
|
|
1794
|
+
rarity: 'rare',
|
|
1795
|
+
weight: 3,
|
|
1796
|
+
damage: { dice: '1d8', damageType: 'slashing' },
|
|
1797
|
+
weaponProperties: ['finesse'],
|
|
1798
|
+
properties: [
|
|
1799
|
+
{
|
|
1800
|
+
type: 'damage_bonus',
|
|
1801
|
+
target: 'fire',
|
|
1802
|
+
value: '2d6',
|
|
1803
|
+
description: '+2d6 fire damage on hit'
|
|
1804
|
+
}
|
|
1805
|
+
],
|
|
1806
|
+
source: 'custom',
|
|
1807
|
+
tags: ['magic', 'fire', 'weapon']
|
|
1808
|
+
};
|
|
1809
|
+
```
|
|
1810
|
+
|
|
1811
|
+
**Method 2: Using a Feature Reference**
|
|
1812
|
+
|
|
1813
|
+
```typescript
|
|
1814
|
+
// Reference an existing feature from the registry
|
|
1815
|
+
const flameTongueWithFeature: EnhancedEquipment = {
|
|
1816
|
+
name: 'Flame Tongue',
|
|
1817
|
+
type: 'weapon',
|
|
1818
|
+
rarity: 'rare',
|
|
1819
|
+
weight: 3,
|
|
1820
|
+
damage: { dice: '1d8', damageType: 'slashing' },
|
|
1821
|
+
weaponProperties: ['finesse'],
|
|
1822
|
+
grantsFeatures: ['flame_weapon'],
|
|
1823
|
+
source: 'custom',
|
|
1824
|
+
tags: ['magic', 'fire', 'weapon']
|
|
1825
|
+
};
|
|
1826
|
+
|
|
1827
|
+
// Or define an inline mini-feature for this item only
|
|
1828
|
+
const flameTongueInlineFeature: EnhancedEquipment = {
|
|
1829
|
+
name: 'Flame Tongue',
|
|
1830
|
+
type: 'weapon',
|
|
1831
|
+
rarity: 'rare',
|
|
1832
|
+
weight: 3,
|
|
1833
|
+
damage: { dice: '1d8', damageType: 'slashing' },
|
|
1834
|
+
weaponProperties: ['finesse'],
|
|
1835
|
+
grantsFeatures: [
|
|
1836
|
+
{
|
|
1837
|
+
id: 'flame_tongue_fire',
|
|
1838
|
+
name: 'Flame Tongue Fire',
|
|
1839
|
+
description: 'This weapon deals extra fire damage.',
|
|
1840
|
+
source: 'equipment_inline',
|
|
1841
|
+
effects: [
|
|
1842
|
+
{
|
|
1843
|
+
type: 'damage_bonus',
|
|
1844
|
+
target: 'fire',
|
|
1845
|
+
value: '2d6',
|
|
1846
|
+
description: '+2d6 fire damage on hit'
|
|
1847
|
+
},
|
|
1848
|
+
{
|
|
1849
|
+
type: 'ability_unlock',
|
|
1850
|
+
target: 'light',
|
|
1851
|
+
value: 'bright_light_20ft',
|
|
1852
|
+
description: 'Sheds bright light in a 20ft radius'
|
|
1853
|
+
}
|
|
1854
|
+
]
|
|
1855
|
+
}
|
|
1856
|
+
],
|
|
1857
|
+
source: 'custom',
|
|
1858
|
+
tags: ['magic', 'fire', 'weapon']
|
|
1859
|
+
};
|
|
1860
|
+
```
|
|
1861
|
+
|
|
1862
|
+
### Example 7: Conditional Effects
|
|
1863
|
+
|
|
1864
|
+
```typescript
|
|
1865
|
+
import type { EnhancedEquipment } from './src/core/types/Equipment.js';
|
|
1866
|
+
|
|
1867
|
+
// ===== VS CREATURE TYPE =====
|
|
1868
|
+
const dragonSlayerAxe: EnhancedEquipment = {
|
|
1869
|
+
name: 'Dragon Slayer Axe',
|
|
1870
|
+
type: 'weapon',
|
|
1871
|
+
rarity: 'very_rare',
|
|
1872
|
+
weight: 5,
|
|
1873
|
+
damage: { dice: '1d12', damageType: 'slashing' },
|
|
1874
|
+
weaponProperties: ['two-handed'],
|
|
1875
|
+
properties: [
|
|
1876
|
+
{
|
|
1877
|
+
type: 'damage_bonus',
|
|
1878
|
+
target: 'dragon',
|
|
1879
|
+
value: '3d6',
|
|
1880
|
+
condition: { type: 'vs_creature_type', value: 'dragon' },
|
|
1881
|
+
description: '+3d6 damage vs dragons'
|
|
1882
|
+
}
|
|
1883
|
+
],
|
|
1884
|
+
spawnWeight: 0.05,
|
|
1885
|
+
source: 'custom',
|
|
1886
|
+
tags: ['magic', 'weapon', 'dragon', 'slayer']
|
|
1887
|
+
};
|
|
1888
|
+
|
|
1889
|
+
// ===== TIME OF DAY =====
|
|
1890
|
+
const moonBlade: EnhancedEquipment = {
|
|
1891
|
+
name: 'Moon Blade',
|
|
1892
|
+
type: 'weapon',
|
|
1893
|
+
rarity: 'rare',
|
|
1894
|
+
weight: 3,
|
|
1895
|
+
damage: { dice: '1d8', damageType: 'slashing' },
|
|
1896
|
+
properties: [
|
|
1897
|
+
{
|
|
1898
|
+
type: 'damage_bonus',
|
|
1899
|
+
target: 'radiant',
|
|
1900
|
+
value: '2d6',
|
|
1901
|
+
condition: { type: 'at_time_of_day', value: 'night' },
|
|
1902
|
+
description: '+2d6 radiant damage at night'
|
|
1903
|
+
},
|
|
1904
|
+
{
|
|
1905
|
+
type: 'damage_bonus',
|
|
1906
|
+
target: 'radiant',
|
|
1907
|
+
value: '1d6',
|
|
1908
|
+
condition: { type: 'at_time_of_day', value: 'dawn' },
|
|
1909
|
+
description: '+1d6 radiant damage at dawn'
|
|
1910
|
+
}
|
|
1911
|
+
],
|
|
1912
|
+
spawnWeight: 0.1,
|
|
1913
|
+
source: 'custom',
|
|
1914
|
+
tags: ['magic', 'weapon', 'moon', 'radiant']
|
|
1915
|
+
};
|
|
1916
|
+
|
|
1917
|
+
// ===== WIELDER RACE =====
|
|
1918
|
+
const elvenChain: EnhancedEquipment = {
|
|
1919
|
+
name: 'Elven Chain',
|
|
1920
|
+
type: 'armor',
|
|
1921
|
+
rarity: 'rare',
|
|
1922
|
+
weight: 20,
|
|
1923
|
+
acBonus: 16,
|
|
1924
|
+
properties: [
|
|
1925
|
+
{
|
|
1926
|
+
type: 'special_property',
|
|
1927
|
+
target: 'sleep_immunity',
|
|
1928
|
+
value: true,
|
|
1929
|
+
condition: { type: 'wielder_race', value: 'Elf' },
|
|
1930
|
+
description: 'Immunity to magic that puts you to sleep (Elf only)'
|
|
1931
|
+
},
|
|
1932
|
+
{
|
|
1933
|
+
type: 'passive_modifier',
|
|
1934
|
+
target: 'stealth_disadvantage',
|
|
1935
|
+
value: false,
|
|
1936
|
+
condition: { type: 'wielder_race', value: 'Elf' },
|
|
1937
|
+
description: 'No stealth disadvantage (Elf only)'
|
|
1938
|
+
}
|
|
1939
|
+
],
|
|
1940
|
+
spawnWeight: 0.1,
|
|
1941
|
+
source: 'custom',
|
|
1942
|
+
tags: ['magic', 'armor', 'elf', 'stealth']
|
|
1943
|
+
};
|
|
1944
|
+
|
|
1945
|
+
// ===== WIELDER CLASS =====
|
|
1946
|
+
const holyAvenger: EnhancedEquipment = {
|
|
1947
|
+
name: 'Holy Avenger',
|
|
1948
|
+
type: 'weapon',
|
|
1949
|
+
rarity: 'legendary',
|
|
1950
|
+
weight: 3,
|
|
1951
|
+
damage: { dice: '1d8', damageType: 'slashing' },
|
|
1952
|
+
weaponProperties: ['versatile'],
|
|
1953
|
+
properties: [
|
|
1954
|
+
{
|
|
1955
|
+
type: 'passive_modifier',
|
|
1956
|
+
target: 'saving_throws',
|
|
1957
|
+
value: 3,
|
|
1958
|
+
condition: { type: 'wielder_class', value: 'Paladin' },
|
|
1959
|
+
description: '+3 to saving throws (Paladin only)'
|
|
1960
|
+
},
|
|
1961
|
+
{
|
|
1962
|
+
type: 'damage_bonus',
|
|
1963
|
+
target: 'radiant',
|
|
1964
|
+
value: '2d6',
|
|
1965
|
+
condition: { type: 'wielder_class', value: 'Paladin' },
|
|
1966
|
+
description: '+2d6 radiant damage vs fiends/undead (Paladin only)'
|
|
1967
|
+
}
|
|
1968
|
+
],
|
|
1969
|
+
spawnWeight: 0.0, // Legendary - never spawns randomly
|
|
1970
|
+
source: 'custom',
|
|
1971
|
+
tags: ['magic', 'weapon', 'paladin', 'holy', 'legendary']
|
|
1972
|
+
};
|
|
1973
|
+
```
|
|
1974
|
+
|
|
1975
|
+
### Example 8: Progressive Enchantment Through Gameplay
|
|
1976
|
+
|
|
1977
|
+
Track equipment upgrades as players progress:
|
|
1978
|
+
|
|
1979
|
+
```typescript
|
|
1980
|
+
import { EquipmentModifier } from './src/core/equipment/EquipmentModifier.js';
|
|
1981
|
+
import type { EquipmentModification } from './src/core/types/Equipment.js';
|
|
1982
|
+
|
|
1983
|
+
// Game loop: Player earns upgrade points
|
|
1984
|
+
let enchantmentLevel = 0;
|
|
1985
|
+
|
|
1986
|
+
function upgradeWeapon(character: CharacterSheet, weaponName: string) {
|
|
1987
|
+
enchantmentLevel++;
|
|
1988
|
+
|
|
1989
|
+
const modification: EquipmentModification = {
|
|
1990
|
+
id: `upgrade_${Date.now()}`,
|
|
1991
|
+
name: `+${enchantmentLevel} ${weaponName}`,
|
|
1992
|
+
properties: [
|
|
1993
|
+
{
|
|
1994
|
+
type: 'passive_modifier',
|
|
1995
|
+
target: 'attack_roll',
|
|
1996
|
+
value: enchantmentLevel,
|
|
1997
|
+
description: `+${enchantmentLevel} to attack rolls`
|
|
1998
|
+
},
|
|
1999
|
+
{
|
|
2000
|
+
type: 'passive_modifier',
|
|
2001
|
+
target: 'damage_roll',
|
|
2002
|
+
value: enchantmentLevel,
|
|
2003
|
+
description: `+${enchantmentLevel} to damage rolls`
|
|
2004
|
+
}
|
|
2005
|
+
],
|
|
2006
|
+
appliedAt: new Date().toISOString(),
|
|
2007
|
+
source: 'gameplay'
|
|
2008
|
+
};
|
|
2009
|
+
|
|
2010
|
+
// Remove previous upgrade if exists
|
|
2011
|
+
if (enchantmentLevel > 1) {
|
|
2012
|
+
const oldModId = `upgrade_${Date.now() - 10000}`;
|
|
2013
|
+
EquipmentModifier.removeModification(
|
|
2014
|
+
character.equipment!,
|
|
2015
|
+
weaponName,
|
|
2016
|
+
oldModId,
|
|
2017
|
+
character
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// Apply new upgrade
|
|
2022
|
+
character.equipment = EquipmentModifier.enchant(
|
|
2023
|
+
character.equipment!,
|
|
2024
|
+
weaponName,
|
|
2025
|
+
modification,
|
|
2026
|
+
character
|
|
2027
|
+
);
|
|
2028
|
+
|
|
2029
|
+
console.log(`Weapon upgraded to +${enchantmentLevel}!`);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// Usage:
|
|
2033
|
+
upgradeWeapon(character, 'Longsword'); // +1 Longsword
|
|
2034
|
+
// ... later in game ...
|
|
2035
|
+
upgradeWeapon(character, 'Longsword'); // +2 Longsword
|
|
2036
|
+
// ... even later ...
|
|
2037
|
+
upgradeWeapon(character, 'Longsword'); // +3 Longsword
|
|
2038
|
+
```
|
|
2039
|
+
|
|
2040
|
+
### Example 9: Removing Debuffs from Cursed Items
|
|
2041
|
+
|
|
2042
|
+
```typescript
|
|
2043
|
+
import { EquipmentModifier } from './src/core/equipment/EquipmentModifier.js';
|
|
2044
|
+
|
|
2045
|
+
// ===== DISenCHANT (Remove beneficial enchantments, keep curses) =====
|
|
2046
|
+
const result = EquipmentModifier.disenchant(
|
|
2047
|
+
character.equipment!,
|
|
2048
|
+
'Cursed Sword of Pain',
|
|
2049
|
+
character
|
|
2050
|
+
);
|
|
2051
|
+
// Removes +1 bonuses but keeps the curse
|
|
2052
|
+
|
|
2053
|
+
// ===== LIFT CURSE (Remove curses, keep enchantments) =====
|
|
2054
|
+
const result = EquipmentModifier.liftCurse(
|
|
2055
|
+
character.equipment!,
|
|
2056
|
+
'Cursed Sword of Pain',
|
|
2057
|
+
character
|
|
2058
|
+
);
|
|
2059
|
+
// Removes curse effects but keeps the +1 enchantment
|
|
2060
|
+
|
|
2061
|
+
// ===== REMOVE SPECIFIC MODIFICATION =====
|
|
2062
|
+
const result = EquipmentModifier.removeModification(
|
|
2063
|
+
character.equipment!,
|
|
2064
|
+
'Cursed Sword',
|
|
2065
|
+
'curse_mod_001', // Modification ID to remove
|
|
2066
|
+
character
|
|
2067
|
+
);
|
|
2068
|
+
// Removes only that specific modification
|
|
2069
|
+
```
|
|
2070
|
+
|
|
2071
|
+
### Example 10: Multiple Effects Stacking
|
|
2072
|
+
|
|
2073
|
+
```typescript
|
|
2074
|
+
import { ExtensionManager } from './src/core/extensions/ExtensionManager.js';
|
|
2075
|
+
import type { EnhancedEquipment } from './src/core/types/Equipment.js';
|
|
2076
|
+
|
|
2077
|
+
// Register two items that both give +1 STR
|
|
2078
|
+
const beltOfStrength1: EnhancedEquipment = {
|
|
2079
|
+
name: 'Belt of Strength I',
|
|
2080
|
+
type: 'item',
|
|
2081
|
+
rarity: 'uncommon',
|
|
2082
|
+
weight: 1,
|
|
2083
|
+
properties: [
|
|
2084
|
+
{ type: 'stat_bonus', target: 'STR', value: 1, description: '+1 STR' }
|
|
2085
|
+
],
|
|
2086
|
+
source: 'custom',
|
|
2087
|
+
tags: ['magic', 'strength']
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
const beltOfStrength2: EnhancedEquipment = {
|
|
2091
|
+
name: 'Belt of Strength II',
|
|
2092
|
+
type: 'item',
|
|
2093
|
+
rarity: 'rare',
|
|
2094
|
+
weight: 1,
|
|
2095
|
+
properties: [
|
|
2096
|
+
{ type: 'stat_bonus', target: 'STR', value: 1, description: '+1 STR' }
|
|
2097
|
+
],
|
|
2098
|
+
source: 'custom',
|
|
2099
|
+
tags: ['magic', 'strength']
|
|
2100
|
+
};
|
|
2101
|
+
|
|
2102
|
+
// If a character equips BOTH items, they get +2 STR total
|
|
2103
|
+
// stackable: true is the default behavior
|
|
2104
|
+
```
|
|
2105
|
+
|
|
2106
|
+
### Example 11: Game-Only Items (spawnWeight: 0)
|
|
2107
|
+
|
|
2108
|
+
Items that never spawn randomly but are available to game logic:
|
|
2109
|
+
|
|
2110
|
+
```typescript
|
|
2111
|
+
import type { EnhancedEquipment } from './src/core/types/Equipment.js';
|
|
2112
|
+
|
|
2113
|
+
// This item will NEVER appear in random loot tables
|
|
2114
|
+
const artifactOfDoom: EnhancedEquipment = {
|
|
2115
|
+
name: 'Artifact of Doom',
|
|
2116
|
+
type: 'item',
|
|
2117
|
+
rarity: 'legendary',
|
|
2118
|
+
weight: 5,
|
|
2119
|
+
properties: [
|
|
2120
|
+
{ type: 'stat_bonus', target: 'STR', value: 5, description: '+5 STR' },
|
|
2121
|
+
{ type: 'special_property', target: 'curse', value: true, description: 'Cursed!' }
|
|
2122
|
+
],
|
|
2123
|
+
spawnWeight: 0, // NEVER spawns randomly
|
|
2124
|
+
source: 'custom',
|
|
2125
|
+
tags: ['artifact', 'unique', 'cursed', 'quest']
|
|
2126
|
+
};
|
|
2127
|
+
|
|
2128
|
+
// Can ONLY be obtained through specific game logic:
|
|
2129
|
+
function awardArtifact(character: CharacterSheet) {
|
|
2130
|
+
// Directly add to character's inventory
|
|
2131
|
+
character.equipment = character.equipment || {
|
|
2132
|
+
weapons: [], armor: [], items: [], totalWeight: 0, equippedWeight: 0
|
|
2133
|
+
};
|
|
2134
|
+
|
|
2135
|
+
character.equipment.items.push({
|
|
2136
|
+
name: 'Artifact of Doom',
|
|
2137
|
+
quantity: 1,
|
|
2138
|
+
equipped: false
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
// Manually apply the effects
|
|
2142
|
+
EquipmentEffectApplier.equipItem(character, artifactOfDoom);
|
|
2143
|
+
}
|
|
2144
|
+
```
|
|
2145
|
+
|
|
2146
|
+
### Example 12: Complete Custom Magic Item System
|
|
2147
|
+
|
|
2148
|
+
```typescript
|
|
2149
|
+
import {
|
|
2150
|
+
ExtensionManager,
|
|
2151
|
+
EquipmentSpawnHelper,
|
|
2152
|
+
EquipmentModifier,
|
|
2153
|
+
EquipmentEffectApplier
|
|
2154
|
+
} from './src/core/index.js';
|
|
2155
|
+
import type { EnhancedEquipment, EquipmentModification } from './src/core/types/Equipment.js';
|
|
2156
|
+
import { SeededRNG } from './src/utils/random.js';
|
|
2157
|
+
|
|
2158
|
+
// ===== STEP 1: Define Custom Equipment =====
|
|
2159
|
+
const customItems: EnhancedEquipment[] = [
|
|
2160
|
+
{
|
|
2161
|
+
name: 'Frostbrand',
|
|
2162
|
+
type: 'weapon',
|
|
2163
|
+
rarity: 'very_rare',
|
|
2164
|
+
weight: 3,
|
|
2165
|
+
damage: { dice: '1d8', damageType: 'slashing' },
|
|
2166
|
+
weaponProperties: ['finesse'],
|
|
2167
|
+
properties: [
|
|
2168
|
+
{ type: 'damage_bonus', target: 'cold', value: '1d8', description: '+1d8 cold damage' },
|
|
2169
|
+
{ type: 'ability_unlock', target: 'fire_resistance', value: true, description: 'Fire resistance' }
|
|
2170
|
+
],
|
|
2171
|
+
grantsFeatures: ['protection_from_fire'],
|
|
2172
|
+
spawnWeight: 0.2,
|
|
2173
|
+
source: 'custom',
|
|
2174
|
+
tags: ['magic', 'ice', 'weapon']
|
|
2175
|
+
},
|
|
2176
|
+
{
|
|
2177
|
+
name: 'Boots of Striding and Springing',
|
|
2178
|
+
type: 'item',
|
|
2179
|
+
rarity: 'uncommon',
|
|
2180
|
+
weight: 1,
|
|
2181
|
+
properties: [
|
|
2182
|
+
{ type: 'passive_modifier', target: 'speed', value: 10, description: '+10 speed' },
|
|
2183
|
+
{ type: 'ability_unlock', target: 'long_jump', value: true, description: 'Stand up from prone as bonus action' }
|
|
2184
|
+
],
|
|
2185
|
+
grantsSkills: [{ skillId: 'athletics', level: 'proficient' }],
|
|
2186
|
+
spawnWeight: 0.5,
|
|
2187
|
+
source: 'custom',
|
|
2188
|
+
tags: ['magic', 'boots', 'movement']
|
|
2189
|
+
},
|
|
2190
|
+
{
|
|
2191
|
+
name: 'Ring of Spell Storing',
|
|
2192
|
+
type: 'item',
|
|
2193
|
+
rarity: 'rare',
|
|
2194
|
+
weight: 0.1,
|
|
2195
|
+
grantsSpells: [
|
|
2196
|
+
{ spellId: 'fireball', level: 3, uses: 1, recharge: 'dawn' },
|
|
2197
|
+
{ spellId: 'shield', level: 1, uses: 1, recharge: 'dawn' }
|
|
2198
|
+
],
|
|
2199
|
+
spawnWeight: 0.3,
|
|
2200
|
+
source: 'custom',
|
|
2201
|
+
tags: ['magic', 'ring', 'spell']
|
|
2202
|
+
}
|
|
2203
|
+
];
|
|
2204
|
+
|
|
2205
|
+
// ===== STEP 2: Register Equipment =====
|
|
2206
|
+
const manager = ExtensionManager.getInstance();
|
|
2207
|
+
manager.register('equipment', customItems, {
|
|
2208
|
+
mode: 'relative',
|
|
2209
|
+
validate: true
|
|
2210
|
+
});
|
|
2211
|
+
|
|
2212
|
+
// ===== STEP 3: Spawn Custom Items =====
|
|
2213
|
+
const rng = new SeededRNG('custom_loot');
|
|
2214
|
+
const customLoot = EquipmentSpawnHelper.spawnByTags(['custom', 'magic'], 3, rng);
|
|
2215
|
+
|
|
2216
|
+
// ===== STEP 4: Add to Character =====
|
|
2217
|
+
const character = CharacterGenerator.generate(seed, audio, track);
|
|
2218
|
+
character = EquipmentSpawnHelper.addToCharacter(character, customLoot, true);
|
|
2219
|
+
|
|
2220
|
+
// ===== STEP 5: Enchant Items During Gameplay =====
|
|
2221
|
+
function enchantItem(character: CharacterSheet, itemName: string, enchantmentLevel: number) {
|
|
2222
|
+
const enchantment: EquipmentModification = {
|
|
2223
|
+
id: `enchant_${itemName}_${Date.now()}`,
|
|
2224
|
+
name: `+${enchantmentLevel} Enhancement`,
|
|
2225
|
+
properties: [
|
|
2226
|
+
{
|
|
2227
|
+
type: 'passive_modifier',
|
|
2228
|
+
target: 'attack_roll',
|
|
2229
|
+
value: enchantmentLevel,
|
|
2230
|
+
description: `+${enchantmentLevel} to attack rolls`
|
|
2231
|
+
}
|
|
2232
|
+
],
|
|
2233
|
+
appliedAt: new Date().toISOString(),
|
|
2234
|
+
source: 'enchanting'
|
|
2235
|
+
};
|
|
2236
|
+
|
|
2237
|
+
character.equipment = EquipmentModifier.enchant(
|
|
2238
|
+
character.equipment!,
|
|
2239
|
+
itemName,
|
|
2240
|
+
enchantment,
|
|
2241
|
+
character
|
|
2242
|
+
);
|
|
2243
|
+
|
|
2244
|
+
console.log(`${itemName} is now +${enchantmentLevel}!`);
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// ===== STEP 6: Quest Rewards =====
|
|
2248
|
+
function awardQuestReward(character: CharacterSheet) {
|
|
2249
|
+
// Spawn a rare item as quest reward
|
|
2250
|
+
const reward = EquipmentSpawnHelper.spawnByRarity('rare', 1, new SeededRNG('quest'));
|
|
2251
|
+
if (reward.length > 0) {
|
|
2252
|
+
character = EquipmentSpawnHelper.addToCharacter(character, reward, false);
|
|
2253
|
+
console.log(`Quest complete! You received: ${reward[0].name}`);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// ===== STEP 7: Boss Drops =====
|
|
2258
|
+
function bossLoot(character: CharacterSheet, bossCR: number) {
|
|
2259
|
+
// Spawn treasure hoard (see Example 3 for full spawnTreasureHoard usage)
|
|
2260
|
+
const hoard = EquipmentSpawnHelper.spawnTreasureHoard(
|
|
2261
|
+
bossCR,
|
|
2262
|
+
new SeededRNG(`boss_${Date.now()}`)
|
|
2263
|
+
);
|
|
2264
|
+
character = EquipmentSpawnHelper.addToCharacter(character, hoard.items, false);
|
|
2265
|
+
console.log(`Boss defeated! Found ${hoard.items.length} items worth ~${hoard.totalValue} gp`);
|
|
2266
|
+
}
|
|
2267
|
+
```
|
|
2268
|
+
|
|
2269
|
+
---
|
|
2270
|
+
|
|
2271
|
+
## Related Documentation
|
|
2272
|
+
|
|
2273
|
+
- [DATA_ENGINE_REFERENCE.md](../DATA_ENGINE_REFERENCE.md) - Complete API reference
|
|
2274
|
+
- [USAGE_IN_OTHER_PROJECTS.md](../USAGE_IN_OTHER_PROJECTS.md) - Usage examples
|
|
2275
|
+
- [EXTENSIBILITY_GUIDE.md](EXTENSIBILITY_GUIDE.md) - Custom content registration and spawn rates
|
|
2276
|
+
- [XP_AND_STATS.md](XP_AND_STATS.md) - Progression, stat increases, and level-ups
|
|
2277
|
+
- [PREREQUISITES.md](PREREQUISITES.md) - Level, ability, and skill requirements
|
|
2278
|
+
- [specs/001-core-engine/SPEC.md](../specs/001-core-engine/SPEC.md) - Core engine specification
|
|
2279
|
+
|