fuzzyworld

dm / commit

feat: Phase 5a/5b/5d — skill system display, starting skills, system prompt (SAVE_VERSION 3)

86441da64c4b45e59f2d8f5b29ef322e28364dff · Pavel Safronov · 2026-05-27 23:06:54 -0700 · parents 1e438f26a5dc

feat: Phase 5a/5b/5d — skill system display, starting skills, system prompt (SAVE_VERSION 3)

- Add Skills sidebar section between Abilities and Equipment; rank badges green/gold/red for trained/specialized/inability
- Add startingSkills to all 4 Types and all 34 Descriptors
- mergeSkills() in beginAdventure(): two trained instances of the same skill promote to specialized (CSRD rule)
- character.skills flows into buildSystemPrompt() SKILLS section so Claude knows relevant skills for rolls
- Bump SAVE_VERSION to 3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 plan.md           |  16 +++---
 public/index.html | 166 ++++++++++++++++++++++++++++++++++++++++++------------
 2 files changed, 138 insertions(+), 44 deletions(-)

diff --git a/plan.md b/plan.md
index 6983f27..284d072 100644
--- a/plan.md
+++ b/plan.md
@@ -183,17 +183,17 @@ The `skills` array already exists in `gameState.character` as `[{ name, rank: 't
 
 ### 5a — Display
 
-- [ ] New "Skills" section in the sidebar (between Abilities and Equipment)
-- [ ] Each skill shows name + rank badge: green "Trained" / gold "Specialized" / red "Inability"
-- [ ] Empty state: "No skills yet"
+- [x] New "Skills" section in the sidebar (between Abilities and Equipment)
+- [x] Each skill shows name + rank badge: green "Trained" / gold "Specialized" / red "Inability"
+- [x] Empty state: "No skills yet"
 
 ### 5b — Type and Descriptor starting skills
 
 Several Type Tier 1 abilities grant skill training ("Physical Skills", "Interaction Skills", "Danger Sense", etc.) but currently these are stored as narrative text in `abilities`, not as actual entries in `skills`. Fix by:
 
-- [ ] Add a `startingSkills` field to each Type (parallel to `startingAbilities`)
-- [ ] Add `descriptorSkills` field to each Descriptor for their trained skills
-- [ ] Populate `character.skills` from both in `beginAdventure()`
+- [x] Add a `startingSkills` field to each Type (parallel to `startingAbilities`)
+- [x] Add `startingSkills` field to each Descriptor for their trained skills
+- [x] Populate `character.skills` from both in `beginAdventure()` — overlapping trained skills promote to specialized
 
 ### 5c — Skill advancement
 
@@ -207,7 +207,7 @@ The XP advancement path ("train a skill") is currently only narrated. To make it
 
 When a player's message mentions an action, Claude already references skills in its narration. Future enhancement:
 
-- [ ] Pass `character.skills` in the system prompt so Claude can cite which skills apply to a given roll
+- [x] Pass `character.skills` in the system prompt so Claude can cite which skills apply to a given roll
 - [ ] Expose "I am trained in X" as a quick action or structured input so the player can assert relevant skills before rolling
 
 ---
@@ -217,5 +217,5 @@ When a player's message mentions an action, Claude already references skills in
 - 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
-- Phase 5 requires a `SAVE_VERSION` bump when 5b lands (new `startingSkills` field changes character shape)
+- Phase 5a/5b/5d landed together with `SAVE_VERSION` bump to 3
 - 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 a06d878..a16f0a2 100644
--- a/public/index.html
+++ b/public/index.html
@@ -621,6 +621,12 @@ body {
 .ability-name { display:block; font-family:'Cinzel',serif; font-size:0.68rem; color:var(--gold-light); margin-bottom:2px; }
 .ability-cost { color:var(--text-dim); font-weight:400; }
 .ability-desc { display:block; font-size:0.75rem; color:var(--text-dim); line-height:1.4; }
+.skill-entry { display:flex; align-items:center; justify-content:space-between; padding:4px 9px; margin-bottom:4px; border-radius:4px; background:var(--bg-card); border:1px solid var(--border); }
+.skill-name { font-size:0.78rem; color:var(--text-main); }
+.skill-rank { font-size:0.65rem; font-weight:700; letter-spacing:0.04em; padding:2px 6px; border-radius:3px; text-transform:uppercase; }
+.skill-rank.trained { background:#14532d; color:#4ade80; }
+.skill-rank.specialized { background:#713f12; color:#fbbf24; }
+.skill-rank.inability { background:#450a0a; color:#f87171; }
 
 /* Advancement row */
 .adv-row { display:flex; align-items:center; gap:8px; margin:4px 0 6px; flex-wrap:wrap; }
@@ -1389,6 +1395,12 @@ body {
       <div id="abilities-list"></div>
     </div>
 
+    <!-- Skills -->
+    <div class="sidebar-section">
+      <div class="sidebar-title">Skills</div>
+      <div id="skills-list"></div>
+    </div>
+
     <!-- Equipment -->
     <div class="sidebar-section">
       <div class="sidebar-title">Equipment</div>
@@ -1737,6 +1749,10 @@ const TYPES = [
       { name: 'Practiced with All Weapons', cost: null, description: 'You can use any weapon without penalty.' },
       { name: 'Physical Skills', cost: null, description: 'Trained in two physical skills of your choice (e.g. climbing, swimming, jumping, running).' },
       { name: 'Bash', cost: '1 Might', description: 'Your attack inflicts 2 additional points of damage.' },
+    ],
+    startingSkills: [
+      { name: 'Climbing', rank: 'trained' },
+      { name: 'Swimming', rank: 'trained' },
     ] },
   { name: 'Adept', icon: '🔮',
     pools: { might: 7, speed: 9, intellect: 12 },
@@ -1749,6 +1765,9 @@ const TYPES = [
       { name: 'Magical Training', cost: null, description: 'Trained in all esoteries as if they were skills.' },
       { name: 'Hedge Magic', cost: '1 Intellect', description: 'Perform a minor magical trick — small light, move a tiny object, clean something, etc.' },
       { name: 'Ward', cost: null, description: 'A constant magical field grants you +1 Armor at all times.' },
+    ],
+    startingSkills: [
+      { name: 'Esoteries', rank: 'trained' },
     ] },
   { name: 'Explorer', icon: '🧭',
     pools: { might: 10, speed: 9, intellect: 9 },
@@ -1761,6 +1780,11 @@ const TYPES = [
       { name: 'Practiced in Armor', cost: null, description: 'Wear light armor without any Speed Effort cost.' },
       { name: 'Danger Sense', cost: null, description: 'Your initiative tasks are eased.' },
       { name: 'Physical Skills', cost: null, description: 'Trained in two physical skills of your choice.' },
+    ],
+    startingSkills: [
+      { name: 'Initiative', rank: 'trained' },
+      { name: 'Climbing', rank: 'trained' },
+      { name: 'Swimming', rank: 'trained' },
     ] },
   { name: 'Speaker', icon: '🗣️',
     pools: { might: 8, speed: 9, intellect: 11 },
@@ -1773,44 +1797,83 @@ const TYPES = [
       { name: 'Spin Identity', cost: '2+ Intellect', description: 'Convince others you are someone you are not; lasts the duration of the interaction.' },
       { name: 'Interaction Skills', cost: null, description: 'Trained in deception, persuasion, and intimidation.' },
       { name: 'Babel', cost: null, description: 'After listening to any spoken language briefly, you can understand it and make yourself understood.' },
+    ],
+    startingSkills: [
+      { name: 'Deception', rank: 'trained' },
+      { name: 'Persuasion', rank: 'trained' },
+      { name: 'Intimidation', rank: 'trained' },
     ] },
 ];
 
 const DESCRIPTORS = [
-  { name: 'Appealing',    icon: '😊', poolBonus: { speed: 2 },     note: 'Your presence puts others at ease. Trained in pleasant social interactions.' },
-  { name: 'Brash',        icon: '😤', poolBonus: { speed: 2 },     note: 'Act first, think later. Trained in initiative; +2 Speed pool.' },
-  { name: 'Calm',         icon: '🧘', poolBonus: { intellect: 2 }, note: 'Unshakeable under pressure. Trained in resisting persuasion and panic.' },
-  { name: 'Charming',     icon: '💫', poolBonus: { intellect: 2 }, note: 'Hard to dislike and easy to trust. Trained in persuasion and performance.' },
-  { name: 'Clever',       icon: '🧠', poolBonus: { intellect: 2 }, note: 'Skilled at lies, trickery, and assessing danger.' },
-  { name: 'Creative',     icon: '🎨', poolBonus: { intellect: 2 }, note: 'Finds unexpected solutions. Trained in all creative tasks (art, music, writing).' },
-  { name: 'Empathic',     icon: '💗', poolBonus: { intellect: 2 }, note: 'Reads emotional states with ease. Trained in sensing motives and first-aid.' },
-  { 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 the damage track.' },
-  { name: 'Honorable',    icon: '🛡️', poolBonus: { might: 2 },     note: 'Trained in pleasant social interactions and detecting lies.' },
-  { name: 'Impulsive',    icon: '⚡', poolBonus: { speed: 2 },     note: 'You go first and think later. Trained in initiative and Speed defense.' },
-  { 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: 'Kind',         icon: '🕊️', poolBonus: { intellect: 2 }, note: 'Genuine warmth that opens doors. Trained in healing and social tasks.' },
-  { name: 'Learned',      icon: '🎓', poolBonus: { intellect: 4 }, note: 'A walking library. Trained in two knowledge areas of your choice.' },
-  { name: 'Loyal',        icon: '🤝', poolBonus: { might: 2 },     note: 'Your allies inspire you. Trained in tasks that help companions directly.' },
-  { 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: 'Rugged',       icon: '🏔️', poolBonus: { might: 2, speed: 2 }, note: 'Built for hard conditions. Light armor without penalties; trained in navigation.' },
-  { name: 'Sharp-Eyed',   icon: '🦅', poolBonus: {},               note: 'Trained in initiative and perception; can spot a foe\'s weakness.' },
-  { 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: 'Swift',        icon: '🐆', poolBonus: { speed: 4 },     note: 'Trained in running and escape; movement is never restricted.' },
-  { name: 'Tough',        icon: '🦾', poolBonus: { might: 2 },     note: '+1 Armor; +1 to recovery rolls.' },
-  { name: 'Vicious',      icon: '🩸', poolBonus: { might: 2 },     note: 'You fight to hurt. Attacks deal 1 additional point of damage.' },
-  { name: 'Virtuous',     icon: '⭐', poolBonus: { intellect: 2 }, note: 'Trained in resisting temptation and corruption; allies trust you deeply.' },
-  { name: 'Weird',        icon: '🌀', poolBonus: { intellect: 2 }, note: 'Trained in supernatural knowledge; senses nearby strange phenomena.' },
+  { name: 'Appealing',    icon: '😊', poolBonus: { speed: 2 },     note: 'Your presence puts others at ease. Trained in pleasant social interactions.',
+    startingSkills: [{ name: 'Pleasant social interactions', rank: 'trained' }] },
+  { name: 'Brash',        icon: '😤', poolBonus: { speed: 2 },     note: 'Act first, think later. Trained in initiative; +2 Speed pool.',
+    startingSkills: [{ name: 'Initiative', rank: 'trained' }] },
+  { name: 'Calm',         icon: '🧘', poolBonus: { intellect: 2 }, note: 'Unshakeable under pressure. Trained in resisting persuasion and panic.',
+    startingSkills: [{ name: 'Resisting persuasion', rank: 'trained' }] },
+  { name: 'Charming',     icon: '💫', poolBonus: { intellect: 2 }, note: 'Hard to dislike and easy to trust. Trained in persuasion and performance.',
+    startingSkills: [{ name: 'Persuasion', rank: 'trained' }, { name: 'Performance', rank: 'trained' }] },
+  { name: 'Clever',       icon: '🧠', poolBonus: { intellect: 2 }, note: 'Skilled at lies, trickery, and assessing danger.',
+    startingSkills: [{ name: 'Deception', rank: 'trained' }, { name: 'Assessing danger', rank: 'trained' }] },
+  { name: 'Creative',     icon: '🎨', poolBonus: { intellect: 2 }, note: 'Finds unexpected solutions. Trained in all creative tasks (art, music, writing).',
+    startingSkills: [{ name: 'Creative tasks', rank: 'trained' }] },
+  { name: 'Empathic',     icon: '💗', poolBonus: { intellect: 2 }, note: 'Reads emotional states with ease. Trained in sensing motives and first-aid.',
+    startingSkills: [{ name: 'Sensing motives', rank: 'trained' }, { name: 'First aid', rank: 'trained' }] },
+  { name: 'Fast',         icon: '💨', poolBonus: { speed: 2 },     note: 'Can move and act in the same round.',
+    startingSkills: [] },
+  { name: 'Graceful',     icon: '🌸', poolBonus: { speed: 2 },     note: 'Trained in balance, performance, and Speed defense.',
+    startingSkills: [{ name: 'Balance', rank: 'trained' }, { name: 'Performance', rank: 'trained' }, { name: 'Speed defense', rank: 'trained' }] },
+  { name: 'Hardy',        icon: '🪨', poolBonus: { might: 4 },     note: 'Functions one step better on the damage track.',
+    startingSkills: [] },
+  { name: 'Honorable',    icon: '🛡️', poolBonus: { might: 2 },     note: 'Trained in pleasant social interactions and detecting lies.',
+    startingSkills: [{ name: 'Pleasant social interactions', rank: 'trained' }, { name: 'Detecting lies', rank: 'trained' }] },
+  { name: 'Impulsive',    icon: '⚡', poolBonus: { speed: 2 },     note: 'You go first and think later. Trained in initiative and Speed defense.',
+    startingSkills: [{ name: 'Initiative', rank: 'trained' }, { name: 'Speed defense', rank: 'trained' }] },
+  { name: 'Inquisitive',  icon: '🔍', poolBonus: { intellect: 4 }, note: 'Trained in learning, geography, and history.',
+    startingSkills: [{ name: 'Learning', rank: 'trained' }, { name: 'Geography', rank: 'trained' }, { name: 'History', rank: 'trained' }] },
+  { name: 'Intelligent',  icon: '📚', poolBonus: { intellect: 2 }, note: 'Trained in one knowledge area and memorization.',
+    startingSkills: [{ name: 'Memorization', rank: 'trained' }] },
+  { name: 'Intuitive',    icon: '✨', poolBonus: { intellect: 2 }, note: 'Trained in perception; can act out of turn once per cycle.',
+    startingSkills: [{ name: 'Perception', rank: 'trained' }] },
+  { name: 'Kind',         icon: '🕊️', poolBonus: { intellect: 2 }, note: 'Genuine warmth that opens doors. Trained in healing and social tasks.',
+    startingSkills: [{ name: 'Healing', rank: 'trained' }, { name: 'Social interactions', rank: 'trained' }] },
+  { name: 'Learned',      icon: '🎓', poolBonus: { intellect: 4 }, note: 'A walking library. Trained in two knowledge areas of your choice.',
+    startingSkills: [] },
+  { name: 'Loyal',        icon: '🤝', poolBonus: { might: 2 },     note: 'Your allies inspire you. Trained in tasks that help companions directly.',
+    startingSkills: [{ name: 'Helping companions', rank: 'trained' }] },
+  { name: 'Lucky',        icon: '🍀', poolBonus: {},               note: 'Gains a 3-point Luck Pool; bonuses to rerolls.',
+    startingSkills: [] },
+  { name: 'Mighty',       icon: '💪', poolBonus: { might: 4 },     note: 'Trained in lifting/throwing; +1 to recovery rolls.',
+    startingSkills: [{ name: 'Lifting', rank: 'trained' }, { name: 'Throwing', rank: 'trained' }] },
+  { name: 'Mysterious',   icon: '🌑', poolBonus: {},               note: 'Trained in stealth and resisting interrogation.',
+    startingSkills: [{ name: 'Stealth', rank: 'trained' }, { name: 'Resisting interrogation', rank: 'trained' }] },
+  { name: 'Mystical',     icon: '🌙', poolBonus: { intellect: 2 }, note: 'Can detect and cast minor magic; trained in the supernatural.',
+    startingSkills: [{ name: 'Supernatural knowledge', rank: 'trained' }] },
+  { name: 'Perceptive',   icon: '👁️', poolBonus: { intellect: 2 }, note: 'Trained in noticing details and recalling trivia.',
+    startingSkills: [{ name: 'Noticing details', rank: 'trained' }, { name: 'Recalling trivia', rank: 'trained' }] },
+  { name: 'Resilient',    icon: '🌳', poolBonus: { might: 2, intellect: 2 }, note: 'Trained in both Might and Intellect defense; extra recovery roll.',
+    startingSkills: [{ name: 'Might defense', rank: 'trained' }, { name: 'Intellect defense', rank: 'trained' }] },
+  { name: 'Rugged',       icon: '🏔️', poolBonus: { might: 2, speed: 2 }, note: 'Built for hard conditions. Light armor without penalties; trained in navigation.',
+    startingSkills: [{ name: 'Navigation', rank: 'trained' }] },
+  { name: 'Sharp-Eyed',   icon: '🦅', poolBonus: {},               note: 'Trained in initiative and perception; can spot a foe\'s weakness.',
+    startingSkills: [{ name: 'Initiative', rank: 'trained' }, { name: 'Perception', rank: 'trained' }] },
+  { name: 'Stealthy',     icon: '🌫️', poolBonus: { speed: 2 },     note: 'Trained in stealth, trickery, and illusions.',
+    startingSkills: [{ name: 'Stealth', rank: 'trained' }, { name: 'Trickery', rank: 'trained' }] },
+  { name: 'Strong',       icon: '🪓', poolBonus: { might: 4 },     note: 'Trained in breaking objects and jumping.',
+    startingSkills: [{ name: 'Breaking objects', rank: 'trained' }, { name: 'Jumping', rank: 'trained' }] },
+  { name: 'Strong-Willed',icon: '🔮', poolBonus: { intellect: 4 }, note: 'Trained in resisting mental effects and concentration.',
+    startingSkills: [{ name: 'Resisting mental effects', rank: 'trained' }, { name: 'Concentration', rank: 'trained' }] },
+  { name: 'Swift',        icon: '🐆', poolBonus: { speed: 4 },     note: 'Trained in running and escape; movement is never restricted.',
+    startingSkills: [{ name: 'Running', rank: 'trained' }, { name: 'Escaping', rank: 'trained' }] },
+  { name: 'Tough',        icon: '🦾', poolBonus: { might: 2 },     note: '+1 Armor; +1 to recovery rolls.',
+    startingSkills: [] },
+  { name: 'Vicious',      icon: '🩸', poolBonus: { might: 2 },     note: 'You fight to hurt. Attacks deal 1 additional point of damage.',
+    startingSkills: [] },
+  { name: 'Virtuous',     icon: '⭐', poolBonus: { intellect: 2 }, note: 'Trained in resisting temptation and corruption; allies trust you deeply.',
+    startingSkills: [{ name: 'Resisting temptation', rank: 'trained' }] },
+  { name: 'Weird',        icon: '🌀', poolBonus: { intellect: 2 }, note: 'Trained in supernatural knowledge; senses nearby strange phenomena.',
+    startingSkills: [{ name: 'Supernatural knowledge', rank: 'trained' }] },
 ];
 
 // settings: which genres this focus fits. 'all' = any setting.
@@ -2127,7 +2190,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 = 2;
+const SAVE_VERSION = 3;
 
 function saveSession() {
   if (!gameState) return;
@@ -2315,6 +2378,22 @@ function buildStartingPools() {
   };
 }
 
+function mergeSkills(typeSkills, descriptorSkills) {
+  const rankOrder = { inability: 0, trained: 1, specialized: 2 };
+  const merged = {};
+  for (const s of [...typeSkills, ...descriptorSkills]) {
+    const key = s.name.toLowerCase();
+    if (!merged[key]) {
+      merged[key] = { ...s };
+    } else if (merged[key].rank === 'trained' && s.rank === 'trained') {
+      merged[key] = { ...s, rank: 'specialized' };
+    } else if (rankOrder[s.rank] > rankOrder[merged[key].rank]) {
+      merged[key] = { ...s };
+    }
+  }
+  return Object.values(merged);
+}
+
 async function beginAdventure() {
   if (!currentApiKey) { openSetupModal(getSharedBlob() ? 'shared' : 'own'); return; }
   const apiKey = currentApiKey;
@@ -2343,7 +2422,7 @@ async function beginAdventure() {
       advancementSteps: 0,
       cypherLimit: selectedType.cypherLimit,
       cyphers: [],
-      skills: [],
+      skills: mergeSkills(selectedType.startingSkills || [], selectedDescriptor.startingSkills || []),
       abilities: [
         ...(selectedType.startingAbilities || []),
         ...(selectedFocus.tier1 ? [selectedFocus.tier1] : []),
@@ -2469,6 +2548,21 @@ function updateSidebar() {
     abList.innerHTML = '<span style="font-size:0.78rem;color:var(--text-dim);font-style:italic">None yet.</span>';
   }
 
+  // Skills
+  const skillsEl = document.getElementById('skills-list');
+  skillsEl.innerHTML = '';
+  if (c.skills?.length) {
+    c.skills.forEach(sk => {
+      const div = document.createElement('div');
+      div.className = 'skill-entry';
+      div.innerHTML = `<span class="skill-name">${sk.name}</span>`
+        + `<span class="skill-rank ${sk.rank}">${sk.rank}</span>`;
+      skillsEl.appendChild(div);
+    });
+  } else {
+    skillsEl.innerHTML = '<span style="font-size:0.78rem;color:var(--text-dim);font-style:italic">No skills yet.</span>';
+  }
+
   // Equipment
   const list = document.getElementById('inventory-list');
   list.innerHTML = '';