175 lines
6.7 KiB
GDScript
175 lines
6.7 KiB
GDScript
extends SceneTree
|
||
|
||
## 第三轮修复契约(2026-07-05):
|
||
## 1. 标题/难度背景改用无 Logo 的干净关卡原画 + 独立悬浮 Logo,
|
||
## 背景以 2 秒为单位渐变轮换,渐变期间 UI 不再重叠。
|
||
## 2. 判定字样(JudgementArt/Label)显示 1 秒后自动消失,新判定顶替重置计时。
|
||
## 3. Boss 被击退:击退残速不再被 handle_movement 清零,滑行距离 ×3
|
||
## (距离 ∝ 初速²,由 CombatManager 按 √3 换算初速)。
|
||
|
||
const CLEAN_PAST_ART := "res://assets/art/ground/past/past.png"
|
||
const CLEAN_FUTURE_ART := "res://assets/art/ground/future/future.png"
|
||
|
||
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_title_composition()
|
||
await _check_judgement_auto_hide()
|
||
await _check_boss_knockback()
|
||
_finish()
|
||
|
||
|
||
func _check_title_composition() -> void:
|
||
var flow: Node = load("res://autoload/game_flow_manager.gd").new()
|
||
flow.name = "FlowUnderTestRound3"
|
||
flow.set("manage_scene", false)
|
||
root.add_child(flow)
|
||
await process_frame
|
||
|
||
var consts: Dictionary = (flow.get_script() as Script).get_script_constant_map()
|
||
_expect_float(float(consts.get("TITLE_BG_HOLD_SECONDS", 0.0)), 2.0, "标题背景以 2 秒为单位轮流切换")
|
||
_expect_float(float(consts.get("TITLE_BG_FADE_SECONDS", 0.0)), 0.8, "轮换带渐入过渡")
|
||
|
||
var title_screen := (flow.get("_screens") as Dictionary).get(&"Title", null) as Control
|
||
_expect(title_screen != null, "标题屏存在")
|
||
if title_screen != null:
|
||
var past := title_screen.get_node_or_null("SceneBackgroundPast") as TextureRect
|
||
var future := title_screen.get_node_or_null("SceneBackgroundFuture") as TextureRect
|
||
_expect(past != null and past.texture != null and past.texture.resource_path == CLEAN_PAST_ART,
|
||
"标题底层背景使用干净的过去原画(无 Logo 烙印)")
|
||
_expect(future != null and future.texture != null and future.texture.resource_path == CLEAN_FUTURE_ART,
|
||
"标题叠层背景使用干净的未来原画(无 Logo 烙印)")
|
||
var logo := title_screen.get_node_or_null("TitleLogo") as TextureRect
|
||
_expect(logo != null, "标题持有独立悬浮 Logo 节点")
|
||
if logo != null:
|
||
_expect(logo.position.x > 340.0, "Logo 悬浮在背景中央区域(面板之外),渐变时保持固定")
|
||
_expect(logo.size.is_equal_approx(Vector2(616.0, 300.0)), "Logo 按原生尺寸显示")
|
||
_expect(logo.mouse_filter == Control.MOUSE_FILTER_IGNORE, "悬浮 Logo 不得拦截鼠标点击")
|
||
|
||
var difficulty_screen := (flow.get("_screens") as Dictionary).get(&"DifficultySelect", null) as Control
|
||
_expect(difficulty_screen != null, "难度屏存在")
|
||
if difficulty_screen != null:
|
||
var bg := difficulty_screen.get_node_or_null("SceneBackground") as TextureRect
|
||
_expect(bg != null and bg.texture != null and bg.texture.resource_path == CLEAN_FUTURE_ART,
|
||
"难度屏背景使用干净的未来原画")
|
||
_expect(difficulty_screen.get_node_or_null("TitleLogo") is TextureRect,
|
||
"难度屏同样叠加悬浮 Logo")
|
||
|
||
flow.free()
|
||
|
||
|
||
func _check_judgement_auto_hide() -> void:
|
||
var scene: PackedScene = load("res://scenes/ui/main_ui.tscn")
|
||
if scene == null:
|
||
failures.append("main_ui.tscn 应可加载")
|
||
return
|
||
var ui := scene.instantiate()
|
||
root.add_child(ui)
|
||
await process_frame
|
||
|
||
var track := ui.get_node_or_null("RhythmTrack") as Control
|
||
var art := track.get_node_or_null("JudgementArt") as TextureRect if track != null else null
|
||
if art == null:
|
||
failures.append("RhythmTrack 应持有 JudgementArt")
|
||
ui.free()
|
||
return
|
||
|
||
var bus := _event_bus()
|
||
bus.emit_signal("judgement_made", &"perfect", 0.0, 12)
|
||
await process_frame
|
||
_expect(art.visible, "判定字样在判定后立即可见")
|
||
await create_timer(0.5).timeout
|
||
_expect(art.visible, "判定字样满 1 秒前保持可见")
|
||
await create_timer(0.85).timeout
|
||
_expect(not art.visible, "判定字样满 1 秒后自动消失")
|
||
|
||
# 顶替语义:1 秒内出现新判定则重置计时,从新判定起再算 1 秒。
|
||
bus.emit_signal("judgement_made", &"good", 20.0, 13)
|
||
await process_frame
|
||
_expect(art.visible, "新判定重新点亮字样")
|
||
await create_timer(0.6).timeout
|
||
bus.emit_signal("judgement_made", &"perfect", 0.0, 14)
|
||
await process_frame
|
||
await create_timer(0.6).timeout
|
||
_expect(art.visible, "被顶替后计时重置:距新判定不足 1 秒仍可见")
|
||
await create_timer(0.7).timeout
|
||
_expect(not art.visible, "顶替后的判定同样满 1 秒消失")
|
||
|
||
ui.free()
|
||
|
||
|
||
func _check_boss_knockback() -> void:
|
||
var combat := root.get_node_or_null("CombatManager")
|
||
var boss_scene: PackedScene = load("res://scenes/enemies/boss.tscn")
|
||
if boss_scene == null or combat == null:
|
||
failures.append("boss.tscn 与 CombatManager 应可用")
|
||
return
|
||
var boss := boss_scene.instantiate() as Node2D
|
||
root.add_child(boss)
|
||
await process_frame
|
||
boss.global_position = Vector2(500.0, 300.0)
|
||
|
||
# 2026-07-05 策划两轮调参:首轮 ×3 试玩仍嫌短,加码到 ×9(初速 ×3)。
|
||
_expect_float(float(boss.call("knockback_distance_taken_mult")), 9.0, "Boss 被击退距离倍数为 9")
|
||
_expect_float(float(combat.call("_knockback_velocity_scale_for_receiver", boss)), 3.0,
|
||
"CombatManager 将距离 ×9 换算为初速 ×3")
|
||
var plain := Node2D.new()
|
||
root.add_child(plain)
|
||
_expect_float(float(combat.call("_knockback_velocity_scale_for_receiver", plain)), 1.0,
|
||
"无钩子的受击者保持原始击退")
|
||
plain.free()
|
||
|
||
# 结构性修复:击退残速必须在 Boss 的 handle_movement 里活下来并产生滑行。
|
||
var motor := boss.get_node("MovementMotor")
|
||
var start_x := boss.global_position.x
|
||
motor.call("apply_knockback", Vector2(120.0, 0.0))
|
||
await physics_frame
|
||
await physics_frame
|
||
await physics_frame
|
||
_expect((boss as CharacterBody2D).velocity.x > 0.0, "击退残速不再被 Boss 每帧清零")
|
||
for _index: int in range(60):
|
||
await physics_frame
|
||
var displacement := boss.global_position.x - start_x
|
||
_expect(displacement > 5.0, "Boss 击退产生实际水平滑行(got %.2f px)" % displacement)
|
||
|
||
boss.free()
|
||
|
||
|
||
func _event_bus() -> Node:
|
||
var bus := root.get_node_or_null("EventBus")
|
||
if bus == null:
|
||
bus = load("res://autoload/event_bus.gd").new()
|
||
bus.name = "EventBus"
|
||
root.add_child(bus)
|
||
return bus
|
||
|
||
|
||
func _expect(condition: bool, label: String) -> void:
|
||
if not condition:
|
||
failures.append(label)
|
||
|
||
|
||
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 round3 fixes")
|
||
quit(0)
|
||
else:
|
||
for failure: String in failures:
|
||
push_error(failure)
|
||
quit(1)
|