Initial project sync

This commit is contained in:
wxm
2026-07-06 22:55:14 -07:00
commit 8cca00d0da
1066 changed files with 58585 additions and 0 deletions
+196
View File
@@ -0,0 +1,196 @@
extends SceneTree
## 第四轮修复契约(2026-07-0507-06 修订):
## 1. 小怪击退:残速不再被 AI 每帧接管清零,产生实际水平滑行;
## 垂直击退后假高度轴正常积分并落地复位 Grounded(修复"被剑雨/下砸
## 打过一次后 ground_state 永久 Airborne、小怪从此不能出招"的隐藏 bug)。
## 2. 趔趄门禁:击退滑行期间 Boss/小怪不得起手新动作(EnemyActionDriver
## 钩子),否则动作突进立刻接管 velocity 冲回玩家,把击退整个吃掉
## (真实 stage 实测净位移为负);Boss 受击链强制撤退豁免(清残速标志)。
## 3. 关卡背景单层视差:背景水平速度为相机 0.8 倍(0.92 肉眼难辨已加强),
## 锚点 2047,无相机保持原位;脚下地砖轻微滑动是单张烙死原画的已知
## 取舍,无伤视差待美术拆层。
## (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
# 无相机:背景保持原位(headless/菜单兜底)。
_expect_float(art_layer.position.x, 896.5, "无相机时背景保持基准位置")
# 有相机:偏移 = (相机中心 - 1734.5) × 0.8——"远景天幕"语义,背景 80%
# 跟随相机,仅 0.2 倍速随世界滚动,保证远景夕阳时刻在画面内(策划定案)。
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 + (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 + (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)