Skip to content

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

  • 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 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 from auto/hc with distinct names/mags/optics); armor weight variants optional — if not adding Light/Med/Heavy ids, tag existing pieces with weight: "Heavy" and allow Type select to edit draft.typeLabel / draft.weight without 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 map merging scenario.fill + overrides
    • onCharacter uses effectiveFill()
    • isDollEdit(p) — true for jewelry OR armor OR physShield OR melee OR gun (not tba)
    • renderPieceEdit(p,d) replaces jewelry-only path; tabs depend on p.gunfeel
    • Kit change in Stats: applyKitChange(kitId) resets draft from D.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 current renderJewelryEdit)

  • Gunfeel: move existing gun rows here

  • Replace isJewelryEdit early return with isDollEdit → renderPieceEdit

  • TBA / leftover path: keep simple single scroll for p.tba only

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

Files:

  • Run: _bundle_mock.py

  • Verify: demo_mod_mock.html

  • Step 1: python tfa_gdd_html/figures/_bundle_mock.py — expect matchChord + kitCatalogForSeat + renderPieceEdit or kitId in bundle

  • Step 2: Manual / DevTools checklist

  1. Weapons → W1 Enter → Stats kit Auto→HC → plate changes → OK → list HC
  2. Armor Chest → Affixes edit → OK persists
  3. Jewelry Ring → Sockets Chord still works
  4. Gun Gunfeel Mag → HUD updates on OK
  5. 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.py and python build_pb_html.py from 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

Terminal window
python build_fs_html.py
python build_pb_html.py
  • Step 5: Mark design spec Implemented

Files:

  • Modify: html/js/kanban-seed.js module demo-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 H or extend A/G) with subs:

DONE design: docs/inventory/demo-mod-item-specs.md + jewelry/equipped-kit specs
NEED 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 scroll
DONE design: Jewelry Phonon Chord + Artifact Unique/Apex sandbox (FS-ITM)
NEED: Specials / Ordnance / Tentacles ×4 hard-coded TBA rows in Mod list

Use same sub/steps shape as neighboring cards (three-step clarify/implement/verify).

  • Step 3: Rebuild PB if kanban embeds seed — python build_pb_html.py or python build_pb_kanban.py if that is the pack path (check which script the repo uses; prefer existing Demo rebuild path).

  • Step 4: graphify update .


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

None — catalogs and handlers specified.

kitId string · typeLabel string · fillOverrides[scenarioId][seat] · kindFromPiece(quality, typeLabel, classFamily).