extends SceneTree ## 2026-07-05 第二轮策划需求契约:剑雨放大(范围/视觉翻倍、伤害不变)、 ## 敌弹降速 80%(玩家斩波不变)、受击不转身 + 击退背离攻击者、 ## 胜负停留时间 ×3、结算中文 + RANK 只留等级。 var failures: Array[String] = [] func _init() -> void: _run.call_deferred() func _run() -> void: var settings := root.get_node_or_null("GameSettings") if settings != null: settings.call("set_difficulty", &"normal") _check_jian_yu_resources() _check_projectile_speeds() _check_knockback_direction() _check_heading_gate() _check_flow_delays() await _check_result_screen() if settings != null: settings.call("set_difficulty", &"normal") _finish() func _check_jian_yu_resources() -> void: var expected := { 1: {"damage_mult": 1.8, "base_cost": 20.0}, 2: {"damage_mult": 2.8, "base_cost": 35.0}, 3: {"damage_mult": 4.2, "base_cost": 50.0}, } for side: String in ["left", "right"]: for level: int in [1, 2, 3]: var path := "res://resources/actions/jian_yu_%s_lv%d.tres" % [side, level] var action := load(path) as Resource if action == null: failures.append("jian_yu resource missing: %s" % path) continue _expect_float(float(action.get("range")), 280.0, "%s range doubled to 280" % path) _expect_float(float(action.get("damage_mult")), expected[level]["damage_mult"], "%s damage unchanged" % path) _expect_float(float(action.get("base_cost")), expected[level]["base_cost"], "%s cost unchanged" % path) func _check_projectile_speeds() -> void: var container: Node2D = load("res://scenes/stage/actors_container.gd").new() container.name = "ActorsContainerUnderTest" root.add_child(container) container.call("_on_projectile_requested", null, Vector2.ZERO, Vector2.LEFT, {"team": &"enemy"}) container.call("_on_projectile_requested", null, Vector2.ZERO, Vector2.RIGHT, {"team": &"player"}) var children := container.get_children() _expect_int(children.size(), 2, "both projectiles spawn") if children.size() == 2: _expect_float(float(children[0].get("speed")), 416.0, "enemy bullets fly at 80% speed (416)") _expect_float(float(children[1].get("speed")), 520.0, "player wave keeps full speed (520)") container.free() func _check_knockback_direction() -> void: var combat := root.get_node_or_null("CombatManager") if combat == null: failures.append("CombatManager autoload missing") return var receiver := Node2D.new() receiver.name = "KnockbackReceiver" root.add_child(receiver) receiver.global_position = Vector2(100.0, 0.0) var from_right := {"from": Vector2(200.0, 0.0)} var from_left := {"from": Vector2(0.0, 0.0)} _expect_float(float(combat.call("_knockback_x_away_from_attacker", receiver, from_right, 120.0)), -120.0, "attacker on the right pushes receiver left") _expect_float(float(combat.call("_knockback_x_away_from_attacker", receiver, from_left, 120.0)), 120.0, "attacker on the left pushes receiver right") _expect_float(float(combat.call("_knockback_x_away_from_attacker", receiver, {}, 120.0)), 120.0, "missing attacker position keeps legacy +X") receiver.free() func _check_heading_gate() -> void: var actor_script := GDScript.new() actor_script.source_code = "extends CharacterBody2D\nvar heading := Vector2.LEFT\nvar speed := 180.0\nvar height := 0.0\nvar height_speed := 0.0\nvar state := &\"idle\"\n" actor_script.reload() var actor := CharacterBody2D.new() actor.name = "HeadingActor" actor.set_script(actor_script) var motor: Node = load("res://scenes/components/movement_motor.gd").new() motor.name = "MovementMotor" actor.add_child(motor) root.add_child(actor) actor.set("heading", Vector2.LEFT) motor.call("apply_knockback", Vector2(120.0, 0.0)) motor.call("set_heading") _expect_bool(Vector2(actor.get("heading")) == Vector2.LEFT, true, "knockback velocity must not flip heading (受击不转身)") # 残速衰减到 0 后门禁解除,自主移动照常改朝向。 motor.call("_decay_stray_velocity", 1.0) _expect_float(actor.velocity.x, 0.0, "knockback stray velocity decays to zero") actor.velocity.x = 50.0 motor.call("set_heading") _expect_bool(Vector2(actor.get("heading")) == Vector2.RIGHT, true, "voluntary velocity flips heading again after decay") actor.free() func _check_flow_delays() -> void: var flow_script := load("res://autoload/game_flow_manager.gd") as GDScript var constants := flow_script.get_script_constant_map() _expect_float(float(constants.get("VICTORY_SCREEN_DELAY", 0.0)), 3.6, "victory delay tripled to 3.6s") _expect_float(float(constants.get("DEFEAT_SCREEN_DELAY", 0.0)), 4.8, "defeat delay tripled to 4.8s") func _check_result_screen() -> void: var flow: Node = load("res://autoload/game_flow_manager.gd").new() flow.name = "FlowRound2" flow.set("manage_scene", false) root.add_child(flow) await process_frame var labels: Dictionary = flow.get("_result_labels") flow.set("_victory", true) flow.call("_populate_result_screen") var rank_label := labels.get("rank", null) as Label if rank_label == null: failures.append("rank label missing") else: _expect_bool(rank_label.text.begins_with("RANK "), true, "rank text keeps the RANK prefix") _expect_bool(rank_label.text.contains("/"), false, "rank text drops the score suffix (只留等级)") var result_screen := (flow.get("_screens") as Dictionary).get(&"Result", null) as Control if result_screen == null: failures.append("result screen missing") else: var texts := _collect_label_texts(result_screen) for expected_text: String in ["最大连击", "完美", "良好", "勉强", "失误", "锚点成功", "相位切换"]: _expect_bool(texts.has(expected_text), true, "result stats show Chinese label %s" % expected_text) # 页脚实时反映当前难度(标题页 + 难度页)。 flow.call("_select_difficulty", &"hard") _expect_string(str((flow.get("_title_difficulty_value") as Label).text), "困难", "title footer tracks difficulty") _expect_string(str((flow.get("_difficulty_footer_value") as Label).text), "当前难度:困难", "difficulty footer tracks difficulty") flow.call("_select_difficulty", &"normal") flow.free() func _collect_label_texts(node: Node) -> Array[String]: var texts: Array[String] = [] if node is Label: texts.append((node as Label).text) for child: Node in node.get_children(): texts.append_array(_collect_label_texts(child)) return texts func _expect_bool(actual: bool, expected: bool, label: String) -> void: if actual != expected: failures.append("%s: expected %s, got %s" % [label, expected, actual]) func _expect_int(actual: int, expected: int, label: String) -> void: if actual != expected: failures.append("%s: expected %d, got %d" % [label, expected, actual]) func _expect_float(actual: float, expected: float, label: String) -> void: if not is_equal_approx(actual, expected): failures.append("%s: expected %s, got %s" % [label, expected, actual]) func _expect_string(actual: String, expected: String, label: String) -> void: if actual != expected: failures.append("%s: expected %s, got %s" % [label, expected, actual]) func _finish() -> void: paused = false if failures.is_empty(): print("PASS round2 changes") quit(0) else: for failure: String in failures: push_error(failure) quit(1)