ttrpg-engine-dnd 0.1.0-alpha.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/CHANGELOG.md +69 -0
- package/CONTRIBUTING.md +98 -0
- package/DEVELOPMENT.md +70 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/VERSIONING.md +151 -0
- package/dist/content/index.d.ts +3 -0
- package/dist/content/index.d.ts.map +1 -0
- package/dist/content/pack.d.ts +1657 -0
- package/dist/content/pack.d.ts.map +1 -0
- package/dist/content/packs/starter.d.ts +4 -0
- package/dist/content/packs/starter.d.ts.map +1 -0
- package/dist/content/validate.d.ts +8 -0
- package/dist/content/validate.d.ts.map +1 -0
- package/dist/derive/ability-check.d.ts +26 -0
- package/dist/derive/ability-check.d.ts.map +1 -0
- package/dist/derive/ability.d.ts +9 -0
- package/dist/derive/ability.d.ts.map +1 -0
- package/dist/derive/ac.d.ts +19 -0
- package/dist/derive/ac.d.ts.map +1 -0
- package/dist/derive/action-economy.d.ts +17 -0
- package/dist/derive/action-economy.d.ts.map +1 -0
- package/dist/derive/attack.d.ts +20 -0
- package/dist/derive/attack.d.ts.map +1 -0
- package/dist/derive/character-view.d.ts +29 -0
- package/dist/derive/character-view.d.ts.map +1 -0
- package/dist/derive/damage-mitigation.d.ts +18 -0
- package/dist/derive/damage-mitigation.d.ts.map +1 -0
- package/dist/derive/effect-stack.d.ts +15 -0
- package/dist/derive/effect-stack.d.ts.map +1 -0
- package/dist/derive/encumbrance.d.ts +17 -0
- package/dist/derive/encumbrance.d.ts.map +1 -0
- package/dist/derive/index.d.ts +12 -0
- package/dist/derive/index.d.ts.map +1 -0
- package/dist/derive/save.d.ts +23 -0
- package/dist/derive/save.d.ts.map +1 -0
- package/dist/derive/spell-dc.d.ts +21 -0
- package/dist/derive/spell-dc.d.ts.map +1 -0
- package/dist/derive/spell-slots.d.ts +21 -0
- package/dist/derive/spell-slots.d.ts.map +1 -0
- package/dist/derive/terrain.d.ts +10 -0
- package/dist/derive/terrain.d.ts.map +1 -0
- package/dist/effects/builder.d.ts +66 -0
- package/dist/effects/builder.d.ts.map +1 -0
- package/dist/effects/formula.d.ts +12 -0
- package/dist/effects/formula.d.ts.map +1 -0
- package/dist/effects/index.d.ts +4 -0
- package/dist/effects/index.d.ts.map +1 -0
- package/dist/effects/predicate.d.ts +12 -0
- package/dist/effects/predicate.d.ts.map +1 -0
- package/dist/engine/apply.d.ts +5 -0
- package/dist/engine/apply.d.ts.map +1 -0
- package/dist/engine/commit.d.ts +12 -0
- package/dist/engine/commit.d.ts.map +1 -0
- package/dist/engine/conveniences.d.ts +7124 -0
- package/dist/engine/conveniences.d.ts.map +1 -0
- package/dist/engine/ids-utils.d.ts +2 -0
- package/dist/engine/ids-utils.d.ts.map +1 -0
- package/dist/engine/index.d.ts +107 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/plan/action-surge.d.ts +10 -0
- package/dist/engine/plan/action-surge.d.ts.map +1 -0
- package/dist/engine/plan/attack.d.ts +30 -0
- package/dist/engine/plan/attack.d.ts.map +1 -0
- package/dist/engine/plan/cast-spell.d.ts +18 -0
- package/dist/engine/plan/cast-spell.d.ts.map +1 -0
- package/dist/engine/plan/checks.d.ts +26 -0
- package/dist/engine/plan/checks.d.ts.map +1 -0
- package/dist/engine/plan/concentration.d.ts +12 -0
- package/dist/engine/plan/concentration.d.ts.map +1 -0
- package/dist/engine/plan/contested.d.ts +28 -0
- package/dist/engine/plan/contested.d.ts.map +1 -0
- package/dist/engine/plan/encounter.d.ts +47 -0
- package/dist/engine/plan/encounter.d.ts.map +1 -0
- package/dist/engine/plan/falling.d.ts +11 -0
- package/dist/engine/plan/falling.d.ts.map +1 -0
- package/dist/engine/plan/index.d.ts +20 -0
- package/dist/engine/plan/index.d.ts.map +1 -0
- package/dist/engine/plan/level-up.d.ts +22 -0
- package/dist/engine/plan/level-up.d.ts.map +1 -0
- package/dist/engine/plan/movement.d.ts +25 -0
- package/dist/engine/plan/movement.d.ts.map +1 -0
- package/dist/engine/plan/multiattack.d.ts +12 -0
- package/dist/engine/plan/multiattack.d.ts.map +1 -0
- package/dist/engine/plan/npc.d.ts +21 -0
- package/dist/engine/plan/npc.d.ts.map +1 -0
- package/dist/engine/plan/offhand-attack.d.ts +13 -0
- package/dist/engine/plan/offhand-attack.d.ts.map +1 -0
- package/dist/engine/plan/opportunity-attack.d.ts +14 -0
- package/dist/engine/plan/opportunity-attack.d.ts.map +1 -0
- package/dist/engine/plan/reactive-spells.d.ts +34 -0
- package/dist/engine/plan/reactive-spells.d.ts.map +1 -0
- package/dist/engine/plan/rest.d.ts +16 -0
- package/dist/engine/plan/rest.d.ts.map +1 -0
- package/dist/engine/plan/travel.d.ts +21 -0
- package/dist/engine/plan/travel.d.ts.map +1 -0
- package/dist/engine/plan/weapon-mastery.d.ts +15 -0
- package/dist/engine/plan/weapon-mastery.d.ts.map +1 -0
- package/dist/engine/reducers/action-economy.d.ts +12 -0
- package/dist/engine/reducers/action-economy.d.ts.map +1 -0
- package/dist/engine/reducers/attack.d.ts +6 -0
- package/dist/engine/reducers/attack.d.ts.map +1 -0
- package/dist/engine/reducers/bastion.d.ts +10 -0
- package/dist/engine/reducers/bastion.d.ts.map +1 -0
- package/dist/engine/reducers/charges.d.ts +7 -0
- package/dist/engine/reducers/charges.d.ts.map +1 -0
- package/dist/engine/reducers/checks.d.ts +6 -0
- package/dist/engine/reducers/checks.d.ts.map +1 -0
- package/dist/engine/reducers/combat.d.ts +12 -0
- package/dist/engine/reducers/combat.d.ts.map +1 -0
- package/dist/engine/reducers/concentration.d.ts +6 -0
- package/dist/engine/reducers/concentration.d.ts.map +1 -0
- package/dist/engine/reducers/downtime.d.ts +5 -0
- package/dist/engine/reducers/downtime.d.ts.map +1 -0
- package/dist/engine/reducers/encounter.d.ts +11 -0
- package/dist/engine/reducers/encounter.d.ts.map +1 -0
- package/dist/engine/reducers/inventory.d.ts +9 -0
- package/dist/engine/reducers/inventory.d.ts.map +1 -0
- package/dist/engine/reducers/level-up.d.ts +7 -0
- package/dist/engine/reducers/level-up.d.ts.map +1 -0
- package/dist/engine/reducers/locations.d.ts +8 -0
- package/dist/engine/reducers/locations.d.ts.map +1 -0
- package/dist/engine/reducers/mounts-vehicles.d.ts +11 -0
- package/dist/engine/reducers/mounts-vehicles.d.ts.map +1 -0
- package/dist/engine/reducers/movement.d.ts +7 -0
- package/dist/engine/reducers/movement.d.ts.map +1 -0
- package/dist/engine/reducers/npc.d.ts +7 -0
- package/dist/engine/reducers/npc.d.ts.map +1 -0
- package/dist/engine/reducers/party.d.ts +10 -0
- package/dist/engine/reducers/party.d.ts.map +1 -0
- package/dist/engine/reducers/progression.d.ts +5 -0
- package/dist/engine/reducers/progression.d.ts.map +1 -0
- package/dist/engine/reducers/quests.d.ts +14 -0
- package/dist/engine/reducers/quests.d.ts.map +1 -0
- package/dist/engine/reducers/reactive-spells.d.ts +7 -0
- package/dist/engine/reducers/reactive-spells.d.ts.map +1 -0
- package/dist/engine/reducers/resources.d.ts +7 -0
- package/dist/engine/reducers/resources.d.ts.map +1 -0
- package/dist/engine/reducers/rest.d.ts +8 -0
- package/dist/engine/reducers/rest.d.ts.map +1 -0
- package/dist/engine/reducers/resurrection.d.ts +5 -0
- package/dist/engine/reducers/resurrection.d.ts.map +1 -0
- package/dist/engine/reducers/session.d.ts +8 -0
- package/dist/engine/reducers/session.d.ts.map +1 -0
- package/dist/engine/reducers/settings.d.ts +5 -0
- package/dist/engine/reducers/settings.d.ts.map +1 -0
- package/dist/engine/reducers/spellcasting.d.ts +7 -0
- package/dist/engine/reducers/spellcasting.d.ts.map +1 -0
- package/dist/engine/reducers/transformations.d.ts +8 -0
- package/dist/engine/reducers/transformations.d.ts.map +1 -0
- package/dist/engine/reducers/travel.d.ts +7 -0
- package/dist/engine/reducers/travel.d.ts.map +1 -0
- package/dist/engine/reducers/triggers.d.ts +9 -0
- package/dist/engine/reducers/triggers.d.ts.map +1 -0
- package/dist/engine/reducers/weapon-mastery.d.ts +5 -0
- package/dist/engine/reducers/weapon-mastery.d.ts.map +1 -0
- package/dist/engine/replay.d.ts +4 -0
- package/dist/engine/replay.d.ts.map +1 -0
- package/dist/engine/triggers/dispatch.d.ts +13 -0
- package/dist/engine/triggers/dispatch.d.ts.map +1 -0
- package/dist/engine/undo-redo.d.ts +4 -0
- package/dist/engine/undo-redo.d.ts.map +1 -0
- package/dist/handlers/context.d.ts +7 -0
- package/dist/handlers/context.d.ts.map +1 -0
- package/dist/handlers/index.d.ts +12 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/ids.d.ts +64 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/internal/clock.d.ts +2 -0
- package/dist/internal/clock.d.ts.map +1 -0
- package/dist/internal/constants.d.ts +6 -0
- package/dist/internal/constants.d.ts.map +1 -0
- package/dist/internal/immer.d.ts +4 -0
- package/dist/internal/immer.d.ts.map +1 -0
- package/dist/internal/invariants.d.ts +5 -0
- package/dist/internal/invariants.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +5 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/rng/default.d.ts +6 -0
- package/dist/rng/default.d.ts.map +1 -0
- package/dist/rng/dice.d.ts +20 -0
- package/dist/rng/dice.d.ts.map +1 -0
- package/dist/rng/index.d.ts +10 -0
- package/dist/rng/index.d.ts.map +1 -0
- package/dist/rng/seeded.d.ts +9 -0
- package/dist/rng/seeded.d.ts.map +1 -0
- package/dist/rng/throw.d.ts +9 -0
- package/dist/rng/throw.d.ts.map +1 -0
- package/dist/schemas/content/background.d.ts +46 -0
- package/dist/schemas/content/background.d.ts.map +1 -0
- package/dist/schemas/content/class.d.ts +264 -0
- package/dist/schemas/content/class.d.ts.map +1 -0
- package/dist/schemas/content/condition.d.ts +90 -0
- package/dist/schemas/content/condition.d.ts.map +1 -0
- package/dist/schemas/content/feat.d.ts +25 -0
- package/dist/schemas/content/feat.d.ts.map +1 -0
- package/dist/schemas/content/index.d.ts +9 -0
- package/dist/schemas/content/index.d.ts.map +1 -0
- package/dist/schemas/content/item.d.ts +602 -0
- package/dist/schemas/content/item.d.ts.map +1 -0
- package/dist/schemas/content/monster.d.ts +203 -0
- package/dist/schemas/content/monster.d.ts.map +1 -0
- package/dist/schemas/content/species.d.ts +63 -0
- package/dist/schemas/content/species.d.ts.map +1 -0
- package/dist/schemas/content/spell.d.ts +253 -0
- package/dist/schemas/content/spell.d.ts.map +1 -0
- package/dist/schemas/effects.d.ts +175 -0
- package/dist/schemas/effects.d.ts.map +1 -0
- package/dist/schemas/events/action-economy.d.ts +38 -0
- package/dist/schemas/events/action-economy.d.ts.map +1 -0
- package/dist/schemas/events/attack.d.ts +139 -0
- package/dist/schemas/events/attack.d.ts.map +1 -0
- package/dist/schemas/events/bastion.d.ts +227 -0
- package/dist/schemas/events/bastion.d.ts.map +1 -0
- package/dist/schemas/events/charges.d.ts +110 -0
- package/dist/schemas/events/charges.d.ts.map +1 -0
- package/dist/schemas/events/checks.d.ts +103 -0
- package/dist/schemas/events/checks.d.ts.map +1 -0
- package/dist/schemas/events/combat.d.ts +308 -0
- package/dist/schemas/events/combat.d.ts.map +1 -0
- package/dist/schemas/events/concentration.d.ts +99 -0
- package/dist/schemas/events/concentration.d.ts.map +1 -0
- package/dist/schemas/events/downtime.d.ts +53 -0
- package/dist/schemas/events/downtime.d.ts.map +1 -0
- package/dist/schemas/events/encounter.d.ts +260 -0
- package/dist/schemas/events/encounter.d.ts.map +1 -0
- package/dist/schemas/events/envelope.d.ts +22 -0
- package/dist/schemas/events/envelope.d.ts.map +1 -0
- package/dist/schemas/events/index.d.ts +4594 -0
- package/dist/schemas/events/index.d.ts.map +1 -0
- package/dist/schemas/events/inventory.d.ts +253 -0
- package/dist/schemas/events/inventory.d.ts.map +1 -0
- package/dist/schemas/events/level-up.d.ts +141 -0
- package/dist/schemas/events/level-up.d.ts.map +1 -0
- package/dist/schemas/events/locations.d.ts +183 -0
- package/dist/schemas/events/locations.d.ts.map +1 -0
- package/dist/schemas/events/mounts-vehicles.d.ts +233 -0
- package/dist/schemas/events/mounts-vehicles.d.ts.map +1 -0
- package/dist/schemas/events/movement.d.ts +131 -0
- package/dist/schemas/events/movement.d.ts.map +1 -0
- package/dist/schemas/events/npc.d.ts +113 -0
- package/dist/schemas/events/npc.d.ts.map +1 -0
- package/dist/schemas/events/party.d.ts +260 -0
- package/dist/schemas/events/party.d.ts.map +1 -0
- package/dist/schemas/events/progression.d.ts +698 -0
- package/dist/schemas/events/progression.d.ts.map +1 -0
- package/dist/schemas/events/quests.d.ts +426 -0
- package/dist/schemas/events/quests.d.ts.map +1 -0
- package/dist/schemas/events/reactive-spells.d.ts +98 -0
- package/dist/schemas/events/reactive-spells.d.ts.map +1 -0
- package/dist/schemas/events/resources.d.ts +107 -0
- package/dist/schemas/events/resources.d.ts.map +1 -0
- package/dist/schemas/events/rest.d.ts +104 -0
- package/dist/schemas/events/rest.d.ts.map +1 -0
- package/dist/schemas/events/resurrection.d.ts +44 -0
- package/dist/schemas/events/resurrection.d.ts.map +1 -0
- package/dist/schemas/events/session.d.ts +144 -0
- package/dist/schemas/events/session.d.ts.map +1 -0
- package/dist/schemas/events/settings.d.ts +47 -0
- package/dist/schemas/events/settings.d.ts.map +1 -0
- package/dist/schemas/events/spellcasting.d.ts +103 -0
- package/dist/schemas/events/spellcasting.d.ts.map +1 -0
- package/dist/schemas/events/transformations.d.ts +279 -0
- package/dist/schemas/events/transformations.d.ts.map +1 -0
- package/dist/schemas/events/travel.d.ts +143 -0
- package/dist/schemas/events/travel.d.ts.map +1 -0
- package/dist/schemas/events/triggers.d.ts +60 -0
- package/dist/schemas/events/triggers.d.ts.map +1 -0
- package/dist/schemas/events/weapon-mastery.d.ts +38 -0
- package/dist/schemas/events/weapon-mastery.d.ts.map +1 -0
- package/dist/schemas/formula.d.ts +103 -0
- package/dist/schemas/formula.d.ts.map +1 -0
- package/dist/schemas/index.d.ts +8 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/predicate.d.ts +72 -0
- package/dist/schemas/predicate.d.ts.map +1 -0
- package/dist/schemas/primitives.d.ts +156 -0
- package/dist/schemas/primitives.d.ts.map +1 -0
- package/dist/schemas/runtime/bastion.d.ts +130 -0
- package/dist/schemas/runtime/bastion.d.ts.map +1 -0
- package/dist/schemas/runtime/campaign.d.ts +2122 -0
- package/dist/schemas/runtime/campaign.d.ts.map +1 -0
- package/dist/schemas/runtime/character.d.ts +580 -0
- package/dist/schemas/runtime/character.d.ts.map +1 -0
- package/dist/schemas/runtime/currency.d.ts +9 -0
- package/dist/schemas/runtime/currency.d.ts.map +1 -0
- package/dist/schemas/runtime/downtime.d.ts +31 -0
- package/dist/schemas/runtime/downtime.d.ts.map +1 -0
- package/dist/schemas/runtime/effect-instance.d.ts +65 -0
- package/dist/schemas/runtime/effect-instance.d.ts.map +1 -0
- package/dist/schemas/runtime/encounter.d.ts +264 -0
- package/dist/schemas/runtime/encounter.d.ts.map +1 -0
- package/dist/schemas/runtime/in-game-time.d.ts +18 -0
- package/dist/schemas/runtime/in-game-time.d.ts.map +1 -0
- package/dist/schemas/runtime/index.d.ts +15 -0
- package/dist/schemas/runtime/index.d.ts.map +1 -0
- package/dist/schemas/runtime/item-instance.d.ts +66 -0
- package/dist/schemas/runtime/item-instance.d.ts.map +1 -0
- package/dist/schemas/runtime/location.d.ts +111 -0
- package/dist/schemas/runtime/location.d.ts.map +1 -0
- package/dist/schemas/runtime/party.d.ts +52 -0
- package/dist/schemas/runtime/party.d.ts.map +1 -0
- package/dist/schemas/runtime/pending-choice.d.ts +77 -0
- package/dist/schemas/runtime/pending-choice.d.ts.map +1 -0
- package/dist/schemas/runtime/quest.d.ts +207 -0
- package/dist/schemas/runtime/quest.d.ts.map +1 -0
- package/dist/schemas/runtime/session.d.ts +102 -0
- package/dist/schemas/runtime/session.d.ts.map +1 -0
- package/dist/schemas/runtime/settings.d.ts +26 -0
- package/dist/schemas/runtime/settings.d.ts.map +1 -0
- package/dist/schemas/runtime/travel.d.ts +34 -0
- package/dist/schemas/runtime/travel.d.ts.map +1 -0
- package/dist/schemas/runtime/vehicle.d.ts +49 -0
- package/dist/schemas/runtime/vehicle.d.ts.map +1 -0
- package/dist/ttrpg-engine-dnd.cjs +6 -0
- package/dist/ttrpg-engine-dnd.cjs.map +1 -0
- package/dist/ttrpg-engine-dnd.js +10464 -0
- package/dist/ttrpg-engine-dnd.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/docs/api-overview.md +111 -0
- package/docs/concepts.md +154 -0
- package/docs/getting-started.md +142 -0
- package/docs/recipes.md +302 -0
- package/package.json +83 -0
package/docs/recipes.md
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# Recipes
|
|
2
|
+
|
|
3
|
+
Common patterns. Skim for the one you need. Each recipe is self-contained.
|
|
4
|
+
|
|
5
|
+
For background on why the API is shaped this way, see [docs/concepts.md](concepts.md).
|
|
6
|
+
|
|
7
|
+
## Save mid-encounter
|
|
8
|
+
|
|
9
|
+
The event log is the durable artifact. Save it to disk, a database, or a query string. Replay reconstructs everything.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { serializeCampaign, loadCampaign } from 'ttrpg-engine-dnd';
|
|
13
|
+
|
|
14
|
+
// Save
|
|
15
|
+
const json = serializeCampaign(campaign);
|
|
16
|
+
fs.writeFileSync('save.json', json);
|
|
17
|
+
|
|
18
|
+
// Load
|
|
19
|
+
const restored = loadCampaign(fs.readFileSync('save.json', 'utf8'));
|
|
20
|
+
// restored.state deep-equals the campaign at save time.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
There is no "save state" step. State is computed; events are the truth.
|
|
24
|
+
|
|
25
|
+
## Undo and redo
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { undo, redo } from 'ttrpg-engine-dnd';
|
|
29
|
+
|
|
30
|
+
campaign = undo(campaign); // moves the cursor back one event; state recomputes
|
|
31
|
+
campaign = redo(campaign); // moves it forward
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Committing new events after an undo discards the redo tail (standard text-editor semantics). If you want branching timelines, fork the events array instead.
|
|
35
|
+
|
|
36
|
+
## Branch the timeline ("what if...")
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { replay } from 'ttrpg-engine-dnd';
|
|
40
|
+
|
|
41
|
+
const hypotheticalEvents = [...campaign.events, ...newPlannedEvents];
|
|
42
|
+
const hypotheticalState = replay(hypotheticalEvents);
|
|
43
|
+
// Inspect hypotheticalState without touching campaign.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This is how you'd implement an AI DM evaluating "what happens if I cast Fireball here?" without committing the action.
|
|
47
|
+
|
|
48
|
+
## Add content to the starter pack
|
|
49
|
+
|
|
50
|
+
The starter pack is JSON. The cleanest extension is to load it alongside your own pack:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { loadStarterPack, loadContentPack, resolveContent } from 'ttrpg-engine-dnd';
|
|
54
|
+
|
|
55
|
+
const homebrew = loadContentPack({
|
|
56
|
+
id: 'my-homebrew',
|
|
57
|
+
name: 'My Homebrew',
|
|
58
|
+
version: '0.1.0',
|
|
59
|
+
spells: [
|
|
60
|
+
{
|
|
61
|
+
id: 'home-fire-arrow',
|
|
62
|
+
name: 'Fire Arrow',
|
|
63
|
+
level: 1,
|
|
64
|
+
school: 'evocation',
|
|
65
|
+
castingTime: 'Action',
|
|
66
|
+
range: '60 feet',
|
|
67
|
+
components: { verbal: true, somatic: true },
|
|
68
|
+
duration: 'Instantaneous',
|
|
69
|
+
concentration: false,
|
|
70
|
+
ritual: false,
|
|
71
|
+
classes: ['wizard', 'sorcerer'],
|
|
72
|
+
mechanicalEffects: [
|
|
73
|
+
{ kind: 'attack', attackKind: 'ranged', damageDice: '2d6', damageType: 'fire' },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const engine = createEngine({ contentPacks: [loadStarterPack(), homebrew] });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Later packs override earlier ones on ID conflicts. This is how you layer core + setting + table-specific homebrew.
|
|
83
|
+
|
|
84
|
+
For the schema shapes (which fields a `Spell` / `Feat` / `Class` accepts), see the Zod schemas under `src/schemas/content/` or the API reference.
|
|
85
|
+
|
|
86
|
+
## Add a houserule via campaign settings
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
campaign = commit(campaign, [
|
|
90
|
+
{
|
|
91
|
+
id: newEventId(),
|
|
92
|
+
at: new Date().toISOString(),
|
|
93
|
+
type: 'CampaignSettingsChanged',
|
|
94
|
+
grittyRest: true,
|
|
95
|
+
customHouserulesAdd: ['critical-fumble', 'inspiration-on-nat-1'],
|
|
96
|
+
},
|
|
97
|
+
]);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The engine doesn't enforce these flags automatically; consumers branch on `campaign.state.settings.grittyRest` in their own planner wrappers (e.g., to scale rest durations). The events and flags exist so the houserule choice is part of the campaign's auditable history.
|
|
101
|
+
|
|
102
|
+
## Add a new feat
|
|
103
|
+
|
|
104
|
+
Feats are content. Add them to your pack:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
{
|
|
108
|
+
feats: [
|
|
109
|
+
{
|
|
110
|
+
id: 'home-iron-stomach',
|
|
111
|
+
name: 'Iron Stomach',
|
|
112
|
+
category: 'general',
|
|
113
|
+
repeatable: false,
|
|
114
|
+
prerequisites: [],
|
|
115
|
+
effects: [
|
|
116
|
+
{ kind: 'GrantConditionImmunity', conditionId: 'poisoned' },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A character takes a feat by including its ID in `Character.featsTaken`. The engine's effect-stack builder walks `featsTaken` and applies the listed effects to derivations.
|
|
124
|
+
|
|
125
|
+
For feats that need code-handler logic (e.g., a triggered reaction that depends on game state in a way primitives can't express), use the `OnEvent` primitive when possible; for truly procedural feats, plan to extend the engine via the handler registry (this is the same path Wild Shape, Polymorph, and Wish take internally).
|
|
126
|
+
|
|
127
|
+
## Display a character sheet in a UI
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const sheet = engine.derive.character(campaign.state, characterId);
|
|
131
|
+
// sheet.hp.current, sheet.hp.max
|
|
132
|
+
// sheet.ac.total, sheet.ac.breakdown (each modifier and its source)
|
|
133
|
+
// sheet.savingThrows.STR.total, .breakdown
|
|
134
|
+
// sheet.spellSlots
|
|
135
|
+
// sheet.hasPendingChoices, sheet.pendingChoiceIds
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Derivations are memoized per `state.version` (every commit invalidates). Repeated calls at the same version return the same object reference, so this is safe to call as often as your UI needs.
|
|
139
|
+
|
|
140
|
+
For the weapon's attack bonus:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const ab = engine.derive.attackBonus(state, characterId, weaponInstanceId);
|
|
144
|
+
console.log(`+${ab.total} to hit`);
|
|
145
|
+
for (const entry of ab.breakdown) console.log(` ${entry.source}: ${entry.value}`);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Get the legal moves for a combatant
|
|
149
|
+
|
|
150
|
+
There isn't a single `legalMoves()` API; the engine instead exposes planners that fail loudly when called illegally. The common pattern is to try-call and catch:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const tryPlan = <T>(fn: () => T): T | undefined => {
|
|
154
|
+
try { return fn(); } catch { return undefined; }
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const canAttack = tryPlan(() =>
|
|
158
|
+
engine.plan.attack(state, { attackerId, targetId, weaponInstanceId }),
|
|
159
|
+
) !== undefined;
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
For action-economy-aware UI ("is the Action used? are attacks remaining?"), inspect the combatant directly:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const encounter = state.encounters[state.activeEncounterId!];
|
|
166
|
+
const me = encounter.combatants.find((c) => c.combatantId === myId);
|
|
167
|
+
if (!me?.turnUsage.actionUsed) {
|
|
168
|
+
// Action available
|
|
169
|
+
}
|
|
170
|
+
const budget = engine.derive.actionEconomyBudget?.(state, myId); // if exposed
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Run combat without an explicit encounter
|
|
174
|
+
|
|
175
|
+
Many planners work out of combat too. `plan.attack` doesn't require an active encounter; it just runs the attack chain. The action-economy guards only kick in when the attacker is the active combatant in an active encounter, so out-of-combat attacks are unmetered.
|
|
176
|
+
|
|
177
|
+
This is useful for sparring matches, narrative combat ("the assassin strikes from behind"), or testing.
|
|
178
|
+
|
|
179
|
+
## Stream events to a multiplayer peer
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
// Sender
|
|
183
|
+
const newEvents = engine.plan.attack(state, intent).events;
|
|
184
|
+
campaign = commit(campaign, newEvents);
|
|
185
|
+
socket.send(JSON.stringify({ events: newEvents }));
|
|
186
|
+
|
|
187
|
+
// Receiver
|
|
188
|
+
socket.on('message', (msg) => {
|
|
189
|
+
const { events } = JSON.parse(msg) as { events: unknown[] };
|
|
190
|
+
const validated = events.map((e) => EventSchema.parse(e));
|
|
191
|
+
campaign = commit(campaign, validated);
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Both sides must use compatible content packs and (for replay safety) the same seeded RNG decisions. The plan/commit split means the dice are already baked into the events; receivers don't need to re-roll.
|
|
196
|
+
|
|
197
|
+
## Migrate between schema versions
|
|
198
|
+
|
|
199
|
+
The engine persists with a `schemaVersion` (separate from package version). When you bump the persisted shape, write a migration:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
// src/migrations/v2.ts (consumer-side, if you fork)
|
|
203
|
+
export const migrateV1ToV2 = (state: V1State): V2State => { ... };
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`migrate(json)` walks all registered migrations forward. The MVP `migrations/v1.ts` is a no-op placeholder so the machinery is real, not vapor, when you need it for the first real bump.
|
|
207
|
+
|
|
208
|
+
## Validate a content pack before shipping
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { loadContentPack, resolveContent, validateCrossReferences, ContentPackLoadError } from 'ttrpg-engine-dnd';
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const pack = loadContentPack(json);
|
|
215
|
+
const content = resolveContent([pack]);
|
|
216
|
+
const issues = validateCrossReferences(content);
|
|
217
|
+
if (issues.length > 0) {
|
|
218
|
+
console.error('Content issues:');
|
|
219
|
+
for (const i of issues) {
|
|
220
|
+
console.error(` ${i.path}: ${i.message}${i.suggestion ? ` ${i.suggestion}` : ''}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch (e) {
|
|
224
|
+
if (e instanceof ContentPackLoadError) {
|
|
225
|
+
for (const i of e.issues) console.error(` ${i.path}: ${i.message}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Shape errors throw `ContentPackLoadError` with path-pointed issues. Cross-reference errors return Levenshtein-suggested fixes: a missing `origin-feat-id` typo'd as "savage-attackr" gets `Did you mean "savage-attacker"?`.
|
|
231
|
+
|
|
232
|
+
## Implement a custom planner
|
|
233
|
+
|
|
234
|
+
You can write your own planners that consume RNG and return events:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import type { CampaignState, Event } from 'ttrpg-engine-dnd';
|
|
238
|
+
import { newEventId } from 'ttrpg-engine-dnd';
|
|
239
|
+
|
|
240
|
+
export const planMyHomebrewAction = (
|
|
241
|
+
state: CampaignState,
|
|
242
|
+
rng: RNG,
|
|
243
|
+
intent: { characterId: string; ... },
|
|
244
|
+
): { events: ReadonlyArray<Event> } => {
|
|
245
|
+
// ... consume rng for any rolls ...
|
|
246
|
+
// ... build events whose reducers already exist ...
|
|
247
|
+
return { events: [...] };
|
|
248
|
+
};
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
If the events you emit are existing event types (DamageApplied, ConditionApplied, etc.), no engine extension is needed. If you need new event types, you'll need to extend `apply.ts` and add reducers (at that point you've forked the engine).
|
|
252
|
+
|
|
253
|
+
## Read a character's pending choices
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const character = state.characters[characterId];
|
|
257
|
+
for (const choiceId of character.pendingChoiceIds) {
|
|
258
|
+
const choice = state.pendingChoices[choiceId];
|
|
259
|
+
console.log(`${choice.kind}: ${choice.prompt}`);
|
|
260
|
+
for (const option of choice.options) {
|
|
261
|
+
console.log(` - ${option.label}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
To resolve a choice once the player picks:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
campaign = commit(
|
|
270
|
+
campaign,
|
|
271
|
+
engine.plan.resolveChoice(campaign.state, {
|
|
272
|
+
choiceId,
|
|
273
|
+
selectedOptionIds: [pickedOptionId],
|
|
274
|
+
}).events,
|
|
275
|
+
);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The chosen option's effects become active immediately (visible in the next derivation).
|
|
279
|
+
|
|
280
|
+
## Inspect an event log human-readably
|
|
281
|
+
|
|
282
|
+
The engine has a transcript formatter used by golden tests. It's not exported (it depends on test fixtures), but the shape is small and easy to lift:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
// Roughly:
|
|
286
|
+
const lines = campaign.events.map((event) => {
|
|
287
|
+
switch (event.type) {
|
|
288
|
+
case 'CharacterCreated': return `${event.snapshot.name} joined.`;
|
|
289
|
+
case 'AttackRolled': return `Attack: d20+${event.bonus} = ${event.total}.`;
|
|
290
|
+
// ...
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Or look at `tests/transcript.ts` for the full formatter as a reference implementation.
|
|
296
|
+
|
|
297
|
+
## Where to next
|
|
298
|
+
|
|
299
|
+
- **Conceptual orientation**: [docs/concepts.md](concepts.md).
|
|
300
|
+
- **API reference**: [docs/api-overview.md](api-overview.md).
|
|
301
|
+
- **Working examples**: [examples/](../examples/).
|
|
302
|
+
- **A full campaign in one transcript**: [tests/golden/transcripts/showcase.transcript.md](../tests/golden/transcripts/showcase.transcript.md).
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ttrpg-engine-dnd",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Standalone, event-sourced TypeScript domain engine for D&D 5.5e (2024 rules). Schema-only content model with effect-primitive vocabulary and code-handler escape hatch.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/ttrpg-engine-dnd.cjs",
|
|
7
|
+
"module": "./dist/ttrpg-engine-dnd.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/ttrpg-engine-dnd.js",
|
|
13
|
+
"require": "./dist/ttrpg-engine-dnd.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"docs",
|
|
19
|
+
"README.md",
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"VERSIONING.md",
|
|
22
|
+
"DEVELOPMENT.md",
|
|
23
|
+
"CONTRIBUTING.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "vite build && tsc --emitDeclarationOnly --project tsconfig.build.json",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"test:coverage": "vitest run --coverage",
|
|
33
|
+
"ci": "npm run typecheck && npm run test:coverage && npm run build",
|
|
34
|
+
"prepublishOnly": "npm run ci"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"tag": "alpha"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"immer": "^10.1.1",
|
|
42
|
+
"ulid": "^2.3.0",
|
|
43
|
+
"zod": "^3.23.8"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^25.7.0",
|
|
47
|
+
"@vitest/coverage-v8": "^1.6.0",
|
|
48
|
+
"fast-check": "^3.19.0",
|
|
49
|
+
"typescript": "^5.5.4",
|
|
50
|
+
"vite": "^5.4.0",
|
|
51
|
+
"vite-plugin-dts": "^4.0.0",
|
|
52
|
+
"vitest": "^1.6.0"
|
|
53
|
+
},
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"author": "Greg Carr",
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "git+https://github.com/greghcarr/ttrpg-engine-dnd.git"
|
|
59
|
+
},
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/greghcarr/ttrpg-engine-dnd/issues"
|
|
62
|
+
},
|
|
63
|
+
"homepage": "https://github.com/greghcarr/ttrpg-engine-dnd#readme",
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=18"
|
|
66
|
+
},
|
|
67
|
+
"keywords": [
|
|
68
|
+
"dnd",
|
|
69
|
+
"dungeons-and-dragons",
|
|
70
|
+
"5e",
|
|
71
|
+
"5.5e",
|
|
72
|
+
"2024",
|
|
73
|
+
"rules-engine",
|
|
74
|
+
"domain-engine",
|
|
75
|
+
"event-sourced",
|
|
76
|
+
"ttrpg",
|
|
77
|
+
"rpg",
|
|
78
|
+
"character-sheet",
|
|
79
|
+
"vtt",
|
|
80
|
+
"game-engine",
|
|
81
|
+
"typescript"
|
|
82
|
+
]
|
|
83
|
+
}
|