[gd_scene load_steps=9 format=3 uid="uid://73jm5qjy52vq"] [ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_a8ve6"] [sub_resource type="GDScript" id="GDScript_i0vys"] script/source = "class_name DialogueManagerExampleBalloon extends CanvasLayer ## A basic dialogue balloon for use with Dialogue Manager. ## 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\" ## The dialogue resource var resource: DialogueResource ## 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(next_dialogue_line): is_waiting_for_input = false balloon.focus_mode = Control.FOCUS_ALL balloon.grab_focus() # The dialogue has finished so close the balloon if not next_dialogue_line: queue_free() return # If the node isn't ready yet then none of the labels will be ready yet either if not is_node_ready(): await ready dialogue_line = next_dialogue_line 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.set_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 input if dialogue_line.responses.size() > 0: balloon.focus_mode = Control.FOCUS_NONE responses_menu.show() elif dialogue_line.time != \"\": var time = 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() get: return dialogue_line ## 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 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 func _unhandled_input(_event: InputEvent) -> void: # Only the balloon is allowed to handle input while it's showing 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 = dialogue_label.visible_ratio self.dialogue_line = await resource.get_next_dialogue_line(dialogue_line.id) if visible_ratio < 1: dialogue_label.skip_typing() ## Start some dialogue func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void: if not is_node_ready(): await ready temporary_game_states = [self] + extra_game_states is_waiting_for_input = false resource = dialogue_resource self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states) ## Go to the next line func next(next_id: String) -> void: self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states) #region Signals func _on_mutated(_mutation: Dictionary) -> void: is_waiting_for_input = false will_hide_balloon = true get_tree().create_timer(0.1).timeout.connect(func(): if will_hide_balloon: will_hide_balloon = false balloon.hide() ) 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 " [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_spyqn"] bg_color = Color(0, 0, 0, 1) border_width_left = 3 border_width_top = 3 border_width_right = 3 border_width_bottom = 3 border_color = Color(0.329412, 0.329412, 0.329412, 1) corner_radius_top_left = 5 corner_radius_top_right = 5 corner_radius_bottom_right = 5 corner_radius_bottom_left = 5 [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ri4m3"] bg_color = Color(0.121569, 0.121569, 0.121569, 1) border_width_left = 3 border_width_top = 3 border_width_right = 3 border_width_bottom = 3 border_color = Color(1, 1, 1, 1) corner_radius_top_left = 5 corner_radius_top_right = 5 corner_radius_bottom_right = 5 corner_radius_bottom_left = 5 [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e0njw"] bg_color = Color(0, 0, 0, 1) border_width_left = 3 border_width_top = 3 border_width_right = 3 border_width_bottom = 3 border_color = Color(0.6, 0.6, 0.6, 1) corner_radius_top_left = 5 corner_radius_top_right = 5 corner_radius_bottom_right = 5 corner_radius_bottom_left = 5 [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uy0d5"] bg_color = Color(0, 0, 0, 1) border_width_left = 3 border_width_top = 3 border_width_right = 3 border_width_bottom = 3 corner_radius_top_left = 5 corner_radius_top_right = 5 corner_radius_bottom_right = 5 corner_radius_bottom_left = 5 [sub_resource type="Theme" id="Theme_qq3yp"] default_font_size = 20 Button/styles/disabled = SubResource("StyleBoxFlat_spyqn") Button/styles/focus = SubResource("StyleBoxFlat_ri4m3") Button/styles/hover = SubResource("StyleBoxFlat_e0njw") Button/styles/normal = SubResource("StyleBoxFlat_e0njw") MarginContainer/constants/margin_bottom = 15 MarginContainer/constants/margin_left = 30 MarginContainer/constants/margin_right = 30 MarginContainer/constants/margin_top = 15 Panel/styles/panel = SubResource("StyleBoxFlat_uy0d5") [sub_resource type="GDScript" id="GDScript_v0j00"] script/source = "@icon(\"./assets/responses_menu.svg\") ## A [Container] for dialogue responses provided by [b]Dialogue Manager[/b]. class_name DialogueResponsesMenu extends Container ## Emitted when a response is selected. signal response_selected(response) ## Optionally specify a control to duplicate for each response @export var response_template: Control ## The action for accepting a response (is possibly overridden by parent dialogue balloon). @export var next_action: StringName = &\"\" ## The list of dialogue responses. var responses: Array = []: get: return responses set(value): responses = value # Remove any current items for item in get_children(): if item == response_template: continue remove_child(item) item.queue_free() # Add new items if responses.size() > 0: for response in responses: var item: Control if is_instance_valid(response_template): item = response_template.duplicate(DUPLICATE_GROUPS | DUPLICATE_SCRIPTS | DUPLICATE_SIGNALS) item.show() else: item = Button.new() item.name = \"Response%d\" % get_child_count() if not response.is_allowed: item.name = String(item.name) + \"Disallowed\" item.disabled = true # If the item has a response property then use that if \"response\" in item: item.response = response # Otherwise assume we can just set the text else: item.text = response.text item.set_meta(\"response\", response) add_child(item) _configure_focus() func _ready() -> void: visibility_changed.connect(func(): if visible and get_menu_items().size() > 0: get_menu_items()[0].grab_focus() ) if is_instance_valid(response_template): response_template.hide() ## Get the selectable items in the menu. func get_menu_items() -> Array: var items: Array = [] for child in get_children(): if not child.visible: continue if \"Disallowed\" in child.name: continue items.append(child) return items ## [b]DEPRECATED[/b]. Do not use. func set_responses(next_responses: Array) -> void: self.responses = next_responses #region Internal # Prepare the menu for keyboard and mouse navigation. func _configure_focus() -> void: var items = get_menu_items() for i in items.size(): var item: Control = items[i] item.focus_mode = Control.FOCUS_ALL item.focus_neighbor_left = item.get_path() item.focus_neighbor_right = item.get_path() if i == 0: item.focus_neighbor_top = item.get_path() item.focus_previous = item.get_path() else: item.focus_neighbor_top = items[i - 1].get_path() item.focus_previous = items[i - 1].get_path() if i == items.size() - 1: item.focus_neighbor_bottom = item.get_path() item.focus_next = item.get_path() else: item.focus_neighbor_bottom = items[i + 1].get_path() item.focus_next = items[i + 1].get_path() item.mouse_entered.connect(_on_response_mouse_entered.bind(item)) item.gui_input.connect(_on_response_gui_input.bind(item, item.get_meta(\"response\"))) items[0].grab_focus() #endregion #region Signals func _on_response_mouse_entered(item: Control) -> void: if \"Disallowed\" in item.name: return item.grab_focus() func _on_response_gui_input(event: InputEvent, item: Control, response) -> void: if \"Disallowed\" in item.name: return if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT: get_viewport().set_input_as_handled() response_selected.emit(response) elif event.is_action_pressed(&\"ui_accept\" if next_action.is_empty() else next_action) and item in get_menu_items(): get_viewport().set_input_as_handled() response_selected.emit(response) #endregion " [node name="ExampleBalloon" type="CanvasLayer"] layer = 100 script = SubResource("GDScript_i0vys") [node name="Balloon" type="Control" parent="."] unique_name_in_owner = true layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 theme = SubResource("Theme_qq3yp") [node name="Panel" type="Panel" parent="Balloon"] clip_children = 2 layout_mode = 0 offset_left = 10.0 offset_top = 820.0 offset_right = 1260.0 offset_bottom = 1070.0 mouse_filter = 1 [node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 [node name="VBoxContainer" type="VBoxContainer" parent="Balloon/Panel/Dialogue"] layout_mode = 2 [node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Panel/Dialogue/VBoxContainer"] unique_name_in_owner = true modulate = Color(1, 1, 1, 0.501961) layout_mode = 2 mouse_filter = 1 bbcode_enabled = true text = "Character" fit_content = true scroll_active = false [node name="DialogueLabel" parent="Balloon/Panel/Dialogue/VBoxContainer" instance=ExtResource("2_a8ve6")] unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 3 text = "Dialogue..." [node name="Responses" type="MarginContainer" parent="Balloon"] layout_mode = 1 anchors_preset = 7 anchor_left = 0.5 anchor_top = 1.0 anchor_right = 0.5 anchor_bottom = 1.0 offset_left = -147.0 offset_top = -558.0 offset_right = 494.0 offset_bottom = -154.0 grow_horizontal = 2 grow_vertical = 0 [node name="ResponsesMenu" type="VBoxContainer" parent="Balloon/Responses" node_paths=PackedStringArray("response_template")] unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 8 theme_override_constants/separation = 2 script = SubResource("GDScript_v0j00") response_template = NodePath("ResponseExample") [node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"] layout_mode = 2 text = "Response example" [connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"]