Files
2026-07-06 22:59:47 -07:00

184 lines
7.1 KiB
GDScript

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)