Initial project sync

This commit is contained in:
wxm
2026-07-06 22:55:14 -07:00
commit 8cca00d0da
1066 changed files with 58585 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
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)