extends SceneTree ## Windowed smoke helper for the ADR-0007–0010 control pass (focus ring + action ## wheel). Boots the real main scene, stages one NPC in front of the guard, opens ## the wheel, and saves two PNGs to user://screenshots/. Exits 1 if the focus or ## radial state did not come up, so it can be used as a smoke check. ## Usage: godot --path . --script res://src/test/smoke_focus_radial.gd const OUT_DIR := "user://screenshots" const MAX_FRAMES := 1800 const STAGE_DISTANCE := 2.4 var frames := 0 var gm: Node = null var state := 0 var state_frames := 0 var stage_npc: Node3D = null var failures: Array[String] = [] var _shift_started := false func _initialize() -> void: root.size = Vector2i(1280, 720) var packed: PackedScene = load("res://scenes/main.tscn") root.add_child(packed.instantiate()) ## The game now boots into the venue menu, so the smoke starts a shift first. func _start_shift_if_needed() -> void: if _shift_started or gm == null: return var menu = gm.get("start_menu") if menu != null and menu.visible: gm.call("_on_venue_selected", "hollywood_building") _shift_started = true print("smoke: started hollywood_building shift from the menu") func _process(_delta: float) -> bool: frames += 1 if gm == null: gm = root.find_child("GameManager", true, false) if gm == null: if frames > 600: _fail("GameManager not found") return false if frames > MAX_FRAMES: _fail("timeout before the radial state") return true _advance() return false func _advance() -> void: state_frames += 1 match state: 0: _start_shift_if_needed() # Wait for the guard to settle on the ground, then grab one NPC. if state_frames >= 40 and gm.npcs.size() > 0: print("smoke: spawn guard_y=%.2f z=%.2f" % [ (gm.guard.global_position as Vector3).y, (gm.guard.global_position as Vector3).z, ]) _shoot("smoke-00-spawn.png") stage_npc = gm.npcs[0] _hide_other_npcs(stage_npc) state = 1 state_frames = 0 1: # Keep the staged NPC in front of the guard so the ring is visible. _stage_target() if state_frames >= 20: gm.call("_cycle_nearby_target") if gm.targeted_npc == null: _fail("no NPC focused") elif gm.radial == null: _fail("radial layer missing") else: print("smoke: guard_y=%.2f npc_y=%.2f" % [ (gm.guard.global_position as Vector3).y, (gm.targeted_npc.global_position as Vector3).y, ]) _shoot("smoke-01-focus-ring.png") gm.call("_open_radial") state = 2 state_frames = 0 2: if state_frames >= 20: if not gm.radial.is_open: _fail("action wheel did not open") else: _shoot("smoke-02-action-wheel.png") gm.call("_cancel_radial") state = 3 state_frames = 0 3: if state_frames >= 5: if gm.radial.is_open: _fail("action wheel did not close on cancel") _finish() func _stage_target() -> void: if stage_npc == null or not is_instance_valid(stage_npc): stage_npc = gm.npcs[0] if gm.npcs.size() > 0 else null if stage_npc == null: return _hide_other_npcs(stage_npc) var guard_pos: Vector3 = gm.guard.global_position var forward: Vector3 = -gm.guard.global_transform.basis.z forward.y = 0.0 forward = forward.normalized() stage_npc.global_position = guard_pos + forward * STAGE_DISTANCE + Vector3(0, 0.4, 0) stage_npc.rotation.y = gm.guard.rotation.y + PI func _hide_other_npcs(keep: Node3D) -> void: for npc in gm.npcs: if is_instance_valid(npc) and npc is Node3D and npc != keep: (npc as Node3D).visible = false func _shoot(file_name: String) -> void: var dir := DirAccess.open("user://") if dir == null or not dir.dir_exists("screenshots"): dir.make_dir_recursive("screenshots") var img: Image = root.get_texture().get_image() var err: int = img.save_png(OUT_DIR.path_join(file_name)) print("%s -> err=%d (%dx%d)" % [file_name, err, img.get_width(), img.get_height()]) func _fail(message: String) -> void: failures.append(message) _finish() func _finish() -> void: if failures.is_empty(): print("SMOKE OK: focus ring + action wheel opened and cancelled") quit(0) else: for message in failures: printerr("SMOKE FAIL: " + message) quit(1)