class_name GroundLoader extends Node2D @export_group("Scene") @export var ignore_archive := false @export var current_scene := "c02_s01" @export var entrance_portal := "left" @export var debug_reload := false: set(new_val): debug_reload = false if is_node_ready() and current_scene and entrance_portal: transition_to_scene(current_scene, entrance_portal, 0.0) # 强制覆盖 archive 记录 @export var force_archive_scene := "" @export var force_archive_portal := "" @onready var mask_layer := %MaskLayer as CanvasLayer @onready var mask := %Mask as ColorRect var first_entered := true var ground: Ground2D var display_mask_sec = 0.0 # 场景名字映射到路径 static var GROUND_SCENE_PATH_DICT = { "c01_s05": "res://scene/ground/scene/c01/s05_院长房间.tscn", "c01_s06": "res://scene/ground/scene/c01/s06_孤儿院长廊围墙.tscn", "c01_s07": "res://scene/ground/scene/c01/s07_书店外.tscn", "c01_s08": "res://scene/ground/scene/c01/s08_书店.tscn", "c01_s09": "res://scene/ground/scene/c01/s09_公寓楼外.tscn", "c01_s10": "res://scene/ground/scene/c01/s10_公寓楼道.tscn", "c01_s11": "res://scene/ground/scene/c01/s11_黄包车演出.tscn", "c01_s12": "res://scene/ground/scene/c01/s12_书店外_诡异版.tscn", "c02_s01": "res://scene/ground/scene/c02/s01_街道.tscn", "c02_s02": "res://scene/ground/scene/c02/s02_过道.tscn", "c02_s03": "res://scene/ground/scene/c02/s03_院子.tscn", "c02_s04": "res://scene/ground/scene/c02/s04_保卫科.tscn", "c02_s05": "res://scene/ground/scene/c02/s05_一楼内侧楼道.tscn", "c02_s06": "res://scene/ground/scene/c02/s06_二楼.tscn", "c02_s07": "res://scene/ground/scene/c02/s07_二楼内侧楼道.tscn", "c02_s08": "res://scene/ground/scene/c02/s08_瞎子卧室.tscn", "c02_s09": "res://scene/ground/scene/c02/s09_裂缝.tscn", "c02_s10": "res://scene/ground/scene/c02/s10_空房间.tscn", "c02_s11": "res://scene/ground/scene/c02/s11_一楼火灾.tscn", # 注:该场景合并在了 c02_s03 院子中 "c02_s12": "res://scene/ground/scene/c02/s12_盒子猫.tscn", "c02_s13": "res://scene/ground/scene/c02/s13_盒子猫二楼.tscn", "c02_s14": "res://scene/ground/scene/c02/s14_盒子猫二楼内侧.tscn", "c02_s15": "res://scene/ground/scene/c02/s15_盒子猫一楼内侧.tscn", "c02_s16": "res://scene/ground/scene/c02/s16_盒子猫三楼内侧.tscn", "c02_s17": "res://scene/ground/scene/c02/s17_盒子猫三楼.tscn", "c02_s18": "res://scene/ground/scene/c02/s18_盒子猫一楼.tscn", } func _ready() -> void: mask_layer.layer = GlobalConfig.CANVAS_LAYER_GROUND_MASK mask.visible = true mask.color.a = 0.0 ground = get_node_or_null("Ground") as Ground2D if ground: ground.queue_free() # load save 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) func _load_save(): # 强制覆盖 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 func toggle_mask(display: bool, mask_color: Color, wait_time: float) -> Tween: var tween = get_tree().create_tween() mask_color.a = mask.color.a mask.color = mask_color var duration = min(0.3, wait_time * 0.5) if display: tween.tween_property(mask, "color:a", 1.0, duration).set_trans(Tween.TRANS_CUBIC) display_mask_sec = Time.get_ticks_msec() * 0.001 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_mask_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 transition_to_scene( scene_name: String, portal: String, wait_time := 1.4, mask_color := Color.BLACK ) -> void: 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, mask_color, wait_time) tween.tween_callback(call_deferred.bind("_do_transition", scene_name)) tween.tween_callback(toggle_mask.bind(false, mask_color, wait_time)) else: _do_transition.call_deferred(scene_name) else: print("Scene not found: " + scene_name) func _update_archive(): ArchiveManager.archive.current_scene = current_scene ArchiveManager.archive.entrance_portal = entrance_portal func _do_transition(scene_name: String) -> void: # SceneManager.freeze_player(0) ground = get_node_or_null("Ground") as Ground2D if ground: # 防止命名冲突 remove_child(ground) ground.queue_free() # 先设置 ground,再添加到场景中 # 因为 ground 在 enter_tree 时会用到 SceneManager 的方法 # 其中间接用到了 GroundLoader 的 ground ground = _load_ground_node(scene_name) _add_ground() # 预先加载邻居场景 _post_transition() if GlobalConfig.DEBUG and not Engine.is_editor_hint(): _watch_scene_update() func _add_ground(): add_child(ground) ground.name = "Ground" if not Engine.is_editor_hint(): # move player to portal ground.move_player_to_portal(entrance_portal) # 更新玩家位置 if first_entered: _update_player_position_from_archive() first_entered = false # SceneManager.release_player() func _update_player_position_from_archive(): if ignore_archive or Engine.is_editor_hint(): return var player = SceneManager.get_player() as MainPlayer if player: # if GlobalConfig.DEBUG: # print("update player position", ArchiveManager.archive.player_global_position) if ArchiveManager.archive.player_global_position_x >= 0: player.global_position.x = ArchiveManager.archive.player_global_position_x player.set_facing_direction(ArchiveManager.archive.player_direction) 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 if ResourceLoader.load_threaded_get_status(path) == ResourceLoader.THREAD_LOAD_LOADED: scene = 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 # 读取 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) 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() first_entered = true transition_to_scene.call_deferred(current_scene, entrance_portal, 0.0)