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)