From e169885d909c067f9e636e30b161788a256fef15 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Wed, 15 Jul 2026 04:02:37 -0500 Subject: [PATCH] feat(m4c): ShellState.for_character derives vitals+location from the real character --- client/scripts/ui/shell/shell_state.gd | 16 ++++++++++ client/tests/unit/test_shell_state.gd | 41 ++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/client/scripts/ui/shell/shell_state.gd b/client/scripts/ui/shell/shell_state.gd index 89b1e7c..c504c2e 100644 --- a/client/scripts/ui/shell/shell_state.gd +++ b/client/scripts/ui/shell/shell_state.gd @@ -34,3 +34,19 @@ static func seed() -> ShellState: s.location_label = "THE LOWER WARD" s.dock_open = true return s + + +static func for_character(state: GameState, log: CanonLog) -> ShellState: + ## The real HUD state for a constructed character. Vitals + location come from + ## the character (§2 — code owns them); turn_order/consumables/round_label are + ## still seed() placeholders because combat (M5) and inventory (M7) own those. + var s := ShellState.seed() # start from the placeholders, then overwrite what is real + s.vitals = { + "hp": state.sheet.hp, + "hp_max": state.sheet.max_hp(), + "mp": state.sheet.mp, + "mp_max": state.sheet.max_mp(), + "purse_copper": state.purse_copper, + } + s.location_label = log.location.name + return s diff --git a/client/tests/unit/test_shell_state.gd b/client/tests/unit/test_shell_state.gd index 2319320..21a6257 100644 --- a/client/tests/unit/test_shell_state.gd +++ b/client/tests/unit/test_shell_state.gd @@ -1,5 +1,46 @@ extends "res://addons/gut/test.gd" +const NewGame = preload("res://scripts/newgame/new_game.gd") +const ContentDB = preload("res://scripts/content/content_db.gd") + + +func _built(): + var world = ContentDB.new() + world.load_from(ContentDB.default_content_root()) + var origin = ContentDB.load_json(ContentDB.origin_path("deserter")) + var creation := { + "name": "Aldric", "race_id": "human", "calling_id": "sellsword", + "seed": 8675309, "spend": {}, + "skills": ["athletics", "endurance"], "bonus_skill": "perception", + } + return NewGame.construct(origin, world, creation) + + +func test_for_character_derives_vitals_and_location_from_the_real_character(): + var res = _built() + assert_true(res["ok"], str(res["errors"])) + var state = res["state"] + var log = res["log"] + var s := ShellState.for_character(state, log) + assert_eq(s.vitals["hp"], state.sheet.hp, "hp from the sheet") + assert_eq(s.vitals["hp_max"], state.sheet.max_hp(), "hp_max from the sheet") + assert_eq(s.vitals["mp"], state.sheet.mp, "mp from the sheet") + assert_eq(s.vitals["mp_max"], state.sheet.max_mp(), "mp_max from the sheet") + assert_eq(s.vitals["purse_copper"], state.purse_copper, "purse from origin grants (47c)") + assert_eq(s.location_label, log.location.name, "location from the canon log") + assert_ne(s.vitals["hp_max"], 60, "NOT the seed's hardcoded 60 — this is a real L1 sheet") + + +func test_for_character_keeps_the_seed_placeholders_for_combat_and_inventory(): + # turn_order/consumables/round_label have no real source yet (M5/M7 own them); + # for_character must reuse seed()'s placeholders, not invent its own. + var res = _built() + var s := ShellState.for_character(res["state"], res["log"]) + var seed := ShellState.seed() + assert_eq(s.turn_order.size(), seed.turn_order.size(), "turn rail is still the placeholder") + assert_eq(s.consumables, seed.consumables, "consumables are still the placeholder") + assert_eq(s.round_label, seed.round_label, "round label is still the placeholder") + func test_turn_entry_holds_fields(): var e := TurnEntry.new("DW", 12, &"ally")