Advanced skills can no longer be unlocked through proficiency picks
Advanced skills must be unlocked with the pool cost. Only a profession's own proficiency still unlocks its skill for free. Proficiency picks need an unlocked skill, locking a skill returns its picks, old characters that used picks to unlock a skill get it unlocked automatically. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
This commit is contained in:
@@ -52,7 +52,7 @@ In the creator, the Concept page only has a "Rules preset" selector. Presets are
|
||||
|
||||
### Skills, proficiency and expertise
|
||||
|
||||
- `creation.advanced_unlock_cost`: pool points needed to unlock an advanced skill (free if a profession or proficiency covers it).
|
||||
- `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`.
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ def compute(ch: dict, rules: dict) -> dict:
|
||||
pips = prof_pip + max(0, free_pips)
|
||||
prof = [0, cr["proficiency_bonus"], cr["expertise_bonus"]][pips]
|
||||
unlocked = s["id"] in (ch.get("unlocked") or [])
|
||||
usable = s["type"] == "basic" or unlocked or prof > 0 # advanced skills must be unlocked
|
||||
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
|
||||
skills.append({**s, "points": pts, "prof": prof, "pips": pips, "usable": usable, "unlocked": unlocked,
|
||||
"total": best + pts + prof})
|
||||
|
||||
@@ -421,7 +421,7 @@
|
||||
"Proficiency: +10. Expertise: +20.",
|
||||
"Every character gets 2 free proficiency picks on top of the profession's proficiencies. A talent can grant 2 more.",
|
||||
"Putting two proficiency picks into the same skill makes it Expertise (a profession proficiency counts as one of the two).",
|
||||
"Proficiency in an advanced skill unlocks it for free.",
|
||||
"Proficiency picks can only be used on skills you can use, so an advanced skill has to be unlocked first.",
|
||||
"Groups: Combat, Survival, Social, Technical, Knowledge & Mental."
|
||||
]
|
||||
},
|
||||
|
||||
@@ -134,6 +134,7 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
|
||||
// Click on pip k (1 = Proficient, 2 = Expertise) of a skill: sets the skill's total pips to k, or one lower if already there.
|
||||
const clickPip = (s, k) => {
|
||||
if (!s.usable) return flash('Unlock this advanced skill first.')
|
||||
const target = s.pips >= k ? k - 1 : k
|
||||
const wantFree = Math.max(0, target - s.profPip)
|
||||
if (wantFree - s.freePips > c.profLeft) return flash('No proficiency picks left.')
|
||||
@@ -263,7 +264,7 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
<h2>Skills</h2>
|
||||
<p className="muted">
|
||||
At most half the attribute per skill. Advanced skills must be unlocked first for {c.unlockCost} points from the pool
|
||||
(free if your profession grants them). The pool size comes from the rules preset.
|
||||
(free if your profession grants proficiency in them). The pool size comes from the rules preset.
|
||||
</p>
|
||||
<div className="pools">
|
||||
<div className={`pool ${c.skillLeft < 0 ? 'over' : ''}`}>
|
||||
@@ -275,7 +276,7 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
</div>
|
||||
<p className="small muted">
|
||||
Proficiency adds +{cr.proficiency_bonus}. Two picks on the same skill make it Expertise (+{cr.expertise_bonus}); a profession proficiency counts as one of them.
|
||||
Proficiency in an advanced skill unlocks it for free. The talent "Versatile Training" grants 2 more picks.
|
||||
Proficiency picks only work on skills you can use, so unlock an advanced skill first. The talent "Versatile Training" grants 2 more picks.
|
||||
</p>
|
||||
{rules.skill_groups.map((g) => (
|
||||
<div key={g.id}>
|
||||
@@ -295,8 +296,8 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
<span className="small muted">Training</span>
|
||||
{[1, 2].map((k) => (
|
||||
<button key={k} className={`pip ${s.pips >= k ? 'on' : ''} ${s.profPip >= k ? 'from-prof' : ''}`}
|
||||
disabled={s.profPip >= k} onClick={() => clickPip(s, k)}
|
||||
title={s.profPip >= k ? 'From your profession' : k === 2 ? 'Expertise (uses 2 picks in total)' : 'Proficiency'}
|
||||
disabled={s.profPip >= k || !s.usable} onClick={() => clickPip(s, k)}
|
||||
title={!s.usable ? 'Unlock the skill first' : s.profPip >= k ? 'From your profession' : k === 2 ? 'Expertise (uses 2 picks in total)' : 'Proficiency'}
|
||||
aria-label={`${s.name} proficiency ${k}`}>{s.pips >= k ? '◆' : '◇'}</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -309,8 +310,8 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
) : (
|
||||
<div className="stepper">
|
||||
{s.type === 'advanced' && !s.free && (
|
||||
<button className="ghost small-btn" title={`Lock again and get ${c.unlockCost} points back`}
|
||||
onClick={() => setCh((x) => ({ ...x, unlocked: x.unlocked.filter((id) => id !== s.id), skill_points: { ...x.skill_points, [s.id]: 0 } }))}>Lock</button>
|
||||
<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>
|
||||
<span className="val">{s.points}</span>
|
||||
|
||||
@@ -82,19 +82,20 @@ export function normalize(ch, rules) {
|
||||
}
|
||||
// The preset may have been deleted since: fall back to whatever matches the numbers
|
||||
if (out.preset !== 'custom' && !(rules.presets || []).some((x) => x.id === out.preset)) out.preset = matchPreset(rules, out.creation)
|
||||
// Older characters: advanced skills that already hold points count as unlocked
|
||||
// Older characters: advanced skills that already hold points or proficiency picks count as unlocked
|
||||
const unlocked = new Set(out.unlocked)
|
||||
rules.skills.forEach((s) => { if (s.type === 'advanced' && (out.skill_points[s.id] || 0) > 0) unlocked.add(s.id) })
|
||||
rules.skills.forEach((s) => { if (s.type === 'advanced' && ((out.skill_points[s.id] || 0) > 0 || (out.proficiency_picks[s.id] || 0) > 0)) unlocked.add(s.id) })
|
||||
return { ...out, unlocked: [...unlocked] }
|
||||
}
|
||||
|
||||
// Clears points on advanced skills that are neither unlocked nor granted by a profession proficiency.
|
||||
// Clears points and proficiency picks on advanced skills that are neither unlocked nor granted by the profession.
|
||||
export function pruneLocked(ch, rules) {
|
||||
const points = { ...ch.skill_points }
|
||||
const picks = { ...ch.proficiency_picks }
|
||||
rules.skills.forEach((s) => {
|
||||
if (s.type === 'advanced' && !ch.unlocked.includes(s.id) && !ch.proficiencies[s.id] && !(ch.proficiency_picks[s.id] > 0)) delete points[s.id]
|
||||
if (s.type === 'advanced' && !ch.unlocked.includes(s.id) && !ch.proficiencies[s.id]) { delete points[s.id]; delete picks[s.id] }
|
||||
})
|
||||
return { ...ch, skill_points: points }
|
||||
return { ...ch, skill_points: points, proficiency_picks: picks }
|
||||
}
|
||||
|
||||
export function kitItems(ch, rules) {
|
||||
@@ -125,15 +126,15 @@ export function compute(ch, rules) {
|
||||
const best = Math.max(...s.attrs.map((a) => attrs[a])) // with several attributes the higher one counts
|
||||
const unlocked = ch.unlocked.includes(s.id)
|
||||
return { ...s, points, prof, profPip, freePips, pips, unlocked, total: best + points + prof,
|
||||
usable: s.type === 'basic' || unlocked || prof > 0, // advanced skills must be unlocked
|
||||
free: s.type === 'advanced' && prof > 0, // unlocked by the profession, costs nothing
|
||||
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) }
|
||||
})
|
||||
|
||||
const profSpent = skills.reduce((s, x) => s + x.freePips, 0)
|
||||
const profAllowed = (cr.free_proficiencies ?? 0) + rules.talents.filter((t) => ch.talents.includes(t.id)).reduce((s, t) => s + (t.proficiency_picks || 0), 0)
|
||||
const unlockCost = cr.advanced_unlock_cost ?? 0
|
||||
const paidUnlocks = skills.filter((s) => s.type === 'advanced' && s.unlocked && !s.prof).length
|
||||
const paidUnlocks = skills.filter((s) => s.type === 'advanced' && s.unlocked && !s.profPip).length
|
||||
const attrSpent = rules.attributes.reduce((s, a) => s + (ch.attributes[a.id] - cr.attr_start), 0)
|
||||
const skillSpent = Object.values(ch.skill_points).reduce((s, v) => s + v, 0) + paidUnlocks * unlockCost
|
||||
const chosen = rules.augments.filter((a) => ch.augments.includes(a.id))
|
||||
|
||||
@@ -33,9 +33,9 @@ export function explainSkill(s, { ch, rules, c }) {
|
||||
`Most points you can put in: best attribute ÷ ${cr.skill_max_divisor}, rounded down = ${s.max}`,
|
||||
]
|
||||
if (s.type === 'advanced') {
|
||||
lines.push(s.free ? 'Advanced skill: unlocked for free by a proficiency.'
|
||||
lines.push(s.free ? 'Advanced skill: unlocked for free by your profession.'
|
||||
: s.unlocked ? `Advanced skill: unlocked (cost ${c.unlockCost} points from the pool).`
|
||||
: `Advanced skill: locked. Unlock it for ${c.unlockCost} points, or get a proficiency in it.`)
|
||||
: `Advanced skill: locked. Unlock it for ${c.unlockCost} points from the pool before you can use it.`)
|
||||
}
|
||||
if (s.pips === 1 && s.profPip) lines.push('Proficient through your profession.')
|
||||
return lines
|
||||
|
||||
Reference in New Issue
Block a user