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; }