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)