extends SceneTree ## Dev helper: run the game windowed and capture staged screenshots for the ## README (day set + LATE-night set with street lamps and flashlight). ## Saves 1280x720 PNGs to user://screenshots/ by default. ## Usage: godot --path . --script res://src/test/capture_screenshots.gd ## godot --path . --script res://src/test/capture_screenshots.gd -- --out=res://docs/screenshots var out_dir := "user://screenshots" const MAX_FRAMES := 3600 # hard cap (~60s) var frames := 0 var gm: Node = null var state := 0 var state_frames := 0 var targeted := false var camera_staged := false var wand_attempts := 0 func _initialize() -> void: for arg in OS.get_cmdline_user_args(): if arg.begins_with("--out="): out_dir = arg.trim_prefix("--out=") root.size = Vector2i(1280, 720) var packed: PackedScene = load("res://scenes/main.tscn") var inst: Node = packed.instantiate() root.add_child(inst) func _process(_delta: float) -> bool: frames += 1 # The main scene's _ready may not have run when _initialize added it; # look for GameManager lazily each frame until it exists. if gm == null: gm = root.find_child("GameManager", true, false) if gm == null and frames > 120: push_error("GameManager not found") quit(1) return true if gm != null and not camera_staged: # The game boots into the venue menu now; start the Hollywood shift first. var menu = gm.get("start_menu") if menu != null and menu.visible: gm.call("_on_venue_selected", "hollywood_building") gm.guard.global_position = Vector3(0, 0.1, 10.5) gm.guard.rotation.y = 0.0 gm.guard.head.rotation.x = deg_to_rad(2.0) camera_staged = true if frames > MAX_FRAMES: print("timeout reached") quit(0) return true _advance() return false func _advance() -> void: state_frames += 1 match state: 0: # 2s overview from the guard post (early morning light). if state_frames >= 120: _shoot("01-venue-overview.png") state = 1 state_frames = 0 1: # Wait for a nearby NPC so the interaction HUD has a target. NPCs now # walk in from the street, so this can take a few simulated seconds. if gm != null and gm.npcs.size() > 0: gm.call("_cycle_nearby_target") _face_target() targeted = true state = 2 state_frames = 0 2: if state_frames >= 30: _shoot("02-targeting-npc.png") if targeted and gm != null: _face_target() gm.call("_toggle_inspection") state = 3 state_frames = 0 3: if state_frames >= 30: _shoot("03-access-inspection.png") if gm != null: gm.hud.hide_inspection() if gm.npcs.size() > 0: gm.call("_cycle_nearby_target") gm.call("_open_radio", false) state = 4 state_frames = 0 4: if state_frames >= 30: _shoot("04-radio-report.png") # Close the radio form, then jump the shift into LATE night: the # avenue and flashlight shots are night scenes (the old 620 s put # them in dusk, where the flashlight had nothing to cut through). if gm != null: gm.radio_panel.hide() gm.hud.set_action_hint("") gm.coordinator.elapsed_s = 760.0 state = 5 state_frames = 0 5: # Stage the avenue view down the boulevard (not into the wall) and let # the night lighting and street lamps settle. if state_frames == 1 and gm != null: gm.guard.rotation.y = deg_to_rad(-70) gm.guard.head.rotation.x = deg_to_rad(3.0) _reset_hud() if state_frames >= 90: _shoot("05-late-night-avenue.png") state = 6 state_frames = 0 6: # Face the entrance again and switch the flashlight on for the night set. if state_frames == 1 and gm != null: gm.guard.rotation.y = 0.0 gm.guard.head.rotation.x = deg_to_rad(4.0) _reset_hud() if state_frames == 26 and gm != null: # Light the facade on the final frames only: turning it on early # made nearby guests log "notices you watching" over the shot. gm.hud.clear_log() gm.call("_toggle_flashlight") if state_frames == 29 and gm != null: gm.hud.clear_log() gm.hud.set_target_label("") if state_frames >= 32: gm.hud.clear_log() gm.hud.set_target_label("") _shoot("06-night-flashlight.png") # Move to The Joke Shop for the wand/shift-review set, and keep # the late-night phase so the UI sits on the night scene. if gm != null: gm.call("_toggle_flashlight") gm.switch_scenario("joke_shop") gm.coordinator.elapsed_s = 620.0 state = 7 state_frames = 0 7: # Wand screening: wait for guests and keep re-targeting until one # accepts the screen (some authored Joke Shop guests refuse it). if state_frames > 40 and state_frames % 20 == 0 and gm != null and gm.npcs.size() > 0: # Prefer a guest whose items make the wand alert, so the panel # shows the interesting case instead of "Alert: none". if gm.wand_open: if _wand_alerts(): state = 8 state_frames = 0 elif wand_attempts >= 6: # Nobody nearby carried an alerting item: stage the documented # false-alarm case (keys) on this guest and re-open the panel. var guest: Object = gm.call("_targeted_controller") if guest != null and guest.profile != null: var false_alarm: Array[String] = ["keys"] guest.profile.carried_items = false_alarm gm.call("_toggle_wand_screening") gm.call("_toggle_wand_screening") state = 8 state_frames = 0 else: gm.call("_toggle_wand_screening") wand_attempts += 1 else: gm.call("_cycle_nearby_target") _face_target() # Stage the documented false-alarm case (keys) so the capture # shows the interesting alert instead of a clear guest. var guest: Object = gm.call("_targeted_controller") if guest != null and guest.profile != null: var false_alarm: Array[String] = ["keys"] guest.profile.carried_items = false_alarm gm.call("_toggle_wand_screening") wand_attempts += 1 elif state_frames > 600 and gm != null: # Fallback: real controller, panel shown directly. var controller: Object = gm.call("_targeted_controller") if controller != null and controller.profile != null: gm.hud.show_wand(controller, gm.wand_screener.screen(controller.profile.carried_items)) state = 8 state_frames = 0 8: if state_frames == 1 and gm != null: gm.hud.clear_log() _stage_closeup() if state_frames >= 45: _shoot("07-wand-screening.png") if gm != null: if gm.wand_open: gm.call("_toggle_wand_screening") # The fallback path can show the wand HUD without setting # `wand_open`; always hide it before the next screens. gm.hud.hide_wand() gm.wand_open = false # Real escalation path: formal warning arms the body camera # (a timeline row), then blocking raises the meter to L3. gm.call("_apply_interaction", 2, "formal warning") gm.call("_apply_interaction", 3, "blocking") state = 9 state_frames = 0 9: if state_frames == 1 and gm != null: # Jump into the late phase now that guests exist: LATE has no # routine spawn pool, so this happens after staging the target. gm.coordinator.elapsed_s = 760.0 _stage_closeup() if state_frames >= 45: _shoot("08-escalation-hud.png") if gm != null and gm.coordinator != null: # Close the open incident so the recorder's review includes it # (to_dict only lists closed incidents). gm.coordinator.recorder.close_all_open("unresolved") gm.call("_open_timeline", {"incidents": gm.coordinator.recorder.to_dict()}) state = 10 state_frames = 0 10: if state_frames >= 30: _shoot("09-body-camera-timeline.png") if gm != null: gm.event_timeline.close() # Precision walk at the Joke Shop entrance, not a blank wall: # face the red curtain door and keep the step line in frame. gm.guard.global_position = Vector3(-5.2, 0.1, 6.4) gm.guard.rotation.y = deg_to_rad(22.0) gm.guard.head.rotation.x = deg_to_rad(2.0) gm.hud.set_body_camera(false) gm.hud.set_escalation(0) gm.hud.clear_log() gm.hud.set_target_label("") Input.action_press("move_forward") Input.action_press("walk_slow") state = 11 state_frames = 0 11: if state_frames == 1 and gm != null: if gm.wand_open: gm.call("_toggle_wand_screening") gm.hud.hide_wand() gm.wand_open = false if gm.inspection_open: gm.call("_toggle_inspection") gm.hud.hide_inspection() gm.hud.clear_log() gm.hud.set_target_label("") if state_frames >= 50: gm.hud.clear_log() gm.hud.set_target_label("") _shoot("11-precision-walk.png") Input.action_release("move_forward") Input.action_release("walk_slow") if gm != null: gm.switch_scenario("joke_shop") gm.coordinator.elapsed_s = 620.0 state = 12 state_frames = 0 12: if state_frames == 1 and gm != null: # The wand fallback can leave the panel open; close every modal # before the venue beauty shot. if gm.wand_open: gm.call("_toggle_wand_screening") gm.hud.hide_wand() gm.wand_open = false if gm.inspection_open: gm.call("_toggle_inspection") gm.hud.hide_inspection() gm.guard.global_position = Vector3(-2.0, 0.1, 13.2) gm.guard.rotation.y = deg_to_rad(12.0) gm.guard.head.rotation.x = deg_to_rad(12.0) gm.hud.clear_log() gm.hud.set_escalation(0) gm.hud.set_body_camera(false) if state_frames >= 90: _shoot("12-joke-shop.png") if gm != null: gm.switch_scenario("tv_studio") gm.coordinator.elapsed_s = 620.0 state = 13 state_frames = 0 13: if state_frames == 1 and gm != null: gm.guard.global_position = Vector3(0, 0.1, 14.0) gm.guard.rotation.y = 0.0 gm.guard.head.rotation.x = deg_to_rad(2.0) gm.hud.clear_log() gm.hud.set_escalation(0) gm.hud.set_body_camera(false) if state_frames >= 120: _shoot("13-studio-arrival.png") quit(0) ## Clear log/target leftovers between staged shots so the HUD only shows the ## current moment. func _reset_hud() -> void: if gm == null or gm.hud == null: return gm.hud.clear_log() gm.hud.set_target_label("") gm.hud.set_action_hint("") ## Put the guard a couple of steps in front of the target so a staged UI capture ## frames the person the panel is about. func _wand_alerts() -> bool: if gm == null or gm.hud == null or gm.hud.wand_text == null: return false return gm.hud.wand_text.text.contains("Alert: YES") func _stage_closeup() -> void: var target: Node3D = gm.targeted_npc if target == null or not is_instance_valid(target): return gm.guard.global_position = target.global_position + Vector3(0, -0.4, 2.2) gm.guard.global_position.y = 0.1 _face_target() gm.guard.head.rotation.x = deg_to_rad(1.0) func _face_target() -> void: var target: Node3D = gm.targeted_npc if target == null: return var to: Vector3 = target.global_position - gm.guard.global_position to.y = 0.0 if to.length() < 0.01: return gm.guard.rotation.y = atan2(-to.x, -to.z) gm.guard.head.rotation.x = deg_to_rad(-4.0) func _shoot(file_name: String) -> void: var dir := DirAccess.open(out_dir) if dir == null: DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(out_dir)) var img: Image = root.get_texture().get_image() var err := img.save_png(out_dir.path_join(file_name)) print("%s -> err=%d (%dx%d)" % [file_name, err, img.get_width(), img.get_height()])