Skills: +-5 buttons, advanced skills use 50% of the attribute (creation.advanced_attr_percent)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
},
|
||||
{
|
||||
|
||||
@@ -314,9 +314,11 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
<button className="ghost small-btn" title={`Lock again: you get ${c.unlockCost} points back, its points and proficiency picks are returned`}
|
||||
onClick={() => setCh((x) => ({ ...x, unlocked: x.unlocked.filter((id) => id !== s.id), skill_points: { ...x.skill_points, [s.id]: 0 }, proficiency_picks: { ...x.proficiency_picks, [s.id]: 0 } }))}>Lock</button>
|
||||
)}
|
||||
<button onClick={() => setPts(s.points - 1)} aria-label={`${s.name} minus`}>−</button>
|
||||
<button className="step5" onClick={() => setPts(s.points - 5)} disabled={s.points <= 0} aria-label={`${s.name} minus 5`}>−5</button>
|
||||
<button onClick={() => setPts(s.points - 1)} disabled={s.points <= 0} aria-label={`${s.name} minus`}>−</button>
|
||||
<span className="val">{s.points}</span>
|
||||
<button onClick={() => setPts(s.points + 1)} disabled={s.points >= s.max || c.skillLeft <= 0} aria-label={`${s.name} plus`}>+</button>
|
||||
<button onClick={() => setPts(Math.min(s.points + 1, s.max, s.points + c.skillLeft))} disabled={s.points >= s.max || c.skillLeft <= 0} aria-label={`${s.name} plus`}>+</button>
|
||||
<button className="step5" onClick={() => setPts(Math.min(s.points + 5, s.max, s.points + c.skillLeft))} disabled={s.points >= s.max || c.skillLeft <= 0} aria-label={`${s.name} plus 5`}>+5</button>
|
||||
<Tip text={explainSkill(s, ctx)}><span className="mod">{s.total}</span></Tip><span className="small muted" title="Most points you can add"> max {s.max}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user