extends SceneTree ## 2026-07-06 策划契约:冲刺穿人机制保留,但其余时间玩家不得与敌人重叠。 ## 旧行为:dash_through 落点嵌在敌人体内时,恢复碰撞的逻辑"干等分离"—— ## 嵌着就无限期保持幽灵态,玩家可长时间站在小怪/Boss 身体里。 ## 新行为:嵌入时沿冲刺方向每物理帧温和推出(保留"从远侧穿出"手感), ## 推清后恢复身体碰撞。 const ENEMY_BODY_BIT := 1 << 6 var failures: Array[String] = [] func _init() -> void: _run.call_deferred() func _run() -> void: root.size = Vector2i(1152, 648) var settings := root.get_node_or_null("GameSettings") if settings != null: settings.call("set_difficulty", &"easy") await _check_embedded_dash_gets_separated() await _check_clean_dash_restores_immediately() _finish() func _make_actors() -> Dictionary: var player := (load("res://scenes/characters/player.tscn") as PackedScene).instantiate() as CharacterBody2D root.add_child(player) var minion := (load("res://scenes/enemies/minion.tscn") as PackedScene).instantiate() as CharacterBody2D root.add_child(minion) var behavior := minion.get_node_or_null("MinionBehavior") if behavior != null: behavior.set("enabled", false) minion.set("approach_direction", 0.0) return {"player": player, "minion": minion} func _dash_action() -> Resource: var script: Script = load("res://resources/action_data.gd") var action: Resource = script.new() var action_tags: Array[StringName] = [&"dash_through"] action.set("id", &"test_dash_embed") action.set("action_tags", action_tags) action.set("move_mult_x", 1.0) action.set("startup_beats", 0.25) action.set("active_beats", 0.25) action.set("recovery_beats", 0.25) return action func _check_embedded_dash_gets_separated() -> void: var actors := _make_actors() var player := actors["player"] as CharacterBody2D var minion := actors["minion"] as CharacterBody2D await process_frame player.global_position = Vector2(500.0, 300.0) # 冲刺时长 0.75 拍 × 0.5s × 220px/s ≈ 82px:小怪摆在 80px 处,落点必然嵌入。 minion.global_position = Vector2(580.0, 300.0) await physics_frame var motion: Node = player.get_node("MotionExecutor") motion.call("execute", _dash_action(), Vector2.RIGHT, 0.5, 220.0) _expect((int(player.collision_mask) & ENEMY_BODY_BIT) == 0, "冲刺期间保持幽灵态(机制保留)") # 裸场景无 handle_movement 驱动,手动按真实链路推进:tick → move_and_slide。 var finished := false for _index: int in range(90): player.velocity = motion.call("tick", 1.0 / 60.0) player.move_and_slide() await physics_frame if not bool(motion.get("active")): finished = true break _expect(finished, "冲刺应在 90 帧内结束") # 新契约:嵌入不再无限期保持幽灵态——有限帧内推出并恢复碰撞。 var separated := false for _index: int in range(45): await physics_frame if not bool(motion.call("is_ghosting")): separated = true break _expect(separated, "落点嵌入后应在 45 帧内被推出并恢复碰撞(旧行为:无限期重叠)") if separated: _expect((int(player.collision_mask) & ENEMY_BODY_BIT) != 0, "分离后玩家身体碰撞恢复") _expect(player.global_position.x > minion.global_position.x, "应从冲刺方向的远侧穿出(不被弹回入口侧)") var gap := absf(player.global_position.x - minion.global_position.x) _expect(gap >= 18.0, "分离后身体不再重叠(间距 %.1f px ≥ 半宽和)" % gap) player.free() minion.free() func _check_clean_dash_restores_immediately() -> void: # 落点干净(前方无敌人)时行为与旧版一致:结束即恢复。 var player := (load("res://scenes/characters/player.tscn") as PackedScene).instantiate() as CharacterBody2D root.add_child(player) await process_frame player.global_position = Vector2(500.0, 300.0) var motion: Node = player.get_node("MotionExecutor") motion.call("execute", _dash_action(), Vector2.RIGHT, 0.5, 220.0) for _index: int in range(90): player.velocity = motion.call("tick", 1.0 / 60.0) player.move_and_slide() await physics_frame if not bool(motion.get("active")): break await physics_frame await physics_frame _expect(not bool(motion.call("is_ghosting")), "干净落点:冲刺结束立即恢复碰撞") _expect((int(player.collision_mask) & ENEMY_BODY_BIT) != 0, "干净落点:敌人身体位复位") player.free() func _expect(condition: bool, label: String) -> void: if not condition: failures.append(label) func _finish() -> void: paused = false if failures.is_empty(): print("PASS dash overlap separation") quit(0) else: for failure: String in failures: push_error(failure) quit(1)