Merge remote-tracking branch 'origin/demo'

This commit is contained in:
cakipaul 2025-07-16 20:02:01 +08:00
commit 87da4d2e73
8 changed files with 772 additions and 567 deletions

View File

@ -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
@ -13,14 +25,6 @@ var archive: AssembledArchive:
print_rich("[color=brown] release_stage = %s[/color]" % GlobalConfig.RELEASE_STAGE)
# 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()
@ -32,24 +36,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()
@ -59,14 +61,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)
@ -76,16 +91,19 @@ func _on_archive_id_changed():
load_archive()
func check_autosave_options():
if (
GlobalConfigManager.config.auto_save_enabled
func check_autosave_options() -> void:
var config = GlobalConfigManager.config
var should_enable_autosave = (
config.auto_save_enabled
and archive
and GlobalConfigManager.config.auto_save_seconds > 1
):
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()
@ -93,15 +111,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]
@ -109,71 +128,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
@ -189,13 +245,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
@ -208,41 +258,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:
@ -250,8 +313,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) + " 号存档", "查找")
@ -262,7 +324,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)
@ -281,9 +343,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:
@ -291,7 +351,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:
@ -302,14 +364,14 @@ 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")
# 供运行时缓存跨场景数据

View File

@ -15,6 +15,16 @@ const RELEASE_STAGE := 1
# .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
@ -67,7 +77,7 @@ signal auto_save_seconds_changed
@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 为未选择
@ -103,3 +113,20 @@ signal auto_save_seconds_changed
if streamer_mode != val:
streamer_mode = val
streamer_mode_updated.emit()
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]

View File

@ -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]

View File

@ -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]

View File

@ -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():
@ -27,42 +42,24 @@ var default_portal := "left"
@export_tool_button("玩家重置到 portal") var _a1 = move_player_to_portal.bind(default_portal)
@export_tool_button("玩家重置 y 高度") var _a2 = reset_player_y
# 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
@ -72,37 +69,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:
@ -121,43 +139,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)
@ -168,29 +208,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)

View File

@ -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,9 +33,9 @@ static var GROUND_SCENE_PATH_DICT = {
"c02_s16": "uid://22hc3oe8t0id",
"c02_s17": "uid://cbr6gbgrl2wb1",
"c02_s18": "uid://d27gv3pbkn4b8",
"c03_s01": "uid://dlrbhfvnd3cs0", # s01_三楼
"c03_s02": "uid://ctwy1ubhm68la", # s03_瞎子卧室
"c03_s03": "uid://bsqt2c061fmin", # s02_瞎子理发店
"c03_s01": "uid://dlrbhfvnd3cs0", # s01_三楼
"c03_s02": "uid://ctwy1ubhm68la", # s03_瞎子卧室
"c03_s03": "uid://bsqt2c061fmin", # s02_瞎子理发店
"c03_s04": "uid://c7c88hg2cl1j7", # s04_李癞房间
"c03_s05": "uid://6ehb3ux2kilu", # s05_肉铺
"c03_s06": "uid://cxacrp8mrrbry", # s06_胖子卧室
@ -58,157 +44,185 @@ static var GROUND_SCENE_PATH_DICT = {
"c03_s09": "uid://dfln301xllqpn", # s09_棺材房
}
# 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:",
@ -222,86 +236,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()
func _post_transition() -> void:
if not ground:
return
_preload_neighbor_scenes()
GlobalConfigManager.print_global_info()
var update_watcher: Timer
var last_modify_time = 0
# 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])

View File

@ -1,68 +1,113 @@
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
var packed_index_page := preload("uid://c4ycvdsabi7lw")
# 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 +116,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
)
var config = GlobalConfigManager.config
GlobalConfigManager.update_locale(config.language, config.caption + direction)
caption_label.text = GlobalConfigManager.get_locale_caption_name()

View File

@ -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:
@ -63,6 +64,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
@ -174,11 +176,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