# Layer 18 — Character Progression, the Flashbang, and the Condition Gold Economy **CodexOfConquest v3 — Engineering Design Record · Verification Pass** **Classification:** Game Mechanics · Combat Action Economy · Progression Design **Original:** 2026-05-21 · **Verified against `play.html` (38,712 lines):** 2026-08-12 (§DOC-02t) **Original status claim:** *"Complete — Layer 18 implemented and verified against `play.html` (8,236 lines)"* **Verified status:** **Three of four subsystems live and largely exact; the fourth — character progression — was replaced wholesale, by a design this report's own §II.A explicitly forbade.** > **HISTORY DOCUMENT.** Per the Lab Report Policy this file is annotated, never rewritten to match > HEAD. Every claim below that did not ship is marked **NOT SHIPPED** and **kept** — a silently > deleted claim reads as one that held. --- ## Abstract This report specified Layer 18 of *The Shattered Codex* combat layer: (1) a ten-level character progression system consuming the existing XP counter, (2) the **Flashbang**, a guaranteed-advantage bonus-action consumable, (3) a **×100 repricing** of `CONDITION_GOLD`, and (4) a unifying analysis of the *item-as-half-action* economy. Re-measured 83 days later: **42 of 44 named identifiers resolve (95 %)**, and of the report's own 22-row Verification Manifest **15 hold outright, 5 are present but semantically changed or inert, and 2 never shipped at all**. The two dead identifiers — `XP_LEVELS` as specified and `_LEVEL_REWARDS` — return **0 commits ever**. The condition repricing is the durable result: `CONDITION_GOLD` is **byte-identical at the earliest surviving build and at HEAD**, unchanged across 79 days and 24,476 lines of file growth. The progression system is the casualty: what shipped is a D&D Fighter advancement track to **level 20** delivered through an interactive modal with player-allocated ability-score improvements — the precise shape §II.A ruled out. --- ## I. Intent, Inspiration, and What It Buys the Player *(Restated from the original Abstract, §I, §II.A and §VII, because it is the part of this document that survived best and is the reason the work was done.)* **The inspiration** is tabletop D&D compressed to a browser tab. CodexOfConquest's story battle is a **rapid-play** interface: the player makes 2–4 decisions per round, and a full battle resolves in under two minutes. Where tabletop spends its time on dice, narration and table consensus, CodexOfConquest spends it on *decision clarity*. The governing constraint the report set for itself: > **Every mechanical addition must pay for itself in decision density per second of play, not in > absolute mechanical richness.** **The playability problem each subsystem solves:** | Subsystem | The playability gap it closes | Why it makes the game better to play | |---|---|---| | **Character levels** | XP had accrued since Layer 12 with **no downstream effect** — a counter the player could watch but draw no inference from. A motivational dead end. | Closes the reward loop. Every fight now advances something the player can feel in the *next* fight, so grinding a corridor of trivial encounters stops being pure time-spend. | | **Flashbang** | Spell Scrolls were the only ADV-setup item and they **fail 25–60 % of the time**, so setup play was unreliable exactly when it mattered (a boss). | Adds a *certainty* option beside the *efficiency* option. The player now chooses a risk posture, not just an item — the decision is denser at identical UI cost. | | **`CONDITION_GOLD` ×100** | At 10–50 gp a condition item cost less than a healing potion while granting ADV **plus** a status for three rounds. Using one was never a real choice. | Restores contest to the pre-battle screen. "Spend or save" becomes a genuine fork, which is the only thing that makes the pre-battle screen worth opening. | | **The 0.5-action rule** | Nothing unified potions, scrolls, shields and grenades, so each addition risked a new special case. | One rule — *every item costs the bonus action* — means no item is unconditionally optimal, and **emergent** strategy (the three-potion arc, the shield-vs-items trade) appears without a dedicated mechanic for it. | **The design rule extracted from all four, and still the correct one:** *deepen the decisions the player already makes; do not add new ones.* Layer 18 adds options to the existing bonus phase and power to the existing attack roll. It adds **zero** decision points per round. That principle is intact at HEAD — and it is precisely the principle the shipped progression system traded away. --- ## II. Method Per §DOC-02 program instruments: 1. **Batch census** — all 44 named identifiers through one `grep -c` pass before reading the report. 2. **`git log -S` on every dead symbol** — separates **RETIRED** (shipped, later removed) from **NOT SHIPPED** (never existed under that name). 3. **Archive adjudication** — this report predates the earliest surviving build, so `32c10c5` (2026-05-24, 14,377 lines) is read as the nearest witness, three days downstream. 4. **Copy-vs-compose** — transcribed data is weighted as evidence; composed illustrations as claims. 5. **Census cross-check** — every extraction total reconciled against `npm run stats` before any delta was derived from it. **Dating caveat (corpus rule, instrument 18).** The report's baseline — *"8,236 lines, Layers 0–18 complete"* — **matches no commit**; the first surviving build is 14,377 lines. This document describes a working tree that was never committed, so its own line citations (`~5979–5983`) are scored **UNVERIFIABLE**, not wrong, and the report is dated by the birth of the things it describes. --- ## III. As-Built Inventory (HEAD, 2026-08-12) **Condition economy — exact.** `const CONDITION_GOLD = {@24618` · `const CONDITION_ITEMS = [@22409` · `function storyPreBattle(node) {@36399` **Flashbang — exact.** `const COMBAT_ITEMS = [@24465` · `function storyBuyFlashbang() {@24366` · `function _renderSboSpells() {@24909` · `function _storyUseFlashbang(invIdx) {@24934` · `function _playerHasBonusOptions() {@24977` · `{ weight:6, _type:"flashbang" },@24522` **Progression — shipped under a different architecture.** `const XP_LEVELS = [@24418` (20 entries) · `const FIGHTER_FEATURES = {@25504` (19 entries, Lv2–20) · `const _ASI_LEVELS = new Set([4, 6, 8, 12, 14, 16, 19]);@25526` · `function _showLevelUpModal(lvl) {@25615` · `function _checkLevelUp() {@25671` · `function _extraAttackCount() {@24995` · `const critMin = _lv >= 20 ? 17@25027` **Integration points named by the report.** `function _storyBattleVictory() {@25280` · `_levelUpQueue = []; // reset before each battle resolution@25296` · `const lvlAtk@25030` · `function _calcPlayerAc() {@24610` · `const nextThresh = XP_LEVELS[curLv];@25469` · `const lvEl = document.getElementById('s-level');@36136` --- ## IV. Spec → Shipped Delta Table Two-way: **REPORT-ROT** = the report aged. **ENGINE-ROT** = HEAD lost something specified. **NOT SHIPPED** = never existed. **EXPANDED** = HEAD exceeds the spec. | # | Report claim (§) | HEAD | Verdict | |---|---|---|---| | 1 | `CONDITION_GOLD` ×100, all 12 values (§IV.C) | All 12 exact, byte-identical at archive **and** HEAD | ✅ **EXACT — 79 days unchanged** | | 2 | Condition item source = *"Pre-existing inventory"* (§V.A) | Not inventory-gated: `// All condition items available for gold purchase (not inventory gated)@36403` | **NOT SHIPPED — superseded, and it strengthens §IV** | | 3 | Cheapest condition = Feint Scroll 1,000 gp (§IV.C) | Live hint string names exactly that | ✅ EXACT | | 4 | `COMBAT_ITEMS` Flashbang `cost:150, sell:75` (§III.B) | Byte-identical | ✅ EXACT | | 5 | Flashbang = vendor purchase, scroll = loot drop (§III.B contrast) | Flashbang is **also** a 6 % `_D100_TABLE` loot row | **REPORT-ROT — EXPANDED** | | 6 | Flashbang guards: attack first · free hand · splice · `spellAdvantageReady` · enemy turn (§III.B/VI) | All five, in order | ✅ EXACT | | 7 | Spell scroll DC = `d20 ≥ AC − 2` (§III.A) | Live tooltip: *"d20 vs your AC−2"* | ✅ EXACT | | 8 | `_playerHasBonusOptions()` flashbang check (§VI) | Line present — but **every branch returns true** | ⚠️ **PRESENT BUT INERT** | | 9 | `XP = OpponentAC × OpponentMaxHP` (§II.B) | Live, plus a party multiplier | ✅ EXACT — EXPANDED | | 10 | §II.B tier AC/HP/XP *"empirical range"* table | Misses at the archive **and** HEAD | ❌ **WRONG WHEN WRITTEN** (Finding 5) | | 11 | `XP_LEVELS = [0, 500, … 90000]`, 10 levels (§II.C) | 20 entries, every threshold different; specified literal = **0 commits ever** | ❌ **NOT SHIPPED** | | 12 | `_LEVEL_REWARDS` flat hp/atk/ac table (§II.D) | **0 commits ever**; replaced by `FIGHTER_FEATURES` | ❌ **NOT SHIPPED** | | 13 | *"Rewards are automatic — zero player overhead"* (§II.A.3, §VII) | Modal requires an HP roll click, an optional bonus-d10, and 2 ASI allocations at 7 levels | ❌ **REVERSED** (Finding 1) | | 14 | `_checkLevelUp()` applies rewards + populates `#svo-levelup` (§II.E) | Queues only; caller writes the banner; rewards live in `_showLevelUpModal` | ⚠️ **PARTIAL** | | 15 | `_checkLevelUp()` recursive + max-level guard (§II.E) | Both exact, guard now reads level 20 | ✅ EXACT | | 16 | `_checkLevelUp()` called in `_storyBattleVictory()` after XP award (§II.E) | Exact | ✅ EXACT | | 17 | `S_story.atkBonus` = *"permanent attack modifier from leveling"* (§II.E) | Repurposed: it is now the **STR modifier** | ⚠️ **SEMANTICS INVERTED** (Finding 3) | | 18 | `S_story.acBonus` = *"permanent AC modifier from leveling"*, +4 by L10 (§II.D/E) | Declared twice, read once, **written nowhere** — at HEAD and at the archive | ❌ **NOT SHIPPED — born dead** (Finding 2) | | 19 | `_calcPlayerAc(): base + shield + acBonus` (§II.E) | Shape survives, plus a §DROP-03 lake term; the `acBonus` term is permanently 0 | ⚠️ PARTIAL | | 20 | `#s-level`, `#svo-levelup`, `#svo-xp-total` surfaces (§II.F/VI) | All three live; `#svo-xp-total` format exact; `#svo-levelup` text changed to a deferred-count line | ✅ / ⚠️ | | 21 | Bare identifier `shieldBonus` (§II.E) | 0 commits ever — a paraphrase of `equippedShield.acBonus`, not a symbol | — (not a defect) | | 22 | Level cap 10; L10 = 90,000 XP; *"casual players reach 4–5"* (§II.C, §VII) | Cap **20**; L10 = **20,000** XP (4.5× cheaper); L20 = 195,000 | **NOT SHIPPED** | **Manifest score: 15 ✅ · 5 ⚠️ · 2 ❌.** --- ## V. Findings ### Finding 1 — The progression system that shipped is the one this report ruled out §II.A constraint 3 reads: *"**No new UI complexity**: Levels should not require the player to make build choices, allocate stat points, or read a menu. Rewards are automatic and announced."* §VII scores it ✅ twice more (*"Rewards are automatic — zero player overhead per level-up"*, *"Does not add decision points"*). HEAD ships a dedicated `story-levelup-modal` in which the player **clicks to roll a d10 for HP**, sometimes **clicks a bonus d10**, and at seven levels **allocates two ability-score points by hand** (`const _ASI_LEVELS = new Set([4, 6, 8, 12, 14, 16, 19]);@25526`). `function _showLevelUpModal(lvl) {@25615` gates each section behind the previous one; `function _checkLevelUp() {@25671` now only *queues*. This is not rot. `FIGHTER_FEATURES`, `_ASI_LEVELS` and the modal are **already present in the earliest surviving build three days later**, while the specified `XP_LEVELS` literal and `_LEVEL_REWARDS` have **0 commits ever**. The 10-level flat-bonus table was superseded before it could be committed. **Why the reversal was right, and worth recording as design history.** The report optimised for *zero overhead*; the shipped system optimised for *the level-up being an event*. Levels also stopped being scalar: `function _extraAttackCount() {@24995` turns the main action into 2/3/4 rolls at L5/11/20 and `const critMin = _lv >= 20 ? 17@25027` widens the crit window at L3/15/20 — changes to the *structure* of a round, which no flat `+1 ATK` table can express. The report's own rule (*deepen existing decisions*) is better served by the design that broke its constraint than by the one it specified. ***13th instrument: a design doc's prohibition is a claim like any other — verify it against what shipped, because the reversal is usually the interesting part.*** ### Finding 2 → §DX-02y — `S_story.acBonus` is a read-only field with no writer, and the home doc says otherwise Two declarations — `acBonus: 0,@23040` inside `_S_DEFAULTS()`, and again in the `S_story` seed literal at `level: 1, atkBonus: 0, acBonus: 0,@23074` — exactly **one reader** — the `(S_story.acBonus || 0)@24614` term inside `function _calcPlayerAc() {@24610` — and **zero writers in 38,712 lines**. The same holds at `32c10c5`: the field was **born dead**, not retired. The specified +1 AC at L5, +1 at L8 and +2 at L10 therefore never accrue; a level-20 character's AC comes entirely from shield and lake-magic terms. `docs/mechanics/mechanics-combat.md:313` states the opposite outright — *"`atkBonus` and `acBonus` in `S_story` are updated immediately"* — the §DOC-02j `LOOT_TABLE` rot direction, and the more dangerous one: a reader greps the doc and concludes the field works. This is the **inverse** of the §DX-02n write-only class: dead weight there, a **broken dependency** here. A `check:deadconsts` scoped to unread fields walks straight past it, which is the standing argument for censusing **readers and writers separately** and failing on either being zero. ### Finding 3 → §AUDIT-03ae — `atkBonus` was repurposed, and three surfaces now disagree about the attack roll The report defined `S_story.atkBonus` as a *level* bonus and specified the integration point `atkTotal = d20Val + atkMod + profB + (S_story.atkBonus || 0)`. Both halves are still here; the meaning underneath one of them changed. At HEAD the field carries the **STR modifier**: seeded at character creation by `S_story.atkBonus = Math.max(0, Math.floor((scores.str - 10) / 2));@23958` and grown only by an ASI STR bump (`if (strDelta > 0) S_story.atkBonus@38540`). The specified integration point survives as `const lvlAtk@25030`. Two individually-correct changes compose into three disagreeing surfaces: - **The battle engine** reads its ability modifier from the dice-roller's own control, which is `