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
+174
View File
@@ -0,0 +1,174 @@
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)