Demo Mod Equipped Kit + Type Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Every equipped doll seat in Demo Mod can pick a Demo kit and edit type/quality via unified OV37 subtabs; FS, 22-demo, item-specs, and Demo kanban stay in sync.
Architecture: Expand demo_mod_mock_data.js with seat-family kit catalogs + missing kit rows + TYPES_BY_FAMILY / kindFromPiece. Generalize jewelry OV37 into renderPieceEdit for all non-TBA doll pieces (Stats / Affixes / Sockets [+ Gunfeel]). Session fillOverrides[scenarioId][seat] = kitId so kit swaps persist without mutating base scenario constants incorrectly. Docs: FS-GUN-012, 22-demo, item-specs, kanban-seed, rebuild PB/FS HTML.
Tech Stack: Vanilla HTML/JS mock · Node asserts · Markdown FS · kanban-seed.js · build_fs_html.py / build_pb_html.py
Spec: docs/superpowers/specs/2026-10-26-demo-mod-equipped-kit-type-design.md
Global Constraints
Section titled “Global Constraints”- Doll seats only (W1–W4 · Wrist · PhysShield · armor · ES · rings · amulet). Specials/Ordnance/Tentacles remain TBA stubs (no kit catalog).
- Kit change resets plate, affixes (3+3 pads), sockets/wells, art, gunfeel to kit defaults.
- Mesh never changes. Demo skips Bore spend.
- Sockets sandbox = jewelry rules (Phonon/Gem/Jewel, Artifact Unique/Apex, Chord Common+Phonon order).
- Guns: 4th tab Gunfeel; Mag OK → HUD ammo.
- Source:
demo_mod_mock_data.js+demo_mod_mock_app.js; rebundle with_bundle_mock.py. - Do not git commit unless the user explicitly asks.
- If subagent dispatch fails (usage), controller executes tasks inline without pausing.
File map
Section titled “File map”| File | Role |
|---|---|
tfa_gdd_html/figures/demo_mod_mock_data.js |
Kits, catalogs, type enums, helpers |
tfa_gdd_html/figures/demo_mod_mock_app.js |
Unified piece edit + fill overrides |
tfa_gdd_html/figures/demo_mod_mock.html |
Bundled + CSS (already has subtabs) |
tfa_gdd_html/figures/_test_kit_catalog.js |
Node asserts for catalogs / kindFrom |
tfa_gdd_html/figures/_bundle_mock.py |
Existing bundler |
docs/inventory/demo-mod-item-specs.md |
Spec sync |
Terra_Firma_Functional_Specification.md |
FS-GUN-012 + AC |
docs/22-demo.md |
UI lock table |
html/js/kanban-seed.js |
Demo Mod kit cards |
build_fs_html.py / build_pb_html.py |
Rebuild readers |
Task 1: Kit catalog data + stubs + helpers
Section titled “Task 1: Kit catalog data + stubs + helpers”Files:
- Modify:
tfa_gdd_html/figures/demo_mod_mock_data.js - Create:
tfa_gdd_html/figures/_test_kit_catalog.js
Interfaces:
-
Produces on
TFA_DEMO_MOD_DATA:KIT_CATALOG: { ranged: string[], melee: string[], physShield: string[], armorHelm: string[], armorChest: string[], armorGloves: string[], armorBelt: string[], armorBoots: string[], jewelryRing: string[], jewelryAmulet: string[], energyShield: string[] }kitCatalogForSeat(seat) -> string[]TYPES_BY_FAMILY: { gun: string[], melee: string[], physShield: string[], armor: string[], jewelry: string[], energyShield: string[] }typesForPiece(piece) -> string[]kindFromPiece(quality, typeLabel, classFamily) -> string- New/expand PIECES: at minimum
scout,sidearm,smg(copy pattern fromauto/hcwith distinct names/mags/optics); armor weight variants optional — if not adding Light/Med/Heavy ids, tag existing pieces withweight: "Heavy"and allow Type select to editdraft.typeLabel/draft.weightwithout new mesh kits.
-
Step 1: Write failing
_test_kit_catalog.js
const fs = require("fs");const path = require("path");const vm = require("vm");const src = fs.readFileSync(path.join(__dirname, "demo_mod_mock_data.js"), "utf8");const ctx = { window: {}, console };vm.runInNewContext(src, ctx);const D = ctx.window.TFA_DEMO_MOD_DATA;if (!D || typeof D.kitCatalogForSeat !== "function") { console.error("FAIL: kitCatalogForSeat missing"); process.exit(1);}const w1 = D.kitCatalogForSeat("W1");if (!w1.includes("auto") || !w1.includes("hc") || !w1.includes("scout")) { console.error("FAIL: W1 catalog", w1); process.exit(1);}if (!D.kitCatalogForSeat("Chest").length) { console.error("FAIL: Chest catalog empty"); process.exit(1);}const k = D.kindFromPiece("Rare", "Hand Cannon", "gun");if (!/Rare/.test(k) || !/Hand Cannon|HC|Jewelry|gun/i.test(k)) { // expect "Rare Hand Cannon" or similar locked format:}if (D.kindFromPiece("Rare", "Hand Cannon", "gun") !== "Rare Hand Cannon") { console.error("FAIL: kindFromPiece", D.kindFromPiece("Rare", "Hand Cannon", "gun")); process.exit(1);}console.log("PASS kit catalog helpers");Fix the kind expectation to exactly "Rare Hand Cannon" in both test and helper.
- Step 2: Run — expect FAIL missing kitCatalogForSeat
Run: node tfa_gdd_html/figures/_test_kit_catalog.js
- Step 3: Implement catalogs + stubs + helpers
Add PIECES scout, sidearm, smg (gunfeel true; sensible mag/optic allowlists from FS-GUN-012 / optics doc). Ensure every catalog id exists in PIECES.
const KIT_CATALOG = { ranged: ["auto", "pulse", "scout", "hc", "sidearm", "smg", "sg", "heavy", "gl"], melee: ["blade", "sword", "wrist"], physShield: ["pshield"], armorHelm: ["helm"], armorChest: ["chest"], armorGloves: ["gloves"], armorBelt: ["belt"], armorBoots: ["boots"], jewelryRing: ["ring1", "ring2"], jewelryAmulet: ["amulet"], energyShield: ["eshield"], }; function kitCatalogForSeat(seat) { const map = { W1: "ranged", W2: "ranged", W3: "ranged", W4: "ranged", Wrist: "melee", PhysShield: "physShield", Helm: "armorHelm", Chest: "armorChest", Gloves: "armorGloves", Belt: "armorBelt", Boots: "armorBoots", Ring1: "jewelryRing", Ring2: "jewelryRing", Amulet: "jewelryAmulet", EnergyShield: "energyShield", }; // Note: W4 may hold melee in scenarios — catalog for W4 should be ranged∪melee OR seat uses ranged only and Wrist for melee. // Spec: W1–W4 ranged; Wrist/W melee. Keep W4 ranged-only in catalog; scenarios that put blade on W4 still list blade if fill has it — when opening kit select, union current id into options: const key = map[seat]; return (KIT_CATALOG[key] || []).slice(); }For W4 scenarios with blade: in app, options = unique(catalog.concat([currentId])).
const TYPES_BY_FAMILY = { gun: ["Auto Rifle", "Pulse Rifle", "Scout Rifle", "Hand Cannon", "Sidearm", "SMG", "Shotgun", "Heavy Gun", "Grenade Launcher"], melee: ["Blade", "Sword", "Wrist"], physShield: ["Phys Shield"], armor: ["Light", "Medium", "Heavy"], jewelry: ["Ring", "Amulet"], energyShield: ["Energy Shield"], }; function typesForPiece(piece) { const f = piece.classFamily; if (f === "gun") return TYPES_BY_FAMILY.gun; if (f === "melee") return TYPES_BY_FAMILY.melee; if (f === "physShield") return TYPES_BY_FAMILY.physShield; if (f === "armor") return TYPES_BY_FAMILY.armor; if (f === "jewelry") return TYPES_BY_FAMILY.jewelry; if (f === "energyShield") return TYPES_BY_FAMILY.energyShield; return []; } function kindFromPiece(quality, typeLabel, classFamily) { if (classFamily === "armor") return quality + " Armor · " + typeLabel; if (classFamily === "energyShield") return quality + " Energy Shield"; if (classFamily === "jewelry") return quality + " Jewelry"; if (classFamily === "physShield") return quality + " Phys Shield"; if (classFamily === "melee") return quality + " " + typeLabel; return quality + " " + typeLabel; // gun }Pad all non-TBA PIECES affixes to 3+3; add quality: "Rare" and typeLabel (from identity or name) on each.
Export new symbols on return object.
- Step 4: Run test PASS +
node --check demo_mod_mock_data.js
Task 2: Unified OV37 piece edit + fill overrides
Section titled “Task 2: Unified OV37 piece edit + fill overrides”Files:
- Modify:
tfa_gdd_html/figures/demo_mod_mock_app.js
Interfaces:
-
Consumes: Task 1 helpers
-
Produces:
state.fillOverrides: { [scenarioId]: { [seat]: kitId } }effectiveFill() -> fill mapmerging scenario.fill + overridesonCharacteruseseffectiveFill()isDollEdit(p)— true for jewelry OR armor OR physShield OR melee OR gun (not tba)renderPieceEdit(p,d)replaces jewelry-only path; tabs depend onp.gunfeel- Kit change in Stats:
applyKitChange(kitId)resets draft fromD.PIECES[kitId]+ seat
-
Step 1: Wire fillOverrides + effectiveFill
state.fillOverrides = {};
function effectiveFill() { const base = Object.assign({}, scenario().fill); const ov = state.fillOverrides[state.scenarioId] || {}; Object.keys(ov).forEach(function (seat) { base[seat] = ov[seat]; }); return base; }Replace scenario().fill reads in onCharacter / selectLoadoutSeat / activeGun paths with effectiveFill() where seat→piece id is resolved.
- Step 2: Generalize defaultDraft for all doll pieces
Extend jewelry draft fields (name, quality, typeLabel/baseType, kind, wells, sockets, art, kitId) to all isDollEdit pieces. Guns also keep gun. Use typeLabel consistently (map jewelry baseType → typeLabel for Rings).
function isDollEdit(p) { return !!(p && !p.tba && p.cat !== "specials" && !HARDCODED_CATS[p.cat]); }Actually HARDCODED_CATS are categories — doll pieces have cat weapons/armor/jewelry. TBA kits have tba: true. Use !p.tba && (weapons|armor|jewelry).
-
Step 3:
renderPieceEdit— unify tabs -
Tabs: gun →
["stats","affixes","sockets","gunfeel"]; else →["stats","affixes","sockets"] -
Stats: Seat · Equipped kit (
sel("meta.kitId", …)) · Name · Type · Quality · Kind RO · plate -
Affixes / Sockets: reuse jewelry body builders (extract
renderAffixBody(d),renderSocketBody(d)functions from currentrenderJewelryEdit) -
Gunfeel: move existing gun rows here
-
Replace
isJewelryEditearly return withisDollEdit→renderPieceEdit -
TBA / leftover path: keep simple single scroll for
p.tbaonly
Kit change handler:
if (k === "kitId") { const kit = D.PIECES[val]; if (!kit) return; state.draft = defaultDraft(Object.assign(deepClone(kit), { seat: p.seat, cat: p.cat, key: p.key, id: kit.id })); state.draft.kitId = val; // pending fill — applied on OK state.draft._pendingKit = val; renderEdit(); return; }- Step 4: commitOk writes fillOverrides + jewelry-like identity fields for all doll
if (isDollEdit(p)) { payload.name = state.draft.name; payload.quality = state.draft.quality; payload.typeLabel = state.draft.typeLabel; payload.kind = state.draft.kind; payload.wells = state.draft.wells; payload.sockets = deepClone(state.draft.sockets); payload.art = ... payload.kitId = state.draft.kitId || p.id; if (state.draft._pendingKit || payload.kitId !== p.id) { state.fillOverrides[state.scenarioId] = state.fillOverrides[state.scenarioId] || {}; state.fillOverrides[state.scenarioId][p.seat] = payload.kitId; } }After kit swap OK, list must show new piece (effectiveFill). Clear committed key for old seat:oldId optional; write under new seat:newId.
- Step 5:
node --check demo_mod_mock_app.js
Task 3: Bundle mock + browser checklist
Section titled “Task 3: Bundle mock + browser checklist”Files:
-
Run:
_bundle_mock.py -
Verify:
demo_mod_mock.html -
Step 1:
python tfa_gdd_html/figures/_bundle_mock.py— expectmatchChord+kitCatalogForSeat+renderPieceEditor kitId in bundle -
Step 2: Manual / DevTools checklist
- Weapons → W1 Enter → Stats kit Auto→HC → plate changes → OK → list HC
- Armor Chest → Affixes edit → OK persists
- Jewelry Ring → Sockets Chord still works
- Gun Gunfeel Mag → HUD updates on OK
- Narrow height: tab body scrolls
Task 4: Docs — item-specs + FS-GUN-012 + 22-demo
Section titled “Task 4: Docs — item-specs + FS-GUN-012 + 22-demo”Files:
-
Modify:
docs/inventory/demo-mod-item-specs.md -
Modify:
Terra_Firma_Functional_Specification.md(FS-GUN-012 + AC-GUN-006) -
Modify:
docs/22-demo.md -
Modify: spec status → Implemented
-
Run:
python build_fs_html.pyandpython build_pb_html.pyfrom pack root -
Step 1: Rewrite OV37 in item-specs to Approach 1 (all doll seats; kit picker; Gunfeel tab; TBA unchanged)
-
Step 2: FS-GUN-012 prose
Replace OV37 sentence with subtabs + kit/type/quality + editable Demo sockets + Specials/Ordnance/Tentacles ×4 category chips. Add AC bullet: change W1 kit Auto→HC session fill updates list; mesh unchanged.
-
Step 3: 22-demo Demo Mod UI lock table match FS
-
Step 4: Rebuild FS + PB HTML
python build_fs_html.pypython build_pb_html.py- Step 5: Mark design spec Implemented
Task 5: Demo kanban seed sync
Section titled “Task 5: Demo kanban seed sync”Files:
-
Modify:
html/js/kanban-seed.jsmoduledemo-modkit -
Step 1: Reword sub
"List all weapons the character currently has (melee + ranged)"→"OV36 equipped doll seats only (Weapons · Armor · Jewelry/ES · Specials/Ordnance/Tentacles ×4 TBA); Mod ON/OFF gate" -
Step 2: Add task block (new task id
Hor extendA/G) with subs:
DONE design: docs/inventory/demo-mod-item-specs.md + jewelry/equipped-kit specsNEED mock→UMG: Equipped kit + type/quality picker on all doll seats (session fill)NEED mock→UMG: OV37 subtabs Stats/Affixes/Sockets[/Gunfeel]; 1080p tab-body scrollDONE design: Jewelry Phonon Chord + Artifact Unique/Apex sandbox (FS-ITM)NEED: Specials / Ordnance / Tentacles ×4 hard-coded TBA rows in Mod listUse same sub/steps shape as neighboring cards (three-step clarify/implement/verify).
-
Step 3: Rebuild PB if kanban embeds seed —
python build_pb_html.pyorpython build_pb_kanban.pyif that is the pack path (check which script the repo uses; prefer existing Demo rebuild path). -
Step 4:
graphify update .
Spec coverage
Section titled “Spec coverage”| Spec | Task |
|---|---|
| Kit picker all doll seats | 1–2 |
| Type/quality/name | 2 |
| Subtabs armor/melee/shield/gun | 2 |
| Gunfeel tab | 2 |
| Kit reset defaults | 2 |
| fillOverrides session | 2 |
| FS / 22-demo / item-specs | 4 |
| Kanban | 5 |
| Chord still works | 2–3 |
Placeholder scan
Section titled “Placeholder scan”None — catalogs and handlers specified.
Type consistency
Section titled “Type consistency”kitId string · typeLabel string · fillOverrides[scenarioId][seat] · kindFromPiece(quality, typeLabel, classFamily).