Skip to content

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

  • 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 #editTabBody scrolls.
  • Source of truth for logic: demo_mod_mock_data.js + demo_mod_mock_app.js; after edits run python tfa_gdd_html/figures/_bundle_mock.py.
  • Do not git commit unless the user explicitly asks.

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

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_DATA return 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 ids
    • chipById(id) -> {id,name,kind}|null
    • kindFrom(quality, baseType) -> string e.g. "Rare Jewelry" / "Apex Energy Shield"
    • isArtifactEligible(quality) -> boolean
    • isChordBase(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 in demo_mod_mock.html head or inject style block via app; prefer editing styles once in HTML source before data script, then re-bundle app only… Styles live in demo_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), art
    • isJewelryEdit(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, "&quot;") + '" />');
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 bindDraftInputs for 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 + onCharacter merge

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


Files:

  • Modify: regenerate tfa_gdd_html/figures/demo_mod_mock.html via 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.md if Demo Mod jewelry bullets exist

  • Step 1: Re-bundle

Run from pack root:

Terminal window
python tfa_gdd_html/figures/_bundle_mock.py

Expected: 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 edits

If 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:

  1. Affixes tab: change a prefix band → OK → re-enter → persists.
  2. Stats: Quality → Unique → Sockets shows Artifact.
  3. Sockets: wells=2, p-a then p-aa, Quality Common → banner Chord: Aa.
  4. Insert Gem → No Chord.
  5. 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 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

None — catalogs, helpers, and UI snippets are concrete.

draft.wells number · draft.sockets string[6] · draft.art string · matchChord(quality, wells, sockets) · commit payload mirrors draft jewelry fields.