extends CanvasLayer # 手动跳过当前行 signal manually_skipped_line(line_id: String) @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 _apply_tags() -> void: 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") 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]" var db_str = dialogue_line.get_tag_value("db") if db_str: var db = float(db_str) audio_stream_player.volume_db = db if GlobalConfig.DEBUG: print("audio_stream_player.volume_db = %s" % db) else: audio_stream_player.volume_db = 0 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() dialogue_label.hide() _apply_tags() 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_new/%s.wav" % initial_translation_key # 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: # 有 response 时标题字体大小调大 dialogue_label.set("theme_override_font_sizes/normal_font_size", 11.0) # 设置为橙色 dialogue_label.text = "[color=orange]" + dialogue_label.text + "[/color]" balloon.focus_mode = Control.FOCUS_NONE responses_menu.show() is_waiting_for_input = true balloon.focus_mode = Control.FOCUS_ALL balloon.grab_focus() return dialogue_label.set("theme_override_font_sizes/normal_font_size", null) 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 # 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 if dialogue_line.responses.size() > 0: 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 # 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(dialogue_line.next_id) next(dialogue_line.next_id) func _on_responses_menu_response_selected(response: DialogueResponse) -> void: next(response.next_id) #endregion