|
| 1 | +# Draw Steel System Module — Full Implementation Plan |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Make the Draw Steel game system a complete, self-contained, "containerized" system module |
| 6 | +for Chronicle — including website reference content, character/entity presets with full |
| 7 | +Foundry VTT field mappings, and all necessary infrastructure fixes. Everything should be |
| 8 | +dynamic and modular: a system manifest drives all behavior, and the Foundry module reads |
| 9 | +field definitions from the API. No hard-coded adapters. |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## Phase 1: Infrastructure Fixes (Bugs & Architecture) |
| 14 | + |
| 15 | +### 1A. CORS — Admin-managed origin whitelist |
| 16 | + |
| 17 | +**Problem:** `AllowedOrigins` in `app.go:137` is hardcoded to only `Config.BaseURL`. Foundry |
| 18 | +VTT (on a different port/origin) gets CORS-blocked. |
| 19 | + |
| 20 | +**Solution:** Store allowed CORS origins in the `site_settings` table (key: |
| 21 | +`cors.allowed_origins`). Admin can manage via the admin panel. The CORS middleware reads |
| 22 | +from DB on startup and reloads on change. |
| 23 | + |
| 24 | +**Files to modify:** |
| 25 | +- `internal/plugins/settings/model.go` — Add `KeyCORSAllowedOrigins` constant |
| 26 | +- `internal/plugins/settings/service.go` — Add `GetCORSOrigins()` / `UpdateCORSOrigins()` methods |
| 27 | +- `internal/plugins/settings/repository.go` — SQL for reading/writing the setting |
| 28 | +- `internal/plugins/settings/handler.go` — HTTP handler for CORS settings form |
| 29 | +- `internal/plugins/settings/routes.go` — Register new routes |
| 30 | +- `internal/plugins/admin/handler.go` — Add API settings page handler |
| 31 | +- `internal/plugins/admin/api_settings.templ` — **New file**: Admin API settings page with |
| 32 | + CORS origin whitelist management (add/remove origins) |
| 33 | +- `internal/middleware/cors.go` — Accept a `OriginProvider` function that returns current |
| 34 | + origins list (called per-request, cached briefly) |
| 35 | +- `internal/app/app.go` — Wire up CORS middleware with DB-backed origin provider that |
| 36 | + includes `Config.BaseURL` + DB origins |
| 37 | + |
| 38 | +### 1B. Foundry actor type — make it dynamic via manifest |
| 39 | + |
| 40 | +**Problem:** `actor-sync.mjs` hardcodes `actor.type !== 'character'` in multiple places. |
| 41 | +Draw Steel uses actor type `'hero'`, not `'character'`. This must be dynamic. |
| 42 | + |
| 43 | +**Solution:** Add `foundry_actor_type` field to manifest `EntityPresetDef` and export it |
| 44 | +via the character-fields API. The generic adapter and actor-sync use it instead of |
| 45 | +hardcoding `'character'`. |
| 46 | + |
| 47 | +**Files to modify:** |
| 48 | +- `internal/systems/manifest.go`: |
| 49 | + - Add `FoundryActorType string` to `EntityPresetDef` struct (`json:"foundry_actor_type,omitempty"`) |
| 50 | + - Add `FoundryActorType string` to `CharacterFieldsResponse` struct |
| 51 | + - Include it in `CharacterFieldsForAPI()` and `ItemFieldsForAPI()` output |
| 52 | +- `internal/systems/drawsteel/manifest.json` — Add `"foundry_actor_type": "hero"` to |
| 53 | + the character preset |
| 54 | +- `internal/systems/dnd5e/manifest.json` — Add `"foundry_actor_type": "character"` to |
| 55 | + character preset (explicit) |
| 56 | +- `internal/systems/pathfinder2e/manifest.json` — Add `"foundry_actor_type": "character"` |
| 57 | +- `foundry-module/scripts/adapters/generic-adapter.mjs` — Read `foundry_actor_type` from |
| 58 | + API response, expose it on the returned adapter object, default to `'character'` |
| 59 | +- `foundry-module/scripts/actor-sync.mjs` — Replace all `actor.type !== 'character'` |
| 60 | + checks with `actor.type !== this._adapter.actorType`, where `actorType` comes from the |
| 61 | + adapter. Also fix `Actor.create()` to use the correct type. Fallback to `'character'`. |
| 62 | + |
| 63 | +### 1C. SYSTEM_MAP_FALLBACK key fix |
| 64 | + |
| 65 | +**Problem:** `sync-manager.mjs` line 21 has `drawsteel: 'drawsteel'` but the Foundry |
| 66 | +system ID is `'draw-steel'` (hyphenated), not `'drawsteel'`. |
| 67 | + |
| 68 | +**Fix:** Change the key to `'draw-steel': 'drawsteel'`. |
| 69 | + |
| 70 | +**File:** `foundry-module/scripts/sync-manager.mjs` line 21 |
| 71 | + |
| 72 | +--- |
| 73 | + |
| 74 | +## Phase 2: Draw Steel Manifest Expansion |
| 75 | + |
| 76 | +### 2A. Character preset — full field set with Foundry paths |
| 77 | + |
| 78 | +Expand the `drawsteel-character` entity preset from 4 fields to a complete Draw Steel |
| 79 | +character sheet. All fields that map to Foundry data get `foundry_path` annotations. |
| 80 | + |
| 81 | +**Fields to add (with Foundry paths):** |
| 82 | + |
| 83 | +| Key | Label | Type | Foundry Path | Writable | |
| 84 | +|-----|-------|------|-------------|----------| |
| 85 | +| class | Class | string | (none — derived from class item) | — | |
| 86 | +| subclass | Subclass | string | (none — derived from subclass item) | — | |
| 87 | +| level | Level | number | system.details.level (if exists) | yes | |
| 88 | +| ancestry | Ancestry | string | (none — derived from ancestry item) | — | |
| 89 | +| career | Career | string | (none — derived from career item) | — | |
| 90 | +| might | Might | number | system.characteristics.might.value | yes | |
| 91 | +| agility | Agility | number | system.characteristics.agility.value | yes | |
| 92 | +| reason | Reason | number | system.characteristics.reason.value | yes | |
| 93 | +| intuition | Intuition | number | system.characteristics.intuition.value | yes | |
| 94 | +| presence | Presence | number | system.characteristics.presence.value | yes | |
| 95 | +| stamina_current | Current Stamina | number | system.stamina.value | yes | |
| 96 | +| stamina_max | Max Stamina | number | system.stamina.max | no (derived) | |
| 97 | +| stamina_temp | Temporary Stamina | number | system.stamina.temporary | yes | |
| 98 | +| recoveries_current | Recoveries | number | system.recoveries.value | yes | |
| 99 | +| recoveries_max | Max Recoveries | number | system.recoveries.max | no (derived) | |
| 100 | +| heroic_resource | Heroic Resource | number | system.hero.primary.value | yes | |
| 101 | +| surges | Surges | number | system.hero.surges | yes | |
| 102 | +| victories | Victories | number | system.hero.victories | yes | |
| 103 | +| xp | Experience | number | system.hero.xp | yes | |
| 104 | +| renown | Renown | number | system.hero.renown | yes | |
| 105 | +| wealth | Wealth | number | system.hero.wealth | yes | |
| 106 | +| speed | Speed | number | system.movement.value | no (derived) | |
| 107 | +| stability | Stability | number | system.combat.stability | no (derived) | |
| 108 | +| size | Size | number | system.combat.size.value | no | |
| 109 | + |
| 110 | +**Sections:** Group characteristics under "Characteristics", stamina/recoveries under |
| 111 | +"Resources", combat stats under "Combat", victories/xp/renown under "Progression". |
| 112 | + |
| 113 | +**File:** `internal/systems/drawsteel/manifest.json` |
| 114 | + |
| 115 | +### 2B. Creature preset — add with proper Draw Steel terminology |
| 116 | + |
| 117 | +Add a `drawsteel-creature` entity preset for NPCs/monsters. |
| 118 | + |
| 119 | +**Fields:** |
| 120 | +- level (number), role (string: "Ambusher", "Artillery", "Brute", etc.), |
| 121 | + ev (number, Encounter Value), role_type (string: "Minion", "Standard", "Elite", "Solo"), |
| 122 | + stamina (number), speed (number), stability (number), size (string), |
| 123 | + free_strike_damage (number) |
| 124 | + |
| 125 | +### 2C. Kit preset — add Foundry paths |
| 126 | + |
| 127 | +The existing kit preset fields are fine but need `foundry_path` annotations where applicable. |
| 128 | +Kits are items in Foundry Draw Steel, so these map to item system data. |
| 129 | + |
| 130 | +### 2D. Update manifest status |
| 131 | + |
| 132 | +Change `"status": "coming_soon"` to `"status": "available"`. |
| 133 | + |
| 134 | +--- |
| 135 | + |
| 136 | +## Phase 3: Reference Data Foundation |
| 137 | + |
| 138 | +### 3A. Create data directory structure |
| 139 | + |
| 140 | +Create `internal/systems/drawsteel/data/` with JSON files for each category. |
| 141 | + |
| 142 | +### 3B. Seed initial reference data |
| 143 | + |
| 144 | +Using correct Draw Steel terminology and CC-BY-4.0 content: |
| 145 | + |
| 146 | +- `data/abilities.json` — Seed with 5-10 representative abilities across different classes |
| 147 | + (e.g., a Tactician, Shadow, Fury, and Elementalist ability each). Fields: name, class, |
| 148 | + level, type (action/maneuver/triggered), keywords, description. |
| 149 | + |
| 150 | +- `data/creatures.json` — Seed with 5-8 creatures covering different roles and levels. |
| 151 | + Fields: name, level, role, role_type, ev, stamina, speed, description. |
| 152 | + |
| 153 | +- `data/ancestries.json` — Seed with the core ancestries (Human, Dwarf, Elf (Wode/High), |
| 154 | + Orc, Hakaan, Memonek, Revenant, Polder, Dragon Knight). Fields: name, size, speed, |
| 155 | + description. |
| 156 | + |
| 157 | +--- |
| 158 | + |
| 159 | +## Phase 4: Generic Adapter Enhancement |
| 160 | + |
| 161 | +The user explicitly wants everything dynamic — no hand-written adapters. The generic |
| 162 | +adapter must be enhanced to handle system-specific details like actor type. |
| 163 | + |
| 164 | +### 4A. Enhance generic adapter |
| 165 | + |
| 166 | +**File:** `foundry-module/scripts/adapters/generic-adapter.mjs` |
| 167 | + |
| 168 | +- Read `foundry_actor_type` from the API response and include it in the returned object |
| 169 | + as `actorType` (default: `'character'`) |
| 170 | +- This is already largely handled by the existing generic adapter; just add the |
| 171 | + `actorType` property |
| 172 | + |
| 173 | +### 4B. Actor sync — use adapter's actorType |
| 174 | + |
| 175 | +**File:** `foundry-module/scripts/actor-sync.mjs` |
| 176 | + |
| 177 | +- `_handleCreateActor`: Replace `actor.type !== 'character'` with |
| 178 | + `actor.type !== (this._adapter.actorType || 'character')` |
| 179 | +- `_onCharacterCreated`: Replace `type: 'character'` with |
| 180 | + `type: this._adapter.actorType || 'character'` |
| 181 | +- `getSyncedActors`: Replace `.filter(a => a.type === 'character')` with |
| 182 | + `.filter(a => a.type === (this._adapter?.actorType || 'character'))` |
| 183 | + |
| 184 | +--- |
| 185 | + |
| 186 | +## Phase 5: Documentation Updates |
| 187 | + |
| 188 | +### 5A. Update `.ai.md` |
| 189 | + |
| 190 | +Update `internal/systems/drawsteel/.ai.md` to reflect completed work — mark handlers, |
| 191 | +data files, and routes as done. |
| 192 | + |
| 193 | +### 5B. Update `.ai/status.md` |
| 194 | + |
| 195 | +Add sprint entry documenting the Draw Steel system completion. |
| 196 | + |
| 197 | +### 5C. Update `.ai/todo.md` |
| 198 | + |
| 199 | +Mark Draw Steel tasks as complete, add any follow-up items. |
| 200 | + |
| 201 | +--- |
| 202 | + |
| 203 | +## Execution Order |
| 204 | + |
| 205 | +1. Phase 1C (trivial fix — SYSTEM_MAP_FALLBACK key) |
| 206 | +2. Phase 1B (foundry_actor_type — manifest struct + API + adapter + actor-sync) |
| 207 | +3. Phase 2A-D (manifest expansion — all entity presets with Foundry paths) |
| 208 | +4. Phase 3A-B (reference data files) |
| 209 | +5. Phase 1A (CORS admin whitelist — biggest infrastructure change) |
| 210 | +6. Phase 4A-B (generic adapter enhancement) |
| 211 | +7. Phase 5 (documentation) |
| 212 | + |
| 213 | +## Risk Notes |
| 214 | + |
| 215 | +- **Draw Steel Foundry paths may change** — The Draw Steel Foundry system is actively |
| 216 | + developed (v0.11.1). Paths like `system.hero.primary.value` could shift. The generic |
| 217 | + adapter approach mitigates this since path definitions live in the manifest, not in code. |
| 218 | +- **Class/subclass/ancestry/career** are item-derived in Foundry — these can't be set via |
| 219 | + `actor.update()` in Foundry, so they're included as read-only Chronicle fields with no |
| 220 | + `foundry_path` (sync is name/description only for these). |
| 221 | +- **The `section` field** on FieldDef is used in manifests but not in the Go struct. It's |
| 222 | + preserved through JSON round-tripping. No changes needed. |
0 commit comments