Initial project sync
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user