From 746a25fd5b6cfe9eed7027aedbeec0cb0bd71451 Mon Sep 17 00:00:00 2001 From: cakipaul Date: Wed, 16 Jul 2025 18:48:16 +0800 Subject: [PATCH 1/2] compression_method=4 --- project.godot | 1 + 1 file changed, 1 insertion(+) diff --git a/project.godot b/project.godot index d99f91a9..78503047 100644 --- a/project.godot +++ b/project.godot @@ -242,6 +242,7 @@ locale/fallback="zh" textures/canvas_textures/default_texture_filter=0 textures/vram_compression/import_etc2_astc=true +textures/webp_compression/compression_method=4 [statistics] From e42b434bed935ec4e61989b22cc55983c0ccd457 Mon Sep 17 00:00:00 2001 From: cakipaul Date: Wed, 16 Jul 2025 19:56:21 +0800 Subject: [PATCH 2/2] =?UTF-8?q?archive=E3=80=81config=E3=80=81ground=20?= =?UTF-8?q?=E7=AD=89=E9=87=8D=E8=A6=81=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=9Btrailer=20=E8=AF=BB=E5=8F=96=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E5=8C=96=E9=85=8D=E7=BD=AE=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- manager/archive_manager/archive_manager.gd | 297 ++++++++------ manager/config_manager/global_config.gd | 34 +- .../config_manager/global_config_manager.gd | 198 +++++---- scene/ground/ground.gd | 239 ++++++----- scene/ground/ground_loader.gd | 383 +++++++++--------- scene/trailer.gd | 184 +++++---- scene/ux/prop_hud.gd | 7 +- 7 files changed, 775 insertions(+), 567 deletions(-) diff --git a/manager/archive_manager/archive_manager.gd b/manager/archive_manager/archive_manager.gd index 2b0926f1..4b3300a1 100644 --- a/manager/archive_manager/archive_manager.gd +++ b/manager/archive_manager/archive_manager.gd @@ -3,6 +3,18 @@ extends Node signal archive_loaded +# Constants +const CURRENT_VERSION = 6 +const ARCHIVE_ID_MIN = 0 +const ARCHIVE_ID_MAX = 99 +const ARCHIVE_ID_DIGITS = 3 + +# Static paths +static var user_root_dir := "user://data/" # must end with "/" +static var archive_dir := "user://data/archives/" +static var archive_prefix := "save" + +# Archive management var archive: AssembledArchive: set(val): archive = val @@ -10,13 +22,6 @@ var archive: AssembledArchive: GlobalConfigManager.print_global_info() print("use archive ", archive.resource_path) -# current archive -static var user_root_dir := "user://data/" # must end with "/" -static var archive_dir := "user://data/archives/" -static var archive_prefix := "save" - -const CURRENT_VERSION = 6 - var archives_dict: Dictionary[int, AssembledArchive] = {} var archives_notes_dict: Dictionary[int, String] = {} var autosave_timer := Timer.new() @@ -26,24 +31,22 @@ func _ready() -> void: # 禁用默认退出行为,在 _notification 处理 NOTIFICATION_WM_CLOSE_REQUEST 时保存数据 get_tree().set_auto_accept_quit(false) process_mode = Node.PROCESS_MODE_ALWAYS + if not _check_dirs_and_archives(): _handle_load_error("存档目录", "读写") return - autosave_timer.timeout.connect(_try_auto_save) - autosave_timer.stop() - add_child(autosave_timer) + + _setup_autosave_timer() + # config should be loaded first load_config() + # 在 debug or editor 模式下,直接保证有 archive if GlobalConfig.DEBUG or Engine.is_editor_hint(): - if archives_dict.size() == 0: - create_and_use_new_archive(0) - else: - # debug 模式下默认使用 0 号存档 - GlobalConfigManager.config.current_selected_archive_id = 0 + _ensure_debug_archive() -func _notification(what): +func _notification(what: int) -> void: # handle window close request if what == NOTIFICATION_WM_CLOSE_REQUEST: save_all() @@ -53,14 +56,27 @@ func _notification(what): SceneManager.quit_game() -func _on_archive_id_changed(): +func _setup_autosave_timer() -> void: + autosave_timer.timeout.connect(_try_auto_save) + autosave_timer.stop() + add_child(autosave_timer) + + +func _ensure_debug_archive() -> void: + if archives_dict.is_empty(): + create_and_use_new_archive(0) + else: + # debug 模式下默认使用 0 号存档 + GlobalConfigManager.config.current_selected_archive_id = 0 + + +func _on_archive_id_changed() -> void: var selected_id = GlobalConfigManager.config.current_selected_archive_id if selected_id < 0: return - # if archive and selected_id == archive.archive_id: - # print("_on_archive_id_changed same id=", selected_id) - # return + print("_on_archive_id_changed id=", selected_id) + if not archives_dict.has(selected_id): print("新建存档 ", selected_id) create_and_use_new_archive(selected_id) @@ -70,16 +86,19 @@ func _on_archive_id_changed(): load_archive() -func check_autosave_options(): - if ( - GlobalConfigManager.config.auto_save_enabled - and archive - and GlobalConfigManager.config.auto_save_seconds > 1 - ): +func check_autosave_options() -> void: + var config = GlobalConfigManager.config + var should_enable_autosave = ( + config.auto_save_enabled + and archive + and config.auto_save_seconds > 1 + ) + + if should_enable_autosave: # reset left time autosave_timer.stop() autosave_timer.one_shot = false - autosave_timer.wait_time = GlobalConfigManager.config.auto_save_seconds + autosave_timer.wait_time = config.auto_save_seconds autosave_timer.start() else: autosave_timer.stop() @@ -87,15 +106,16 @@ func check_autosave_options(): if GlobalConfig.DEBUG: print( "check_autosave_option: ", - GlobalConfigManager.config.auto_save_enabled, + config.auto_save_enabled, " wait_time=", autosave_timer.wait_time ) -func _try_auto_save(): +func _try_auto_save() -> void: if GlobalConfig.DEBUG: print("Auto save") + if archive and GlobalConfigManager.config.auto_save_seconds > 1: save_all() # 自动保存成功 [ID:ui_auto_saved] @@ -103,71 +123,108 @@ func _try_auto_save(): func _check_dirs_and_archives() -> bool: - if !DirAccess.dir_exists_absolute(user_root_dir): - DirAccess.make_dir_recursive_absolute(user_root_dir) - print("Create user_root_dir:", user_root_dir) + # Ensure directories exist + _ensure_directory_exists(user_root_dir) + _ensure_directory_exists(archive_dir) + # Check if the archive directory is accessible - if !DirAccess.dir_exists_absolute(archive_dir): - DirAccess.make_dir_recursive_absolute(archive_dir) - print("Create archive_dir:", archive_dir) var archive_dir_access = DirAccess.open(archive_dir) - if !archive_dir_access: + if not archive_dir_access: _handle_load_error("存档目录", "读取") - # TODO pop up a dialog to inform the user return false - var files = archive_dir_access.get_files() - files.sort() - # get archive number - for file in files: - if file.begins_with(archive_prefix) and file.ends_with(GlobalConfig.RES_FILE_FORMAT): - # format: save012_xxxxx; save000 - var id_and_note = file.get_basename().substr(archive_prefix.length()).strip_escapes().split("_", true, 1) - var id_str = id_and_note[0] - var note_str = id_and_note[1] if id_and_note.size() >= 2 else (archive_prefix + id_str) - # 非三位数的 id 会被忽略 - if id_str.length() != 3: - continue - var id = int(id_str) - # 读取范围是 0-99 - if id < 0 or id > 99: - continue - archives_notes_dict[id] = note_str - var path = archive_dir + file - if not archives_dict.has(id): - var res = ResourceLoader.load( - path, "AssembledArchive", ResourceLoader.CACHE_MODE_REPLACE_DEEP - ) - if is_instance_valid(res) and res.version >= CURRENT_VERSION: - archives_dict[id] = res - else: - printerr("SKIP INVALID ARCHIVE! file=", file) + + # Load existing archives + _load_existing_archives(archive_dir_access) return true +func _ensure_directory_exists(dir_path: String) -> void: + if not DirAccess.dir_exists_absolute(dir_path): + DirAccess.make_dir_recursive_absolute(dir_path) + print("Create directory:", dir_path) + + +func _load_existing_archives(dir_access: DirAccess) -> void: + var files = dir_access.get_files() + files.sort() + + for file in files: + if not _is_valid_archive_filename(file): + continue + var archive_info = _parse_archive_filename(file) + if not archive_info: + continue + var id = archive_info.id + var note = archive_info.note + archives_notes_dict[id] = note + if not archives_dict.has(id): + var archive_resource = _load_archive_resource(archive_dir + file) + if archive_resource: + archives_dict[id] = archive_resource + + +func _is_valid_archive_filename(filename: String) -> bool: + return filename.begins_with(archive_prefix) and filename.ends_with(GlobalConfig.RES_FILE_FORMAT) + + +func _parse_archive_filename(filename: String) -> Dictionary: + # format: save012_xxxxx; save000 + var basename = filename.get_basename() + var id_and_note = basename.substr(archive_prefix.length()).strip_escapes().split("_", true, 1) + var id_str = id_and_note[0] + # 非三位数的 id 会被忽略 + if id_str.length() != ARCHIVE_ID_DIGITS: + return {} + var id = int(id_str) + # 读取范围是 0-99 + if id < ARCHIVE_ID_MIN or id > ARCHIVE_ID_MAX: + return {} + var note_str = id_and_note[1] if id_and_note.size() >= 2 else (archive_prefix + id_str) + return { + "id": id, + "note": note_str + } + + +func _load_archive_resource(path: String) -> AssembledArchive: + var res = ResourceLoader.load( + path, "AssembledArchive", ResourceLoader.CACHE_MODE_REPLACE_DEEP + ) + if is_instance_valid(res) and res.version >= CURRENT_VERSION: + return res + else: + printerr("SKIP INVALID ARCHIVE! path=", path) + return null + + # 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() - 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) + id = _find_next_available_id() _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) + var archive_path = _get_archive_path(id) + var take_over_path = FileAccess.file_exists(archive_path) + _create_and_save_new_archive_resoure(id, take_over_path) + # 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: +func _find_next_available_id() -> int: + var id = 0 + var archive_path = _get_archive_path(id) + while FileAccess.file_exists(archive_path) and id <= ARCHIVE_ID_MAX: + id += 1 + archive_path = _get_archive_path(id) + return id + + +func _create_and_save_new_archive_resoure(id: int, take_over_path := false) -> void: var archive_path = _get_archive_path(id) archive = AssembledArchive.new() as Resource archive.version = CURRENT_VERSION @@ -183,13 +240,7 @@ func _create_and_save_new_archive_resoure(id, take_over_path = false) -> void: # 超过 999 个存档会出问题;不过这个游戏不会有这么多存档 func _get_archive_path(id: int) -> String: - var id_str := "" - if id < 10: - id_str = "00" + str(id) - elif id < 100: - id_str = "0" + str(id) - else: - id_str = str(id) + var id_str := str(id).pad_zeros(ARCHIVE_ID_DIGITS) return archive_dir + archive_prefix + id_str + GlobalConfig.RES_FILE_FORMAT @@ -202,41 +253,54 @@ func save_all() -> void: var config = GlobalConfigManager.config if config: ResourceSaver.save(config) - - # player_global_position - var player = SceneManager.get_player() as MainPlayer - # 在此处保存 player 的位置信息 - if archive and player: - archive.player_global_position_x = player.global_position.x - archive.player_direction = player.facing_direction + # save player state + _save_player_state() + # save archive if archive: ResourceSaver.save(archive) # reset autosave timer check_autosave_options() +func _save_player_state() -> void: + if not archive: + return + var player = SceneManager.get_player() as MainPlayer + if player: + archive.player_global_position_x = player.global_position.x + archive.player_direction = player.facing_direction + + func load_config() -> void: if GlobalConfigManager.config: return var path = user_root_dir + "config" + GlobalConfig.RES_FILE_FORMAT if FileAccess.file_exists(path): - var config = ResourceLoader.load(path) - if is_instance_valid(config) and config.version >= CURRENT_VERSION: - GlobalConfigManager.config = config + var loaded_config = ResourceLoader.load(path) + if is_instance_valid(loaded_config) and loaded_config.version >= CURRENT_VERSION: + GlobalConfigManager.config = loaded_config else: printerr("SKIP INVALID CONFIG!") - if GlobalConfigManager.config == null: - var config = GlobalConfig.new() - config.version = CURRENT_VERSION - GlobalConfigManager.config = config - ResourceSaver.save(config, path) + + if not GlobalConfigManager.config: + _create_default_config(path) GlobalConfigManager.config.resource_path = path - if Engine.is_editor_hint(): - return - # connect signals - GlobalConfigManager.config.current_selected_archive_id_changed.connect(_on_archive_id_changed) - GlobalConfigManager.config.auto_save_seconds_changed.connect(check_autosave_options) - GlobalConfigManager.config.auto_save_enabled_changed.connect(check_autosave_options) + if not Engine.is_editor_hint(): + _connect_config_signals() + + +func _create_default_config(path: String) -> void: + var config = GlobalConfig.new() + config.version = CURRENT_VERSION + GlobalConfigManager.config = config + ResourceSaver.save(config, path) + + +func _connect_config_signals() -> void: + var config = GlobalConfigManager.config + config.current_selected_archive_id_changed.connect(_on_archive_id_changed) + config.auto_save_seconds_changed.connect(check_autosave_options) + config.auto_save_enabled_changed.connect(check_autosave_options) func load_archive() -> void: @@ -244,8 +308,7 @@ func load_archive() -> void: var selected_id = 0 if GlobalConfigManager.config: selected_id = GlobalConfigManager.config.current_selected_archive_id - # if archive and selected_id == archive.archive_id: - # return + print("load_archive ", selected_id) if not archives_dict.has(selected_id): _handle_load_error(str(selected_id) + " 号存档", "查找") @@ -256,7 +319,7 @@ func load_archive() -> void: check_autosave_options() -func _handle_load_error(target, action) -> void: +func _handle_load_error(target: String, action: String) -> void: var msg = str(target) + " " + str(action) + " failed. Permission Error." SceneManager.pop_notification(msg) printerr(msg) @@ -275,9 +338,7 @@ func get_global_value(property: StringName, default = null) -> Variant: printerr("Archive is null, cannot get global value") return default var val = archive.global_data_dict.get(property) - if val == null: - return default - return val + return default if val == null else val func set_chapter_if_greater(c: int) -> void: @@ -285,7 +346,9 @@ func set_chapter_if_greater(c: int) -> void: printerr("Archive is null, cannot set chapter") return # 1:序章;2-5:一~四章;6:结尾 - if c < 1 or c > 6: + const MIN_CHAPTER = 1 + const MAX_CHAPTER = 6 + if c < MIN_CHAPTER or c > MAX_CHAPTER: printerr("[ArchiveManager] set_chapter_if_greater: invalid chapter value: " + str(c)) return if EventManager.get_chapter_stage() >= c: @@ -296,11 +359,11 @@ func set_chapter_if_greater(c: int) -> void: func unlock_memory(id: int) -> void: - if archive: - if archive.mem_display_dict.get(id): - print("memory already unlocked. id=", id) - return - archive.mem_display_dict[id] = true - SceneManager.pop_notification("ui_notify_mem_update") - else: + if not archive: printerr("Archive is null, cannot unlock memory. id=", id) + return + if archive.mem_display_dict.get(id): + print("memory already unlocked. id=", id) + return + archive.mem_display_dict[id] = true + SceneManager.pop_notification("ui_notify_mem_update") \ No newline at end of file diff --git a/manager/config_manager/global_config.gd b/manager/config_manager/global_config.gd index f080e3a7..d341bb47 100644 --- a/manager/config_manager/global_config.gd +++ b/manager/config_manager/global_config.gd @@ -8,6 +8,16 @@ static var DEBUG = false # .res would be binary encoded, .tres is text encoded const RES_FILE_FORMAT = ".tres" +# Audio bus names +const BUS_MASTER := "Master" +const BUS_GAME_SFX := "game_sfx" +const BUS_DIALOG := "dialog" + +const LANGUAGE_OPTIONS = ["简体中文", "English"] +const LOCALE_PREFIX_MAPPING = {"zh": 0, "en": 1} +const CAPTION_OPTIONS_DICT = {0: ["上海话", "普通话"], 1: [""]} +const CAPTION_LOCALES_DICT = {0: ["zh_SH", "zh_CN"], 1: ["en"]} + ## layers # 设置 const CANVAS_LAYER_SETTINGS = 30 @@ -57,10 +67,10 @@ signal current_selected_archive_id_changed signal auto_save_enabled_changed signal auto_save_seconds_changed -@export var debug_mode := false # 开启 debug 模式 -@export var skip_trailer := false # 跳过 trailer +@export var debug_mode := false # 开启 debug 模式 +@export var skip_trailer := false # 跳过 trailer @export var version: int #存档版本 -@export var game_launched_times := 0 # 启动游戏次数 +@export var game_launched_times := 0 # 启动游戏次数 @export var game_total_seconds := 0 # 游戏总时长 @export var game_rounds := 1 # 当前周目数 @export var current_selected_archive_id := -1: # 当前选定存档, -1 为未选择 @@ -90,3 +100,21 @@ signal auto_save_seconds_changed # 最大范围为 10 秒,精度 0.1 @export var os_wait_time := 3.0 @export var os_auto_end := true + + +func get_locale_language_name() -> String: + language = wrapi(language, 0, LANGUAGE_OPTIONS.size()) + return LANGUAGE_OPTIONS[language] + + +func get_locale_caption_name() -> String: + language = wrapi(language, 0, LANGUAGE_OPTIONS.size()) + var caption_options = CAPTION_OPTIONS_DICT.get(language, [""]) + caption = wrapi(caption, 0, caption_options.size()) + return caption_options[caption] + + +func get_locale() -> String: + var locales = CAPTION_LOCALES_DICT.get(language, [""]) + caption = wrapi(caption, 0, locales.size()) + return locales[caption] diff --git a/manager/config_manager/global_config_manager.gd b/manager/config_manager/global_config_manager.gd index 7fd8879b..33561e57 100644 --- a/manager/config_manager/global_config_manager.gd +++ b/manager/config_manager/global_config_manager.gd @@ -1,129 +1,165 @@ @tool extends Node +# Constants +const TIMER_INTERVAL := 5.0 +const TIMER_LOG_INTERVAL := 6 # 30秒打印一次 (6 * 5秒) +const TIMER_EDITOR_LOG_INTERVAL := 120 # 编辑器中600秒打印一次 + +# Static config static var config: GlobalConfig: set = _set_config -var timer = Timer.new() +# Timer for tracking game time +var timer := Timer.new() +var _timer_tick_counter := 0 func _ready() -> void: - timer.wait_time = 5.0 + _setup_timer() + + +func _setup_timer() -> void: + timer.wait_time = TIMER_INTERVAL timer.one_shot = false timer.timeout.connect(_on_timer_timeout) add_child(timer) timer.start() -func _set_config(val: GlobalConfig) -> void: +static func _set_config(val: GlobalConfig) -> void: config = val - if Engine.is_editor_hint(): + if not config or Engine.is_editor_hint(): return - # debug) + + _apply_debug_mode() + _apply_window_settings() + _apply_audio_settings() + _apply_locale_settings() + + +static func _apply_debug_mode() -> void: if config.debug_mode: GlobalConfig.DEBUG = true print_rich("[color=orange]Debug mode enabled[/color]") - # set up window + + +static func _apply_window_settings() -> void: + var window = Engine.get_main_loop().root.get_window() + if config.window_fullscreen: - get_window().mode = Window.MODE_EXCLUSIVE_FULLSCREEN - # get_window().mode = Window.MODE_FULLSCREEN + window.mode = Window.MODE_EXCLUSIVE_FULLSCREEN else: - get_window().mode = Window.MODE_WINDOWED - get_window().always_on_top = config.window_top - # set up sound - AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Master"), config.db_master) - AudioServer.set_bus_volume_db(AudioServer.get_bus_index("game_sfx"), config.db_game_sfx) - AudioServer.set_bus_volume_db(AudioServer.get_bus_index("dialog"), config.db_dialog) + window.mode = Window.MODE_WINDOWED + + window.always_on_top = config.window_top + + +static func _apply_audio_settings() -> void: + AudioServer.set_bus_volume_db( + AudioServer.get_bus_index(GlobalConfig.BUS_MASTER), config.db_master + ) + AudioServer.set_bus_volume_db( + AudioServer.get_bus_index(GlobalConfig.BUS_GAME_SFX), config.db_game_sfx + ) + AudioServer.set_bus_volume_db( + AudioServer.get_bus_index(GlobalConfig.BUS_DIALOG), config.db_dialog + ) + prints( "config load volume_db settings (master, sfx, dialog): ", config.db_master, config.db_game_sfx, config.db_dialog ) - # set locale - update_locale(config.language, config.caption) + + +static func _apply_locale_settings() -> void: + var locale = config.get_locale() + print("set language to: ", locale) + TranslationServer.set_locale(locale) # -1 null; 0 zh; 1 en -var language_options = ["简体中文", "English"] -var caption_options_dict = {0: ["上海话", "普通话"], 1: [""]} -var caption_loacles_dict = {0: ["zh_SH", "zh_CN"], 1: ["en"]} - - -# return example: "中文_简体中文" "English_" func update_locale(lang_id: int, caption_id: int) -> void: - lang_id = wrapi(lang_id, 0, language_options.size()) - var caption_loacles = caption_loacles_dict.get(lang_id) - caption_id = wrapi(caption_id, 0, caption_loacles.size()) - var lang = caption_loacles[caption_id] - GlobalConfigManager.config.language = lang_id - GlobalConfigManager.config.caption = caption_id - print("set language to: ", lang) - TranslationServer.set_locale(lang) + config.language = wrapi(lang_id, 0, GlobalConfig.LANGUAGE_OPTIONS.size()) + var caption_options = GlobalConfig.CAPTION_OPTIONS_DICT.get(config.language, [""]) + config.caption = wrapi(caption_id, 0, caption_options.size()) + _apply_locale_settings() func get_locale_language_name() -> String: - GlobalConfigManager.config.language = wrapi( - GlobalConfigManager.config.language, 0, language_options.size() - ) - return language_options[GlobalConfigManager.config.language] + if config: + return config.get_locale_language_name() + return "" func get_locale_caption_name() -> String: - GlobalConfigManager.config.language = wrapi( - GlobalConfigManager.config.language, 0, language_options.size() - ) - var caption_options = caption_options_dict.get(GlobalConfigManager.config.language) - GlobalConfigManager.config.caption = wrapi( - GlobalConfigManager.config.caption, 0, caption_options.size() - ) - return caption_options[GlobalConfigManager.config.caption] + if config: + return config.get_locale_caption_name() + return "" -var _on_timer_timeout_counter := 0 - - -func _on_timer_timeout(): - var archive := ArchiveManager.archive - if archive and config: - archive.game_seconds += 5 - config.game_total_seconds += 5 - _on_timer_timeout_counter += 1 - # 30s 打印一次,无需首次打印 - # ArchiveManager 设置 archive 时会调用 print_global_info - if _on_timer_timeout_counter % 6 == 0: - # editor 中 600s 打印一次 - if Engine.is_editor_hint() and _on_timer_timeout_counter % 120 != 0: - return - print_global_info() - - -# @warning_ignore("intege") -func print_global_info(): +func _on_timer_timeout() -> void: var archive := ArchiveManager.archive if not archive or not config: return + # Update game time + archive.game_seconds += int(TIMER_INTERVAL) + config.game_total_seconds += int(TIMER_INTERVAL) + _timer_tick_counter += 1 + # Check if should log + if _should_log_game_info(): + print_global_info() + + +func _should_log_game_info() -> bool: + # 30s 打印一次,无需首次打印 + if _timer_tick_counter % TIMER_LOG_INTERVAL != 0: + return false + # editor 中 600s 打印一次 + if Engine.is_editor_hint() and _timer_tick_counter % TIMER_EDITOR_LOG_INTERVAL != 0: + return false + return true + + +func print_global_info() -> void: + var archive := ArchiveManager.archive + if not archive or not config: + return + var game_time_str = _format_game_time(archive.game_seconds) + var tick_time_str = _format_tick_time() + var round_info = _get_round_info() + var scene_info = archive.current_scene + prints( + "[timemark]", + Time.get_datetime_string_from_system(), + round_info, + scene_info, + game_time_str + " " + tick_time_str + ) + + +static func _format_game_time(total_seconds: int) -> String: @warning_ignore("integer_division") - var hour := archive.game_seconds / 3600 + var hours := total_seconds / 3600 @warning_ignore("integer_division") - var minute := (archive.game_seconds % 3600) / 60 - var second := archive.game_seconds % 60 + var minutes := (total_seconds % 3600) / 60 + var seconds := total_seconds % 60 + return "game:%d:%02d:%02d" % [hours, minutes, seconds] + + +static func _get_round_info() -> String: # 0:未开始游戏;1:序章;2-5:一~四章;6:结尾 var chapter := EventManager.get_chapter_stage() - var round_info = "r" + str(config.game_rounds) + "_c" + str(chapter) - var game_time_info = "game:" + str(hour) + ":" + str(minute) + ":" + str(second) - # get ticks since game app run + return "r%d_c%d" % [config.game_rounds, chapter] + + +@warning_ignore_start("integer_division") +static func _format_tick_time() -> String: var ticks = Time.get_ticks_msec() - @warning_ignore("integer_division") - hour = ticks / 3600000 - @warning_ignore("integer_division") - minute = (ticks % 3600000) / 60000 - @warning_ignore("integer_division") - second = (ticks % 60000) / 1000 - var msec = ticks % 1000 - var tick_time_info = ( - "tick:" + str(hour) + ":" + str(minute) + ":" + str(second) + "." + str(msec) - ) - var time_info = game_time_info + " " + tick_time_info - var scene_info = archive.current_scene - prints("[timemark]", Time.get_datetime_string_from_system(), round_info, scene_info, time_info) + var hours := ticks / 3600000 as int + var minutes := (ticks % 3600000) / 60000 as int + var seconds := (ticks % 60000) / 1000 as int + var msec := ticks % 1000 as int + return "tick:%d:%02d:%02d.%03d" % [hours, minutes, seconds, msec] diff --git a/scene/ground/ground.gd b/scene/ground/ground.gd index 3754fdab..9a8487c8 100644 --- a/scene/ground/ground.gd +++ b/scene/ground/ground.gd @@ -1,6 +1,21 @@ @tool class_name Ground2D extends Node2D +# Constants +const DEFAULT_PLAYER_Y := 70 +const CAMERA_MIN_WIDTH := 564.0 +const CAMERA_MIN_HEIGHT := 316.0 +const PLAYER_PADDING_X := 30.0 + +const FOOTSTEP_AUDIO = { + "ghost": preload("res://config/audio/sfx/footstep_ghost.tres"), + "硬地面": preload("res://config/audio/sfx/footstep_硬地面.tres"), + "室外": preload("res://config/audio/sfx/footstep_室外.tres"), + "crawling": preload("res://config/audio/sfx/footstep_crawling.tres"), + "盒子猫": preload("res://config/audio/sfx/footstep_meow.tres"), +} + +# Exports @export var scene_name := "" # 用于在 debug 时态下,指定进入的 portal @export_enum("left", "right", "1", "2", "3", "4", "5", "6", "7", "8", "9") @@ -12,7 +27,7 @@ var default_portal := "left" player_y_fixed = val if is_node_ready(): reset_player_y() -@export var player_y := 70: +@export var player_y := DEFAULT_PLAYER_Y: set(val): player_y = val if is_node_ready(): @@ -29,42 +44,24 @@ var default_portal := "left" if is_node_ready(): _load_footstep_audio() -# var main_scene := preload("res://scene/main.tscn") as PackedScene - -@onready var player_line = %PlayerLine2D as Line2D -@onready var reenter_lock = %PlayerReenterLock as PlayerReenterLock -@onready var player = %MainPlayer as MainPlayer +# Nodes +@onready var player_line := %PlayerLine2D as Line2D +@onready var reenter_lock := %PlayerReenterLock as PlayerReenterLock +@onready var player := %MainPlayer as MainPlayer @onready var directional_light := %DirectionalLight2D as DirectionalLight2D -@onready var bg_sprite = %BGSprite2D as Sprite2D -@onready var foreground = %ParallaxForeground as ParallaxBackground -@onready var camera_focus_marker = %CameraFocusMarker as CameraFocusMarker +@onready var bg_sprite := %BGSprite2D as Sprite2D +@onready var foreground := %ParallaxForeground as ParallaxBackground +@onready var camera_focus_marker := %CameraFocusMarker as CameraFocusMarker +@onready var footstep_audio := %FootstepAudioPlayer as RandomAudioStreamPlayer -@onready var footstep_audio = %FootstepAudioPlayer as RandomAudioStreamPlayer - -const FOOTSTEP_AUDIO = { - #"wood": preload("res://config/audio/footstep/footstep_wood.tres"), - #"carpet": preload("res://config/audio/footstep/footstep_carpet.tres"), - # "concrete": preload("res://config/audio/footstep/footstep_concrete.tres"), - #"grass": preload("res://config/audio/footstep/footstep_grass.tres"), - #"snow": preload("res://config/audio/footstep/footstep_snow.tres"), - "ghost": preload("res://config/audio/sfx/footstep_ghost.tres"), - "硬地面": preload("res://config/audio/sfx/footstep_硬地面.tres"), - "室外": preload("res://config/audio/sfx/footstep_室外.tres"), - "crawling": preload("res://config/audio/sfx/footstep_crawling.tres"), - # 待替换 - "盒子猫": preload("res://config/audio/sfx/footstep_meow.tres"), -} - -var restarting = false +# State +var restarting := false func _enter_tree() -> void: # 仅在编辑器中调试时,通过 main 场景启动 - if not Engine.is_editor_hint() and (not get_parent() is GroundLoader): - print("restarting... set GlobalConfig.DEBUG = true") - restarting = true - GlobalConfig.DEBUG = true - _restart_from_main() + if not Engine.is_editor_hint() and not (get_parent() is GroundLoader): + _handle_restart() return if camera_focus_marker: camera_focus_marker.enabled = true @@ -74,37 +71,58 @@ func _ready() -> void: if restarting: print("restarting: skip ground _ready()") return - foreground.layer = GlobalConfig.CANVAS_LAYER_FG - # 检查 scene_name 是否合法 - scene_name = scene_name.strip_edges() - if get_parent().name.begins_with("S") and (not scene_name or scene_name.length() != 7): - printerr("scene_name is not valid") + _setup_scene() + _validate_scene_name() _set_camera_and_player_boundary() if Engine.is_editor_hint(): return - # 隐藏 player_line + _setup_runtime() + + +func _handle_restart() -> void: + print("restarting... set GlobalConfig.DEBUG = true") + restarting = true + GlobalConfig.DEBUG = true + _restart_from_main() + + +func _setup_scene() -> void: + foreground.layer = GlobalConfig.CANVAS_LAYER_FG player_line.visible = false + + +func _validate_scene_name() -> void: + scene_name = scene_name.strip_edges() + if get_parent().name.begins_with("S") and (not scene_name or scene_name.length() != 7): + printerr("scene_name is not valid") + + +func _setup_runtime() -> void: _load_footstep_audio() - # marker 默认就在 foucs player 状态 - # camera_focus_marker.focus_node(player) - # %ColorRectTop.visible = true - # %ColorRectBottom.visible = true - # 如果 debug 模式下不通过 GroundLoader 启动,则插入到 main 以下 _setup_player_light() SceneManager.toggle_hud_display(display_hud) -func _restart_from_main(): +func _restart_from_main() -> void: # _enter_tree, wait for ready await ready + _ensure_managers_loaded() + _update_archive_for_restart() + get_tree().change_scene_to_file.call_deferred("res://scene/main.tscn") + + +func _ensure_managers_loaded() -> void: if not ArchiveManager.archive: ArchiveManager.load_archive() if not GlobalConfigManager.config: ArchiveManager.load_config() - ArchiveManager.archive.current_scene = scene_name - ArchiveManager.archive.entrance_portal = default_portal - # get_tree().change_scene_to_packed.call_deferred(main_scene) - get_tree().change_scene_to_file.call_deferred("res://scene/main.tscn") + + +func _update_archive_for_restart() -> void: + var archive = ArchiveManager.archive + if archive: + archive.current_scene = scene_name + archive.entrance_portal = default_portal func get_player() -> MainPlayer: @@ -123,43 +141,65 @@ func get_camera() -> CameraFocusMarker: return get_node_or_null("CameraFocusMarker") as CameraFocusMarker -func reset_player_y(): +func reset_player_y() -> void: # 从屏幕下边缘算起 if player_y_fixed: - get_player().set_y_from_ground(158.0 - player_y) + var p = get_player() + if p: + p.set_y_from_ground(158.0 - player_y) -func _set_camera_and_player_boundary(): - # set current_boarder by bg size - var camera_rect = Rect2(0, -158, 564, 316) - var player_rect = Rect2(0, -158, 564, 316) - if bg_sprite.texture: - var size = bg_sprite.texture.get_size() * bg_sprite.scale - # camera rect - var camera_size = size - camera_size.x += bg_sprite.position.x - camera_size = Vector2(max(564.0, camera_size.x), max(camera_size.y, 316.0)) - var camera_upleft = Vector2(0, -camera_size.y / 2.0) - camera_rect = Rect2(camera_upleft, camera_size) - # player rect should be set centered, with 30px x padding - player_rect.position.x = player_line.get_point_position(0).x + player_line.global_position.x - player_rect.size.x = player_line.get_point_position(1).x - player_line.get_point_position(0).x +func _set_camera_and_player_boundary() -> void: + var camera_rect = _calculate_camera_rect() + var player_rect = _calculate_player_rect() if GlobalConfig.DEBUG: print("try to _set_camera_and_player_boundary as:", camera_rect, player_rect) - # set_camera_boundary + _apply_camera_limits(camera_rect) + _apply_player_boundary(player_rect) + + +func _calculate_camera_rect() -> Rect2: + var camera_rect = Rect2(0, -158, CAMERA_MIN_WIDTH, CAMERA_MIN_HEIGHT) + if bg_sprite.texture: + var size = bg_sprite.texture.get_size() * bg_sprite.scale + var camera_size = size + camera_size.x += bg_sprite.position.x + camera_size = Vector2( + max(CAMERA_MIN_WIDTH, camera_size.x), max(camera_size.y, CAMERA_MIN_HEIGHT) + ) + var camera_upleft = Vector2(0, -camera_size.y / 2.0) + camera_rect = Rect2(camera_upleft, camera_size) + return camera_rect + + +func _calculate_player_rect() -> Rect2: + var player_rect = Rect2(0, -158, CAMERA_MIN_WIDTH, CAMERA_MIN_HEIGHT) + if player_line: + var line_start = player_line.get_point_position(0) + var line_end = player_line.get_point_position(1) + player_rect.position.x = line_start.x + player_line.global_position.x + player_rect.size.x = line_end.x - line_start.x + return player_rect + + +func _apply_camera_limits(camera_rect: Rect2) -> void: var camera_marker = get_camera() - camera_marker.limit_left = camera_rect.position.x - camera_marker.limit_right = camera_rect.position.x + camera_rect.size.x - camera_marker.limit_top = camera_rect.position.y - camera_marker.limit_bottom = camera_rect.position.y + camera_rect.size.y - # set_player_boundary - get_player().player_movement_rect = player_rect + if camera_marker: + camera_marker.limit_left = camera_rect.position.x + camera_marker.limit_right = camera_rect.position.x + camera_rect.size.x + camera_marker.limit_top = camera_rect.position.y + camera_marker.limit_bottom = camera_rect.position.y + camera_rect.size.y -func _load_footstep_audio(): - # foot step sound +func _apply_player_boundary(player_rect: Rect2) -> void: + var p = get_player() + if p: + p.player_movement_rect = player_rect + + +func _load_footstep_audio() -> void: footstep_audio.audio_collections.clear() - if footstep_type != "none": + if footstep_type != "none" and FOOTSTEP_AUDIO.has(footstep_type): var audio = FOOTSTEP_AUDIO[footstep_type] as AudioStreamCollection footstep_audio.audio_collections.append(audio) @@ -170,29 +210,44 @@ func play_footstep_sound() -> void: func move_player_to_portal(portal_name: String) -> void: - var node_path = NodePath("DeployLayer/portal_" + portal_name) - var portal_node = get_node_or_null(node_path) as Portal2D + var portal_node = _get_portal_node(portal_name) var mov_player = get_player() as MainPlayer - if portal_node and mov_player: - mov_player.global_position.x = portal_node.global_position.x - if portal_name == "left": - mov_player.set_facing_direction(Vector2.RIGHT) - elif portal_name == "right": - mov_player.set_facing_direction(Vector2.LEFT) - reset_player_y() - print("[ground] move player to portal:", portal_name, portal_node.global_position) - elif mov_player: - printerr(scene_name, " portal not found: ", node_path) - else: + if not portal_node: + if mov_player: + printerr(scene_name, " portal not found: portal_", portal_name) + else: + printerr("move_player_to_portal player not ready") + return + if not mov_player: printerr("move_player_to_portal player not ready") + return + _position_player_at_portal(mov_player, portal_node, portal_name) + reset_player_y() + print("[ground] move player to portal:", portal_name, portal_node.global_position) -func _setup_player_light(): +func _get_portal_node(portal_name: String) -> Portal2D: + var node_path = NodePath("DeployLayer/portal_" + portal_name) + return get_node_or_null(node_path) as Portal2D + + +func _position_player_at_portal( + mov_player: MainPlayer, portal_node: Portal2D, portal_name: String +) -> void: + mov_player.global_position.x = portal_node.global_position.x + + if portal_name == "left": + mov_player.set_facing_direction(Vector2.RIGHT) + elif portal_name == "right": + mov_player.set_facing_direction(Vector2.LEFT) + + +func _setup_player_light() -> void: # 强制显示 directional_light directional_light.visible = true # 设置角色身上光源 - if directional_light.blend_mode == Light2D.BLEND_MODE_SUB and directional_light.energy > 0.6: - player.enable_light = true - else: - player.enable_light = false + var should_enable_light = ( + directional_light.blend_mode == Light2D.BLEND_MODE_SUB and directional_light.energy > 0.6 + ) + player.enable_light = should_enable_light print("_setup_player_light player.enable_light=", player.enable_light) diff --git a/scene/ground/ground_loader.gd b/scene/ground/ground_loader.gd index f5a481bb..f70ffd63 100644 --- a/scene/ground/ground_loader.gd +++ b/scene/ground/ground_loader.gd @@ -1,26 +1,12 @@ class_name GroundLoader extends Node2D -@export_group("Scene") -@export var ignore_archive := false -@export var current_scene := "c02_s01" -@export var entrance_portal := "left" -@export var debug_reload := false: - set(new_val): - debug_reload = false - if is_node_ready() and current_scene and entrance_portal: - transition_to_scene(current_scene, entrance_portal, 0.0) -# 强制覆盖 archive 记录 -@export var force_archive_scene := "" -@export var force_archive_portal := "" +# Constants +const DEFAULT_TRANSITION_TIME := 1.4 +const MIN_TRANSITION_TIME := 0.6 +const EASE_DURATION := 0.3 -@onready var mask_layer := %MaskLayer as CanvasLayer -@onready var mask := %Mask as ColorRect - -var has_entered := false -var ground: Ground2D - -# 场景名字映射到路径 -static var GROUND_SCENE_PATH_DICT = { +# Scene name to path mapping +const GROUND_SCENE_PATH_DICT = { "c01_s05": "uid://dlx5xxbg53rb8", "c01_s06": "uid://bx16c8nn32f40", "c01_s07": "uid://ds2iyfndwamiy", @@ -47,162 +33,190 @@ static var GROUND_SCENE_PATH_DICT = { "c02_s16": "uid://22hc3oe8t0id", "c02_s17": "uid://cbr6gbgrl2wb1", "c02_s18": "uid://d27gv3pbkn4b8", - "c03_s01": "uid://dlrbhfvnd3cs0", # 三楼 - "c03_s02": "uid://rkro7u5wd3t1", # 三楼内侧 - "c03_s03": "uid://bsqt2c061fmin", # 瞎子理发店 + "c03_s01": "uid://dlrbhfvnd3cs0", # 三楼 + "c03_s02": "uid://rkro7u5wd3t1", # 三楼内侧 + "c03_s03": "uid://bsqt2c061fmin", # 瞎子理发店 } +# Exports +@export var ignore_archive := false +@export var current_scene := "c02_s01" +@export var entrance_portal := "left" +@export var debug_reload := false: + set(new_val): + debug_reload = false + if is_node_ready() and current_scene and entrance_portal: + transition_to_scene(current_scene, entrance_portal, 0.0) + +# 强制覆盖 archive 记录 +@export var force_archive_scene := "" +@export var force_archive_portal := "" + +# Nodes +@onready var mask_layer := %MaskLayer as CanvasLayer +@onready var mask := %Mask as ColorRect + +# State +var has_entered := false +var ground: Ground2D +var display_start_sec := 0.0 +var _frozen_start_time_ms: int +var _allow_ground_start := false: + set(val): + _allow_ground_start = val + if ground and val: + if ground.process_mode != Node.PROCESS_MODE_INHERIT: + SceneManager.ground_start.emit() + ground.process_mode = Node.PROCESS_MODE_INHERIT + print( + "GroundLoader _allow_ground_start: unfrozen. frozen duration(ms):", + Time.get_ticks_msec() - _frozen_start_time_ms + ) + +# Debug +var update_watcher: Timer +var last_modify_time := 0 + func _ready() -> void: - mask.visible = true - mask.color.a = 0.0 - mask_layer.layer = GlobalConfig.CANVAS_LAYER_GROUND_MASK - # mask layer 独立的 always 处理模式,可以保证转场正常运行 - # toggle_mask = mask_layer.toggle_mask - ground = get_node_or_null("Ground") as Ground2D - if ground: - print("GroundLoader remove old ground:", ground.scene_name) - # remove_child(ground) - ground.queue_free() - # load save + _setup_mask_layer() if not ignore_archive: _load_save() if current_scene and entrance_portal: # 首次进入渐隐效果 transition_to_scene(current_scene, entrance_portal) - # transition_to_scene(current_scene, entrance_portal, 0.0) -# # var toggle_mask:Callable -# func toggle_mask(display: bool, mask_color: Color, wait_time: float) -> Tween: -# return mask_layer.toggle_mask(display, mask_color, wait_time) - -var display_start_sec = 0.0 +func _setup_mask_layer() -> void: + mask.visible = true + mask.color.a = 0.0 + mask_layer.layer = GlobalConfig.CANVAS_LAYER_GROUND_MASK -# wait_time 包含 ease in + wait + ease out 完整时长 -# ease duration = min(ease_min_duration, wait_time * 0.5) -func toggle_mask( - display: bool, wait_time: float, ease_min_duration := 0.3, mask_color := Color.BLACK -) -> Tween: - var tween = get_tree().create_tween() - mask_color.a = mask.color.a - mask.color = mask_color - var duration = min(ease_min_duration, wait_time * 0.5) - if display: - display_start_sec = Time.get_ticks_msec() * 0.001 - tween.tween_property(mask, "color:a", 1.0, duration).set_trans(Tween.TRANS_CUBIC) - else: - # 转场至少 0.6s, 除去 0.3s 最后的淡出,需要 0.3s 的等待时间(包含 mask 的淡入) - if wait_time: - var time = Time.get_ticks_msec() * 0.001 - wait_time = max(wait_time + display_start_sec - time - 0.3, 0.0) - if wait_time: - tween.tween_interval(wait_time) - tween.tween_property(mask, "color:a", 0.0, duration).set_trans(Tween.TRANS_CUBIC) - return tween - - -func _load_save(): +func _load_save() -> void: # 强制覆盖 archive 记录 if force_archive_scene or force_archive_portal: current_scene = force_archive_scene entrance_portal = force_archive_portal return - if not Engine.is_editor_hint(): - if ArchiveManager.archive.current_scene: - current_scene = ArchiveManager.archive.current_scene - if ArchiveManager.archive.entrance_portal: - entrance_portal = ArchiveManager.archive.entrance_portal + + if Engine.is_editor_hint(): + return + + var archive = ArchiveManager.archive + if archive: + if archive.current_scene: + current_scene = archive.current_scene + if archive.entrance_portal: + entrance_portal = archive.entrance_portal -func transition_to_scene(scene_name: String, portal: String, wait_time := 1.4) -> void: - if ground: - print("GroundLoader transition_to_scene: pause prev ground.") - # 先发送,再暂停,允许 sfx 等节点执行 ease out - SceneManager.ground_transition_pre_paused.emit() - ground.set_deferred("process_mode", Node.PROCESS_MODE_DISABLED) - # print reenter lock status - print("GroundLoader transition_to_scene: reenter lock status: ", ground.reenter_lock) - var scene_path = GROUND_SCENE_PATH_DICT.get(scene_name) - if scene_path: - current_scene = scene_name - entrance_portal = portal - # 优先更新 archive,使 ground 可以访问自己的 current_scene 键值 - if not Engine.is_editor_hint(): - _update_archive() - if wait_time > 0.0: - # 转场效果,在 _load_ground_node 之前播放 - var tween = toggle_mask(true, wait_time) - tween.tween_callback(_do_transition.call_deferred.bind(scene_name)) - _allow_ground_start = false - # 等到 toggle_mask 结束,再重置 freeze 状态 - toggle_mask(false, wait_time).tween_callback(func(): _allow_ground_start = true) - else: - _allow_ground_start = true - _do_transition.call_deferred(scene_name) +func toggle_mask( + display: bool, wait_time: float, ease_min_duration := EASE_DURATION, mask_color := Color.BLACK +) -> Tween: + var tween = get_tree().create_tween() + mask_color.a = mask.color.a + mask.color = mask_color + var duration = min(ease_min_duration, wait_time * 0.5) + + if display: + display_start_sec = Time.get_ticks_msec() * 0.001 + tween.tween_property(mask, "color:a", 1.0, duration).set_trans(Tween.TRANS_CUBIC) else: + # 转场至少 0.6s, 除去 0.3s 最后的淡出,需要 0.3s 的等待时间(包含 mask 的淡入) + if wait_time > 0.0: + var time = Time.get_ticks_msec() * 0.001 + wait_time = max(wait_time + display_start_sec - time - EASE_DURATION, 0.0) + if wait_time > 0.0: + tween.tween_interval(wait_time) + tween.tween_property(mask, "color:a", 0.0, duration).set_trans(Tween.TRANS_CUBIC) + + return tween + + +func transition_to_scene( + scene_name: String, portal: String, wait_time := DEFAULT_TRANSITION_TIME +) -> void: + if not GROUND_SCENE_PATH_DICT.has(scene_name): print("Scene not found: " + scene_name) + return + _pause_current_ground() + current_scene = scene_name + entrance_portal = portal + # 优先更新 archive,使 ground 可以访问自己的 current_scene 键值 + if not Engine.is_editor_hint(): + _update_archive() + if wait_time > 0.0: + _transition_with_effect(scene_name, wait_time) + else: + _allow_ground_start = true + _do_transition.call_deferred(scene_name) -var _frozen_start_time_ms: int -var _allow_ground_start := false: - set(val): - _allow_ground_start = val - if ground: - if val: - if ground.process_mode != Node.PROCESS_MODE_INHERIT: - # ground_start 信号 - SceneManager.ground_start.emit() - ground.process_mode = Node.PROCESS_MODE_INHERIT - print( - "GroundLoader _allow_ground_start: unfrozen. frozen duration(ms):", - Time.get_ticks_msec() - _frozen_start_time_ms - ) +func _pause_current_ground() -> void: + if not ground: + return + print("GroundLoader transition_to_scene: pause prev ground.") + # 先发送,再暂停,允许 sfx 等节点执行 ease out + SceneManager.ground_transition_pre_paused.emit() + ground.set_deferred("process_mode", Node.PROCESS_MODE_DISABLED) + # print reenter lock status + print("GroundLoader transition_to_scene: reenter lock status: ", ground.reenter_lock) -func _update_archive(): - ArchiveManager.archive.current_scene = current_scene - ArchiveManager.archive.entrance_portal = entrance_portal +func _transition_with_effect(scene_name: String, wait_time: float) -> void: + # 转场效果,在 _load_ground_node 之前播放 + var tween = toggle_mask(true, wait_time) + tween.tween_callback(_do_transition.call_deferred.bind(scene_name)) + _allow_ground_start = false + # 等到 toggle_mask 结束,再重置 freeze 状态 + toggle_mask(false, wait_time).tween_callback(func(): _allow_ground_start = true) + + +func _update_archive() -> void: + var archive = ArchiveManager.archive + if archive: + archive.current_scene = current_scene + archive.entrance_portal = entrance_portal func _do_transition(scene_name: String) -> void: - # SceneManager.freeze_player(0) print("GroundLoader Transition to scene:", scene_name, "portal:", entrance_portal) + _remove_current_ground() + _load_and_setup_ground(scene_name) + _add_ground() + if _allow_ground_start: + SceneManager.ground_start.emit() + _post_transition() + + +func _remove_current_ground() -> void: ground = get_node_or_null("Ground") as Ground2D if ground: # 防止命名冲突 remove_child(ground) ground.queue_free() + + +func _load_and_setup_ground(scene_name: String) -> void: # 先设置 ground,再添加到场景中 # 因为 ground 在 enter_tree 时会用到 SceneManager 的方法 # 其中间接用到了 GroundLoader 的 ground ground = _load_ground_node(scene_name) + if not _allow_ground_start: ground.set_deferred("process_mode", Node.PROCESS_MODE_DISABLED) print("GroundLoader not _allow_ground_start: frozen (delayed)") _frozen_start_time_ms = Time.get_ticks_msec() - _add_ground() - if _allow_ground_start: - # 如果不阻塞,直接 ground_start 信号 - SceneManager.ground_start.emit() - # 预先加载邻居场景 - _post_transition() - if GlobalConfig.DEBUG and not Engine.is_editor_hint(): - _watch_scene_update() -func _add_ground(): +func _add_ground() -> void: ground.ready.connect(SceneManager.ground_ready.emit.bind(ground)) ground.name = "Ground" + # 在 add child 之前,调整 ground 内部元素属性,在 on ground ready 前设置完成 if not Engine.is_editor_hint(): - # 更新玩家位置 - if not has_entered: - _update_player_position_from_archive() - else: - # move player to portal - ground.move_player_to_portal(entrance_portal) + _setup_player_position() add_child(ground) print( "GroundLoader add_ground finished:", @@ -216,85 +230,66 @@ func _add_ground(): has_entered = true -func _update_player_position_from_archive(): +func _setup_player_position() -> void: + if not has_entered: + _update_player_position_from_archive() + else: + ground.move_player_to_portal(entrance_portal) + + +func _update_player_position_from_archive() -> void: if ignore_archive or Engine.is_editor_hint(): return + var archive = ArchiveManager.archive + if not archive: + return var player = ground.get_player() as MainPlayer - player.global_position.x = ArchiveManager.archive.player_global_position_x - player.set_facing_direction(ArchiveManager.archive.player_direction) - ground.reset_player_y() + if player: + player.global_position.x = archive.player_global_position_x + player.set_facing_direction(archive.player_direction) + ground.reset_player_y() func _load_ground_node(scene_name: String) -> Ground2D: if not GROUND_SCENE_PATH_DICT.has(scene_name): return null var path = GROUND_SCENE_PATH_DICT[scene_name] - var scene: PackedScene + var scene: PackedScene = _load_scene_resource(path) + if not scene: + return null + var instance = scene.instantiate() as Node2D + var ground_node = instance.get_child(0) + instance.remove_child(ground_node) + ground_node.owner = null + instance.queue_free() + return ground_node + + +func _load_scene_resource(path: String) -> PackedScene: if ResourceLoader.load_threaded_get_status(path) == ResourceLoader.THREAD_LOAD_LOADED: - scene = ResourceLoader.load_threaded_get(path) as PackedScene + return ResourceLoader.load_threaded_get(path) as PackedScene else: - scene = ResourceLoader.load(path) as PackedScene - if scene: - var instance = scene.instantiate() as Node2D - var ground_node = instance.get_child(0) - instance.remove_child(ground_node) - ground_node.owner = null - instance.queue_free() - return ground_node - return null + return ResourceLoader.load(path) as PackedScene -# 读取 portals,预加载邻居场景 -func _post_transition(): - if ground: - var scene_names = [] - var deploy_layer = ground.get_node("DeployLayer") - if deploy_layer: - for node in deploy_layer.get_children(): - var portal = node as Portal2D - if not portal or not portal.target_scene: - continue - if GROUND_SCENE_PATH_DICT.has(portal.target_scene): - scene_names.append(portal.target_scene) - if scene_names: - for scene_name in scene_names: - ResourceLoader.load_threaded_request(GROUND_SCENE_PATH_DICT[scene_name]) - if GlobalConfig.DEBUG: - print("preload neighbor scenes:", scene_names) - GlobalConfigManager.print_global_info() - -var update_watcher: Timer -var last_modify_time = 0 +func _post_transition() -> void: + if not ground: + return + _preload_neighbor_scenes() + GlobalConfigManager.print_global_info() -# DEBUG 时重新加载资源 -func _watch_scene_update(): - var scene_path = GROUND_SCENE_PATH_DICT[current_scene] - if scene_path: - last_modify_time = FileAccess.get_modified_time(scene_path) - if not update_watcher: - update_watcher = Timer.new() - update_watcher.wait_time = 1 - update_watcher.one_shot = false - add_child(update_watcher) - update_watcher.start() - else: - # remove all connections - for c in update_watcher.timeout.get_connections(): - update_watcher.timeout.disconnect(c.callable) - update_watcher.timeout.connect(_check_scene_update.bind(scene_path)) - - -func _check_scene_update(scene_path): - var modify = FileAccess.get_modified_time(scene_path) - if modify != last_modify_time: - last_modify_time = modify - _on_resources_reload(scene_path) - - -func _on_resources_reload(res): - print("resources_reload processing:", res) - if not Engine.is_editor_hint() and res.ends_with(".tscn"): - ArchiveManager.save_all() - has_entered = false - transition_to_scene.call_deferred(current_scene, entrance_portal, 0.0) +func _preload_neighbor_scenes() -> void: + var scene_names: Array[String] = [] + var deploy_layer = ground.get_node_or_null("DeployLayer") + if not deploy_layer: + return + for node in deploy_layer.get_children(): + var portal = node as Portal2D + if portal and portal.target_scene and GROUND_SCENE_PATH_DICT.has(portal.target_scene): + scene_names.append(portal.target_scene) + if GlobalConfig.DEBUG: + print("preload neighbor scenes:", scene_names) + for scene_name in scene_names: + if GROUND_SCENE_PATH_DICT.has(scene_name): + ResourceLoader.load_threaded_request(GROUND_SCENE_PATH_DICT[scene_name]) diff --git a/scene/trailer.gd b/scene/trailer.gd index bc601da9..7e51ff69 100644 --- a/scene/trailer.gd +++ b/scene/trailer.gd @@ -1,68 +1,114 @@ extends Control -@onready var video_player = $VideoStreamPlayer -@onready var earplug_notice = $"耳机提示" as Control +# Constants +const EARPLUG_FADE_DURATION := 1.0 +const EARPLUG_DISPLAY_DURATION := 3.0 -@onready var sfx_click = $SfxClick as Sfx -@onready var settings = %"设置界面" as Control -@onready var lang_label = %LangLabel as Label -@onready var lang_left_btn = %LangLeft as Button -@onready var lang_right_btn = %LangRight as Button -@onready var caption_box = %CaptionBox as BoxContainer -@onready var caption_label = %CaptionLabel as Label -@onready var caption_left_btn = %CaptionLeft as Button -@onready var caption_right_btn = %CaptionRight as Button -@onready var confirm_btn = %ConfirmButton as Button +# Nodes +@onready var video_player := $VideoStreamPlayer +@onready var earplug_notice := $"耳机提示" as Control +@onready var sfx_click := $SfxClick as Sfx +@onready var settings := %"设置界面" as Control +@onready var lang_label := %LangLabel as Label +@onready var lang_left_btn := %LangLeft as Button +@onready var lang_right_btn := %LangRight as Button +@onready var caption_box := %CaptionBox as BoxContainer +@onready var caption_label := %CaptionLabel as Label +@onready var caption_left_btn := %CaptionLeft as Button +@onready var caption_right_btn := %CaptionRight as Button +@onready var confirm_btn := %ConfirmButton as Button +# Resources var packed_index_page := preload("res://scene/index_page.tscn") +# State var first_launching_game := true +var earplug_notice_tween: Tween func _ready() -> void: earplug_notice.hide() - if GlobalConfigManager.config: - var game_launched_times = GlobalConfigManager.config.game_launched_times - GlobalConfigManager.config.game_launched_times = game_launched_times + 1 - first_launching_game = game_launched_times == 0 - if GlobalConfigManager.config.skip_trailer: - SceneManager.checkout_index_page(false) - return + if not GlobalConfigManager.config: + SceneManager.checkout_index_page(false) + return + var game_launched_times = GlobalConfigManager.config.game_launched_times + GlobalConfigManager.config.game_launched_times += 1 + first_launching_game = (game_launched_times == 0) + if GlobalConfigManager.config.skip_trailer: + SceneManager.checkout_index_page(false) + return + _setup_video_player() + _setup_language_settings(game_launched_times) + _connect_button_signals() + + +func _setup_video_player() -> void: video_player.play() video_player.finished.connect(_on_video_finished) + +func _setup_language_settings(game_launched_times: int) -> void: + if game_launched_times == 0: + _read_system_locale() + _update_language_display() + + +func _update_language_display() -> void: lang_label.text = GlobalConfigManager.get_locale_language_name() caption_label.text = GlobalConfigManager.get_locale_caption_name() - if caption_label.text == "": - caption_box.hide() - lang_left_btn.pressed.connect(_on_lang_left_btn_pressed) - lang_right_btn.pressed.connect(_on_lang_right_btn_pressed) - caption_left_btn.pressed.connect(_on_caption_left_btn_pressed) - caption_right_btn.pressed.connect(_on_caption_right_btn_pressed) + caption_box.visible = (caption_label.text != "") -var earplug_notice_tween: Tween +func _connect_button_signals() -> void: + lang_left_btn.pressed.connect(_on_lang_change.bind(-1)) + lang_right_btn.pressed.connect(_on_lang_change.bind(1)) + caption_left_btn.pressed.connect(_on_caption_change.bind(-1)) + caption_right_btn.pressed.connect(_on_caption_change.bind(1)) + + +func _read_system_locale() -> void: + if not GlobalConfigManager.config: + return + var locale = OS.get_locale() + var prefix = locale.split("_")[0] + var config = GlobalConfigManager.config + if GlobalConfig.LOCALE_PREFIX_MAPPING.has(prefix): + config.language = GlobalConfig.LOCALE_PREFIX_MAPPING.get(prefix) + GlobalConfigManager.update_locale(config.language, config.caption) + print("[FirstLaunch] System locale: ", locale, " -> Language ID: ", GlobalConfigManager.config.language) func _on_video_finished() -> void: earplug_notice.visible = true earplug_notice.modulate.a = 0 earplug_notice_tween = create_tween() - earplug_notice_tween.tween_property(earplug_notice, "modulate:a", 1.0, 1.0) - if not first_launching_game: - settings.hide() - earplug_notice_tween.tween_interval(3.0) - earplug_notice_tween.tween_property(earplug_notice, "modulate:a", 0.0, 1.0) - earplug_notice_tween.finished.connect(_on_earplug_notice_finished) + earplug_notice_tween.tween_property(earplug_notice, "modulate:a", 1.0, EARPLUG_FADE_DURATION) + if first_launching_game: + _show_first_launch_settings() else: - settings.show() - confirm_btn.pressed.connect(_on_confirm_btn_pressed) + _show_regular_launch_sequence() + + +func _show_first_launch_settings() -> void: + settings.show() + confirm_btn.pressed.connect(_on_confirm_btn_pressed) + + +func _show_regular_launch_sequence() -> void: + settings.hide() + earplug_notice_tween.tween_interval(EARPLUG_DISPLAY_DURATION) + earplug_notice_tween.tween_property(earplug_notice, "modulate:a", 0.0, EARPLUG_FADE_DURATION) + earplug_notice_tween.finished.connect(_on_earplug_notice_finished) func _on_confirm_btn_pressed() -> void: confirm_btn.disabled = true + _fade_out_and_continue() + + +func _fade_out_and_continue() -> void: var tween = create_tween() - tween.tween_property(earplug_notice, "modulate:a", 0.0, 1.0) + tween.tween_property(earplug_notice, "modulate:a", 0.0, EARPLUG_FADE_DURATION) tween.finished.connect(_on_earplug_notice_finished) @@ -71,49 +117,37 @@ func _on_earplug_notice_finished() -> void: func _unhandled_input(event: InputEvent) -> void: - if event.is_action_pressed("escape") and not first_launching_game: - if video_player.is_playing(): - get_viewport().set_input_as_handled() - video_player.stop() - _on_video_finished() - elif earplug_notice_tween and earplug_notice_tween.is_running(): - get_viewport().set_input_as_handled() - earplug_notice_tween.kill() - earplug_notice.modulate.a = 0 - _on_earplug_notice_finished() + if not event.is_action_pressed("escape") or first_launching_game: + return + + if video_player.is_playing(): + _skip_video() + elif earplug_notice_tween and earplug_notice_tween.is_running(): + _skip_earplug_notice() -func _on_lang_left_btn_pressed() -> void: +func _skip_video() -> void: + get_viewport().set_input_as_handled() + video_player.stop() + _on_video_finished() + + +func _skip_earplug_notice() -> void: + get_viewport().set_input_as_handled() + earplug_notice_tween.kill() + earplug_notice.modulate.a = 0 + _on_earplug_notice_finished() + + +func _on_lang_change(direction: int) -> void: sfx_click.play() - GlobalConfigManager.update_locale( - GlobalConfigManager.config.language - 1, GlobalConfigManager.config.caption - ) - lang_label.text = GlobalConfigManager.get_locale_language_name() - caption_label.text = GlobalConfigManager.get_locale_caption_name() - caption_box.visible = caption_label.text != "" + var config = GlobalConfigManager.config + GlobalConfigManager.update_locale(config.language + direction, config.caption) + _update_language_display() -func _on_lang_right_btn_pressed() -> void: +func _on_caption_change(direction: int) -> void: sfx_click.play() - GlobalConfigManager.update_locale( - GlobalConfigManager.config.language + 1, GlobalConfigManager.config.caption - ) - lang_label.text = GlobalConfigManager.get_locale_language_name() - caption_label.text = GlobalConfigManager.get_locale_caption_name() - caption_box.visible = caption_label.text != "" - - -func _on_caption_left_btn_pressed() -> void: - sfx_click.play() - GlobalConfigManager.update_locale( - GlobalConfigManager.config.language, GlobalConfigManager.config.caption - 1 - ) - caption_label.text = GlobalConfigManager.get_locale_caption_name() - - -func _on_caption_right_btn_pressed() -> void: - sfx_click.play() - GlobalConfigManager.update_locale( - GlobalConfigManager.config.language, GlobalConfigManager.config.caption + 1 - ) - caption_label.text = GlobalConfigManager.get_locale_caption_name() + var config = GlobalConfigManager.config + GlobalConfigManager.update_locale(config.language, config.caption + direction) + caption_label.text = GlobalConfigManager.get_locale_caption_name() \ No newline at end of file diff --git a/scene/ux/prop_hud.gd b/scene/ux/prop_hud.gd index 6f161531..d5a2caed 100644 --- a/scene/ux/prop_hud.gd +++ b/scene/ux/prop_hud.gd @@ -10,6 +10,7 @@ const TWEEN_DURATION = 0.5 const SHAKE_FPS = 15.0 const SHAKE_DURATION = 0.8 const SHAKE_DELTA = 12.0 +const HUD_FADE_DURATION = 0.3 @export_group("DebugItem") @export var enable_item := false: @@ -73,6 +74,7 @@ var listen_mouse = false var displaying = false var timer := Timer.new() var display_tween: Tween +var hud_visibility_tween: Tween var container_tween: Tween var prop_blink: Tween var tween_scroll: Tween @@ -187,11 +189,6 @@ func checkout_inventory(character: String) -> void: _reload_cache_and_realign_display() -const HUD_FADE_DURATION = 0.3 - -# 在变量定义区域添加 -var hud_visibility_tween: Tween - func hide_hud(duration: float = HUD_FADE_DURATION): if not visible: return