From 9ce82f7c677fc19a48b1e986096fddf76c429ec8 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Sat, 9 May 2026 22:20:24 -0700 Subject: [PATCH] =?UTF-8?q?Combat:=20round=20half-up=20on=20HP=20wound=20p?= =?UTF-8?q?ersistence=20to=20fix=20101=E2=86=92100=20drift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wound carry-over went through three integer truncations across the legacy combat scale (123 max) and the dndChar scale (78 max). 101/123 persisted as int(64.04)=64, then restored as int(100.92)=100 — losing 0.92 HP every fight. Switching both sides to math.Round means most HPs round-trip exactly; the residual error is bounded by ±1 HP from the inherent scale mismatch (1 dndChar HP ≈ 1.58 legacy HP, so some pairs collide on the smaller scale). Acceptable; the prior always-truncate behavior was directionally biased against the player. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/plugin/dnd_combat.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/plugin/dnd_combat.go b/internal/plugin/dnd_combat.go index 46ee85b..bf86d71 100644 --- a/internal/plugin/dnd_combat.go +++ b/internal/plugin/dnd_combat.go @@ -2,6 +2,7 @@ package plugin import ( "log/slog" + "math" "time" "gogobee/internal/db" @@ -429,7 +430,11 @@ func applyDnDHPScaling(stats *CombatStats, c *DnDCharacter) { if pct < dndWoundFloor { pct = dndWoundFloor } - scaled := int(float64(stats.MaxHP) * pct) + // Round half-up. Naive int truncation loses up to 1 HP on every + // round-trip through the dndChar scale (which is coarser than the + // legacy combat scale): 101/123 → persist as 64/78 → restore as + // int(100.92) = 100, silently dropping a HP between fights. + scaled := int(math.Round(float64(stats.MaxHP) * pct)) if scaled < 1 { scaled = 1 } @@ -470,7 +475,10 @@ func persistDnDHPAfterCombat(userID id.UserID, legacyStartHP, legacyEndHP int) { pct = 1 } - newHP := int(float64(c.HPMax) * pct) + // Round half-up to mirror applyDnDHPScaling — a fight that ends at + // 101/123 should round-trip cleanly through the dndChar (78-scale) + // store and come back as 101, not 100. + newHP := int(math.Round(float64(c.HPMax) * pct)) if newHP < 0 { newHP = 0 }