saumwacht-godot/tools/verify_ability_events.gd
Martin Stickelbroeck c2b447610c Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern
unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore
unversioniert; letztere sind unter ../backups gesichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:00:36 +02:00

115 lines
5.5 KiB
GDScript

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