fuzzyworld

dm / commit

feat: migrate to Cypher System (Phases 1–2, SAVE_VERSION 2)

efa9a3e4cecf8c0384be2cd98bf11a0144a8fb28 · Pavel Safronov · 2026-05-27 22:22:39 -0700 · parents 1d2c9736e99c

feat: migrate to Cypher System (Phases 1–2, SAVE_VERSION 2)

Replace D&D 5e with the Cypher System Reference Document ruleset:

- Character creation: Descriptor × Type × Focus sentence replaces
  Race × Class; 20 descriptors, 4 types, 20 foci with live preview
- New character shape: Might/Speed/Intellect pools with edge, damage
  track (hale/impaired/debilitated/dead), cyphers, shins, tier/effort
- Sidebar redesign: three pool bars colour-coded by fill level, damage
  track badge, XP counter, Tier (Roman numeral), Shins, Cyphers panel
  with limit enforcement, Equipment list
- UPDATE_TOOL schema rewritten for pool damage/restore, cyphers,
  equipment, tier_advance; applyStatUpdate handles all new fields
- buildSystemPrompt rewritten: task difficulty (0–10 × 3), effort,
  skills, GM intrusions, recovery rolls, XP advancement, cyphers
- SAVE_VERSION bumped to 2; legacy D&D saves offered for export

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 README.md         |  12 +-
 plan.md           | 182 ++++++++++++++++
 public/index.html | 641 +++++++++++++++++++++++++++++++++++++++++-------------
 3 files changed, 677 insertions(+), 158 deletions(-)

diff --git a/README.md b/README.md
index a964c6c..5333d01 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # 🎲 Solo Dungeon Master
 
