dm / commit
feat: Phase 5a/5b/5d — skill system display, starting skills, system prompt (SAVE_VERSION 3)
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 = '';