Files
security-game/src/test/capture_screenshots.gd
T
zwitschi 71b0b53705
Full build / Import + headless tests (push) Successful in 27s
Full build / Full desktop build + optional release (push) Failing after 3m37s
Add crowd presentation, fix NPC navigation, refresh screenshots
NPC navigation (previously dead in the live game)
- Only assign NavigationAgent3D.target_position when it changes: re-assigning every
  frame invalidated the path, so get_next_path_position() kept returning the current
  point and avatars never moved.
- Follow the first path point that is closer to the destination than the avatars are.
  Nav points sit on the navmesh plane (y=0) while bodies stand at y~0.5, so the old
  3D distance test always looked "already there"; path[0] is also the old start point,
  so a naive "far enough away" pick steered backwards and oscillated.
- Fix fsm.state -> fsm.current_state in the idle-lifetime check.

Crowd presentation
- Aggressive behavior, visible weapons, and responder arrivals make idle bystanders
  stop and face the incident for 12 s; someone waiting at the entrance keeps focus.
- Panic (mood >= 0.75) walks people away from the door at 2.4 m/s, then they despawn.
- Wire reactions to EventBus.crowd_changed: CrowdSystem.apply_event() emits on the bus,
  not on its own `changed` signal (the first wiring received nothing).
- Measured a 16-NPC entrance at ~1.2 ms/frame (824 fps, vsync off), so no LOD/culling.

World and HUD
- Skyline is now a ring of four alpha-scissored planes at 200 m instead of a single
  plane; PlaneMesh defaults to horizontal, which is why the backdrop rendered as a band
  overhead. Larger ground plane, mipmapped texture, scissor threshold 0.5 (0.3 left
  mip dashes in the sky), lifted albedo so the city is not pure black.
- Flashlight aims straight ahead and lights the person/door instead of the ground.
- Shorter HUD controls line so it stops overlapping itself.

Tests, tooling, docs
- Headless suite 98 -> 105 (new test_crowd_presentation.gd, 7 tests).
- New smoke_crowd.gd: rubbernecking, panic, and flee-target sanity.
- capture_screenshots.gd starts a shift from the venue menu and stages the night set
  properly; all six README screenshots regenerated.
- README, TODO/DONE (crowd section moved), MEMORY updated. Version 0.2.4.
2026-10-31 20:55:10 +01:00

134 lines
4.0 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/.
## Usage: godot --path . --script res://src/test/capture_screenshots.gd
const 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
func _initialize() -> void:
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 to the LATE phase.
if gm != null:
gm.radio_panel.hide()
gm.hud.set_action_hint("")
gm.coordinator.elapsed_s = 750.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")
state = 7
quit(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("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 := img.save_png(OUT_DIR.path_join(file_name))
print("%s -> err=%d (%dx%d)" % [file_name, err, img.get_width(), img.get_height()])