302 lines
9.2 KiB
GDScript
302 lines
9.2 KiB
GDScript
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 = {
|
||
"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_棺材房
|
||
}
|
||
|
||
# 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
|
||
|
||
|
||
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)
|
||
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 = GROUND_SCENE_PATH_DICT[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(GROUND_SCENE_PATH_DICT[scene_name])
|