class_name CrowdSystem extends RefCounted ## Models crowd mood and public reaction (PLAN.md: Public Crowd Reaction). const EventBus := preload("res://src/core/event_bus.gd") ## mood: 0.0 (calm) .. 1.0 (panic). attention: how many people are watching. signal changed(payload: Dictionary) var event_bus: EventBus = null var mood: float = 0.05 var attention: float = 0.0 var crowd_panic := false func set_event_bus(bus: EventBus) -> void: event_bus = bus ## Apply a crowd event. type: "calm_professionalism", "aggressive_behavior", ## "visible_weapon", "emergency_responder", "de_escalation". func apply_event(event_type: String) -> void: match event_type: "calm_professionalism": attention = maxf(0.0, attention - 0.05) mood = maxf(0.0, mood - 0.02) "de_escalation": mood = maxf(0.0, mood - 0.06) attention = clampf(attention + 0.05, 0.0, 1.0) "aggressive_behavior": attention = clampf(attention + 0.20, 0.0, 1.0) mood = clampf(mood + 0.12, 0.0, 1.0) "visible_weapon": attention = 1.0 mood = clampf(mood + 0.35, 0.0, 1.0) "emergency_responder": attention = 1.0 mood = clampf(mood + 0.25, 0.0, 1.0) _: pass crowd_panic = mood >= 0.75 if event_bus: event_bus.crowd_changed.emit({ "mood": mood, "attention": attention, "panic": crowd_panic, "event": event_type, }) ## Presentation cue for the avatars after a crowd event (PLAN: Public Crowd ## Reaction). "watch" = rubberneck at the incident, "flee" = move away from the ## door, "" = no reaction. Pure so it is headless-testable. static func reaction_for_event(event_type: String) -> String: match event_type: "aggressive_behavior", "visible_weapon", "emergency_responder": return "watch" _: return "" ## True once the crowd is panicking hard enough that people should clear out. func should_flee() -> bool: return crowd_panic func summary() -> Dictionary: return { "mood": mood, "attention": attention, "panic": crowd_panic, }