class_name DialogueCoordinator ## Dialogue snapshot consumers and signal handlers extracted from main.gd (#775). ## ## Owns NPC identity tracking state (_last_dialogue_npc_id/name) shared between ## consuming dialogue snapshots and handling dialogue_box signals. ## Registered with SnapshotEventRouter; reads from GameState directly. var dialogue_box: Node = null var monologue_display: Node = null var journal_panel: Node = null # Shared mutable reference — GDScript arrays are reference types. # main.gd and this coordinator both append to the same array. var _pending_record_inputs: Array = [] var _last_dialogue_npc_id: int = -1 # D-064: NPC entity_id for WalkAway input var _last_dialogue_npc_name: String = "" # #535: NPC name for dialogue_response attribution var _last_dialogue_tick: int = -1 var _last_confrontation_tick: int = -1 func init(refs: Dictionary, pending_record_inputs: Array) -> DialogueCoordinator: dialogue_box = refs.get("dialogue_box") monologue_display = refs.get("monologue_display") journal_panel = refs.get("journal_panel") _pending_record_inputs = pending_record_inputs return self ## Connect dialogue_box signals. Call from main.gd._ready() after init(). func connect_signals() -> void: if not dialogue_box: return dialogue_box.option_selected.connect(on_dialogue_option_selected) dialogue_box.dialogue_dismissed.connect(on_dialogue_dismissed) dialogue_box.confrontation_monologue.connect(on_confrontation_monologue) dialogue_box.pause_requested.connect(on_dialogue_pause_requested) dialogue_box.unpause_requested.connect(on_dialogue_unpause_requested) # D-020 (#558): Decoupled signals — coordinator routes state changes. dialogue_box.dialogue_state_changed.connect(on_dialogue_state_changed) dialogue_box.audio_dip_requested.connect(on_audio_dip_requested) dialogue_box.audio_dip_cleared.connect(on_audio_dip_cleared) # -- Snapshot consumers (registered with SnapshotEventRouter) ----------------- # Consume-once per tick with ID tracking: show dialogue, then clear. func consume_dialogue() -> void: if GameState.current_dialogue == null or not dialogue_box: return if GameState.current_tick == _last_dialogue_tick: return if dialogue_box.is_dialogue_active(): GameState.current_dialogue = null return _last_dialogue_tick = GameState.current_tick # #264: Close journal when dialogue opens if journal_panel and journal_panel.has_method("close"): journal_panel.close() var dlg: Dictionary = GameState.current_dialogue _last_dialogue_npc_id = dlg.get("npc_entity_id", -1) _last_dialogue_npc_name = dlg.get("npc_name", "") dialogue_box.show_dialogue( dlg.get("npc_name", ""), dlg.get("speech", ""), dlg.get("options", []), _last_dialogue_npc_id ) GameState.current_dialogue = null # #535: Consume overheard NPC-NPC conversation events (D-078). func consume_conversation_events() -> void: if not dialogue_box: return for event in GameState.conversation_events: dialogue_box.append_conversation_event(event) GameState.conversation_events = [] # #535: Handle conversation_ended events. func consume_conversation_ended() -> void: if not dialogue_box: return for event in GameState.conversation_ended: dialogue_box.on_conversation_ended(event) GameState.conversation_ended = [] # #535: Consume dialogue_response — NPC follow-up line after player picks an option. func consume_dialogue_response() -> void: if GameState.dialogue_response == null or not dialogue_box: return var dr: Dictionary = GameState.dialogue_response var speaker_entity_id: int = dr.get("speaker_entity_id", _last_dialogue_npc_id) var speaker_color_index: int = dr.get("speaker_color_index", -1) var speaker_name: String = dr.get("speaker_name", _last_dialogue_npc_name) dialogue_box.update_entity_display(speaker_entity_id, speaker_name, speaker_color_index) dialogue_box.append_dialogue_response(speaker_name, dr.get("text", ""), speaker_entity_id) GameState.dialogue_response = null # -- Signal handlers ---------------------------------------------------------- # D-061: Handle dialogue option selection -> send to server func on_dialogue_option_selected(response_id: String, _text: String) -> void: ( SimBridge . send_input( { "action": InputMapper.Action.INTERACT, "timestamp_msec": Time.get_ticks_msec(), "action_data": { "target_entity_id": null, "verb": "DialogueResponse", "response_id": response_id, }, } ) ) # D-063: Confrontation beat monologue -> show on monologue display (layer 7) func on_confrontation_monologue(text: String, duration: float) -> void: if not monologue_display: return if GameState.current_tick == _last_confrontation_tick: return _last_confrontation_tick = GameState.current_tick monologue_display.show_monologue(text, duration, 3, true) # D-061: Auto-pause on dialogue open func on_dialogue_pause_requested() -> void: var input := {"action": InputMapper.Action.PAUSE, "timestamp_msec": Time.get_ticks_msec()} SimBridge.send_input(input) _pending_record_inputs.append(input) # D-061: Auto-unpause on dialogue close func on_dialogue_unpause_requested() -> void: var input := {"action": InputMapper.Action.UNPAUSE, "timestamp_msec": Time.get_ticks_msec()} SimBridge.send_input(input) _pending_record_inputs.append(input) # D-064: Handle walk-away -> send WalkAway{npc_id} to server func on_dialogue_dismissed() -> void: ( SimBridge . send_input( { "action": InputMapper.Action.INTERACT, "timestamp_msec": Time.get_ticks_msec(), "action_data": { "target_entity_id": _last_dialogue_npc_id if _last_dialogue_npc_id >= 0 else null, "verb": "WalkAway", }, } ) ) # D-020 (#558): Coordinator handles dialogue state changes. func on_dialogue_state_changed(active: bool) -> void: GameState.dialogue_active = active # D-020 (#558): Route audio dip requests. func on_audio_dip_requested(profile: String) -> void: AudioManager.apply_dip(profile) # D-020 (#558): Route audio dip clear. func on_audio_dip_cleared() -> void: AudioManager.clear_dip()