From a8c9740a05f9ff70032a18e075a0f772b83d19a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 14:37:52 +0000 Subject: [PATCH] Skills: +-5 buttons, advanced skills use 50% of the attribute (creation.advanced_attr_percent) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K --- README.md | 1 + backend/app/derive.py | 3 ++- backend/data/rules.json | 4 +++- frontend/src/Creator.jsx | 6 ++++-- frontend/src/calc.js | 3 ++- frontend/src/explain.js | 2 +- frontend/src/styles.css | 1 + 7 files changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4a04b1c..706260f 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ In the creator, the Concept page only has a "Rules preset" selector. Presets are ### Skills, proficiency and expertise +- `creation.advanced_attr_percent`: share of the attribute that counts for **advanced skills** (default 50, rounded down; 100 = full attribute like basic skills). - `creation.advanced_unlock_cost`: pool points needed to unlock an advanced skill (free if the character's profession grants proficiency in it; proficiency picks do not unlock a skill). - `creation.free_proficiencies`: free proficiency picks every character gets. Talents can add more with `"proficiency_picks": N` (see "Versatile Training"). - Proficiency is +`proficiency_bonus`; two picks on one skill (a profession proficiency counts as one) give Expertise, +`expertise_bonus`. diff --git a/backend/app/derive.py b/backend/app/derive.py index bc347cf..c6ad89e 100644 --- a/backend/app/derive.py +++ b/backend/app/derive.py @@ -39,8 +39,9 @@ def compute(ch: dict, rules: dict) -> dict: unlocked = s["id"] in (ch.get("unlocked") or []) usable = s["type"] == "basic" or unlocked or prof_pip > 0 # advanced skills must be unlocked (a profession proficiency does it for free) best = max(attrs[a] for a in s["attrs"]) # with several attributes the higher one counts + base = best * cr.get("advanced_attr_percent", 100) // 100 if s["type"] == "advanced" else best # advanced skills only use part of the attribute skills.append({**s, "points": pts, "prof": prof, "pips": pips, "usable": usable, "unlocked": unlocked, - "total": best + pts + prof}) + "base": base, "total": base + pts + prof}) aug_by_id = {a["id"]: a for a in rules["augments"]} chosen = [aug_by_id[i] for i in ch.get("augments", []) if i in aug_by_id] diff --git a/backend/data/rules.json b/backend/data/rules.json index b8a06e5..b8e90d2 100644 --- a/backend/data/rules.json +++ b/backend/data/rules.json @@ -15,6 +15,7 @@ "skill_points_default": 150, "skill_max_divisor": 2, "advanced_unlock_cost": 10, + "advanced_attr_percent": 50, "free_proficiencies": 2, "max_proficiency_pips": 2, "talents_min": 1, @@ -41,6 +42,7 @@ {"key": "profession_attr_bonus", "label": "Profession attribute bonus", "group": "Profession", "min": 0, "max": 50}, {"key": "free_proficiencies", "label": "Free proficiency picks", "group": "Skills", "min": 0, "max": 20}, {"key": "skill_points_default", "label": "Skill point pool", "group": "Skills", "min": 0, "max": 1000}, + {"key": "advanced_attr_percent", "label": "Advanced skills: share of the attribute that counts (%)", "group": "Skills", "min": 0, "max": 100}, {"key": "advanced_unlock_cost", "label": "Cost to unlock an advanced skill", "group": "Skills", "min": 0, "max": 100}, {"key": "skill_max_divisor", "label": "Skill maximum = attribute divided by", "group": "Skills", "min": 1, "max": 10}, {"key": "proficiency_bonus", "label": "Proficiency bonus", "group": "Skills", "min": 0, "max": 50}, @@ -434,7 +436,7 @@ "placeholder": true }, { - "p": "The unlock cost for advanced skills (10 points) is a house rule of the app, not part of the brainstorm. It can be changed in rules.json (creation.advanced_unlock_cost).", + "p": "The unlock cost for advanced skills (10 points) and the rule that advanced skills only use half of the attribute are house rules of the app, not part of the brainstorm. They can be changed in rules.json (creation.advanced_unlock_cost, creation.advanced_attr_percent).", "placeholder": true }, { diff --git a/frontend/src/Creator.jsx b/frontend/src/Creator.jsx index 1c561fb..3bf9be0 100644 --- a/frontend/src/Creator.jsx +++ b/frontend/src/Creator.jsx @@ -314,9 +314,11 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad )} - + + {s.points} - + + {s.total} max {s.max} )} diff --git a/frontend/src/calc.js b/frontend/src/calc.js index 88e8cd8..0ebf6fe 100644 --- a/frontend/src/calc.js +++ b/frontend/src/calc.js @@ -124,8 +124,9 @@ export function compute(ch, rules) { const pips = profPip + freePips const prof = [0, cr.proficiency_bonus, cr.expertise_bonus][pips] const best = Math.max(...s.attrs.map((a) => attrs[a])) // with several attributes the higher one counts + const base = s.type === 'advanced' ? Math.floor((best * (cr.advanced_attr_percent ?? 100)) / 100) : best // advanced skills only use part of the attribute const unlocked = ch.unlocked.includes(s.id) - return { ...s, points, prof, profPip, freePips, pips, unlocked, total: best + points + prof, + return { ...s, points, prof, profPip, freePips, pips, unlocked, base, total: base + points + prof, usable: s.type === 'basic' || unlocked || profPip > 0, // advanced skills must be unlocked (a profession proficiency does it for free) free: s.type === 'advanced' && profPip > 0, // unlocked by the profession, costs nothing max: Math.floor(best / cr.skill_max_divisor) } diff --git a/frontend/src/explain.js b/frontend/src/explain.js index ed88497..fd0286f 100644 --- a/frontend/src/explain.js +++ b/frontend/src/explain.js @@ -29,7 +29,7 @@ export function explainSkill(s, { ch, rules, c }) { const list = s.attrs.map((id) => `${attrShort(rules, id)} ${c.attrs[id]}`).join(', ') const training = s.pips === 2 ? `Expertise +${s.prof}` : s.pips === 1 ? `Proficient +${s.prof}` : 'no training +0' const lines = [ - `Target number = best attribute ${best} (${s.attrs.length > 1 ? `higher of ${list}` : list}) + ${s.points} skill points + ${training} = ${s.total}`, + `Target number = ${s.type === 'advanced' && s.base !== best ? `${cr.advanced_attr_percent}% of the best attribute (${best}) = ${s.base}` : `best attribute ${best}`} (${s.attrs.length > 1 ? `higher of ${list}` : list}) + ${s.points} skill points + ${training} = ${s.total}`, `Most points you can put in: best attribute รท ${cr.skill_max_divisor}, rounded down = ${s.max}`, ] if (s.type === 'advanced') { diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 3694e56..eeb63d2 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -384,3 +384,4 @@ body:has(.lobbychat) main { padding: .5rem calc(var(--chatw) + .5rem) 0 .5rem; } .tablelog { position: absolute; left: .6rem; bottom: .5rem; display: grid; gap: .25rem; max-width: 70%; } .tablelog .trayroll { padding: .2rem .6rem; border-radius: 8px; font-size: .85rem; background: color-mix(in srgb, var(--panel) 88%, transparent); border: 1px solid var(--line); border-top: 3px solid var(--line); box-shadow: none; } @media (prefers-reduced-motion: reduce) { .thrown { animation: none; } } +.stepper .step5 { font-size: .75rem; padding: .25rem .4rem; min-width: 2.2rem; }