extends Area2D signal collected ## 钥匙中心到"脚底"的距离:射线从脚底出发,落地时钥匙正好站在表面上 const FOOT_OFFSET := 7.0 const STEP := 1.0 / 60.0 const MAX_STEPS := 240 enum State { IDLE, THROWN, LANDED } ## 关卡里那把钥匙原本是用 TerrainBase 的一块地砖画的。 ## 这个开关让钥匙对象在开场时把脚下那块占位地砖擦掉,避免两个钥匙重叠。 @export var erase_placeholder_on_ready: bool = true ## 落地后多久才允许再次被捡起 @export var settle_time: float = 0.4 ## 飞行时用哪几层做落地判定。玩家投掷时会把它设成玩家自己的 collision_mask, ## 所以"钥匙能落在哪"严格等于"玩家能站在哪"。 @export var flight_mask: int = 1 var state: State = State.IDLE var velocity := Vector2.ZERO var _settle := 0.0 func _ready() -> void: body_entered.connect(_on_body_entered) if erase_placeholder_on_ready: _erase_placeholder_tile() _start_idle() func _physics_process(_delta: float) -> void: if state == State.LANDED: _settle += STEP if _settle >= settle_time: _start_idle() return if state != State.THROWN: return var g := gravity() var motion := velocity * STEP var from := global_position + Vector2(0.0, FOOT_OFFSET) var query := PhysicsRayQueryParameters2D.create(from, from + motion, flight_mask) var hit := get_world_2d().direct_space_state.intersect_ray(query) if hit.is_empty(): global_position += motion velocity.y += g * STEP else: global_position = hit["position"] - Vector2(0.0, FOOT_OFFSET) velocity = Vector2.ZERO state = State.LANDED _settle = 0.0 # ---------------------------------------------------------------- 对外接口 func launch(v: Vector2) -> void: velocity = v state = State.THROWN monitoring = false _settle = 0.0 func _start_idle() -> void: state = State.IDLE velocity = Vector2.ZERO monitoring = true func _on_body_entered(body: Node2D) -> void: if state != State.IDLE: return if body.has_method("collect_key"): body.collect_key() collected.emit() queue_free() # ---------------------------------------------------------------- 轨迹模拟 static func gravity() -> float: return float(ProjectSettings.get_setting("physics/2d/default_gravity", 980.0)) ## 和 _physics_process 里的推进方式完全一致,玩家用它来画预览弧线, ## 所以预览落点和真正落点会重合。mask 就是玩家的碰撞掩码。 static func simulate_trajectory( space: PhysicsDirectSpaceState2D, start: Vector2, vel: Vector2, mask: int, max_steps: int = MAX_STEPS) -> Dictionary: var points := PackedVector2Array() var p := start var v := vel var g := gravity() points.append(p) for i in max_steps: var next := p + v * STEP var query := PhysicsRayQueryParameters2D.create( p + Vector2(0.0, FOOT_OFFSET), next + Vector2(0.0, FOOT_OFFSET), mask) var hit := space.intersect_ray(query) if not hit.is_empty(): var landing: Vector2 = hit["position"] - Vector2(0.0, FOOT_OFFSET) points.append(landing) return {"points": points, "landed": true, "landing": landing} p = next v.y += g * STEP points.append(p) return {"points": points, "landed": false, "landing": p} # ---------------------------------------------------------------- 清理占位地砖 func _erase_placeholder_tile() -> void: var root: Node = owner if owner != null else get_parent() if root == null: return for layer in root.find_children("*", "TileMapLayer", true, false): var cell: Vector2i = layer.local_to_map(layer.to_local(global_position)) if layer.get_cell_source_id(cell) != -1: layer.erase_cell(cell)