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)