Add the campaign shell, pause-safe checkpoint resume, post-shift campaign continue, confirmed campaign reset, difficulty presets, searchable Help, content-matrix validation, recovery events, contextual dialogue, policy/debrief context and eligible-event previews. Extend persistence with atomic separate files and add campaign/checkpoint/tutorial smokes. Headless 247/247, Python 94/94, tutorial/campaign/session/scenario smokes green.
76 lines
2.2 KiB
GDScript
76 lines
2.2 KiB
GDScript
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,
|
|
}
|
|
|
|
func snapshot() -> Dictionary:
|
|
return summary()
|
|
|
|
func restore(data: Dictionary) -> void:
|
|
mood = clampf(float(data.get("mood", 0.05)), 0.0, 1.0)
|
|
attention = clampf(float(data.get("attention", 0.0)), 0.0, 1.0)
|
|
crowd_panic = bool(data.get("panic", mood >= 0.75))
|