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)