Files
rthythm_archor_game/tests/test_new3_enemy_rules.gd
T
2026-07-06 22:59:47 -07:00

167 lines
6.7 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends SceneTree
## new3 定案 (2026-07-05) 专项回归:
## 1. Boss 对不耗能量的攻击恒霸体(掉血但不打断、不击退);耗能技能可压制。
## 2. 近战小怪基础攻击 50 / 远程 25LevelDirector 按职责配置)。
## 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)