-An AI-powered solo D&D app that runs entirely in your browser. Your API key never leaves your device — all requests go directly from your browser to the Anthropic API.
+An AI-powered solo tabletop RPG app that runs entirely in your browser, using the **Cypher System** rules. Your API key never leaves your device — all requests go directly from your browser to the Anthropic API.
 
 **Live:** [pavelsafronov.gitlab.io/dm/](https://pavelsafronov.gitlab.io/dm/)
 
@@ -10,14 +10,18 @@ An AI-powered solo D&D app that runs entirely in your browser. Your API key neve
 
 On your first visit you'll be asked for an [Anthropic API key](https://console.anthropic.com/). The key is saved to your browser's `localStorage` (AES-GCM encrypted) so you only enter it once. After that, the game UI appears immediately on every return visit, resuming your previous adventure automatically.
 
-The game is powered by `claude-haiku-4-5-20251001` and uses Claude's tool-use API to maintain a structured character sheet (HP, XP, inventory, gold) while generating freeform narrative. Costs roughly a few cents per session.
+The game is powered by `claude-haiku-4-5-20251001` and uses Claude's tool-use API to maintain a structured character sheet (Might/Speed/Intellect pools, XP, cyphers, equipment) while generating freeform narrative. Costs roughly a few cents per session.
 
 ## Features
 
-- **10 races × 10 classes** — Human, Elf, Dwarf, Tiefling, Half-Orc, Gnome, Halfling, Dragonborn, Half-Elf, Aasimar × Fighter, Wizard, Rogue, Paladin, Bard, Ranger, Cleric, Druid, Barbarian, Warlock
-- **Session persistence** — adventure state and conversation history saved to `localStorage`; reloading the page resumes mid-scene
+- **Cypher System character creation** — pick a Descriptor (20 options), Type (Warrior, Adept, Explorer, Speaker), and Focus (20 options) to form the sentence *"I am a Clever Warrior who Hunts."*
+- **Three stat pools** — Might, Speed, and Intellect each with current/max and Edge displayed; colour-coded bars shift green → amber → red as pools deplete
+- **Damage track** — one pool at 0 → Impaired; two at 0 → Debilitated; all at 0 → Dead
+- **Cyphers** — one-use items found during play; displayed in the sidebar with Cypher Limit enforcement
+- **Session persistence** — adventure state and conversation history saved to `localStorage`; reloading the page resumes mid-scene; save version guard offers Markdown export before discarding incompatible sessions
 - **Cost display** — per-turn and session cost estimates shown in the sidebar (based on token counts from the SSE stream)
 - **Dice roller** — d4 through d100, configurable count
+- **World Notes** — orange-bordered GM meta-context panel for providing background lore, world physics, and house rules that Claude treats as absolute truth
 - **Share links** — generate an encrypted `#k=…` URL so someone else can use the app with your API key; they enter a passphrase you share separately; the key is never in the URL
 - **Report Issue** — in-game button that collects debug info (character state, recent errors, session cost) and opens a pre-filled GitLab issue or Service Desk email
 
diff --git a/plan.md b/plan.md
new file mode 100644
index 0000000..efe1985
--- /dev/null
+++ b/plan.md
@@ -0,0 +1,182 @@
+# Cypher System (CSRD) Migration Plan
+
+## Background
+
+The app currently runs D&D 5e via Claude. The goal is to switch to the Cypher System Reference Document (CSRD) — Monte Cook Games' open ruleset used in Numenera, The Strange, and others.
+
+Every phase that changes `gameState` shape must increment `SAVE_VERSION` in `public/index.html`. Old saves are automatically offered for export and discarded.
+
+---
+
+## Source material
+
+All content is available under the [Cypher System Open License (CSOL)](https://www.montecookgames.com/cypher-system-open-license/) — free for digital use.
+
+| Source | URL | Used for |
+| --- | --- | --- |
+| Cypher-System-JSON-DB | github.com/Jon-Davis/Cypher-System-JSON-DB | **Data arrays** — types (with pool values), descriptors, foci, abilities, cyphers. 1.74 MB JSON; too large to bundle directly, so we extract just the fields we need into compact inline JS arrays during development. |
+| cyphersrd.quest | cyphersrd.quest | **System prompt** — authoritative rules text for task resolution, damage track, effort, GM intrusions, advancement. We write a tight summary (~800 tokens) from this. |
+| Obsidian CSRD Markdown | github.com/Obsidian-TTRPG-Community/Cypher-SRD-Markdown | Cross-reference during development. |
+
+**Workflow for data extraction:** download CSRD.json from JSON-DB, run a small local script to pull the specific fields needed (type name + starting pools + cypher limit + starting abilities summary; descriptor name + stat bonus; focus name + one-line summary + tier 1 ability), and paste the result as inline JS arrays in `index.html`. No build step needed.
+
+---
+
+## Phase 1 — Character foundation
+
+The biggest structural change. D&D's Race + Class becomes Cypher's **Descriptor + Type + Focus** sentence: *"I am a Clever Adept who Talks to Machines."*
+
+### 1a — Data definitions
+
+- [x] Replace `RACES` array with `DESCRIPTORS` (20 entries implemented)
+- [x] Replace `CLASSES` array with `TYPES` (4 entries: Warrior, Adept, Explorer, Speaker — each with starting pools, Cypher Limit, and starting equipment)
+- [x] Add `FOCI` array (20 entries implemented)
+
+### 1b — New `gameState.character` shape
+
+Replace current `{ name, race, class, level, hp, maxHp, xp, gold, inventory }` with:
+
+```js
+{
+  name, descriptor, type, focus,
+  tier,            // 1–6
+  effort,          // = tier
+  might:     { pool, max, edge },
+  speed:     { pool, max, edge },
+  intellect: { pool, max, edge },
+  xp,
+  damageTrack,     // 'hale' | 'impaired' | 'debilitated'
+  recoveryUsed,    // 0–4, resets each full rest
+  cypherLimit,     // from type (usually 2)
+  cyphers,         // [{ name, level, effect }]
+  skills,          // [{ name, rank: 'trained'|'specialized'|'inability' }]
+  abilities,       // [{ name, cost, description }] — from type + focus
+  equipment,       // string[] (replaces inventory)
+  shins,           // currency (or 'gold' for generic settings)
+}
+```
+
+- [x] Update `beginAdventure()` to build this shape from Type + Descriptor + Focus selections
+- [x] Bump `SAVE_VERSION` to 2
+
+### 1c — Creation screen
+
+- [x] Replace two-grid layout (Race + Class) with three-grid layout (Descriptor + Type + Focus)
+- [x] The sentence "I am a ___ ___ who ___" assembles live as user picks each
+- [x] Type cards show starting pool values and Cypher Limit, not HP
+- [x] Focus cards show a one-line flavour summary
+- [x] Descriptor cards show the key stat bonus it provides
+
+### 1d — Header title format
+
+- [x] Update `header-title` from `"Name — Race Class"` to `"Name — Descriptor Type who Focus"`
+
+---
+
+## Phase 2 — Core mechanics
+
+### 2a — New `UPDATE_TOOL` schema
+
+Replace current fields entirely:
+
+```
+might_damage       // number — damage to Might pool
+speed_damage       // number — damage to Speed pool
+intellect_damage   // number — damage to Intellect pool
+might_restore      // number — points restored to Might pool
+speed_restore      // number — points restored to Speed pool
+intellect_restore  // number — points restored to Intellect pool
+xp_gain            // number
+shins_change       // number
+cyphers_add        // [{ name, level, effect }]
+cyphers_remove     // string[] — names of used/lost cyphers
+abilities_add      // [{ name, cost, description }]
+equipment_add      // string[]
+equipment_remove   // string[]
+skills_add         // [{ name, rank }]
+tier_advance       // boolean
+recovery_used      // boolean — mark one recovery slot as spent
+reason             // string
+```
+
+### 2b — New `applyStatUpdate()` logic
+
+- [x] Apply pool damage/restore, clamping to 0 and max
+- [x] Recalculate `damageTrack` after every pool change:
+  - Hale → one pool at 0 → Impaired (−1 step to all rolls)
+  - Impaired → two pools at 0 → Debilitated (can barely act)
+  - Debilitated → all pools at 0 → Dead (end screen)
+- [x] Enforce Cypher Limit: if `cyphers.length > cypherLimit`, warn player to use one
+- [x] Handle `tier_advance`: increment `tier`, recalculate `effort = tier`, show tier-up fanfare
+- [ ] Handle `recovery_used`: increment `recoveryUsed` (max 4; fourth resets all and marks 10-hour rest taken) — deferred to Phase 3d (recovery button)
+- [x] Replace level-up toast with Tier-up toast (text: "TIER [N]!")
+
+### 2c — System prompt rewrite ✅
+
+Complete replacement of `buildSystemPrompt`. Claude needs to understand:
+
+- **Character sentence** — what Descriptor, Type, and Focus each contribute
+- **Task difficulty** — 0–10 scale; target = difficulty × 3; player rolls d20
+- **Special roll results** — 1 = GM intrusion (free); 17 = minor effect; 18 = minor effect; 19 = major effect or +1 damage; 20 = major effect
+- **Effort** — costs 3 from a pool per level (minus Edge); reduces difficulty by 1 per level applied
+- **Assets** — each relevant asset reduces difficulty by 1 (max 2 from assets)
+- **Skills** — trained = −1 difficulty; specialized = −2 difficulty; inability = +1 difficulty
+- **Damage track** — Impaired penalty (−1 step = difficulty +1 to all rolls); Debilitated behavior
+- **GM Intrusions** — Claude should offer them occasionally; player accepts (gains 2 XP, gives 1 away) or rejects (spends 1 XP); if player has no XP they must accept
+- **Recovery rolls** — 1d6 + Tier; progression: 1 action → 10 min → 1 hour → 10 hours; after four the character must sleep to reset
+- **Cyphers** — found during play; Claude should introduce them narratively; remind player when at Cypher Limit; one-use
+- **XP flow** — earned via intrusions and discoveries; spent on advancement (4 per step) or short-term benefits (1 each: reroll, refuse intrusion, add +2 to any roll)
+- **Advancement steps** (any 4 to advance a tier): increase pool max by 4; add +1 Edge to a stat; train a skill; other improvement
+- **Stat tracking** — call `update_character` before narrative text, same as current
+
+---
+
+## Phase 3 — Sidebar UI redesign
+
+### 3a — Three pool bars
+
+- [x] Replace HP bar with three bars: Might, Speed, Intellect
+- [x] Each bar shows `current / max` and `Edge: N`
+- [x] Bar colour shifts: green (full) → amber (half) → red (low) — same as current HP logic
+- [x] Damage Track indicator below the bars: coloured badge — green "Hale" / amber "Impaired" / red "Debilitated"
+
+### 3b — XP & advancement panel
+
+- [x] Replace XP progress bar (which tracked D&D level thresholds) with a plain XP counter
+- [x] Tier shown as Roman numeral next to XP
+- [ ] Show advancement progress: how many of the 4 steps toward next Tier are done
+- [ ] "Spend 4 XP" button to initiate tier advancement (disabled if XP < 4 or steps < 4)
+
+### 3c — Cyphers panel
+
+- [x] New sidebar section "Cyphers" (replaces or extends Inventory)
+- [x] Each cypher shows name, level, effect summary
+- [ ] "Use" button per cypher (removes it from list and tells Claude)
+- [x] Cypher Limit displayed; section border turns orange when at limit
+- [x] Equipment (non-cypher items) stays as a separate Inventory list
+
+### 3d — Recovery roll button
+
+- [ ] Small section showing 4 recovery slots (filled/empty pips)
+- [ ] "Recovery roll" button — disabled when busy or all slots used
+- [ ] Clicking it rolls 1d6 + Tier, adds result to the lowest depleted pool, marks one slot used
+- [ ] On 10-hour rest: all slots reset
+
+---
+
+## Phase 4 — Setting / genre selector
+
+Cypher System is genre-neutral. Offer a setting choice at creation that adjusts tone, vocabulary, and available Foci.
+
+- [ ] Add setting selector to creation screen: Generic Fantasy / Post-Apocalyptic Sci-Fantasy (Numenera-flavoured) / Modern / Science Fiction
+- [ ] Each setting customises: currency name (shins vs gold), "magic" vocabulary (esoteries vs spells vs powers), DM tone, and a curated subset of Foci
+- [ ] System prompt includes setting context
+
+---
+
+## Notes
+
+- Phases 1–2 land together (one `SAVE_VERSION` bump to 2) — they're too intertwined to separate safely
+- Phase 3 can ship independently after Phase 2 (UI only, no `gameState` shape change)
+- Phase 4 is cosmetic and can go in any order
+- The CSRD is available at [cypher-srd.com](https://cypher-srd.com) for rules reference
diff --git a/public/index.html b/public/index.html
index 963dc01..edfef06 100644
--- a/public/index.html
+++ b/public/index.html
@@ -492,7 +492,131 @@ body {
   font-style: italic;
 }
 
-/* HP bar */
+/* Character sentence (creation screen) */
+.char-sentence {
+  font-family: 'Cinzel', serif;
+  font-size: 0.85rem;
+  color: var(--text-dim);
+  text-align: center;
+  margin: 0 0 16px;
+  min-height: 1.4em;
+  letter-spacing: 0.04em;
+}
+.char-sentence .picked { color: var(--gold-light); }
+
+/* Compact grid for descriptors/foci (20 items) */
+.selection-grid.compact-grid {
+  grid-template-columns: repeat(5, 1fr);
+}
+.selection-grid.compact-grid .sel-card {
+  padding: 8px 4px;
+}
+.selection-grid.compact-grid .sel-card .icon { font-size: 1.2rem; }
+.selection-grid.compact-grid .sel-card .name { font-size: 0.6rem; }
+
+/* Pool bars (Might/Speed/Intellect) */
+.pool-label-row {
+  display: flex;
+  align-items: baseline;
+  justify-content: space-between;
+  margin-bottom: 3px;
+}
+.pool-label {
+  font-family: 'Cinzel', serif;
+  font-size: 0.6rem;
+  letter-spacing: 0.1em;
+  color: var(--text-dim);
+  text-transform: uppercase;
+}
+.pool-value {
+  font-family: 'Cinzel', serif;
+  font-size: 0.78rem;
+  color: var(--text-bright);
+  font-weight: 600;
+}
+.pool-edge {
+  font-size: 0.62rem;
+  color: var(--text-dim);
+  margin-left: 4px;
+}
+.pool-bar-wrap { margin-bottom: 8px; }
+#might-fill   { background: #c0392b; }
+#speed-fill   { background: #27ae60; }
+#intellect-fill { background: #6b5fc7; }
+#might-fill.warning, #speed-fill.warning, #intellect-fill.warning { filter: brightness(0.75); }
+#might-fill.danger, #speed-fill.danger, #intellect-fill.danger {
+  filter: brightness(0.6);
+  animation: hp-pulse 1.5s infinite;
+}
+
+/* Damage track badge */
+.damage-track-row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin: 6px 0 8px;
+  flex-wrap: wrap;
+}
+.dt-badge {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  font-family: 'Cinzel', serif;
+  font-size: 0.62rem;
+  letter-spacing: 0.1em;
+  text-transform: uppercase;
+  padding: 2px 8px;
+  border-radius: 10px;
+  border: 1px solid;
+}
+.dt-badge.hale         { color: #27ae60; border-color: #27ae60; background: rgba(39,174,96,0.08); }
+.dt-badge.impaired     { color: #f39c12; border-color: #f39c12; background: rgba(243,156,18,0.1); }
+.dt-badge.debilitated  { color: #e74c3c; border-color: #e74c3c; background: rgba(231,76,60,0.1); animation: hp-pulse 1.5s infinite; }
+.dt-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
+.xp-tier-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-family: 'Cinzel', serif;
+  font-size: 0.72rem;
+  color: var(--text-dim);
+}
+.xp-tier-row .val { color: var(--gold); font-weight: 600; }
+
+/* Cyphers section */
+.cypher-entry {
+  border: 1px solid var(--border);
+  border-radius: 5px;
+  padding: 7px 9px;
+  margin-bottom: 6px;
+  background: var(--bg-card);
+}
+.cypher-name {
+  font-family: 'Cinzel', serif;
+  font-size: 0.68rem;
+  color: var(--gold-light);
+  margin-bottom: 2px;
+}
+.cypher-effect {
+  font-size: 0.75rem;
+  color: var(--text-dim);
+  line-height: 1.4;
+}
+.cypher-use-btn {
+  margin-top: 5px;
+  font-size: 0.65rem;
+  font-family: 'Cinzel', serif;
+  background: none;
+  border: 1px solid var(--border);
+  border-radius: 3px;
+  color: var(--text-dim);
+  cursor: pointer;
+  padding: 2px 8px;
+}
+.cypher-use-btn:hover { border-color: var(--gold-dim); color: var(--gold); }
+.cypher-limit-warn { color: #d97706; font-size: 0.72rem; }
+
+/* Stat row */
 .stat-row {
   display: flex;
   align-items: center;
@@ -1079,14 +1203,21 @@ body {
   <div class="creation-title"><img src="dice.png" alt="" style="height:0.85em;vertical-align:middle;margin-right:0.2em;filter:invert(1) sepia(1) saturate(2) hue-rotate(5deg) brightness(0.9)"> Solo Dungeon Master</div>
   <div class="creation-sub">Powered by Claude — your story begins here</div>
 
+  <p class="char-sentence" id="char-sentence">I am a <em>___</em> <em>___</em> who <em>___</em></p>
+
+  <div class="creation-section">
+    <div class="section-label">Descriptor — who are you?</div>
+    <div class="selection-grid compact-grid" id="descriptor-grid"></div>
+  </div>
+
   <div class="creation-section">
-    <div class="section-label">Choose Your Race</div>
-    <div class="selection-grid" id="race-grid"></div>
+    <div class="section-label">Type — what do you do?</div>
+    <div class="selection-grid" id="type-grid"></div>
   </div>
 
   <div class="creation-section">
-    <div class="section-label">Choose Your Class</div>
-    <div class="selection-grid class-grid" id="class-grid"></div>
+    <div class="section-label">Focus — what makes you special?</div>
+    <div class="selection-grid compact-grid" id="focus-grid"></div>
   </div>
 
   <div class="creation-section">
@@ -1158,35 +1289,57 @@ body {
         <div class="char-icon" id="char-icon">⚔️</div>
         <div class="char-identity">
           <div class="char-name" id="char-name-display">—</div>
-          <div class="char-race-class" id="char-race-class">— / —</div>
+          <div class="char-race-class" id="char-identity-sub">—</div>
         </div>
       </div>
 
-      <div class="stat-row">
-        <span class="stat-label">HP</span>
-        <span class="stat-value" id="hp-display">—</span>
+      <div class="pool-bar-wrap">
+        <div class="pool-label-row">
+          <span class="pool-label">Might <span class="pool-edge" id="might-edge"></span></span>
+          <span class="pool-value" id="might-display">—</span>
+        </div>
+        <div class="bar-track"><div class="bar-fill" id="might-fill" style="width:100%"></div></div>
       </div>
-      <div class="bar-track">
-        <div class="bar-fill" id="hp-fill" style="width:100%"></div>
+
+      <div class="pool-bar-wrap">
+        <div class="pool-label-row">
+          <span class="pool-label">Speed <span class="pool-edge" id="speed-edge"></span></span>
+          <span class="pool-value" id="speed-display">—</span>
+        </div>
+        <div class="bar-track"><div class="bar-fill" id="speed-fill" style="width:100%"></div></div>
       </div>
 
-      <div class="stat-row">
-        <span class="stat-label">XP <span id="level-display" style="color:var(--gold)">Lv 1</span></span>
-        <span class="stat-value" id="xp-display">0 / 300</span>
+      <div class="pool-bar-wrap">
+        <div class="pool-label-row">
+          <span class="pool-label">Intellect <span class="pool-edge" id="intellect-edge"></span></span>
+          <span class="pool-value" id="intellect-display">—</span>
+        </div>
+        <div class="bar-track"><div class="bar-fill" id="intellect-fill" style="width:100%"></div></div>
       </div>
-      <div class="bar-track">
-        <div class="bar-fill" id="xp-fill" style="width:0%"></div>
+
+      <div class="damage-track-row">
+        <span class="dt-badge hale" id="dt-badge"><span class="dt-dot"></span><span id="dt-label">Hale</span></span>
+        <span class="xp-tier-row" style="flex:1;margin-left:8px">
+          <span>XP <span class="val" id="xp-display">0</span></span>
+          <span>Tier <span class="val" id="tier-display">I</span></span>
+        </span>
       </div>
 
       <div class="gold-row">
         <span>🪙</span>
-        <span id="gold-display">10 GP</span>
+        <span id="shins-display">10 Shins</span>
       </div>
     </div>
 
-    <!-- Inventory -->
+    <!-- Cyphers -->
     <div class="sidebar-section">
-      <div class="sidebar-title">Inventory</div>
+      <div class="sidebar-title" id="cyphers-title">Cyphers (0/2)</div>
+      <div id="cyphers-list"><span style="font-size:0.78rem;color:var(--text-dim);font-style:italic">No cyphers yet.</span></div>
+    </div>
+
+    <!-- Equipment -->
+    <div class="sidebar-section">
+      <div class="sidebar-title">Equipment</div>
       <ul id="inventory-list"></ul>
     </div>
 
@@ -1510,34 +1663,82 @@ function getSharedBlob() {
 
 // ── DATA ──────────────────────────────────────────────
 
-const RACES = [
-  { name: 'Human',    icon: '👤' },
-  { name: 'Elf',      icon: '🧝' },
-  { name: 'Dwarf',    icon: '⛏️' },
-  { name: 'Tiefling', icon: '😈' },
-  { name: 'Half-Orc', icon: '💪' },
-  { name: 'Gnome',    icon: '🎭' },
-  { name: 'Halfling', icon: '🍃' },
-  { name: 'Dragonborn', icon: '🐉' },
-  { name: 'Half-Elf', icon: '🌟' },
-  { name: 'Aasimar',  icon: '👼' },
+// ── CYPHER SYSTEM DATA (CSRD) ─────────────────────────
+// Source: Cypher System Reference Document, Cypher System Open License
+// © Monte Cook Games — montecookgames.com/cypher-system-open-license
+
+const TYPES = [
+  { name: 'Warrior', icon: '⚔️',
+    pools: { might: 10, speed: 10, intellect: 8 },
+    edge:  { might: 1,  speed: 0,  intellect: 0 },
+    cypherLimit: 2,
+    note: 'Practiced with all weapons. Might Edge 1.',
+    equipment: ['Weapon of choice', 'Light armor', "Explorer's pack", 'Moderate-priced item'] },
+  { name: 'Adept', icon: '🔮',
+    pools: { might: 7, speed: 9, intellect: 12 },
+    edge:  { might: 0, speed: 0,  intellect: 1 },
+    cypherLimit: 3,
+    note: 'Masters esoteries (special abilities). Intellect Edge 1. Cypher Limit 3.',
+    equipment: ['Clothing', 'Book of lore', 'Two expensive items', 'Moderate-priced item'] },
+  { name: 'Explorer', icon: '🧭',
+    pools: { might: 10, speed: 9, intellect: 9 },
+    edge:  { might: 1,  speed: 0,  intellect: 0 },
+    cypherLimit: 2,
+    note: 'Balanced adventurer; light/medium weapons. Might Edge 1.',
+    equipment: ['Weapon of choice', 'Light armor', 'Exploration gear', 'Moderate-priced item'] },
+  { name: 'Speaker', icon: '🗣️',
+    pools: { might: 8, speed: 9, intellect: 11 },
+    edge:  { might: 0, speed: 0,  intellect: 1 },
+    cypherLimit: 2,
+    note: 'Social and deceptive abilities. Intellect Edge 1.',
+    equipment: ['Light weapon', 'Stylish clothing', 'Two expensive items', 'Moderate-priced item'] },
 ];
 
-const CLASSES = [
-  { name: 'Fighter',   icon: '⚔️',  hp: 10, items: ['Longsword', 'Shield', 'Chain Mail', "Explorer's Pack"] },
-  { name: 'Wizard',    icon: '🔮',  hp: 6,  items: ['Quarterstaff', 'Spellbook', 'Arcane Focus', "Scholar's Pack"] },
-  { name: 'Rogue',     icon: '🗡️',  hp: 8,  items: ['Shortsword', 'Leather Armor', "Thieves' Tools", '2x Daggers'] },
-  { name: 'Paladin',   icon: '⚜️',  hp: 10, items: ['Longsword', 'Shield', 'Chain Mail', 'Holy Symbol'] },
-  { name: 'Bard',      icon: '🎵',  hp: 8,  items: ['Rapier', 'Leather Armor', 'Lute', "Entertainer's Pack"] },
-  { name: 'Ranger',    icon: '🏹',  hp: 10, items: ['Longbow', '20x Arrows', 'Leather Armor', 'Shortsword'] },
-  { name: 'Cleric',    icon: '✨',  hp: 8,  items: ['Mace', 'Shield', 'Scale Mail', 'Holy Symbol'] },
-  { name: 'Druid',     icon: '🌿',  hp: 8,  items: ['Quarterstaff', 'Hide Armor', 'Herbalism Kit', 'Druidic Focus'] },
-  { name: 'Barbarian', icon: '🪓',  hp: 12, items: ['Greataxe', '2x Handaxes', "Explorer's Pack"] },
-  { name: 'Warlock',   icon: '🌑',  hp: 8,  items: ['Dagger', 'Leather Armor', 'Arcane Focus', "Scholar's Pack"] },
+const DESCRIPTORS = [
+  { name: 'Clever',       icon: '🧠', poolBonus: { intellect: 2 }, note: 'Skilled at lies, trickery, and assessing danger.' },
+  { name: 'Fast',         icon: '💨', poolBonus: { speed: 2 },     note: 'Can move and act in the same round.' },
+  { name: 'Graceful',     icon: '🌸', poolBonus: { speed: 2 },     note: 'Trained in balance, performance, and Speed defense.' },
+  { name: 'Hardy',        icon: '🪨', poolBonus: { might: 4 },     note: 'Functions one step better on damage track.' },
+  { name: 'Honorable',    icon: '🛡️', poolBonus: { might: 2 },     note: 'Trained in pleasant social interactions and detecting lies.' },
+  { name: 'Inquisitive',  icon: '🔍', poolBonus: { intellect: 4 }, note: 'Trained in learning, geography, and history.' },
+  { name: 'Intelligent',  icon: '📚', poolBonus: { intellect: 2 }, note: 'Trained in one knowledge area and memorization.' },
+  { name: 'Intuitive',    icon: '✨', poolBonus: { intellect: 2 }, note: 'Trained in perception; can act out of turn once per cycle.' },
+  { name: 'Lucky',        icon: '🍀', poolBonus: {},               note: 'Gains a 3-point Luck Pool; bonuses to rerolls.' },
+  { name: 'Mighty',       icon: '💪', poolBonus: { might: 4 },     note: 'Trained in lifting/throwing; +1 to recovery rolls.' },
+  { name: 'Mysterious',   icon: '🌑', poolBonus: {},               note: 'Trained in stealth and resisting interrogation.' },
+  { name: 'Mystical',     icon: '🌙', poolBonus: { intellect: 2 }, note: 'Can detect and cast minor magic; trained in the supernatural.' },
+  { name: 'Perceptive',   icon: '👁️', poolBonus: { intellect: 2 }, note: 'Trained in noticing details and recalling trivia.' },
+  { name: 'Resilient',    icon: '🌳', poolBonus: { might: 2, intellect: 2 }, note: 'Trained in both Might and Intellect defense; extra recovery roll.' },
+  { name: 'Sharp-Eyed',   icon: '🦅', poolBonus: {},               note: 'Trained in initiative and perception; can find foe weaknesses.' },
+  { name: 'Stealthy',     icon: '🌫️', poolBonus: { speed: 2 },     note: 'Trained in stealth, trickery, and illusions.' },
+  { name: 'Strong',       icon: '🪓', poolBonus: { might: 4 },     note: 'Trained in breaking objects and jumping.' },
+  { name: 'Strong-Willed',icon: '🔮', poolBonus: { intellect: 4 }, note: 'Trained in resisting mental effects and concentration.' },
+  { name: 'Tough',        icon: '🦾', poolBonus: { might: 2 },     note: '+1 Armor; +1 to recovery rolls.' },
+  { name: 'Weird',        icon: '🌀', poolBonus: { intellect: 2 }, note: 'Trained in supernatural knowledge; senses nearby strange phenomena.' },
 ];
 
-const XP_THRESHOLDS = [300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000,
-  85000, 100000, 120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000];
+const FOCI = [
+  { name: 'Controls Beasts',        icon: '🦁', playstyle: 'Communicate with and command animals.' },
+  { name: 'Crafts Illusions',       icon: '🎭', playstyle: 'Fashion images from light so real they deceive all senses.' },
+  { name: 'Crafts Unique Objects',  icon: '⚙️', playstyle: 'Inventor of strange gadgets, gear, and contraptions.' },
+  { name: 'Explores Dark Places',   icon: '🕯️', playstyle: 'Treasure hunter and scavenger of lost and forgotten things.' },
+  { name: 'Fights With Panache',    icon: '🎪', playstyle: 'Swashbuckling daredevil who wins through flamboyant style.' },
+  { name: 'Howls at the Moon',      icon: '🐺', playstyle: 'Transforms into a fearsome beast with dangerous control issues.' },
+  { name: 'Hunts',                  icon: '🏹', playstyle: 'Stalking hunter who tracks and takes down a chosen quarry.' },
+  { name: 'Infiltrates',            icon: '🕵️', playstyle: 'Uses subtlety, guile, and stealth to enter impossible places.' },
+  { name: 'Leads',                  icon: '👑', playstyle: 'Natural commander who maintains a loyal band of followers.' },
+  { name: 'Lives in the Wilderness',icon: '🌲', playstyle: 'Survivalist who thrives where others perish.' },
+  { name: 'Masters Spells',         icon: '📜', playstyle: 'Memorizes and rapidly deploys a variety of arcane spells.' },
+  { name: 'Masters Weaponry',       icon: '🗡️', playstyle: 'Grandmaster of one weapon type, surpassing all others.' },
+  { name: 'Murders',                icon: '🩸', playstyle: 'Assassin specialising in lethal surprise and clean eliminations.' },
+  { name: 'Never Says Die',         icon: '💀', playstyle: 'Unstoppable survivor who keeps fighting past all reason.' },
+  { name: 'Rides the Lightning',    icon: '⚡', playstyle: 'Creates and discharges electrical power to shock and energize.' },
+  { name: 'Slays Monsters',         icon: '🐉', playstyle: 'Dedicated monster hunter with lethal knowledge of creatures.' },
+  { name: 'Solves Mysteries',       icon: '🔎', playstyle: 'Master of deduction who reads evidence to find hidden answers.' },
+  { name: 'Wears a Sheen of Ice',   icon: '❄️', playstyle: 'Commands the wintry power of cold and ice.' },
+  { name: 'Wields Two Weapons',     icon: '🤺', playstyle: 'Dual-wielder who overwhelms enemies with relentless twin attacks.' },
+  { name: 'Works Miracles',         icon: '💫', playstyle: 'Healer and time-bender who restores the injured.' },
+];
 
 const DIE_TYPES = [4, 6, 8, 10, 12, 20, 100];
 
@@ -1547,39 +1748,83 @@ const UPDATE_TOOL = {
   input_schema: {
     type: 'object',
     properties: {
-      hp_change:        { type: 'number', description: 'HP delta. Negative=damage, positive=healing. 0 if unchanged.' },
-      gold_change:      { type: 'number', description: 'Gold delta. 0 if unchanged.' },
-      xp_gain:          { type: 'number', description: 'XP gained this turn. 0 if none.' },
-      inventory_add:    { type: 'array', items: { type: 'string' }, description: 'Items gained.' },
-      inventory_remove: { type: 'array', items: { type: 'string' }, description: 'Items lost.' },
+      might_damage:     { type: 'number', description: 'Points of damage to Might pool. 0 if none.' },
+      speed_damage:     { type: 'number', description: 'Points of damage to Speed pool. 0 if none.' },
+      intellect_damage: { type: 'number', description: 'Points of damage to Intellect pool. 0 if none.' },
+      might_restore:    { type: 'number', description: 'Points restored to Might pool (recovery, healing). 0 if none.' },
+      speed_restore:    { type: 'number', description: 'Points restored to Speed pool. 0 if none.' },
+      intellect_restore:{ type: 'number', description: 'Points restored to Intellect pool. 0 if none.' },
+      xp_gain:          { type: 'number', description: 'XP awarded (intrusions, discoveries). 0 if none.' },
+      shins_change:     { type: 'number', description: 'Shins (currency) delta. Negative=spent. 0 if unchanged.' },
+      cyphers_add:      { type: 'array',  items: { type: 'object', properties: { name: { type: 'string' }, level: { type: 'number' }, effect: { type: 'string' } }, required: ['name','level','effect'] }, description: 'Cyphers found.' },
+      cyphers_remove:   { type: 'array',  items: { type: 'string' }, description: 'Names of cyphers used or lost.' },
+      equipment_add:    { type: 'array',  items: { type: 'string' }, description: 'Equipment gained.' },
+      equipment_remove: { type: 'array',  items: { type: 'string' }, description: 'Equipment lost.' },
+      tier_advance:     { type: 'boolean', description: 'True if character advances a Tier.' },
       reason:           { type: 'string', description: 'Brief reason for the change.' },
     },
-    required: ['hp_change', 'gold_change', 'xp_gain', 'inventory_add', 'inventory_remove', 'reason'],
+    required: ['might_damage','speed_damage','intellect_damage','might_restore','speed_restore','intellect_restore','xp_gain','shins_change','cyphers_add','cyphers_remove','equipment_add','equipment_remove','tier_advance','reason'],
   },
 };
 
 function buildSystemPrompt(character, theme, notes) {
-  return `You are a masterful Dungeon Master running a solo D&D 5e adventure.
+  const c = character;
+  const dt = c.damageTrack || 'hale';
+  return `You are a masterful Game Master running a solo Cypher System adventure (CSRD rules).
 
-CHARACTER: ${character.name} — Level ${character.level} ${character.race} ${character.class}
+CHARACTER: ${c.name} — Tier ${c.tier} ${c.descriptor} ${c.type} who ${c.focus}
 ADVENTURE THEME: ${theme || 'classic fantasy'}
 
+STAT POOLS
+• Might: ${c.might.pool}/${c.might.max} (Edge ${c.might.edge})
+• Speed: ${c.speed.pool}/${c.speed.max} (Edge ${c.speed.edge})
+• Intellect: ${c.intellect.pool}/${c.intellect.max} (Edge ${c.intellect.edge})
+• Damage track: ${dt.toUpperCase()} | Effort: ${c.tier} | XP: ${c.xp} | Shins: ${c.shins}
+• Cyphers: ${(c.cyphers||[]).length}/${c.cypherLimit}${(c.cyphers||[]).length ? ' — ' + c.cyphers.map(cy=>`${cy.name} (Lv${cy.level})`).join(', ') : ''}
+
+CYPHER SYSTEM RULES — TASK RESOLUTION
+You set difficulty 0–10; target number = difficulty × 3; player rolls d20.
+Roll ≥ target = success. Special results: 1 = fumble/free GM Intrusion; 17–18 = minor effect; 19–20 = major effect.
+Reducing difficulty: each asset (tool, positioning, help) −1 (max 2 from assets); trained skill −1; specialized skill −2; inability +1.
+Effort: player may apply Effort to reduce difficulty by 1 per level; each Effort level costs 3 from a pool minus that pool's Edge.
+
+DAMAGE
+Typical weapon damage: light 2 pts, medium 4 pts, heavy 6 pts. Damage depletes Might pool by default (Speed or Intellect if specified).
+Damage track:
+• Hale — all pools above 0, no penalties.
+• Impaired — one pool at 0; all tasks hindered (difficulty +1).
+• Debilitated — two pools at 0; can only move or take defensive actions.
+• Dead — all pools at 0.
+
+RECOVERY ROLLS
+Player rolls 1d6 + Tier and adds result to any one pool. Four recoveries per cycle (1 action → 10 min → 1 hour → 10 hours), then must rest to reset.
+
+GM INTRUSIONS
+Occasionally introduce a complication: "GM Intrusion — [describe]. Accept for 2 XP (give 1 to another character), or spend 1 XP to refuse." On a player's natural 1, you may offer a free intrusion (they receive 1 XP; refusing is free).
+
+XP & ADVANCEMENT
+XP is earned through intrusions and discoveries; 1 XP can be spent to reroll or refuse an intrusion.
+To advance: spend 4 XP and gain one improvement (increase a pool max by 4, gain +1 Edge, train a skill, or another improvement). After 4 improvements, increase Tier (max Tier 6).
+
+CYPHERS
+One-use items with powerful effects. This character's limit is ${c.cypherLimit}. When the character finds one, describe it and its effect. Remind the player when at their limit — carrying too many cyphers is unstable.
+
+STAT TRACKING — call update_character BEFORE writing narrative whenever: pools change, shins change, XP is awarded, items/cyphers are gained or lost, or Tier advances.
+
 NARRATIVE RULES
 • Write 2–4 vivid, immersive paragraphs per turn.
 • Give NPCs distinct voices and motivations.
 • Make choices matter — show consequences immediately.
-• Run combat with suspense and tactical detail.
-• End every turn with either 2–3 numbered options OR "What do you do?"
-
-STAT TRACKING — call update_character whenever HP changes, gold is gained/lost, XP is earned, or items change hands. Call the tool BEFORE writing your narrative text.
+• End every turn with 2–3 numbered options OR "What do you do?"
 
-TONE: Cinematic. Dangerous. Immersive. Legendary.${notes ? `\n\nWORLD NOTES — ABSOLUTE TRUTHS\nThe GM has defined the following facts about this world. Treat them as immutable and weave them naturally into every scene:\n\n${notes}` : ''}`;
+TONE: Cinematic. Dangerous. Immersive. Legendary.${notes ? `\n\nWORLD NOTES — ABSOLUTE TRUTHS\nThe GM has defined the following facts about this world. Treat them as immutable:\n\n${notes}` : ''}`;
 }
 
 // ── STATE ─────────────────────────────────────────────
 
-let selectedRace = null;
-let selectedClass = null;
+let selectedDescriptor = null;
+let selectedType = null;
+let selectedFocus = null;
 let gameState = null;
 let conversationHistory = [];
 let isBusy = false;
@@ -1632,8 +1877,8 @@ function buildDebugInfo(userDesc) {
     lines.push('', '--- Session ---');
     lines.push(`Player ID: p_${getOrCreatePlayerId()}`);
     if (gameState.gameId) lines.push(`Game ID:   g_${gameState.gameId}`);
-    lines.push(`Character: ${c.name} — Lv ${c.level} ${c.race} ${c.class}`);
-    lines.push(`HP: ${c.hp}/${c.maxHp}  XP: ${c.xp}  Gold: ${c.gold}g`);
+    lines.push(`Character: ${c.name} — Tier ${c.tier} ${c.descriptor} ${c.type} who ${c.focus}`);
+    lines.push(`Might: ${c.might?.pool}/${c.might?.max}  Speed: ${c.speed?.pool}/${c.speed?.max}  Intellect: ${c.intellect?.pool}/${c.intellect?.max}  XP: ${c.xp}`);
     lines.push(`Turns: ${conversationHistory.length}  Cost: $${sessionCost.toFixed(5)}`);
   }
 
@@ -1651,7 +1896,7 @@ function buildDebugInfo(userDesc) {
 // ── SESSION PERSISTENCE ───────────────────────────────
 // Increment SAVE_VERSION on any breaking change to gameState shape or
 // session format. tryResumeSession discards saves from older versions.
-const SAVE_VERSION = 1;
+const SAVE_VERSION = 2;
 
 function saveSession() {
   if (!gameState) return;
@@ -1691,7 +1936,7 @@ async function tryResumeSession() {
   document.getElementById('game-screen').classList.add('visible');
   document.getElementById('messages').innerHTML = session.messagesHTML || '';
   document.getElementById('header-title').textContent =
-    `${gameState.character.name} — ${gameState.character.race} ${gameState.character.class}`;
+    `${gameState.character.name} — ${gameState.character.descriptor} ${gameState.character.type}`;
 
   sessionCost = session.sessionCost || 0;
   lastTurnCost = 0;
@@ -1705,7 +1950,7 @@ async function tryResumeSession() {
   const msgEl = document.getElementById('messages');
   msgEl.scrollTop = msgEl.scrollHeight;
 
-  console.log(`▶ Resumed session — ${conversationHistory.length} messages, Lv ${gameState.character.level} ${gameState.character.name}, $${sessionCost.toFixed(5)} spent`);
+  console.log(`▶ Resumed session — ${conversationHistory.length} messages, Tier ${gameState.character.tier} ${gameState.character.name}, $${sessionCost.toFixed(5)} spent`);
   return true;
 }
 
@@ -1716,9 +1961,19 @@ function generateAdventureMarkdown(session) {
   const lines = ['# Solo Dungeon Master — Adventure Log', ''];
 
   if (c) {
-    lines.push(`**Character:** ${c.name} — Level ${c.level} ${c.race} ${c.class}`);
-    lines.push(`**HP:** ${c.hp}/${c.maxHp} · **XP:** ${c.xp} · **Gold:** ${c.gold} GP`);
-    if (c.inventory?.length) lines.push(`**Inventory:** ${c.inventory.join(', ')}`);
+    // Cypher format (v2+)
+    if (c.type) {
+      lines.push(`**Character:** ${c.name} — Tier ${c.tier} ${c.descriptor} ${c.type} who ${c.focus}`);
+      lines.push(`**Might:** ${c.might?.pool}/${c.might?.max} · **Speed:** ${c.speed?.pool}/${c.speed?.max} · **Intellect:** ${c.intellect?.pool}/${c.intellect?.max}`);
+      lines.push(`**XP:** ${c.xp} · **Shins:** ${c.shins}`);
+      if (c.cyphers?.length) lines.push(`**Cyphers:** ${c.cyphers.map(cy=>`${cy.name} (Lv${cy.level})`).join(', ')}`);
+      if (c.equipment?.length) lines.push(`**Equipment:** ${c.equipment.join(', ')}`);
+    } else {
+      // Legacy D&D format (v0/v1)
+      lines.push(`**Character:** ${c.name} — Level ${c.level} ${c.race} ${c.class}`);
+      lines.push(`**HP:** ${c.hp}/${c.maxHp} · **XP:** ${c.xp} · **Gold:** ${c.gold} GP`);
+      if (c.inventory?.length) lines.push(`**Inventory:** ${c.inventory.join(', ')}`);
+    }
   }
   if (session.sessionCost) {
     lines.push(`**Session cost:** $${session.sessionCost.toFixed(4)}`);
@@ -1764,67 +2019,91 @@ function showVersionMismatchModal(session) {
 
 // ── CREATION SCREEN ───────────────────────────────────
 
-async function buildCreationScreen() {
-  const raceGrid = document.getElementById('race-grid');
-  RACES.forEach(r => {
-    const card = document.createElement('div');
-    card.className = 'sel-card';
-    card.innerHTML = `<span class="icon">${r.icon}</span><span class="name">${r.name}</span>`;
-    card.addEventListener('click', () => {
-      document.querySelectorAll('#race-grid .sel-card').forEach(c => c.classList.remove('selected'));
-      card.classList.add('selected');
-      selectedRace = r;
-      checkBeginReady();
-    });
-    raceGrid.appendChild(card);
-  });
+function updateCharSentence() {
+  const d = selectedDescriptor?.name || '<em>___</em>';
+  const t = selectedType?.name      || '<em>___</em>';
+  const f = selectedFocus?.name     || '<em>___</em>';
+  const wrap = n => `<span class="picked">${n}</span>`;
+  document.getElementById('char-sentence').innerHTML =
+    `I am a ${selectedDescriptor ? wrap(d) : d} ${selectedType ? wrap(t) : t} who ${selectedFocus ? wrap(f) : f}`;
+}
 
-  const classGrid = document.getElementById('class-grid');
-  CLASSES.forEach(c => {
+function makeSelGrid(gridId, items, getCurrent, setCurrent) {
+  const grid = document.getElementById(gridId);
+  items.forEach(item => {
     const card = document.createElement('div');
     card.className = 'sel-card';
-    card.innerHTML = `<span class="icon">${c.icon}</span><span class="name">${c.name}</span>`;
+    card.innerHTML = `<span class="icon">${item.icon}</span><span class="name">${item.name}</span>`;
+    card.title = item.note || item.playstyle || '';
     card.addEventListener('click', () => {
-      document.querySelectorAll('#class-grid .sel-card').forEach(el => el.classList.remove('selected'));
+      grid.querySelectorAll('.sel-card').forEach(c => c.classList.remove('selected'));
       card.classList.add('selected');
-      selectedClass = c;
+      setCurrent(item);
+      updateCharSentence();
       checkBeginReady();
     });
-    classGrid.appendChild(card);
+    grid.appendChild(card);
   });
+}
 
+async function buildCreationScreen() {
+  makeSelGrid('descriptor-grid', DESCRIPTORS, () => selectedDescriptor, v => { selectedDescriptor = v; });
+  makeSelGrid('type-grid',       TYPES,       () => selectedType,       v => { selectedType = v; });
+  makeSelGrid('focus-grid',      FOCI,        () => selectedFocus,      v => { selectedFocus = v; });
   document.getElementById('begin-btn').addEventListener('click', () => beginAdventure());
   document.getElementById('change-key-btn').addEventListener('click', () => openSetupModal(getSharedBlob() ? 'shared' : 'own'));
 }
 
 function checkBeginReady() {
-  document.getElementById('begin-btn').disabled = !(selectedRace && selectedClass);
+  document.getElementById('begin-btn').disabled = !(selectedDescriptor && selectedType && selectedFocus);
+}
+
+function buildStartingPools() {
+  const t = selectedType;
+  const d = selectedDescriptor;
+  const bonus = d.poolBonus || {};
+  return {
+    might:     { pool: t.pools.might     + (bonus.might     || 0), max: t.pools.might     + (bonus.might     || 0), edge: t.edge.might },
+    speed:     { pool: t.pools.speed     + (bonus.speed     || 0), max: t.pools.speed     + (bonus.speed     || 0), edge: t.edge.speed },
+    intellect: { pool: t.pools.intellect + (bonus.intellect || 0), max: t.pools.intellect + (bonus.intellect || 0), edge: t.edge.intellect },
+  };
 }
 
 async function beginAdventure() {
   if (!currentApiKey) { openSetupModal(getSharedBlob() ? 'shared' : 'own'); return; }
   const apiKey = currentApiKey;
 
-  const name = document.getElementById('char-name').value.trim() || `The ${selectedClass.name}`;
+  const name = document.getElementById('char-name').value.trim() || `The ${selectedType.name}`;
   const theme = document.getElementById('adventure-theme').value.trim();
   worldNotes = document.getElementById('creation-world-notes').value.trim();
 
+  const pools = buildStartingPools();
+
   gameState = {
     apiKey,
     gameId: randomId(),
     character: {
       name,
-      race: selectedRace.name,
-      class: selectedClass.name,
-      level: 1,
-      hp: selectedClass.hp,
-      maxHp: selectedClass.hp,
+      descriptor: selectedDescriptor.name,
+      type:       selectedType.name,
+      focus:      selectedFocus.name,
+      tier: 1,
+      effort: 1,
+      might:     pools.might,
+      speed:     pools.speed,
+      intellect: pools.intellect,
+      damageTrack: 'hale',
+      recoveryUsed: 0,
+      cypherLimit: selectedType.cypherLimit,
+      cyphers: [],
+      skills: [],
+      abilities: [],
+      equipment: [...selectedType.equipment],
+      shins: 10,
       xp: 0,
-      gold: 10,
-      inventory: [...selectedClass.items],
     },
     theme,
-    icon: selectedClass.icon,
+    icon: selectedType.icon,
   };
 
   conversationHistory = [];
@@ -1837,10 +2116,9 @@ async function beginAdventure() {
   syncWorldNotesSidebar();
 
   document.getElementById('header-title').textContent =
-    `${name} — ${selectedRace.name} ${selectedClass.name}`;
+    `${name} — ${selectedDescriptor.name} ${selectedType.name}`;
 
-  // Auto-send the opening message
-  const opening = `The adventure begins. I stand ready, ${name} — a ${selectedRace.name} ${selectedClass.name}${theme ? ` venturing into ${theme}` : ''}. Set the scene. Bring this world to life.`;
+  const opening = `A new adventure begins. I am ${name} — a ${selectedDescriptor.name} ${selectedType.name} who ${selectedFocus.name}${theme ? `, setting out into ${theme}` : ''}. Introduce the world and set the opening scene.`;
   sendMessage(opening, false);
 }
 
@@ -1851,32 +2129,62 @@ function updateSidebar() {
 
   document.getElementById('char-icon').textContent = gameState.icon;
   document.getElementById('char-name-display').textContent = c.name;
-  document.getElementById('char-race-class').textContent = `${c.race} · ${c.class}`;
-  document.getElementById('level-display').textContent = `Lv ${c.level}`;
-
-  // HP
-  document.getElementById('hp-display').textContent = `${c.hp} / ${c.maxHp}`;
-  const hpPct = Math.max(0, (c.hp / c.maxHp) * 100);
-  const hpFill = document.getElementById('hp-fill');
-  hpFill.style.width = hpPct + '%';
-  hpFill.className = 'bar-fill';
-  if (hpPct <= 25) hpFill.classList.add('danger');
-  else if (hpPct <= 50) hpFill.classList.add('warning');
-
-  // XP
-  const xpNeeded = XP_THRESHOLDS[c.level - 1] || 999999;
-  const prevXp = c.level > 1 ? XP_THRESHOLDS[c.level - 2] : 0;
-  const xpPct = Math.min(100, ((c.xp - prevXp) / (xpNeeded - prevXp)) * 100);
-  document.getElementById('xp-display').textContent = `${c.xp} / ${xpNeeded}`;
-  document.getElementById('xp-fill').style.width = xpPct + '%';
-
-  // Gold
-  document.getElementById('gold-display').textContent = `${c.gold} GP`;
-
-  // Inventory
+  document.getElementById('char-identity-sub').textContent = `${c.descriptor} ${c.type} · ${c.focus}`;
+
+  // Pool bars
+  const pools = ['might', 'speed', 'intellect'];
+  pools.forEach(stat => {
+    const pool = c[stat];
+    const pct = pool.max > 0 ? Math.max(0, (pool.pool / pool.max) * 100) : 0;
+    document.getElementById(`${stat}-display`).textContent = `${pool.pool} / ${pool.max}`;
+    document.getElementById(`${stat}-edge`).textContent = pool.edge > 0 ? `Edge ${pool.edge}` : '';
+    const fill = document.getElementById(`${stat}-fill`);
+    fill.style.width = pct + '%';
+    fill.className = 'bar-fill';
+    if (pct <= 25) fill.classList.add('danger');
+    else if (pct <= 50) fill.classList.add('warning');
+  });
+
+  // Damage track
+  const badge = document.getElementById('dt-badge');
+  badge.className = `dt-badge ${c.damageTrack}`;
+  document.getElementById('dt-label').textContent =
+    c.damageTrack.charAt(0).toUpperCase() + c.damageTrack.slice(1);
+
+  // XP and Tier
+  document.getElementById('xp-display').textContent = c.xp;
+  const tierNumerals = ['I','II','III','IV','V','VI'];
+  document.getElementById('tier-display').textContent = tierNumerals[(c.tier || 1) - 1] || c.tier;
+
+  // Shins
+  document.getElementById('shins-display').textContent = `${c.shins} Shins`;
+
+  // Cyphers
+  const cypherList = document.getElementById('cyphers-list');
+  document.getElementById('cyphers-title').textContent =
+    `Cyphers (${c.cyphers.length}/${c.cypherLimit})`;
+  if (c.cyphers.length === 0) {
+    cypherList.innerHTML = '<span style="font-size:0.78rem;color:var(--text-dim);font-style:italic">No cyphers yet.</span>';
+  } else {
+    cypherList.innerHTML = '';
+    c.cyphers.forEach(cypher => {
+      const div = document.createElement('div');
+      div.className = 'cypher-entry';
+      div.innerHTML = `<span class="cypher-name">${cypher.name} <small>(lvl ${cypher.level})</small></span>`
+        + `<span class="cypher-effect">${cypher.effect}</span>`;
+      cypherList.appendChild(div);
+    });
+  }
+  if (c.cyphers.length >= c.cypherLimit) {
+    cypherList.classList.add('cypher-limit-warn');
+  } else {
+    cypherList.classList.remove('cypher-limit-warn');
+  }
+
+  // Equipment
   const list = document.getElementById('inventory-list');
   list.innerHTML = '';
-  c.inventory.forEach(item => {
+  (c.equipment || []).forEach(item => {
     const li = document.createElement('li');
     li.textContent = item;
     list.appendChild(li);
@@ -1885,55 +2193,79 @@ function updateSidebar() {
 
 function applyStatUpdate(update) {
   const c = gameState.character;
-  let leveledUp = false;
-  let newLevel = c.level;
 
-  if (update.hp_change) {
-    c.hp = Math.max(0, Math.min(c.maxHp, c.hp + update.hp_change));
-  }
-  if (update.gold_change) {
-    c.gold = Math.max(0, c.gold + update.gold_change);
-  }
-  if (update.xp_gain) {
-    c.xp += update.xp_gain;
-    while (c.level < 20 && c.xp >= XP_THRESHOLDS[c.level - 1]) {
-      c.level++;
-      c.maxHp += Math.max(1, Math.floor(CLASSES.find(cl => cl.name === c.class)?.hp / 2) + 1);
-      c.hp = Math.min(c.hp + Math.max(1, Math.floor(CLASSES.find(cl => cl.name === c.class)?.hp / 2) + 1), c.maxHp);
-      newLevel = c.level;
-      leveledUp = true;
+  // Pool damage / restore
+  const pools = ['might', 'speed', 'intellect'];
+  pools.forEach(stat => {
+    const dmg = update[`${stat}_damage`] || 0;
+    const rst = update[`${stat}_restore`] || 0;
+    if (dmg) c[stat].pool = Math.max(0, c[stat].pool - dmg);
+    if (rst) c[stat].pool = Math.min(c[stat].max, c[stat].pool + rst);
+  });
+
+  // Recalculate damage track
+  const zeroPools = pools.filter(s => c[s].pool === 0).length;
+  if (zeroPools === 0)      c.damageTrack = 'hale';
+  else if (zeroPools === 1) c.damageTrack = 'impaired';
+  else if (zeroPools === 2) c.damageTrack = 'debilitated';
+
+  // XP and shins
+  if (update.xp_gain)     c.xp    = (c.xp    || 0) + update.xp_gain;
+  if (update.shins_change) c.shins = Math.max(0, (c.shins || 0) + update.shins_change);
+
+  // Cyphers
+  if (update.cyphers_add?.length) {
+    c.cyphers.push(...update.cyphers_add);
+    if (c.cyphers.length > c.cypherLimit) {
+      addSystemMessage(`⚠️ You are carrying ${c.cyphers.length} cyphers — over your limit of ${c.cypherLimit}! Use one before it causes instability.`);
     }
   }
-  if (update.inventory_add?.length) {
-    c.inventory.push(...update.inventory_add);
+  if (update.cyphers_remove?.length) {
+    update.cyphers_remove.forEach(name => {
+      const idx = c.cyphers.findIndex(cy => cy.name.toLowerCase().includes(name.toLowerCase()));
+      if (idx !== -1) c.cyphers.splice(idx, 1);
+    });
   }
-  if (update.inventory_remove?.length) {
-    update.inventory_remove.forEach(item => {
-      const idx = c.inventory.findIndex(i => i.toLowerCase().includes(item.toLowerCase()));
-      if (idx !== -1) c.inventory.splice(idx, 1);
+
+  // Equipment
+  if (update.equipment_add?.length)    c.equipment.push(...update.equipment_add);
+  if (update.equipment_remove?.length) {
+    update.equipment_remove.forEach(item => {
+      const idx = c.equipment.findIndex(e => e.toLowerCase().includes(item.toLowerCase()));
+      if (idx !== -1) c.equipment.splice(idx, 1);
     });
   }
 
+  // Tier advance
+  let tieredUp = false;
+  if (update.tier_advance && c.tier < 6) {
+    c.tier++;
+    c.effort = c.tier;
+    tieredUp = true;
+  }
+
   updateSidebar();
 
   if (update.reason) {
     addSystemMessage(`⚡ ${update.reason}`);
   }
 
-  if (leveledUp) {
-    showLevelUp(newLevel);
+  if (tieredUp) {
+    showTierUp(c.tier);
   }
 
-  if (c.hp <= 0) {
-    addSystemMessage('💀 You have fallen. Your adventure ends here... for now.');
+  // Death: all three pools at 0
+  if (pools.every(s => c[s].pool === 0)) {
+    addSystemMessage('💀 All your pools have been depleted. Your adventure ends here... for now.');
     setBusy(true);
   }
 }
 
-function showLevelUp(level) {
+function showTierUp(tier) {
+  const tierNumerals = ['I','II','III','IV','V','VI'];
   const toast = document.getElementById('levelup-toast');
-  document.getElementById('toast-title').textContent = `LEVEL ${level}!`;
-  document.getElementById('toast-sub').textContent = `${gameState.character.name} grows stronger. The world trembles.`;
+  document.getElementById('toast-title').textContent = `TIER ${tierNumerals[tier - 1] || tier}!`;
+  document.getElementById('toast-sub').textContent = `${gameState.character.name} has grown in power. New abilities await.`;
   toast.style.display = 'block';
   setTimeout(() => {
     if (toast.style.display === 'block') toast.style.display = 'none';
@@ -2359,8 +2691,9 @@ document.getElementById('new-game-btn').addEventListener('click', async () => {
   document.getElementById('game-screen').classList.remove('visible');
   document.getElementById('creation-screen').style.display = '';
   document.getElementById('messages').innerHTML = '';
-  selectedRace = null;
-  selectedClass = null;
+  selectedDescriptor = null;
+  selectedType = null;
+  selectedFocus = null;
   gameState = null;
   conversationHistory = [];
   isBusy = false;