diff --git a/addons/dialogue_manager/dialogue_manager.gd b/addons/dialogue_manager/dialogue_manager.gd index ab9a17ed..537e4a0e 100644 --- a/addons/dialogue_manager/dialogue_manager.gd +++ b/addons/dialogue_manager/dialogue_manager.gd @@ -444,7 +444,8 @@ func show_dialogue_balloon_scene(balloon_scene, resource: DialogueResource, titl # Call "start" on the given balloon. func _start_balloon(balloon: Node, resource: DialogueResource, title: String, extra_game_states: Array) -> void: - get_current_scene.call().add_child(balloon) + if balloon.get_parent() == null: + get_current_scene.call().add_child(balloon) if balloon.has_method(&"start"): balloon.start(resource, title, extra_game_states) diff --git a/asset/art/gif/c01_公寓外街道/frames.tres b/asset/art/gif/c01_公寓外街道/frames.tres index 6b4e48b1..4cdc2eba 100644 --- a/asset/art/gif/c01_公寓外街道/frames.tres +++ b/asset/art/gif/c01_公寓外街道/frames.tres @@ -10,22 +10,22 @@ [resource] animations = [{ "frames": [{ -"duration": 3.0, +"duration": 6.0, "texture": ExtResource("1_i8sqx") }, { -"duration": 3.0, +"duration": 6.0, "texture": ExtResource("2_q01yr") }, { -"duration": 3.0, +"duration": 6.0, "texture": ExtResource("3_l6n6k") }, { -"duration": 3.0, +"duration": 6.0, "texture": ExtResource("4_0y176") }, { -"duration": 3.0, +"duration": 6.0, "texture": ExtResource("5_bmxf1") }, { -"duration": 3.0, +"duration": 6.0, "texture": ExtResource("6_jlcn1") }], "loop": true, diff --git a/manager/archive_manager/archive_manager.gd b/manager/archive_manager/archive_manager.gd index 052033f3..748b5f3a 100644 --- a/manager/archive_manager/archive_manager.gd +++ b/manager/archive_manager/archive_manager.gd @@ -135,24 +135,39 @@ func _check_dirs_and_archives() -> bool: # id = -1 means create a new archive, otherwise create an archive with the given id func create_and_use_new_archive(id := -1) -> void: _check_dirs_and_archives() - archive = AssembledArchive.new() var archive_path = _get_archive_path(id) if id < 0: + # 如果 id 小于 0,找到一个新的 id,创建新存档 id = 0 # find a new id archive_path = _get_archive_path(id) while FileAccess.file_exists(archive_path): id += 1 archive_path = _get_archive_path(id) - archive.resource_path = archive_path - archive.archive_id = id - archive.created_time = Time.get_datetime_string_from_system(false, true) - ResourceSaver.save(archive, archive_path) + _create_and_save_new_archive_resoure(id) + else: + # 如果 id 大于等于 0,创建指定 id 的存档 + if FileAccess.file_exists(archive_path): + _create_and_save_new_archive_resoure(id, true) + else: + _create_and_save_new_archive_resoure(id) archives[id] = archive # this will auto trigger signal and load the new archive GlobalConfigManager.config.current_selected_archive_id = id +func _create_and_save_new_archive_resoure(id, take_over_path = false) -> void: + var archive_path = _get_archive_path(id) + archive = AssembledArchive.new() as Resource + if take_over_path: + archive.take_over_path(archive_path) + else: + archive.resource_path = archive_path + archive.archive_id = id + archive.created_time = Time.get_datetime_string_from_system(false, true) + ResourceSaver.save(archive, archive_path) + + # 超过 999 个存档会出问题;不过这个游戏不会有这么多存档 func _get_archive_path(id: int) -> String: var id_str := "" diff --git a/manager/audio_manager/audio_manager.gd b/manager/audio_manager/audio_manager.gd index 1606d50a..2476b1af 100644 --- a/manager/audio_manager/audio_manager.gd +++ b/manager/audio_manager/audio_manager.gd @@ -2,6 +2,7 @@ extends Node var sfx_players = [] as Array[AudioStreamPlayer] var idx = 0 +var bgm_dict = {} func _ready() -> void: @@ -12,13 +13,37 @@ func _ready() -> void: add_child(sfx_player) -func play_animation_sound(animation): - #TODO - print("Playing sound for animation: ", animation) - - func play_sfx(sfx: AudioStream, db := 1.0) -> void: sfx_players[idx].stream = sfx sfx_players[idx].volume_db = db sfx_players[idx].play() idx = wrapi(idx + 1, 0, 5) + + +# 挂载并循环播放 bgm 音效 +func loop_bgm_music(music_name: StringName, stream: AudioStream, db := 0.0, loop := true) -> void: + var audio_player = bgm_dict.get(music_name) as AudioStreamPlayer + if audio_player: + bgm_dict.erase(music_name) + audio_player.queue_free() + audio_player.stop() + audio_player = AudioStreamPlayer.new() + add_child(audio_player) + bgm_dict[music_name] = audio_player + audio_player.stream = stream + audio_player.volume_db = db + audio_player.bus = "game_music" + audio_player.play() + if loop and stream.get_length() > 0: + audio_player.finished.connect(audio_player.play) + + +func stop_bgm_music(music_name: StringName) -> void: + var audio_player = bgm_dict.get(music_name) as AudioStreamPlayer + if audio_player: + audio_player.stop() + remove_child(audio_player) + audio_player.queue_free() + bgm_dict.erase(music_name) + else: + print("music bgm not found: ", music_name) diff --git a/manager/scene/scene_manager.gd b/manager/scene/scene_manager.gd index a36f4a44..5b5458d8 100644 --- a/manager/scene/scene_manager.gd +++ b/manager/scene/scene_manager.gd @@ -116,7 +116,6 @@ func set_player_boundary(rect: Rect2) -> void: var balloon_node - func pop_debug_dialog_info(character: String, content: String): if GlobalConfig.DEBUG: if not is_instance_valid(balloon_node): diff --git a/scene/dialog/balloon.gd b/scene/dialog/balloon.gd index c0f0539c..b00c2529 100755 --- a/scene/dialog/balloon.gd +++ b/scene/dialog/balloon.gd @@ -15,12 +15,6 @@ signal manually_skipped_line ## The action to use to skip typing the dialogue @export var skip_action: StringName = &"" -@onready var audio_stream_player: AudioStreamPlayer = $AudioStreamPlayer - -@onready var balloon: Control = %Balloon -@onready var character_label: RichTextLabel = %CharacterLabel -@onready var dialogue_label: DialogueLabel = %DialogueLabel -# @onready var responses_menu: DialogueResponsesMenu = %ResponsesMenu ## The dialogue resource var resource: DialogueResource @@ -34,110 +28,51 @@ 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") - #主要角色颜色 - 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] + set(value): + if value: + dialogue_line = value + apply_dialogue_line() else: - character_label.modulate = GlobalConfig.CHARACTER_COLOR_MAP["default"] - # 配色结束后匿名化处理 - if dialogue_line.tags.has("anonymous"): - character_label.text = tr("???", "dialogue") - - #print(dialogue_line.character,character_label.modulate) - - dialogue_label.hide() - _setup_content_text() - 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() - - #sound - var audio_time_len := 0.0 - # 因为版权问题,有些 mp3 文件打不开,所以使用 ogg 格式 - var audio_path = "res://asset/audio/peiyin/ogg/%s.ogg" % dialogue_line.translation_key - # var audio_path = "res://asset/audio/peiyin/%s.mp3" % dialogue_line.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 next_dialogue_line.time: - # 如果音频时长为 0,则等待玩家读完文本;否则音频播放结束后等待 0.5 秒 - var wait_time = 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 = next_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 - # 如果不再是当前 line,则不跳转 - if dialogue_line.translation_key == next_dialogue_line.translation_key: - next(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 - # 如果当前 line 运行结束,则 queue free 释放资源 - if dialogue_line == next_dialogue_line: + # 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 + # 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: @@ -161,44 +96,148 @@ func _setup_content_text() -> void: # 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 is_instance_valid(dialogue_label): + ## 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 + 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 - get_tree().create_timer(0.1).timeout.connect(func(): - if will_hide_balloon: - will_hide_balloon = false - balloon.hide() - ) - - -# func _unhandled_input(_event: InputEvent) -> void: -# # Only the balloon is allowed to handle input while it's showing -# get_viewport().set_input_as_handled() + mutation_cooldown.start(0.1) func _on_balloon_gui_input(event: InputEvent) -> void: @@ -212,25 +251,31 @@ func _on_balloon_gui_input(event: InputEvent) -> void: 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 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) - + # 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) + +func _on_responses_menu_response_selected(response: DialogueResponse) -> void: + next(response.next_id) + #endregion diff --git a/scene/dialog/balloon_debug.gd b/scene/dialog/balloon_debug.gd index 947ada83..cf3a6bbf 100644 --- a/scene/dialog/balloon_debug.gd +++ b/scene/dialog/balloon_debug.gd @@ -13,11 +13,6 @@ extends CanvasLayer ## The action to use to skip typing the dialogue @export var skip_action: StringName = &"" -@onready var audio_stream_player: AudioStreamPlayer = $AudioStreamPlayer - -@onready var balloon: Control = %Balloon -@onready var character_label: RichTextLabel = %CharacterLabel -@onready var dialogue_label: DialogueLabel = %DialogueLabel ## The dialogue resource var resource: DialogueResource @@ -31,65 +26,39 @@ 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 -const CHARACTER_COLOR_MAP = { - "音效": Color.DARK_VIOLET, - "美术": Color.WHITE_SMOKE, - "default": Color.LIGHT_SALMON, -} ## 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") - #主要角色颜色 - if CHARACTER_COLOR_MAP.has(dialogue_line.character): - character_label.modulate = CHARACTER_COLOR_MAP[dialogue_line.character] + set(value): + if value: + dialogue_line = value + apply_dialogue_line() else: - character_label.modulate = CHARACTER_COLOR_MAP["default"] + # The dialogue has finished so close the balloon + queue_free() + get: + return dialogue_line - #print(dialogue_line.character,character_label.modulate) +## A cooldown timer for delaying the balloon hide when encountering a mutation. +var mutation_cooldown: Timer = Timer.new() - dialogue_label.hide() - _setup_content_text() - dialogue_label.dialogue_line = dialogue_line +## The base balloon anchor +@onready var balloon: Control = %Balloon - # Show our balloons - balloon.show() - will_hide_balloon = false +## The label showing the name of the currently speaking character +@onready var character_label: RichTextLabel = %CharacterLabel - dialogue_label.show() - if not dialogue_line.text.is_empty(): - dialogue_label.type_out() +## The label showing the currently spoken dialogue +@onready var dialogue_label: DialogueLabel = %DialogueLabel - is_waiting_for_input = true - balloon.focus_mode = Control.FOCUS_ALL - balloon.grab_focus() - await get_tree().create_timer(2.0).timeout - # debug line 不需要下一行,直接释放(避免触发 dialogue_ended 信号) - queue_free() - # next(next_dialogue_line.next_id) - - # # 如果当前 line 运行结束,则 queue free 释放资源 - # if dialogue_line == next_dialogue_line: - # queue_free() +@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) + mutation_cooldown.timeout.connect(_on_mutation_cooldown_timeout) + add_child(mutation_cooldown) # 自定义获得文本,从 tags 中获取备注参数 func _setup_content_text() -> void: @@ -113,63 +82,126 @@ func _setup_content_text() -> void: # orange color text = "[color=orange]" + text + "[/color]" dialogue_line.text = text - - -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 is_instance_valid(dialogue_label): - 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() - ) - -# debug balloon 无需接管输入事件 # 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 _on_balloon_gui_input(event: InputEvent) -> void: -# # 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"): +# 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() -# get_viewport().set_input_as_handled() -# return -# #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) -# 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) -#endregion +## 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) + + +const CHARACTER_COLOR_MAP = { + "音效": Color.DARK_VIOLET, + "美术": Color.WHITE_SMOKE, + "default": Color.LIGHT_SALMON, +} + +## 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") + #主要角色颜色 + if CHARACTER_COLOR_MAP.has(dialogue_line.character): + character_label.modulate = CHARACTER_COLOR_MAP[dialogue_line.character] + else: + character_label.modulate = CHARACTER_COLOR_MAP["default"] + + dialogue_label.hide() + _setup_content_text() + dialogue_label.dialogue_line = dialogue_line + + # Show our balloon + balloon.show() + will_hide_balloon = false + + dialogue_label.show() + if not dialogue_line.text.is_empty(): + dialogue_label.type_out() + + # # 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() + await get_tree().create_timer(2.0).timeout + # debug line 不需要下一行,直接释放(避免触发 dialogue_ended 信号) + queue_free() + + +## 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) diff --git a/scene/entity/interactable.gd b/scene/entity/interactable.gd index 5ea5c328..a07be38a 100644 --- a/scene/entity/interactable.gd +++ b/scene/entity/interactable.gd @@ -61,8 +61,7 @@ func _reset(_body = null) -> void: func _reset_sign_testure_to_prop(): var key = SceneManager.get_current_prop(false) - if key: - _set_sign_texture_to_prop(key) + _set_sign_texture_to_prop(key) # 根据当前 prop,调整 sign 所显示的 texture diff --git a/scene/ground/scene/c01/s05_院长房间.tscn b/scene/ground/scene/c01/s05_院长房间.tscn index 8d2c83e0..d30bf2da 100644 --- a/scene/ground/scene/c01/s05_院长房间.tscn +++ b/scene/ground/scene/c01/s05_院长房间.tscn @@ -574,7 +574,7 @@ scene_name = "c01_s05" [node name="AnimationPlayer" parent="Ground" index="0"] libraries = { -"": SubResource("AnimationLibrary_ifimj") +&"": SubResource("AnimationLibrary_ifimj") } script = ExtResource("2_j5oim") data = { @@ -595,7 +595,6 @@ position = Vector2(27, 3) position = Vector2(503, 11) texture = ExtResource("4_gdhoy") enabled = false -immediately = false target_scene = "c01_s06" target_portal = "left" default_texture = ExtResource("4_gdhoy") @@ -855,10 +854,10 @@ bus = &"game_sfx" script = ExtResource("23_o1482") file = "门锁互动.mp3" -[node name="参考" type="Sprite2D" parent="Ground"] +[node name="参考" type="Sprite2D" parent="."] visible = false modulate = Color(1, 1, 1, 0.219608) -position = Vector2(281.5, 3.3) +position = Vector2(282.5, 3.3) scale = Vector2(0.333, 0.333) texture = ExtResource("3_7u4bh") diff --git a/scene/ground/scene/c01/s09_公寓楼外.tscn b/scene/ground/scene/c01/s09_公寓楼外.tscn index f6e5a5ea..112d6057 100644 --- a/scene/ground/scene/c01/s09_公寓楼外.tscn +++ b/scene/ground/scene/c01/s09_公寓楼外.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=15 format=3 uid="uid://c777lv8mjojcw"] +[gd_scene load_steps=16 format=3 uid="uid://c777lv8mjojcw"] [ext_resource type="PackedScene" uid="uid://dayyx4jerj7io" path="res://scene/ground/ground.tscn" id="1_mrwu5"] [ext_resource type="Script" uid="uid://c7rvvsuf18ykn" path="res://scene/ground/scene/c01/s09_animation.gd" id="2_u053j"] @@ -9,6 +9,7 @@ [ext_resource type="PackedScene" uid="uid://bicuc35kbn8hd" path="res://scene/shading/fog.tscn" id="7_wrhtk"] [ext_resource type="Shader" uid="uid://bcfnbll451i2r" path="res://asset/shader/fog.gdshader" id="8_0ohlv"] [ext_resource type="Texture2D" uid="uid://cs56isj1je50a" path="res://asset/art/scene/c01/s07_书店外/fog_mask.png" id="9_j34rt"] +[ext_resource type="SpriteFrames" uid="uid://c2peyi2l65h47" path="res://asset/art/gif/c01_公寓外街道/frames.tres" id="10_0ohlv"] [sub_resource type="LabelSettings" id="LabelSettings_7adaf"] font_size = 20 @@ -78,7 +79,14 @@ one_shot = false freeze_time = 0.1 hook_method = "player_been_catched" -[node name="Fog" parent="Ground/DeployLayer" index="6" instance=ExtResource("7_wrhtk")] +[node name="书店老板害怕" type="AnimatedSprite2D" parent="Ground/DeployLayer" index="6"] +position = Vector2(507, 0) +sprite_frames = ExtResource("10_0ohlv") +animation = &"书店老板害怕" +autoplay = "书店老板害怕" + +[node name="Fog" parent="Ground/DeployLayer" index="7" instance=ExtResource("7_wrhtk")] +visible = false z_index = 10 material = SubResource("ShaderMaterial_828bq") position = Vector2(399, -49) diff --git a/scene/ground/scene/c01/s10_animation.gd b/scene/ground/scene/c01/s10_animation.gd index faaf1353..618bf0bc 100644 --- a/scene/ground/scene/c01/s10_animation.gd +++ b/scene/ground/scene/c01/s10_animation.gd @@ -12,7 +12,6 @@ func _ready() -> void: if Engine.is_editor_hint(): return - func _on_ground_ready() -> void: # 不显示玩家,锁定玩家移动 SceneManager.freeze_player(0) @@ -24,4 +23,4 @@ func _on_ground_ready() -> void: func _on_animation_finished(): # 鬼差眼睛 var ghost = $"../S10鬼差探头" - ghost.play() \ No newline at end of file + ghost.play() diff --git a/scene/ground/scene/c01/s11_animation.gd b/scene/ground/scene/c01/s11_animation.gd index 7f044cf3..13d27e90 100644 --- a/scene/ground/scene/c01/s11_animation.gd +++ b/scene/ground/scene/c01/s11_animation.gd @@ -33,7 +33,8 @@ func _on_ground_ready() -> void: chapter_sfx = $chapter_sfx SceneManager.get_camera_marker().limit_bottom = 158 SceneManager.get_camera_marker().limit_top = -158 - # play("intro") + + play("intro") # ## test 测试最后运镜 ## # main_character.global_position.x = target_x diff --git a/scene/ground/scene/c01/s11_黄包车演出.tscn b/scene/ground/scene/c01/s11_黄包车演出.tscn index 178dd7da..374b73a5 100644 --- a/scene/ground/scene/c01/s11_黄包车演出.tscn +++ b/scene/ground/scene/c01/s11_黄包车演出.tscn @@ -126,7 +126,7 @@ player_y = 40 [node name="AnimationPlayer" parent="Ground" index="0"] libraries = { -"": SubResource("AnimationLibrary_6ojod") +&"": SubResource("AnimationLibrary_6ojod") } script = ExtResource("2_espm6") oneshot_animation = "" @@ -146,9 +146,9 @@ dir = "c01" file = "序章标题出现.wav" [node name="BGSprite2D" parent="Ground" index="2"] -position = Vector2(0, 6) +position = Vector2(-12, 2) texture = ExtResource("6_vjki6") -offset = Vector2(0, -640) +offset = Vector2(0, -646) [node name="portal_left" parent="Ground/DeployLayer" index="0"] position = Vector2(95, 40) @@ -157,7 +157,7 @@ position = Vector2(95, 40) target_scene = "c02_s01" [node name="车夫与吕萍" type="AnimatedSprite2D" parent="Ground/DeployLayer" index="2"] -position = Vector2(136, 30) +position = Vector2(157, 68) sprite_frames = ExtResource("7_wo6md") animation = &"车夫静止" autoplay = "车夫静止" @@ -171,7 +171,6 @@ shape = SubResource("RectangleShape2D_b0oon") position = Vector2(461, 80) sprite_frames = ExtResource("3_hnnuc") animation = &"c01_捡球男孩_关键帧" -autoplay = "c01_捡球男孩_关键帧" [node name="ambush_title1" parent="Ground/DeployLayer" index="4" instance=ExtResource("6_3k8jj")] position = Vector2(1569, 21) diff --git a/scene/ground/script/c01/s10_鬼差探头.gd b/scene/ground/script/c01/s10_鬼差探头.gd index 11c836db..dce873cb 100644 --- a/scene/ground/script/c01/s10_鬼差探头.gd +++ b/scene/ground/script/c01/s10_鬼差探头.gd @@ -4,4 +4,17 @@ extends Node2D func play(): var tween = create_tween() tween.tween_property(self, "modulate:a", 1.0, 0.5) - $AnimationPlayer.play("鬼差探头") + $Sfx.play() + var animation_player = $AnimationPlayer as AnimationPlayer + animation_player.play("鬼差探头") + animation_player.animation_finished.connect(_on_animation_finished) + +func play_bgm(): + # TODO 音效 + # var stream = preload("res://asset/sfx/c01/s10_bgm.mp3") + # AudioManager.loop_bgm_music("黄包车背景音效", stream) + SceneManager.pop_debug_dialog_info("音效", "播放黄包车背景音乐过渡") + +func _on_animation_finished(_name): + # 跳转场景 + SceneManager.get_ground_loader().transition_to_scene("c01_s11", "left") diff --git a/scene/ground/script/c01/s10_鬼差探头.tscn b/scene/ground/script/c01/s10_鬼差探头.tscn index dea4e7f7..c3542928 100644 --- a/scene/ground/script/c01/s10_鬼差探头.tscn +++ b/scene/ground/script/c01/s10_鬼差探头.tscn @@ -1,9 +1,26 @@ -[gd_scene load_steps=8 format=3 uid="uid://bf6oxxe2e6vxj"] +[gd_scene load_steps=10 format=3 uid="uid://bf6oxxe2e6vxj"] [ext_resource type="Script" uid="uid://bgffnekpvw8pu" path="res://scene/ground/script/c01/s10_鬼差探头.gd" id="1_pvnth"] [ext_resource type="Texture2D" uid="uid://dsyb81xacsbc8" path="res://asset/art/scene/c01/s03_旧版序章/ux_背景gaise.png" id="2_r4vbv"] [ext_resource type="Texture2D" uid="uid://bqawq75la061h" path="res://asset/art/scene/c01/s03_旧版序章/ux_鬼差脸gaise.png" id="3_dxjux"] [ext_resource type="Texture2D" uid="uid://bvxa5gejmlk4o" path="res://asset/art/scene/c01/s03_旧版序章/e_柱子有红纹gaise.png" id="4_beln2"] +[ext_resource type="AudioStream" uid="uid://ctdoent5ye3us" path="res://asset/audio/sfx/c01/黑雾说话.wav" id="5_c13qq"] +[ext_resource type="Script" uid="uid://rq6w1vuhuq1m" path="res://scene/entity/general/sfx.gd" id="5_ug335"] + +[sub_resource type="Animation" id="Animation_ug335"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("脸与眼:position") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector2(-67, -10)] +} [sub_resource type="Animation" id="Animation_ptmad"] resource_name = "鬼差探头" @@ -20,20 +37,19 @@ tracks/0/keys = { "update": 0, "values": [Vector2(-87, -5), Vector2(-78, 3), Vector2(-66, 2), Vector2(-56, 5), Vector2(-45, 5), Vector2(-41, 10), Vector2(-40, 19), Vector2(-30, 24), Vector2(-27, 28)] } - -[sub_resource type="Animation" id="Animation_ug335"] -length = 0.001 -tracks/0/type = "value" -tracks/0/imported = false -tracks/0/enabled = true -tracks/0/path = NodePath("脸与眼:position") -tracks/0/interp = 1 -tracks/0/loop_wrap = true -tracks/0/keys = { -"times": PackedFloat32Array(0), +tracks/1/type = "method" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(3.7), "transitions": PackedFloat32Array(1), -"update": 0, -"values": [Vector2(-67, -10)] +"values": [{ +"args": [], +"method": &"play_bgm" +}] } [sub_resource type="AnimationLibrary" id="AnimationLibrary_ug335"] @@ -66,3 +82,11 @@ position = Vector2(22, -3) scale = Vector2(1.0601, 1.38482) texture = ExtResource("4_beln2") centered = false + +[node name="Sfx" type="AudioStreamPlayer" parent="."] +stream = ExtResource("5_c13qq") +bus = &"game_sfx" +script = ExtResource("5_ug335") +dir = "c01" +file = "黑雾说话.wav" +metadata/_custom_type_script = "uid://rq6w1vuhuq1m" diff --git a/scene/prop/prop_hud.gd b/scene/prop/prop_hud.gd index 5162c668..983e071e 100644 --- a/scene/prop/prop_hud.gd +++ b/scene/prop/prop_hud.gd @@ -169,7 +169,7 @@ func _update_prop_display_with_texture(): if not inventory: return # 在没有道具时,展示空手 placeholder - if inventory.enabled_items.size() == 0: + if inventory.enabled_items.is_empty(): display_prop.texture_normal = preload("res://asset/art/ui/hud/placeholder.png") display_prop.size = Vector2(PROP_CONTROL_X, PROP_CONTROL_X) display_prop.scale = Vector2(1.0, 1.0) @@ -188,7 +188,7 @@ func _update_prop_display_with_texture(): select_mark.custom_minimum_size = Vector2(PROP_CONTAINER_X, PROP_CONTAINER_X) select_mark.texture = preload("res://asset/art/ui/hud/select_mark.png") # bag - for i in range(prop_containers.size()): + for i in range(inventory.enabled_items.size()): var id = wrapi(i, 0, inventory.enabled_items.size()) var key = inventory.enabled_items[id] var button = prop_containers[i].get_child(0).get_child(0) as TextureButton diff --git a/scene/prop/prop_inventory_resource.gd b/scene/prop/prop_inventory_resource.gd index c5085769..8b8a9a4b 100644 --- a/scene/prop/prop_inventory_resource.gd +++ b/scene/prop/prop_inventory_resource.gd @@ -14,9 +14,9 @@ signal current_item_changed(prop_key: String) current_item_changed.emit("") -func current_item_key(): +func current_item_key() -> String: if enabled_items.size() == 0: - return null + return "" return enabled_items[current_index]