extends Node ## Exercise real ability producers, battle clocks and imported character poses. const BattleScript = preload("res://scripts/combat/battle_controller.gd") const STEP: float = 1.0 / 60.0 var checks: int = 0 var failures: Array[String] = [] var protection_restarted: bool = false func _ready() -> void: WaveManager.set_physics_process(false) call_deferred("run") func check(ok: bool, label: String) -> void: checks += 1 if not ok: failures.append(label) printerr("ABILITY_EVENT_FAIL: ", label) func run() -> void: for speed: int in [1, 2]: verify_battle(speed) check(checks == 32, "Both battle-clock scenarios complete every assertion") var file := FileAccess.open("res://docs/ability-animation-verification.json", FileAccess.WRITE) file.store_string(JSON.stringify({"checks": checks, "failures": failures}, "\t")) file.close() print("ABILITY_EVENT_PASS: ", checks, " checks; ", failures.size(), " failures") await Soundscape.quit_game(0 if failures.is_empty() else 1) func verify_battle(speed: int) -> void: protection_restarted = false GameState.reset_match() GameState.speed = speed var battle: BattleController = BattleScript.new() add_child(battle) battle.set_process(false) WaveManager.attach_battle(battle) var roster: Array = [ {"type_id": "g04", "uid": 1, "cell": Vector2i(2, 3)}, {"type_id": "w10", "uid": 2, "cell": Vector2i(3, 3)}, {"type_id": "w04", "uid": 3, "cell": Vector2i(2, 4)}] battle.show_formation([roster, []]) GameState.set_phase(GameState.Phase.BUILD) battle._process(.1) var bastion: CombatUnit = battle.units[0] check(bastion.ability_pose_flash == 0 and bastion._model.current_clip == "idle", "%dx: formation does not activate the opening protection pose" % speed) var enemy: Dictionary = DataCatalog.get_attackers(1)[0] enemy.speed = 0.0 enemy.damage = 0.0 battle.begin_battle([roster, []], [[enemy], []], 1) GameState.set_phase(GameState.Phase.COMBAT) bastion = battle.units[0] var healer: CombatUnit = battle.units[1] var ringborke: CombatUnit = battle.units[2] for index: int in range(battle.units.size()): var unit: CombatUnit = battle.units[index] unit.data.speed = 0.0 unit.position = Vector3(-9, 0, float(index) if not unit.is_attacker else -30.0) unit.refresh_visuals() check(bastion.ability_pose_flash == 0, "%dx: battle setup waits for a combat step" % speed) WaveManager._physics_process(STEP) check(bastion.ability_pose_flash > 0 and bastion._model.current_clip == "ability", "%dx: first combat step starts Basaltbastion's imported protection clip" % speed) check(is_equal_approx(bastion.damage_reduction(), .25), "%dx: opening reduction remains 25%%" % speed) var pose_at: float = bastion._model.player.current_animation_position var elapsed: float = battle.elapsed var flash: float = bastion.ability_pose_flash GameState.paused = true for frame: int in range(30): WaveManager._physics_process(STEP) check(is_equal_approx(battle.elapsed, elapsed) and is_equal_approx(bastion.ability_pose_flash, flash) and is_equal_approx(bastion._model.player.current_animation_position, pose_at), "%dx: pause freezes combat, activation pulse and skeletal playback" % speed) GameState.paused = false for frame: int in range(30): WaveManager._physics_process(STEP) check(is_equal_approx(battle.elapsed - elapsed, .5 * speed) and absf(bastion._model.player.current_animation_position - pose_at - .5 * speed) < .0001, "%dx: skeletal protection follows the fixed battle clock" % speed) advance_until(battle, 3.0) check(bastion.ability_pose_flash == 0 and bastion._model.current_clip == "idle", "%dx: protection completes without repeating" % speed) bastion.hp = bastion.max_hp - 150.0 ringborke.hp = ringborke.max_hp - 150.0 advance_until(battle, 4.0) check(is_equal_approx(bastion.hp, bastion.max_hp - 70.0) and is_equal_approx(ringborke.hp, ringborke.max_hp - 70.0), "%dx: Quellgeflecht still heals each wounded recipient by 80 HP" % speed) check(healer.ability_pose_flash > 0 and healer._model.current_clip == "ability", "%dx: the caster starts its own multi-heal animation" % speed) for recipient: CombatUnit in [bastion, ringborke]: check(recipient.ability_flash > 0 and recipient._pulse.visible, "%dx: %s retains the received healing effect" % [speed, recipient.data.id]) check(recipient.ability_pose_flash == 0 and recipient._model.current_clip == "idle" and recipient._model._ability_time < 0, "%dx: %s does not cast its own ability when healed" % [speed, recipient.data.id]) advance_until(battle, 5.0) check(ringborke.ability_pose_flash > 0 and ringborke._model.current_clip == "ability" and is_equal_approx(ringborke.hp, ringborke.max_hp), "%dx: a later self-heal still activates after receiving someone else's healing" % speed) advance_until(battle, 10.1) check(is_equal_approx(bastion.damage_reduction(), .10), "%dx: later reduction remains 10%%" % speed) check(bastion.ability_pose_flash == 0 and bastion._model.current_clip == "idle" and bastion._model._ability_time < 0 and not protection_restarted, "%dx: passive expiry and repeated external healing do not restart protection" % speed) battle.free() WaveManager.battle = null func advance_until(battle: BattleController, seconds: float) -> void: while battle.elapsed < seconds - .000001 and not battle.finished: WaveManager._physics_process(STEP) if battle.elapsed > 3.0: var bastion: CombatUnit = battle.units[0] protection_restarted = protection_restarted or bastion.ability_pose_flash > 0 \ or bastion._model.current_clip == "ability" or bastion._model._ability_time >= 0