@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") var default_portal := "left" @export var display_hud := true @export_group("Player", "player_") @export var player_y_fixed := true: set(val): player_y_fixed = val if is_node_ready(): reset_player_y() @export var player_y := DEFAULT_PLAYER_Y: set(val): player_y = val if is_node_ready(): reset_player_y() @export var replace_player_to_portal := false: set(val): replace_player_to_portal = false if is_node_ready(): move_player_to_portal(default_portal) @export_group("Sound") @export_enum("none", "ghost", "硬地面", "室外", "crawling", "盒子猫") var footstep_type: String = "硬地面": set(val): footstep_type = val if is_node_ready(): _load_footstep_audio() # 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 footstep_audio := %FootstepAudioPlayer as RandomAudioStreamPlayer # State var restarting := false func _enter_tree() -> void: # 仅在编辑器中调试时,通过 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 func _ready() -> void: if restarting: print("restarting: skip ground _ready()") return _setup_scene() _validate_scene_name() _set_camera_and_player_boundary() if Engine.is_editor_hint(): return _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() _setup_player_light() SceneManager.toggle_hud_display(display_hud) 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() 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: if player: return player if GlobalConfig.DEBUG: print("[Ground] get_player before ready") return get_node_or_null("MainPlayer") as MainPlayer func get_camera() -> CameraFocusMarker: if camera_focus_marker: return camera_focus_marker if GlobalConfig.DEBUG: print("[Ground] get_camera before ready") return get_node_or_null("CameraFocusMarker") as CameraFocusMarker func reset_player_y() -> void: # 从屏幕下边缘算起 if player_y_fixed: var p = get_player() if p: p.set_y_from_ground(158.0 - player_y) 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) _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() 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 _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" and FOOTSTEP_AUDIO.has(footstep_type): var audio = FOOTSTEP_AUDIO[footstep_type] as AudioStreamCollection footstep_audio.audio_collections.append(audio) func play_footstep_sound() -> void: if not footstep_audio.audio_collections.is_empty(): footstep_audio.play_random() func move_player_to_portal(portal_name: String) -> void: var portal_node = _get_portal_node(portal_name) var mov_player = get_player() as MainPlayer 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 _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 # 设置角色身上光源 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)