GENERATED VIEW · PRIVATE ARCHIVE · DO NOT SERVE
RobCo Industries · Archive Museum

NATIVE_USE_AND_TERMINAL_STATS_PLAN.md



RELEASE

planning/2.8.0/plans/NATIVE_USE_AND_TERMINAL_STATS_PLAN.md

sha256 d64d4c30af12e548 · 34696 bytes · original held in the private archive

# NATIVE USE + TERMINAL STAT-EDITS + AI→NATIVE SURVEY — Implementation Plan > **Protocol 8 stage: OPUS — Diagnose & Plan.** No edits made. A Sonnet session > implements from this plan afterward, reviews it against the live files first > (line numbers drift), then runs the full pre-commit gate + Protocol 2/2a docs. > > **Branch:** `dev` (Protocol 43 — same gate as `main`). > **Baseline read at HEAD `2f4ba79`** (clean `origin/dev`). APP_VERSION `2.7.0`, > CACHE_NAME `robco-terminal-v2.7.0-r116`, 2659 tests / 199 suites. > > Parts A + B are new **user-facing** features → **MINOR** `APP_VERSION` bump is > permitted without asking (Protocol 2), but both land under the existing > `[Unreleased]` block for the v2.8.0 cycle — **do not** open a dated version > block on `dev` (Protocol 43). Served files (`js/`, `index.html`) change → > **bump CACHE_NAME** to `-r117` (Protocol 1). Part C is **report-only**. --- ## PART A — NATIVE USE (deterministic item consumption, no AI) ### A.0 — Confirmed current behavior (Protocol 27) Clicking **USE** on an inventory row sends the free-text string `> [USE] <name>` to the Director (Gemini) and lets the AI narrate + apply the effect. Quoted from [`js/ui-render.js:245`](js/ui-render.js) (`renderInventory()` click handler): ```js const use = e.target.closest('[data-use]'); if (use) { const item = state.inventory[+use.dataset.use]; if (!item) return; document.getElementById('chatInput').value = `> [USE] ${item.name}`; transmitMessage(); // ← round-trips to the AI return; } ``` The button is rendered on **every** row unconditionally ([`js/ui-render.js:234`](js/ui-render.js)). The AI side is governed only by the freeform directive line *"Consumable Purge: Upon consumption of ANY item, execute a -1 deduction"* ([`js/api.js:139`](js/api.js)) — there is **no** structured `[USE]` contract. This is also the code path behind the critical "USE dims the screen and locks out interaction" bug fixed in `ff00975` (the pre-existing unguarded dim-in inside `transmitMessage()`). Making USE native removes the AI round-trip for consumption entirely. The data source is already parsed and game-agnostic: `getChemsTable()` ([`js/db_nv.js:878`](js/db_nv.js), [`js/db_fo3.js:532`](js/db_fo3.js)) returns `{name, effect, addictionRisk, addictionDebuff, family}` for every CHEMS.CSV row. **All aid/food/chem/med items are typed `aid`** (the CSV section maps to type `aid`, e.g. [`js/db_nv.js:769`](js/db_nv.js)), so a single table + `it.type` check covers the whole consumable set for both games. ### A.1 — Effect-string patterns observed across BOTH games From the full CHEMS.CSV of `db_nv.js` and `db_fo3.js` (the `Effect` column, `/`- delimited clauses). These are the ONLY shapes the parser must recognize: | Clause shape | Examples | Deterministic action | |---|---|---| | `Restore <N> HP` | `Restore 100 HP`, `Restore 15 HP` | heal +N | | `+<N> HP` (not "Max HP") | `+20 HP`, `+16 HP` (FO3 foods) | heal +N | | `Restore HP` (no number) | FNV `Caravan Lunch`, `Pinyon Nuts` | heal +`_AID_DEFAULT_HEAL` | | `+<N> HP/s for <S>s` | `+2 HP/s for 10s` | heal +(N×S) (instant total) | | `Remove <N> Rads` | `Remove 150 Rads` | rads −N | | `+<N> RAD` | `+5 RAD`, `+32 RAD` | rads +N | | `Restore crippled limb` | Hydra | heal ONE crippled limb | | `Restore all crippled limbs` / `Heal limbs` | Doctor's Bag (FNV/FO3) | heal ALL limbs | | `Remove addiction` | Fixer | clear addiction status(es) | | `Remove Poison` | Antivenom/Antidote | clear poison status | | any timed buff clause (`+N STR`, `+25 DR`, `+25% DMG`, `+N AP`, `+N Max HP`, `Night Vision`, `Invisibility`, `Slow time …`) with **Duration > 0** | Buffout, Med-X, Jet, Psycho, Mentats, Rad-X, Steady… | add named **BUFF** status effect (duration→ticks); surfaces addiction risk in BIO-SCAN | | `… crafting ingredient` | `Stimpak crafting ingredient`, `Xander Root` | **NO use effect** — no-op, no decrement | | aid item absent from CHEMS.CSV | AI-added ad-hoc aid | **NO effect data** — no-op, no decrement | **Addiction model (deterministic, no RNG).** The app has **no** dedicated addiction state field. Addiction risk is already surfaced by `_bioScanCompute()` ([`js/ui-render.js:3714`](js/ui-render.js)), which flags any active **status effect** whose name/family matches an addictive chem (`_bioChemHasRisk()`). Therefore adding the chem's **named BUFF status effect** on use *is* the deterministic representation of "addiction risk" — BIO-SCAN will report `ADDICTION RISK: <CHEM> (active)` for free. **No random addiction roll, no persistent withdrawal-debuff application** (the `Addiction_Debuff` column stays unconsumed — implementing a full withdrawal system is out of scope and noted as future work). This keeps USE deterministic (Protocol 24) and reuses the existing addiction surfacing (Protocol 22). **Why a structured parser, not a lookup table:** the effect strings are semi-structured and shared across two games' CSVs; a token parser over the existing `getChemsTable().effect` column keeps the data as the single authority (Protocol 3) and needs **zero** new per-item data. A hand-maintained item→effect map would duplicate the CSVs and drift. ### A.2 — Shared native stat setters (used by BOTH Part A and Part B) Create these in **`js/ui-core.js`** next to the existing setters (`commitStat` @5450, `_skillVuSet` @5661, `nativeLevelUp` @3265, the bar-drag handlers @3023–3201). Each is the ONE clamp/mirror/emit/save choke point for its stat (Protocol 22), mirroring the exact idiom the drag handlers already use (set `state.X` **and** the DOM `#input.value`, then `updateMath()` + `saveState()` — the WU-N2 caps lesson: without the DOM mirror the next `syncStateFromDom()` reverts the write): - `_nativeSetHp(v)` → clamp `[0, hpMax]` (read `#stat_hp_max`/`state.hpMax`), write `#stat_hp_cur` + `state.hpCur`, `_emitStatChangeIfDiffers('hp', v)`, `updateMath()`, `saveState()`; returns clamped value. - `_nativeSetRads(v)` → clamp `[0, _resolveMaxRads()]` (the existing per-game ceiling helper @5433), write `#stat_rads` + `state.rads`, emit `'rads'`, … - `_nativeSetXp(v)` → clamp `[0, xpNext(lvl)-1]` using the SAME curve as `onXpInputChanged()` (@3229 — `75*(lvl+1)² − 25*(lvl+1) − 50`, Protocol 22, no second formula), write `#stat_xp` + `state.xp`, emit `'xp'`, … - `_nativeSetLevel(v)` → clamp `[1, MAX_PLAYER_LEVEL]` (@3252); set `#stat_lvl`+`state.lvl`; if `v > old` emit `'level.up'` `{oldLvl, newLvl}` (same event `nativeLevelUp()` emits); `updateMath()`+`saveState()`. - `_nativeSetSpecial(key, v)` → clamp `[1,10]`, write `#s_<key>` + `state[key]`, emit `'stat.change'` `{key, oldVal, newVal}` (guard `old!==v`), `updateMath()`, `saveState()`. **Refactor `commitStat(el)` to delegate** to this (extract `key=el.id.slice(2)`, `v=parseInt(el.value)` → `_nativeSetSpecial(key,v)`), so the DOM `onchange` path and the terminal path share one clamp (Protocol 22). - `_nativeSetSkill(key, v)` → thin wrapper over the existing **`_skillVuSet(key, v)`** (@5661 — already clamps `[0,100]`, mirrors `#sk_<key>`+VU fill/aria, emits `'stat.change'` `skill:<key>`, `saveState()`). Reuse verbatim. - `_nativeSetKarma(v)` → clamp `[-1000, 1000]` (matches the `_KARMA_TIERS` range @5528), write `#stat_karma` + `state.karma`, `updateMath()` (which runs `updateKarmaUI()`), `saveState()`. - `_nativeSetCaps(v)` / caps deltas already covered by `_quickLogCaps(delta)` ([`js/api.js:1373`](js/api.js)); add a tiny `_nativeSetCaps(v)` mirroring it (clamp `≥0`, mirror `#c_caps`, `_logEvent`, `updateMath`, `saveState`). Delta forms compute `cur + delta` and call the same `_nativeSet*` (one truth for both set and delta). These are `function` declarations (global scope, hoisted), callable cross-file from `ui-render.js`/`api.js` at runtime (load order: ui-render → ui-core → api; USE runs at click time, terminal handlers run after full parse). ### A.3 — The USE handler (Part A core) **In `js/ui-render.js`:** 1. **Gate the USE button to consumables.** Change the row template ([`js/ui-render.js:234`](js/ui-render.js)) to render `use-btn` **only when `cat === 'aid'`** (weapons/armor already have EQUIP; ammo/mod/misc have no consume semantics). Removes the confusing "USE a rifle → AI narration" path. 2. **Pure compute core `_computeAidUse(chemEntry)`** (side-effect-free, unit- testable like `_bioScanCompute`). Splits `chemEntry.effect` on `/`, matches each clause against the A.1 table, and returns: ``` { heal, radDelta, healLimbs: 'all'|'one'|null, clearAddiction, clearPoison, buff: {name, ticks, type:'BUFF'} | null, recognized: bool, summary: [strings] } ``` Helpers: `_durationToTicks(durStr)` — `Nh→N*10`, `Nm→max(1,round(N/6))`, `Ns→1`, `0`/unparseable → `0` (the app's canonical 6-min/tick scale from `_nativeSleep`/`_nativeWait`); a status effect is emitted iff `ticks > 0`. `_AID_DEFAULT_HEAL` — a named constant (propose `20`) for bare `Restore HP`; commented as a deterministic default (the CSV literally carries no number, Protocol 3 — not an AI guess). 3. **Executor `nativeUseItem(idx)`** (replaces the inline click handler): - Read `item = state.inventory[idx]`; if missing → return. - If `item.type !== 'aid'` → `appendToChat('> [USE] <name> is not a consumable.', 'sys')`, no decrement, return. - `chemEntry = getChemsTable().find(c => c.name.toLowerCase() === item.name.toLowerCase())` (exact, case-insensitive). If none → `> [USE] No effect data for <name>.`, **no decrement**, return. - `r = _computeAidUse(chemEntry)`. If `!r.recognized` (e.g. crafting ingredient) → `> [USE] <name> has no usable effect (<effect text>).`, **no decrement**, return. - Apply, routing through the A.2 shared setters: - `r.heal` → `_nativeSetHp(state.hpCur + r.heal)` - `r.radDelta` → `_nativeSetRads(state.rads + r.radDelta)` - `r.healLimbs === 'all'` → set `hd/la/ra/ll/rl = 'OK'`; `=== 'one'` → set the FIRST crippled limb (order hd,la,ra,ll,rl) to `OK`. - `r.buff` → `_applyStatusEffect(buff.name, buff.ticks, 'BUFF')` (see A.4). - `r.clearAddiction` → drop `state.status` entries matching an addictive chem (`_bioChemHasRisk`); `r.clearPoison` → drop `name`-contains-"poison". - **Decrement qty by 1** (only reached when ≥1 effect applied), mirroring `adjItemQty()`'s "0 → splice the row" semantics. - Re-render: `updateMath()` (bars) is called by the setters; additionally `renderStatus()` if a buff/removal changed `state.status`, `renderInventory()` for the qty, and a limb re-render (`loadUI()` if limbs changed — heaviest but correct; else the targeted renders). `saveState()` is called by the setters; call once more after qty change. - `appendToChat('> [USE] ' + r.summary.join(' · ') + ' — ' + qtyLeft + ' remaining.', 'sys')`. - Update the click handler @245 to call `nativeUseItem(+use.dataset.use)` instead of the `chatInput`+`transmitMessage()` pair. ### A.4 — Reuse `addStatusEffect()` (Protocol 22) Extract the effect-push core from `addStatusEffect()` ([`js/ui-render.js:746`](js/ui-render.js)) into `_applyStatusEffect(name, ticks, type)` that both the DOM add-form and `nativeUseItem()` call — it keeps the existing dedup (update ticks/type if the compound is already active), the `RobcoEvents.emit('effect.applied', …)` + `_pendingEffectWarmup.push(name)` (Wave 3 #28) in **one** place. `addStatusEffect()` becomes: read the 3 DOM inputs → `_applyStatusEffect(...)` → clear inputs → `renderStatus()`+`updateMath()`. ### A.5 — Acceptance criteria (Protocol 26) — enumerated cases Verified on the real UI at 360/412/desktop (Protocol 10) in **both** games: 1. **Stimpak** (FNV `Restore HP`… actually `Stimpak,Restore HP`? no — FNV has no plain Stimpak number; FO3 `Restore 30 HP`) → HP rises by the parsed amount, clamped at `hpMax`; qty −1; row vanishes at qty 0. 2. **Super Stimpak** FNV `Restore 100 HP` / FO3 `Restore 100 HP / -1 STR (1m)` → +100 HP (the `-1 STR (1m)` clause is a timed debuff clause with duration → status-effect representation via the duration column, **not** a permanent SPECIAL change). 3. **RadAway** `Remove 150 Rads` → rads −150, clamped ≥0; qty −1. 4. **Rad-X** `+25 Rad Resistance` (Duration 4m) → BUFF status `Rad-X` added (ticks from 4m), no HP/rad change. 5. **Buffout** `+2 STR / +2 END / +60 Max HP` (4m, 25% risk) → BUFF status `Buffout` added; BIO-SCAN now flags `ADDICTION RISK: BUFFOUT (active)`; **no** permanent SPECIAL/hpMax change; qty −1. 6. **Food w/ rads** FO3 `Cram` `+10 HP / +5 RAD` → +10 HP **and** +5 rads. 7. **Bare `Restore HP` food** (FNV `Caravan Lunch`) → +`_AID_DEFAULT_HEAL` HP. 8. **Regen food** `+2 HP/s for 10s` → +20 HP instant. 9. **Hydra** `Restore crippled limb` → one crippled limb → OK (no-op message if none crippled — but still a valid use? propose: if nothing to heal, treat as recognized but "no effect right now", **no decrement**). 10. **Doctor's Bag** `Restore all crippled limbs` / FO3 `Heal limbs` → all limbs OK. 11. **Fixer** `Remove addiction` → clears addictive-chem status effects. 12. **Antivenom/Antidote** `Remove Poison` → clears poison-named status. 13. **Crafting ingredient** (`Broc Flower` `Stimpak crafting ingredient`) → "no usable effect" message, **no decrement**, no state change. 14. **Aid item not in CHEMS.CSV** → "No effect data", **no decrement**. 15. **Non-aid item** (should not show USE) → if forced, "not a consumable", no decrement. 16. **Zero AI**: no `fetch`/`transmitMessage` on any USE path (grep-guarded). 17. **Offline-safe** (Protocol 24): works with no key/no network. 18. **Both games** exercise the same code via `getChemsTable()` (Protocol 38 — no hardcoded item names in the handler). ### A.6 — Files / functions / line ranges (Part A) | File | Change | |---|---| | `js/ui-render.js` | Gate `use-btn` render to `cat==='aid'` (@234); rewrite click handler (@245–258) → `nativeUseItem(idx)`; add `_computeAidUse`, `_durationToTicks`, `_AID_DEFAULT_HEAL`, `nativeUseItem`; extract `_applyStatusEffect` from `addStatusEffect` (@746). | | `js/ui-core.js` | Add the A.2 shared setters; refactor `commitStat` (@5450) → `_nativeSetSpecial`. | | `js/db_nv.js` / `js/db_fo3.js` | **No change** — `getChemsTable()` already exposes everything. | | `js/api.js` | **No change for Part A** (the "Consumable Purge" directive line @139 stays — free-text "I use a stimpak" during narrative is still valid AI behavior; only the button stops calling the AI). | | `sw.js` | Bump CACHE_NAME → `-r117`. | --- ## PART B — TERMINAL comm-link edits EVERY stat (deterministic, no AI) ### B.0 — Confirmed current behavior (Protocol 27) TERMINAL mode routes a submitted line through `transmitTerminal()` ([`js/api.js:1497`](js/api.js)) → `_routeNativeCommand()` (native `[TOKEN]`s) → `_routeQuickLogMulti()` (comma-split) → `_routeQuickLog()` ([`js/api.js:1459`](js/api.js)), which matches against `QUICK_LOG_PATTERNS` ([`js/api.js:1414`](js/api.js)). Today only **four** patterns exist: `kill`, `caps`, `location`, `faction`. Anything else — `+50 rads`, `str 8`, `leveled up` — falls through to the hint at [`js/api.js:1529`](js/api.js): ```js appendToChat( '> [TERM] UNRECOGNIZED — did you mean a native command (see [FEATURES]) or a quick-log entry like "killed <target>", "+50 caps", "arrived <location>", or "rep <faction> up/down"?', 'sys'); ``` Autocomplete for `#chatInput` comes from `_commandSuggestions()` ([`js/api.js:1664`](js/api.js)) + `_quickLogContentSuggestions()` (@1606), both already iterating `QUICK_LOG_PATTERNS`. The `[FEATURES]` help modal renders `COMMAND_REGISTRY` ([`js/ui-core.js:5223`](js/ui-core.js), `showHelpModal` @5329). Comma multi-action (`_routeQuickLogMulti`) already splits on commas and routes each segment, so adding patterns yields multi-edit lines for free. ### B.1 — Grammar (owner-LOCKED: both set + delta) All matched after `_stripPrompt()`, case-insensitive, per comma-segment. **New** `QUICK_LOG_PATTERNS` entries (added to the existing array so `_routeQuickLog`, `_routeQuickLogMulti`, and autocomplete pick them up with zero new plumbing): 1. **Level-up phrase** — `re: /^level(?:ed)?\s*up$/i` → `nativeLevelUp()`. (Placed before the generic set pattern; has no numeric arg.) 2. **Generic stat SET** — `re: /^([a-z][a-z _]*?)\s+(\d+)$/i` → resolve group 1 via `_resolveStatToken()`; apply group 2 through the matching A.2 setter. Covers `hp 80`, `rads 50`, `xp 200`, `level 5`, `karma 100`, `caps 30`, `str 8`, `guns 45`, `energy weapons 60`, `small_guns 45`. 3. **Generic stat DELTA (leading sign)** — `re: /^([+-]\d+)\s+([a-z][a-z _]*?)$/i` → resolve group 2; apply `cur + (+group1)`. Covers `+2 str`, `-1 per`, `+50 rads`, `-10 hp`, `+500 xp`. 4. **Generic stat DELTA (trailing sign)** — `re: /^([a-z][a-z _]*?)\s+([+-]\d+)$/i` → resolve group 1; apply delta group 2. Covers `str +2`, `melee weapons -5`. Ordering: existing `kill`/`caps`/`location`/`faction` patterns stay **first** (zero regression — `+50 caps` still hits the specific caps pattern; the resolver also recognizes `caps` so `caps 30`/`caps +5` work via the generic set/delta). The level-up phrase and the three generic stat patterns append after them. The generic patterns' handlers **return `false`** when the token doesn't resolve, so a non-stat line (`something 5`) correctly falls through to the UNRECOGNIZED hint (the established "shape matched, content didn't" convention, e.g. `_quickLogFaction`). ### B.2 — `_resolveStatToken(token)` (game-agnostic, Protocol 38) Normalize `token` → `trim().toLowerCase().replace(/\s+/g,'_')`, then resolve: - **Scalar stats** (universal Fallout mechanics — a static alias map is fine, not a game literal): `hp` → `_nativeSetHp`; `rads`/`rad` → `_nativeSetRads`; `xp` → `_nativeSetXp`; `level`/`lvl` → `_nativeSetLevel`; `karma` → `_nativeSetKarma`; `caps` → `_nativeSetCaps`. - **SPECIAL** (universal 7-attribute set — a static token→state-key map, NOT per-game data): `str/strength→s`, `per/perception→p`, `end/endurance→e`, `cha/chr/charisma→c`, `int/intelligence→i`, `agi/agl/agility→a`, `lck/luck→l` → `_nativeSetSpecial(key, …)`. - **Skills** (per-game — MUST be data-driven): match the normalized token against `getSkillKeys()` exactly, against `getSkillKeys()` with `_`↔space, and against `SKILL_LABELS[key].toLowerCase()`. This yields `guns` (FNV) vs `small_guns`/`big_guns` (FO3) automatically — no hardcoded skill list. → `_nativeSetSkill(key, …)`. - Anything else → `null` (handler returns `false`). `SKILL_LABELS` lives in `ui-core.js` (@5608) and `getSkillKeys()` in `state.js` (@1432) — both global, callable from `api.js`. **Note:** the SPECIAL/scalar maps are universal to every Fallout title (not game-specific data), so a static map here does **not** violate Protocol 38 (which targets game-specific literals like faction keys / file paths / two-game coercions). Only the **skill** resolution must be registry-driven — and it is. ### B.3 — Hint expansion (owner spec #2) Rewrite the UNRECOGNIZED hint (@1529) to list the new stat commands concisely, e.g.: `… or a stat edit like "hp 80", "+2 str", "rads 50", "level up", "guns 45".` Keep it one line, scannable. ### B.4 — Autocomplete (owner spec #3) - Each new pattern carries `stub` / `hint` / `tag` so `_commandSuggestions()` (@1664) surfaces them automatically (it already maps `QUICK_LOG_PATTERNS` → suggestions). Propose stubs: `'hp 80'`, `'+2 str'`, `'level up'`, `'rads 50'`. - Add a `_statTokenSuggestions(text)` branch to `_quickLogContentSuggestions()` (@1606) so a partial token completes: typing a fragment that prefix-matches a SPECIAL alias, a scalar-stat name, or a `getSkillKeys()` key suggests `"<token> "` (then the user types the value). Game-agnostic — skill names come from `getSkillKeys()`/`SKILL_LABELS`, never a literal list. ### B.5 — Help modal / COMMAND_REGISTRY (owner spec #4) Add a group (or entries) to `COMMAND_REGISTRY` ([`js/ui-core.js:5223`](js/ui-core.js)) under a new **`STAT EDITS — TERMINAL, OFFLINE`** heading, e.g.: `{cmd:'<stat> <N>', desc:'Set any stat: hp, rads, xp, level, karma, caps, a SPECIAL (str/per/…), or a skill. Offline.'}`, `{cmd:'+N <stat> / <stat> +N', desc:'Nudge a stat up/down by N. Offline.'}`, `{cmd:'level up / leveled up', desc:'Gain one level (deterministic). Offline.'}`. `showHelpModal()` renders it unchanged. This keeps `[FEATURES]` in lock-step (the Suite 113 registry↔router consistency guard covers this pattern). ### B.6 — Acceptance criteria (Protocol 26) Both games, verified by driving `transmitTerminal(text)` behaviorally: 1. `hp 80` → HP set to 80 (clamped ≤ hpMax); `#stat_hp_cur` + `state.hpCur` both updated; no revert after `saveState()`. 2. `+2 str` / `str +2` → STR = min(10, cur+2); `-1 per` → PER = max(1, cur−1). 3. `str 8` → STR set to 8; `str 99` → clamped to 10; `str 0` → clamped to 1. 4. `guns 45` (FNV) / `small_guns 45` (FO3) → skill set via `_skillVuSet` (VU fill repaints); FO3 `guns 45` → UNRECOGNIZED (no such skill) — proves per-game. 5. `rads 50` / `+50 rads` / `-30 rads` → clamped `[0, maxRads]`. 6. `xp 500` → clamped to `xpNext(lvl)-1`; `+500 xp` → delta then clamp. 7. `level 5` → set; `level up` / `leveled up` → +1 (≤ MAX_PLAYER_LEVEL), emits `level.up` (jingle/haptic/auto-log subscribers fire). 8. `karma 250` / `-100 karma` → clamped `[-1000,1000]`; karma tier UI updates. 9. `caps 30` / `+5 caps` → existing caps pattern still handles `+5 caps`; `caps 30` set works via resolver. 10. **Multi-action**: `hp 100, +2 str, rads 0, killed 2 raiders` applies ALL four (comma routing via `_routeQuickLogMulti`), one collated hint if any segment fails. 11. Unknown token `foo 5` → falls through to UNRECOGNIZED hint (not a silent no-op). 12. Autocomplete surfaces the new commands; `[FEATURES]` lists them. 13. Zero AI on every path (no `fetch`, no `transmitMessage`). ### B.7 — Files / functions / line ranges (Part B) | File | Change | |---|---| | `js/api.js` | Add 4 `QUICK_LOG_PATTERNS` entries (@1414); add `_resolveStatToken()`; expand UNRECOGNIZED hint (@1529); add `_statTokenSuggestions` branch to `_quickLogContentSuggestions` (@1606). | | `js/ui-core.js` | A.2 shared setters (shared with Part A); add STAT-EDITS group to `COMMAND_REGISTRY` (@5223). | | `sw.js` | CACHE_NAME `-r117` (shared with Part A). | --- ## Cross-cutting: Protocol 8 plan-audit (entry paths / states / edge cases) - **Entry paths to consumption:** the USE button (native now) + free-text "I use X" typed to OVERSEER (still AI narrative — unchanged, not our concern) + the AI's own `autoImportState()` state writes (unchanged). Only the **button** changes. ✔ - **Entry paths to stat edits:** DOM inputs (`commitStat`/bar drags/VU drag/`onXpInputChanged`/`nativeLevelUp` — unchanged, now some route through the shared setters) + TERMINAL grammar (new) + AI `autoImportState()` (unchanged). All clamp identically because the shared setters own the clamp. ✔ - **Mode resolution:** `/msg` forces TERMINAL, `@msg` forces OVERSEER (`_resolveCommandInput`) — stat grammar only runs in the TERMINAL branch, so an `@`-pinged AI message is never intercepted. ✔ - **Comma multi-action:** each segment independently routed; a native `[TOKEN]` runs on the whole unsplit line first, so a token's args aren't comma-split. New stat patterns are pure per-segment. ✔ - **Empty / malformed:** parsers return `false`/`recognized:false` → hint or no-op, never a throw; setters `parseInt`-guard and clamp (no `NaN` reaches state — the Suite 133 `|| 0` lesson). ✔ - **Games:** FNV (`guns`) vs FO3 (`small_guns`/`big_guns`) via `getSkillKeys()`; FO4 design-only entry (`skillKeys: []`) degrades to "no skills resolvable" with no throw. ✔ - **Offline / no-key:** every path is pure-local (Protocol 24/32 — nothing new to register with the kill-switch; no network). ✔ - **Persistence round-trip:** setters mirror to DOM so `saveState()` → `syncStateFromDom()` reads the new values back (no revert); serialized-whole into `robco_v8`, survives sanitize→migrate (Protocol 34 — no new state field, so no Protocol 4 work). ✔ - **UTF-8 (Protocol 39):** all edits via the Edit tool / Node — never PowerShell writes (the files contain box-drawing/emoji). ✔ **No new `state` field is introduced** (Part A reuses `status`/`hpCur`/`rads`/ limbs; Part B edits existing fields), so Protocol 4 (4-file state-field dance) does **not** apply. `state.padBindings` remains the only recent additive field. --- ## Regression tests (Protocol 13/15/42 — both runners at parity, Protocol 2a) Add **Suite 200** (Native USE) and **Suite 201** (Terminal stat edits) to BOTH `tests/robco-diagnostics.js` and `tests/robco-diagnostics.ps1` at identical coverage/counts, plus the runtime mirror `tests/test.html` where the import contract is touched (it is not here — no new state field — so only the `Suites: N` marker if a runtime suite were added; these are static/behavioral, so no test.html change beyond confirming none is needed). **Suite 200 — Native USE (mix of static + VM-behavioral):** 1. Static: `nativeUseItem` defined in `ui-render.js`; the row click handler calls it and **not** `transmitMessage()` (no-AI guard — grep the handler body). 2. Static: `use-btn` rendered only for `cat==='aid'`. 3. Static: `_applyStatusEffect(name,ticks,type)` extracted; `addStatusEffect` delegates to it (Protocol 22 — one emit site for `effect.applied`). 4. Behavioral (Node `vm`, load `state.js`+`reg_nv.js`+`db_nv.js` + the extracted `_computeAidUse`/`_durationToTicks`, the Suite 133/151 sandbox idiom): - Stimpak/Super-Stimpak → `heal` = parsed N. - RadAway → `radDelta` = −150. - Cram (FO3) → heal +10 **and** radDelta +5. - Buffout → `buff.name==='Buffout'`, `buff.ticks>0`, no permanent SPECIAL. - bare `Restore HP` → heal `_AID_DEFAULT_HEAL`. - `+2 HP/s for 10s` → heal 20. - Doctor's Bag → `healLimbs==='all'`; Hydra → `'one'`. - crafting ingredient → `recognized:false`, no ops. - `_durationToTicks`: `4m→1`, `1h→10`, `30s→1`, `0→0`. 5. Behavioral: running the FULL apply against a synthetic state heals HP clamped at hpMax; qty decrements; qty-0 removes the row. 6. Behavioral: a crafting-ingredient / unknown-aid use leaves qty **unchanged**. 7. Static (Protocol 38): the USE handler contains no hardcoded game item name (Stimpak/RadAway/etc.) — items resolved via `getChemsTable()`. 8. Static (Protocol 24): no `fetch`/`XMLHttpRequest`/`transmitMessage` anywhere in `nativeUseItem`/`_computeAidUse`. 9. Behavioral: adding Buffout's status then running `_bioScanCompute` yields an `ADDICTION RISK: BUFFOUT` advisory (proves the addiction-surfacing reuse). **Suite 201 — Terminal stat edits (mostly VM-behavioral):** 1. Static: the 4 new `QUICK_LOG_PATTERNS` entries + `_resolveStatToken` exist. 2. Behavioral (`vm`, load `state.js`+`reg_nv.js` + the shared setters + the terminal router bodies against a synthetic DOM, the Suite 151/152 idiom): `hp 80`→state.hpCur 80 clamped; `str 8`/`str 99`(→10)/`str 0`(→1); `+2 str`/`str +2`/`-1 per`; `rads 50`/`+50 rads`/`-30 rads` clamped; `xp 500` clamped to band; `level 5`; `level up`/`leveled up` (+1, emits `level.up`); `karma 250`/`-100 karma`; `caps 30`. 3. Behavioral (Protocol 38): FNV `guns 45` sets the skill; **FO3** `guns 45` returns `false` (UNRECOGNIZED) while `small_guns 45` sets it — proves registry-driven skill resolution, no literal list. 4. Behavioral: multi-action `hp 100, +2 str, rads 0` applies all three (`_routeQuickLogMulti`). 5. Behavioral: `foo 5` → `false` → UNRECOGNIZED path (no silent no-op). 6. Behavioral: DOM mirror — after `hp 80`, `#stat_hp_cur.value==='80'` so a following `syncStateFromDom()` does not revert it. 7. Static: `commitStat` delegates to `_nativeSetSpecial` (shared clamp, one truth); the SPECIAL clamp stays `[1,10]`, skill `[0,100]`. 8. Static: UNRECOGNIZED hint text lists the new stat commands; `COMMAND_REGISTRY` has the STAT-EDITS group; `_commandSuggestions` surfaces a new stub (registry↔router↔help lock-step, the Suite 113 pattern). **Protocol 2a count sync (same commit):** update the hardcoded `2659`→ new total in `CLAUDE.md` (5 sites), `RULES.md`, `README.md` (4 sites), `ARCHITECTURE.md`, `CHANGELOG.md` `[Unreleased]` header comment, both `robco-diagnostics.*` per-suite markers, and `tests/test.html` `Suites:` marker if a runtime suite is added. Run the `Select-String` drift scan from CLAUDE.md Protocol 2a before committing. **Docs (Protocol 2):** `CHANGELOG.md` `[Unreleased]` (plain-English, Protocol 21 — "USE now applies effects instantly offline"; "type any stat change straight into the TERMINAL command line"); `ARCHITECTURE.md` (version header + the **Native-Input-Path Audit** table — USE + all stat edits now have native setters; the U10 `state.equipped`/affinity notes get company); `README.md` Current-State + feature table. --- ## PART C — AI → NATIVE SURVEY (report-only; owner decides) Every remaining feature that still calls Gemini or depends on the network, classified **convertible-to-native** / **must-stay-AI** / **hybrid**. Nothing here is planned or built beyond USE + terminal stats. ### C.1 — Gemini (AI) surfaces | Feature | Where | Class | Notes / native sketch | |---|---|---|---| | **Director narrative** (freeform story, combat outcomes, dialogue, world reactions) | `transmitMessage()` → Gemini, Tri-Node JSON | **MUST STAY AI** | The generative core. Inherently open-ended; no deterministic equivalent. Keep. | | **`[USE]` consumption** | button → `transmitMessage` | **CONVERTIBLE** ← *this plan, Part A* | Fully deterministic from CHEMS.CSV. Done here. | | **VISUAL UPLOAD** (screenshot → inventory parse) | `attachedImageData` → Gemini vision; directive @143 | **HYBRID / CONVERTIBLE (owner goal)** | Owner wants on-device OCR eventually. Sketch: bundle a WASM OCR (e.g. Tesseract) or use the browser's `Shape/TextDetector` where available → parse item lines → cross-ref `lookupItemInDb()` for wgt/val/type → additively merge (the `item.added` diff path already exists). Fully offline once OCR is local. Fallback to AI vision when OCR unavailable/low-confidence = the hybrid. **Largest of the remaining conversions.** | | **`[GPS]` / `[MAP]` compass grid** | AI `modal.type:'GPS'` (directive @89–92) | **CONVERTIBLE** | The app already ships a native **CARTOGRAPHY TABLE** (`renderWorldMap`, SVG node map from `FALLOUT_REGISTRY.locations` + `state.locationHistory`). A native `[GPS]` could render a localized compass slice of that data offline — no AI. | | **`[ROADMAP]` perk roadmap** | AI `modal` (COMMAND_REGISTRY @5297) | **HYBRID** | Perk data is in the registry (`level_taken`, ranks), so a native "perks you qualify for / upcoming by level" table is convertible; but a *build-goal-aware* roadmap ("toward a sniper build") needs generative reasoning → keep an AI mode, add a deterministic "eligible perks by level" native view. | | **LEVEL-UP skill-point award** | AI modal on `[LEVEL UP]` (directive @162) | **HYBRID** | The **native LEVEL UP button** already exists (`nativeLevelUp`, deterministic +1). The AI's *skill-point distribution* modal (`10 + INT/2` points) is a generative allocation; a native "here are N points, allocate via the skill VU meters" flow is convertible (the VU setters exist) — worth considering. | | **Gemini key validation** | `fetchAuthorizedModels()` (@272) | **MUST STAY NETWORK** | Support infra for the AI, not generative. Only runs when a key is entered. | ### C.2 — Network (non-AI) surfaces — not "AI", included for completeness | Feature | Where | Class | Notes | |---|---|---|---| | **Cloud save sync** (push/pull, version history) | `cloud.js` (Firestore) | **MUST STAY NETWORK** | It *is* remote storage; "native" = the already-shipped local slots + rolling backups + IDB, which are the offline fallback. Not an AI conversion. | | **Auth** (Google sign-in, anon) | `cloud.js` / `ui-account.js` | **MUST STAY NETWORK** | Popup-only (Protocol 30). Gated + offline-degrading already. | | **Remote kill-switch config** | `loadRemoteConfig()` (Firestore `/config/flags`) | **MUST STAY NETWORK** | Fail-safe by design (Protocol 33); LKG-cached. | | **Gemini-key cloud sync** | `saveGeminiKeyToCloud` / secrets doc | **MUST STAY NETWORK** | Convenience sync of the key; local mirror already exists. | | **AI Studio external link** | `ui-account.js` anchor | n/a | Static outbound link. | ### C.3 — Survey takeaways (for the owner to prioritize) - **Genuinely generative → keep AI:** the Director narrative, and the *reasoning* half of ROADMAP / build-goal advice. These are the product's soul. - **Best next native conversions (deterministic data already present):** 1. **`[GPS]`/`[MAP]`** → trivial, reuses `renderWorldMap` data (offline today). 2. **LEVEL-UP point allocation** → reuses the skill VU setters. 3. **ROADMAP "eligible perks by level"** native view (keep AI for goal-aware). - **Biggest, owner-flagged:** **VISUAL UPLOAD → on-device OCR** — the one large, high-value conversion; hybrid (local OCR primary, AI vision fallback) is the safe path and fits Protocol 32's kill-switch+fallback model. - **Leave as-is:** all `cloud.js`/auth/config network surfaces (not AI; already offline-degrading). --- ## Suggested implementation order for Sonnet 1. Add the A.2 shared setters in `ui-core.js`; refactor `commitStat` to use one. (Foundation for both parts.) 2. Part A: `_computeAidUse`/`_durationToTicks`, `_applyStatusEffect` extraction, `nativeUseItem`, USE-button gating + handler swap. 3. Part B: `_resolveStatToken`, 4 new patterns, hint + autocomplete + registry. 4. Suites 200 + 201 in both runners; Protocol 2a count sync everywhere. 5. Docs (CHANGELOG/ARCHITECTURE/README); CACHE_NAME `-r117`. 6. Full gate (`npm run gate`) + `render-check.mjs`; verify USE + a few terminal edits live at 360/412/desktop in BOTH games (Protocol 10/26). Batch → ONE push (Protocol 19).
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).