Wire up talent HP bonus with per-level scaling (e.g. Grit +2 HP and +1/level)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aaron Wood 2026-04-09 14:19:18 -04:00
parent 0bd83e5f92
commit f552a475c1
7 changed files with 42 additions and 11 deletions

View file

@ -3,6 +3,7 @@ import HpBar from "./HpBar";
import StatBlock from "./StatBlock"; import StatBlock from "./StatBlock";
import styles from "./CharacterCard.module.css"; import styles from "./CharacterCard.module.css";
import { calculateAC } from "../utils/derived-ac"; import { calculateAC } from "../utils/derived-ac";
import { getTalentHpBonus } from "../utils/talent-effects";
interface CharacterCardProps { interface CharacterCardProps {
character: Character; character: Character;
@ -38,7 +39,7 @@ export default function CharacterCard({
<div className={styles.hpAcRow} onClick={(e) => e.stopPropagation()}> <div className={styles.hpAcRow} onClick={(e) => e.stopPropagation()}>
<HpBar <HpBar
current={character.hp_current} current={character.hp_current}
max={character.hp_max} max={character.hp_max + getTalentHpBonus(character)}
onChange={(hp) => onHpChange(character.id, hp)} onChange={(hp) => onHpChange(character.id, hp)}
/> />
<div className={styles.ac}> <div className={styles.ac}>

View file

@ -76,6 +76,12 @@
font-weight: 600; font-weight: 600;
} }
.hpBonus {
color: #4caf50;
font-size: 0.65rem;
margin-left: 0.15rem;
}
.xpThreshold { .xpThreshold {
font-size: 0.75rem; font-size: 0.75rem;
color: #666; color: #666;

View file

@ -1,6 +1,7 @@
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import type { Character, GameItem } from "../types"; import type { Character, GameItem } from "../types";
import { calculateAC } from "../utils/derived-ac"; import { calculateAC } from "../utils/derived-ac";
import { getTalentHpBonus } from "../utils/talent-effects";
import AcDisplay from "./AcDisplay"; import AcDisplay from "./AcDisplay";
import InlineNumber from "./InlineNumber"; import InlineNumber from "./InlineNumber";
import StatsPanel from "./StatsPanel"; import StatsPanel from "./StatsPanel";
@ -51,6 +52,8 @@ export default function CharacterSheet({
const [confirmDelete, setConfirmDelete] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false);
const debounceRef = useRef<ReturnType<typeof setTimeout>>(); const debounceRef = useRef<ReturnType<typeof setTimeout>>();
const acBreakdown = calculateAC(character); const acBreakdown = calculateAC(character);
const hpBonus = getTalentHpBonus(character);
const effectiveHpMax = character.hp_max + hpBonus;
const xpThreshold = character.level * 10; const xpThreshold = character.level * 10;
useEffect(() => { useEffect(() => {
@ -122,14 +125,24 @@ export default function CharacterSheet({
/> />
<span className={styles.hpSlash}>/</span> <span className={styles.hpSlash}>/</span>
{mode === "edit" ? ( {mode === "edit" ? (
<InlineNumber <>
value={character.hp_max} <InlineNumber
onChange={(hp) => onUpdate(character.id, { hp_max: hp })} value={character.hp_max}
className={styles.hpMax} onChange={(hp) => onUpdate(character.id, { hp_max: hp })}
min={0} className={styles.hpMax}
/> min={0}
/>
{hpBonus > 0 && (
<span className={styles.hpBonus}>(+{hpBonus})</span>
)}
</>
) : ( ) : (
<span className={styles.hpMax}>{character.hp_max}</span> <span className={styles.hpMax}>
{effectiveHpMax}
{hpBonus > 0 && (
<span className={styles.hpBonus}> (+{hpBonus})</span>
)}
</span>
)} )}
</div> </div>
</div> </div>

View file

@ -73,7 +73,7 @@ export function getTalentGearSlotsBonus(character: Character): number {
} }
/** /**
* Get HP bonus from talents (e.g. Grit +2). * Get HP bonus from talents (e.g. Grit +2 and +1 per level).
*/ */
export function getTalentHpBonus(character: Character): number { export function getTalentHpBonus(character: Character): number {
let bonus = 0; let bonus = 0;
@ -81,6 +81,9 @@ export function getTalentHpBonus(character: Character): number {
if (typeof talent.effect.hp_bonus === "number") { if (typeof talent.effect.hp_bonus === "number") {
bonus += talent.effect.hp_bonus; bonus += talent.effect.hp_bonus;
} }
if (typeof talent.effect.hp_per_level === "number") {
bonus += talent.effect.hp_per_level * character.level;
}
} }
return bonus; return bonus;
} }

View file

@ -155,4 +155,12 @@ if (talentCount === 0) {
} }
} }
// --- Migration: update Grit talent to include hp_per_level ---
db.prepare(
`UPDATE game_talents SET effect = '{"hp_bonus":2,"hp_per_level":1}' WHERE name = 'Grit' AND effect = '{"hp_bonus":2}'`,
).run();
db.prepare(
`UPDATE character_talents SET effect = '{"hp_bonus":2,"hp_per_level":1}' WHERE name = 'Grit' AND effect = '{"hp_bonus":2}'`,
).run();
export default db; export default db;

View file

@ -202,7 +202,7 @@ export function seedDevData() {
brynnId, brynnId,
"Grit", "Grit",
"+2 HP and +1 HP each level", "+2 HP and +1 HP each level",
'{"hp_bonus":2}', '{"hp_bonus":2,"hp_per_level":1}',
null, null,
); );

View file

@ -23,7 +23,7 @@ export const SEED_TALENTS: SeedTalent[] = [
name: "Grit", name: "Grit",
source: "Fighter", source: "Fighter",
description: "Gain +2 HP and +1 HP each level", description: "Gain +2 HP and +1 HP each level",
effect: { hp_bonus: 2 }, effect: { hp_bonus: 2, hp_per_level: 1 },
}, },
// --- Priest --- // --- Priest ---