xiandie/scene/ground/ground_loader.gd

324 lines
11 KiB
GDScript3
Raw Normal View History

class_name GroundLoader extends Node2D
# 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": "棺材房"},
2025-04-01 08:12:29 +00:00
}
# Exports
@export var ignore_archive := false
@export var current_scene := "c02_s01"
@export var entrance_portal := "left"
@export var debug_reload := false:
set(new_val):
debug_reload = false
if is_node_ready() and current_scene and entrance_portal:
transition_to_scene(current_scene, entrance_portal, 0.0)
# 强制覆盖 archive 记录
@export var force_archive_scene := ""
@export var force_archive_portal := ""
# Nodes
@onready var mask_layer := %MaskLayer as CanvasLayer
@onready var mask := %Mask as ColorRect
# State
var has_entered := false
var ground: Ground2D
var display_start_sec := 0.0
var _frozen_start_time_ms: int
var _allow_ground_start := false:
set(val):
_allow_ground_start = val
if ground and val:
if ground.process_mode != Node.PROCESS_MODE_INHERIT:
SceneManager.ground_start.emit()
ground.process_mode = Node.PROCESS_MODE_INHERIT
print(
"GroundLoader _allow_ground_start: unfrozen. frozen duration(ms):",
Time.get_ticks_msec() - _frozen_start_time_ms
)
# Debug
var update_watcher: Timer
var last_modify_time := 0
func _ready() -> void:
_setup_mask_layer()
if not ignore_archive:
_load_save()
if current_scene and entrance_portal:
2025-06-07 09:04:08 +00:00
# 首次进入渐隐效果
transition_to_scene(current_scene, entrance_portal)
func _setup_mask_layer() -> void:
mask.visible = true
mask.color.a = 0.0
mask_layer.layer = GlobalConfig.CANVAS_LAYER_GROUND_MASK
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 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
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
2025-06-30 10:33:40 +00:00
func toggle_mask(
display: bool, wait_time: float, ease_min_duration := EASE_DURATION, mask_color := Color.BLACK
2025-06-30 10:33:40 +00:00
) -> Tween:
var tween = get_tree().create_tween()
mask_color.a = mask.color.a
mask.color = mask_color
2025-06-30 10:33:40 +00:00
var duration = min(ease_min_duration, wait_time * 0.5)
if display:
2025-06-30 10:33:40 +00:00
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)
2025-06-30 10:33:40 +00:00
return
_pause_current_ground()
current_scene = scene_name
entrance_portal = portal
# 优先更新 archive使 ground 可以访问自己的 current_scene 键值
2025-06-30 10:33:40 +00:00
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)
2025-06-30 10:33:40 +00:00
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 _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:
2025-06-25 17:53:16 +00:00
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:
2025-01-31 10:44:30 +00:00
ground = get_node_or_null("Ground") as Ground2D
if ground:
2025-01-31 10:44:30 +00:00
# 防止命名冲突
remove_child(ground)
ground.queue_free()
func _load_and_setup_ground(scene_name: String) -> void:
# 先设置 ground再添加到场景中
# 因为 ground 在 enter_tree 时会用到 SceneManager 的方法
# 其中间接用到了 GroundLoader 的 ground
2025-01-31 10:44:30 +00:00
ground = _load_ground_node(scene_name)
2025-06-27 14:52:46 +00:00
if not _allow_ground_start:
ground.set_deferred("process_mode", Node.PROCESS_MODE_DISABLED)
2025-06-27 14:52:46 +00:00
print("GroundLoader not _allow_ground_start: frozen (delayed)")
_frozen_start_time_ms = Time.get_ticks_msec()
func _add_ground() -> void:
2025-06-27 14:52:46 +00:00
ground.ready.connect(SceneManager.ground_ready.emit.bind(ground))
2025-01-31 10:44:30 +00:00
ground.name = "Ground"
# 在 add child 之前,调整 ground 内部元素属性,在 on ground ready 前设置完成
if not Engine.is_editor_hint():
_setup_player_position()
2025-07-18 11:52:49 +00:00
add_child(ground)
2025-07-19 07:02:32 +00:00
# debug 模式在 ground add 之后加载音频
# 防止影响 Sfx 的 META 设置 ORIGINAL_STREAM
2025-07-18 11:52:49 +00:00
if GlobalConfig.DEBUG:
# headless 模式
SfxConfigPanel.new().refresh_sfx_list(ground, true)
print(
"GroundLoader add_ground finished:",
ground.scene_name,
" player.pos=",
ground.get_player().global_position
)
# ready 后,再整体重置 camera 位置
if not Engine.is_editor_hint():
ground.get_camera().reset_position_immediately()
has_entered = true
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
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 = 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:
return ResourceLoader.load_threaded_get(path) as PackedScene
else:
return ResourceLoader.load(path) as PackedScene
func _post_transition() -> void:
if not ground:
return
_preload_neighbor_scenes()
GlobalConfigManager.print_global_info()
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))