fuzzyworld

dm / commit

feat: skills_add in update_character tool + descriptor inabilities

e0ac52e786f66fe1594bdcbd638de1c266d39ec5 ยท Pavel Safronov ยท 2026-05-27 23:23:50 -0700 ยท parents 4aedf3e4eb0b

feat: skills_add in update_character tool + descriptor inabilities

- Add skills_add field to UPDATE_TOOL schema (required array, enum rank)
- applyStatUpdate now processes skills_add: adds new skills, upgrades ranks,
  promotes double-trained to specialized (CSRD rule)
- Add inability startingSkills to Brash (patience tasks), Impulsive (careful
  deliberation), and Vicious (pleasant social interactions)
- Inability rank renders with red badge in sidebar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 public/index.html | 33 ++++++++++++++++++++++++++-------
 1 file changed, 26 insertions(+), 7 deletions(-)

diff --git a/public/index.html b/public/index.html
index 65a516f..a9621bb 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1858,8 +1858,8 @@ const TYPES = [
 const DESCRIPTORS = [
   { 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: 'Brash',        icon: '๐Ÿ˜ค', poolBonus: { speed: 2 },     note: 'Act first, think later. Trained in initiative; inability in patience tasks.',
+    startingSkills: [{ name: 'Initiative', rank: 'trained' }, { name: 'Tasks requiring patience', rank: 'inability' }] },
   { 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.',
@@ -1878,8 +1878,8 @@ const DESCRIPTORS = [
     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: 'Impulsive',    icon: 'โšก', poolBonus: { speed: 2 },     note: 'You go first and think later. Trained in initiative and Speed defense; inability in careful deliberation.',
+    startingSkills: [{ name: 'Initiative', rank: 'trained' }, { name: 'Speed defense', rank: 'trained' }, { name: 'Careful deliberation', rank: 'inability' }] },
   { 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.',
@@ -1918,8 +1918,8 @@ const DESCRIPTORS = [
     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: 'Vicious',      icon: '๐Ÿฉธ', poolBonus: { might: 2 },     note: 'You fight to hurt. Attacks deal 1 additional point of damage; inability in pleasant social interactions.',
+    startingSkills: [{ name: 'Pleasant social interactions', rank: 'inability' }] },
   { 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.',
@@ -2089,10 +2089,11 @@ const UPDATE_TOOL = {
       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.' },
+      skills_add:       { type: 'array',  items: { type: 'object', properties: { name: { type: 'string' }, rank: { type: 'string', enum: ['trained','specialized','inability'] } }, required: ['name','rank'] }, description: 'Skills to grant or upgrade. Use to reflect training earned during play. Rank: trained, specialized, or inability. Two trained in the same skill auto-promotes to specialized.' },
       tier_advance:     { type: 'boolean', description: 'True if character advances a Tier.' },
       reason:           { type: 'string', description: 'Brief reason for the change.' },
     },
-    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'],
+    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','skills_add','tier_advance','reason'],
   },
 };
 
@@ -2684,6 +2685,24 @@ function applyStatUpdate(update) {
     });
   }
 
+  // Skills
+  if (update.skills_add?.length) {
+    const rankOrder = { inability: 0, trained: 1, specialized: 2 };
+    update.skills_add.forEach(({ name, rank }) => {
+      if (!name || !rank) return;
+      const existing = (c.skills || []).find(s => s.name.toLowerCase() === name.toLowerCase());
+      if (existing) {
+        if (existing.rank === 'trained' && rank === 'trained') {
+          existing.rank = 'specialized';
+        } else if (rankOrder[rank] > rankOrder[existing.rank]) {
+          existing.rank = rank;
+        }
+      } else {
+        c.skills = [...(c.skills || []), { name, rank }];
+      }
+    });
+  }
+
   // Equipment
   if (update.equipment_add?.length)    c.equipment.push(...update.equipment_add);
   if (update.equipment_remove?.length) {