Add the alley walls, warm lantern, shutters, dumpster, AC clutter and door step for the Joke Shop. Build the studio stone facade, bulb canopy, glossy black car, stanchions and barriers. Rebuild only the world container when the venue changes, so the guard and UI survive; add per-venue navigation checks. Capture gallery shots 12/13, update the concept mapping and docs. Headless 231/231, Python 87/87, screenshot gate 13 good/0 bad. The vision-judge retry is tracked separately because the API key is invalid.
276 lines
9.1 KiB
GDScript
276 lines
9.1 KiB
GDScript
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)
|
|
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)
|
|
gm.call("_toggle_flashlight")
|
|
if state_frames >= 30:
|
|
_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()
|
|
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")
|
|
# 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()
|
|
gm.guard.global_position = Vector3(0, 0.1, 6.2)
|
|
gm.guard.rotation.y = 0.0
|
|
Input.action_press("move_forward")
|
|
Input.action_press("walk_slow")
|
|
state = 11
|
|
state_frames = 0
|
|
11:
|
|
if state_frames >= 50:
|
|
_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:
|
|
gm.guard.global_position = Vector3(-1.5, 0.1, 8.5)
|
|
gm.guard.rotation.y = deg_to_rad(-5.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 >= 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)
|
|
|
|
## 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()])
|