diff --git a/client/src/components/GearPanel.tsx b/client/src/components/GearPanel.tsx index f967d23..0df7209 100644 --- a/client/src/components/GearPanel.tsx +++ b/client/src/components/GearPanel.tsx @@ -1,4 +1,5 @@ import type { Character, GameItem } from "../types"; +import { getTalentGearSlotsBonus } from "../utils/talent-effects"; import GearList from "./GearList"; import styles from "./GearPanel.module.css"; @@ -27,6 +28,8 @@ export default function GearPanel({ onCurrencyChange, }: GearPanelProps) { const slotsUsed = character.gear.reduce((sum, g) => sum + g.slot_count, 0); + const slotsMax = + character.gear_slots_max + getTalentGearSlotsBonus(character); return (
@@ -36,7 +39,7 @@ export default function GearPanel({ sp={character.sp} cp={character.cp} slotsUsed={slotsUsed} - slotsMax={character.gear_slots_max} + slotsMax={slotsMax} onAddFromItem={(item) => onAddGearFromItem(character.id, item)} onAddCustom={(data) => onAddGearCustom(character.id, data)} onRemove={(gearId) => onRemoveGear(character.id, gearId)} diff --git a/client/src/components/StatBlock.module.css b/client/src/components/StatBlock.module.css index dd8d76f..2543972 100644 --- a/client/src/components/StatBlock.module.css +++ b/client/src/components/StatBlock.module.css @@ -47,6 +47,11 @@ margin-top: 0.15rem; } +.bonus { + color: #4caf50; + font-size: 0.65rem; +} + .btn { width: 22px; height: 22px; diff --git a/client/src/components/StatBlock.tsx b/client/src/components/StatBlock.tsx index 1e265e6..c96b3fa 100644 --- a/client/src/components/StatBlock.tsx +++ b/client/src/components/StatBlock.tsx @@ -7,6 +7,7 @@ const STAT_ORDER = ["STR", "DEX", "CON", "INT", "WIS", "CHA"]; interface StatBlockProps { stats: Stat[]; + statBonuses?: Map; onStatChange: (statName: string, newValue: number) => void; mode?: "view" | "edit"; campaignId?: number; @@ -17,6 +18,7 @@ interface StatBlockProps { export default function StatBlock({ stats, + statBonuses, onStatChange, mode = "view", campaignId, @@ -29,7 +31,9 @@ export default function StatBlock({ return (
{STAT_ORDER.map((name) => { - const value = statMap.get(name) ?? 10; + const baseValue = statMap.get(name) ?? 10; + const bonus = statBonuses?.get(name) ?? 0; + const value = baseValue + bonus; const mod = getModifier(value); return (
@@ -38,7 +42,7 @@ export default function StatBlock({ {mode === "edit" && ( @@ -58,13 +62,16 @@ export default function StatBlock({ {mode === "edit" && ( )}
- {value} + + {value} + {bonus > 0 && (+{bonus})} +
); })} diff --git a/client/src/components/StatsPanel.tsx b/client/src/components/StatsPanel.tsx index 97a43e2..c6b3cd1 100644 --- a/client/src/components/StatsPanel.tsx +++ b/client/src/components/StatsPanel.tsx @@ -4,6 +4,8 @@ import StatBlock from "./StatBlock"; import AttackBlock from "./AttackBlock"; import styles from "./StatsPanel.module.css"; +const STATS = ["STR", "DEX", "CON", "INT", "WIS", "CHA"]; + interface StatsPanelProps { character: Character; mode: "view" | "edit"; @@ -21,11 +23,24 @@ export default function StatsPanel({ }: StatsPanelProps) { const attacks = generateAttacks(character); + // Compute talent stat bonuses + const statBonuses = new Map(); + for (const stat of STATS) { + let bonus = 0; + for (const talent of character.talents) { + if (talent.effect.stat_bonus === stat) { + bonus += (talent.effect.stat_bonus_value as number) || 0; + } + } + if (bonus > 0) statBonuses.set(stat, bonus); + } + return (
Ability Scores
0 ? statBonuses : undefined} onStatChange={(statName, value) => onStatChange(character.id, statName, value) } diff --git a/client/src/utils/derived-ac.ts b/client/src/utils/derived-ac.ts index 820ed3d..c8f31ba 100644 --- a/client/src/utils/derived-ac.ts +++ b/client/src/utils/derived-ac.ts @@ -1,5 +1,6 @@ import type { Character } from "../types"; import { getModifier } from "./modifiers"; +import { getEffectiveStat, getTalentAcBonus } from "./talent-effects"; export interface AcBreakdown { calculated: number; @@ -9,9 +10,7 @@ export interface AcBreakdown { } export function calculateAC(character: Character): AcBreakdown { - const dexMod = getModifier( - character.stats.find((s) => s.stat_name === "DEX")?.value ?? 10, - ); + const dexMod = getModifier(getEffectiveStat(character, "DEX")); let base = 10 + dexMod; let source = "Unarmored"; @@ -36,6 +35,13 @@ export function calculateAC(character: Character): AcBreakdown { source += " + " + shield.name; } + // Talent AC bonuses (e.g. Shield of Faith) + const talentAcBonus = getTalentAcBonus(character); + if (talentAcBonus > 0) { + base += talentAcBonus; + source += " + talents"; + } + const override = (character.overrides?.ac as number | undefined) ?? null; return { diff --git a/client/src/utils/derived-attacks.ts b/client/src/utils/derived-attacks.ts index cbd23cd..8109307 100644 --- a/client/src/utils/derived-attacks.ts +++ b/client/src/utils/derived-attacks.ts @@ -1,13 +1,16 @@ import type { Character, AttackLine } from "../types"; import { getModifier, formatModifier } from "./modifiers"; +import { + getEffectiveStat, + getTalentAttackBonus, + getTalentDamageBonus, +} from "./talent-effects"; export function generateAttacks(character: Character): AttackLine[] { - const strMod = getModifier( - character.stats.find((s) => s.stat_name === "STR")?.value ?? 10, - ); - const dexMod = getModifier( - character.stats.find((s) => s.stat_name === "DEX")?.value ?? 10, - ); + const strMod = getModifier(getEffectiveStat(character, "STR")); + const dexMod = getModifier(getEffectiveStat(character, "DEX")); + const talentAttackBonus = getTalentAttackBonus(character); + const talentDamageBonus = getTalentDamageBonus(character); const attacks: AttackLine[] = []; @@ -15,7 +18,7 @@ export function generateAttacks(character: Character): AttackLine[] { if (gear.type !== "weapon") continue; const effects = gear.effects; - const damage = (effects.damage as string) || "1d4"; + let damage = (effects.damage as string) || "1d4"; const tags: string[] = []; let mod: number; @@ -28,8 +31,14 @@ export function generateAttacks(character: Character): AttackLine[] { mod = strMod; } - const bonus = (effects.bonus as number) || 0; - mod += bonus; + // Gear-specific bonus + const gearBonus = (effects.bonus as number) || 0; + mod += gearBonus + talentAttackBonus; + + // Talent damage bonus — append to damage expression + if (talentDamageBonus > 0) { + damage = `${damage}+${talentDamageBonus}`; + } if (effects.two_handed) tags.push("2H"); if (effects.thrown) tags.push("T"); diff --git a/client/src/utils/talent-effects.ts b/client/src/utils/talent-effects.ts new file mode 100644 index 0000000..9d7453c --- /dev/null +++ b/client/src/utils/talent-effects.ts @@ -0,0 +1,86 @@ +import type { Character } from "../types"; + +/** + * Get the effective stat value after talent bonuses are applied. + * e.g. if character has base STR 14 and a talent with { stat_bonus: 'STR', stat_bonus_value: 2 }, + * this returns 16. + */ +export function getEffectiveStat( + character: Character, + statName: string, +): number { + const base = + character.stats.find((s) => s.stat_name === statName)?.value ?? 10; + let bonus = 0; + for (const talent of character.talents) { + if (talent.effect.stat_bonus === statName) { + bonus += (talent.effect.stat_bonus_value as number) || 0; + } + } + return base + bonus; +} + +/** + * Get total AC bonus from talents (e.g. Shield of Faith +1 AC). + */ +export function getTalentAcBonus(character: Character): number { + let bonus = 0; + for (const talent of character.talents) { + if (typeof talent.effect.ac_bonus === "number") { + bonus += talent.effect.ac_bonus; + } + } + return bonus; +} + +/** + * Get total attack bonus from talents (e.g. Weapon Mastery +1). + */ +export function getTalentAttackBonus(character: Character): number { + let bonus = 0; + for (const talent of character.talents) { + if (typeof talent.effect.attack_bonus === "number") { + bonus += talent.effect.attack_bonus; + } + } + return bonus; +} + +/** + * Get total damage bonus from talents (e.g. Weapon Mastery +1). + */ +export function getTalentDamageBonus(character: Character): number { + let bonus = 0; + for (const talent of character.talents) { + if (typeof talent.effect.damage_bonus === "number") { + bonus += talent.effect.damage_bonus; + } + } + return bonus; +} + +/** + * Get extra gear slots from talents (e.g. Hauler +2). + */ +export function getTalentGearSlotsBonus(character: Character): number { + let bonus = 0; + for (const talent of character.talents) { + if (typeof talent.effect.gear_slots_bonus === "number") { + bonus += talent.effect.gear_slots_bonus; + } + } + return bonus; +} + +/** + * Get HP bonus from talents (e.g. Grit +2). + */ +export function getTalentHpBonus(character: Character): number { + let bonus = 0; + for (const talent of character.talents) { + if (typeof talent.effect.hp_bonus === "number") { + bonus += talent.effect.hp_bonus; + } + } + return bonus; +}