Files
settled-reach/client/tests/visual_scenarios.gd
T
jpmschweitzerandClaude Fable 5 799156b140 feat(client): Atlas screenshot capture matrix + set_view API (T-1120)
AtlasViewer gains public set_view(zoom, offset)/get_view_offset (clamped, review-flagged API gap) + gdUnit suite. visual_capture.gd gains an atlas_matrix scenario (one boot, tests/atlas_shots.json-driven: 7 verified bodies x zoom x overlay sets, signal-gated waits on atlas_layers_received/city_names_received with timeout fallback). 12 individually-addressable golden scenarios registered in tests/visual.json. Root-level settings.db* gitignored (capture-spawned servers write their settings store to cwd). Fixes found live: GDScript lambdas capture value-type locals BY VALUE (wait-flag moved to Array carrier); RegionalScreen.enter() no-ops when the screen id is unchanged (direct enter() per body after the first).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 20:29:14 +02:00

409 lines
14 KiB
GDScript

extends RefCounted
## Setup hooks for visual test scenarios.
## Config (tests/visual.json) controls WHAT to run; this file controls HOW to
## set up each scenario in-engine. Called by visual_capture.gd after main.tscn
## is loaded and settled.
##
## Autoload globals (FogState, SimBridge, GameState) are NOT available as
## compile-time identifiers in -s mode. All methods receive tree_root and
## look up autoloads via get_node("/root/...").
## Apply scenario-specific setup before tick advancement.
## Returns true if the scenario is recognized, false otherwise.
func apply_setup(scenario_name: String, tree_root: Node) -> bool:
var sim_bridge := tree_root.get_node("/root/SimBridge")
var fog_state := tree_root.get_node("/root/FogState")
var game_state := tree_root.get_node("/root/GameState")
match scenario_name:
"fog_3state":
# Default TestHarness state — player at (10,10), 8x8 room.
# All 3 fog states naturally visible: clear (cone), explored (out of cone), unexplored.
pass
"fog_diagonal":
# Diagonal LOS boundary — staircase regression target.
# Default room works: the diamond-shaped visibility radius creates diagonal edges.
# 10 ticks builds enough explored tiles to show the boundary.
pass
"fog_boundary":
# Explored/unexplored transition centered.
# Move player south so the northern explored boundary is center-frame.
if sim_bridge.harness:
sim_bridge.harness.player_pos = Vector2i(10, 12)
"fog_zone_tint":
# Bar zone warm amber tint (D-046).
# Inject zone_id into visible_tiles so fog_state.gd writes bar tint.
# TestHarness tiles don't include zone_id by default — patch them.
if sim_bridge.harness:
sim_bridge.harness.player_pos = Vector2i(10, 10)
# Zone tint is written from visible_tiles with zone_id field.
# We patch GameState.visible_tiles after the first snapshot in post_setup().
"fog_debug":
# Raw exploration overlay (green/blue/red).
fog_state.debug_exploration = true
"npc_in_fog":
# NPC sprite visible above fog (D-033).
# Position player so NPC at (12,9) is at the fog edge.
if sim_bridge.harness:
sim_bridge.harness.player_pos = Vector2i(10, 10)
"hud_default":
# Full HUD: time, Careful stance, minimap, monologue.
# Tick 1 triggers monologue from TestHarness.
# Inject Careful stance for visible green indicator.
game_state.player_stance = "Careful"
"dialogue_open":
# Dialogue box + interaction prompt + key bar.
# Move adjacent to NPC and interact.
if sim_bridge.harness:
sim_bridge.harness.player_pos = Vector2i(11, 9)
sim_bridge.harness.process_input("Interact")
"dialogue_with_monologue":
# Confrontation beat: dialogue + monologue overlap.
if sim_bridge.harness:
sim_bridge.harness.player_pos = Vector2i(11, 9)
sim_bridge.harness.process_input("Interact")
# Monologue will be injected in post_setup after dialogue is active.
"minimap_stance":
# Minimap + Sprint stance indicator overlap (top-right).
game_state.player_stance = "Sprint"
"cursor_menu":
# Cursor rendering over dialogue option area.
if sim_bridge.harness:
sim_bridge.harness.player_pos = Vector2i(11, 9)
sim_bridge.harness.process_input("Interact")
"fog_live_replay", "fog_theater_replay", "fog_boundary_replay":
# Replay real server snapshots via MessagePack → Protocol.decode_snapshot().
# Setup handled by visual_capture.gd (reads replay_snapshot from config).
pass
"fog_live_hub":
# Live server connection — Hub spawn position.
# No setup needed: server starts in --test-mode with Gauntlet,
# player spawns at Hub (50,58). Captures real fog pipeline output.
pass
"implant_pending":
# #960: overlay the diegetic Layer-1 "generating" indicator for
# visual sign-off. Loaded via load() — class_name isn't available
# in -s mode (per this file's header note).
var pending_script: GDScript = load("res://ui/implant/implant_pending.gd")
var theme_res: Resource = load("res://ui/implant/default_implant.tres")
var layer := CanvasLayer.new()
layer.layer = 100
tree_root.add_child(layer)
var indicator: Control = pending_script.new()
indicator.name = "VisualImplantPending"
layer.add_child(indicator)
indicator.apply_implant_theme(theme_res)
indicator.position = Vector2(370.0, 240.0) # centered at 960x540
indicator.start("GENERATING LAYER 1")
"atlas_gen_open":
# #960 live inspection: open the actual atlas app through HudGroups (the
# production opener), navigate to GJ71c's regional screen, and enable the
# generation overlays — exercises the opener + the live request → cascade
# → render pipeline against a connected server (SR_LIVE=1).
var hud_groups: Node = tree_root.get_node("/root/HudGroups")
var registry: Node = tree_root.get_node("/root/ImplantRegistry")
hud_groups.open_app("implant/map")
var app: Variant = registry.get_app_instance("implant/map")
if app == null:
push_warning("VisualScenarios: atlas app not instantiated")
return true
var body := {
"body_id": "GJ71c",
"proper_name": "Threshold",
"terrain_reference": "wiki/star-systems/GJ-71/bodies/GJ71c/heightmap.png",
}
app.nav.push(
"regional", {"body": body, "system": {"system_id": "GJ 71", "proper_name": "GJ 71"}}
)
var viewer: Variant = app._regional_screen._viewer
viewer.set_overlay_visible("gen_l1_rivers", true)
viewer.set_overlay_visible("gen_l1_basins", true)
viewer.set_overlay_visible("gen_l1_attractors", true)
# T-1120: curated Atlas screenshot capture matrix golden subset (12
# shots, one per case below). Each pins ONE body/zoom/overlay-set combo
# from tests/atlas_shots.json (the full 42-shot capture matrix — see
# visual_capture.gd's _run_atlas_matrix()) as an individually re-runnable
# tests/visual.json golden scenario, matching atlas_gen_open's pattern
# above (open -> nav.push -> configure overlays; the scenario's "ticks"
# count is the wait for generation to reach Ready, not an explicit
# poll — proven sufficient by atlas_gen_open's existing golden).
"atlas_GJ380c_zfit_C":
_setup_atlas_golden_shot(
tree_root,
{
"body_id": "GJ380c",
"proper_name": "Lendel",
"terrain_reference": "wiki/star-systems/GJ-380/bodies/GJ380c/heightmap.png",
},
{"system_id": "GJ 380", "proper_name": "Groombridge"},
"fit",
["gen_district", "gen_l3_settlements", "gen_l2_roads", "gen_l1_rivers"]
)
"atlas_GJ380c_z4_0_B":
_setup_atlas_golden_shot(
tree_root,
{
"body_id": "GJ380c",
"proper_name": "Lendel",
"terrain_reference": "wiki/star-systems/GJ-380/bodies/GJ380c/heightmap.png",
},
{"system_id": "GJ 380", "proper_name": "Groombridge"},
4.0,
["gen_l3_settlements", "gen_l2_roads"]
)
"atlas_GJ144e_zfit_A":
_setup_atlas_golden_shot(
tree_root,
{
"body_id": "GJ144e",
"proper_name": "Vethis",
"terrain_reference": "wiki/star-systems/GJ-144/bodies/GJ144e/heightmap.png",
},
{"system_id": "GJ 144", "proper_name": "Ran"},
"fit",
["gen_district"]
)
"atlas_GJ144e_z2_0_C":
_setup_atlas_golden_shot(
tree_root,
{
"body_id": "GJ144e",
"proper_name": "Vethis",
"terrain_reference": "wiki/star-systems/GJ-144/bodies/GJ144e/heightmap.png",
},
{"system_id": "GJ 144", "proper_name": "Ran"},
2.0,
["gen_district", "gen_l3_settlements", "gen_l2_roads", "gen_l1_rivers"]
)
"atlas_GJ338Bd_z2_0_C":
_setup_atlas_golden_shot(
tree_root,
{
"body_id": "GJ338Bd",
"proper_name": "Arbour",
"terrain_reference": "wiki/star-systems/GJ-338B/bodies/GJ338Bd/heightmap.png",
},
{"system_id": "GJ 338B", "proper_name": "Arbour"},
2.0,
["gen_district", "gen_l3_settlements", "gen_l2_roads", "gen_l1_rivers"]
)
"atlas_GJ338Bd_z4_0_B":
_setup_atlas_golden_shot(
tree_root,
{
"body_id": "GJ338Bd",
"proper_name": "Arbour",
"terrain_reference": "wiki/star-systems/GJ-338B/bodies/GJ338Bd/heightmap.png",
},
{"system_id": "GJ 338B", "proper_name": "Arbour"},
4.0,
["gen_l3_settlements", "gen_l2_roads"]
)
"atlas_GJ820Bc_zfit_A":
_setup_atlas_golden_shot(
tree_root,
{
"body_id": "GJ820Bc",
"proper_name": "Ferrath",
"terrain_reference": "wiki/star-systems/GJ-820B/bodies/GJ820Bc/heightmap.png",
},
{"system_id": "GJ 820B", "proper_name": "Cygni B"},
"fit",
["gen_district"]
)
"atlas_GJ820Bc_z2_0_C":
_setup_atlas_golden_shot(
tree_root,
{
"body_id": "GJ820Bc",
"proper_name": "Ferrath",
"terrain_reference": "wiki/star-systems/GJ-820B/bodies/GJ820Bc/heightmap.png",
},
{"system_id": "GJ 820B", "proper_name": "Cygni B"},
2.0,
["gen_district", "gen_l3_settlements", "gen_l2_roads", "gen_l1_rivers"]
)
"atlas_GJ251c_z2_0_B":
_setup_atlas_golden_shot(
tree_root,
{
"body_id": "GJ251c",
"proper_name": "Ruhr",
"terrain_reference": "wiki/star-systems/GJ-251/bodies/GJ251c/heightmap.png",
},
{"system_id": "GJ 251", "proper_name": "Renaissance"},
2.0,
["gen_l3_settlements", "gen_l2_roads"]
)
"atlas_GJ251c_zfit_C":
_setup_atlas_golden_shot(
tree_root,
{
"body_id": "GJ251c",
"proper_name": "Ruhr",
"terrain_reference": "wiki/star-systems/GJ-251/bodies/GJ251c/heightmap.png",
},
{"system_id": "GJ 251", "proper_name": "Renaissance"},
"fit",
["gen_district", "gen_l3_settlements", "gen_l2_roads", "gen_l1_rivers"]
)
"atlas_GJ244Ad_zfit_C":
_setup_atlas_golden_shot(
tree_root,
{
"body_id": "GJ244Ad",
"proper_name": "Edict",
"terrain_reference": "wiki/star-systems/GJ-244A/bodies/GJ244Ad/heightmap.png",
},
{"system_id": "GJ 244A", "proper_name": "Sirius"},
"fit",
["gen_district", "gen_l3_settlements", "gen_l2_roads", "gen_l1_rivers"]
)
"atlas_GJ445c-m1_z4_0_B":
_setup_atlas_golden_shot(
tree_root,
{
"body_id": "GJ445c-m1",
"proper_name": "Jinghu",
"terrain_reference": "wiki/star-systems/GJ-445/bodies/GJ445c-m1/heightmap.png",
},
{"system_id": "GJ 445", "proper_name": "Jinghu"},
4.0,
["gen_l3_settlements", "gen_l2_roads"]
)
_:
push_warning("VisualScenarios: unknown scenario '%s'" % scenario_name)
return false
return true
## Post-tick setup — called after N ticks have been advanced.
## Use for state that depends on tick processing (e.g. zone tint patching).
func post_setup(scenario_name: String, tree_root: Node) -> void:
var fog_state := tree_root.get_node("/root/FogState")
var game_state := tree_root.get_node("/root/GameState")
match scenario_name:
"fog_zone_tint":
# Patch visible_tiles with zone_id for fog_state.gd zone tint pipeline.
var patched: Array = []
for tile in game_state.visible_tiles:
if tile is Dictionary:
var t: Dictionary = tile.duplicate()
t["zone_id"] = "bar"
patched.append(t)
game_state.visible_tiles = patched
# Force fog_state to re-process with zone tint data
fog_state.update_from_state()
"dialogue_with_monologue":
# Inject monologue to create the overlap condition.
game_state.current_monologue = {
"id": "test_confrontation",
"text": "Something about that answer doesn't add up.",
"duration_seconds": 5.0,
"priority": "high",
"is_urgent": true,
}
"hud_default":
# Ensure stance is still Careful after ticks (TestHarness may override).
game_state.player_stance = "Careful"
## Apply flow action — called for each step in a movie flow.
func apply_flow_action(action: String, tree_root: Node) -> void:
var sim_bridge := tree_root.get_node("/root/SimBridge")
match action:
"wait":
pass # No-op — just captures current state
"select_option_1":
# Simulate selecting the first dialogue option
if sim_bridge.harness:
sim_bridge.harness.process_input("DialogueOption1")
_:
# Movement actions and Interact pass through to TestHarness
if sim_bridge.harness:
sim_bridge.harness.process_input(action)
## T-1120: shared setup for the 12 curated "atlas_<body>_z<zoom>_<set>" golden
## cases above — opens the
## Atlas (same production opener as atlas_gen_open), navigates to `body`, then
## applies `zoom` ("fit" or a float) and exactly the overlay ids in
## `overlay_ids`. Mirrors visual_capture.gd's _run_atlas_matrix() zoom/overlay
## logic (kept independent rather than shared code — visual_capture.gd extends
## SceneTree and this extends RefCounted, different -s mode load contexts, and
## the duplication is small and stable: both read the same public AtlasViewer
## surface added for this ticket, set_view()/get_heightmap_texture()/get_rect().
func _setup_atlas_golden_shot(
tree_root: Node, body: Dictionary, system: Dictionary, zoom: Variant, overlay_ids: Array
) -> void:
var hud_groups: Node = tree_root.get_node("/root/HudGroups")
var registry: Node = tree_root.get_node("/root/ImplantRegistry")
hud_groups.open_app("implant/map")
var app: Variant = registry.get_app_instance("implant/map")
if app == null:
push_warning("VisualScenarios: atlas app not instantiated")
return
app.nav.push("regional", {"body": body, "system": system})
var viewer: Variant = app._regional_screen._viewer
if viewer == null:
push_warning("VisualScenarios: regional screen has no viewer")
return
for def: Dictionary in viewer.get_overlay_defs():
var overlay_id: String = str(def.get("id", ""))
if str(def.get("group", "")) == "toggle":
viewer.set_overlay_visible(overlay_id, overlay_ids.has(overlay_id))
for overlay_id: String in overlay_ids:
viewer.set_overlay_visible(overlay_id, true)
if zoom is String and zoom == "fit":
return # show_body() (called by nav.push above) already fit-zoomed.
var tex: Texture2D = viewer.get_heightmap_texture()
if tex == null:
return
var tex_w: float = float(tex.get_width())
var tex_h: float = float(tex.get_height())
var sz: Vector2 = viewer.get_rect().size
if sz == Vector2.ZERO:
sz = Vector2(1280.0, 720.0)
var zoom_f: float = float(zoom)
var scaled: Vector2 = Vector2(tex_w, tex_h) * zoom_f
var offset: Vector2 = (sz - scaled) * 0.5 + Vector2(0, 20)
viewer.set_view(zoom_f, offset)