2024-12-23 01:31:10 +00:00
|
|
|
|
extends CanvasLayer
|
|
|
|
|
|
2025-06-13 08:03:19 +00:00
|
|
|
|
# 手动跳过当前行
|
|
|
|
|
signal manually_skipped_line(line_id: String)
|
2025-01-21 10:08:16 +00:00
|
|
|
|
|
2025-01-08 14:08:37 +00:00
|
|
|
|
@export var force_locale :String:
|
|
|
|
|
set(val):
|
|
|
|
|
force_locale = val
|
|
|
|
|
if force_locale:
|
|
|
|
|
TranslationServer.set_locale(force_locale)
|
|
|
|
|
|
|
|
|
|
@export var auto_play := true
|
|
|
|
|
|
2024-12-23 01:31:10 +00:00
|
|
|
|
## The action to use for advancing the dialogue
|
2025-01-08 00:51:09 +00:00
|
|
|
|
@export var next_action: StringName = &"interact"
|
2024-12-23 01:31:10 +00:00
|
|
|
|
|
|
|
|
|
## The action to use to skip typing the dialogue
|
2025-01-08 00:51:09 +00:00
|
|
|
|
@export var skip_action: StringName = &""
|
2024-12-23 01:31:10 +00:00
|
|
|
|
|
|
|
|
|
## 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
|
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
## A dictionary to store any ephemeral variables
|
|
|
|
|
var locals: Dictionary = {}
|
|
|
|
|
|
|
|
|
|
var _locale: String = TranslationServer.get_locale()
|
|
|
|
|
|
2024-12-23 01:31:10 +00:00
|
|
|
|
## The current line
|
|
|
|
|
var dialogue_line: DialogueLine:
|
2025-03-11 09:16:24 +00:00
|
|
|
|
set(value):
|
|
|
|
|
if value:
|
|
|
|
|
dialogue_line = value
|
|
|
|
|
apply_dialogue_line()
|
2024-12-23 01:31:10 +00:00
|
|
|
|
else:
|
2025-03-11 09:16:24 +00:00
|
|
|
|
# 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
|
2025-01-08 14:08:37 +00:00
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
## The label showing the name of the currently speaking character
|
|
|
|
|
@onready var character_label: RichTextLabel = %CharacterLabel
|
2025-01-08 14:08:37 +00:00
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
## The label showing the currently spoken dialogue
|
|
|
|
|
@onready var dialogue_label: DialogueLabel = %DialogueLabel
|
2025-01-08 14:08:37 +00:00
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
## The menu of responses
|
|
|
|
|
@onready var responses_menu: DialogueResponsesMenu = %ResponsesMenu
|
2024-12-23 01:31:10 +00:00
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
@onready var audio_stream_player: AudioStreamPlayer = $AudioStreamPlayer
|
2024-12-23 01:31:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
2025-03-20 10:00:53 +00:00
|
|
|
|
layer = GlobalConfig.CANVAS_LAYER_DIALOG
|
2024-12-23 01:31:10 +00:00
|
|
|
|
balloon.hide()
|
|
|
|
|
Engine.get_singleton("DialogueManager").mutated.connect(_on_mutated)
|
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
# 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
|
2024-12-23 01:31:10 +00:00
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
mutation_cooldown.timeout.connect(_on_mutation_cooldown_timeout)
|
|
|
|
|
add_child(mutation_cooldown)
|
2024-12-23 01:31:10 +00:00
|
|
|
|
|
2025-01-08 14:08:37 +00:00
|
|
|
|
# 自定义获得文本,从 tags 中获取备注参数
|
|
|
|
|
func _setup_content_text() -> void:
|
|
|
|
|
var translation_key = dialogue_line.translation_key
|
2025-01-20 13:45:47 +00:00
|
|
|
|
var text
|
|
|
|
|
if translation_key:
|
|
|
|
|
text = tr(translation_key, "dialogue")
|
|
|
|
|
if text == translation_key:
|
|
|
|
|
text = dialogue_line.text
|
|
|
|
|
else:
|
2025-01-08 14:08:37 +00:00
|
|
|
|
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
|
2025-03-11 09:16:24 +00:00
|
|
|
|
|
|
|
|
|
# func _unhandled_input(_event: InputEvent) -> void:
|
|
|
|
|
# # Only the balloon is allowed to handle input while it's showing
|
|
|
|
|
# get_viewport().set_input_as_handled()
|
|
|
|
|
|
2025-01-09 11:54:34 +00:00
|
|
|
|
|
2024-12-23 01:31:10 +00:00
|
|
|
|
func _notification(what: int) -> void:
|
2025-03-11 09:16:24 +00:00
|
|
|
|
## 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()
|
2024-12-23 01:31:10 +00:00
|
|
|
|
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()
|
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
|
2024-12-23 01:31:10 +00:00
|
|
|
|
## Start some dialogue
|
|
|
|
|
func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void:
|
2025-03-11 09:16:24 +00:00
|
|
|
|
temporary_game_states = [self] + extra_game_states
|
2024-12-23 01:31:10 +00:00
|
|
|
|
is_waiting_for_input = false
|
|
|
|
|
resource = dialogue_resource
|
|
|
|
|
self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states)
|
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
|
|
|
|
|
## 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 格式
|
2025-06-06 16:19:37 +00:00
|
|
|
|
var audio_path = "res://asset/audio/peiyin_new/%s.wav" % initial_translation_key
|
|
|
|
|
# var audio_path = "res://asset/audio/peiyin/ogg/%s.ogg" % initial_translation_key
|
2025-03-11 09:16:24 +00:00
|
|
|
|
# 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
|
2025-06-14 08:46:32 +00:00
|
|
|
|
|
|
|
|
|
# Wait for input
|
|
|
|
|
if dialogue_line.responses.size() > 0:
|
2025-06-15 05:11:41 +00:00
|
|
|
|
# 有 response 时标题字体大小调大
|
|
|
|
|
dialogue_label.set("theme_override_font_sizes/normal_font_size", 11.0)
|
|
|
|
|
# 设置为橙色
|
|
|
|
|
dialogue_label.text = "[color=orange]" + dialogue_label.text + "[/color]"
|
2025-06-14 08:46:32 +00:00
|
|
|
|
balloon.focus_mode = Control.FOCUS_NONE
|
|
|
|
|
responses_menu.show()
|
|
|
|
|
is_waiting_for_input = true
|
|
|
|
|
balloon.focus_mode = Control.FOCUS_ALL
|
|
|
|
|
balloon.grab_focus()
|
|
|
|
|
return
|
2025-06-15 05:11:41 +00:00
|
|
|
|
dialogue_label.set("theme_override_font_sizes/normal_font_size", null)
|
2025-06-14 08:46:32 +00:00
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
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)
|
2025-06-14 08:46:32 +00:00
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
# 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()
|
|
|
|
|
|
|
|
|
|
|
2024-12-23 01:31:10 +00:00
|
|
|
|
## 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)
|
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
|
2024-12-23 01:31:10 +00:00
|
|
|
|
#region Signals
|
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
|
|
|
|
|
func _on_mutation_cooldown_timeout() -> void:
|
|
|
|
|
if will_hide_balloon:
|
|
|
|
|
will_hide_balloon = false
|
|
|
|
|
balloon.hide()
|
|
|
|
|
|
|
|
|
|
|
2024-12-23 01:31:10 +00:00
|
|
|
|
func _on_mutated(_mutation: Dictionary) -> void:
|
|
|
|
|
is_waiting_for_input = false
|
|
|
|
|
will_hide_balloon = true
|
2025-03-11 09:16:24 +00:00
|
|
|
|
mutation_cooldown.start(0.1)
|
2025-01-21 10:08:16 +00:00
|
|
|
|
|
|
|
|
|
|
2024-12-23 01:31:10 +00:00
|
|
|
|
func _on_balloon_gui_input(event: InputEvent) -> void:
|
2025-01-24 14:19:17 +00:00
|
|
|
|
# 根据全局配置,是否允许忽略输入
|
|
|
|
|
if temporary_game_states.has(GlobalConfig.DIALOG_IGNORE_INPUT):
|
|
|
|
|
return
|
|
|
|
|
|
2025-06-14 08:46:32 +00:00
|
|
|
|
if dialogue_line.responses.size() > 0: return
|
|
|
|
|
|
2024-12-23 01:31:10 +00:00
|
|
|
|
# See if we need to skip typing of the dialogue
|
|
|
|
|
if dialogue_label.is_typing:
|
2025-01-21 10:08:16 +00:00
|
|
|
|
if event.is_action_pressed("interact") or event.is_action_pressed("cancel"):
|
2024-12-23 01:31:10 +00:00
|
|
|
|
dialogue_label.skip_typing()
|
2025-01-08 00:51:09 +00:00
|
|
|
|
get_viewport().set_input_as_handled()
|
2024-12-23 01:31:10 +00:00
|
|
|
|
return
|
2025-03-11 09:16:24 +00:00
|
|
|
|
# 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
|
2024-12-23 01:31:10 +00:00
|
|
|
|
|
|
|
|
|
# When there are no response options the balloon itself is the clickable thing
|
2025-01-21 10:08:16 +00:00
|
|
|
|
# get_viewport().set_input_as_handled()
|
2024-12-23 01:31:10 +00:00
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
# 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:
|
2025-01-21 10:08:16 +00:00
|
|
|
|
if event.is_action_pressed("interact") or event.is_action_pressed("cancel"):
|
2025-01-08 00:51:09 +00:00
|
|
|
|
# if event.is_action_pressed("interact") and get_viewport().gui_get_focus_owner() == balloon:
|
2025-01-21 10:08:16 +00:00
|
|
|
|
get_viewport().set_input_as_handled()
|
2025-06-13 08:03:19 +00:00
|
|
|
|
manually_skipped_line.emit(dialogue_line.next_id)
|
2024-12-23 01:31:10 +00:00
|
|
|
|
next(dialogue_line.next_id)
|
2025-01-08 14:08:37 +00:00
|
|
|
|
|
2025-03-11 09:16:24 +00:00
|
|
|
|
|
|
|
|
|
func _on_responses_menu_response_selected(response: DialogueResponse) -> void:
|
|
|
|
|
next(response.next_id)
|
|
|
|
|
|
2024-12-23 01:31:10 +00:00
|
|
|
|
|
|
|
|
|
#endregion
|