Files
rthythm_archor_game/tests/test_round4_fixes.gd
T

201 lines
8.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends SceneTree
## 第四轮修复契约(2026-07-0507-06 修订):
## 1. 小怪击退:残速不再被 AI 每帧接管清零,产生实际水平滑行;
## 垂直击退后假高度轴正常积分并落地复位 Grounded(修复"被剑雨/下砸
## 打过一次后 ground_state 永久 Airborne、小怪从此不能出招"的隐藏 bug)。
## 2. 趔趄门禁:击退滑行期间 Boss/小怪不得起手新动作(EnemyActionDriver
## 钩子),否则动作突进立刻接管 velocity 冲回玩家,把击退整个吃掉
## (真实 stage 实测净位移为负);Boss 受击链强制撤退豁免(清残速标志)。
## 3. 关卡背景分层契约:当前整图里的地面/步道先锁在 1:1 世界层,避免脚下
## 地砖相对角色滑动;美术拆出的天空/夕阳放入 SkyParallaxLayer 后继续按
## 0.2 倍世界滚动(相机 0.8 跟随)。
## (Boss 击退 ×9 的加码钉在 test_round3_fixes。)
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_minion_horizontal_knockback()
await _check_minion_vertical_knockback_recovers()
await _check_stagger_gates_enemy_actions()
await _check_boss_to_player_knockback()
await _check_background_parallax()
_finish()
func _make_minion() -> Node2D:
var scene: PackedScene = load("res://scenes/enemies/minion.tscn")
var minion := scene.instantiate() as Node2D
root.add_child(minion)
# 与 test_minion_phase_system 同款:关 AI,排除追击/巡逻对位移的污染。
var behavior := minion.get_node_or_null("MinionBehavior")
if behavior != null:
behavior.set("enabled", false)
minion.set("approach_direction", 0.0)
return minion
func _check_minion_horizontal_knockback() -> void:
var minion := _make_minion()
await process_frame
minion.global_position = Vector2(500.0, 300.0)
var motor := minion.get_node("MovementMotor")
var start_x := minion.global_position.x
motor.call("apply_knockback", Vector2(120.0, 0.0))
await physics_frame
await physics_frame
await physics_frame
_expect((minion as CharacterBody2D).velocity.x > 0.0, "小怪击退残速不再被 AI 每帧清零")
for _index: int in range(40):
await physics_frame
var displacement := minion.global_position.x - start_x
_expect(displacement > 5.0, "小怪击退产生实际水平滑行(got %.2f px" % displacement)
_expect(not bool(motor.call("has_knockback_stray")), "残速衰减完后 stray 标志自清(AI 可恢复接管)")
minion.free()
func _check_minion_vertical_knockback_recovers() -> void:
var minion := _make_minion()
await process_frame
minion.global_position = Vector2(500.0, 300.0)
var motor := minion.get_node("MovementMotor")
var state_machine := minion.get_node("StateMachine")
motor.call("apply_knockback", Vector2(0.0, 300.0))
await physics_frame
_expect_equal(state_machine.call("get_ground_state"), &"Airborne", "垂直击退令小怪进入 Airborne")
var landed := false
for _index: int in range(180):
await physics_frame
if state_machine.call("get_ground_state") == &"Grounded" \
and is_zero_approx(float(minion.get("height"))) \
and is_zero_approx(float(minion.get("height_speed"))):
landed = true
break
_expect(landed, "垂直击退后小怪应自然落地复位 Grounded(旧版永久卡 Airborne 导致无法出招)")
minion.free()
func _check_stagger_gates_enemy_actions() -> void:
# Boss:击退滑行期间不得起手新动作;强制撤退清残速后恢复。
var boss_scene: PackedScene = load("res://scenes/enemies/boss.tscn")
var boss := boss_scene.instantiate() as Node2D
root.add_child(boss)
await process_frame
boss.set("combat_enabled", true)
boss.set("stationary", false)
# 用不解析的动作 ID 旁路 BossBehaviorTree 的相位限型分支(melee/ranged
# 按时间相位限制与本契约无关),只隔离趔趄门禁本身。
var boss_motor := boss.get_node("MovementMotor")
_expect(bool(boss.call("can_start_enemy_action", &"__stagger_probe__")), "无击退时 Boss 可正常起手")
boss_motor.call("apply_knockback", Vector2(120.0, 0.0))
_expect(not bool(boss.call("can_start_enemy_action", &"__stagger_probe__")),
"击退滑行(趔趄)期间 Boss 不得起手新动作——否则突进会吃掉击退位移")
boss_motor.call("clear_knockback_stray")
_expect(bool(boss.call("can_start_enemy_action", &"__stagger_probe__")),
"清残速标志后恢复起手(受击链强制撤退走此豁免)")
boss.free()
# 小怪:同款门禁。
var minion := _make_minion()
await process_frame
var minion_motor := minion.get_node("MovementMotor")
_expect(bool(minion.call("can_start_enemy_action", &"__stagger_probe__")), "无击退时小怪可正常起手")
minion_motor.call("apply_knockback", Vector2(120.0, 0.0))
_expect(not bool(minion.call("can_start_enemy_action", &"__stagger_probe__")),
"击退滑行期间小怪不得起手新动作")
minion.free()
func _check_boss_to_player_knockback() -> void:
# 2026-07-06 策划(连续三轮):"Boss 对玩家没有任何击退效果"。两个根因:
# ①Boss 近战沿用默认基准 120,普通连段倍率 0.4/0.5 → 位移仅 2~3px,且相机
# 1:1 跟随玩家把位移完全吃进视觉;②敌方弹丸基准击退 (0,0),未来相位
# Boss 只用远程 → 恒为零。修复=Boss 近战基准 ×3 初速 + 敌弹补基准。
var boss_scene: PackedScene = load("res://scenes/enemies/boss.tscn")
var boss := boss_scene.instantiate()
root.add_child(boss)
await process_frame
var emitter := boss.get_node("DamageEmitter")
_expect((emitter.get("base_knockback") as Vector2).is_equal_approx(Vector2(360.0, 304.056)),
"Boss 近战基准击退 ×3 初速(普通连段 2px→20~32px、重击 190~343px")
boss.free()
var container: Node2D = (load("res://scenes/stage/actors_container.gd") as GDScript).new()
root.add_child(container)
container.call("_on_projectile_requested", null, Vector2.ZERO, Vector2.LEFT, {"team": &"enemy"})
await process_frame
var bullet := container.get_child(0)
_expect((bullet.get("base_knockback") as Vector2).is_equal_approx(Vector2(360.0, 0.0)),
"敌方弹丸补上基准击退(未来相位不再恒为零,弹丸不击飞)")
_expect_float(float(bullet.get("speed")), 416.0, "敌弹速度保持 416 不受击退注入影响")
container.free()
func _check_background_parallax() -> void:
var scene: PackedScene = load("res://scenes/ground/ground1.tscn")
var ground := scene.instantiate()
root.add_child(ground)
await process_frame
await process_frame
var art_layer := ground.get_node("ArtLayer") as Node2D
var sky_layer := ground.get_node("SkyParallaxLayer") as Node2D
# 无相机:世界层与可选远景层都保持原位(headless/菜单兜底)。
_expect_float(art_layer.position.x, 896.5, "无相机时地面世界层保持基准位置")
_expect_float(sky_layer.position.x, 896.5, "无相机时远景视差层保持基准位置")
# 有相机:地面/步道保持 1:1 世界坐标;远景容器按
# (相机中心 - 1734.5) × 0.8 视差移动。
var camera := Camera2D.new()
root.add_child(camera)
camera.make_current()
camera.global_position = Vector2(2547.0, 395.0)
await process_frame
await process_frame
_expect_float(art_layer.position.x, 896.5, "相机右移时地面世界层不做视差")
_expect_float(sky_layer.position.x, 896.5 + (2547.0 - 1734.5) * 0.8, "相机右移时远景层 80% 跟随")
camera.global_position = Vector2(1547.0, 395.0)
await process_frame
await process_frame
_expect_float(art_layer.position.x, 896.5, "相机左移时地面世界层仍不做视差")
_expect_float(sky_layer.position.x, 896.5 + (1547.0 - 1734.5) * 0.8, "相机左移时远景层对称跟随")
camera.free()
ground.free()
func _expect(condition: bool, label: String) -> void:
if not condition:
failures.append(label)
func _expect_equal(actual: Variant, expected: Variant, label: String) -> void:
if actual != expected:
failures.append("%s: expected %s, got %s" % [label, expected, actual])
func _expect_float(actual: float, expected: float, label: String) -> void:
if not is_equal_approx(actual, expected):
failures.append("%s: expected %.4f, got %.4f" % [label, expected, actual])
func _finish() -> void:
paused = false
if failures.is_empty():
print("PASS round4 fixes")
quit(0)
else:
for failure: String in failures:
push_error(failure)
quit(1)