diff --git a/src/features/characters/builder/CreationWizard.tsx b/src/features/characters/builder/CreationWizard.tsx
index bd7a566..4add558 100644
--- a/src/features/characters/builder/CreationWizard.tsx
+++ b/src/features/characters/builder/CreationWizard.tsx
@@ -18,7 +18,7 @@ import { getClassTip, raceSynergyNote } from '@/lib/assistant/builder';
import { briefOverview, dedupeByName } from './overview';
import { parseAsiBonuses, makeSkillResolver, parseBackgroundSkills, parseTraitSkills } from './origin';
import { PF2E_SUBCLASSES } from '@/lib/rules/pf2e/progression';
-import { DND5E_SUBCLASSES } from '@/lib/rules/dnd5e/progression';
+import { DND5E_SUBCLASSES, dnd5eAsiLevels } from '@/lib/rules/dnd5e/progression';
import { Modal } from '@/components/ui/Modal';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Codex';
@@ -233,16 +233,24 @@ export function CreationWizard({ campaign, onClose }: { campaign?: Campaign | un
return pf2eApplyBoosts([...boostSlots.fixed, ...chosen], boostSlots.flaw);
}, [boostSlots, boostPicks]);
+ // 5e: ability-score improvements gained by the input level for this class (4/8/12/16/19, +class extras).
+ const asiCount = system === '5e' && selectedClass ? dnd5eAsiLevels(selectedClass.name).filter((l) => l <= level).length : 0;
+ const [asiAlloc, setAsiAlloc] = useState>>({});
+ useEffect(() => { setAsiAlloc({}); }, [classSlug, level, system]); // reset when class/level changes
+ const asiUsed = ABILITIES.reduce((n, a) => n + (asiAlloc[a] ?? 0), 0);
+ const asiRemaining = asiCount * 2 - asiUsed;
+
const abilities = useMemo(() => {
if (system === 'pf2e') return pf2eAbilities ?? { str: 10, dex: 10, con: 10, int: 10, wis: 10, cha: 10 };
const out = {} as AbilityScores;
ABILITIES.forEach((a, i) => { out[a] = usesPool ? (pool[assignment[i]!] ?? 10) : pb[i]!; });
- // 5e: fold the chosen race's ability score increases into the final scores.
+ // 5e: fold the chosen race's ability score increases + any level-up ASIs into the final scores.
if (selectedOrigin?.asiBonuses) {
for (const a of ABILITIES) out[a] += selectedOrigin.asiBonuses[a] ?? 0;
}
+ for (const a of ABILITIES) out[a] += asiAlloc[a] ?? 0;
return out;
- }, [system, pf2eAbilities, usesPool, pool, assignment, pb, selectedOrigin]);
+ }, [system, pf2eAbilities, usesPool, pool, assignment, pb, selectedOrigin, asiAlloc]);
const poolValid = !usesPool || new Set(assignment).size === ABILITIES.length;
const pbValid = method !== 'pointbuy' || pointBuyRemaining(pb) >= 0;
@@ -339,7 +347,7 @@ export function CreationWizard({ campaign, onClose }: { campaign?: Campaign | un
const stepValid: Record = {
Class: !!selectedClass,
Origin: true,
- Abilities: system === 'pf2e' ? true : poolValid && pbValid,
+ Abilities: system === 'pf2e' ? true : poolValid && pbValid && asiRemaining >= 0,
Skills: skills.length === Math.min(skillCount, skillOptions.length),
Spells: true,
Details: true,
@@ -562,6 +570,30 @@ export function CreationWizard({ campaign, onClose }: { campaign?: Campaign | un
{selectedOrigin.name} racial bonus ({ABILITIES.filter((a) => selectedOrigin.asiBonuses?.[a]).map((a) => `+${selectedOrigin.asiBonuses![a]} ${ABILITY_ABBR[a]}`).join(', ')}) is added to your final scores.
)}
+ {asiCount > 0 && (
+
+
+ Ability Score Improvements at level {level} ({asiCount} ASI{asiCount > 1 ? 's' : ''})
+ {asiRemaining} / {asiCount * 2} pts left
+
+
+ {ABILITIES.map((a) => {
+ const v = asiAlloc[a] ?? 0;
+ const base = (usesPool ? (pool[assignment[ABILITIES.indexOf(a)]!] ?? 10) : pb[ABILITIES.indexOf(a)]!) + (selectedOrigin?.asiBonuses?.[a] ?? 0);
+ return (
+
+ {ABILITY_ABBR[a]} {v > 0 ? +{v} : null}
+
+
+
+
+
+ );
+ })}
+
+
Assign your improvements (+1 each, max 20 per ability). You can take feats instead later on the sheet.
+
+ )}
{selectedClass && (() => {
const tip = getClassTip(system, selectedClass.slug);
const text = tip?.abilityTip ?? (selectedClass.keyAbilities.length > 0
@@ -578,8 +610,9 @@ export function CreationWizard({ campaign, onClose }: { campaign?: Campaign | un
{ABILITIES.map((a, i) => {
const racial = (system === '5e' && selectedOrigin?.asiBonuses?.[a]) || 0;
+ const asi = (system === '5e' && asiAlloc[a]) || 0;
const base = usesPool ? (pool[assignment[i]!] ?? 0) : pb[i]!;
- const value = base + racial;
+ const value = base + racial + asi;
const isKey = selectedClass?.keyAbilities.includes(a);
const fMin = method === 'pointbuy' ? POINT_BUY_MIN : 1;
const fMax = method === 'pointbuy' ? POINT_BUY_MAX : 30;
@@ -606,7 +639,7 @@ export function CreationWizard({ campaign, onClose }: { campaign?: Campaign | un
{value}
{formatModifier(abilityModifier(value))}
- {racial ?
{base} base · +{racial} race
: null}
+ {(racial || asi) ?
{base} base{racial ? ` · +${racial} race` : ''}{asi ? ` · +${asi} ASI` : ''}
: null}
);