extends RefCounted ## Body-camera timeline: the review screen steps through recorded incident events ## (ADR-0005). No live 3D replay in this version. const EventTimeline := preload("res://src/game/ui/event_timeline.gd") const Strings := preload("res://src/core/strings.gd") const Coordinator := preload("res://src/core/shift_coordinator.gd") const Factory := preload("res://src/content/scenario_factory.gd") func _review() -> Dictionary: return { "started_at_ms": 1000, "incidents": { "open_count": 0, "incidents": [ { "id": "inc_a", "npc_id": "npc_a", "outcome": "peaceful", "events": [ {"type": "player_action", "action": "eye contact", "level": 1, "at_ms": 3000}, {"type": "body_camera", "on": true, "reason": "formal warning", "at_ms": 8000}, ], }, { "id": "inc_b", "npc_id": "npc_b", "outcome": "left", "events": [ {"type": "reason_requested", "reply": "I have a ticket.", "at_ms": 5000}, {"type": "access_check", "evidence": {"decided": true, "verified_granted": true}, "at_ms": 9000}, ], }, ], }, "reputation": {"rating": 70.0, "grade": "C", "modifiers": []}, "crowd": {"mood": 0.1, "panic": false}, "radio": {"battery": 100.0, "emergency_cooldown_s": 0.0}, } func test_flatten_orders_events_across_incidents() -> bool: var timeline: Node = EventTimeline.new() var flat: Array = timeline.flatten(_review()) var a := TestUtils.check_eq(flat.size(), 4, "every recorded event is flattened") var stamps: Array[int] = [] for row in flat: stamps.append(int(row.get("at_ms", 0))) var sorted := stamps.duplicate() sorted.sort() var b := TestUtils.check_eq(stamps, sorted, "events are time ordered") var c := TestUtils.check_eq(flat[0].get("npc_id", ""), "npc_a", "event keeps its NPC id") var d := TestUtils.check_eq(flat[1].get("npc_id", ""), "npc_b", "second incident interleaves") var e := TestUtils.check_eq(flat[0].get("incident_id", ""), "inc_a", "event keeps its incident id") timeline.free() return a and b and c and d and e func test_describe_covers_event_types() -> bool: var timeline: Node = EventTimeline.new() var a := TestUtils.check(timeline.describe({"type": "player_action", "action": "eye contact", "level": 1, "at_ms": 3000}).contains("eye contact (L1)"), "player action line") var b := TestUtils.check(timeline.describe({"type": "body_camera", "on": true, "reason": "formal warning"}).contains("body camera on"), "body camera line") var c := TestUtils.check(timeline.describe({"type": "reason_requested", "reply": "I have a ticket."}).contains("I have a ticket."), "reason reply line") var d := TestUtils.check(timeline.describe({"type": "access_check", "evidence": {"decided": false, "verified_granted": true}}).contains("deny"), "access decision line") var e := TestUtils.check(timeline.describe({"type": "responder_action", "call_type": "emergency", "outcome": "evacuation"}).contains("evacuation"), "responder line") var f := TestUtils.check(timeline.describe({"type": "investigation_opened", "reason": "unjustified"}).contains("investigation"), "investigation line") timeline.free() return a and b and c and d and e and f func test_empty_timeline_is_safe() -> bool: var timeline: Node = EventTimeline.new() var flat: Array = timeline.flatten({}) var a := TestUtils.check_eq(flat.size(), 0, "no incidents means no events") timeline.show_timeline({}) var b := TestUtils.check_eq(timeline.index, 0, "index stays at zero when empty") timeline.step(1) var c := TestUtils.check_eq(timeline.index, 0, "stepping an empty timeline does nothing") timeline.free() return a and b and c func test_stepping_clamps() -> bool: var timeline: Node = EventTimeline.new() timeline.show_timeline(_review()) timeline.step(-1) var a := TestUtils.check_eq(timeline.index, 0, "cannot step before the first event") timeline.step(1) var b := TestUtils.check_eq(timeline.index, 1, "next advances one event") timeline.step(99) var c := TestUtils.check_eq(timeline.index, 3, "cannot step past the last event") timeline.free() return a and b and c func test_review_records_a_shift_start_stamp() -> bool: var coordinator: Object = Coordinator.new() coordinator.setup(Factory.hollywood_building(), 30.0, 7) coordinator.start() var review: Dictionary = coordinator.build_review() var a := TestUtils.check(int(review.get("started_at_ms", 0)) > 0, "review carries the shift start stamp") coordinator = null return a func test_timeline_strings_exist() -> bool: var ok := true for key in ["timeline.title", "timeline.position", "timeline.empty", "timeline.prev", "timeline.next", "timeline.close", "timeline.hint", "review.timeline_button"]: if Strings.lookup(key) == key: ok = false return TestUtils.check(ok, "timeline UI strings are localized") func test_review_button_opens_the_timeline() -> bool: var Review := load("res://src/game/ui/shift_review.gd") var panel: Node = Review.new() var got := {"review": {}} panel.timeline_requested.connect(func(review): got["review"] = review) panel.show_review(_review(), Factory.hollywood_building(), []) var button: Button = panel.find_child("TimelineButton", true, false) as Button var a := TestUtils.check(button != null, "review screen has a timeline button") if button: button.pressed.emit() var b := TestUtils.check(not (got["review"] as Dictionary).is_empty(), "pressing it hands the review to the timeline") panel.free() return a and b