xiandie/scene/dialog/balloon.gd

283 lines
9.5 KiB
GDScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends CanvasLayer
signal manually_skipped_line
@export var force_locale :String:
set(val):
force_locale = val
if force_locale:
TranslationServer.set_locale(force_locale)
@export var auto_play := true
## The action to use for advancing the dialogue
@export var next_action: StringName = &"interact"
## The action to use to skip typing the dialogue
@export var skip_action: StringName = &""
## 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(value):
if value:
dialogue_line = value
apply_dialogue_line()
else:
# The dialogue has finished so close the balloon
queue_free()
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
@onready var audio_stream_player: AudioStreamPlayer = $AudioStreamPlayer
func _ready() -> void:
layer = GlobalConfig.CANVAS_LAYER_DIALOG
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)
# 自定义获得文本,从 tags 中获取备注参数
func _setup_content_text() -> void:
var translation_key = dialogue_line.translation_key
var text
if translation_key:
text = tr(translation_key, "dialogue")
if text == translation_key:
text = dialogue_line.text
else:
text = dialogue_line.text
if dialogue_line.tags.has("shake"):
# eg. [shake rate=20 level=10][/shake]
text = "[shake rate=20 level=6]" + text + "[/shake]"
character_label.text = "[shake rate=20 level=6]" + character_label.text + "[/shake]"
if dialogue_line.tags.has("wave"):
# eg. [wave amp=25 freq=5][/wave]
text = "[wave amp=15 freq=5]" + text + "[/wave]"
character_label.text = "[wave amp=15 freq=5]" + character_label.text + "[/wave]"
if dialogue_line.tags.has("item"):
# orange color
text = "[color=orange]" + text + "[/color]"
dialogue_line.text = text
# 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:
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)
## Apply any changes to the balloon given a new [DialogueLine].
func apply_dialogue_line() -> void:
mutation_cooldown.stop()
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")
#主要角色颜色
var color = dialogue_line.get_tag_value("color")
if color:
character_label.modulate = Color.WHITE
character_label.text = "[color=" + color + "]" + character_label.text + "[/color]"
elif GlobalConfig.CHARACTER_COLOR_MAP.has(dialogue_line.character):
character_label.modulate = GlobalConfig.CHARACTER_COLOR_MAP[dialogue_line.character]
else:
character_label.modulate = GlobalConfig.CHARACTER_COLOR_MAP["default"]
# 配色结束后匿名化处理
if dialogue_line.tags.has("anonymous"):
character_label.text = tr("", "dialogue")
dialogue_label.hide()
_setup_content_text()
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
#sound
var initial_translation_key = dialogue_line.translation_key
var audio_time_len := 0.0
# 因为版权问题,有些 mp3 文件打不开,所以使用 ogg 格式
var audio_path = "res://asset/audio/peiyin/ogg/%s.ogg" % initial_translation_key
# var audio_path = "res://asset/audio/peiyin/%s.mp3" % initial_translation_key
if FileAccess.file_exists(audio_path):
var stream = load(audio_path)
audio_time_len = stream.get_length()
if audio_stream_player.stream != stream or not audio_stream_player.playing:
audio_stream_player.stream = stream
audio_stream_player.play()
elif audio_stream_player.playing:
audio_stream_player.stop()
if dialogue_label.is_typing:
await dialogue_label.finished_typing
if audio_stream_player.playing:
await audio_stream_player.finished
# # Wait for input
# if dialogue_line.responses.size() > 0:
# balloon.focus_mode = Control.FOCUS_NONE
# responses_menu.show()
# else:
is_waiting_for_input = true
balloon.focus_mode = Control.FOCUS_ALL
balloon.grab_focus()
# if dialogue_line.time != "":
# auto_play: 不管是否配置等待时长,都执行自动播放
if auto_play or dialogue_line.time:
var wait_time = 0.5
# 如果音频时长为 0则等待玩家读完文本否则音频播放结束后等待 0.5 秒
if not audio_time_len:
# 0.2 秒每个字符,最小 3 秒,最大 5 秒
wait_time = max(min(0.2 * dialogue_line.text.length(), 5.0), 3.0)
# 从 tags 中获取备注参数,覆盖默认等待时长
var wait = dialogue_line.get_tag_value("wait")
# eg. [#wait=2.5]
if wait:
wait_time = wait.to_float()
await get_tree().create_timer(wait_time).timeout
# 在 translation key 仍旧是当前 line 时跳转;如果不再是当前 line则不跳转
if dialogue_line.translation_key == initial_translation_key:
next(dialogue_line.next_id)
# var time = next_dialogue_line.text.length() * 0.2 if next_dialogue_line.time == "auto" else next_dialogue_line.time.to_float()
# await get_tree().create_timer(time).timeout
# # 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()
## 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_mutation_cooldown_timeout() -> void:
if will_hide_balloon:
will_hide_balloon = false
balloon.hide()
func _on_mutated(_mutation: Dictionary) -> void:
is_waiting_for_input = false
will_hide_balloon = true
mutation_cooldown.start(0.1)
func _on_balloon_gui_input(event: InputEvent) -> void:
# 根据全局配置,是否允许忽略输入
if temporary_game_states.has(GlobalConfig.DIALOG_IGNORE_INPUT):
return
# See if we need to skip typing of the dialogue
if dialogue_label.is_typing:
if event.is_action_pressed("interact") or event.is_action_pressed("cancel"):
dialogue_label.skip_typing()
get_viewport().set_input_as_handled()
return
# 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:
if event.is_action_pressed("interact") or event.is_action_pressed("cancel"):
# if event.is_action_pressed("interact") and get_viewport().gui_get_focus_owner() == balloon:
get_viewport().set_input_as_handled()
manually_skipped_line.emit()
next(dialogue_line.next_id)
func _on_responses_menu_response_selected(response: DialogueResponse) -> void:
next(response.next_id)
#endregion