extends SceneTree const Adapter = preload("res://campaign/p100_adapter.gd") 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 func _initialize() -> void: call_deferred("run_test") func run_test() -> void: 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 verify_full_presentation() await verify_resource_variants() await verify_recovery_presentation() if is_instance_valid(experience): experience.queue_free() for _index in 45: await process_frame if failures.is_empty(): print("ABG_FG2_MOTION_RUNTIME_OK events=source-bound choreography=attack_then_fallen casualty_delay_max=0.12 scribe=entry+attack sprout=entry+attack aegis=guard damage=signed mitigation=derived fallen=cue+label result=transient recovery=receipt-bound hit_pause=0.055 shake=2 transients=8 labels=4 rest=true reduced_motion=true reduced_flashes=true low_power=true event_hash=exact fallback=0 direct=0") quit(0) else: for failure in failures: push_error(failure) quit(1) func verify_full_presentation() -> void: await mount(false, false, false) var expected = Adapter.new() expect(bool(expected.configure(Fixture.canonical_encounter_state()).ok), "expected adapter configures") var expected_terminal: Dictionary = expected.run_battle_to_terminal() expect(bool(expected_terminal.get("ok", false)), "expected battle reaches terminal") var expected_hash := String(expected_terminal.get("event_log_hash", "")) var saw_scribe_entry := false var saw_scribe_attack := false var saw_sprout_entry := false var saw_sprout_attack := false var saw_guard := false var saw_damage_value := false var saw_fallen_cue := false var saw_fallen_label := false var last_fallen_target_id := "" var last_events: Array = [] var guard := 0 while not experience.battle_result_visible and guard < 2200: var result: Dictionary = experience.advance_battle_ticks(1) last_events = result.get("events", []) for event in last_events: var kind := String(event.get("kind", "")) var source_id := String(event.get("source_id", "")) var target_id := String(event.get("target_id", "")) var identity := String(experience.event_presenter.actor_identities.get(source_id, "")) var snapshot: Dictionary = experience.event_presenter.snapshot() if kind == "wave_arrived" and identity == "EN-R1-09": saw_scribe_entry = cue_is(snapshot, source_id, "entry") elif kind == "wave_arrived" and identity == "EN-R1-08": saw_sprout_entry = cue_is(snapshot, source_id, "entry") elif kind == "damage": if identity == "EN-R1-09": saw_scribe_attack = cue_is(snapshot, source_id, "attack") elif identity == "EN-R1-08": saw_sprout_attack = cue_is(snapshot, source_id, "attack") var amount := int(event.get("detail", {}).get("effective", 0)) saw_damage_value = saw_damage_value or has_text(snapshot.get("transients", []), "-%d" % amount) var raw := int(event.get("detail", {}).get("raw", 0)) if int(event.get("detail", {}).get("guard_before", 0)) > 0 and raw > amount: saw_guard = cue_is(snapshot, target_id, "guard") and has_text(snapshot.get("transients", []), "%d BLOCKED" % (raw - amount)) elif kind == "fallen": expect(cue_is(snapshot, target_id, "fallen") or queued_motion_is(snapshot, target_id, "fallen"), "fallen target keeps a source-bound current or queued cue after authority removes it") saw_fallen_cue = saw_fallen_cue or cue_is(snapshot, target_id, "fallen") or queued_motion_is(snapshot, target_id, "fallen") saw_fallen_label = saw_fallen_label or has_text(snapshot.get("transients", []), "FALLEN") last_fallen_target_id = target_id await process_frame expect(experience.feedback_labels.size() <= 4, "native feedback labels stay capped") guard += 1 expect(guard < 2200 and String(experience.view.battle.outcome) == "victory", "canonical battle reaches the real victory") expect(saw_scribe_entry and saw_scribe_attack, "Crosswind Scribe shows entry and attack") expect(saw_sprout_entry and saw_sprout_attack, "Glassroot Sprout shows entry and attack") expect(saw_guard, "Aegis mitigation shows guard pose and exact blocked amount") expect(saw_damage_value, "effective damage appears as an actor-local signed value") expect(saw_fallen_cue and saw_fallen_label, "fallen identity keeps a bounded cue and explicit label") expect(has_text(experience.event_presenter.snapshot().transients, "VICTORY"), "terminal outcome exposes a result transient") experience.event_presenter.advance(0.13) expect(cue_is(experience.event_presenter.snapshot(), last_fallen_target_id, "fallen"), "same-tick final attacker reaches fallen motion within 130 ms") var runtime_hash := String(experience.view.battle.event_log_hash) expect(runtime_hash == expected_hash, "presentation preserves the exact authoritative event hash") var seen_before := int(experience.event_presenter.snapshot().seen_event_count) experience.event_presenter.consume(last_events) expect(int(experience.event_presenter.snapshot().seen_event_count) == seen_before, "runtime presenter ignores duplicate event delivery") experience.event_presenter.advance(1.2) var rested: Dictionary = experience.event_presenter.snapshot() expect(rested.actor_cues.is_empty() and rested.transients.is_empty(), "runtime presentation returns to rest") expect(experience.fallback_input_count == 0 and experience.direct_command_count == 0, "presentation creates no command fallback") func verify_resource_variants() -> void: await mount(true, true, true) var damage_event: Dictionary = {} var guard := 0 while damage_event.is_empty() and guard < 1200: var result: Dictionary = experience.advance_battle_ticks(1) for event in result.get("events", []): if String(event.get("kind", "")) == "damage": damage_event = event break guard += 1 var snapshot: Dictionary = experience.event_presenter.snapshot() expect(not damage_event.is_empty(), "resource fixture reaches damage") expect(float(snapshot.hit_pause) == 0.0 and Vector2i(snapshot.shake) == Vector2i.ZERO, "resource variants remove pause and shake") for transient in snapshot.transients: expect(not bool(transient.get("flash", false)), "resource variants remove flashes") var effective := int(damage_event.get("detail", {}).get("effective", 0)) expect(has_text(snapshot.transients, "-%d" % effective), "resource variants preserve the damage fact") expect(experience.reduced_motion and experience.reduced_flashes and experience.low_power, "all resource variants stay active") func verify_recovery_presentation() -> void: await mount_snapshot(Fixture.recovery_market_state(), false, false, false) var company_before: int = experience.view.get("company", []).size() await tap("FG2RecoveryButton") var snapshot: Dictionary = experience.event_presenter.snapshot() expect(experience.screen == Controller.SCREEN_MARKET and experience.view.company.size() == company_before + 1, "successful protected hire returns to the changed Market") expect(int(snapshot.event_counts.get("recovery", 0)) == 1 and has_text(snapshot.transients, "RECOVERED"), "recovery cue is bound to the successful receipt") expect(has_label_text("补员完成"), "zh_CN recovery feedback is visible at the new Recruit") expect(experience.input_touch_count == 1 and experience.fallback_input_count == 0 and experience.direct_command_count == 0, "recovery uses one parsed touch and no fallback") 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 has_label_text(expected: String) -> bool: for label in experience.feedback_labels.values(): if is_instance_valid(label) and String(label.text) == expected: return true return false func cue_is(snapshot: Dictionary, actor_id: String, motion: String) -> bool: return String(snapshot.get("actor_cues", {}).get(actor_id, {}).get("motion", "")) == motion func queued_motion_is(snapshot: Dictionary, actor_id: String, motion: String) -> bool: for cue in snapshot.get("pending_actor_cues", {}).get(actor_id, []): if String(cue.get("motion", "")) == motion: return true return false func has_text(transients: Array, expected: String) -> bool: for transient in transients: if String(transient.get("text", "")) == expected: return true return false func expect(condition: bool, message: String) -> void: if not condition: failures.append("FG-2 motion runtime: %s" % message)