269 lines
11 KiB
GDScript
269 lines
11 KiB
GDScript
extends SceneTree
|
|
|
|
const Controller = preload("res://campaign/fg4_runtime_controller.gd")
|
|
const Fixture = preload("res://tests/fg4/contract1_fixture.gd")
|
|
|
|
const VIEWPORT := Vector2i(844, 390)
|
|
const MIN_CONTENT_FRAMES := 600
|
|
const TARGET_CONTENT_FRAMES := 690
|
|
const MAX_CONTENT_FRAMES := 900
|
|
const EMULATION_SETTING := "input_devices/pointing/emulate_mouse_from_touch"
|
|
const EXPECTED_ENEMIES := [
|
|
"EN-R3-01", "EN-R3-02", "EN-R3-03", "EN-R3-04", "EN-R3-05", "EN-R3-06",
|
|
"EN-R3-07", "EN-R3-08", "EN-R3-09", "EN-R3-10", "EN-R3-11", "EN-R3-12",
|
|
]
|
|
const REQUIRED_AUDIO := [
|
|
"FG4-SFX-COMMIT", "FG4-SFX-IMPACT", "FG4-SFX-VICTORY",
|
|
"FG4-SFX-VALVE", "FG4-SFX-HEAT", "FG4-SFX-SPARK", "FG4-SFX-PROMOTION",
|
|
]
|
|
|
|
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-R3-01": {}, "BO-R3-02": {}}
|
|
var enemy_ids_seen: Dictionary = {}
|
|
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 saw_pressure := false
|
|
var saw_eight_sparks := false
|
|
var saw_promotion := 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().market_1_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")
|
|
|
|
for round_index in range(12):
|
|
await play_ordinary_round(round_index)
|
|
await play_boss("BO-R3-01", "vent_and_brace")
|
|
expect(experience.view.mode == "contract_record", "Furnace opens first Record")
|
|
await tap("FG4RecordButton")
|
|
expect(experience.view.mode == "contract_handoff", "first Record opens handoff")
|
|
await tap("FG4RecordButton")
|
|
expect(experience.view.mode == "market" and experience.view.contract_id == "CTR-R3-02", "handoff opens Contract two")
|
|
|
|
for round_index in range(12):
|
|
await play_ordinary_round(round_index)
|
|
await play_boss("BO-R3-02", "vary_tags")
|
|
expect(experience.view.mode == "region_record", "Foreman opens second Record")
|
|
await tap("FG4RecordButton")
|
|
expect(experience.view.mode == "region_closed", "second Record reveals RG-04")
|
|
|
|
while recording_frames < TARGET_CONTENT_FRAMES:
|
|
await next_frame()
|
|
verify_complete_flow()
|
|
var raw_button_count := int(experience.raw_button_activation_count)
|
|
ProjectSettings.set_setting(EMULATION_SETTING, original_emulation)
|
|
experience.audio_router.cleanup()
|
|
await create_timer(0.85).timeout
|
|
experience.queue_free()
|
|
experience = null
|
|
await hold(30)
|
|
expect(recording_frames <= MAX_CONTENT_FRAMES, "cleanup remains inside 30 seconds")
|
|
if failures.is_empty():
|
|
print("ABG_FG4_CAMPAIGN_RECORDING_OK frames=%d seconds=%.6f rounds=24 bosses=2 records=2 terminal=RG-04 touch=%d drag=%d raw_exact_once=%d fallback=0 direct=0 enemies=12 boss_frames=%d,%d static_motion=true damage=true hit_pause=true shake=true pressure=true sparks=8 promotion=true audio=music+ambience+7" % [recording_frames, float(recording_frames) / 30.0, parsed_touches, parsed_drags, raw_button_count, Dictionary(boss_frames["BO-R3-01"]).size(), Dictionary(boss_frames["BO-R3-02"]).size()])
|
|
quit(0)
|
|
else:
|
|
for failure in failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
|
|
func play_ordinary_round(round_index: int) -> void:
|
|
expect(experience.view.mode == "market", "ordinary round starts from Market")
|
|
var routes: Array = experience.view.routes
|
|
var route: Dictionary = routes[round_index % routes.size()]
|
|
await tap("FG4Option_%s" % String(route.offer_id))
|
|
await tap("FG4ContinueButton")
|
|
expect(experience.view.mode == "plan", "route confirmation opens response plans")
|
|
if experience.view.contract_id == "CTR-R3-02" and round_index == 0:
|
|
await tap("FG4Option_safe_promotion")
|
|
await tap("FG4ContinueButton")
|
|
expect(experience.screen == Controller.SCREEN_BATTLE, "response starts automatic battle")
|
|
await advance_to_terminal(false)
|
|
expect(String(experience.view.battle.outcome) == "victory", "selected response wins")
|
|
await tap("FG2SettleButton")
|
|
ordinary_battles += 1
|
|
|
|
|
|
func play_boss(boss_id: String, plan_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("FG4Option_%s" % plan_id)
|
|
await tap("FG4ContinueButton")
|
|
expect(experience.screen == Controller.SCREEN_BATTLE, "%s starts from parsed touch" % boss_id)
|
|
await advance_to_terminal(true)
|
|
expect(String(experience.view.battle.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 < 220:
|
|
experience.advance_battle_ticks(1 if boss else 12)
|
|
await hold(5 if boss else 1)
|
|
guard += 1
|
|
expect(guard < 220 and experience.battle_result_visible, "battle reaches bounded visible terminal")
|
|
await hold(3)
|
|
|
|
|
|
func verify_complete_flow() -> void:
|
|
expect(recording_frames >= MIN_CONTENT_FRAMES and recording_frames <= MAX_CONTENT_FRAMES, "recording stays inside 20-30 seconds")
|
|
expect(ordinary_battles == 24 and boss_battles == 2, "recording completes 24 ordinary battles and both Bosses")
|
|
expect(experience.screen == Controller.SCREEN_MARKET and experience.view.mode == "region_closed", "recording ends on visible Region close")
|
|
expect(int(experience.view.region_records_count) == 2 and experience.view.unlocked_region_ids.count("RG-04") == 1, "two Records and RG-04 remain visible once")
|
|
for boss_id in boss_frames:
|
|
expect(Dictionary(boss_frames[boss_id]).size() >= 3, "%s shows three source-bound action frames" % boss_id)
|
|
for enemy_id in EXPECTED_ENEMIES:
|
|
expect(enemy_ids_seen.has(enemy_id), "%s is runtime-seen" % enemy_id)
|
|
for kind in ["pressure_valve_disclosed", "construct_state_changed", "delayed_heat_forecast", "repeat_counter_disclosed", "next_spark_disclosed", "spark_promoted", "damage", "outcome"]:
|
|
expect(seen_event_kinds.has(kind), "recording presents %s" % kind)
|
|
expect(saw_static_motion and saw_damage_value and saw_hit_pause and saw_shake, "recording exposes actor motion and battle feedback")
|
|
expect(saw_pressure and saw_eight_sparks and saw_promotion, "recording exposes valve, eight sparks and promotion")
|
|
for asset_id in REQUIRED_AUDIO:
|
|
expect(heard_audio.has(asset_id), "%s plays during 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 input is parsed touch/drag")
|
|
expect(experience.raw_button_activation_count == parsed_touches - 1, "every button touch activates exactly once")
|
|
expect(experience.fallback_input_count == 0 and experience.direct_command_count == 0, "recording has no fallback or direct command")
|
|
|
|
|
|
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-R3-")) 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 actors = experience.find_child("ActorsLayer", true, false)
|
|
if actors != null:
|
|
for identity_id in Array(actors.get_meta("authored_actor_ids", [])):
|
|
if String(identity_id).begins_with("EN-R3-"):
|
|
enemy_ids_seen[String(identity_id)] = true
|
|
var vfx = experience.find_child("VfxLayer", true, false)
|
|
if vfx != null:
|
|
saw_pressure = saw_pressure or bool(vfx.get_meta("fg4_pressure_visible", false))
|
|
saw_eight_sparks = saw_eight_sparks or int(vfx.get_meta("fg4_spark_slot_count", 0)) == 8
|
|
saw_promotion = saw_promotion or int(vfx.get_meta("fg4_promoted_spark_count", 0)) == 1
|
|
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, "company 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-4 campaign recording: %s" % message)
|