458 lines
13 KiB
Plaintext
458 lines
13 KiB
Plaintext
[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"]
|