213 lines
6.8 KiB
GDScript
213 lines
6.8 KiB
GDScript
|
extends CanvasLayer
|
||
|
|
||
|
|
||
|
@onready var balloon: ColorRect = $Balloon
|
||
|
@onready var margin: MarginContainer = $Balloon/Margin
|
||
|
@onready var character_label: RichTextLabel = $Balloon/Margin/VBox/CharacterLabel
|
||
|
@onready var dialogue_label := $Balloon/Margin/VBox/DialogueLabel
|
||
|
@onready var responses_menu: VBoxContainer = $Balloon/Margin/VBox/Responses
|
||
|
@onready var response_template: RichTextLabel = %ResponseTemplate
|
||
|
|
||
|
## 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
|
||
|
|
||
|
## The current line
|
||
|
var dialogue_line: DialogueLine:
|
||
|
set(next_dialogue_line):
|
||
|
is_waiting_for_input = false
|
||
|
|
||
|
if not next_dialogue_line:
|
||
|
queue_free()
|
||
|
return
|
||
|
|
||
|
# Remove any previous responses
|
||
|
for child in responses_menu.get_children():
|
||
|
responses_menu.remove_child(child)
|
||
|
child.queue_free()
|
||
|
|
||
|
dialogue_line = next_dialogue_line
|
||
|
|
||
|
character_label.visible = not dialogue_line.character.is_empty()
|
||
|
character_label.text = tr(dialogue_line.character, "dialogue")
|
||
|
|
||
|
dialogue_label.modulate.a = 0
|
||
|
dialogue_label.custom_minimum_size.x = dialogue_label.get_parent().size.x - 1
|
||
|
dialogue_label.dialogue_line = dialogue_line
|
||
|
|
||
|
# Show any responses we have
|
||
|
responses_menu.modulate.a = 0
|
||
|
if dialogue_line.responses.size() > 0:
|
||
|
for response in dialogue_line.responses:
|
||
|
# Duplicate the template so we can grab the fonts, sizing, etc
|
||
|
var item: RichTextLabel = response_template.duplicate(0)
|
||
|
item.name = "Response%d" % responses_menu.get_child_count()
|
||
|
if not response.is_allowed:
|
||
|
item.name = String(item.name) + "Disallowed"
|
||
|
item.modulate.a = 0.4
|
||
|
item.text = response.text
|
||
|
item.show()
|
||
|
responses_menu.add_child(item)
|
||
|
|
||
|
# Show our balloon
|
||
|
balloon.show()
|
||
|
will_hide_balloon = false
|
||
|
|
||
|
dialogue_label.modulate.a = 1
|
||
|
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:
|
||
|
responses_menu.modulate.a = 1
|
||
|
configure_menu()
|
||
|
elif dialogue_line.time != null:
|
||
|
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
|
||
|
|
||
|
|
||
|
func _ready() -> void:
|
||
|
response_template.hide()
|
||
|
balloon.hide()
|
||
|
balloon.custom_minimum_size.x = balloon.get_viewport_rect().size.x
|
||
|
|
||
|
Engine.get_singleton("DialogueManager").mutated.connect(_on_mutated)
|
||
|
|
||
|
|
||
|
func _unhandled_input(_event: InputEvent) -> void:
|
||
|
# Only the balloon is allowed to handle input while it's showing
|
||
|
get_viewport().set_input_as_handled()
|
||
|
|
||
|
|
||
|
## Start some dialogue
|
||
|
func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void:
|
||
|
temporary_game_states = 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)
|
||
|
|
||
|
|
||
|
### Helpers
|
||
|
|
||
|
|
||
|
# Set up keyboard movement and signals for the response menu
|
||
|
func configure_menu() -> void:
|
||
|
balloon.focus_mode = Control.FOCUS_NONE
|
||
|
|
||
|
var items = get_responses()
|
||
|
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))
|
||
|
|
||
|
items[0].grab_focus()
|
||
|
|
||
|
|
||
|
# Get a list of enabled items
|
||
|
func get_responses() -> Array:
|
||
|
var items: Array = []
|
||
|
for child in responses_menu.get_children():
|
||
|
if "Disallowed" in child.name: continue
|
||
|
items.append(child)
|
||
|
|
||
|
return items
|
||
|
|
||
|
|
||
|
func handle_resize() -> void:
|
||
|
if not is_instance_valid(margin):
|
||
|
call_deferred("handle_resize")
|
||
|
return
|
||
|
|
||
|
balloon.custom_minimum_size.y = margin.size.y
|
||
|
# Force a resize on only the height
|
||
|
balloon.size.y = 0
|
||
|
var viewport_size = balloon.get_viewport_rect().size
|
||
|
balloon.global_position = Vector2((viewport_size.x - balloon.size.x) * 0.5, viewport_size.y - balloon.size.y)
|
||
|
|
||
|
|
||
|
### 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_response_mouse_entered(item: Control) -> void:
|
||
|
if "Disallowed" in item.name: return
|
||
|
|
||
|
item.grab_focus()
|
||
|
|
||
|
|
||
|
func _on_response_gui_input(event: InputEvent, item: Control) -> void:
|
||
|
if "Disallowed" in item.name: return
|
||
|
|
||
|
if event is InputEventMouseButton and event.is_pressed() and event.button_index == 1:
|
||
|
next(dialogue_line.responses[item.get_index()].next_id)
|
||
|
elif event.is_action_pressed("ui_accept") and item in get_responses():
|
||
|
next(dialogue_line.responses[item.get_index()].next_id)
|
||
|
|
||
|
|
||
|
func _on_balloon_gui_input(event: InputEvent) -> void:
|
||
|
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 == 1:
|
||
|
next(dialogue_line.next_id)
|
||
|
elif event.is_action_pressed("ui_accept") and get_viewport().gui_get_focus_owner() == balloon:
|
||
|
next(dialogue_line.next_id)
|
||
|
|
||
|
|
||
|
func _on_margin_resized() -> void:
|
||
|
handle_resize()
|