From 8d963e803081ff9d421ffa18ccbf5f7ff9bb7ba4 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Wed, 15 Jul 2026 04:06:44 -0500 Subject: [PATCH] feat(m4c): shell consumes an injected character (seed stays the F6 fallback) --- client/scripts/ui/shell/main_window_shell.gd | 8 +++-- client/tests/unit/test_main_window_shell.gd | 35 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/client/scripts/ui/shell/main_window_shell.gd b/client/scripts/ui/shell/main_window_shell.gd index a5d314b..fdf1e39 100644 --- a/client/scripts/ui/shell/main_window_shell.gd +++ b/client/scripts/ui/shell/main_window_shell.gd @@ -19,6 +19,8 @@ extends Control const HttpTransport = preload("res://scripts/net/http_transport.gd") var service: DmService = null # injection seam; real HttpTransport built if null +var injected_log: CanonLog = null # injection seam; _build_seed_log() if null (F6/standalone) +var injected_state: GameState = null # injection seam; ShellState.seed() if null var _state: ShellState var _log: CanonLog @@ -40,8 +42,10 @@ func _ready() -> void: _fit_to_viewport() get_viewport().size_changed.connect(_fit_to_viewport) - _state = ShellState.seed() - _log = _build_seed_log() + # The injected character wins; the seed calls are the F6/standalone fallback — + # same shape as `service == null` building a real transport below. + _state = ShellState.for_character(injected_state, injected_log) if injected_state != null else ShellState.seed() + _log = injected_log if injected_log != null else _build_seed_log() _bind_state() if service == null: diff --git a/client/tests/unit/test_main_window_shell.gd b/client/tests/unit/test_main_window_shell.gd index 13527d6..7615487 100644 --- a/client/tests/unit/test_main_window_shell.gd +++ b/client/tests/unit/test_main_window_shell.gd @@ -3,6 +3,8 @@ extends "res://addons/gut/test.gd" const SCENE := "res://scenes/shell/MainWindowShell.tscn" const FakeTransport = preload("res://tests/doubles/fake_transport.gd") const DmResponse = preload("res://scripts/net/dm_response.gd") +const NewGame = preload("res://scripts/newgame/new_game.gd") +const ContentDB = preload("res://scripts/content/content_db.gd") func _shell() -> MainWindowShell: @@ -47,3 +49,36 @@ func test_shell_seed_player_has_a_real_race_and_calling(): assert_true(Races.exists(node._log.player.race_id), "seed race must be a real race") assert_true(Callings.exists(node._log.player.calling_id), "seed calling must be a real calling") assert_ne(node._log.player.luck_descriptor, "", "the descriptor must not be lost to an arg-slot shift") + + +func _built_character(): + 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 _shell_with_character(res) -> MainWindowShell: + var node = load(SCENE).instantiate() + var transport = FakeTransport.new() + transport.set_response(DmResponse.ok(200, {"prose": "Rain on the cobbles."})) + node.service = DmService.new(transport, FallbackLibrary.new()) + node.injected_log = res["log"] + node.injected_state = res["state"] + add_child_autofree(node) + return node + + +func test_shell_shows_the_injected_character_not_the_seed(): + var res = _built_character() + assert_true(res["ok"], str(res["errors"])) + var node := _shell_with_character(res) + assert_same(node._log, res["log"], "the injected canon log drives narration") + var expected := "HP %d / %d" % [res["state"].sheet.hp, res["state"].sheet.max_hp()] + assert_eq(node._hp.text, expected, "HP label derives from the real sheet") + assert_ne(node._hp.text, "HP 42 / 60", "NOT the seed's Vexcca vitals")