Demo Mod Jewelry / ES OV37 Subtabs 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: Jewelry / Energy Shield Demo Mod edit uses OV37 subtabs (Stats | Affixes | Sockets) with editable type/quality/affixes/wells/chips/Artifact and Phonon Chord readout, usable on 1080p via tab-body scroll.
Architecture: Extend demo_mod_mock_data.js with CHIPS, ARTS, QUALITIES, BASE_TYPES, DEMO_CHORDS + pure helpers (kindFrom, matchChord, isArtifactEligible). Extend demo_mod_mock_app.js so jewelry/ES renderEdit builds subtab chrome; non-jewelry keeps the existing single-scroll sections. Re-bundle into demo_mod_mock.html via _bundle_mock.py. Update demo-mod-item-specs.md to match the approved design.
Tech Stack: Vanilla HTML/CSS/JS mock · Node --check · optional Node assert script for Chord helper · graphify update after edits
Spec: docs/superpowers/specs/2026-10-26-demo-mod-jewelry-sockets-design.md
Global Constraints
Section titled “Global Constraints”- Jewelry / ES only this pass (Ring1 · Ring2 · Amulet · EnergyShield). Armor/weapons socket parity deferred.
- Affix caps ≤3 prefix + ≤3 suffix; pools from existing mock / gui.js.
- Open wells 0–6; Demo skips Bore cost.
- Artifact only Unique / Apex (
a-crown,a-well). - Chord only when Quality is Common (white/gray) and all open wells are Phonons in exact demo sequence; Artifact never in Chord length.
- 1080p: fixed subtabs + actions; only
#editTabBodyscrolls. - Source of truth for logic:
demo_mod_mock_data.js+demo_mod_mock_app.js; after edits runpython tfa_gdd_html/figures/_bundle_mock.py. - Do not git commit unless the user explicitly asks.
File map
Section titled “File map”| File | Role |
|---|---|
tfa_gdd_html/figures/demo_mod_mock_data.js |
Catalogs + Chord match helpers |
tfa_gdd_html/figures/demo_mod_mock_app.js |
Subtab UI, draft fields, commit |
tfa_gdd_html/figures/demo_mod_mock.html |
Bundled output (regen) |
tfa_gdd_html/figures/_bundle_mock.py |
Existing bundler (unchanged unless broken) |
tfa_gdd_html/figures/_test_jewelry_chord.js |
Node assert script for Chord helper |
docs/inventory/demo-mod-item-specs.md |
Unlock sockets/Artifact/Chord for Jewelry/ES; document subtabs |
docs/superpowers/specs/2026-10-26-demo-mod-jewelry-sockets-design.md |
Mark Status implemented when done |
Task 1: Data catalogs + Chord helper
Section titled “Task 1: Data catalogs + Chord helper”Files:
- Modify:
tfa_gdd_html/figures/demo_mod_mock_data.js - Create:
tfa_gdd_html/figures/_test_jewelry_chord.js - Test: run Node assert script
Interfaces:
-
Consumes: existing
affixSlot/affixSides/ jewelry PIECES -
Produces (on
window.TFA_DEMO_MOD_DATAreturn object):CHIPS: { Phonon: [{id,name}], Gem: [...], Jewel: [...] }ARTS: [{id,name}]QUALITIES: string[]—["Common","Augmented","Rare","Legendary","Unique","Apex"]BASE_TYPES_BY_SEAT: { Ring1:"Ring", Ring2:"Ring", Amulet:"Amulet", EnergyShield:"Energy Shield" }DEMO_CHORDS: [{ word: string, sequence: string[] }]— sequence entries are Phonon idschipById(id) -> {id,name,kind}|nullkindFrom(quality, baseType) -> stringe.g."Rare Jewelry"/"Apex Energy Shield"isArtifactEligible(quality) -> booleanisChordBase(quality) -> boolean— true only for"Common"matchChord(quality, wells, sockets) -> string|""— word or empty
-
Step 1: Write the failing Node assert script
Create tfa_gdd_html/figures/_test_jewelry_chord.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.matchChord !== "function") { console.error("FAIL: matchChord missing"); process.exit(1);}const chord = D.DEMO_CHORDS[0];const ok = D.matchChord("Common", chord.sequence.length, chord.sequence.slice());if (ok !== chord.word) { console.error("FAIL: expected", chord.word, "got", ok); process.exit(1);}if (D.matchChord("Rare", chord.sequence.length, chord.sequence.slice()) !== "") { console.error("FAIL: Rare must not Chord"); process.exit(1);}if (D.isArtifactEligible("Unique") !== true || D.isArtifactEligible("Rare") !== false) { console.error("FAIL: Artifact gate"); process.exit(1);}console.log("PASS jewelry chord helpers");- Step 2: Run test — expect FAIL (matchChord missing)
Run: node tfa_gdd_html/figures/_test_jewelry_chord.js
Expected: FAIL: matchChord missing (exit 1)
- Step 3: Add catalogs + helpers to
demo_mod_mock_data.js
Before the return { block, add (mirror gui.js CHIPS / ARTS; keep ids identical):
const QUALITIES = ["Common", "Augmented", "Rare", "Legendary", "Unique", "Apex"]; const BASE_TYPES_BY_SEAT = { Ring1: "Ring", Ring2: "Ring", Amulet: "Amulet", EnergyShield: "Energy Shield", }; const CHIPS = { Phonon: [ { id: "p-a", name: "A" }, { id: "p-aa", name: "Aa" }, { id: "p-i", name: "I" }, { id: "p-u", name: "U" }, { id: "p-ri", name: "Ri" }, { id: "p-e", name: "E" }, { id: "p-o", name: "O" }, { id: "p-am", name: "Am" }, { id: "p-ka", name: "Ka" }, { id: "p-ma", name: "Ma" }, ], Gem: [ { id: "g-ruby", name: "Ruby" }, { id: "g-sapphire", name: "Sapphire" }, { id: "g-topaz", name: "Topaz" }, { id: "g-emerald", name: "Emerald" }, { id: "g-gammitite", name: "Gammitite" }, { id: "g-nucleite", name: "Nucleite" }, { id: "g-helion", name: "Helion" }, { id: "g-nullite", name: "Nullite" }, { id: "g-amethyst", name: "Amethyst" }, { id: "g-diamond", name: "Diamond" }, { id: "g-siphonite", name: "Siphonite" }, ], Jewel: [ { id: "j-edias", name: "ED / IAS" }, { id: "j-facet-th", name: "Facet Thermal" }, { id: "j-facet-void", name: "Facet Void" }, { id: "j-fhr", name: "FHR Lens" }, ], }; const ARTS = [ { id: "a-crown", name: "Set Crown" }, { id: "a-well", name: "Set Well" }, ]; // Demo Chord sandbox (not full DT_TFA_PhononChords) const DEMO_CHORDS = [ { word: "Aa", sequence: ["p-a", "p-aa"] }, { word: "Karma", sequence: ["p-ka", "p-ri", "p-ma"] }, { word: "Om", sequence: ["p-o", "p-am"] }, ];
function chipById(id) { if (!id) return null; const kinds = ["Phonon", "Gem", "Jewel"]; for (let k = 0; k < kinds.length; k++) { const list = CHIPS[kinds[k]]; for (let i = 0; i < list.length; i++) { if (list[i].id === id) return Object.assign({ kind: kinds[k] }, list[i]); } } return null; } function kindFrom(quality, baseType) { if (baseType === "Energy Shield") return quality + " Energy Shield"; return quality + " Jewelry"; } function isArtifactEligible(quality) { return quality === "Unique" || quality === "Apex"; } function isChordBase(quality) { return quality === "Common"; } function matchChord(quality, wells, sockets) { if (!isChordBase(quality)) return ""; const n = Math.max(0, Math.min(6, +wells || 0)); const ids = (sockets || []).slice(0, n); if (ids.length !== n || ids.some(function (id) { return !id; })) return ""; for (let i = 0; i < ids.length; i++) { const c = chipById(ids[i]); if (!c || c.kind !== "Phonon") return ""; } for (let c = 0; c < DEMO_CHORDS.length; c++) { const row = DEMO_CHORDS[c]; if (row.sequence.length !== n) continue; let ok = true; for (let i = 0; i < n; i++) { if (ids[i] !== row.sequence[i]) { ok = false; break; } } if (ok) return row.word; } return ""; }Export them on the return object. Ensure jewelry pieces still ship with affixes padded to 3+3 via affixSides(..., 3, 3) where currently 1+1 — call sites for ring1 / ring2 / amulet / eshield:
affixes: affixSides( [/* existing filled */], [/* existing filled */], 3, 3),Add default fields on jewelry pieces if missing: quality: "Rare", baseType matching seat, art: "".
- Step 4: Run assert — expect PASS
Run: node tfa_gdd_html/figures/_test_jewelry_chord.js
Expected: PASS jewelry chord helpers
- Step 5: Syntax check
Run: node --check tfa_gdd_html/figures/demo_mod_mock_data.js
Expected: exit 0
Task 2: Draft schema + 1080p CSS + subtab shell
Section titled “Task 2: Draft schema + 1080p CSS + subtab shell”Files:
- Modify:
tfa_gdd_html/figures/demo_mod_mock_app.js(defaultDraft,state, CSS is in HTML — also edit styles indemo_mod_mock.htmlhead or inject style block via app; prefer editing styles once in HTML source before data script, then re-bundle app only… Styles live indemo_mod_mock.html<style>— edit that file’s style section directly.) - Modify:
tfa_gdd_html/figures/demo_mod_mock.html(CSS only in this task if bundler overwrites scripts — CSS is above scripts, safe)
Interfaces:
-
Consumes: Task 1 helpers on
D -
Produces:
state.editTab: "stats"|"affixes"|"sockets"defaultDraft(piece)includes for jewelry/ES:name,quality,baseType,kind,wells,sockets(len 6),artisJewelryEdit(piece) -> boolean—piece.classFamily === "jewelry" || piece.classFamily === "energyShield" || piece.cat === "jewelry"
-
Step 1: Add CSS for subtabs + scroll body
In demo_mod_mock.html <style>, append:
.hud { max-height: 90vh; display: flex; flex-direction: column; } .edit-chrome { flex: 0 0 auto; } .edit-tabs { display: flex; gap: 8px; margin: 0 0 10px; flex-wrap: wrap; } .edit-tabs button { font: inherit; letter-spacing: 0.1em; text-transform: uppercase; font-size: 11px; padding: 8px 12px; cursor: pointer; border: 1px solid var(--cyan-dim); background: rgba(8,22,32,0.7); color: var(--muted); } .edit-tabs button.on { border-color: var(--cyan); color: var(--cyan); box-shadow: 0 0 12px var(--cyan-glow); } #editTabBody { flex: 1 1 auto; overflow: auto; max-height: calc(90vh - 280px); padding-right: 4px; } .chord-banner { margin: 8px 0; padding: 8px 10px; border: 1px solid var(--cyan); color: var(--cyan); letter-spacing: 0.08em; font-size: 12px; } .chord-banner.off { border-color: var(--muted); color: var(--muted); }Ensure #viewEdit uses flex column so #editTabBody can shrink/scroll. Structure target for jewelry:
<div id="editSections"> <div class="edit-chrome">…tabs…</div> <div id="editTabBody">…</div></div>(Built in JS in Task 3; CSS can land now.)
- Step 2: Extend state + defaultDraft
In demo_mod_mock_app.js:
// in state: editTab: "stats",
function isJewelryEdit(p) { return !!( p && (p.cat === "jewelry" || p.classFamily === "jewelry" || p.classFamily === "energyShield") ); }
function defaultDraft(piece) { const draft = { plate: deepClone(piece.plate || {}), affixes: deepClone(piece.affixes || { prefix: [], suffix: [] }), gun: piece.gunfeel ? defaultGunDraft(piece) : null, }; if (isJewelryEdit(piece)) { const baseType = piece.baseType || (D.BASE_TYPES_BY_SEAT && D.BASE_TYPES_BY_SEAT[piece.seat]) || "Ring"; const quality = piece.quality || "Rare"; draft.name = piece.name; draft.baseType = baseType; draft.quality = quality; draft.kind = piece.kind || D.kindFrom(quality, baseType); draft.wells = typeof piece.wells === "number" ? piece.wells : 0; draft.sockets = (piece.sockets || ["", "", "", "", "", ""]).slice(); while (draft.sockets.length < 6) draft.sockets.push(""); draft.sockets = draft.sockets.slice(0, 6); draft.art = piece.art || ""; if (!D.isArtifactEligible(draft.quality)) draft.art = ""; } return draft; }When building onCharacter jewelry rows, merge committed overrides for name/quality/baseType/kind/wells/sockets/art the same way plate/affixes already merge (extend the ov block).
- Step 3: Syntax check app
Run: node --check tfa_gdd_html/figures/demo_mod_mock_app.js
Expected: exit 0
Task 3: Jewelry renderEdit subtabs + bind + commit
Section titled “Task 3: Jewelry renderEdit subtabs + bind + commit”Files:
- Modify:
tfa_gdd_html/figures/demo_mod_mock_app.js(renderEdit,bindDraftInputs,commitOk,enterPiece)
Interfaces:
-
Consumes:
defaultDraft,isJewelryEdit,D.matchChord,D.CHIPS,D.ARTS,D.QUALITIES -
Produces: working Stats / Affixes / Sockets UI; OK persists jewelry fields
-
Step 1: Split
renderEdit— early branch for jewelry
At start of renderEdit, after showing edit chrome:
state.editTab = state.editTab || "stats"; if (isJewelryEdit(p)) { renderJewelryEdit(p, d); return; } // existing single-column path for weapons/armor/TBA…Implement renderJewelryEdit(p, d):
function setEditTab(tab) { state.editTab = tab; renderEdit(); }
function renderJewelryEdit(p, d) { $("plateId").textContent = "TERRA FIRMA AWAKENING // SYSTEMS | OV37"; $("brandSub").textContent = (d.name || p.name).toUpperCase() + " · " + p.seat; $("crumb").textContent = "EDIT › " + p.seat + " › " + (d.name || p.name); $("viewList").classList.add("hidden"); $("viewEdit").classList.remove("hidden"); $("actionsList").classList.add("hidden"); $("actionsEdit").classList.remove("hidden");
const tabs = '<div class="edit-chrome"><div class="edit-tabs" role="tablist">' + ["stats", "affixes", "sockets"] .map(function (t) { return ( '<button type="button" role="tab" data-edit-tab="' + t + '" class="' + (state.editTab === t ? "on" : "") + '">' + t + "</button>" ); }) .join("") + '</div></div><div id="editTabBody"></div>';
$("editSections").innerHTML = tabs; let body = ""; if (state.editTab === "stats") { body += '<div class="section-title">Identity + plate</div><div class="rows">'; body += row("Seat", p.seat, '<span class="ro">RO</span>'); body += row("Name", d.name, '<input type="text" data-path="meta.name" value="' + String(d.name).replace(/"/g, """) + '" />'); const types = [D.BASE_TYPES_BY_SEAT[p.seat] || d.baseType]; body += row("Base type", d.baseType, sel("meta.baseType", d.baseType, types)); body += row("Quality", d.quality, sel("meta.quality", d.quality, D.QUALITIES)); body += row("Kind", d.kind, '<span class="ro">auto</span>'); Object.keys(d.plate || {}).forEach(function (k) { body += row(k, d.plate[k], num("plate." + k, d.plate[k])); }); body += "</div>"; } else if (state.editTab === "affixes") { body += '<div class="section-title">Prefixes / suffixes</div><div class="rows">'; (d.affixes.prefix || []).forEach(function (slot, i) { body += affixRow("prefix", i, slot); }); (d.affixes.suffix || []).forEach(function (slot, i) { body += affixRow("suffix", i, slot); }); body += "</div>"; } else { const chord = D.matchChord(d.quality, d.wells, d.sockets); body += '<div class="chord-banner ' + (chord ? "" : "off") + '">' + (chord ? "Chord: " + chord : "No Chord") + "</div>"; body += '<div class="section-title">Wells + chips</div><div class="rows">'; body += row("Open wells", d.wells, num("meta.wells", d.wells)); for (let i = 0; i < 6; i++) { const open = i < d.wells; const id = d.sockets[i] || ""; if (!open) { body += row("Well " + (i + 1), "locked", '<span class="ro">locked</span>'); continue; } const chip = D.chipById(id); const kind = (chip && chip.kind) || "Phonon"; const ids = (D.CHIPS[kind] || []).map(function (c) { return c.id; }); body += '<div class="var affix"><label>Well ' + (i + 1) + "</label>" + sel("sock." + i + ".kind", kind, ["Phonon", "Gem", "Jewel"]) + sel("sock." + i + ".id", id, [""].concat(ids)) + '<span class="ro">chip</span></div>'; } if (D.isArtifactEligible(d.quality)) { const artOpts = [""].concat( D.ARTS.map(function (a) { return a.id; }) ); body += row("Artifact", d.art || "(empty)", sel("meta.art", d.art || "", artOpts)); } else { body += row("Artifact", "—", '<span class="ro">Unique / Apex only</span>'); } body += "</div>"; } $("editTabBody").innerHTML = body; $("editNote").textContent = "Jewelry/ES · Stats / Affixes / Sockets · OK saves session (no Bore spend)."; document.querySelectorAll("[data-edit-tab]").forEach(function (b) { b.onclick = function () { setEditTab(b.getAttribute("data-edit-tab")); }; }); bindDraftInputs($("editSections")); $("hudPreview").innerHTML = "Jewelry — ammo HUD unchanged."; }- Step 2: Extend
bindDraftInputsfor meta + sock paths
Inside apply:
if (path.indexOf("meta.") === 0) { const k = path.slice(5); if (k === "name") state.draft.name = val; else if (k === "baseType") { state.draft.baseType = val; state.draft.kind = D.kindFrom(state.draft.quality, state.draft.baseType); } else if (k === "quality") { state.draft.quality = val; state.draft.kind = D.kindFrom(state.draft.quality, state.draft.baseType); if (!D.isArtifactEligible(val)) state.draft.art = ""; renderEdit(); return; } else if (k === "wells") { state.draft.wells = Math.max(0, Math.min(6, Math.floor(+val || 0))); for (let i = state.draft.wells; i < 6; i++) state.draft.sockets[i] = ""; renderEdit(); return; } else if (k === "art") state.draft.art = val; } else if (path.indexOf("sock.") === 0) { const bits = path.split("."); const idx = +bits[1]; if (bits[2] === "kind") { const first = (D.CHIPS[val] && D.CHIPS[val][0] && D.CHIPS[val][0].id) || ""; state.draft.sockets[idx] = first; renderEdit(); return; } if (bits[2] === "id") state.draft.sockets[idx] = val; // refresh Chord banner without full remount when only id changes: renderEdit(); return; }- Step 3: Extend
commitOk+onCharactermerge
commitOk jewelry branch:
const payload = { plate: deepClone(state.draft.plate), affixes: deepClone(state.draft.affixes), gun: state.draft.gun ? deepClone(state.draft.gun) : null, }; if (isJewelryEdit(p) || state.draft.quality) { payload.name = state.draft.name; payload.baseType = state.draft.baseType; payload.quality = state.draft.quality; payload.kind = state.draft.kind; payload.wells = state.draft.wells; payload.sockets = deepClone(state.draft.sockets); payload.art = D.isArtifactEligible(state.draft.quality) ? state.draft.art || "" : ""; payload.chord = D.matchChord(payload.quality, payload.wells, payload.sockets); } state.committed[p.key] = payload;In onCharacter, when applying ov, also copy those fields onto base.
On enterPiece for jewelry: state.editTab = "stats".
- Step 4: Syntax check
Run: node --check tfa_gdd_html/figures/demo_mod_mock_app.js
Expected: exit 0
Task 4: Bundle HTML + docs + verify
Section titled “Task 4: Bundle HTML + docs + verify”Files:
-
Modify: regenerate
tfa_gdd_html/figures/demo_mod_mock.htmlvia bundler -
Modify:
docs/inventory/demo-mod-item-specs.md -
Modify:
docs/superpowers/specs/2026-10-26-demo-mod-jewelry-sockets-design.md(status → Implemented) -
Optional one-liner in
docs/22-demo.mdif Demo Mod jewelry bullets exist -
Step 1: Re-bundle
Run from pack root:
python tfa_gdd_html/figures/_bundle_mock.pyExpected: prints ok … and switchCat True / ready True.
If bundler only injects demo_mod_mock_app.js after inlined data: after Task 1, also refresh inlined data. Update _bundle_mock.py if needed so it re-inlines data from demo_mod_mock_data.js then app, e.g.:
# read html, replace first window.TFA_DEMO_MOD_DATA script with data.js contents,# replace second app script with app.js — keep CSS editsIf current bundler only replaces app, manually replace the data <script>…TFA_DEMO_MOD_DATA…</script> body with demo_mod_mock_data.js contents once, or extend the bundler in this step so one command refreshes both.
- Step 2: Update
demo-mod-item-specs.md
Replace OV37 section for jewelry with subtabs:
## OV37 — edit
**Non-jewelry** (weapons / armor / TBA): single scroll — Identity · Plate · Affixes · Sockets RO · Gunfeel(if gun).
**Jewelry / ES** — subtabs (1080p: tab body scrolls; chrome fixed):
| Tab | Contents || --- | --- || **Stats** | Seat RO · Name · Base type (seat-gated) · Quality · Kind auto · plate || **Affixes** | Prefix ≤3 · Suffix ≤3 || **Sockets** | Open wells 0–6 · Phonon/Gem/Jewel per well · Artifact if Unique/Apex · Chord banner (Common + Phonon order) |
OK merges plate + affixes + (jewelry) name/type/quality/wells/sockets/art. Demo skips Bore spend.Update Out of scope: remove Artifact/sockets for jewelry; keep Bore spend, Crafting, UMG shipping, armor/weapon socket parity.
- Step 3: Browser checklist (manual)
Open tfa_gdd_html/figures/demo_mod_mock.html → Jewelry → Enter Ring:
- Affixes tab: change a prefix band → OK → re-enter → persists.
- Stats: Quality → Unique → Sockets shows Artifact.
- Sockets: wells=2,
p-athenp-aa, Quality Common → bannerChord: Aa. - Insert Gem →
No Chord. - Window height ~700px: only tab body scrolls; OK visible.
- Step 4: graphify
Run: graphify update . from pack root.
- Step 5: Spec status
Set design doc Status line to Implemented (mock) · 2026-10-26.
Spec coverage check
Section titled “Spec coverage check”| Spec requirement | Task |
|---|---|
| Subtabs Stats/Affixes/Sockets | 3 |
| Name / type / quality / kind auto | 2–3 |
| Affixes ≤3+3 | 1 + 3 |
| Wells 0–6 + chips | 3 |
| Artifact Unique/Apex | 1 + 3 |
| Chord Common + Phonon order | 1 + 3 |
| 1080p tab-body scroll | 2 |
| Docs unlock | 4 |
| Jewelry-only scope | Global + 3 early branch |
Placeholder scan
Section titled “Placeholder scan”None — catalogs, helpers, and UI snippets are concrete.
Type consistency
Section titled “Type consistency”draft.wells number · draft.sockets string[6] · draft.art string · matchChord(quality, wells, sockets) · commit payload mirrors draft jewelry fields.