extends RefCounted ## Crowd presentation: rubbernecking on aggressive/emergency events and clearing ## out when the crowd panics (PLAN: Public Crowd Reaction). const CrowdSystem := preload("res://src/core/crowd_system.gd") const NpcAvatar := preload("res://src/game/npc_avatar.gd") const NpcProfile := preload("res://src/core/npc_profile.gd") const Rules := preload("res://src/core/rules.gd") func _profile() -> NpcProfile: var p := NpcProfile.new() p.id = "bystander" p.display_name = "Bystander" p.npc_type = Rules.NpcType.TOURIST return p func _avatar() -> Node: var avatar: Node = NpcAvatar.new() avatar.setup(_profile(), "photo", 99, Vector3(4, 0.5, 8), Vector3(2, 0.5, 12)) return avatar func test_reaction_for_event() -> bool: var a := TestUtils.check_eq(CrowdSystem.reaction_for_event("aggressive_behavior"), "watch", "aggression draws watchers") var b := TestUtils.check_eq(CrowdSystem.reaction_for_event("emergency_responder"), "watch", "responder draws watchers") var c := TestUtils.check_eq(CrowdSystem.reaction_for_event("visible_weapon"), "watch", "a weapon draws watchers") var d := TestUtils.check_eq(CrowdSystem.reaction_for_event("de_escalation"), "", "de-escalation is not a watch cue") var e := TestUtils.check_eq(CrowdSystem.reaction_for_event("calm_professionalism"), "", "calm is not a watch cue") return a and b and c and d and e func test_panic_flag_and_flee_cue() -> bool: var crowd := CrowdSystem.new() var a := TestUtils.check(not crowd.should_flee(), "crowd starts calm") crowd.apply_event("visible_weapon") crowd.apply_event("aggressive_behavior") crowd.apply_event("aggressive_behavior") crowd.apply_event("emergency_responder") var b := TestUtils.check(crowd.should_flee(), "stacked alarms reach panic") var c := TestUtils.check_eq(crowd.summary().get("panic"), true, "summary reports panic") var calm := CrowdSystem.new() calm.apply_event("de_escalation") calm.apply_event("calm_professionalism") var d := TestUtils.check(not calm.should_flee(), "a calm crowd does not flee") crowd = null calm = null return a and b and c and d func test_idle_bystander_rubbernecks() -> bool: var avatar := _avatar() # Still walking: should refuse so traffic is not frozen mid-approach. var walking := TestUtils.check(not avatar.start_rubberneck(Vector3(0, 1, 1)), "walkers keep walking") avatar.arrived = true var accepted := TestUtils.check(avatar.start_rubberneck(Vector3(0, 1, 1)), "idle bystander watches") var mode := TestUtils.check_eq(avatar.reaction_mode, "watch", "watch mode is set") var active := TestUtils.check(avatar.reaction_is_active(), "reaction is active") avatar.free() return walking and accepted and mode and active func test_engaged_person_does_not_rubberneck() -> bool: var avatar := _avatar() avatar.arrived = true avatar.awaiting_interaction = true # Not in a tree, so use local position (global_position is a no-op here). avatar.position = avatar.door_position var ok := TestUtils.check(not avatar.start_rubberneck(Vector3.ZERO), "someone waiting at the door does not rubberneck") # The same person further away is an ordinary bystander and does watch. avatar.position = avatar.door_position + Vector3(8, 0, 0) var far_ok := TestUtils.check(avatar.start_rubberneck(Vector3.ZERO), "a bystander away from the door still watches") avatar.free() return ok and far_ok func test_panic_moves_people_away_from_the_door() -> bool: var avatar := _avatar() avatar.position = Vector3(1, 0.5, 2) var door := Vector3(0, 0.5, 1.2) avatar.start_flee(door) var a := TestUtils.check_eq(avatar.reaction_mode, "flee", "flee mode is set") var b := TestUtils.check((avatar.target_position - door).length() > (avatar.position - door).length(), "flee target is farther from the door than the start") var c := TestUtils.check(not avatar.awaiting_interaction, "fleeing cancels waiting at the entrance") avatar.free() return a and b and c func test_crowd_updates_arrive_through_the_event_bus() -> bool: ## CrowdSystem.apply_event() emits EventBus.crowd_changed, not its own ## `changed` signal; wiring the wrong one silently disables crowd reactions. var src := FileAccess.get_file_as_string("res://src/game/game_manager.gd") var ok := TestUtils.check(src.contains("event_bus.crowd_changed.connect(_on_crowd_changed)"), "GameManager listens on EventBus.crowd_changed") var bus: Object = load("res://src/core/event_bus.gd").new() var seen: Array = [] bus.crowd_changed.connect(func(payload): seen.append(payload.get("event", ""))) var crowd := CrowdSystem.new() crowd.set_event_bus(bus) crowd.apply_event("aggressive_behavior") var delivers := TestUtils.check_eq(seen, ["aggressive_behavior"], "apply_event reaches the bus listener") bus = null crowd = null return ok and delivers func test_leaving_npc_does_not_flee() -> bool: var avatar := _avatar() avatar.npc.leave_requested = true avatar.start_flee(Vector3.ZERO) var ok := TestUtils.check_eq(avatar.reaction_mode, "", "someone already leaving is left alone") avatar.free() return ok