extends SceneTree const Controller = preload("res://campaign/fg2_graybox_controller.gd") const Fixture = preload("res://campaign/fg2_fixture.gd") var failures: Array[String] = [] var experience var capture_root := "" var captures := 0 func _initialize() -> void: call_deferred("run_test") func run_test() -> void: capture_root = OS.get_environment("ABG_FG2_CAPTURE_ROOT") if not capture_root.is_empty(): DirAccess.make_dir_recursive_absolute(capture_root) await fixture("01-market-small-zh", Fixture.canonical_market_state(), Vector2i(760, 360), "zh_CN", 1.0, Controller.SCREEN_MARKET, "380x180@2x") await fixture("02-market-reference-en130", Fixture.canonical_market_state(), Vector2i(844, 390), "en", 1.3, Controller.SCREEN_MARKET, "422x195@2x") await fixture("03-market-large-zh", Fixture.canonical_market_state(), Vector2i(1280, 720), "zh_CN", 1.0, Controller.SCREEN_MARKET, "320x180@4x") await battle_fixture("04-battle-reference-zh", Vector2i(844, 390), "zh_CN", 1.0) await fixture("05-reward-reference-en", Fixture.reward_market_state(), Vector2i(844, 390), "en", 1.0, Controller.SCREEN_REWARD, "422x195@2x") await fixture("06-recovery-reference-zh130", Fixture.recovery_market_state(), Vector2i(844, 390), "zh_CN", 1.3, Controller.SCREEN_RECOVERY, "422x195@2x") await reduced_fixture("07-battle-reduced-low-power", Vector2i(844, 390)) if is_instance_valid(experience): experience.queue_free() for _index in 45: await process_frame if failures.is_empty(): print("ABG_FG2_GRAYBOX_LAYOUT_OK fixtures=7 states=market,battle,reward,recovery grids=380x180@2x,422x195@2x,320x180@4x viewports=760x360,844x390,1280x720 locales=zh_CN,en text_130=true reduced_motion=true muted=true low_power=true controls=48 panels=0 cards=0 captures=%d" % captures) quit(0) else: for failure in failures: push_error(failure) quit(1) func fixture(label: String, snapshot_value: Dictionary, viewport: Vector2i, locale_value: String, scale_value: float, expected_screen: String, expected_grid: String) -> void: await mount(snapshot_value, viewport, locale_value, scale_value, false, false) expect(experience.screen == expected_screen, "%s restores %s" % [label, expected_screen]) expect(String(experience.get_meta("logical_grid", "")) == expected_grid, "%s uses %s" % [label, expected_grid]) verify_current(label, viewport) await capture(label) func battle_fixture(label: String, viewport: Vector2i, locale_value: String, scale_value: float) -> void: await mount(Fixture.canonical_encounter_state(), viewport, locale_value, scale_value, false, false) experience.advance_battle_ticks(96) await settle_frames() expect(experience.screen == Controller.SCREEN_BATTLE and not experience.view.battle.enemies.is_empty(), "%s shows the real Company and enemy together" % label) verify_current(label, viewport) await capture(label) func reduced_fixture(label: String, viewport: Vector2i) -> void: await mount(Fixture.canonical_encounter_state(), viewport, "zh_CN", 1.0, true, true) experience.advance_battle_ticks(96) await settle_frames() expect(experience.reduced_motion and experience.low_power and experience.muted, "%s retains all resource variants" % label) verify_current(label, viewport) await capture(label) func mount(snapshot_value: Dictionary, viewport: Vector2i, locale_value: String, scale_value: float, reduced: bool, low_power_value: bool) -> void: if is_instance_valid(experience): experience.queue_free() await settle_frames(12) 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 experience = Controller.new() experience.battle_auto_advance = false experience.locale = locale_value experience.text_scale = scale_value experience.reduced_motion = reduced experience.low_power = low_power_value experience.muted = true experience.size = Vector2(viewport) experience.configure_entry(snapshot_value) root.add_child(experience) await settle_frames(8) func verify_current(label: String, viewport: Vector2i) -> void: var bounds := Rect2(Vector2.ZERO, Vector2(viewport)) expect(float(experience.get_meta("visible_world_ratio", 0.0)) >= 0.80, "%s keeps world-first composition" % label) expect(experience.find_children("*", "Panel", true, false).is_empty(), "%s adds no app Panel surfaces" % label) for layer_name in ["BackgroundLayer", "MidgroundLayer", "ActorsLayer", "ForegroundLayer", "VfxLayer"]: expect(experience.find_child(layer_name, true, false) != null, "%s retains %s" % [label, layer_name]) for node in experience.find_children("*", "Button", true, false): var button := node as Button if not button.visible or button.is_queued_for_deletion(): continue expect(button.size.x >= 48 and button.size.y >= 48, "%s target %s is at least 48px" % [label, button.name]) expect(bounds.encloses(button.get_global_rect()), "%s target %s stays inside %s" % [label, button.name, viewport]) for node in experience.find_children("*", "Control", true, false): var lowered := String(node.name).to_lower() expect("card" not in lowered and "modal" not in lowered and "dialog" not in lowered, "%s has no card/modal surface: %s" % [label, node.name]) func capture(label: String) -> void: if capture_root.is_empty(): return await RenderingServer.frame_post_draw var error := root.get_texture().get_image().save_png(capture_root.path_join(label + ".png")) expect(error == OK, "%s capture writes" % label) if error == OK: captures += 1 await process_frame func settle_frames(count := 5) -> void: for _index in count: await process_frame func expect(condition: bool, message: String) -> void: if not condition: failures.append("FG-2 graybox layout: %s" % message)