extends SceneTree ## new3 定案 (2026-07-05) 专项回归: ## 1. Boss 对不耗能量的攻击恒霸体(掉血但不打断、不击退);耗能技能可压制。 ## 2. 近战小怪基础攻击 50 / 远程 25(LevelDirector 按职责配置)。 ## 3. 远程小怪连续受击 4 次触发无敌闪烁 + 跳跃脱离。 var failures: Array[String] = [] func _init() -> void: _run.call_deferred() func _run() -> void: await process_frame var rhythm := root.get_node_or_null("RhythmManager") if rhythm != null and rhythm.has_method("stop_manager"): rhythm.call("stop_manager") await _check_boss_super_armor_rule() await _check_role_based_base_attack() await _check_ranged_hit_escape() _finish() func _check_boss_super_armor_rule() -> void: var boss_scene: PackedScene = load("res://scenes/enemies/boss.tscn") _expect(boss_scene != null, "boss.tscn should load") if boss_scene == null: return var boss: Node = boss_scene.instantiate() root.add_child(boss) await process_frame var normal_action: Resource = load("res://resources/actions/ground_attack_left_1.tres") var energy_action: Resource = load("res://resources/actions/dash_slash_left.tres") _expect_bool(bool(boss.call("shrugs_off_hit", null)), true, "Boss should shrug off action-less hits") _expect_bool(bool(boss.call("shrugs_off_hit", normal_action)), true, "Boss should shrug off zero-cost normal attacks") _expect_bool(bool(boss.call("shrugs_off_hit", energy_action)), false, "Boss should be suppressed by energy-cost skills") var receiver := boss.get_node("DamageReceiver") as Area2D var emitter_script: Script = load("res://scenes/components/damage_emitter.gd") var emitter: Area2D = emitter_script.new() emitter.set("damage", 100) root.add_child(emitter) var resolver: Script = load("res://scripts/resolvers/combat_resolver.gd") emitter.set("action_context", normal_action) emitter.set("judgement_context", {"label": "perfect"}) var normal_result: Dictionary = resolver.call("resolve_hit", emitter, receiver) _expect_bool(int(normal_result.get("damage", 0)) > 0, true, "Normal attack should still damage the Boss") _expect_bool(bool(normal_result.get("interrupts", true)), false, "Normal attack should not interrupt the Boss") _expect_bool((normal_result.get("knockback", Vector2.ZERO) as Vector2) == Vector2.ZERO, true, "Normal attack should not knock the Boss back") emitter.set("action_context", energy_action) emitter.set("judgement_context", {"label": "perfect"}) var skill_result: Dictionary = resolver.call("resolve_hit", emitter, receiver) _expect_bool(int(skill_result.get("damage", 0)) > 0, true, "Energy skill should damage the Boss") _expect_bool(bool(skill_result.get("interrupts", true)), true, "Energy skill should interrupt the Boss") _expect_bool((skill_result.get("knockback", Vector2.ZERO) as Vector2).x != 0.0, true, "Energy skill should knock the Boss back") emitter.free() boss.free() func _check_role_based_base_attack() -> void: var stage_scene: PackedScene = load("res://scenes/stage/stage.tscn") _expect(stage_scene != null, "stage.tscn should load") if stage_scene == null: return var stage: Node = stage_scene.instantiate() root.add_child(stage) await process_frame await process_frame var jin := stage.get_node_or_null("ActorsContainer/JinZhanMinion") _expect(jin != null, "Stage should keep the preplaced melee minion") if jin != null: var emitter := jin.get_node("DamageEmitter") _expect_int(int(emitter.get("damage")), 50, "Melee minion base attack should be doubled to 50") var director := stage.get_node_or_null("LevelDirector") if director != null: var yuan: Node = director.call("spawn_minion", &"ranged", Vector2(2100.0, 560.0)) if yuan != null: var yuan_emitter := yuan.get_node("DamageEmitter") _expect_int(int(yuan_emitter.get("damage")), 25, "Ranged minion base attack should stay 25") var behavior := yuan.get_node("MinionBehavior") _expect_bool(float(behavior.get("attack_range")) <= 460.0, true, "Ranged minion must not fire from off-screen (attack range within camera half-width)") stage.free() func _check_ranged_hit_escape() -> void: var minion_scene: PackedScene = load("res://scenes/enemies/minion.tscn") _expect(minion_scene != null, "minion.tscn should load") if minion_scene == null: return var minion: Node = minion_scene.instantiate() minion.set("past_form_id", &"yuan_cheng_1") minion.set("future_form_id", &"yuan_cheng_2") minion.set("strength_profile", &"future_strong") root.add_child(minion) await process_frame var behavior := minion.get_node("MinionBehavior") behavior.set("role", &"ranged") behavior.set("state", &"Hold") var receiver := minion.get_node("DamageReceiver") as Area2D # 连续受击 3 次仍不脱离;第 4 次触发。 for _index: int in range(3): behavior.call("_on_damage_received", 10, &"melee", Vector2.ZERO) _expect_bool(bool(behavior.call("_should_escape")), false, "Three consecutive hits should not trigger the escape yet") behavior.call("_on_damage_received", 10, &"melee", Vector2.ZERO) _expect_bool(bool(behavior.call("_should_escape")), true, "Fourth consecutive hit should trigger the escape") behavior.call("_begin_escape") _expect_equal(behavior.get("state"), &"Retreat", "Escape should enter the Retreat state") _expect_bool(bool(behavior.get("_escaping")), true, "Escape retreat should use the jump-out mode") await physics_frame await physics_frame _expect_bool(receiver.monitoring, false, "Escape should grant brief invulnerability (receiver off)") # 无敌结束后恢复可受击。 behavior.call("_tick_escape_invulnerability", 1.0) await physics_frame await physics_frame _expect_bool(receiver.monitoring, true, "Invulnerability should expire and restore the receiver") # 触发后进入冷却:不能连续无敌脱离。 for _index: int in range(4): behavior.call("_on_damage_received", 10, &"melee", Vector2.ZERO) _expect_bool(bool(behavior.call("_should_escape")), false, "Escape must not chain while the retreat cooldown is running") minion.free() func _expect(condition: bool, label: String) -> void: if not condition: failures.append(label) 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_equal(actual: Variant, expected: Variant, label: String) -> void: if actual != expected: failures.append("%s: expected %s, got %s" % [label, expected, actual]) func _finish() -> void: if failures.is_empty(): print("PASS new3 enemy rules") quit(0) else: for failure: String in failures: push_error(failure) quit(1)