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