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>
115 lines
5.5 KiB
GDScript
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
|