RELEASE
planning/2.5.0/designs/SKILLBOOKS_TRACKER_DESIGN.md
sha256 8daffdd5a4a7642b · 11380 bytes ·
original held in the private archive
# SKILL BOOKS TRACKER — Build-Ready Spec
> Design-only. No code changes. Targets `origin/main` @ `1fad1ea` (v2.5.0-r32, 1020 tests).
> Mirrors the existing Collectibles / Lincoln Memorabilia / Traits trackers exactly.
## Pattern verified (current code)
- **Collectibles** = binary checklist: `state.collectibles = []` (string[] of found names), `FALLOUT_REGISTRY.collectibles` per game, `renderCollectibles()` reads registry + a `Set` of found names, shows `[found/total]` in the sub-panel `<summary><h3>`, found-first then missing, click-to-toggle. **This is the closest mirror for skill books** (binary, per-game catalog).
- **Lincoln** uses a `{name: disposition}` map only because it tracks a _value_ (disposition). Skill books are pure found/missing → **the array model (collectibles/traits) is the correct, simpler fit.**
- **Traits autoImportState** (`api.js:770`) = array + registry-name filter + dedup → the exact validation to copy.
- Panels: `<details class="panel" data-tab="…">` → `<details class="sub-panel" data-sub-id="…">` (collapse persisted in `robco_panel_state`); `loadUI()` calls each `render*()` (`ui-core.js:1394–1396`); `_updatePanelBadges()` h2→count (`ui-core.js:797`); `expandPanelForCategory` tabMap (`ui-core.js:931`).
---
## A. State model
**`state.skillBooks = []`** — a `string[]` of read/found skill-book names (binary). Recommended over a `{name:true}` map: it's the binary-correct shape and identical to `state.collectibles`/`state.traits` (Lincoln's map exists only for its disposition value). Per-campaign automatically — it lives in `state`, which is serialized whole into `robco_v8.campaigns[ctx]`. Round-trips through save/migrate (see §D).
## B. Registry
Add **`FALLOUT_REGISTRY.skillBooks = [{ name, skill }]`** to **both** `reg_nv.js` (13) and `reg_fo3.js` (13), read-only (P23), placed alongside the other tracker arrays (near `collectibles`/`traits` in `reg_nv`, near `collectibles`/`lincolnMemorabilia` in `reg_fo3`). `skill` = the **exact app skill KEY** from `getSkillKeys()` for that game, **not the display label**.
**Builder MUST map each title to the exact key and STOP if any doesn't resolve** against `SKILL_KEYS`/`SKILL_KEYS_FO3`:
FNV (`reg_nv`, 13) — keys: `barter, energy_weapons, explosives, guns, lockpick, medicine, melee_weapons, repair, science, sneak, speech, survival, unarmed`:
```js
{ name: 'Tales of a Junktown Jerky Vendor', skill: 'barter' },
{ name: 'Nikola Tesla and You', skill: 'energy_weapons' },
{ name: 'Duck and Cover!', skill: 'explosives' },
{ name: 'Guns and Bullets', skill: 'guns' },
{ name: 'Tumblers Today', skill: 'lockpick' },
{ name: 'D.C. Journal of Internal Medicine', skill: 'medicine' },
{ name: 'Grognak the Barbarian', skill: 'melee_weapons' },
{ name: "Dean's Electronics", skill: 'repair' },
{ name: 'Big Book of Science', skill: 'science' },
{ name: 'Chinese Army: Special Ops Training Manual', skill: 'sneak' },
{ name: 'Lying, Congressional Style', skill: 'speech' },
{ name: 'Wasteland Survival Guide', skill: 'survival' },
{ name: 'Pugilism Illustrated', skill: 'unarmed' },
```
FO3 (`reg_fo3`, 13) — keys: `barter, big_guns, energy_weapons, explosives, lockpick, medicine, melee_weapons, repair, science, small_guns, sneak, speech, unarmed`:
```js
{ name: 'Tales of a Junktown Jerky Vendor', skill: 'barter' },
{ name: 'U.S. Army: 30 Handy Flamethrower Recipes', skill: 'big_guns' },
{ name: 'Nikola Tesla and You', skill: 'energy_weapons' },
{ name: 'Duck and Cover!', skill: 'explosives' },
{ name: 'Tumblers Today', skill: 'lockpick' },
{ name: 'D.C. Journal of Internal Medicine', skill: 'medicine' },
{ name: 'Grognak the Barbarian', skill: 'melee_weapons' },
{ name: "Dean's Electronics", skill: 'repair' },
{ name: 'Big Book of Science', skill: 'science' },
{ name: 'Guns and Bullets', skill: 'small_guns' },
{ name: 'Chinese Army: Special Ops Training Manual', skill: 'sneak' },
{ name: 'Lying, Congressional Style', skill: 'speech' },
{ name: 'Pugilism Illustrated', skill: 'unarmed' },
```
All 26 map cleanly against the current key lists (verified). Differences: FNV `guns`+`survival` ↔ FO3 `big_guns`+`small_guns` (no `survival`/`guns`). The `skill` is for the per-row "(boosts X)" display + the test guard.
## C. Protocol 4 wiring (every file/function)
1. **`state.js`** — default: `skillBooks: [],` (near `traits`/`lincolnItems`, ~line 355).
2. **`state.js` `migrateState()`** — `if (!Array.isArray(s.skillBooks)) s.skillBooks = [];`
3. **`api.js` `autoImportState()`** — mirror the traits block: array + registry-name filter (`FALLOUT_REGISTRY.skillBooks` names) + dedup:
```js
{
const raw = _g(parsed, 'skillBooks');
if (Array.isArray(raw)) {
const names =
typeof FALLOUT_REGISTRY !== 'undefined' && Array.isArray(FALLOUT_REGISTRY.skillBooks)
? new Set(FALLOUT_REGISTRY.skillBooks.map(b => b.name))
: new Set();
const seen = new Set();
state.skillBooks = raw.filter(b => {
if (typeof b !== 'string' || !names.has(b) || seen.has(b)) return false;
seen.add(b);
return true;
});
}
}
```
Also add `'skillBooks'` to the delta-tracking category list (the array near `api.js:823`).
4. **`api.js` `getSystemDirective()`** — add an **unconditional** schema block (both games track skill books; the registry is per-game):
```
### **Skill Books Tracker**
state.skillBooks is a string[] of skill-book titles the Courier has read. Include only names exactly as defined in the active game's skill-book registry. Update when the Courier reads a skill book.
```
(No `ctx` ternary — unlike Lincoln (FO3-only) / Traits (FNV-only), skill books exist in both games.)
5. **`ui-render.js`** — `renderSkillBooks()` mirroring `renderCollectibles()`: read `FALLOUT_REGISTRY.skillBooks` + `Set(state.skillBooks)`, set the sub-panel `<summary><h3>` to `> SKILL BOOKS [found/total]`, list found-first then missing, each row = `title — (boosts <Label>)` with `[FOUND]/[ ]` click-toggle → `toggleSkillBook(name)`. Add `toggleSkillBook(name)` (add/remove from `state.skillBooks` + `saveState()` + re-render), exactly like `toggleCollectible`.
6. **`ui-core.js` `loadUI()`** — add `renderSkillBooks();` next to `renderCollectibles()` (~`1396`).
7. **`index.html`** — new top-level panel (see §E).
8. **`ui-core.js` `_updatePanelBadges()`** — `{ h2text: '> SKILL BOOKS', count: (state.skillBooks || []).length }`.
9. **`ui-core.js` `expandPanelForCategory()` tabMap** — `skillBooks: 'stat'`.
**Suite 1 (autoImportState field-coverage auto-discovery)** will auto-detect `state.skillBooks` and assert it's covered by autoImportState + the save envelope — step 3 satisfies it (gate fails otherwise).
## D. Cloud-sync determination (state explicitly in commit message)
> **Cloud-sync determination:** `state.skillBooks` is part of the campaign `state` object, which is serialized **whole** into every save and cloud payload (`saveState`/`exportSaveFile`/`saveCurrentToCloud`/`pullFromCloud`/`loadCloudSave` all do `campaigns[ctx] = JSON.parse(JSON.stringify(state))`). Therefore the field **automatically rides the cloud save** — no separate cloud wiring. Older saves lacking it are normalized to `[]` by `migrateState()` on load. No auto-push is introduced (cloud remains manual-button only, Protocol 34).
## E. UI
- **Placement: STAT tab, near the Skill Matrix** (skill books boost skills) — a dedicated top-level `<details class="panel" data-tab="stat">` titled `> SKILL BOOKS`, structured identically to the COLLECTIBLES panel:
```html
<details class="panel" data-tab="stat">
<summary><h2>> SKILL BOOKS</h2></summary>
<details class="sub-panel" id="skillBooksSubPanel" data-sub-id="skill_books">
<summary><h3>> SKILL BOOKS</h3></summary>
<!-- render sets [found/total] -->
<div id="skillBooksDisplay"><span class="empty-state">No skill books loaded</span></div>
</details>
</details>
```
- Compact + collapsible + **persisted collapse** via `data-sub-id="skill_books"` (default-collapsed on first load), `[found/total]` in the summary h3, each row = book title + `(boosts <Skill>)` + click-to-toggle found/missing, found-first — **identical to collectibles/Lincoln**.
- **Per-game**: the active registry drives it — FNV renders its 13, FO3 its 13. **No show/hide needed** (both games have a `skillBooks` registry; unlike Lincoln which is FO3-only and hides itself). A clean FNV load shows only FNV books; FO3 only FO3 books.
## F. Test plan (new Suite, both runners, parity)
**Registry/data:** `FALLOUT_REGISTRY.skillBooks` length **13** in each game; every `skill` ∈ `getSkillKeys()` for that game; no duplicate names; sentinels — FNV has `Guns and Bullets`(guns) + `Wasteland Survival Guide`(survival); FO3 has `U.S. Army: 30 Handy Flamethrower Recipes`(big_guns) + `Guns and Bullets`(small_guns) and **no** `Wasteland Survival Guide`.
**Protocol 4:** `state.skillBooks` default `[]`; `migrateState` coerces non-array → `[]`.
**autoImportState (behavioral):** a valid registry name is kept; a non-registry name is rejected; a non-string is rejected; duplicates de-duped.
**Render/wiring:** `renderSkillBooks` + `toggleSkillBook` defined; `renderSkillBooks` called from `loadUI`; `#skillBooksDisplay` exists; `_updatePanelBadges` has the `> SKILL BOOKS` entry; `expandPanelForCategory` tabMap has `skillBooks`.
**Coverage/persistence:** Suite 1 auto-coverage passes for `skillBooks`; a round-trip guard that `skillBooks` survives a save-envelope → load (state→serialize→migrate→state).
~14–16 guards → 1020 → ~1035. Sync count both runners + docs.
## G. Sequencing / cache / version
**ONE commit** — small and additive, identical in shape to the just-shipped Lincoln/Traits trackers (each was a single commit): registry data + Protocol-4 wiring + panel + Suite. **Cache bump r32 → r33** (served: `state.js`, `api.js`, `reg_nv.js`, `reg_fo3.js`, `ui-render.js`, `ui-core.js`, `index.html`). Docs (P2/2a): ARCHITECTURE, README, CHANGELOG `[Unreleased]` Added, test-count sync in both runners + all doc locations. **APP_VERSION stays 2.5.0** (Phase 6 → `[Unreleased]`; bumps to 2.6.0 only at Phase-6 completion).
## H. Key decisions for the owner (recommend defaults; minimal)
1. **State shape** — **(a) `string[]` of found names** _[recommended — binary, matches collectibles/traits]_ vs (b) `{name:true}` map.
2. **Toggle model** — **(a) binary found/missing** _[recommended — mirrors Lincoln/collectibles exactly]_ vs (b) count/percent (N/A — a skill book is a one-time read).
3. **Default state** — **(a) empty (all missing)** _[recommended]_ vs (b) pre-seeded.
4. **Placement** — **(a) STAT tab near the Skill Matrix** _[recommended — owner-suggested; thematically skill-related]_ vs (b) a third sub-panel inside the existing INV-tab COLLECTIBLES panel (maximal structural mirror).
All four recommendations keep it a simple found/missing checklist mirroring the Lincoln/collectibles trackers — no reason to deviate.
STAMP · generated for RELEASE v2.8.5 commit 06e5180 (06e51801b38a) · archive input-tree hash c07fbfbdd2e1ddeb · 754 files · no wall-clock timestamp (regenerates identically when nothing changed).