WIP: add ttrpg-encounter-builder skill + server-mode API docs
UNTESTED work in progress. Do not assume correct. Scope: SERVER BACKEND ONLY (SQLite/Express, REACT_APP_STORAGE=server). Does NOT work with Firebase SDK mode — no HTTP backend there, different transport/auth. Skill + doc explicitly call this out. - .agents/skills/ttrpg-encounter-builder/: harness-agnostic skill (pi/claude/codex via .agents/skills + symlinks). SKILL.md + helper script that batch-writes encounters via REST, rolls initiative, verifies. - docs/ENCOUNTER_BUILDER.md: add Path normalization section, Build flow (API/scripts) section with REST endpoint table, object templates, recipe. Server-mode-only caveat noted. Helper script syntax-checked + campaign lookup verified against running instance, but full seed flow not regression-tested against test suite.
This commit is contained in:
@@ -64,6 +64,15 @@ Object in `encounter.participants[]`:
|
||||
| `deathSaveSuccesses` | int | character/NPC only, 0-3 successes |
|
||||
| `deathSaveFailures` | int | character/NPC only, 0-3 failures |
|
||||
|
||||
## Path normalization
|
||||
|
||||
App passes firebase-prefixed paths (`artifacts/{APP_ID}/public/data/campaigns/...`). Server-mode adapter `norm()` strips prefix → bare canonical (`campaigns/...`).
|
||||
|
||||
- **UI / app code** → prefixed paths (`getPath.*` helpers)
|
||||
- **REST API / direct DB / scripts** → bare canonical paths (`campaigns/...`)
|
||||
|
||||
Mixing = empty results. If `GET /api/collection?path=artifacts/.../campaigns` returns `[]`, you used the prefixed form. Drop the prefix.
|
||||
|
||||
## Build flow (UI)
|
||||
|
||||
Admin view at `/`. Steps:
|
||||
@@ -121,6 +130,101 @@ Pre-combat only (`!isStarted || isPaused`). Drag handles shown for **tied initia
|
||||
|
||||
During active combat, cross-current-pointer drag is blocked to avoid skip/double-act ambiguity. Paused combat can reorder same-initiative ties freely.
|
||||
|
||||
## Build flow (API / scripts)
|
||||
|
||||
Same logical steps as UI, via REST. For LLM agents or seed scripts.
|
||||
|
||||
**Server mode only.** REST endpoints (`/api/*`) exist only when tracker runs
|
||||
with `REACT_APP_STORAGE=server` (bundled Express backend mounted). Firebase
|
||||
SDK mode has no HTTP backend — data lives in Firestore via in-browser SDK,
|
||||
different auth + write paths. This section does not apply to firebase mode.
|
||||
|
||||
Confirm server mode before scripting:
|
||||
```bash
|
||||
curl -s <HOST>/api/collection?path=campaigns | head -c 200
|
||||
```
|
||||
JSON array = server mode. HTML (SPA shell) = firebase mode or backend not
|
||||
mounted — API path won't work.
|
||||
|
||||
### REST endpoints
|
||||
|
||||
| Method | Path | Body / Query | Action |
|
||||
|---|---|---|---|
|
||||
| `GET` | `/api/doc` | `?path=` | read one doc |
|
||||
| `PUT` | `/api/doc` | `{path, data}` | replace doc |
|
||||
| `PATCH` | `/api/doc` | `{path, patch}` | shallow merge, create-on-miss |
|
||||
| `DELETE` | `/api/doc` | `?path=` | delete one doc |
|
||||
| `GET` | `/api/collection` | `?path=` | list docs under collection |
|
||||
| `POST` | `/api/collection` | `{path, data}` | addDoc: auto-id under collection |
|
||||
| `POST` | `/api/batch` | `{ops:[{type:'set'\|'update'\|'delete', path, data?}]}` | atomic multi-write |
|
||||
|
||||
All paths bare canonical (no `artifacts/...` prefix).
|
||||
|
||||
### id format
|
||||
|
||||
`generateId()` = `crypto.randomUUID()` (fallback `id_{Date.now()}_{rand10}`). Use either. Server `POST /api/collection` generates its own UUID — fine for campaigns/encounters. Participants are inline array items; generate their ids client-side so they're stable across batch writes.
|
||||
|
||||
### Object templates
|
||||
|
||||
Encounter doc (full, for `PUT` / batch `set`):
|
||||
```js
|
||||
{
|
||||
name: 'Barrowdeep 1 - Antechamber',
|
||||
createdAt: new Date().toISOString(),
|
||||
participants: [], // inline, NOT a sub-collection
|
||||
round: 0,
|
||||
currentTurnParticipantId: null,
|
||||
isStarted: false,
|
||||
isPaused: false,
|
||||
ruleset: '5e',
|
||||
}
|
||||
```
|
||||
|
||||
Participant object (item in `encounter.participants[]`):
|
||||
```js
|
||||
{
|
||||
id: generateId(),
|
||||
name: 'Ankheg',
|
||||
type: 'monster', // 'character' | 'npc' | 'monster'
|
||||
originalCharacterId: null, // set only if type==='character'
|
||||
initiative: rollD20() + initMod, // rolled ONCE at add; stored, not re-derived
|
||||
maxHp: 45,
|
||||
currentHp: 45, // start at full
|
||||
conditions: [],
|
||||
isActive: true,
|
||||
status: 'conscious',
|
||||
deathSaveSuccesses: 0, // character/npc only
|
||||
deathSaveFailures: 0, // character/npc only
|
||||
}
|
||||
```
|
||||
|
||||
### Initiative semantics
|
||||
|
||||
- Default: `initiative = rollD20() + initMod` (d20 ∈ [1,20], roll fresh per participant).
|
||||
- Manual override (e.g. pre-rolled values): set `initiative` directly, skip the roll.
|
||||
- Fixed at add time. NOT re-derived from `initMod` after — `initMod` is not stored on participant.
|
||||
|
||||
### Recipe: build encounters for a campaign
|
||||
|
||||
1. **Find campaign id by name:**
|
||||
```bash
|
||||
curl -s "$HOST/api/collection?path=campaigns" \
|
||||
| jq -r '.[] | select(.name=="2026 D&D") | .id'
|
||||
```
|
||||
2. **Build encounter docs + participants client-side** (object templates above), roll initiative per participant.
|
||||
3. **Write all encounters in one batch:**
|
||||
```bash
|
||||
curl -s -X POST "$HOST/api/batch" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"ops":[{"type":"set","path":"campaigns/CID/encounters/EID1","data":{...}},{"type":"set","path":"campaigns/CID/encounters/EID2","data":{...}}]}'
|
||||
```
|
||||
4. **Verify:**
|
||||
```bash
|
||||
curl -s "$HOST/api/collection?path=campaigns/CID/encounters" | jq '.[] | {name, count: (.participants|length)}'
|
||||
```
|
||||
|
||||
Participants go inline in the encounter doc — there is no `participants` sub-collection. A single batch `set` per encounter writes the whole roster atomically.
|
||||
|
||||
## Combat flow (UI)
|
||||
|
||||
InitiativeControls panel (sticky, right side).
|
||||
@@ -198,6 +302,8 @@ No re-sort after `startEncounter`.
|
||||
|
||||
## Storage paths quick reference
|
||||
|
||||
Bare canonical (server mode). App code prefixes these with `artifacts/{APP_ID}/public/data/` — adapter strips it.
|
||||
|
||||
```
|
||||
campaigns/{cid} campaign doc
|
||||
campaigns/{cid}/encounters/{eid} encounter doc (participants[])
|
||||
@@ -206,6 +312,8 @@ activeDisplay/status player display control
|
||||
logs/{logId} action log entry
|
||||
```
|
||||
|
||||
REST mapping: `campaigns/{cid}` doc → `GET/PUT/PATCH/DELETE /api/doc?path=campaigns/{cid}`. Collection parent (`campaigns`, `campaigns/{cid}/encounters`) → `GET/POST /api/collection?path=...`.
|
||||
|
||||
## DM tips
|
||||
|
||||
- Initiative rolled ONCE at add time. Stored. Edit via EditParticipantModal to override.
|
||||
|
||||
Reference in New Issue
Block a user