Tell the roster the truth about a wounded character
ci / verify (push) Successful in 52s

Health survives a run now, so a character sitting on the select screen
can be nearly dead -- and the screen was still printing only the ceiling.
"level 11, 2000 hp" for a character on 160 is a lie told at the exact
moment it matters most, because the next thing that happens is the player
walks it into a dungeon under permadeath.

The roster message carries current hit points alongside the maximum, and
the screen shows a fraction only when it means something: a healthy
character still reads "1300 hp" rather than "1300/1300". The field is
inserted mid-record, which shifts every field after it, so the protocol
version goes to 10 -- a mismatched client would read the active flag out
of the middle of a health value.

tools/screenshot.gd had no capture for this screen, which is how a UI
change to it could have shipped unlooked-at. It now shows a made-up
roster of three characters -- whole, badly hurt, nearly dead -- for the
same reason the upgrade screen is fed a made-up offer: the point is the
layout, not a dungeon run. Getting there needs the player walked off the
quartermaster first, or the game scene re-opens the upgrade screen over
the top of it every frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5 committed 2026-09-07 21:54:40 +02:00
1 parent 4aeb6d229d
commit a6084c1a8d
5 files changed
+110 -7

No files matched your search

+27
View File
@@ -651,3 +651,30 @@ func test_an_unknown_event_type_stops_the_stream_rather_than_desyncing_it() -> v
assert_eq(got.size(), 1,
"an event whose length is unknown unaligns everything after it, " +
"so the only safe answer is to stop")
func test_the_roster_carries_current_health_as_well_as_the_ceiling() -> void:
# Health survives a run now, so a character on the select screen can be
# badly hurt. Sending only the maximum would advertise it as whole, which
# under permadeath is the worst thing on that screen to be wrong about.
var rng := RandomNumberGenerator.new()
var whole := Character.create("Whole", rng)
var hurt := Character.create("Hurt", rng)
hurt.hp = 42
var got: Array = NetCodec.decode_characters(
NetCodec.encode_characters([whole, hurt] as Array[Character], ""))["characters"]
assert_eq(got.size(), 2)
assert_eq(int(got[0]["hp"]), int(got[0]["max_hp"]),
"a character that has never been hurt reads as full")
assert_eq(int(got[1]["hp"]), 42)
assert_gt(int(got[1]["max_hp"]), 42, "and the ceiling is still its own number")
func test_roster_health_is_never_sent_as_zero() -> void:
# A zero on this screen reads as a dead character the player is being
# offered, which cannot happen: the roster is living characters only.
var c := Character.create("Edge", RandomNumberGenerator.new())
c.hp = 0
var got: Array = NetCodec.decode_characters(
NetCodec.encode_characters([c] as Array[Character], ""))["characters"]
assert_gte(int(got[0]["hp"]), 1)