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

View file

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

View file

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

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 {
let bonus = 0;
@ -81,6 +81,9 @@ export function getTalentHpBonus(character: Character): number {
if (typeof talent.effect.hp_bonus === "number") {
bonus += talent.effect.hp_bonus;
}
if (typeof talent.effect.hp_per_level === "number") {
bonus += talent.effect.hp_per_level * character.level;
}
}
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;

View file

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

View file

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