Every atlas capture scenario built its body dict without body_radius_km. Radius is Global's ONLY spacing input (2*pi*R / width), so every Global golden has been a degenerate block since these scenarios were created — the map rendered at 0.000 km/gridunit. The fixed rungs were unaffected, since they derive spacing from the rung's own cell size rather than the body. That means the "eyeball check against .cache/screenshots baselines" discipline has been hollow at Global specifically: a baseline that is a solid rectangle diffs clean against a new solid rectangle. It is how a 2x1 canvas survived in front of two verification layers. Adds the real radii to all ten scenarios (Lendel 6238.4, Vethis 6959.3, Arbour 6711.0, Ferrath 6062.0, Threshold 5503.5), and makes the capture log course and settlement counts alongside canvas_cells — an empty annotation layer was previously indistinguishable from a populated one in the log, which is exactly the signal needed to tell "no rivers" from "rivers not drawn". Also updates three suites to the corrected Global cache contract: the key now honours extent (collapsing it meant a resize could never miss), so the shared _land_global_canvas helper must cache under the extent the viewer will actually request, and the two make_key tests now assert the real rule — centre collapsed, extent honoured — instead of the retired sentinel. Client suite 1830 total / 1804 passed / 0 failed / 26 skipped. Pair session with Jeroen, 2026-07-27. Co-Authored-By: Claude <noreply@anthropic.com>
440 lines
15 KiB
GDScript
440 lines
15 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
|
|
# region-ramp overlays — exercises the opener + the live request → cascade
|
|
# → render pipeline against a connected server (SR_LIVE=1). D-255 rewrite:
|
|
# the pre-stepped l1_rivers/l1_basins/l1_attractors overlay ids retired
|
|
# with AtlasWindowViewer — StepCanvasViewer's own toggle group is exactly
|
|
# the three gen_dw_* region-ramp colorizers (step_canvas_viewer.gd's
|
|
# OVERLAY_DEFS), so this now enables all three instead.
|
|
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",
|
|
"body_radius_km": 5503.5,
|
|
"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_dw_temp", true)
|
|
|
|
# T-1157 (D-255 rewrite): the stepped-Atlas golden matrix — goldens key
|
|
# on (body, rung), not the retired continuous-zoom (body, zoom, overlay-
|
|
# set) triples T-1120 curated. Each case below pins ONE body/rung/
|
|
# overlay combo, driven through the SAME production surface the app
|
|
# itself uses: AtlasApp._on_body_selected()'s own tail
|
|
# (nav.push("regional", ...)) to enter, then StepCanvasViewer.jump_to()
|
|
# for every rung below Global — the T-1157 harness-inventory "fixed-
|
|
# center revisit" pattern (world_center literally re-requested, not
|
|
# re-derived from a cursor gesture), so the SAME world point is hit on
|
|
# every run regardless of viewport size or cursor-anchoring math. The
|
|
# Global rung needs no jump_to() call at all: enter() always lands on
|
|
# it (world_center == ZERO, D-255(a)) — it is the natural replacement
|
|
# for the old whole-body "fit zoom" shot.
|
|
"atlas_GJ380c_Global":
|
|
_setup_atlas_golden_shot(
|
|
tree_root,
|
|
{
|
|
"body_id": "GJ380c",
|
|
"body_radius_km": 6238.4,
|
|
"proper_name": "Lendel",
|
|
"terrain_reference": "wiki/star-systems/GJ-380/bodies/GJ380c/heightmap.png",
|
|
},
|
|
{"system_id": "GJ 380", "proper_name": "Groombridge"},
|
|
"Global",
|
|
["gen_dw_temp"]
|
|
)
|
|
|
|
"atlas_GJ380c_Region":
|
|
_setup_atlas_golden_shot(
|
|
tree_root,
|
|
{
|
|
"body_id": "GJ380c",
|
|
"body_radius_km": 6238.4,
|
|
"proper_name": "Lendel",
|
|
"terrain_reference": "wiki/star-systems/GJ-380/bodies/GJ380c/heightmap.png",
|
|
},
|
|
{"system_id": "GJ 380", "proper_name": "Groombridge"},
|
|
"Region",
|
|
["gen_dw_moisture"]
|
|
)
|
|
|
|
"atlas_GJ380c_District":
|
|
_setup_atlas_golden_shot(
|
|
tree_root,
|
|
{
|
|
"body_id": "GJ380c",
|
|
"body_radius_km": 6238.4,
|
|
"proper_name": "Lendel",
|
|
"terrain_reference": "wiki/star-systems/GJ-380/bodies/GJ380c/heightmap.png",
|
|
},
|
|
{"system_id": "GJ 380", "proper_name": "Groombridge"},
|
|
"District",
|
|
[]
|
|
)
|
|
|
|
"atlas_GJ144e_Global":
|
|
_setup_atlas_golden_shot(
|
|
tree_root,
|
|
{
|
|
"body_id": "GJ144e",
|
|
"body_radius_km": 6959.3,
|
|
"proper_name": "Vethis",
|
|
"terrain_reference": "wiki/star-systems/GJ-144/bodies/GJ144e/heightmap.png",
|
|
},
|
|
{"system_id": "GJ 144", "proper_name": "Ran"},
|
|
"Global",
|
|
[]
|
|
)
|
|
|
|
"atlas_GJ144e_Quarter":
|
|
_setup_atlas_golden_shot(
|
|
tree_root,
|
|
{
|
|
"body_id": "GJ144e",
|
|
"body_radius_km": 6959.3,
|
|
"proper_name": "Vethis",
|
|
"terrain_reference": "wiki/star-systems/GJ-144/bodies/GJ144e/heightmap.png",
|
|
},
|
|
{"system_id": "GJ 144", "proper_name": "Ran"},
|
|
"Quarter",
|
|
["gen_dw_veg"]
|
|
)
|
|
|
|
"atlas_GJ338Bd_Global":
|
|
_setup_atlas_golden_shot(
|
|
tree_root,
|
|
{
|
|
"body_id": "GJ338Bd",
|
|
"body_radius_km": 6711.0,
|
|
"proper_name": "Arbour",
|
|
"terrain_reference": "wiki/star-systems/GJ-338B/bodies/GJ338Bd/heightmap.png",
|
|
},
|
|
{"system_id": "GJ 338B", "proper_name": "Arbour"},
|
|
"Global",
|
|
["gen_dw_moisture"]
|
|
)
|
|
|
|
"atlas_GJ338Bd_Block":
|
|
_setup_atlas_golden_shot(
|
|
tree_root,
|
|
{
|
|
"body_id": "GJ338Bd",
|
|
"body_radius_km": 6711.0,
|
|
"proper_name": "Arbour",
|
|
"terrain_reference": "wiki/star-systems/GJ-338B/bodies/GJ338Bd/heightmap.png",
|
|
},
|
|
{"system_id": "GJ 338B", "proper_name": "Arbour"},
|
|
"Block",
|
|
[]
|
|
)
|
|
|
|
"atlas_GJ820Bc_Global":
|
|
_setup_atlas_golden_shot(
|
|
tree_root,
|
|
{
|
|
"body_id": "GJ820Bc",
|
|
"body_radius_km": 6062.0,
|
|
"proper_name": "Ferrath",
|
|
"terrain_reference": "wiki/star-systems/GJ-820B/bodies/GJ820Bc/heightmap.png",
|
|
},
|
|
{"system_id": "GJ 820B", "proper_name": "Cygni B"},
|
|
"Global",
|
|
[]
|
|
)
|
|
|
|
"atlas_GJ820Bc_District":
|
|
_setup_atlas_golden_shot(
|
|
tree_root,
|
|
{
|
|
"body_id": "GJ820Bc",
|
|
"body_radius_km": 6062.0,
|
|
"proper_name": "Ferrath",
|
|
"terrain_reference": "wiki/star-systems/GJ-820B/bodies/GJ820Bc/heightmap.png",
|
|
},
|
|
{"system_id": "GJ 820B", "proper_name": "Cygni B"},
|
|
"District",
|
|
["gen_dw_temp"]
|
|
)
|
|
|
|
"atlas_GJ251c_Global":
|
|
_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"},
|
|
"Global",
|
|
["gen_dw_veg"]
|
|
)
|
|
|
|
"atlas_GJ251c_Region":
|
|
_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"},
|
|
"Region",
|
|
[]
|
|
)
|
|
|
|
"atlas_GJ244Ad_Global":
|
|
_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"},
|
|
"Global",
|
|
["gen_dw_temp"]
|
|
)
|
|
|
|
"atlas_GJ445c-m1_Chunk":
|
|
_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"},
|
|
"Chunk",
|
|
[]
|
|
)
|
|
|
|
_:
|
|
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-1157 (D-255 rewrite, supersedes the T-1120 continuous-zoom helper):
|
|
## shared setup for the "atlas_<body>_<rung>" golden cases above — opens the
|
|
## Atlas via the SAME production tail AtlasApp._on_body_selected() itself
|
|
## uses (nav.push("regional", {body, system})), then lands on `rung` via
|
|
## StepCanvasViewer's real navigation surface: entering always lands on
|
|
## Global (D-255(a), RegionalScreen.enter() -> StepCanvasViewer.enter()) —
|
|
## for any other rung, jump_to(Vector2.ZERO, rung) is called next, the T-1157
|
|
## harness-inventory "fixed-center revisit" pattern (a literal, always-valid
|
|
## world point — the exact center of every body's own region grid — rather
|
|
## than a cursor-anchored derivation that would depend on viewport size).
|
|
## Every fixed rung's viewport-fit extent is still whatever _request_extent()
|
|
## itself computes from the live viewport (matching production — the goldens
|
|
## are NOT pinning a synthetic extent), only world_center and rung are fixed.
|
|
##
|
|
## Mirrors visual_capture.gd's _run_atlas_matrix() rung/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 StepCanvasViewer
|
|
## surface, jump_to()/get_overlay_defs()/set_overlay_visible()/get_request()).
|
|
func _setup_atlas_golden_shot(
|
|
tree_root: Node, body: Dictionary, system: Dictionary, rung: String, 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 rung != "Global":
|
|
# enter() (called via nav.push above) always lands on Global first —
|
|
# jump_to() is the fixed-center revisit intent that descends to any
|
|
# other rung at a literal, reproducible world_center (D-255(a): every
|
|
# body's region grid is centered on world-metre ZERO).
|
|
viewer.jump_to(Vector2.ZERO, rung)
|