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

+10 -3
View File
@@ -618,7 +618,13 @@ static func encode_characters(chars: Array[Character], selected: String) -> Pack
b.put_u8(clampi(c.level, 1, 255))
b.put_u32(maxi(c.total_xp, 0))
b.put_u8(clampi(roundi(c.xp_progress() * 255.0), 0, 255))
b.put_u16(clampi(c.max_hp(), 1, 65535))
var ceiling := c.max_hp()
b.put_u16(clampi(ceiling, 1, 65535))
# Current health as well as the ceiling. Health survives a run now, so a
# character on the roster can be badly hurt, and a screen that showed
# only the maximum would advertise a wounded character as whole -- which
# under permadeath is the worst possible thing to be wrong about.
b.put_u16(clampi(ceiling if c.hp == Character.HP_FULL else c.hp, 1, 65535))
b.put_u8(1 if c.active else 0)
b.put_u32(c.colour.to_rgba32())
for i in SimConfig.INVENTORY_SLOTS:
@@ -657,8 +663,8 @@ static func decode_characters(data: PackedByteArray) -> Dictionary:
return {"selected": selected, "characters": out}
var count := b.get_u8()
# Bytes each entry needs after its two strings: level, xp, progress,
# max_hp, active, colour, then one byte per inventory slot.
var fixed := 1 + 4 + 1 + 2 + 1 + 4 + SimConfig.INVENTORY_SLOTS
# max_hp, hp, active, colour, then one byte per inventory slot.
var fixed := 1 + 4 + 1 + 2 + 2 + 1 + 4 + SimConfig.INVENTORY_SLOTS
for _i in count:
var id := _safe_utf8(b)
var display := _safe_utf8(b)
@@ -671,6 +677,7 @@ static func decode_characters(data: PackedByteArray) -> Dictionary:
"xp": b.get_u32(),
"progress": float(b.get_u8()) / 255.0,
"max_hp": b.get_u16(),
"hp": b.get_u16(),
"active": b.get_u8() == 1,
"colour": Color.hex(b.get_u32()),
"inventory": _read_inventory(b),