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

170 lines
7.1 KiB
GDScript

extends SceneTree
## 关卡规则:只有处于强势状态的怪才会打断玩家当前的状态。
## 弱势怪的攻击只结算伤害 —— 不造成 Hitstun、不取消动作、不产生击退;
## 强弱在动作开始时快照(§17.4);Boss 恒定视为强势。
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")
_ensure_event_bus()
var manager := _ensure_time_phase_manager()
var combat := _ensure_combat_manager()
# 受击方:最小可受击体(StateMachine + HealthComponent + DamageReceiver)。
var victim := Node2D.new()
victim.name = "Player"
root.add_child(victim)
var state_machine: Node = load("res://scenes/components/state_machine.gd").new()
state_machine.name = "StateMachine"
victim.add_child(state_machine)
var health: Node = load("res://scenes/components/health_component.gd").new()
health.name = "HealthComponent"
victim.add_child(health)
var receiver: Area2D = load("res://scenes/components/damage_receiver.gd").new()
receiver.name = "DamageReceiver"
victim.add_child(receiver)
health.call("set_values", 300, 300)
# 攻击方:真实小怪场景(近战 / past_strong)。
var minion: Node2D = (load("res://scenes/enemies/minion.tscn") as PackedScene).instantiate()
minion.name = "AttackerMinion"
minion.global_position = Vector2(4000.0, 0.0)
root.add_child(minion)
await process_frame
var behavior := minion.get_node_or_null("MinionBehavior")
if behavior != null:
behavior.set("enabled", false)
var emitter: Area2D = minion.get_node("DamageEmitter")
# ---- 弱势(近战怪 × Future):只掉血,不打断,不击退 ----
manager.call("reset_to_initial", &"past")
await process_frame
manager.call("set_time_phase", &"future", &"debug")
await process_frame
_expect(not bool(minion.call("is_strong_in_current_time_phase")), "past_strong melee minion should be WEAK in Future")
var weak_action: Resource = load("res://resources/actions/enemies/minion_jin_zhan_1_attack.tres")
emitter.call("configure_hit", weak_action, {"label": "perfect"})
var weak_result: Dictionary = combat.call("resolve_hit", emitter, receiver)
_expect(int(weak_result.get("damage", 0)) > 0, "Weak hit should still deal damage")
_expect(int(health.get("current")) < 300, "Weak hit should reduce player HP")
_expect(not bool(weak_result.get("interrupts", true)), "Weak-state attacker must NOT interrupt")
_expect_equal(weak_result.get("knockback", Vector2.ONE), Vector2.ZERO, "Weak-state attacker must not knock the player back")
_expect_equal(state_machine.call("build_context").get("life_state"), &"Alive", "Weak hit must not put the player into Hitstun")
# ---- 快照规则(§17.4):出手时弱势,命中前切回强势相位仍不打断 ----
emitter.call("configure_hit", weak_action, {"label": "perfect"})
manager.call("set_time_phase", &"past", &"debug")
await physics_frame
await process_frame
var snapshot_result: Dictionary = combat.call("resolve_hit", emitter, receiver)
_expect(not bool(snapshot_result.get("interrupts", true)), "Interrupt authority must snapshot at action START, not at hit time")
_expect_equal(state_machine.call("build_context").get("life_state"), &"Alive", "Snapshot-weak hit must not stun the player")
# ---- 强势(近战怪 × Past):正常打断 ----
health.call("set_values", 300, 300)
state_machine.call("set_life_state", &"Alive")
await physics_frame
await process_frame
_expect(bool(minion.call("is_strong_in_current_time_phase")), "past_strong melee minion should be STRONG in Past")
var strong_action: Resource = load("res://resources/actions/enemies/minion_jin_zhan_3_attack_1.tres")
emitter.call("configure_hit", strong_action, {"label": "perfect"})
var strong_result: Dictionary = combat.call("resolve_hit", emitter, receiver)
_expect(bool(strong_result.get("interrupts", false)), "Strong-state attacker keeps interrupt rights")
_expect_equal(state_machine.call("build_context").get("life_state"), &"Hitstun", "Strong hit should put the player into Hitstun")
# ---- 弹丸同样遵守规则(弱势远程怪的子弹) ----
health.call("set_values", 300, 300)
state_machine.call("set_life_state", &"Alive")
await physics_frame
await process_frame
var projectile: Area2D = load("res://scenes/combat/player_projectile.gd").new()
root.add_child(projectile)
projectile.set("damage", 25)
projectile.set("attacker_interrupts", false)
projectile.set("judgement_context", {"label": "perfect"})
var weak_shot: Dictionary = combat.call("resolve_hit", projectile, receiver)
_expect(int(weak_shot.get("damage", 0)) > 0, "Weak projectile should still deal damage")
_expect(not bool(weak_shot.get("interrupts", true)), "Weak-state projectile must not interrupt")
_expect_equal(state_machine.call("build_context").get("life_state"), &"Alive", "Weak projectile must not stun the player")
projectile.set("attacker_interrupts", true)
await physics_frame
await process_frame
var strong_shot: Dictionary = combat.call("resolve_hit", projectile, receiver)
_expect(bool(strong_shot.get("interrupts", false)), "Strong-state projectile keeps interrupt rights")
_expect_equal(state_machine.call("build_context").get("life_state"), &"Hitstun", "Strong projectile should stun the player")
# ---- Boss 不分强弱:两个相位都视为强势 ----
var boss: Node = (load("res://scenes/enemies/boss.tscn") as PackedScene).instantiate()
boss.set("combat_enabled", false)
root.add_child(boss)
await process_frame
manager.call("set_time_phase", &"future", &"debug")
await process_frame
_expect(bool(boss.call("is_strong_in_current_time_phase")), "Boss must count as strong in Future")
manager.call("set_time_phase", &"past", &"debug")
await process_frame
_expect(bool(boss.call("is_strong_in_current_time_phase")), "Boss must count as strong in Past")
boss.free()
projectile.free()
minion.free()
victim.free()
_finish()
func _ensure_event_bus() -> Node:
var bus := root.get_node_or_null("EventBus")
if bus == null:
bus = load("res://autoload/event_bus.gd").new()
bus.name = "EventBus"
root.add_child(bus)
return bus
func _ensure_time_phase_manager() -> Node:
var manager := root.get_node_or_null("TimePhaseManager")
if manager == null:
manager = load("res://autoload/time_phase_manager.gd").new()
manager.name = "TimePhaseManager"
root.add_child(manager)
return manager
func _ensure_combat_manager() -> Node:
var combat := root.get_node_or_null("CombatManager")
if combat == null:
combat = load("res://autoload/combat_manager.gd").new()
combat.name = "CombatManager"
root.add_child(combat)
return combat
func _expect(condition: bool, label: String) -> void:
if not condition:
failures.append(label)
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 strong interrupt rule")
quit(0)
else:
for failure: String in failures:
push_error(failure)
quit(1)