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": "棺材房"}, } # 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: # 首次进入渐隐效果 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 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) 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: 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() 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(): _setup_player_position() add_child(ground) # debug 模式在 ground add 之后加载音频 # 防止影响 Sfx 的 META 设置 ORIGINAL_STREAM 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))