131 lines
4.6 KiB
GDScript
131 lines
4.6 KiB
GDScript
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)
|