Files
aetherbound-guild/runtime/tests/fg2/motion_capture_test.gd
T

180 lines
7.5 KiB
GDScript

extends SceneTree
const Controller = preload("res://campaign/fg2_graybox_controller.gd")
const Fixture = preload("res://campaign/fg2_fixture.gd")
const VIEWPORT := Vector2i(844, 390)
var failures: Array[String] = []
var experience
var capture_root := ""
var captured: Dictionary = {}
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
capture_root = OS.get_environment("ABG_FG2_CAPTURE_ROOT")
expect(not capture_root.is_empty(), "capture root is required")
if capture_root.is_empty():
quit(1)
return
DirAccess.make_dir_recursive_absolute(capture_root)
root.size = VIEWPORT
root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND
root.content_scale_size = VIEWPORT
await mount(false, false, false)
await capture_battle_states()
await mount(true, true, true)
await capture_reduced_damage()
await capture_recovery()
if is_instance_valid(experience):
experience.queue_free()
for _index in 45:
await process_frame
var required := ["01-scribe-entry", "02-scribe-attack", "03-aegis-guard", "04-sprout-entry", "05-sprout-attack", "06-fallen", "07-victory", "08-reduced-damage", "09-recovery"]
for label in required:
expect(captured.has(label), "%s capture exists" % label)
if failures.is_empty():
print("ABG_FG2_MOTION_CAPTURE_OK captures=9 states=scribe-entry,scribe-attack,aegis-guard,sprout-entry,sprout-attack,fallen,victory,reduced-damage,recovery viewport=844x390 labels=native ghost=true result_vfx=true recovery_vfx=true")
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func capture_battle_states() -> void:
var guard := 0
while not experience.battle_result_visible and guard < 2200:
var result: Dictionary = experience.advance_battle_ticks(1)
for event in result.get("events", []):
var kind := String(event.get("kind", ""))
var source_id := String(event.get("source_id", ""))
var identity := String(experience.event_presenter.actor_identities.get(source_id, ""))
var presentation: Dictionary = experience.event_presenter.snapshot()
var source_cue: Dictionary = presentation.get("actor_cues", {}).get(source_id, {})
var current_motion := String(source_cue.get("motion", ""))
if kind in ["wave_arrived", "damage", "fallen", "outcome"]:
var cue_identity := String(source_cue.get("identity_id", ""))
var pending_count := Array(presentation.get("pending_actor_cues", {}).get(source_id, [])).size()
print("FG2_MOTION_CAPTURE_EVENT kind=%s source=%s identity=%s cue_identity=%s motion=%s pending=%d" % [kind, source_id, identity, cue_identity, current_motion, pending_count])
if kind == "wave_arrived" and identity == "EN-R1-09" and not captured.has("01-scribe-entry"):
await settle_and_capture("01-scribe-entry")
if kind == "damage" and identity == "EN-R1-09" and not captured.has("02-scribe-attack"):
await settle_and_capture("02-scribe-attack")
if kind == "damage" and identity == "EN-R1-08" and not captured.has("05-sprout-attack") and current_motion == "attack":
await settle_and_capture("05-sprout-attack", 3)
if kind == "damage" and int(event.get("detail", {}).get("guard_before", 0)) > 0 and int(event.get("detail", {}).get("raw", 0)) > int(event.get("detail", {}).get("effective", 0)) and not captured.has("03-aegis-guard"):
await settle_and_capture("03-aegis-guard")
if kind == "wave_arrived" and identity == "EN-R1-08" and not captured.has("04-sprout-entry"):
await settle_and_capture("04-sprout-entry")
if kind == "fallen" and not captured.has("06-fallen"):
await wait_for_motion(String(event.get("target_id", "")), "fallen", 20)
await settle_and_capture("06-fallen")
var actor_layer := experience.find_child("ActorsLayer", true, false) as Control
expect(int(actor_layer.get_meta("fallen_ghost_count", 0)) > 0, "fallen frame renders a ghost")
if kind == "outcome" and not captured.has("07-victory"):
await settle_and_capture("07-victory")
var vfx_layer := experience.find_child("VfxLayer", true, false) as Control
expect(bool(vfx_layer.get_meta("result_vfx_visible", false)), "victory frame renders result VFX")
guard += 1
expect(guard < 2200, "capture battle reaches terminal")
func capture_reduced_damage() -> void:
var guard := 0
while not captured.has("08-reduced-damage") and guard < 1200:
var result: Dictionary = experience.advance_battle_ticks(1)
for event in result.get("events", []):
if String(event.get("kind", "")) == "damage":
await settle_and_capture("08-reduced-damage")
var snapshot: Dictionary = experience.event_presenter.snapshot()
expect(float(snapshot.hit_pause) == 0.0 and Vector2i(snapshot.shake) == Vector2i.ZERO, "reduced frame has no pause or shake")
for transient in snapshot.transients:
expect(not bool(transient.get("flash", false)), "reduced frame has no flash")
break
guard += 1
func capture_recovery() -> void:
await mount_snapshot(Fixture.recovery_market_state(), false, false, false)
await tap("FG2RecoveryButton")
await settle_and_capture("09-recovery", 3)
var snapshot: Dictionary = experience.event_presenter.snapshot()
expect(int(snapshot.event_counts.get("recovery", 0)) == 1, "recovery frame is receipt-bound")
var vfx_layer := experience.find_child("VfxLayer", true, false) as Control
expect(bool(vfx_layer.get_meta("recovery_vfx_visible", false)), "recovery frame renders recovery VFX")
func mount(reduced: bool, flashes: bool, power: bool) -> void:
await mount_snapshot(Fixture.canonical_encounter_state(), reduced, flashes, power)
func mount_snapshot(snapshot_value: Dictionary, reduced: bool, flashes: bool, power: bool) -> void:
if is_instance_valid(experience):
experience.queue_free()
for _index in 12:
await process_frame
experience = Controller.new()
experience.battle_auto_advance = false
experience.reduced_motion = reduced
experience.reduced_flashes = flashes
experience.low_power = power
experience.size = Vector2(VIEWPORT)
experience.configure_entry(snapshot_value)
root.add_child(experience)
for _index in 8:
await process_frame
func tap(node_name: String) -> void:
var control := experience.find_child(node_name, true, false) as Control
expect(control != null, "%s exists" % node_name)
if control == null:
return
var point := root.get_final_transform() * control.get_global_rect().get_center()
var down := InputEventScreenTouch.new()
down.index = 0
down.position = point
down.pressed = true
Input.parse_input_event(down)
await process_frame
var up := InputEventScreenTouch.new()
up.index = 0
up.position = point
up.pressed = false
Input.parse_input_event(up)
for _index in 4:
await process_frame
func settle_and_capture(label: String, settle_frames := 1) -> void:
for _index in settle_frames:
experience.event_presenter.advance(1.0 / 60.0)
await process_frame
await RenderingServer.frame_post_draw
var path := capture_root.path_join(label + ".png")
var error := root.get_texture().get_image().save_png(path)
expect(error == OK, "%s writes" % label)
if error == OK:
captured[label] = path
func wait_for_motion(actor_id: String, motion: String, max_frames: int) -> void:
for _index in max_frames:
var cue: Dictionary = experience.event_presenter.snapshot().get("actor_cues", {}).get(actor_id, {})
if String(cue.get("motion", "")) == motion:
return
experience.event_presenter.advance(1.0 / 60.0)
await process_frame
expect(false, "%s reaches %s motion" % [actor_id, motion])
func expect(condition: bool, message: String) -> void:
if not condition:
failures.append("FG-2 motion capture: %s" % message)