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
@@ -0,0 +1,94 @@
|
||||
extends SceneTree
|
||||
## Emergency-aftermath smoke: places an unjustified emergency call, checks the
|
||||
## investigation note and reputation hit, forces the responder to arrive, and
|
||||
## verifies the evacuation outcome locks the door and clears the entrance.
|
||||
##
|
||||
## Usage:
|
||||
## godot --headless --path . --script res://src/test/smoke_emergency.gd
|
||||
## Exit code 0 = pass, 1 = failure.
|
||||
|
||||
const MAX_FRAMES := 900
|
||||
|
||||
var frame := 0
|
||||
var gm: Node = null
|
||||
var started := false
|
||||
var step := 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 the emergency aftermath ran")
|
||||
return true
|
||||
if frame < 200:
|
||||
return false
|
||||
if step == 0:
|
||||
if gm.npcs.is_empty():
|
||||
return false
|
||||
gm.call("_on_guard_target", gm.npcs[0])
|
||||
# A tourist: no weapon, low danger.
|
||||
gm.current_npc_truth = {"weapons_seen": false, "danger_level": 1}
|
||||
var rating_before: float = gm.coordinator.reputation.rating
|
||||
gm.call("_on_radio_submitted", {
|
||||
"npc_id": gm.current_incident_npc_id,
|
||||
"location": "entrance",
|
||||
"description": "tourist",
|
||||
"behavior": "standing",
|
||||
"weapons_seen": false,
|
||||
"danger_level": 1,
|
||||
}, true)
|
||||
_check(gm.coordinator.investigations.size() == 1, "unjustified emergency opens one investigation")
|
||||
_check(gm.coordinator.reputation.rating < rating_before, "unjustified emergency loses reputation")
|
||||
_check(gm.coordinator.radio.pending_calls.size() == 1, "the emergency call is pending")
|
||||
_check(not gm.door.locked, "the door is open before the responder arrives")
|
||||
step = 1
|
||||
if step == 1:
|
||||
# Fast-forward the arrival delay.
|
||||
gm.coordinator.radio.tick(30.0)
|
||||
_check(gm.coordinator.radio.pending_calls.is_empty(), "the responder arrived")
|
||||
_check(gm.coordinator.responder_outcomes.has("evacuation"), "low-danger emergency resolves as evacuation")
|
||||
_check(gm.door.locked, "evacuation locks the door")
|
||||
var clearing := 0
|
||||
for npc in gm.npcs:
|
||||
if is_instance_valid(npc) and (npc.reaction_mode == "flee" or npc.npc.leave_requested):
|
||||
clearing += 1
|
||||
_check(clearing > 0, "evacuation sends people away from the entrance (%d clearing)" % clearing)
|
||||
var review: Dictionary = gm.coordinator.build_review()
|
||||
_check((review.get("review", review).get("responder_outcomes", []) as Array).has("evacuation"),
|
||||
"the review carries the responder outcome")
|
||||
_check((review.get("investigations", []) as Array).size() == 1, "the review carries the investigation note")
|
||||
step = 2
|
||||
if step == 2:
|
||||
_finish()
|
||||
return true
|
||||
return false
|
||||
|
||||
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: emergency aftermath (investigation, evacuation, review)")
|
||||
quit(0)
|
||||
else:
|
||||
for message in failures:
|
||||
printerr("SMOKE FAIL: " + message)
|
||||
quit(1)
|
||||
@@ -0,0 +1,103 @@
|
||||
extends RefCounted
|
||||
## Emergency aftermath: responder outcomes reach the review, evacuation clears the
|
||||
## entrance and locks the door, and an unjustified call opens an investigation.
|
||||
|
||||
const Rules := preload("res://src/core/rules.gd")
|
||||
const RadioSystem := preload("res://src/core/radio_system.gd")
|
||||
const Coordinator := preload("res://src/core/shift_coordinator.gd")
|
||||
const Factory := preload("res://src/content/scenario_factory.gd")
|
||||
const Strings := preload("res://src/core/strings.gd")
|
||||
|
||||
func _coordinator() -> Object:
|
||||
var c := Coordinator.new()
|
||||
c.setup(Factory.hollywood_building(), 60.0, 4242)
|
||||
return c
|
||||
|
||||
func test_responder_outcomes_reach_the_review() -> bool:
|
||||
var c := _coordinator()
|
||||
c.responder_outcomes.append("arrest")
|
||||
c.responder_outcomes.append("command")
|
||||
var review: Dictionary = c.build_review()
|
||||
var outcomes: Array = review.get("responder_outcomes", [])
|
||||
var a := TestUtils.check_eq(outcomes, ["arrest", "command"], "responder outcomes are in the review")
|
||||
var b := TestUtils.check(review.has("investigations"), "review carries an investigations list")
|
||||
c = null
|
||||
return a and b
|
||||
|
||||
func test_outcomes_reset_with_a_new_shift() -> bool:
|
||||
var c := _coordinator()
|
||||
c.responder_outcomes.append("evacuation")
|
||||
c.investigations.append("note")
|
||||
c.setup(Factory.hollywood_building(), 60.0, 1)
|
||||
var a := TestUtils.check_eq(c.responder_outcomes.size(), 0, "outcomes reset on setup")
|
||||
var b := TestUtils.check_eq(c.investigations.size(), 0, "investigations reset on setup")
|
||||
c = null
|
||||
return a and b
|
||||
|
||||
func test_evacuation_is_the_emergency_default() -> bool:
|
||||
var call := {"type": "emergency", "report": {"danger_level": 2, "weapons_seen": false}}
|
||||
var a := TestUtils.check_eq(RadioSystem.responder_outcome(call), "restraint", "danger 2 restrains")
|
||||
var call2 := {"type": "emergency", "report": {"danger_level": 1, "weapons_seen": false}}
|
||||
var b := TestUtils.check_eq(RadioSystem.responder_outcome(call2), "evacuation", "low-danger emergency evacuates")
|
||||
var call3 := {"type": "assist", "report": {"danger_level": 1, "weapons_seen": false}}
|
||||
var c := TestUtils.check_eq(RadioSystem.responder_outcome(call3), "command", "assistance just commands")
|
||||
var call4 := {"type": "emergency", "report": {"danger_level": 3, "weapons_seen": true}}
|
||||
var d := TestUtils.check_eq(RadioSystem.responder_outcome(call4), "arrest", "weapons arrest")
|
||||
return a and b and c and d
|
||||
|
||||
func test_evacuation_wiring_locks_the_door_and_clears_npcs() -> bool:
|
||||
var src := FileAccess.get_file_as_string("res://src/game/game_manager.gd")
|
||||
var a := TestUtils.check(src.contains("func _evacuate_entrance"), "evacuation has a handler")
|
||||
var b := TestUtils.check(src.contains('if outcome == "evacuation":'), "arrival routes evacuation")
|
||||
var c := TestUtils.check(src.contains("door.set_locked(true)"), "evacuation locks the door")
|
||||
var d := TestUtils.check(src.contains("npc.start_flee(door_pos)"), "evacuation sends NPCs away")
|
||||
var e := TestUtils.check(src.contains("controller.leave_requested = true"), "evacuated NPCs are marked as leaving")
|
||||
return a and b and c and d and e
|
||||
|
||||
func test_unjustified_emergency_is_detected() -> bool:
|
||||
var src := FileAccess.get_file_as_string("res://src/game/game_manager.gd")
|
||||
var a := TestUtils.check(src.contains("func _is_unjustified_emergency"), "unjustified check exists")
|
||||
var b := TestUtils.check(src.contains("return not weapons and danger <= 1"), "no weapon and low danger is unjustified")
|
||||
var c := TestUtils.check(src.contains("coordinator.investigations.append"), "an investigation note is recorded")
|
||||
var d := TestUtils.check(src.contains('"type": "investigation_opened"'), "the incident log records the investigation")
|
||||
var e := TestUtils.check(src.contains('coordinator.reputation.apply("false_report"'), "the false-report hit applies")
|
||||
return a and b and c and d and e
|
||||
|
||||
func test_radio_arrival_reconnects_after_setup() -> bool:
|
||||
## coordinator.setup() builds a new RadioSystem, so the arrival handler must be
|
||||
## re-connected each shift or dispatched responders never resolve anything.
|
||||
var src := FileAccess.get_file_as_string("res://src/game/game_manager.gd")
|
||||
var a := TestUtils.check(src.contains("func _connect_radio"), "radio wiring has a helper")
|
||||
var b := TestUtils.check(src.contains("coordinator.radio.responder_arrived.connect(_on_responder_arrived)"),
|
||||
"the helper connects the arrival handler")
|
||||
var c := TestUtils.check(src.contains("_connect_radio()"), "shifts call the helper")
|
||||
return a and b and c
|
||||
|
||||
func test_review_strings_exist() -> bool:
|
||||
var a := TestUtils.check(Strings.lookup("review.responder") != "review.responder", "responder line exists")
|
||||
var b := TestUtils.check(Strings.lookup("review.investigations") != "review.investigations", "investigation line exists")
|
||||
var c := TestUtils.check(Strings.lookup("review.none") != "review.none", "empty placeholder exists")
|
||||
var d := TestUtils.check(Strings.lookup("responder.evacuated") != "responder.evacuated", "evacuation log line exists")
|
||||
var e := TestUtils.check(Strings.lookup("investigation.started") != "investigation.started", "investigation log line exists")
|
||||
return a and b and c and d and e
|
||||
|
||||
func test_review_renders_outcomes_and_investigations() -> bool:
|
||||
var Review := load("res://src/game/ui/shift_review.gd")
|
||||
var panel: Node = Review.new()
|
||||
var review := {
|
||||
"scenario_id": "hollywood_building",
|
||||
"reputation": {"rating": 61.0, "grade": "C", "modifiers": []},
|
||||
"incidents": {"incidents": []},
|
||||
"crowd": {"mood": 0.4, "panic": false},
|
||||
"radio": {"battery": 70.0, "emergency_cooldown_s": 0.0},
|
||||
"spawned_count": 3,
|
||||
"responder_outcomes": ["evacuation"],
|
||||
"investigations": ["Emergency call reviewed"],
|
||||
}
|
||||
panel.show_review(review, Factory.hollywood_building(), [])
|
||||
var text: String = panel.report_label.text
|
||||
var a := TestUtils.check(text.contains("evacuation"), "review names the responder outcome")
|
||||
var b := TestUtils.check(text.contains("Emergency call reviewed"), "review shows the investigation note")
|
||||
var c := TestUtils.check(text.contains("Investigations:"), "investigations line is labelled")
|
||||
panel.free()
|
||||
return a and b and c
|
||||
Reference in new issue
Block a user