Add emergency aftermath: evacuation, investigation, review copy
- Responder outcomes now reach the shift review (ShiftCoordinator.responder_outcomes), and the review screen names them. - "evacuation" (emergency call, low danger, no weapon) actually clears the entrance: the door is locked, every NPC flees and is marked as leaving, and a security_failure reputation hit applies. - An emergency call the situation did not justify (no weapon, danger <= 1) appends an investigation note, records investigation_opened, and applies the existing false_report hit. The review lists the investigations. - Fix dispatch dying after the first shift: coordinator.setup() builds a new RadioSystem, so the responder_arrived connection made in _ready was lost on every venue switch/restart. _connect_radio() re-wires it beside _connect_crowd(). Tests/docs - Headless suite 105 -> 113 (test_emergency_aftermath.gd), new smoke_emergency.gd covering the investigation, evacuation aftermath, and review copy. - README controls/status/smoke checklist, TODO section moved to DONE, MEMORY notes. - Version 0.2.5.
This commit is contained in:
1 parent
71b0b53705
commit
921bab796e
25 files changed
+1019
-23
No files matched your search
@@ -134,7 +134,7 @@ func _ready() -> void:
|
||||
coordinator.setup(selected_scenario, scenario_duration_s, 4242)
|
||||
coordinator.npc_spawn_requested.connect(_spawn_npc)
|
||||
coordinator.shift_ended.connect(_on_shift_ended)
|
||||
coordinator.radio.responder_arrived.connect(_on_responder_arrived)
|
||||
_connect_radio()
|
||||
_connect_crowd()
|
||||
|
||||
_build_ui()
|
||||
@@ -222,6 +222,7 @@ func _reset_shift_state() -> void:
|
||||
hud.set_body_camera(false)
|
||||
scenario_duration_s = selected_scenario.shift_duration_s
|
||||
coordinator.setup(selected_scenario, scenario_duration_s, 4242)
|
||||
_connect_radio()
|
||||
_connect_crowd()
|
||||
_last_phase = -1
|
||||
_apply_phase_lighting()
|
||||
@@ -480,12 +481,51 @@ func _on_responder_arrived(call: Dictionary) -> void:
|
||||
scene_root.add_child(avatar)
|
||||
npcs.append(avatar)
|
||||
hud.log(Strings.fmt("responder.arrived", [outcome]))
|
||||
coordinator.responder_outcomes.append(outcome)
|
||||
coordinator.recorder.add_event(call.get("report", {}).get("npc_id", ""), {
|
||||
"type": "responder_action",
|
||||
"outcome": outcome,
|
||||
"call_type": call.get("type", "assist"),
|
||||
})
|
||||
coordinator.crowd.apply_event("emergency_responder" if call.get("type", "") == "emergency" else "calm_professionalism")
|
||||
if outcome == "evacuation":
|
||||
_evacuate_entrance()
|
||||
|
||||
## Responder outcome "evacuation": the entrance is cleared and the door stays
|
||||
## locked while the block is secured (PLAN.md: Level 5 Emergency Response).
|
||||
func _evacuate_entrance() -> void:
|
||||
if door and not door.locked:
|
||||
door.set_locked(true)
|
||||
if world_builder:
|
||||
world_builder.set_door_locked(true)
|
||||
if hud:
|
||||
hud.set_door(true)
|
||||
var door_pos: Vector3 = selected_scenario._location_or_default("entrance", Vector3(0, 0.5, 1.2))
|
||||
for npc in npcs:
|
||||
if not is_instance_valid(npc):
|
||||
continue
|
||||
if npc.has_method("start_flee"):
|
||||
npc.start_flee(door_pos)
|
||||
var controller: Object = npc.get_npc_controller() if npc.has_method("get_npc_controller") else null
|
||||
if controller != null:
|
||||
coordinator.recorder.add_event(controller.npc_id, {
|
||||
"type": "evacuation",
|
||||
"outcome": "evacuated",
|
||||
})
|
||||
if controller.leave_requested == false:
|
||||
controller.leave_requested = true
|
||||
coordinator.reputation.apply("security_failure", -3.0, "entrance evacuated by responder")
|
||||
if hud:
|
||||
hud.log(Strings.lookup("responder.evacuated"))
|
||||
|
||||
## Responder dispatch. `coordinator.setup()` builds a new RadioSystem for each
|
||||
## shift, so the arrival signal must be re-connected after every setup or radio
|
||||
## calls are silently resolved by nobody.
|
||||
func _connect_radio() -> void:
|
||||
if coordinator == null or coordinator.radio == null:
|
||||
return
|
||||
if not coordinator.radio.responder_arrived.is_connected(_on_responder_arrived):
|
||||
coordinator.radio.responder_arrived.connect(_on_responder_arrived)
|
||||
|
||||
## Crowd presentation (PLAN: Public Crowd Reaction). The crowd system is a
|
||||
## mood/attention float; this turns its events into avatar behavior.
|
||||
@@ -1073,12 +1113,33 @@ func _on_radio_submitted(report: Dictionary, emergency: bool) -> void:
|
||||
if emergency:
|
||||
coordinator.crowd.apply_event("emergency_responder")
|
||||
hud.log(Strings.lookup("radio.emergency_dispatched"))
|
||||
# A call that the situation did not justify is reviewed afterwards
|
||||
# (PLAN.md: Level 5 consequences).
|
||||
if _is_unjustified_emergency():
|
||||
var who: String = "the person"
|
||||
var controller: Object = _targeted_controller()
|
||||
if controller and controller.profile:
|
||||
who = str(controller.profile.display_name)
|
||||
coordinator.investigations.append(Strings.fmt("investigation.unjustified", [who]))
|
||||
coordinator.recorder.add_event(current_incident_npc_id, {
|
||||
"type": "investigation_opened",
|
||||
"reason": "unjustified emergency call",
|
||||
})
|
||||
coordinator.reputation.apply("false_report", -6.0, "unjustified emergency call")
|
||||
hud.log(Strings.lookup("investigation.started"))
|
||||
else:
|
||||
coordinator.crowd.apply_event("calm_professionalism")
|
||||
if audio:
|
||||
audio.play_radio_response()
|
||||
_maybe_radio_easter_egg()
|
||||
|
||||
## True when an emergency call is not supported by the situation: no weapon
|
||||
## seen and low danger (PLAN.md: unjustified call consequences).
|
||||
func _is_unjustified_emergency() -> bool:
|
||||
var weapons := bool(current_npc_truth.get("weapons_seen", false))
|
||||
var danger := int(current_npc_truth.get("danger_level", 0))
|
||||
return not weapons and danger <= 1
|
||||
|
||||
## Rare hidden radio transmission: a stray voice asks "¿Dónde está la mota?"
|
||||
## over the radio. Fires on a small random chance after a successful call.
|
||||
func _maybe_radio_easter_egg() -> void:
|
||||
|
||||
@@ -2,6 +2,8 @@ class_name ShiftReview
|
||||
extends CanvasLayer
|
||||
## End-of-shift review screen (PLAN.md: Evidence and Body Camera, Shift Review).
|
||||
|
||||
const Strings := preload("res://src/core/strings.gd")
|
||||
|
||||
signal restart_requested()
|
||||
|
||||
var report_label: Label
|
||||
@@ -49,6 +51,8 @@ func show_review(review: Dictionary, scenario: Resource, history: Array = []) ->
|
||||
var incidents: Dictionary = review.get("incidents", {})
|
||||
var crowd: Dictionary = review.get("crowd", {})
|
||||
var radio: Dictionary = review.get("radio", {})
|
||||
var outcomes: Array = review.get("responder_outcomes", [])
|
||||
var investigations: Array = review.get("investigations", [])
|
||||
var incident_list: Array = incidents.get("incidents", [])
|
||||
var peaceful := 0
|
||||
for inc in incident_list:
|
||||
@@ -78,6 +82,8 @@ func show_review(review: Dictionary, scenario: Resource, history: Array = []) ->
|
||||
"NPCs encountered: %d" % review.get("spawned_count", 0),
|
||||
"Radio battery: %.0f%% emergency cooldown: %.0fs" % [radio.get("battery", 0.0), radio.get("emergency_cooldown_s", 0.0)],
|
||||
"Crowd mood: %.0f%% (panic: %s)" % [crowd.get("mood", 0.0) * 100.0, str(crowd.get("panic", false))],
|
||||
Strings.fmt("review.responder", [", ".join(outcomes) if outcomes.size() > 0 else Strings.lookup("review.none")]),
|
||||
Strings.fmt("review.investigations", [" | ".join(investigations) if investigations.size() > 0 else Strings.lookup("review.none")]),
|
||||
"",
|
||||
"Highlights:",
|
||||
]
|
||||
|
||||
Reference in new issue
Block a user