extends SceneTree ## Crowd-presentation smoke: starts a shift, waits for bystanders to settle, then ## checks that an aggressive crowd event makes idle NPCs stop and watch, and that ## panic sends them away from the door. ## ## Usage: ## godot --headless --path . --script res://src/test/smoke_crowd.gd ## Exit code 0 = pass, 1 = failure. const MAX_FRAMES := 2400 var frame := 0 var gm: Node = null var started := false var phase := 0 var failures: Array[String] = [] func _initialize() -> void: root.size = Vector2i(1280, 720) root.add_child(load("res://scenes/main.tscn").instantiate()) func _process(_delta: float) -> bool: frame += 1 if gm == null: gm = root.find_child("GameManager", true, false) return false if not started: gm.call("_on_venue_selected", "hollywood_building") started = true return false if frame > MAX_FRAMES: _fail("timeout before crowd reactions were observed") return true match phase: 0: if frame < 300: return false # Wait until at least one settled bystander is away from the door (someone # waiting at the entrance is being dealt with and must not rubberneck). if _bystander_count() == 0: return false gm.coordinator.crowd.apply_event("aggressive_behavior") var watching := _count_mode("watch") _check(watching > 0, "aggressive behavior makes idle NPCs watch (%d watching)" % watching) phase = 1 1: if frame < 320: return false var door: Vector3 = gm.selected_scenario._location_or_default("entrance", Vector3(0, 0.5, 1.2)) for npc in gm.npcs: if is_instance_valid(npc) and npc.has_method("reaction_is_active"): pass gm.coordinator.crowd.apply_event("visible_weapon") gm.coordinator.crowd.apply_event("aggressive_behavior") gm.coordinator.crowd.apply_event("aggressive_behavior") gm.coordinator.crowd.apply_event("emergency_responder") _check(gm.coordinator.crowd.should_flee(), "stacked alarms panic the crowd") var fleeing := _count_mode("flee") _check(fleeing > 0, "panic sends NPCs away from the door (%d fleeing)" % fleeing) var away_ok := true for npc in gm.npcs: if not is_instance_valid(npc) or npc.reaction_mode != "flee": continue if (npc.target_position - door).length() <= (npc.global_position - door).length(): away_ok = false _check(away_ok, "every fleeing NPC targets a point farther from the door") phase = 2 _: _finish() return true return false func _bystander_count() -> int: var n := 0 for npc in gm.npcs: if not is_instance_valid(npc) or not npc.arrived: continue if npc.awaiting_interaction and npc.global_position.distance_to(npc.door_position) < 3.0: continue n += 1 return n func _settled_count() -> int: var n := 0 for npc in gm.npcs: if is_instance_valid(npc) and npc.arrived: n += 1 return n func _count_mode(mode: String) -> int: var n := 0 for npc in gm.npcs: if is_instance_valid(npc) and npc.reaction_mode == mode: n += 1 return n func _check(condition: bool, message: String) -> void: if condition: print("ok: " + message) else: _fail(message) func _fail(message: String) -> void: failures.append(message) _finish() func _finish() -> void: if failures.is_empty(): print("SMOKE OK: crowd rubberneck and panic flee") quit(0) else: for message in failures: printerr("SMOKE FAIL: " + message) quit(1)