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