Compare commits
8 Commits
c720a78161
...
571c6ab74c
Author | SHA1 | Date | |
---|---|---|---|
571c6ab74c | |||
850900ad5f | |||
4e3183b013 | |||
2f431cff52 | |||
a60beeea1d | |||
87da4d2e73 | |||
e42b434bed | |||
746a25fd5b |
@ -3,24 +3,30 @@ 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_data_root_dir := "user://data/" # must end with "/"
|
||||
static var user_archives_dir := "user://data/archives/"
|
||||
static var archive_prefix := "save"
|
||||
|
||||
# Archive management
|
||||
var archive: AssembledArchive:
|
||||
set(val):
|
||||
archive = val
|
||||
if archive:
|
||||
# emit signal
|
||||
archive_loaded.emit()
|
||||
GlobalConfigManager.print_global_info()
|
||||
print("use archive ", archive.resource_path)
|
||||
archive.event_stage["release_stage"] = GlobalConfig.RELEASE_STAGE
|
||||
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 +38,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 +63,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 +93,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()
|
||||
@ -93,15 +113,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,72 +130,109 @@ 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_data_root_dir)
|
||||
_ensure_directory_exists(user_archives_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:
|
||||
var archive_dir_access = DirAccess.open(user_archives_dir)
|
||||
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(user_archives_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:
|
||||
var archive_path = _get_archive_path(id)
|
||||
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
|
||||
if take_over_path:
|
||||
@ -188,15 +246,9 @@ 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)
|
||||
return archive_dir + archive_prefix + id_str + GlobalConfig.RES_FILE_FORMAT
|
||||
func get_archive_path(id: int) -> String:
|
||||
var id_str := str(id).pad_zeros(ARCHIVE_ID_DIGITS)
|
||||
return user_archives_dir + archive_prefix + id_str + GlobalConfig.RES_FILE_FORMAT
|
||||
|
||||
|
||||
func allow_resume(id := 1) -> bool:
|
||||
@ -208,41 +260,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
|
||||
var path = user_data_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)
|
||||
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 GlobalConfigManager.config:
|
||||
_create_default_config(path)
|
||||
if not Engine.is_editor_hint():
|
||||
_connect_config_signals()
|
||||
|
||||
|
||||
func _create_default_config(path: String) -> void:
|
||||
var config = GlobalConfig.new()
|
||||
config.version = CURRENT_VERSION
|
||||
config.resource_path = path
|
||||
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,19 +315,16 @@ 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) + " 号存档", "查找")
|
||||
return
|
||||
archive = archives_dict[selected_id]
|
||||
# emit signal
|
||||
archive_loaded.emit()
|
||||
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")
|
||||
|
||||
|
||||
# 供运行时缓存跨场景数据
|
||||
|
@ -11,8 +11,8 @@ class_name AssembledArchive extends Resource
|
||||
printerr("[AssembledArchive] current_scene is not valid: " + val)
|
||||
return
|
||||
# 尝试后台预先加载该场景
|
||||
if GroundLoader.GROUND_SCENE_PATH_DICT.has(val):
|
||||
var path = GroundLoader.GROUND_SCENE_PATH_DICT[val]
|
||||
var path = GroundLoader.get_ground_scene_uid(val)
|
||||
if path:
|
||||
if GlobalConfig.DEBUG:
|
||||
print("[AssembledArchive] current_scene: " + current_scene)
|
||||
ResourceLoader.load_threaded_request(path, "PackedScene")
|
||||
|
314
manager/archive_manager/savings_panel.gd
Normal file
314
manager/archive_manager/savings_panel.gd
Normal file
@ -0,0 +1,314 @@
|
||||
extends Control
|
||||
|
||||
# UI References
|
||||
@onready var quit_debug_button: Button = %QuitDebugModeButton
|
||||
@onready var archive_grid: GridContainer = %ArchiveGrid
|
||||
@onready var scroll_container: ScrollContainer = %ScrollContainer
|
||||
@onready var current_archive_label: Label = %CurrentArchiveLabel
|
||||
@onready var name_input: LineEdit = %NameInput
|
||||
@onready var save_button: Button = %SaveButton
|
||||
@onready var refresh_button: Button = %RefreshButton
|
||||
# Constants
|
||||
const MAX_MANUAL_ARCHIVES = 99
|
||||
const GRID_COLUMNS = 4
|
||||
|
||||
# Variables
|
||||
var manual_archives: Dictionary = {} # {id: {name: String, path: String, time: String}}
|
||||
var next_available_id: int = 2 # Start from 2 since 1 is reserved for main archive
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
get_parent().layer = GlobalConfig.CANVAS_LAYER_SETTINGS
|
||||
# Setup UI
|
||||
archive_grid.columns = GRID_COLUMNS
|
||||
# Connect signals
|
||||
quit_debug_button.pressed.connect(_on_quit_debug_button_pressed)
|
||||
save_button.pressed.connect(_on_save_button_pressed)
|
||||
refresh_button.pressed.connect(_refresh_archive_list)
|
||||
name_input.text_submitted.connect(_on_name_submitted)
|
||||
# Set default name
|
||||
_update_default_name()
|
||||
# Initial load
|
||||
_refresh_archive_list()
|
||||
_update_current_archive_label()
|
||||
|
||||
|
||||
func _update_current_archive_label() -> void:
|
||||
current_archive_label.text = "当前使用存档:1号存档(主存档)"
|
||||
|
||||
|
||||
func _update_default_name() -> void:
|
||||
name_input.placeholder_text = "输入存档名称"
|
||||
var chapter_name = EventManager.get_chapter_stage()
|
||||
if chapter_name == 1:
|
||||
chapter_name = "序章"
|
||||
elif chapter_name <= 5:
|
||||
chapter_name = "第%s章" % (chapter_name - 1)
|
||||
elif chapter_name == 6:
|
||||
chapter_name = "结尾"
|
||||
else:
|
||||
chapter_name = "未知"
|
||||
var scene_name = SceneManager.get_current_scene_name()
|
||||
var saving_name = chapter_name + "_" + scene_name
|
||||
name_input.text = _get_unique_archive_name(saving_name)
|
||||
|
||||
|
||||
func _on_name_submitted(_text: String) -> void:
|
||||
_on_save_button_pressed()
|
||||
|
||||
|
||||
func _on_quit_debug_button_pressed() -> void:
|
||||
# 不写入配置
|
||||
GlobalConfig.DEBUG = false
|
||||
quit()
|
||||
|
||||
|
||||
func _on_save_button_pressed() -> void:
|
||||
# Check limit
|
||||
if manual_archives.size() >= MAX_MANUAL_ARCHIVES:
|
||||
_show_notification("已达到最大存档数量限制(99个)")
|
||||
return
|
||||
# Get and validate name
|
||||
var archive_name = name_input.text.strip_edges()
|
||||
if archive_name.is_empty():
|
||||
archive_name = "未命名存档_" + Time.get_datetime_string_from_system()
|
||||
# Save current progress
|
||||
ArchiveManager.save_all()
|
||||
# Get unique name
|
||||
archive_name = _get_unique_archive_name(archive_name)
|
||||
# Copy current archive
|
||||
_copy_current_archive(archive_name)
|
||||
# Reset input field
|
||||
_update_default_name()
|
||||
name_input.select_all()
|
||||
|
||||
|
||||
func _get_unique_archive_name(base_name: String) -> String:
|
||||
var final_name = base_name
|
||||
var counter = 1
|
||||
|
||||
# Check if name already exists
|
||||
var name_exists = true
|
||||
while name_exists:
|
||||
name_exists = false
|
||||
for data in manual_archives.values():
|
||||
if data.name == final_name:
|
||||
name_exists = true
|
||||
final_name = base_name + "_" + str(counter)
|
||||
counter += 1
|
||||
break
|
||||
|
||||
return final_name
|
||||
|
||||
|
||||
func _copy_current_archive(archive_name: String) -> void:
|
||||
# Get current archive path
|
||||
var current_archive = ArchiveManager.archive
|
||||
if not current_archive:
|
||||
_show_notification("当前没有活动存档")
|
||||
return
|
||||
|
||||
# Find next available ID
|
||||
while manual_archives.has(next_available_id) and next_available_id <= MAX_MANUAL_ARCHIVES + 1:
|
||||
next_available_id += 1
|
||||
|
||||
if next_available_id > MAX_MANUAL_ARCHIVES + 1:
|
||||
_show_notification("无法创建更多存档")
|
||||
return
|
||||
|
||||
# Create new archive path
|
||||
var new_archive_path = (
|
||||
ArchiveManager.user_archives_dir
|
||||
+ "manual_"
|
||||
+ str(next_available_id)
|
||||
+ "_"
|
||||
+ archive_name.validate_filename()
|
||||
+ GlobalConfig.RES_FILE_FORMAT
|
||||
)
|
||||
|
||||
# Copy the archive file
|
||||
var source_path = current_archive.resource_path
|
||||
var dir = DirAccess.open(ArchiveManager.user_archives_dir)
|
||||
if dir:
|
||||
var error = dir.copy(source_path, new_archive_path)
|
||||
print("Copying archive from: ", source_path, " to: ", new_archive_path)
|
||||
if error == OK:
|
||||
# Save manual archive info
|
||||
manual_archives[next_available_id] = {
|
||||
"name": archive_name,
|
||||
"path": new_archive_path,
|
||||
"time": Time.get_datetime_string_from_system(false, true)
|
||||
}
|
||||
# Save manual archives data
|
||||
_save_manual_archives_data()
|
||||
# Refresh UI
|
||||
_refresh_archive_list()
|
||||
_show_notification("存档已保存:" + archive_name)
|
||||
next_available_id += 1
|
||||
else:
|
||||
_show_notification("存档复制失败:" + error_string(error))
|
||||
else:
|
||||
_show_notification("无法访问存档目录")
|
||||
|
||||
|
||||
func _refresh_archive_list() -> void:
|
||||
# refresh savings name
|
||||
_update_default_name()
|
||||
|
||||
# Clear existing items
|
||||
for child in archive_grid.get_children():
|
||||
child.queue_free()
|
||||
|
||||
# Load manual archives data
|
||||
_load_manual_archives_data()
|
||||
|
||||
# Create UI items for each manual archive
|
||||
var sorted_ids = manual_archives.keys()
|
||||
sorted_ids.sort()
|
||||
|
||||
for id in sorted_ids:
|
||||
var data = manual_archives[id]
|
||||
_create_archive_item(id, data)
|
||||
|
||||
|
||||
func _create_archive_item(id: int, data: Dictionary) -> void:
|
||||
# Create container for the archive item
|
||||
var item_container = PanelContainer.new()
|
||||
item_container.custom_minimum_size = Vector2(200, 60)
|
||||
|
||||
var vbox = VBoxContainer.new()
|
||||
vbox.add_theme_constant_override("separation", 4)
|
||||
item_container.add_child(vbox)
|
||||
|
||||
# Archive name (editable)
|
||||
var name_edit = LineEdit.new()
|
||||
name_edit.text = data.name
|
||||
name_edit.tooltip_text = "创建时间:" + data.time
|
||||
vbox.add_child(name_edit)
|
||||
|
||||
# Time label
|
||||
var time_label = Label.new()
|
||||
time_label.text = data.time
|
||||
time_label.add_theme_font_size_override("font_size", 12)
|
||||
time_label.modulate.a = 0.7
|
||||
vbox.add_child(time_label)
|
||||
|
||||
# Action buttons container
|
||||
var button_container = HBoxContainer.new()
|
||||
button_container.add_theme_constant_override("separation", 4)
|
||||
|
||||
# Load button
|
||||
var load_btn = Button.new()
|
||||
load_btn.text = "加载"
|
||||
load_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button_container.add_child(load_btn)
|
||||
|
||||
# Delete button
|
||||
var delete_btn = Button.new()
|
||||
delete_btn.text = "删除"
|
||||
delete_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button_container.add_child(delete_btn)
|
||||
|
||||
vbox.add_child(button_container)
|
||||
# Connect signals
|
||||
name_edit.text_changed.connect(func(new_text): _on_archive_renamed(id, new_text))
|
||||
load_btn.pressed.connect(func(): _load_manual_archive(id, data))
|
||||
delete_btn.pressed.connect(func(): _delete_manual_archive(id))
|
||||
archive_grid.add_child(item_container)
|
||||
|
||||
|
||||
func _on_archive_renamed(id: int, new_name: String) -> void:
|
||||
if new_name.strip_edges().is_empty():
|
||||
return
|
||||
# Update name in data
|
||||
manual_archives[id].name = new_name.strip_edges()
|
||||
# Save changes
|
||||
_save_manual_archives_data()
|
||||
|
||||
|
||||
func _load_manual_archive(_id: int, data: Dictionary) -> void:
|
||||
# Save current state first
|
||||
ArchiveManager.save_all()
|
||||
# Copy manual archive to archive 1
|
||||
var dir = DirAccess.open(ArchiveManager.user_archives_dir)
|
||||
if dir:
|
||||
var saving_archive = load(data.path)
|
||||
if saving_archive:
|
||||
var target_path = ArchiveManager.get_archive_path(1)
|
||||
print("Loading archive from: ", data.path, " to: ", target_path)
|
||||
GlobalConfigManager.config.current_selected_archive_id = 1
|
||||
if ArchiveManager.archive:
|
||||
saving_archive.take_over_path(target_path)
|
||||
else:
|
||||
saving_archive.resource_path = target_path
|
||||
ArchiveManager.archives_dict[1] = saving_archive
|
||||
ArchiveManager.archive = saving_archive
|
||||
print("Loading archive from: ", data.path, " to: ", target_path)
|
||||
# Reload current scene
|
||||
get_tree().reload_current_scene()
|
||||
else:
|
||||
_show_notification("加载存档失败:" + data.name)
|
||||
|
||||
|
||||
func _delete_manual_archive(id: int) -> void:
|
||||
var data = manual_archives[id]
|
||||
# Delete file
|
||||
var dir = DirAccess.open(ArchiveManager.user_archives_dir)
|
||||
if dir:
|
||||
dir.remove(data.path)
|
||||
# Remove from dictionary
|
||||
manual_archives.erase(id)
|
||||
# Save changes
|
||||
_save_manual_archives_data()
|
||||
# Refresh UI
|
||||
_refresh_archive_list()
|
||||
_show_notification("已删除存档:" + data.name)
|
||||
|
||||
|
||||
func _save_manual_archives_data() -> void:
|
||||
var save_path = ArchiveManager.user_data_root_dir + "test_manual_archives.dat"
|
||||
var file = FileAccess.open(save_path, FileAccess.WRITE)
|
||||
if file:
|
||||
file.store_var(manual_archives)
|
||||
file.close()
|
||||
|
||||
|
||||
func _load_manual_archives_data() -> void:
|
||||
var save_path = ArchiveManager.user_data_root_dir + "test_manual_archives.dat"
|
||||
if FileAccess.file_exists(save_path):
|
||||
var file = FileAccess.open(save_path, FileAccess.READ)
|
||||
if file:
|
||||
manual_archives = file.get_var()
|
||||
file.close()
|
||||
# Find next available ID
|
||||
next_available_id = 2
|
||||
for id in manual_archives.keys():
|
||||
if id >= next_available_id:
|
||||
next_available_id = id + 1
|
||||
|
||||
|
||||
func _show_notification(message: String) -> void:
|
||||
SceneManager.pop_notification(message)
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
SceneManager.toggle_pause_counter(true, "savings")
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
SceneManager.toggle_pause_counter(false, "savings")
|
||||
|
||||
|
||||
func quit() -> void:
|
||||
queue_free()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
# savings 界面接受所有输入事件
|
||||
get_viewport().set_input_as_handled()
|
||||
if (
|
||||
event.is_action_pressed("savings")
|
||||
or event.is_action_pressed("cancel")
|
||||
or event.is_action_pressed("escape")
|
||||
):
|
||||
quit()
|
1
manager/archive_manager/savings_panel.gd.uid
Normal file
1
manager/archive_manager/savings_panel.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cirf1nw72l315
|
97
manager/archive_manager/savings_panel.tscn
Normal file
97
manager/archive_manager/savings_panel.tscn
Normal file
@ -0,0 +1,97 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://d4jeeteyq8kk3"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cirf1nw72l315" path="res://manager/archive_manager/savings_panel.gd" id="1_oo2ip"]
|
||||
|
||||
[node name="SavingsLayer" type="CanvasLayer"]
|
||||
|
||||
[node name="TestArchivePanel" type="PanelContainer" parent="."]
|
||||
process_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 59.0
|
||||
offset_top = 21.0
|
||||
offset_right = 395.0
|
||||
offset_bottom = 262.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
scale = Vector2(0.5, 0.5)
|
||||
script = ExtResource("1_oo2ip")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="TestArchivePanel"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="TestArchivePanel/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Title" type="Label" parent="TestArchivePanel/VBoxContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_font_sizes/font_size = 24
|
||||
text = "存档测试管理器(测试专用)"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="QuitDebugModeButton" type="Button" parent="TestArchivePanel/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "关闭 Debug 模式"
|
||||
|
||||
[node name="存档管理" type="VBoxContainer" parent="TestArchivePanel/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="CurrentArchiveLabel" type="Label" parent="TestArchivePanel/VBoxContainer/存档管理"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.7, 0.9, 0.7, 1)
|
||||
text = "当前使用存档:1号存档(主存档)"
|
||||
|
||||
[node name="HSeparator" type="HSeparator" parent="TestArchivePanel/VBoxContainer/存档管理"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SaveContainer" type="HBoxContainer" parent="TestArchivePanel/VBoxContainer/存档管理"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="NameInput" type="LineEdit" parent="TestArchivePanel/VBoxContainer/存档管理/SaveContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "输入存档名称"
|
||||
|
||||
[node name="SaveButton" type="Button" parent="TestArchivePanel/VBoxContainer/存档管理/SaveContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(120, 0)
|
||||
layout_mode = 2
|
||||
text = "保存当前进度"
|
||||
|
||||
[node name="RefreshButton" type="Button" parent="TestArchivePanel/VBoxContainer/存档管理/SaveContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(60, 0)
|
||||
layout_mode = 2
|
||||
text = "刷新"
|
||||
|
||||
[node name="HSeparator2" type="HSeparator" parent="TestArchivePanel/VBoxContainer/存档管理"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="TestArchivePanel/VBoxContainer/存档管理"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.8, 0.8, 0.8, 1)
|
||||
text = "手动存档列表:"
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="TestArchivePanel/VBoxContainer/存档管理"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 380)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
horizontal_scroll_mode = 0
|
||||
vertical_scroll_mode = 2
|
||||
|
||||
[node name="ArchiveGrid" type="GridContainer" parent="TestArchivePanel/VBoxContainer/存档管理/ScrollContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/h_separation = 10
|
||||
theme_override_constants/v_separation = 10
|
||||
columns = 4
|
@ -2,21 +2,27 @@ class_name GlobalConfig extends Resource
|
||||
|
||||
signal streamer_mode_updated
|
||||
|
||||
#const DEBUG = true
|
||||
static var DEBUG = false
|
||||
# 影响事件的 release_stage
|
||||
# 0:demo
|
||||
# 1:v1.0正式版
|
||||
const RELEASE_STAGE := 1
|
||||
|
||||
# 与 Editor 编辑器有 Debugger 连接
|
||||
# static var EDITOR = 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
|
||||
# 设置
|
||||
# 设置, DebugPanel
|
||||
const CANVAS_LAYER_SETTINGS = 30
|
||||
# note
|
||||
const CANVAS_LAYER_NOTE = 25
|
||||
@ -64,10 +70,10 @@ signal current_selected_archive_id_changed
|
||||
signal auto_save_enabled_changed
|
||||
signal auto_save_seconds_changed
|
||||
|
||||
@export var version: int #存档版本
|
||||
@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 +109,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]
|
||||
|
@ -1,129 +1,161 @@
|
||||
@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]
|
||||
|
@ -53,6 +53,13 @@ func get_ground() -> Ground2D:
|
||||
return null
|
||||
|
||||
|
||||
func get_current_scene_name() -> String:
|
||||
var ground = get_ground()
|
||||
if ground:
|
||||
return GroundLoader.get_ground_scene_readable_name(ground.scene_name)
|
||||
return ""
|
||||
|
||||
|
||||
func get_camera_marker() -> CameraFocusMarker:
|
||||
var ground = get_ground()
|
||||
if ground:
|
||||
@ -356,6 +363,15 @@ func enter_main_scene() -> void:
|
||||
get_tree().paused = false
|
||||
|
||||
|
||||
#### Savings
|
||||
|
||||
var savings_scene = preload("uid://d4jeeteyq8kk3")
|
||||
|
||||
|
||||
func show_savings() -> void:
|
||||
get_tree().current_scene.add_child(savings_scene.instantiate())
|
||||
|
||||
|
||||
#### UX: settings; panel: note, bag, memory ####
|
||||
|
||||
var panel_scene = preload("uid://ddlwnsccsmr8u")
|
||||
@ -394,7 +410,7 @@ func show_settings() -> void:
|
||||
|
||||
|
||||
#### 游戏场景树暂停计数器,设置、memory、bag 等菜单都会导致 pause
|
||||
## 目前有(5类): settings, panel, bag, note, memory
|
||||
## 目前有(6类): settings, panel, bag, note, memory, savings
|
||||
var pause_counter_arr: Array[String] = []
|
||||
var pause_counter_mutex := Mutex.new()
|
||||
|
||||
|
@ -211,6 +211,11 @@ panel={
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":80,"key_label":0,"unicode":112,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
savings={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":96,"key_label":0,"unicode":96,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[internationalization]
|
||||
|
||||
@ -242,6 +247,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]
|
||||
|
||||
|
@ -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")
|
||||
_validate_scene_name()
|
||||
_set_camera_and_player_boundary()
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
# 隐藏 player_line
|
||||
_setup_scene()
|
||||
_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)
|
||||
|
@ -1,6 +1,50 @@
|
||||
class_name GroundLoader extends Node2D
|
||||
|
||||
@export_group("Scene")
|
||||
# Constants
|
||||
const DEFAULT_TRANSITION_TIME := 1.4
|
||||
const MIN_TRANSITION_TIME := 0.6
|
||||
const EASE_DURATION := 0.3
|
||||
|
||||
# Scene name to path mapping
|
||||
const GROUND_SCENE_PATH_DICT: Dictionary[String, Dictionary] = {
|
||||
"c01_s05": {"path": "uid://dlx5xxbg53rb8", "name": "院长房间"},
|
||||
"c01_s06": {"path": "uid://bx16c8nn32f40", "name": "孤儿院长廊"},
|
||||
"c01_s07": {"path": "uid://ds2iyfndwamiy", "name": "书店外"},
|
||||
"c01_s08": {"path": "uid://cwu4dhayra8pg", "name": "书店"},
|
||||
"c01_s09": {"path": "uid://c777lv8mjojcw", "name": "公寓楼外"},
|
||||
"c01_s10": {"path": "uid://be57l2o3vxxtm", "name": "公寓楼道"},
|
||||
"c01_s11": {"path": "uid://coiumaaenimbc", "name": "黄包车"},
|
||||
"c01_s12": {"path": "uid://bol5hl68pbpgq", "name": "诡异书店外"},
|
||||
"c02_s01": {"path": "uid://bbs7yy5aofw1v", "name": "公寓门口"},
|
||||
"c02_s02": {"path": "uid://brck77w81fhvc", "name": "公寓楼道"},
|
||||
"c02_s03": {"path": "uid://djc2uaefhmu7", "name": "一楼院子"},
|
||||
"c02_s04": {"path": "uid://bivc5cdap370p", "name": "一楼保卫科"},
|
||||
"c02_s05": {"path": "uid://cp8d3ag5nbjq0", "name": "一楼内侧楼道"},
|
||||
"c02_s06": {"path": "uid://cootarwb44vvh", "name": "二楼楼道"},
|
||||
"c02_s07": {"path": "uid://t4xjt774ngwh", "name": "二楼内侧楼道"},
|
||||
"c02_s08": {"path": "uid://ce2vyyg2reg52", "name": "瞎子卧室"},
|
||||
"c02_s09": {"path": "uid://ryups1dnwdto", "name": "裂缝空间"},
|
||||
"c02_s10": {"path": "uid://dny21yhtuteap", "name": "空房间"},
|
||||
"c02_s11": {"path": "uid://dq41rvwl5hyrk", "name": "一楼火灾"},
|
||||
"c02_s12": {"path": "uid://da4cuf2i3nwpj", "name": "盒子猫安全屋"},
|
||||
"c02_s13": {"path": "uid://bvjutch6jex0v", "name": "盒子猫二楼"},
|
||||
"c02_s14": {"path": "uid://d0p4x5st2r315", "name": "盒子猫二楼内侧"},
|
||||
"c02_s15": {"path": "uid://b21p53g42j2nt", "name": "盒子猫一楼内侧"},
|
||||
"c02_s16": {"path": "uid://22hc3oe8t0id", "name": "盒子猫三楼内侧"},
|
||||
"c02_s17": {"path": "uid://cbr6gbgrl2wb1", "name": "盒子猫三楼"},
|
||||
"c02_s18": {"path": "uid://d27gv3pbkn4b8", "name": "盒子猫一楼"},
|
||||
"c03_s01": {"path": "uid://dlrbhfvnd3cs0", "name": "三楼楼道"},
|
||||
"c03_s02": {"path": "uid://ctwy1ubhm68la", "name": "瞎子卧室"},
|
||||
"c03_s03": {"path": "uid://bsqt2c061fmin", "name": "瞎子理发店"},
|
||||
"c03_s04": {"path": "uid://c7c88hg2cl1j7", "name": "李癞房间"},
|
||||
"c03_s05": {"path": "uid://6ehb3ux2kilu", "name": "胖子肉铺"},
|
||||
"c03_s06": {"path": "uid://cxacrp8mrrbry", "name": "胖子卧室"},
|
||||
"c03_s07": {"path": "uid://c67732f2we13j", "name": "屠宰间"},
|
||||
"c03_s08": {"path": "uid://bixdbbyhroepi", "name": "囚室"},
|
||||
"c03_s09": {"path": "uid://dfln301xllqpn", "name": "棺材房"},
|
||||
}
|
||||
|
||||
# Exports
|
||||
@export var ignore_archive := false
|
||||
@export var current_scene := "c02_s01"
|
||||
@export var entrance_portal := "left"
|
||||
@ -9,206 +53,190 @@ class_name GroundLoader extends Node2D
|
||||
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
|
||||
)
|
||||
|
||||
# 场景名字映射到路径
|
||||
static var GROUND_SCENE_PATH_DICT = {
|
||||
"c01_s05": "uid://dlx5xxbg53rb8",
|
||||
"c01_s06": "uid://bx16c8nn32f40",
|
||||
"c01_s07": "uid://ds2iyfndwamiy",
|
||||
"c01_s08": "uid://cwu4dhayra8pg",
|
||||
"c01_s09": "uid://c777lv8mjojcw",
|
||||
"c01_s10": "uid://be57l2o3vxxtm",
|
||||
"c01_s11": "uid://coiumaaenimbc",
|
||||
"c01_s12": "uid://bol5hl68pbpgq",
|
||||
"c02_s01": "uid://bbs7yy5aofw1v",
|
||||
"c02_s02": "uid://brck77w81fhvc",
|
||||
"c02_s03": "uid://djc2uaefhmu7",
|
||||
"c02_s04": "uid://bivc5cdap370p",
|
||||
"c02_s05": "uid://cp8d3ag5nbjq0",
|
||||
"c02_s06": "uid://cootarwb44vvh",
|
||||
"c02_s07": "uid://t4xjt774ngwh",
|
||||
"c02_s08": "uid://ce2vyyg2reg52",
|
||||
"c02_s09": "uid://ryups1dnwdto",
|
||||
"c02_s10": "uid://dny21yhtuteap",
|
||||
"c02_s11": "uid://dq41rvwl5hyrk", # 注:该场景合并在了 c02_s03 院子中
|
||||
"c02_s12": "uid://da4cuf2i3nwpj",
|
||||
"c02_s13": "uid://bvjutch6jex0v",
|
||||
"c02_s14": "uid://d0p4x5st2r315",
|
||||
"c02_s15": "uid://b21p53g42j2nt",
|
||||
"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_s04": "uid://c7c88hg2cl1j7", # s04_李癞房间
|
||||
"c03_s05": "uid://6ehb3ux2kilu", # s05_肉铺
|
||||
"c03_s06": "uid://cxacrp8mrrbry", # s06_胖子卧室
|
||||
"c03_s07": "uid://c67732f2we13j", # s07_屠宰间
|
||||
"c03_s08": "uid://bixdbbyhroepi", # s08_囚室
|
||||
"c03_s09": "uid://dfln301xllqpn", # s09_棺材房
|
||||
}
|
||||
# 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)
|
||||
static func get_ground_scene_uid(scene_name: String) -> String:
|
||||
if GROUND_SCENE_PATH_DICT.has(scene_name):
|
||||
return GROUND_SCENE_PATH_DICT[scene_name]["path"]
|
||||
printerr("GroundLoader get_ground_scene_uid: scene not found:", scene_name)
|
||||
return ""
|
||||
|
||||
|
||||
static func get_ground_scene_readable_name(scene_name: String) -> String:
|
||||
if GROUND_SCENE_PATH_DICT.has(scene_name):
|
||||
return GROUND_SCENE_PATH_DICT[scene_name]["name"]
|
||||
printerr("GroundLoader get_ground_scene_readable_name: scene not found:", scene_name)
|
||||
return 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 +250,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 path = get_ground_scene_uid(scene_name)
|
||||
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(get_ground_scene_uid(scene_name))
|
||||
|
@ -101,15 +101,15 @@ func cant_read():
|
||||
func _on_note_read():
|
||||
frame.enabled = false
|
||||
# 稍加延时后显示鸡毛掸子
|
||||
Util.timer(4.5, func(): ambush.enabled = true)
|
||||
Util.timer(1.0, func(): ambush.enabled = true)
|
||||
|
||||
|
||||
func _on_ambush_triggered():
|
||||
ambush.enabled = false
|
||||
frame.note_key = "c01_摆正的洋相片"
|
||||
Util.timer(5.5, func(): frame.enabled = true)
|
||||
# 鸡毛掸子 4.5s,再等待 3s 后掉落纸片
|
||||
Util.timer(8, _play_paper_animation)
|
||||
# 鸡毛掸子 4.5s,再等待 1.5s 后掉落纸片
|
||||
Util.timer(6.0, _play_paper_animation)
|
||||
|
||||
|
||||
func lock_on_use_stick():
|
||||
|
@ -5,8 +5,6 @@
|
||||
[ext_resource type="AudioStream" uid="uid://cvttds81trcoc" path="res://asset/audio/sfx/UI/click.ogg" id="2_te6ya"]
|
||||
[ext_resource type="Script" uid="uid://rq6w1vuhuq1m" path="res://scene/entity/audio/sfx.gd" id="3_5oc6i"]
|
||||
[ext_resource type="Script" uid="uid://bbg4vopj4apl6" path="res://scene/entity/audio/bgm_control.gd" id="4_5oc6i"]
|
||||
[ext_resource type="AudioStream" uid="uid://b3b4a6nm8557i" path="res://asset/audio/专用/衔蝶_主菜单music剪辑人声版.wav" id="5_5oc6i"]
|
||||
[ext_resource type="Script" uid="uid://c63lrar71o17d" path="res://scene/index/启动入口.gd" id="7_1sxgt"]
|
||||
[ext_resource type="AudioStream" uid="uid://b3b4a6nm8557i" path="res://asset/audio/专用/衔蝶_主菜单music剪辑人声版.ogg" id="5_5oc6i"]
|
||||
[ext_resource type="FontFile" uid="uid://bjmhscwn1ixj1" path="res://asset/font/字体/ChillJinshuSongMedium.otf" id="7_1sxgt"]
|
||||
|
||||
@ -162,34 +160,3 @@ grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
color = Color(0, 0, 0, 1)
|
||||
|
||||
[node name="启动入口" type="VBoxContainer" parent="."]
|
||||
layout_mode = 0
|
||||
offset_left = 298.0
|
||||
offset_top = 50.0
|
||||
offset_right = 396.0
|
||||
offset_bottom = 172.0
|
||||
script = ExtResource("7_1sxgt")
|
||||
|
||||
[node name="Label" type="Label" parent="启动入口"]
|
||||
layout_mode = 2
|
||||
text = "【仅测试版本启用】"
|
||||
|
||||
[node name="DebugMode" type="Button" parent="启动入口"]
|
||||
custom_minimum_size = Vector2(50, 0)
|
||||
layout_mode = 2
|
||||
text = "退出 debug mode(仅本次启动游戏有效,若想持续生效需退出游戏后改动 config.tres/.res )"
|
||||
autowrap_mode = 3
|
||||
|
||||
[node name="Chapter1" type="Button" parent="启动入口"]
|
||||
layout_mode = 2
|
||||
text = "从第一章开始新游戏"
|
||||
|
||||
[node name="Chapter2" type="Button" parent="启动入口"]
|
||||
layout_mode = 2
|
||||
text = "从第二章开始新游戏"
|
||||
|
||||
[node name="Chapter3" type="Button" parent="启动入口"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
text = "从第三章开始新游戏"
|
||||
|
@ -1,39 +0,0 @@
|
||||
extends VBoxContainer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if not GlobalConfig.DEBUG:
|
||||
queue_free()
|
||||
return
|
||||
$DebugMode.pressed.connect(_exit_debug_mode)
|
||||
$Chapter1.pressed.connect(_start_game.bind(1))
|
||||
$Chapter2.pressed.connect(_start_game.bind(2))
|
||||
$Chapter3.pressed.connect(_start_game.bind(3))
|
||||
|
||||
|
||||
func _exit_debug_mode() -> void:
|
||||
GlobalConfig.DEBUG = false
|
||||
print_rich("[color=orange]Debug mode disabled[/color]")
|
||||
queue_free()
|
||||
|
||||
|
||||
func _start_game(chapter: int) -> void:
|
||||
$"../SfxClick".global_play()
|
||||
var archive_res: AssembledArchive
|
||||
if chapter == 1:
|
||||
archive_res = load("uid://x2yr8b2ul5al")
|
||||
if archive_res.version != ArchiveManager.CURRENT_VERSION:
|
||||
printerr("存档版本不匹配,已自动更新存档版本")
|
||||
archive_res.version = ArchiveManager.CURRENT_VERSION
|
||||
elif chapter == 2:
|
||||
archive_res = load("uid://b45y2iffjdieg")
|
||||
if archive_res.version != ArchiveManager.CURRENT_VERSION:
|
||||
printerr("存档版本不匹配,已自动更新存档版本")
|
||||
archive_res.version = ArchiveManager.CURRENT_VERSION
|
||||
archive_res = archive_res.duplicate(true)
|
||||
ArchiveManager.create_and_use_new_archive(1)
|
||||
archive_res.take_over_path(ArchiveManager.archive.resource_path)
|
||||
ArchiveManager.archive = archive_res
|
||||
# new game 时音量渐隐
|
||||
$"../BgmControl".stop(0.5)
|
||||
SceneManager.enter_main_scene()
|
@ -1 +0,0 @@
|
||||
uid://c63lrar71o17d
|
@ -1,37 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="AssembledArchive" load_steps=5 format=3 uid="uid://x2yr8b2ul5al"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b63jgb1ag242f" path="res://manager/archive_manager/ground_archive.gd" id="1_na6rk"]
|
||||
[ext_resource type="Script" uid="uid://cay6xv3x51phl" path="res://scene/ux/prop_inventory_resource.gd" id="2_uc52y"]
|
||||
[ext_resource type="Script" uid="uid://cusw17h37oc0g" path="res://manager/archive_manager/assembled_archive.gd" id="3_sr6ce"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_6q3u4"]
|
||||
script = ExtResource("2_uc52y")
|
||||
owner = ""
|
||||
current_index = 0
|
||||
important_items = Array[String]([])
|
||||
unviewed_important_items = Array[String]([])
|
||||
default_enabled_items = Array[String]([])
|
||||
xdie_enabled_items = Array[String]([])
|
||||
xxdie_enabled_items = Array[String]([])
|
||||
xxxdie_enabled_items = Array[String]([])
|
||||
|
||||
[resource]
|
||||
script = ExtResource("3_sr6ce")
|
||||
version = 6
|
||||
archive_id = 1
|
||||
entrance_portal = &"left"
|
||||
current_scene = &"c02_s01"
|
||||
player_global_position_x = -100.0
|
||||
player_direction = Vector2(0, 0)
|
||||
game_seconds = 0
|
||||
created_time = "2025-07-11 01:22:58"
|
||||
event_stage = Dictionary[StringName, int]({})
|
||||
global_data_dict = Dictionary[StringName, Variant]({})
|
||||
ground_archives = Dictionary[StringName, ExtResource("1_na6rk")]({})
|
||||
npc_anonymous_states = Dictionary[StringName, bool]({})
|
||||
player_running_locked = false
|
||||
prop_inventory = SubResource("Resource_6q3u4")
|
||||
mem_display_dict = Dictionary[int, bool]({})
|
||||
bayinhe_current_answer = [0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
bayinhe_mode = "closed"
|
||||
metadata/_custom_type_script = "uid://cusw17h37oc0g"
|
@ -1,672 +0,0 @@
|
||||
[gd_resource type="Resource" script_class="AssembledArchive" load_steps=19 format=3 uid="uid://b45y2iffjdieg"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b63jgb1ag242f" path="res://manager/archive_manager/ground_archive.gd" id="1_6q3u4"]
|
||||
[ext_resource type="Script" uid="uid://cay6xv3x51phl" path="res://scene/ux/prop_inventory_resource.gd" id="2_sql7f"]
|
||||
[ext_resource type="Script" uid="uid://cusw17h37oc0g" path="res://manager/archive_manager/assembled_archive.gd" id="3_qjj7j"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_6q3u4"]
|
||||
script = ExtResource("1_6q3u4")
|
||||
scene_name = "c02_s01"
|
||||
data = {
|
||||
"AnimationPlayer": {},
|
||||
"Local寻人启事": {
|
||||
"icount": 0
|
||||
},
|
||||
"Note门口": {
|
||||
"icount": 2
|
||||
},
|
||||
"portal_left": {
|
||||
"icount": 0
|
||||
},
|
||||
"portal_right": {
|
||||
"icount": 1
|
||||
}
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_qjj7j"]
|
||||
script = ExtResource("1_6q3u4")
|
||||
scene_name = "c02_s02"
|
||||
data = {
|
||||
"Ambush太暗了": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush小蝉闪现": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush推小鞋子": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush杂物堆": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush纸人": {
|
||||
"played": false
|
||||
},
|
||||
"AnimationPlayer": {
|
||||
"first_enter": true,
|
||||
"flyer_shaven": true
|
||||
},
|
||||
"Closeup八音盒": {
|
||||
"icount": 8,
|
||||
"interacted_times": 8
|
||||
},
|
||||
"Closeup刮海报": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"InteractableLeftPortal": {
|
||||
"icount": 0,
|
||||
"interacted_times": 0
|
||||
},
|
||||
"Interactable小鞋子": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"Note大头娃娃": {
|
||||
"icount": 0
|
||||
},
|
||||
"Note老鼠洞": {
|
||||
"icount": 0
|
||||
},
|
||||
"portal_left": {
|
||||
"icount": 0
|
||||
},
|
||||
"portal_right": {
|
||||
"icount": 3
|
||||
},
|
||||
"海报1": {
|
||||
"icount": 1
|
||||
},
|
||||
"煤油灯": {
|
||||
"icount": 0,
|
||||
"interacted_times": 0
|
||||
},
|
||||
"青岛啤酒": {
|
||||
"icount": 1
|
||||
},
|
||||
"鼠疫海报": {
|
||||
"icount": 0
|
||||
}
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_ja7w6"]
|
||||
script = ExtResource("1_6q3u4")
|
||||
scene_name = "c02_s03"
|
||||
data = {
|
||||
"Ambush保卫科旁边os": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush偷听对话": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush惊悚闪电": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush点火游戏阻挡右移": {
|
||||
"played": false
|
||||
},
|
||||
"Ambush等待的小蝉": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush老虎钳后小蝉跑": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush要下雨了": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush闷雷纸人": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush霸凌": {
|
||||
"played": true
|
||||
},
|
||||
"AnimationPlayer": {},
|
||||
"Closeup井": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"Closeup抓药游戏": {
|
||||
"icount": 0,
|
||||
"interacted_times": 0
|
||||
},
|
||||
"Closeup敲门游戏": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"Interactable柜子": {
|
||||
"icount": 2,
|
||||
"interacted_times": 2
|
||||
},
|
||||
"Interactable铁门": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"Note柜子可放置区域": {
|
||||
"icount": 0
|
||||
},
|
||||
"Note算命摊位": {
|
||||
"icount": 0
|
||||
},
|
||||
"Note血迹": {
|
||||
"icount": 0
|
||||
},
|
||||
"Npc井边疯子": {
|
||||
"icount": 2
|
||||
},
|
||||
"Pickable小鞋子": {
|
||||
"picked": true
|
||||
},
|
||||
"portal_1": {
|
||||
"icount": 3
|
||||
},
|
||||
"portal_2": {
|
||||
"icount": 4
|
||||
},
|
||||
"portal_3": {
|
||||
"icount": 2
|
||||
},
|
||||
"portal_4": {
|
||||
"icount": 0
|
||||
},
|
||||
"portal_5": {
|
||||
"icount": 12
|
||||
},
|
||||
"portal_left": {
|
||||
"icount": 2
|
||||
},
|
||||
"portal_right": {
|
||||
"icount": 0
|
||||
}
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_175lw"]
|
||||
script = ExtResource("1_6q3u4")
|
||||
scene_name = "c02_s04"
|
||||
data = {
|
||||
"Ambush进门看到小蝉": {
|
||||
"played": true
|
||||
},
|
||||
"AnimationPlayer": {},
|
||||
"Closeup折锡纸": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"Closeup老鼠精": {
|
||||
"icount": 0,
|
||||
"interacted_times": 0
|
||||
},
|
||||
"Closeup花名册": {
|
||||
"icount": 0,
|
||||
"interacted_times": 0
|
||||
},
|
||||
"Local寻人启事": {
|
||||
"icount": 0
|
||||
},
|
||||
"Pickable元宝": {
|
||||
"picked": true
|
||||
},
|
||||
"portal_left": {
|
||||
"icount": 3
|
||||
},
|
||||
"portal_right": {
|
||||
"icount": 0
|
||||
},
|
||||
"小手讨东西": {
|
||||
"holding_prop": "",
|
||||
"icount": 5,
|
||||
"is_holding_cup": false,
|
||||
"tin_coin_drop": false
|
||||
}
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_xnubk"]
|
||||
script = ExtResource("1_6q3u4")
|
||||
scene_name = "c02_s05"
|
||||
data = {
|
||||
"Ambush小蝉消失": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush更新一楼地图": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush杂物堆": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush老鼠叼肉": {
|
||||
"played": false
|
||||
},
|
||||
"AnimationPlayer": {},
|
||||
"Closeup戏台": {
|
||||
"icount": 0,
|
||||
"interacted_times": 0
|
||||
},
|
||||
"Closeup拿人偶后记忆闪回": {
|
||||
"icount": 0,
|
||||
"interacted_times": 0
|
||||
},
|
||||
"Interactable粘鼠板": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"Note一楼内侧通道": {
|
||||
"icount": 0
|
||||
},
|
||||
"Note垃圾通道": {
|
||||
"icount": 0
|
||||
},
|
||||
"Note死老鼠": {
|
||||
"icount": 0
|
||||
},
|
||||
"Npc疯子": {
|
||||
"icount": 1
|
||||
},
|
||||
"Pickable掉落的肉": {
|
||||
"picked": true
|
||||
},
|
||||
"portal_1": {
|
||||
"icount": 3
|
||||
},
|
||||
"portal_2": {
|
||||
"icount": 0
|
||||
},
|
||||
"portal_left": {
|
||||
"icount": 5
|
||||
},
|
||||
"portal_right": {
|
||||
"icount": 0
|
||||
}
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_xfhwh"]
|
||||
script = ExtResource("1_6q3u4")
|
||||
scene_name = "c02_s06"
|
||||
data = {
|
||||
"Ambush三男孩": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush小蝉偷看": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush解锁二楼地图": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush走到尽头后小蝉跑": {
|
||||
"played": false
|
||||
},
|
||||
"AnimationPlayer": {},
|
||||
"Closeup弹珠游戏": {
|
||||
"icount": 0,
|
||||
"interacted_times": 0
|
||||
},
|
||||
"Closeup水盆": {
|
||||
"icount": 0,
|
||||
"interacted_times": 0
|
||||
},
|
||||
"Closeup谢幕演出": {
|
||||
"icount": 0,
|
||||
"interacted_times": 0
|
||||
},
|
||||
"Inspectable小猫纸条": {
|
||||
"icount": 1
|
||||
},
|
||||
"Interactable老鼠洞": {
|
||||
"icount": 2,
|
||||
"interacted_times": 2
|
||||
},
|
||||
"Note楼梯杂物堆": {
|
||||
"icount": 0
|
||||
},
|
||||
"portal_1": {
|
||||
"icount": 0
|
||||
},
|
||||
"portal_2": {
|
||||
"icount": 0
|
||||
},
|
||||
"portal_3": {
|
||||
"icount": 1
|
||||
},
|
||||
"portal_4": {
|
||||
"icount": 0
|
||||
},
|
||||
"portal_left": {
|
||||
"icount": 3
|
||||
},
|
||||
"portal_right": {
|
||||
"icount": 0
|
||||
},
|
||||
"煤油灯": {
|
||||
"icount": 2,
|
||||
"interacted_times": 1
|
||||
}
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_53fcv"]
|
||||
script = ExtResource("1_6q3u4")
|
||||
scene_name = "c02_s07"
|
||||
data = {
|
||||
"AnimationPlayer": {
|
||||
"flyer_shaven": true
|
||||
},
|
||||
"Closeup刮海报": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"Note老鼠洞": {
|
||||
"icount": 0
|
||||
},
|
||||
"Pickable元宝": {
|
||||
"picked": false
|
||||
},
|
||||
"Pickable粘鼠板": {
|
||||
"picked": true
|
||||
},
|
||||
"portal_1": {
|
||||
"icount": 2
|
||||
},
|
||||
"portal_left": {
|
||||
"icount": 0
|
||||
},
|
||||
"portal_right": {
|
||||
"icount": 4
|
||||
},
|
||||
"小手讨东西": {
|
||||
"holding_prop": "",
|
||||
"icount": 2,
|
||||
"is_holding_cup": true,
|
||||
"tin_coin_drop": false
|
||||
}
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_cio48"]
|
||||
script = ExtResource("1_6q3u4")
|
||||
scene_name = "c02_s08"
|
||||
data = {
|
||||
"Ambush偷听": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush回看洞口": {
|
||||
"played": true
|
||||
},
|
||||
"AnimationPlayer": {
|
||||
"first_entered": false,
|
||||
"hole_interacted_times": 3
|
||||
},
|
||||
"Closeup画": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"Closeup讨厌他们": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"Note八卦图": {
|
||||
"icount": 1
|
||||
},
|
||||
"Note十字架": {
|
||||
"icount": 0
|
||||
},
|
||||
"portal_1": {
|
||||
"icount": 1
|
||||
},
|
||||
"portal_left": {
|
||||
"icount": 1
|
||||
},
|
||||
"portal_right": {
|
||||
"icount": 0
|
||||
},
|
||||
"煤油灯": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
}
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_i3b22"]
|
||||
script = ExtResource("1_6q3u4")
|
||||
scene_name = "c02_s09"
|
||||
data = {
|
||||
"Ambush剪影指天": {
|
||||
"played": true
|
||||
},
|
||||
"Ambush老鼠": {
|
||||
"played": true
|
||||
},
|
||||
"AnimationPlayer": {},
|
||||
"portal_left": {
|
||||
"icount": 1
|
||||
},
|
||||
"portal_right": {
|
||||
"icount": 0
|
||||
}
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_cpr7i"]
|
||||
script = ExtResource("1_6q3u4")
|
||||
scene_name = "c02_s10"
|
||||
data = {
|
||||
"Ambush首次进入血脚印": {
|
||||
"played": true
|
||||
},
|
||||
"AnimationPlayer": {
|
||||
"display_wood_puppet": false
|
||||
},
|
||||
"Closeup头套": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"Interactable小蝉": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"Interactable柜子": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"Note进入隧道询问": {
|
||||
"icount": 1
|
||||
},
|
||||
"Pickable小猫玩具脑袋": {
|
||||
"picked": true
|
||||
},
|
||||
"Pickable小蝉人偶": {
|
||||
"picked": true
|
||||
},
|
||||
"portal_left": {
|
||||
"icount": 1
|
||||
},
|
||||
"portal_right": {
|
||||
"icount": 0
|
||||
},
|
||||
"煤油灯": {
|
||||
"icount": 0,
|
||||
"interacted_times": 1
|
||||
}
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_06ncl"]
|
||||
script = ExtResource("1_6q3u4")
|
||||
scene_name = "c02_s12"
|
||||
data = {
|
||||
"AnimationPlayer": {},
|
||||
"portal_left": {
|
||||
"icount": 2
|
||||
},
|
||||
"portal_right": {
|
||||
"icount": 0
|
||||
}
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_xhybu"]
|
||||
script = ExtResource("1_6q3u4")
|
||||
scene_name = "c02_s13"
|
||||
data = {
|
||||
"Ambush猫咪敲门互动": {
|
||||
"played": false
|
||||
},
|
||||
"Ambush猫咪敲门互动2": {
|
||||
"played": false
|
||||
},
|
||||
"Ambush猫咪敲门互动3": {
|
||||
"played": false
|
||||
},
|
||||
"Ambush终点光芒互动": {
|
||||
"played": true
|
||||
},
|
||||
"AnimationPlayer": {},
|
||||
"portal_1": {
|
||||
"icount": 1
|
||||
},
|
||||
"portal_left": {
|
||||
"icount": 0
|
||||
},
|
||||
"portal_right": {
|
||||
"icount": 0
|
||||
}
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_w25c8"]
|
||||
script = ExtResource("1_6q3u4")
|
||||
scene_name = "c02_s17"
|
||||
data = {
|
||||
"Ambush猫咪敲门互动2": {
|
||||
"played": false
|
||||
},
|
||||
"AnimationPlayer": {},
|
||||
"portal_1": {
|
||||
"icount": 1
|
||||
},
|
||||
"portal_left": {
|
||||
"icount": 0
|
||||
},
|
||||
"portal_right": {
|
||||
"icount": 2
|
||||
}
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_gujks"]
|
||||
script = ExtResource("1_6q3u4")
|
||||
scene_name = "c03_s01"
|
||||
data = {
|
||||
"Ambush偷听陆仁": {
|
||||
"played": false
|
||||
},
|
||||
"AnimationPlayer": {},
|
||||
"CloseupDemo公告": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"Interactable放肉处": {
|
||||
"icount": 1,
|
||||
"interacted_times": 1
|
||||
},
|
||||
"Note治病木牌": {
|
||||
"icount": 2
|
||||
},
|
||||
"portal_left": {
|
||||
"icount": 0
|
||||
},
|
||||
"portal_right": {
|
||||
"icount": 0
|
||||
}
|
||||
}
|
||||
|
||||
[sub_resource type="Resource" id="Resource_sql7f"]
|
||||
script = ExtResource("2_sql7f")
|
||||
owner = ""
|
||||
current_index = 0
|
||||
important_items = Array[String](["prop_眼镜盒", "prop_船票", "prop_单只小鞋子", "prop_小蝉人偶"])
|
||||
unviewed_important_items = Array[String]([])
|
||||
default_enabled_items = Array[String]([])
|
||||
xdie_enabled_items = Array[String](["prop_火柴", "prop_撕下的照片上", "prop_小猫玩具完整", "prop_麻将"])
|
||||
xxdie_enabled_items = Array[String]([])
|
||||
xxxdie_enabled_items = Array[String]([])
|
||||
|
||||
[resource]
|
||||
script = ExtResource("3_qjj7j")
|
||||
version = 6
|
||||
archive_id = 1
|
||||
entrance_portal = &"right"
|
||||
current_scene = &"c03_s01"
|
||||
player_global_position_x = 616.833
|
||||
player_direction = Vector2(-1, 0)
|
||||
game_seconds = 1170
|
||||
created_time = "2025-07-11 01:22:58"
|
||||
event_stage = Dictionary[StringName, int]({
|
||||
&"c02_2f_xchan_run_away": 1,
|
||||
&"c02_ball_game_stage": 4,
|
||||
&"c02_burning_end_stage": 2,
|
||||
&"c02_got_pliers": 2,
|
||||
&"c02_little_hand": 2,
|
||||
&"c02_madman_interacted_stage": 2,
|
||||
&"c02_musicbox_stage": 4,
|
||||
&"c03_s01_meat_put": 1,
|
||||
&"current_chapter_stage": 3,
|
||||
&"handnote_c02_gate_chain": 2,
|
||||
&"handnote_c02_hand_exchange": 2,
|
||||
&"handnote_c02_hand_requireCoin": 2,
|
||||
&"handnote_c02_map_1Left": 1,
|
||||
&"handnote_c02_map_1Right": 1,
|
||||
&"handnote_c02_map_2": 1,
|
||||
&"handnote_c02_meat_knockDoor": 1,
|
||||
&"handnote_c02_musicbox_needPuppet": 2,
|
||||
&"handnote_c02_road_address": 1,
|
||||
&"handnote_c02_shoe_giveToMouce": 2,
|
||||
&"handnote_c02_split_illusionSpace": 1,
|
||||
&"handnote_c02_xchan_giveHerPuppet": 2,
|
||||
&"handnote_c02_xchan_sheKnowsMe": 2
|
||||
})
|
||||
global_data_dict = Dictionary[StringName, Variant]({
|
||||
&"c02_boxcat_chasing_started": true,
|
||||
&"c02_boxcat_skip_intro": true,
|
||||
&"c02_burning": true,
|
||||
&"c02_counter_pushed_out": true,
|
||||
&"c02_counter_x": 159.0,
|
||||
&"c02_eavesdrop_finished": true,
|
||||
&"c02_entered_the_splitted_space": true,
|
||||
&"c02_gaslight_first_failed": true,
|
||||
&"c02_gaslight_first_lighted": true,
|
||||
&"c02_little_hand_dropped_tin": true,
|
||||
&"c02_madman_hitwall": true,
|
||||
&"c02_meat_door_interacted": true,
|
||||
&"c02_meat_dropping": true,
|
||||
&"c02_meat_given": true,
|
||||
&"c02_mouse_follow_player": false,
|
||||
&"c02_musicbox_has_opened": false,
|
||||
&"c02_musicbox_taken_prop_弹珠": true,
|
||||
&"c02_musicbox_taken_prop_撕下的照片上": true,
|
||||
&"c02_musicbox_taken_prop_无头小猫玩具": true,
|
||||
&"c02_musicbox_taken_prop_木头人偶": true,
|
||||
&"c02_ready_to_fire": true,
|
||||
&"c02_s02_mouse_push_shoe": true,
|
||||
&"c02_show_grounded_coins": true,
|
||||
&"c02_the_blind_room_unlocked": true,
|
||||
&"c02_tin_coin_taken": true,
|
||||
&"c02_watched_the_well": true,
|
||||
&"刮海报_二楼内侧": true,
|
||||
&"刮海报_过道": true
|
||||
})
|
||||
ground_archives = Dictionary[StringName, ExtResource("1_6q3u4")]({
|
||||
&"c02_s01": SubResource("Resource_6q3u4"),
|
||||
&"c02_s02": SubResource("Resource_qjj7j"),
|
||||
&"c02_s03": SubResource("Resource_ja7w6"),
|
||||
&"c02_s04": SubResource("Resource_175lw"),
|
||||
&"c02_s05": SubResource("Resource_xnubk"),
|
||||
&"c02_s06": SubResource("Resource_xfhwh"),
|
||||
&"c02_s07": SubResource("Resource_53fcv"),
|
||||
&"c02_s08": SubResource("Resource_cio48"),
|
||||
&"c02_s09": SubResource("Resource_i3b22"),
|
||||
&"c02_s10": SubResource("Resource_cpr7i"),
|
||||
&"c02_s12": SubResource("Resource_06ncl"),
|
||||
&"c02_s13": SubResource("Resource_xhybu"),
|
||||
&"c02_s17": SubResource("Resource_w25c8"),
|
||||
&"c03_s01": SubResource("Resource_gujks")
|
||||
})
|
||||
npc_anonymous_states = Dictionary[StringName, bool]({})
|
||||
player_running_locked = false
|
||||
prop_inventory = SubResource("Resource_sql7f")
|
||||
mem_display_dict = Dictionary[int, bool]({
|
||||
1: true,
|
||||
2: true
|
||||
})
|
||||
bayinhe_current_answer = [0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
bayinhe_mode = "closed"
|
||||
metadata/_custom_type_script = "uid://cusw17h37oc0g"
|
@ -16,6 +16,10 @@ func _ready() -> void:
|
||||
# settings.exited.connect(grab_focus)
|
||||
|
||||
|
||||
var debug_button_last_press_msec := 0
|
||||
var debug_button_pressed_count := 0
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("escape"):
|
||||
get_viewport().set_input_as_handled()
|
||||
@ -33,6 +37,23 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
elif event.is_action_pressed("panel"):
|
||||
get_viewport().set_input_as_handled()
|
||||
SceneManager.show_panel()
|
||||
elif event.is_action_pressed("savings"):
|
||||
if GlobalConfig.DEBUG:
|
||||
get_viewport().set_input_as_handled()
|
||||
SceneManager.show_savings()
|
||||
else:
|
||||
# 连续按 5 次开启 debug mode
|
||||
var time = Time.get_ticks_msec()
|
||||
if time - debug_button_last_press_msec < 500:
|
||||
debug_button_pressed_count += 1
|
||||
else:
|
||||
debug_button_pressed_count = 1
|
||||
debug_button_last_press_msec = time
|
||||
if debug_button_pressed_count >= 5:
|
||||
get_viewport().set_input_as_handled()
|
||||
# 不写入配置
|
||||
GlobalConfig.DEBUG = true
|
||||
SceneManager.show_savings()
|
||||
|
||||
|
||||
var vignette_tween: Tween
|
||||
@ -48,6 +69,3 @@ func tween_vignette(ratio := 0.5, duration := 0.3, color := Color.RED):
|
||||
vignette_tween.parallel().tween_property(
|
||||
vignette.material, "shader_parameter/vignette_rgb", color, duration
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
183
scene/trailer.gd
183
scene/trailer.gd
@ -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
|
||||
)
|
||||
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()
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user