Files
Dawn-Godot/addons/dialogue_manager/example_balloon/example_balloon.gd
T
2026-06-11 19:59:31 -05:00

218 lines
6.8 KiB
GDScript

class_name DialogueManagerExampleBalloon extends CanvasLayer
## A basic dialogue balloon for use with Dialogue Manager.
## The dialogue resource
@export var dialogue_resource: DialogueResource
## Start from a given title when using balloon as a [Node] in a scene.
@export var start_from_title: String = ""
## If running as a [Node] in a scene then auto start the dialogue.
@export var auto_start: bool = false
## If all other input is blocked as long as dialogue is shown.
@export var will_block_other_input: bool = true
## The action to use for advancing the dialogue
@export var next_action: StringName = &"ui_accept"
## The action to use to skip typing the dialogue
@export var skip_action: StringName = &"ui_cancel"
## A sound player for voice lines (if they exist).
@onready var audio_stream_player: AudioStreamPlayer = %AudioStreamPlayer
## Temporary game states
var temporary_game_states: Array = []
## See if we are waiting for the player
var is_waiting_for_input: bool = false
## See if we are running a long mutation and should hide the balloon
var will_hide_balloon: bool = false
## A dictionary to store any ephemeral variables
var locals: Dictionary = {}
var _locale: String = TranslationServer.get_locale()
## The current line
var dialogue_line: DialogueLine:
set(value):
if value:
dialogue_line = value
apply_dialogue_line()
else:
# The dialogue has finished so close the balloon
if owner == null:
queue_free()
else:
hide()
get:
return dialogue_line
## A cooldown timer for delaying the balloon hide when encountering a mutation.
var mutation_cooldown: Timer = Timer.new()
## The base balloon anchor
@onready var balloon: Control = %Balloon
## The label showing the name of the currently speaking character
@onready var character_label: RichTextLabel = %CharacterLabel
## The label showing the currently spoken dialogue
@onready var dialogue_label: DialogueLabel = %DialogueLabel
## The menu of responses
@onready var responses_menu: DialogueResponsesMenu = %ResponsesMenu
## Indicator to show that player can progress dialogue.
@onready var progress: Polygon2D = %Progress
func _ready() -> void:
balloon.hide()
Engine.get_singleton("DialogueManager").mutated.connect(_on_mutated)
# If the responses menu doesn't have a next action set, use this one
if responses_menu.next_action.is_empty():
responses_menu.next_action = next_action
mutation_cooldown.timeout.connect(_on_mutation_cooldown_timeout)
add_child(mutation_cooldown)
if auto_start:
if not is_instance_valid(dialogue_resource):
assert(false, DMConstants.get_error_message(DMConstants.ERR_MISSING_RESOURCE_FOR_AUTOSTART))
start()
func _process(delta: float) -> void:
if is_instance_valid(dialogue_line):
progress.visible = not dialogue_label.is_typing and dialogue_line.responses.size() == 0 and not dialogue_line.has_tag("voice")
func _unhandled_input(_event: InputEvent) -> void:
# Only the balloon is allowed to handle input while it's showing
if will_block_other_input:
get_viewport().set_input_as_handled()
func _notification(what: int) -> void:
## Detect a change of locale and update the current dialogue line to show the new language
if what == NOTIFICATION_TRANSLATION_CHANGED and _locale != TranslationServer.get_locale() and is_instance_valid(dialogue_label):
_locale = TranslationServer.get_locale()
var visible_ratio: float = dialogue_label.visible_ratio
dialogue_line = await dialogue_resource.get_next_dialogue_line(dialogue_line.id)
if visible_ratio < 1:
dialogue_label.skip_typing()
## Start some dialogue
func start(with_dialogue_resource: DialogueResource = null, title: String = "", extra_game_states: Array = []) -> void:
temporary_game_states = [self] + extra_game_states
is_waiting_for_input = false
if is_instance_valid(with_dialogue_resource):
dialogue_resource = with_dialogue_resource
if not title.is_empty():
start_from_title = title
dialogue_line = await dialogue_resource.get_next_dialogue_line(start_from_title, temporary_game_states)
show()
## Apply any changes to the balloon given a new [DialogueLine].
func apply_dialogue_line() -> void:
mutation_cooldown.stop()
progress.hide()
is_waiting_for_input = false
balloon.focus_mode = Control.FOCUS_ALL
balloon.grab_focus()
character_label.visible = not dialogue_line.character.is_empty()
character_label.text = tr(dialogue_line.character, "dialogue")
dialogue_label.hide()
dialogue_label.dialogue_line = dialogue_line
responses_menu.hide()
responses_menu.responses = dialogue_line.responses
# Show our balloon
balloon.show()
will_hide_balloon = false
dialogue_label.show()
if not dialogue_line.text.is_empty():
dialogue_label.type_out()
await dialogue_label.finished_typing
# Wait for next line
if dialogue_line.has_tag("voice"):
audio_stream_player.stream = load(dialogue_line.get_tag_value("voice"))
audio_stream_player.play()
await audio_stream_player.finished
next(dialogue_line.next_id)
elif dialogue_line.responses.size() > 0:
balloon.focus_mode = Control.FOCUS_NONE
responses_menu.show()
elif dialogue_line.time != "":
var time: float = dialogue_line.text.length() * 0.02 if dialogue_line.time == "auto" else dialogue_line.time.to_float()
await get_tree().create_timer(time).timeout
next(dialogue_line.next_id)
else:
is_waiting_for_input = true
balloon.focus_mode = Control.FOCUS_ALL
balloon.grab_focus()
## Go to the next line
func next(next_id: String) -> void:
dialogue_line = await dialogue_resource.get_next_dialogue_line(next_id, temporary_game_states)
#region Signals
func _on_mutation_cooldown_timeout() -> void:
if will_hide_balloon:
will_hide_balloon = false
balloon.hide()
func _on_mutated(mutation: Dictionary) -> void:
if not mutation.is_inline:
is_waiting_for_input = false
will_hide_balloon = true
mutation_cooldown.start(0.1)
func _on_balloon_gui_input(event: InputEvent) -> void:
# See if we need to skip typing of the dialogue
if dialogue_label.is_typing:
var mouse_was_clicked: bool = event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed()
var skip_button_was_pressed: bool = event.is_action_pressed(skip_action)
if mouse_was_clicked or skip_button_was_pressed:
get_viewport().set_input_as_handled()
dialogue_label.skip_typing()
return
if not is_waiting_for_input: return
if dialogue_line.responses.size() > 0: return
# When there are no response options the balloon itself is the clickable thing
get_viewport().set_input_as_handled()
if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT:
next(dialogue_line.next_id)
elif event.is_action_pressed(next_action) and get_viewport().gui_get_focus_owner() == balloon:
next(dialogue_line.next_id)
func _on_responses_menu_response_selected(response: DialogueResponse) -> void:
next(response.next_id)
#endregion