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
+681
View File
@@ -0,0 +1,681 @@
extends SceneTree
var failures: Array[String] = []
var received_hits: Array[Dictionary] = []
var confirmed_hits: Array[Dictionary] = []
var hit_flow_events: Array[StringName] = []
var target_cancel_reasons: Array[StringName] = []
func _init() -> void:
_run.call_deferred()
func _run() -> void:
await process_frame
_check_resolver_scripts()
_check_player_scene_has_phase5_combat_nodes()
_check_judgement_policy_scales_stats()
_check_health_component_life_state()
await _check_combat_manager_resolve_hit()
await _check_combat_knockback_resolution()
await _check_combat_stump_acceptance()
await _check_knockback_lifts_target()
await _check_combat_manager_drives_target_hit_flow()
await _check_lethal_hit_cancels_with_death_reason()
await _check_effect_defense_modifiers_affect_combat_hit_flow()
await _check_dead_targets_are_skipped()
await _check_attack_tags_filter_defense_modifiers()
await _check_combat_hit_dispatches_effect_events()
await _check_combat_hit_event_filters_use_action_context()
await _check_combat_hit_dispatches_kill_and_parry_events()
await _check_action_on_hit_effects_apply_to_receiver()
_finish()
func _check_resolver_scripts() -> void:
for path: String in [
"res://scripts/resolvers/stat_resolver.gd",
"res://scripts/resolvers/defense_resolver.gd",
"res://scripts/resolvers/action_rule_resolver.gd",
"res://scripts/resolvers/combat_resolver.gd",
"res://resources/judgement_policy.gd",
"res://resources/judgement_policy.tres",
]:
_expect(load(path) != null, "%s should load" % path)
_expect(ProjectSettings.get_setting("autoload/CombatManager", "") == "*res://autoload/combat_manager.gd", "CombatManager should be registered as an autoload")
_expect(int(ProjectSettings.call("get_order", "autoload/EventBus")) < int(ProjectSettings.call("get_order", "autoload/RhythmManager")), "EventBus autoload should initialize before RhythmManager")
_expect(int(ProjectSettings.call("get_order", "autoload/RhythmManager")) < int(ProjectSettings.call("get_order", "autoload/CombatManager")), "CombatManager autoload should initialize after RhythmManager")
var combat: Node = load("res://autoload/combat_manager.gd").new()
_expect(combat.has_method("resolve_hit"), "CombatManager should expose resolve_hit(emitter, receiver)")
combat.free()
var emitter_source := _read_text("res://scenes/components/damage_emitter.gd")
_expect(emitter_source.contains("resolve_hit"), "DamageEmitter should route hits through CombatManager.resolve_hit")
_expect(not emitter_source.contains("receiver.emit_signal(\"damage_received\""), "DamageEmitter should not emit receiver damage directly")
func _check_player_scene_has_phase5_combat_nodes() -> void:
var scene: PackedScene = load("res://scenes/characters/player.tscn")
_expect(scene != null, "player.tscn should load for phase-5 combat node check")
if scene == null:
return
var player := scene.instantiate()
root.add_child(player)
var action_executor := player.get_node_or_null("ActionExecutor")
var health := player.get_node_or_null("HealthComponent")
_expect(player.get_node_or_null("DamageEmitter") is Area2D, "Player should have DamageEmitter in phase 5")
_expect(player.get_node_or_null("DamageReceiver") is Area2D, "Player should have DamageReceiver in phase 5")
_expect(health != null, "Player should have HealthComponent in phase 5")
if health != null:
_expect_float(float(health.get("hitstun_seconds")), 0.4, "HealthComponent.hitstun_seconds should match hit stun action 14")
_expect(action_executor != null and action_executor.get("damage_emitter_path") == NodePath("../DamageEmitter"), "ActionExecutor should point at Player DamageEmitter")
_expect(action_executor != null and action_executor.get("energy_component_path") == NodePath("../EnergyComponent"), "ActionExecutor should point at Player EnergyComponent")
player.free()
func _check_judgement_policy_scales_stats() -> void:
var stat_resolver: Script = load("res://scripts/resolvers/stat_resolver.gd")
if stat_resolver == null:
return
var action := _make_action(20.0, 10.0, 1.0)
_expect_int(int(round(stat_resolver.call("resolve_damage", 100.0, action, {"label": "perfect"}))), 100, "Perfect damage should use policy damage x1.0")
_expect_int(int(round(stat_resolver.call("resolve_damage", 100.0, action, {"label": "good"}))), 85, "Good damage should use policy damage x0.85")
_expect_int(int(round(stat_resolver.call("resolve_damage", 100.0, action, {"label": "bad"}))), 70, "Bad damage should use policy damage x0.7")
_expect_int(int(round(stat_resolver.call("resolve_cost", action, {"label": "perfect"}))), 16, "Perfect cost should use policy cost x0.8")
_expect_int(int(round(stat_resolver.call("resolve_cost", action, {"label": "good"}))), 20, "Good cost should use policy cost x1.0")
_expect_int(int(round(stat_resolver.call("resolve_cost", action, {"label": "bad"}))), 24, "Bad cost should use policy cost x1.2")
_expect_int(int(round(stat_resolver.call("resolve_reward", action, {"label": "perfect"}))), 10, "Perfect reward should use policy reward x1.0")
_expect_int(int(round(stat_resolver.call("resolve_reward", action, {"label": "good"}))), 8, "Good reward should use policy reward x0.8")
_expect_int(int(round(stat_resolver.call("resolve_reward", action, {"label": "bad"}))), 6, "Bad reward should use policy reward x0.6")
func _check_health_component_life_state() -> void:
var host := Node.new()
root.add_child(host)
var state_machine: Node = load("res://scenes/components/state_machine.gd").new()
state_machine.name = "StateMachine"
host.add_child(state_machine)
var health: Node = load("res://scenes/components/health_component.gd").new()
health.name = "HealthComponent"
host.add_child(health)
await process_frame
_expect_float(float(health.get("hitstun_seconds")), 0.4, "HealthComponent default hitstun should be 0.4 seconds")
health.call("set_values", 10, 10)
health.call("receive_hit", {"damage": 3})
_expect_int(int(health.get("current")), 7, "HealthComponent.receive_hit should apply damage")
_expect_equal(state_machine.call("build_context").get("life_state"), &"Hitstun", "Non-lethal hit should write LifeState Hitstun")
health.call("receive_hit", {"damage": 20})
_expect_equal(state_machine.call("build_context").get("life_state"), &"Dead", "Lethal hit should write LifeState Dead")
host.free()
func _check_combat_manager_resolve_hit() -> void:
var combat: Node = load("res://autoload/combat_manager.gd").new()
combat.name = "CombatManager"
root.add_child(combat)
var emitter: Area2D = load("res://scenes/components/damage_emitter.gd").new()
var receiver: Area2D = load("res://scenes/components/damage_receiver.gd").new()
root.add_child(emitter)
root.add_child(receiver)
emitter.set("damage", 10)
receiver.connect("damage_received", _on_damage_received)
await process_frame
var result: Dictionary = combat.call("resolve_hit", emitter, receiver)
_expect_int(int(result.get("damage", 0)), 10, "CombatManager.resolve_hit should return resolved damage")
_expect_int(received_hits.size(), 1, "CombatManager.resolve_hit should notify the receiver once")
emitter.free()
receiver.free()
combat.free()
func _check_combat_knockback_resolution() -> void:
var combat: Node = load("res://autoload/combat_manager.gd").new()
combat.name = "CombatManager"
root.add_child(combat)
var emitter: Area2D = load("res://scenes/components/damage_emitter.gd").new()
var receiver: Area2D = load("res://scenes/components/damage_receiver.gd").new()
root.add_child(emitter)
root.add_child(receiver)
var action := _make_action(0.0, 0.0, 0.0, 0.5, 2.0)
emitter.set("damage", 0)
emitter.set("base_knockback", Vector2(40, 100))
emitter.set("action_context", action)
emitter.set("judgement_context", {"label": "good"})
await process_frame
var result: Dictionary = combat.call("resolve_hit", emitter, receiver)
_expect_vector(result.get("knockback", Vector2.ZERO), Vector2(17, 170), "Combat knockback should come from ActionData fields and judgement policy")
emitter.free()
receiver.free()
combat.free()
func _check_combat_stump_acceptance() -> void:
confirmed_hits.clear()
var bus := root.get_node_or_null("EventBus")
_expect(bus != null and bus.has_signal("hit_confirmed"), "EventBus should expose hit_confirmed for stump acceptance")
if bus != null and bus.has_signal("hit_confirmed") and not bus.is_connected("hit_confirmed", _on_hit_confirmed):
bus.connect("hit_confirmed", _on_hit_confirmed)
var combat: Node = load("res://autoload/combat_manager.gd").new()
combat.name = "CombatManager"
root.add_child(combat)
var target := _make_target("Phase5CombatStump", false)
var health: Node = target.get_node("HealthComponent")
var receiver: Area2D = target.get_node("DamageReceiver")
var action := _make_action(0.0, 0.0, 1.0)
var expected_damage := {
"perfect": 100,
"good": 85,
"bad": 70,
}
for label: String in expected_damage.keys():
health.call("set_values", 999, 999)
var emitter: Area2D = load("res://scenes/components/damage_emitter.gd").new()
root.add_child(emitter)
emitter.set("damage", 100)
emitter.set("action_context", action)
emitter.set("judgement_context", {"label": label})
await process_frame
var result: Dictionary = combat.call("resolve_hit", emitter, receiver)
var expected := int(expected_damage[label])
_expect_int(int(result.get("damage", -1)), expected, "Stump hit_confirmed damage should scale for %s" % label)
_expect_int(int(health.get("current")), 999 - expected, "Stump health should reflect %s damage" % label)
emitter.free()
_expect_int(confirmed_hits.size(), 3, "Combat stump should emit hit_confirmed once for each judgement")
if confirmed_hits.size() == 3:
_expect_int(int(confirmed_hits[0].get("damage", -1)), 100, "Perfect stump hit_confirmed damage")
_expect_int(int(confirmed_hits[1].get("damage", -1)), 85, "Good stump hit_confirmed damage")
_expect_int(int(confirmed_hits[2].get("damage", -1)), 70, "Bad stump hit_confirmed damage")
target.free()
combat.free()
func _check_knockback_lifts_target() -> void:
var combat: Node = load("res://autoload/combat_manager.gd").new()
combat.name = "CombatManager"
root.add_child(combat)
var scene: PackedScene = load("res://scenes/characters/player.tscn")
if scene == null:
combat.free()
return
var target := scene.instantiate()
target.name = "KnockupTarget"
root.add_child(target)
var emitter: Area2D = load("res://scenes/components/damage_emitter.gd").new()
root.add_child(emitter)
var action := _make_action(0.0, 0.0, 0.0, 0.0, 1.0)
emitter.set("damage", 0)
emitter.set("base_knockback", Vector2(0, 120))
emitter.set("action_context", action)
emitter.set("judgement_context", {"label": "perfect"})
await process_frame
combat.call("resolve_hit", emitter, target.get_node("DamageReceiver"))
_expect_equal(target.get_node("StateMachine").call("get_ground_state"), &"Airborne", "Vertical knockback should set target GroundState Airborne")
_expect(float(target.get("height_speed")) > 0.0, "Vertical knockback should set positive height speed")
emitter.free()
target.free()
combat.free()
func _check_combat_manager_drives_target_hit_flow() -> void:
received_hits.clear()
hit_flow_events.clear()
target_cancel_reasons.clear()
var combat: Node = load("res://autoload/combat_manager.gd").new()
combat.name = "CombatManager"
root.add_child(combat)
var target := Node.new()
target.name = "Target"
root.add_child(target)
var state_machine: Node = load("res://scenes/components/state_machine.gd").new()
state_machine.name = "StateMachine"
target.add_child(state_machine)
var action_controller: Node = load("res://scenes/components/action_controller.gd").new()
action_controller.name = "ActionController"
target.add_child(action_controller)
action_controller.connect("action_cancelled", _on_target_action_cancelled)
var health: Node = load("res://scenes/components/health_component.gd").new()
health.name = "HealthComponent"
target.add_child(health)
health.connect("health_changed", _on_target_health_changed)
var receiver: Area2D = load("res://scenes/components/damage_receiver.gd").new()
receiver.name = "DamageReceiver"
target.add_child(receiver)
receiver.connect("damage_received", _on_target_damage_received)
var emitter: Area2D = load("res://scenes/components/damage_emitter.gd").new()
root.add_child(emitter)
emitter.set("damage", 3)
await process_frame
health.call("set_values", 10, 10)
hit_flow_events.clear()
combat.call("resolve_hit", emitter, receiver)
_expect_int(int(health.get("current")), 7, "CombatManager should call target HealthComponent.receive_hit exactly once")
_expect_equal(state_machine.call("build_context").get("life_state"), &"Hitstun", "CombatManager hit flow should let HealthComponent write LifeState")
_expect(hit_flow_events.has(&"cancel"), "CombatManager should interrupt the target action before applying hit facts")
_expect(hit_flow_events.has(&"health"), "CombatManager should apply health facts through HealthComponent")
_expect(hit_flow_events.has(&"damage_signal"), "CombatManager should still notify DamageReceiver listeners once")
if hit_flow_events.has(&"cancel") and hit_flow_events.has(&"health"):
_expect(hit_flow_events.find(&"cancel") < hit_flow_events.find(&"health"), "CombatManager should cancel action before HealthComponent writes hit facts")
if hit_flow_events.has(&"cancel") and hit_flow_events.has(&"damage_signal"):
_expect(hit_flow_events.find(&"cancel") < hit_flow_events.find(&"damage_signal"), "CombatManager should cancel action before DamageReceiver emits damage_received")
emitter.free()
target.free()
combat.free()
func _check_lethal_hit_cancels_with_death_reason() -> void:
received_hits.clear()
hit_flow_events.clear()
target_cancel_reasons.clear()
var combat: Node = load("res://autoload/combat_manager.gd").new()
combat.name = "CombatManager"
root.add_child(combat)
var target := _make_target("LethalCancelTarget", true)
var health: Node = target.get_node("HealthComponent")
var receiver: Area2D = target.get_node("DamageReceiver")
health.call("set_values", 3, 3)
var emitter: Area2D = load("res://scenes/components/damage_emitter.gd").new()
root.add_child(emitter)
emitter.set("damage", 5)
await process_frame
combat.call("resolve_hit", emitter, receiver)
_expect(target_cancel_reasons.has(&"death"), "Lethal CombatManager hit should cancel current action with death reason")
_expect_equal(target.get_node("StateMachine").call("build_context").get("life_state"), &"Dead", "Lethal CombatManager hit should still let HealthComponent write Dead")
emitter.free()
target.free()
combat.free()
func _check_effect_defense_modifiers_affect_combat_hit_flow() -> void:
received_hits.clear()
hit_flow_events.clear()
target_cancel_reasons.clear()
var combat: Node = load("res://autoload/combat_manager.gd").new()
combat.name = "CombatManager"
root.add_child(combat)
var target := Node.new()
target.name = "InvincibleTarget"
root.add_child(target)
var state_machine: Node = load("res://scenes/components/state_machine.gd").new()
state_machine.name = "StateMachine"
target.add_child(state_machine)
var action_controller: Node = load("res://scenes/components/action_controller.gd").new()
action_controller.name = "ActionController"
target.add_child(action_controller)
action_controller.connect("action_cancelled", _on_target_action_cancelled)
var health: Node = load("res://scenes/components/health_component.gd").new()
health.name = "HealthComponent"
target.add_child(health)
var effect_container: Node = load("res://scenes/components/effect_container.gd").new()
effect_container.name = "EffectContainer"
target.add_child(effect_container)
var receiver: Area2D = load("res://scenes/components/damage_receiver.gd").new()
receiver.name = "DamageReceiver"
target.add_child(receiver)
receiver.connect("damage_received", _on_target_damage_received)
var emitter: Area2D = load("res://scenes/components/damage_emitter.gd").new()
root.add_child(emitter)
emitter.set("damage", 3)
await process_frame
health.call("set_values", 10, 10)
effect_container.call("add_effect", _make_invincible_effect())
var result: Dictionary = combat.call("resolve_hit", emitter, receiver)
_expect_int(int(result.get("damage", -1)), 0, "Defense Effect should reduce combat damage to zero")
_expect_int(int(health.get("current")), 10, "Invincible Defense Effect should prevent HealthComponent damage")
_expect(not hit_flow_events.has(&"cancel"), "Invincible Defense Effect should prevent interrupt cancellation")
_expect(hit_flow_events.has(&"damage_signal"), "CombatManager should still notify hit facts for an invincible hit")
emitter.free()
target.free()
combat.free()
func _check_dead_targets_are_skipped() -> void:
received_hits.clear()
hit_flow_events.clear()
var combat: Node = load("res://autoload/combat_manager.gd").new()
combat.name = "CombatManager"
root.add_child(combat)
var target := _make_target("DeadTarget", true)
var state_machine: Node = target.get_node("StateMachine")
var health: Node = target.get_node("HealthComponent")
var receiver: Area2D = target.get_node("DamageReceiver")
receiver.connect("damage_received", _on_target_damage_received)
state_machine.call("set_life_state", &"Dead")
health.call("set_values", 0, 10)
var emitter: Area2D = load("res://scenes/components/damage_emitter.gd").new()
root.add_child(emitter)
emitter.set("damage", 3)
await process_frame
var result: Dictionary = combat.call("resolve_hit", emitter, receiver)
_expect(result.is_empty(), "CombatManager should return an empty result when receiver LifeState is Dead")
_expect_int(int(health.get("current")), 0, "Dead target health should not be touched")
_expect(not hit_flow_events.has(&"cancel"), "Dead target should not be interrupted")
_expect(not hit_flow_events.has(&"damage_signal"), "Dead target should not emit damage_received")
emitter.free()
target.free()
combat.free()
func _check_attack_tags_filter_defense_modifiers() -> void:
received_hits.clear()
hit_flow_events.clear()
var combat: Node = load("res://autoload/combat_manager.gd").new()
combat.name = "CombatManager"
root.add_child(combat)
var projectile_target := _make_target("ProjectileShieldTarget", true)
var projectile_health: Node = projectile_target.get_node("HealthComponent")
var projectile_effects: Node = projectile_target.get_node("EffectContainer")
projectile_health.call("set_values", 10, 10)
projectile_effects.call("add_effect", _make_tagged_invincible_effect(&"projectile"))
var projectile_emitter: Area2D = _make_tagged_emitter(&"projectile")
root.add_child(projectile_emitter)
await process_frame
var projectile_result: Dictionary = combat.call("resolve_hit", projectile_emitter, projectile_target.get_node("DamageReceiver"))
_expect_int(int(projectile_result.get("damage", -1)), 0, "Tagged DefenseModifier should apply to matching attack tags")
_expect_int(int(projectile_health.get("current")), 10, "Matching tagged defense should prevent damage")
var melee_target := _make_target("MeleeShieldTarget", true)
var melee_health: Node = melee_target.get_node("HealthComponent")
var melee_effects: Node = melee_target.get_node("EffectContainer")
melee_health.call("set_values", 10, 10)
melee_effects.call("add_effect", _make_tagged_invincible_effect(&"projectile"))
var melee_emitter: Area2D = _make_tagged_emitter(&"melee")
root.add_child(melee_emitter)
await process_frame
var melee_result: Dictionary = combat.call("resolve_hit", melee_emitter, melee_target.get_node("DamageReceiver"))
_expect_int(int(melee_result.get("damage", -1)), 3, "Tagged DefenseModifier should not apply to non-matching attack tags")
_expect_int(int(melee_health.get("current")), 7, "Non-matching tagged defense should allow damage")
projectile_emitter.free()
melee_emitter.free()
projectile_target.free()
melee_target.free()
combat.free()
func _check_combat_hit_dispatches_effect_events() -> void:
var combat: Node = load("res://autoload/combat_manager.gd").new()
combat.name = "CombatManager"
root.add_child(combat)
var attacker := _make_actor("HitEventAttacker", true)
var target := _make_target("HurtEventTarget", true)
var attacker_effects: Node = attacker.get_node("EffectContainer")
var target_effects: Node = target.get_node("EffectContainer")
attacker_effects.call("add_effect", _make_event_listener_effect(&"on_hit", &"test_on_hit_reward"))
target_effects.call("add_effect", _make_event_listener_effect(&"on_hurt", &"test_on_hurt_reward"))
var emitter: Area2D = load("res://scenes/components/damage_emitter.gd").new()
emitter.name = "DamageEmitter"
attacker.add_child(emitter)
emitter.set("damage", 3)
await process_frame
combat.call("resolve_hit", emitter, target.get_node("DamageReceiver"))
_expect_int(int(attacker_effects.call("active_count")), 2, "CombatManager should dispatch on_hit to attacker EffectContainer")
_expect_int(int(target_effects.call("active_count")), 2, "CombatManager should dispatch on_hurt to receiver EffectContainer")
attacker.free()
target.free()
combat.free()
func _check_combat_hit_event_filters_use_action_context() -> void:
var combat: Node = load("res://autoload/combat_manager.gd").new()
combat.name = "CombatManager"
root.add_child(combat)
var attacker := _make_actor("FilteredHitEventAttacker", true)
var projectile_target := _make_target("FilteredProjectileTarget", true)
var melee_target := _make_target("FilteredMeleeTarget", true)
var attacker_effects: Node = attacker.get_node("EffectContainer")
attacker_effects.call("add_effect", _make_tag_filtered_event_listener(&"on_hit", &"test_filtered_melee_hit_reward", &"melee"))
var projectile_emitter: Area2D = _make_tagged_emitter(&"projectile")
projectile_emitter.name = "ProjectileEmitter"
attacker.add_child(projectile_emitter)
await process_frame
combat.call("resolve_hit", projectile_emitter, projectile_target.get_node("DamageReceiver"))
_expect_int(int(attacker_effects.call("active_count")), 1, "Filtered on_hit should ignore non-matching action tags from CombatManager")
var melee_emitter: Area2D = _make_tagged_emitter(&"melee")
melee_emitter.name = "MeleeEmitter"
attacker.add_child(melee_emitter)
await process_frame
combat.call("resolve_hit", melee_emitter, melee_target.get_node("DamageReceiver"))
_expect_int(int(attacker_effects.call("active_count")), 2, "Filtered on_hit should receive matching action tags from CombatManager")
attacker.free()
projectile_target.free()
melee_target.free()
combat.free()
func _check_combat_hit_dispatches_kill_and_parry_events() -> void:
var combat: Node = load("res://autoload/combat_manager.gd").new()
combat.name = "CombatManager"
root.add_child(combat)
var attacker := _make_actor("KillEventAttacker", true)
var lethal_target := _make_target("KillEventTarget", true)
var attacker_effects: Node = attacker.get_node("EffectContainer")
var target_health: Node = lethal_target.get_node("HealthComponent")
attacker_effects.call("add_effect", _make_event_listener_effect(&"on_kill", &"test_on_kill_reward"))
target_health.call("set_values", 3, 3)
var lethal_emitter: Area2D = load("res://scenes/components/damage_emitter.gd").new()
lethal_emitter.name = "DamageEmitter"
attacker.add_child(lethal_emitter)
lethal_emitter.set("damage", 5)
await process_frame
combat.call("resolve_hit", lethal_emitter, lethal_target.get_node("DamageReceiver"))
_expect_int(int(attacker_effects.call("active_count")), 2, "CombatManager should dispatch on_kill to attacker EffectContainer after lethal damage")
var parry_attacker := _make_actor("ParryEventAttacker", true)
var parry_target := _make_target("ParryEventTarget", true)
var parry_target_effects: Node = parry_target.get_node("EffectContainer")
var parry_state_machine: Node = parry_target.get_node("StateMachine")
parry_target_effects.call("add_effect", _make_event_listener_effect(&"on_parry_success", &"test_on_parry_reward"))
parry_state_machine.call("set_defense_state", &"Parrying")
var parry_emitter: Area2D = load("res://scenes/components/damage_emitter.gd").new()
parry_emitter.name = "DamageEmitter"
parry_attacker.add_child(parry_emitter)
parry_emitter.set("damage", 5)
await process_frame
combat.call("resolve_hit", parry_emitter, parry_target.get_node("DamageReceiver"))
_expect_int(int(parry_target_effects.call("active_count")), 2, "CombatManager should dispatch on_parry_success to parrying receiver EffectContainer")
attacker.free()
lethal_target.free()
parry_attacker.free()
parry_target.free()
combat.free()
func _check_action_on_hit_effects_apply_to_receiver() -> void:
var combat: Node = load("res://autoload/combat_manager.gd").new()
combat.name = "CombatManager"
root.add_child(combat)
var attacker := _make_actor("OnHitEffectAttacker", true)
var target := _make_target("OnHitEffectTarget", true)
var target_effects: Node = target.get_node("EffectContainer")
var action: Resource = load("res://resources/action_data.gd").new()
action.set("id", &"test_rooting_action")
var effect_definition: Resource = load("res://resources/effects/effect_definition.gd").new()
effect_definition.set("id", &"test_on_hit_effect")
effect_definition.set("duration_type", &"infinite")
var effects: Array[Resource] = [effect_definition]
action.set("on_hit_effects", effects)
var emitter: Area2D = load("res://scenes/components/damage_emitter.gd").new()
emitter.name = "DamageEmitter"
attacker.add_child(emitter)
emitter.set("damage", 1)
emitter.set("action_context", action)
await process_frame
combat.call("resolve_hit", emitter, target.get_node("DamageReceiver"))
_expect(target_effects.call("active_effect_ids").has(&"test_on_hit_effect"), "CombatManager should apply ActionData.on_hit_effects to the receiver")
attacker.free()
target.free()
combat.free()
func _on_damage_received(amount: int, hit_type: StringName, from: Vector2) -> void:
received_hits.append({
"amount": amount,
"hit_type": hit_type,
"from": from,
})
func _on_hit_confirmed(result: Dictionary) -> void:
confirmed_hits.append(result)
func _on_target_action_cancelled(_action: Resource, reason: StringName) -> void:
target_cancel_reasons.append(reason)
if reason == &"interrupt" or reason == &"death":
hit_flow_events.append(&"cancel")
func _on_target_health_changed(_current: int, _maximum: int) -> void:
hit_flow_events.append(&"health")
func _on_target_damage_received(_amount: int, _hit_type: StringName, _from: Vector2) -> void:
hit_flow_events.append(&"damage_signal")
func _read_text(path: String) -> String:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("Could not read %s" % path)
return ""
return file.get_as_text()
func _make_invincible_effect() -> Resource:
var definition: Resource = load("res://resources/effects/effect_definition.gd").new()
definition.set("id", &"test_invincible")
definition.set("duration_type", &"infinite")
var modifier: Resource = load("res://resources/effects/defense_modifier.gd").new()
modifier.set("defense_state", &"Invincible")
modifier.set("damage_mult", 0.0)
modifier.set("interrupts", false)
var modifiers: Array[Resource] = [modifier]
definition.set("defense_modifiers", modifiers)
return definition
func _make_tagged_invincible_effect(required_attack_tag: StringName) -> Resource:
var definition: Resource = load("res://resources/effects/effect_definition.gd").new()
definition.set("id", &"test_tagged_invincible")
definition.set("duration_type", &"infinite")
var modifier: Resource = load("res://resources/effects/defense_modifier.gd").new()
modifier.set("defense_state", &"Invincible")
modifier.set("damage_mult", 0.0)
modifier.set("interrupts", false)
var required_tags: Array[StringName] = [required_attack_tag]
modifier.set("required_attack_tags", required_tags)
var modifiers: Array[Resource] = [modifier]
definition.set("defense_modifiers", modifiers)
return definition
func _make_tagged_emitter(action_tag: StringName) -> Area2D:
var emitter: Area2D = load("res://scenes/components/damage_emitter.gd").new()
emitter.set("damage", 3)
var action: Resource = load("res://resources/action_data.gd").new()
var action_tags: Array[StringName] = [action_tag]
action.set("action_tags", action_tags)
emitter.set("action_context", action)
return emitter
func _make_action(base_cost: float, base_reward: float, damage_mult: float, knockback_mult_x := 0.0, knockback_mult_y := 0.0) -> Resource:
var action: Resource = load("res://resources/action_data.gd").new()
action.set("base_cost", base_cost)
action.set("base_reward", base_reward)
action.set("damage_mult", damage_mult)
action.set("knockback_mult_x", knockback_mult_x)
action.set("knockback_mult_y", knockback_mult_y)
return action
func _make_event_listener_effect(event_name: StringName, reward_id: StringName) -> Resource:
var definition_script: Script = load("res://resources/effects/effect_definition.gd")
var reward: Resource = definition_script.new()
reward.set("id", reward_id)
reward.set("duration_type", &"infinite")
var listener: Resource = definition_script.new()
listener.set("id", StringName("listener_%s" % event_name))
listener.set("duration_type", &"infinite")
listener.set("trigger_event", event_name)
var triggered_effects: Array[Resource] = [reward]
listener.set("trigger_effects", triggered_effects)
return listener
func _make_tag_filtered_event_listener(event_name: StringName, reward_id: StringName, required_action_tag: StringName) -> Resource:
var listener: Resource = _make_event_listener_effect(event_name, reward_id)
var required_tags: Array[StringName] = [required_action_tag]
listener.set("required_event_action_tags", required_tags)
return listener
func _make_actor(actor_name: String, include_effect_container: bool) -> Node:
var actor := Node.new()
actor.name = actor_name
root.add_child(actor)
if include_effect_container:
var effect_container: Node = load("res://scenes/components/effect_container.gd").new()
effect_container.name = "EffectContainer"
actor.add_child(effect_container)
return actor
func _make_target(target_name: String, include_effect_container: bool) -> Node:
var target := Node.new()
target.name = target_name
root.add_child(target)
var state_machine: Node = load("res://scenes/components/state_machine.gd").new()
state_machine.name = "StateMachine"
target.add_child(state_machine)
var action_controller: Node = load("res://scenes/components/action_controller.gd").new()
action_controller.name = "ActionController"
target.add_child(action_controller)
action_controller.connect("action_cancelled", _on_target_action_cancelled)
var health: Node = load("res://scenes/components/health_component.gd").new()
health.name = "HealthComponent"
target.add_child(health)
if include_effect_container:
var effect_container: Node = load("res://scenes/components/effect_container.gd").new()
effect_container.name = "EffectContainer"
target.add_child(effect_container)
var receiver: Area2D = load("res://scenes/components/damage_receiver.gd").new()
receiver.name = "DamageReceiver"
target.add_child(receiver)
return target
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 _expect_int(actual: int, expected: int, label: String) -> void:
if actual != expected:
failures.append("%s: expected %d, got %d" % [label, expected, actual])
func _expect_float(actual: float, expected: float, label: String) -> void:
if not is_equal_approx(actual, expected):
failures.append("%s: expected %.3f, got %.3f" % [label, expected, actual])
func _expect_vector(actual: Variant, expected: Vector2, label: String) -> void:
if not (actual is Vector2) or not (actual as Vector2).is_equal_approx(expected):
failures.append("%s: expected %s, got %s" % [label, expected, actual])
func _finish() -> void:
if failures.is_empty():
print("PASS combat manager resolvers")
quit(0)
else:
for failure: String in failures:
push_error(failure)
quit(1)