Files
aetherbound-guild/runtime/tests/fg3/campaign_runtime_recording.gd
T

248 lines
10 KiB
GDScript

extends SceneTree
const Controller = preload("res://campaign/fg3_runtime_controller.gd")
const Fixture = preload("res://tests/fg3/contract1_fixture.gd")
const VIEWPORT := Vector2i(844, 390)
const MIN_CONTENT_FRAMES := 450
const TARGET_CONTENT_FRAMES := 660
const MAX_CONTENT_FRAMES := 900
const EMULATION_SETTING := "input_devices/pointing/emulate_mouse_from_touch"
const REQUIRED_AUDIO := [
"FG2-SFX-COMMIT",
"FG2-SFX-FORECAST",
"FG2-SFX-IMPACT",
"FG2-SFX-VICTORY",
]
var failures: Array[String] = []
var experience
var parsed_touches := 0
var parsed_drags := 0
var recording_frames := 0
var ordinary_battles := 0
var boss_battles := 0
var current_boss := ""
var boss_frames: Dictionary = {"BO-R2-01": {}, "BO-R2-02": {}}
var seen_event_kinds: Dictionary = {}
var heard_audio: Dictionary = {}
var saw_static_motion := false
var saw_damage_value := false
var saw_hit_pause := false
var saw_shake := false
var disclosure_seen: Dictionary = {"BO-R2-01": false, "BO-R2-02": false}
func _initialize() -> void:
call_deferred("run_recording")
func run_recording() -> void:
var original_emulation := bool(ProjectSettings.get_setting(EMULATION_SETTING, true))
ProjectSettings.set_setting(EMULATION_SETTING, false)
root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND
root.content_scale_size = VIEWPORT
root.size = VIEWPORT
experience = Controller.new()
experience.battle_auto_advance = false
experience.muted = false
experience.locale = "zh_CN"
experience.size = Vector2(VIEWPORT)
experience.configure_entry(Fixture.new().fg3_market_8_snapshot())
root.add_child(experience)
await hold(45)
var company: Array = experience.view.get("company", [])
if company.size() >= 2:
await drag(
"FG2CompanyTarget_%s" % String(company[0].recruit_id),
"FG2CompanyTarget_%s" % String(company[1].recruit_id)
)
expect(experience.last_code == "formation_authority_frozen", "recording drag stays read-only without an order transaction")
for _round in range(5):
await play_ordinary_round()
await play_boss("BO-R2-01")
expect(experience.view.mode == "contract_record", "first Boss opens the first Company Record")
await tap("FG3RecordButton")
expect(experience.view.mode == "contract_handoff", "first Record opens the explicit Contract handoff")
await tap("FG3RecordButton")
expect(experience.view.mode == "market" and experience.view.contract_id == "CTR-R2-02", "handoff opens Contract two")
for _round in range(12):
await play_ordinary_round()
await play_boss("BO-R2-02")
expect(experience.view.mode == "region_record", "second Boss opens the Region Record")
await tap("FG3RecordButton")
expect(experience.view.mode == "region_closed", "second Record reveals the closed Region")
while recording_frames < TARGET_CONTENT_FRAMES:
await next_frame()
observe()
expect(recording_frames >= MIN_CONTENT_FRAMES and recording_frames <= MAX_CONTENT_FRAMES, "recording stays inside the 15-30 second window")
expect(ordinary_battles == 17 and boss_battles == 2, "recording completes all 17 ordinary battles and both Bosses")
expect(experience.screen == Controller.SCREEN_MARKET and experience.view.mode == "region_closed", "recording ends on the visible Region close")
expect(int(experience.view.records_count) == 2 and experience.view.unlocked_region_ids.count("RG-03") == 1, "two Records and RG-03 remain visible exactly once")
for boss_id in boss_frames:
expect(Dictionary(boss_frames[boss_id]).size() >= 3, "%s shows at least three source-bound action frames" % boss_id)
expect(bool(disclosure_seen[boss_id]), "%s shows its event-bound disclosure VFX" % boss_id)
for kind in ["allied_basic", "damage", "outcome", "linked_positions_disclosed", "third_pulse_link", "pulse_family_interrupted"]:
expect(seen_event_kinds.has(kind), "recording presents %s" % kind)
expect(saw_static_motion, "single-frame Company/enemy identities receive event-bound motion")
expect(saw_damage_value and saw_hit_pause and saw_shake, "recording exposes damage values, hit pause and world shake")
for asset_id in REQUIRED_AUDIO:
expect(heard_audio.has(asset_id), "%s plays during the coherent Region flow" % asset_id)
var audio_state: Dictionary = experience.audio_router.snapshot()
expect(bool(audio_state.music_playing) and bool(audio_state.ambience_playing), "music and ambience remain active at Region close")
expect(experience.input_touch_count == parsed_touches and experience.input_drag_count == parsed_drags, "all recording commands use parsed touch/drag")
expect(experience.raw_button_activation_count == parsed_touches - 1, "every button touch activates exactly once and the drag activates none")
expect(experience.fallback_input_count == 0 and experience.direct_command_count == 0, "recording has no fallback or direct player command")
ProjectSettings.set_setting(EMULATION_SETTING, original_emulation)
experience.audio_router.cleanup()
await hold(6)
expect(bool(experience.audio_router.snapshot().cleaned_up), "audio loops release before MovieWriter exit")
expect(recording_frames <= MAX_CONTENT_FRAMES, "cleanup remains inside 30 seconds")
if failures.is_empty():
print("ABG_FG3_CAMPAIGN_RECORDING_OK frames=%d seconds=%.6f rounds=17 bosses=2 records=2 terminal=RG-03 touch=%d drag=%d raw_exact_once=%d fallback=0 direct=0 boss_frames=%d,%d static_motion=true damage=true hit_pause=true shake=true disclosure=2 audio=music+ambience+4" % [recording_frames, float(recording_frames) / 30.0, parsed_touches, parsed_drags, experience.raw_button_activation_count, Dictionary(boss_frames["BO-R2-01"]).size(), Dictionary(boss_frames["BO-R2-02"]).size()])
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func play_ordinary_round() -> void:
expect(experience.view.mode == "market", "ordinary round starts from a Market")
await tap("FG3ContinueButton")
expect(experience.view.mode == "plan", "route confirmation opens response plans")
await tap("FG3ContinueButton")
expect(experience.screen == Controller.SCREEN_BATTLE, "response confirmation starts the battle")
await advance_to_terminal(false)
expect(String(experience.view.get("battle", {}).get("outcome", "")) == "victory", "selected ordinary response wins")
await tap("FG2SettleButton")
ordinary_battles += 1
func play_boss(boss_id: String) -> void:
expect(experience.view.mode == "boss_muster" and experience.view.boss_id == boss_id, "%s Muster is visible" % boss_id)
current_boss = boss_id
await tap("FG3ContinueButton")
expect(experience.screen == Controller.SCREEN_BATTLE, "%s starts from parsed touch" % boss_id)
await advance_to_terminal(true)
expect(String(experience.view.get("battle", {}).get("outcome", "")) == "victory", "%s selected response wins" % boss_id)
await tap("FG2SettleButton")
boss_battles += 1
current_boss = ""
func advance_to_terminal(boss: bool) -> void:
var guard := 0
while not experience.battle_result_visible and guard < 180:
experience.advance_battle_ticks(1 if boss else 12)
if boss:
await hold(4)
else:
await next_frame()
guard += 1
expect(guard < 180 and experience.battle_result_visible, "battle reaches a bounded visible terminal")
await hold(3)
func observe() -> void:
if not is_instance_valid(experience):
return
for kind in experience.presented_event_counts:
if int(experience.presented_event_counts[kind]) > 0:
seen_event_kinds[String(kind)] = true
var presentation: Dictionary = experience.event_presenter.snapshot()
for actor_id in presentation.actor_cues:
var cue: Dictionary = presentation.actor_cues[actor_id]
var identity_id := String(cue.get("identity_id", ""))
var frame_index := int(cue.get("frame_index", 0))
if boss_frames.has(identity_id):
boss_frames[identity_id][frame_index] = true
elif (identity_id.begins_with("PF-") or identity_id.begins_with("EN-R2-")) and String(cue.get("motion", "")) in ["attack", "hit", "fallen", "guard"]:
saw_static_motion = true
for transient in presentation.transients:
saw_damage_value = saw_damage_value or String(transient.get("text", "")).begins_with("-")
saw_hit_pause = saw_hit_pause or float(presentation.hit_pause) > 0.0
saw_shake = saw_shake or Vector2i(presentation.shake) != Vector2i.ZERO
var vfx = experience.find_child("VfxLayer", true, false)
if not current_boss.is_empty() and vfx != null and int(vfx.get_meta("fg3_disclosure_vfx_count", 0)) > 0:
disclosure_seen[current_boss] = true
var audio_state: Dictionary = experience.audio_router.snapshot()
for asset_id in audio_state.asset_play_counts:
if int(audio_state.asset_play_counts[asset_id]) > 0:
heard_audio[String(asset_id)] = true
func tap(node_name: String) -> void:
var control := experience.find_child(node_name, true, false) as Control
expect(control != null, "%s exists for parsed touch" % 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)
parsed_touches += 1
await next_frame()
var up := InputEventScreenTouch.new()
up.index = 0
up.position = point
up.pressed = false
Input.parse_input_event(up)
await hold(2)
func drag(source_name: String, target_name: String) -> void:
var source := experience.find_child(source_name, true, false) as Control
var target := experience.find_child(target_name, true, false) as Control
expect(source != null and target != null, "recording drag targets exist")
if source == null or target == null:
return
var start := root.get_final_transform() * source.get_global_rect().get_center()
var finish := root.get_final_transform() * target.get_global_rect().get_center()
var down := InputEventScreenTouch.new()
down.index = 1
down.position = start
down.pressed = true
Input.parse_input_event(down)
parsed_touches += 1
await next_frame()
for index in range(1, 5):
var motion := InputEventScreenDrag.new()
motion.index = 1
motion.position = start.lerp(finish, float(index) / 4.0)
motion.relative = (finish - start) / 4.0
Input.parse_input_event(motion)
if index == 1:
parsed_drags += 1
await next_frame()
var up := InputEventScreenTouch.new()
up.index = 1
up.position = finish
up.pressed = false
Input.parse_input_event(up)
await hold(2)
func hold(frames: int) -> void:
for _index in frames:
await next_frame()
func next_frame() -> void:
await process_frame
recording_frames += 1
observe()
func expect(condition: bool, message: String) -> void:
if not condition:
failures.append("FG-3 campaign recording: %s" % message)