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
|
### 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").
|
- `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`.
|
- 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)
|
pips = prof_pip + max(0, free_pips)
|
||||||
prof = [0, cr["proficiency_bonus"], cr["expertise_bonus"]][pips]
|
prof = [0, cr["proficiency_bonus"], cr["expertise_bonus"]][pips]
|
||||||
unlocked = s["id"] in (ch.get("unlocked") or [])
|
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
|
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,
|
skills.append({**s, "points": pts, "prof": prof, "pips": pips, "usable": usable, "unlocked": unlocked,
|
||||||
"total": best + pts + prof})
|
"total": best + pts + prof})
|
||||||
|
|||||||
@@ -421,7 +421,7 @@
|
|||||||
"Proficiency: +10. Expertise: +20.",
|
"Proficiency: +10. Expertise: +20.",
|
||||||
"Every character gets 2 free proficiency picks on top of the profession's proficiencies. A talent can grant 2 more.",
|
"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).",
|
"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."
|
"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.
|
// 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) => {
|
const clickPip = (s, k) => {
|
||||||
|
if (!s.usable) return flash('Unlock this advanced skill first.')
|
||||||
const target = s.pips >= k ? k - 1 : k
|
const target = s.pips >= k ? k - 1 : k
|
||||||
const wantFree = Math.max(0, target - s.profPip)
|
const wantFree = Math.max(0, target - s.profPip)
|
||||||
if (wantFree - s.freePips > c.profLeft) return flash('No proficiency picks left.')
|
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>
|
<h2>Skills</h2>
|
||||||
<p className="muted">
|
<p className="muted">
|
||||||
At most half the attribute per skill. Advanced skills must be unlocked first for {c.unlockCost} points from the pool
|
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>
|
</p>
|
||||||
<div className="pools">
|
<div className="pools">
|
||||||
<div className={`pool ${c.skillLeft < 0 ? 'over' : ''}`}>
|
<div className={`pool ${c.skillLeft < 0 ? 'over' : ''}`}>
|
||||||
@@ -275,7 +276,7 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
|||||||
</div>
|
</div>
|
||||||
<p className="small muted">
|
<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 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>
|
</p>
|
||||||
{rules.skill_groups.map((g) => (
|
{rules.skill_groups.map((g) => (
|
||||||
<div key={g.id}>
|
<div key={g.id}>
|
||||||
@@ -295,8 +296,8 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
|||||||
<span className="small muted">Training</span>
|
<span className="small muted">Training</span>
|
||||||
{[1, 2].map((k) => (
|
{[1, 2].map((k) => (
|
||||||
<button key={k} className={`pip ${s.pips >= k ? 'on' : ''} ${s.profPip >= k ? 'from-prof' : ''}`}
|
<button key={k} className={`pip ${s.pips >= k ? 'on' : ''} ${s.profPip >= k ? 'from-prof' : ''}`}
|
||||||
disabled={s.profPip >= k} onClick={() => clickPip(s, k)}
|
disabled={s.profPip >= k || !s.usable} onClick={() => clickPip(s, k)}
|
||||||
title={s.profPip >= k ? 'From your profession' : k === 2 ? 'Expertise (uses 2 picks in total)' : 'Proficiency'}
|
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>
|
aria-label={`${s.name} proficiency ${k}`}>{s.pips >= k ? '◆' : '◇'}</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -309,8 +310,8 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
|||||||
) : (
|
) : (
|
||||||
<div className="stepper">
|
<div className="stepper">
|
||||||
{s.type === 'advanced' && !s.free && (
|
{s.type === 'advanced' && !s.free && (
|
||||||
<button className="ghost small-btn" title={`Lock again and get ${c.unlockCost} points back`}
|
<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 } }))}>Lock</button>
|
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 onClick={() => setPts(s.points - 1)} aria-label={`${s.name} minus`}>−</button>
|
||||||
<span className="val">{s.points}</span>
|
<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
|
// 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)
|
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)
|
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] }
|
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) {
|
export function pruneLocked(ch, rules) {
|
||||||
const points = { ...ch.skill_points }
|
const points = { ...ch.skill_points }
|
||||||
|
const picks = { ...ch.proficiency_picks }
|
||||||
rules.skills.forEach((s) => {
|
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) {
|
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 best = Math.max(...s.attrs.map((a) => attrs[a])) // with several attributes the higher one counts
|
||||||
const unlocked = ch.unlocked.includes(s.id)
|
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, total: best + points + prof,
|
||||||
usable: s.type === 'basic' || unlocked || prof > 0, // advanced skills must be unlocked
|
usable: s.type === 'basic' || unlocked || profPip > 0, // advanced skills must be unlocked (a profession proficiency does it for free)
|
||||||
free: s.type === 'advanced' && prof > 0, // unlocked by the profession, costs nothing
|
free: s.type === 'advanced' && profPip > 0, // unlocked by the profession, costs nothing
|
||||||
max: Math.floor(best / cr.skill_max_divisor) }
|
max: Math.floor(best / cr.skill_max_divisor) }
|
||||||
})
|
})
|
||||||
|
|
||||||
const profSpent = skills.reduce((s, x) => s + x.freePips, 0)
|
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 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 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 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 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))
|
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}`,
|
`Most points you can put in: best attribute ÷ ${cr.skill_max_divisor}, rounded down = ${s.max}`,
|
||||||
]
|
]
|
||||||
if (s.type === 'advanced') {
|
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).`
|
: 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.')
|
if (s.pips === 1 && s.profPip) lines.push('Proficient through your profession.')
|
||||||
return lines
|
return lines
|
||||||
|
|||||||
Reference in New Issue
Block a user