class_name IncidentRecorder extends RefCounted const EventBus := preload("res://src/core/event_bus.gd") var event_bus: EventBus = null func set_event_bus(bus: EventBus) -> void: event_bus = bus ## Structured incident history (ADR-0005). Records observations, player actions, ## access evidence, escalation levels, responses, and outcomes for evidence, ## reputation, crowd reactions, and shift review. var incidents: Array[Dictionary] = [] var _open_incidents: Dictionary = {} # npc_id -> incident dict ## Begin an incident for an NPC. Returns incident_id. func start_incident(npc_id: String, trigger: String) -> String: var incident := { "id": "inc_%s_%d" % [npc_id, Time.get_ticks_usec()], "npc_id": npc_id, "trigger": trigger, "opened_at_ms": Time.get_ticks_msec(), "events": [], "access_evidence": [], "escalation_history": [], "outcome": null, } _open_incidents[npc_id] = incident return incident["id"] func add_event(npc_id: String, event: Dictionary) -> void: if not _open_incidents.has(npc_id): return var incident: Dictionary = _open_incidents[npc_id] event["at_ms"] = Time.get_ticks_msec() incident["events"].append(event) if event_bus: event_bus.incident_recorded.emit(incident.duplicate(true)) func add_access_evidence(npc_id: String, evidence: Dictionary) -> void: if not _open_incidents.has(npc_id): return var incident: Dictionary = _open_incidents[npc_id] incident["access_evidence"].append(evidence) if event_bus: event_bus.access_decided.emit(evidence.duplicate(true)) incident["events"].append({ "type": "access_check", "at_ms": Time.get_ticks_msec(), "evidence": evidence, }) func add_escalation(npc_id: String, level: int, reason: String) -> void: if not _open_incidents.has(npc_id): return var incident: Dictionary = _open_incidents[npc_id] incident["escalation_history"].append({ "level": level, "reason": reason, "at_ms": Time.get_ticks_msec(), }) ## Close an incident. outcome: e.g. "peaceful", "escort_off_property", ## "assisted", "responded", "custody". Returns the closed incident. func close_incident(npc_id: String, outcome: String) -> Dictionary: if not _open_incidents.has(npc_id): return {} var incident: Dictionary = _open_incidents[npc_id] incident["outcome"] = outcome incident["closed_at_ms"] = Time.get_ticks_msec() incidents.append(incident) _open_incidents.erase(npc_id) if event_bus: event_bus.incident_recorded.emit(incident.duplicate(true)) return incident func open_incidents_count() -> int: return _open_incidents.size() ## True when an incident is already open for this NPC (avoids duplicating one ## incident every time the player re-focuses the same person). func has_open_incident(npc_id: String) -> bool: return _open_incidents.has(npc_id) ## Close every still-open incident at shift end so the review does not silently ## drop interactions the player never resolved. Returns how many were closed. func close_all_open(outcome: String = "unresolved") -> int: var ids: Array = _open_incidents.keys() for npc_id in ids: close_incident(str(npc_id), outcome) return ids.size() func to_dict() -> Dictionary: return { "incidents": incidents.duplicate(true), "open_count": _open_incidents.size(), }