286 lines
13 KiB
GDScript
286 lines
13 KiB
GDScript
extends SceneTree
|
|
|
|
const Contract1Battle = preload("res://fg4/contract1_battle_service.gd")
|
|
const Adapter = preload("res://campaign/fg4_adapter.gd")
|
|
const Controller = preload("res://campaign/fg4_runtime_controller.gd")
|
|
const Fixture = preload("res://tests/fg4/contract1_fixture.gd")
|
|
|
|
const VIEWPORT := Vector2i(844, 390)
|
|
|
|
var failures: Array[String] = []
|
|
var experience
|
|
var parsed_touches := 0
|
|
var parsed_drags := 0
|
|
var observed_touches := 0
|
|
var observed_drags := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("run_test")
|
|
|
|
|
|
func run_test() -> void:
|
|
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
|
|
await verify_market_plan_battle()
|
|
await verify_permanent_loss_recovery()
|
|
await verify_first_boss_record_handoff()
|
|
await verify_contract2_promotion()
|
|
await verify_second_boss_region_close()
|
|
await release_experience(false)
|
|
if failures.is_empty():
|
|
print("ABG_FG4_CAMPAIGN_RUNTIME_TOUCH_OK viewport=844x390 states=market,plan,battle,boss1,record1,handoff,contract2,boss2,record2,rg04 touch=%d drag=%d fallback=0 direct=0 world_layers=5 sparks=8" % [parsed_touches, parsed_drags])
|
|
quit(0)
|
|
else:
|
|
for failure in failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
|
|
func verify_market_plan_battle() -> void:
|
|
await mount(Fixture.new().market_1_snapshot())
|
|
expect(experience.screen == Controller.SCREEN_MARKET and experience.view.mode == "market", "RG-03 Market 1 opens in world-first layout")
|
|
expect(String(experience.get_meta("logical_grid", "")) == "422x195@2x", "reference viewport preserves accepted logical grid")
|
|
for layer_name in ["BackgroundLayer", "MidgroundLayer", "ActorsLayer", "ForegroundLayer", "VfxLayer"]:
|
|
expect(experience.find_child(layer_name, true, false) != null, "%s remains present" % layer_name)
|
|
var background = experience.find_child("BackgroundLayer", true, false)
|
|
expect(String(background.get_meta("environment_region_id", "")) == "RG-03", "Market uses the Emberloom environment binding")
|
|
var first_company := String(experience.view.company[0].recruit_id)
|
|
var second_company := String(experience.view.company[1].recruit_id)
|
|
await drag("FG2CompanyTarget_%s" % first_company, "FG2CompanyTarget_%s" % second_company)
|
|
expect(experience.last_code == "formation_authority_frozen", "parsed drag never forges a formation transaction")
|
|
var route: Dictionary = experience.view.routes[0]
|
|
await tap("FG4Option_%s" % String(route.offer_id))
|
|
await tap("FG4ContinueButton")
|
|
expect(experience.view.mode == "plan" and experience.view.plans.size() == 4, "route confirmation opens four response plans")
|
|
var plan_id := String(Contract1Battle.PRIMARY_PLAN[1])
|
|
await tap("FG4Option_%s" % plan_id)
|
|
await tap("FG4ContinueButton")
|
|
expect(experience.screen == Controller.SCREEN_BATTLE, "plan confirmation enters automatic battle")
|
|
experience.advance_battle_ticks(4)
|
|
for _index in 3:
|
|
await process_frame
|
|
var vfx = experience.find_child("VfxLayer", true, false)
|
|
expect(bool(vfx.get_meta("fg4_pressure_visible", false)), "pressure valve is a visible world object")
|
|
expect(int(vfx.get_meta("fg4_world_vfx_count", 0)) > 0, "valve disclosure emits visible world VFX")
|
|
var actors = experience.find_child("ActorsLayer", true, false)
|
|
var authored_ids := Array(actors.get_meta("authored_actor_ids", []))
|
|
for enemy_id in route.enemy_ids:
|
|
expect(String(enemy_id) in authored_ids, "%s uses its authored RG-03 identity" % String(enemy_id))
|
|
var guard := 0
|
|
while not experience.battle_result_visible and guard < 80:
|
|
experience.advance_battle_ticks(4)
|
|
guard += 1
|
|
await process_frame
|
|
expect(guard < 80 and experience.view.battle.outcome == "victory", "Market-1 battle reaches visible victory")
|
|
for kind in ["pressure_valve_disclosed", "wave_arrived", "damage", "outcome"]:
|
|
expect(int(experience.presented_event_counts.get(kind, 0)) > 0, "runtime consumes %s" % kind)
|
|
await tap("FG2SettleButton")
|
|
expect(experience.view.mode == "market" and experience.view.market_index == 2, "settlement returns to changed Market 2")
|
|
|
|
|
|
func verify_first_boss_record_handoff() -> void:
|
|
await mount(Fixture.new().boss_muster_snapshot())
|
|
expect(experience.view.mode == "boss_muster" and experience.view.plans.size() == 2, "Furnace Muster renders two plans")
|
|
await tap("FG4Option_vent_and_brace")
|
|
await tap("FG4ContinueButton")
|
|
experience.advance_battle_ticks(8)
|
|
for _index in 3:
|
|
await process_frame
|
|
var actors = experience.find_child("ActorsLayer", true, false)
|
|
expect("BO-R3-01" in Array(actors.get_meta("authored_actor_ids", [])), "Furnace uses the authored action atlas")
|
|
var vfx = experience.find_child("VfxLayer", true, false)
|
|
expect(bool(vfx.get_meta("fg4_pressure_visible", false)) and int(vfx.get_meta("fg4_world_vfx_count", 0)) > 0, "Furnace pressure and vent response are world-visible")
|
|
while not experience.battle_result_visible:
|
|
experience.advance_battle_ticks(8)
|
|
await process_frame
|
|
await tap("FG2SettleButton")
|
|
expect(experience.screen == Controller.SCREEN_REWARD and experience.view.mode == "contract_record", "Furnace opens first Record screen")
|
|
await tap("FG4RecordButton")
|
|
expect(experience.view.mode == "contract_handoff", "first Record advances to explicit handoff")
|
|
await tap("FG4RecordButton")
|
|
expect(experience.view.mode == "market" and experience.view.contract_id == "CTR-R3-02", "handoff opens second Contract Market")
|
|
|
|
|
|
func verify_permanent_loss_recovery() -> void:
|
|
var snapshot_value := recovery_snapshot()
|
|
expect(not snapshot_value.is_empty(), "permanent-loss recovery fixture builds")
|
|
if snapshot_value.is_empty():
|
|
return
|
|
await mount(snapshot_value)
|
|
expect(experience.screen == Controller.SCREEN_RECOVERY and experience.view.mode == "recovery", "defeat opens visible recovery")
|
|
var survivors: int = experience.view.company.size()
|
|
var board_hash := String(snapshot_value.current_run.market_board.board_hash)
|
|
await tap("FG4RecoveryButton")
|
|
expect(experience.view.mode == "market" and experience.view.company.size() == survivors, "parsed recovery touch returns survivors without resurrection")
|
|
expect(String(experience.adapter.snapshot().current_run.market_board.board_hash) == board_hash, "recovery preserves the disclosed board")
|
|
var audio_state: Dictionary = experience.audio_router.snapshot()
|
|
expect(int(audio_state.event_counts.get("recovery", 0)) == 1, "recovery reaches semantic audio once")
|
|
expect(Array(audio_state.playback_log).any(func(row): return String(row.get("kind", "")) == "recovery" and String(row.get("result", "")) == "played"), "recovery plays its semantic cue")
|
|
|
|
|
|
func verify_second_boss_region_close() -> void:
|
|
await mount(Fixture.new().contract2_boss_muster_snapshot())
|
|
expect(experience.view.mode == "boss_muster" and experience.view.boss_id == "BO-R3-02", "Foreman Muster is visible")
|
|
await tap("FG4Option_vary_tags")
|
|
await tap("FG4ContinueButton")
|
|
experience.advance_battle_ticks(5)
|
|
for _index in 3:
|
|
await process_frame
|
|
var actors = experience.find_child("ActorsLayer", true, false)
|
|
expect("BO-R3-02" in Array(actors.get_meta("authored_actor_ids", [])), "Foreman uses the authored action atlas")
|
|
var vfx = experience.find_child("VfxLayer", true, false)
|
|
expect(int(vfx.get_meta("fg4_spark_slot_count", 0)) == 8, "Foreman composes exactly eight source-bound spark slots")
|
|
expect(int(vfx.get_meta("fg4_promoted_spark_count", 0)) == 0, "tag variation does not forge a promoted spark")
|
|
expect(int(vfx.get_meta("fg4_world_vfx_count", 0)) > 0, "repeat and promotion events own visible world VFX")
|
|
var presenter_state: Dictionary = experience.event_presenter.snapshot()
|
|
for kind in ["repeat_counter_disclosed", "next_spark_disclosed", "repeat_counter_reset"]:
|
|
expect(int(presenter_state.event_counts.get(kind, 0)) == 1, "%s reaches the event-bound presenter" % kind)
|
|
var audio_state: Dictionary = experience.audio_router.snapshot()
|
|
for kind in ["repeat_counter_disclosed", "next_spark_disclosed", "repeat_counter_reset"]:
|
|
expect(int(audio_state.event_counts.get(kind, 0)) == 1, "%s reaches the semantic audio router" % kind)
|
|
expect(Array(audio_state.playback_log).any(func(row): return String(row.get("kind", "")) == kind and String(row.get("result", "")) != "unmapped"), "%s owns a mapped audio decision" % kind)
|
|
while not experience.battle_result_visible:
|
|
experience.advance_battle_ticks(8)
|
|
await process_frame
|
|
await tap("FG2SettleButton")
|
|
expect(experience.view.mode == "region_record", "Foreman opens second Record")
|
|
await tap("FG4RecordButton")
|
|
expect(experience.view.mode == "region_closed" and experience.view.unlocked_region_ids.count("RG-04") == 1, "final touch reveals stable RG-04 unlock")
|
|
expect(observed_touches + experience.input_touch_count == parsed_touches and observed_drags + experience.input_drag_count == parsed_drags, "all input is accounted as parsed touch/drag")
|
|
expect(experience.fallback_input_count == 0 and experience.direct_command_count == 0, "no fallback or direct player command path runs")
|
|
|
|
|
|
func verify_contract2_promotion() -> void:
|
|
await mount(Fixture.new().contract2_market_1_snapshot())
|
|
var route: Dictionary = experience.view.routes[0]
|
|
await tap("FG4Option_%s" % String(route.offer_id))
|
|
await tap("FG4ContinueButton")
|
|
await tap("FG4Option_safe_promotion")
|
|
await tap("FG4ContinueButton")
|
|
experience.advance_battle_ticks(8)
|
|
for _index in 3:
|
|
await process_frame
|
|
var vfx = experience.find_child("VfxLayer", true, false)
|
|
expect(int(vfx.get_meta("fg4_spark_slot_count", 0)) == 8, "ordinary Contract-2 battle composes eight spark slots")
|
|
expect(int(vfx.get_meta("fg4_promoted_spark_count", 0)) == 1, "safe promotion changes exactly one spark")
|
|
expect(int(experience.presented_event_counts.get("spark_promoted", 0)) == 1, "safe promotion event is consumed exactly once")
|
|
|
|
|
|
func recovery_snapshot() -> Dictionary:
|
|
var snapshot_value := Fixture.new().market_1_snapshot()
|
|
var company: Array = Array(snapshot_value.current_run.company).duplicate()
|
|
var deployed_count := mini(4, company.size())
|
|
snapshot_value.current_run.deployed_recruit_ids = company.slice(0, deployed_count)
|
|
snapshot_value.current_run.reserve_recruit_ids = company.slice(deployed_count)
|
|
snapshot_value.current_run.formation_proposal = company.slice(0, deployed_count)
|
|
var adapter := Adapter.new()
|
|
if not bool(adapter.configure(snapshot_value).get("ok", false)):
|
|
return {}
|
|
var route: Dictionary = adapter.view_state().routes[2]
|
|
if not bool(adapter.prepare_route(String(route.offer_id)).get("ok", false)):
|
|
return {}
|
|
var primary := String(Contract1Battle.PRIMARY_PLAN[1])
|
|
var secondary := String(Contract1Battle.SECONDARY_PLAN[1])
|
|
var wrong_plan := ""
|
|
for candidate in Contract1Battle.PLAN_IDS:
|
|
if candidate not in [primary, secondary]:
|
|
wrong_plan = String(candidate)
|
|
break
|
|
if wrong_plan.is_empty() or not bool(adapter.start_plan(wrong_plan).get("ok", false)):
|
|
return {}
|
|
var terminal: Dictionary = adapter.run_battle_to_terminal()
|
|
if String(terminal.get("view", {}).get("battle", {}).get("outcome", "")) != "defeat":
|
|
return {}
|
|
var settled: Dictionary = adapter.settle_terminal()
|
|
return adapter.snapshot() if bool(settled.get("ok", false)) else {}
|
|
|
|
|
|
func mount(snapshot_value: Dictionary) -> void:
|
|
if is_instance_valid(experience):
|
|
await release_experience(true)
|
|
experience = Controller.new()
|
|
experience.battle_auto_advance = false
|
|
experience.muted = false
|
|
experience.locale = "zh_CN"
|
|
experience.text_scale = 1.0
|
|
experience.size = Vector2(VIEWPORT)
|
|
experience.configure_entry(snapshot_value)
|
|
root.add_child(experience)
|
|
for _index in 10:
|
|
await process_frame
|
|
|
|
|
|
func release_experience(record_input: bool) -> void:
|
|
if not is_instance_valid(experience):
|
|
return
|
|
if record_input:
|
|
observed_touches += int(experience.input_touch_count)
|
|
observed_drags += int(experience.input_drag_count)
|
|
experience.audio_router.cleanup()
|
|
await create_timer(0.85).timeout
|
|
experience.queue_free()
|
|
experience = null
|
|
for _index in 45:
|
|
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 before 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 process_frame
|
|
var up := InputEventScreenTouch.new()
|
|
up.index = 0
|
|
up.position = point
|
|
up.pressed = false
|
|
Input.parse_input_event(up)
|
|
for _index in 5:
|
|
await process_frame
|
|
|
|
|
|
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 process_frame
|
|
var motion := InputEventScreenDrag.new()
|
|
motion.index = 1
|
|
motion.position = finish
|
|
motion.relative = finish - start
|
|
Input.parse_input_event(motion)
|
|
parsed_drags += 1
|
|
await process_frame
|
|
var up := InputEventScreenTouch.new()
|
|
up.index = 1
|
|
up.position = finish
|
|
up.pressed = false
|
|
Input.parse_input_event(up)
|
|
for _index in 5:
|
|
await process_frame
|
|
|
|
|
|
func expect(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append("FG-4 campaign runtime touch: %s" % message)
|