Files
2026-04-05 11:09:10 +02:00

182 lines
5.9 KiB
GDScript

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()