diff --git a/README.md b/README.md index 03e7f18..2a37d12 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/backend/app/derive.py b/backend/app/derive.py index 9faa5b4..bc347cf 100644 --- a/backend/app/derive.py +++ b/backend/app/derive.py @@ -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}) diff --git a/backend/data/rules.json b/backend/data/rules.json index 7530cf1..b8a06e5 100644 --- a/backend/data/rules.json +++ b/backend/data/rules.json @@ -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." ] }, diff --git a/frontend/src/Creator.jsx b/frontend/src/Creator.jsx index 20ba1b9..2bac4bc 100644 --- a/frontend/src/Creator.jsx +++ b/frontend/src/Creator.jsx @@ -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

Skills

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.

@@ -275,7 +276,7 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad

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.

{rules.skill_groups.map((g) => (
@@ -295,8 +296,8 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad Training {[1, 2].map((k) => ( ))}
@@ -309,8 +310,8 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad ) : (
{s.type === 'advanced' && !s.free && ( - + )} {s.points} diff --git a/frontend/src/calc.js b/frontend/src/calc.js index 7ea9377..88e8cd8 100644 --- a/frontend/src/calc.js +++ b/frontend/src/calc.js @@ -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)) diff --git a/frontend/src/explain.js b/frontend/src/explain.js index 65ae9a5..ed88497 100644 --- a/frontend/src/explain.js +++ b/frontend/src/explain.js @@ -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