extends SceneTree const MainScene = preload("res://main.tscn") const State = preload("res://p9_3/core_loop_state.gd") const VIEWPORT_SIZE := Vector2i(844, 390) var failures: Array[String] = [] var game: Control var experience: Control var touch_count := 0 var capture_root := "" var contact_captured := false var ranged_captured := false var recording_only := false func _initialize() -> void: call_deferred("run_test") func run_test() -> void: capture_root = OS.get_environment("ABG_P9_3_CAPTURE_ROOT") recording_only = OS.get_environment("ABG_P9_3_RECORDING_ONLY") == "1" root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND root.content_scale_size = VIEWPORT_SIZE root.size = VIEWPORT_SIZE game = MainScene.instantiate() game.auto_boot = false game.audio_enabled = false root.add_child(game) await settle(8) game.start_p93_experience() await settle(8) experience = game.p93_experience expect(experience != null, "modular experience starts independently") if experience == null: for failure in failures: push_error(failure) quit(1) return expect(float(experience.get_meta("visible_world_ratio", 0.0)) >= 0.82, "world owns at least 82 percent of the surface") expect(find_control(experience, "P93EnterHallButton") != null, "Title exposes one entry action") await capture("01-title") await recording_hold(45) await tap("P93EnterHallButton") expect(experience.service.state.phase == State.Phase.HALL, "Title enters the Hall directly") expect(find_control(experience, "P93OfferTarget_Wayrunner") != null, "Wayrunner body is a touch target") expect(find_control(experience, "P93OfferTarget_Farstring") != null, "Farstring body is a touch target") expect(find_control(experience, "P93OfferTarget_Kindhand") != null, "Kindhand body is a touch target") expect(count_named_fragment(experience, "Card") == 0, "Hall contains no card surface") await capture("02-hall") await recording_hold(55) await tap("P93OfferTarget_Farstring") expect(experience.service.state.focused_offer == "Farstring", "first actor tap focuses the person") expect(find_control(experience, "P93OfferDetail") != null, "focused person exposes a short in-world detail") await capture("03-offer-focused") await recording_hold(45) await tap("P93OfferTarget_Farstring") expect(experience.service.state.phase == State.Phase.ENCOUNTER, "second actor tap hires and opens threats") expect(experience.service.state.company == ["Hearthguard", "Farstring"], "hired actor joins the line") expect(experience.service.state.coin == 12, "hire deducts the exact 12 Coin") expect(find_control(experience, "P93EncounterTarget_open_road") != null, "Open Road is a world target") expect(find_control(experience, "P93EncounterTarget_crosswind") != null, "Crosswind is a world target") expect(find_control(experience, "P93EncounterTarget_bell_breach") != null, "Bell Breach is a world target") await capture("04-encounters") await recording_hold(55) await tap("P93EncounterTarget_open_road") expect(experience.service.state.selected_encounter == "open_road", "first enemy tap selects actual pressure") await capture("05-encounter-selected") await recording_hold(45) await tap("P93EncounterTarget_open_road") expect(experience.service.state.phase == State.Phase.BATTLE, "second enemy tap enters automatic combat") expect(experience.service.battle.state.living_enemy_ids().size() == 1, "Open Road contains one living enemy") if not recording_only: await tap("P93SpeedButton") var contact_seen := false var ranged_event_seen := false var projectile_seen := false var guard := 0 while experience.service.state.phase == State.Phase.BATTLE and guard < 1400: for event in experience.service.battle.state.event_log: contact_seen = contact_seen or String(event.get("kind", "")) == "contact" ranged_event_seen = ranged_event_seen or (String(event.get("kind", "")) == "member_action" and String(event.get("source_member_id", "")) == "Farstring") if contact_seen and not contact_captured: await capture("06-contact") contact_captured = true var projectile := find_control(experience, "P93Projectile") projectile_seen = projectile_seen or (projectile != null and projectile.is_visible_in_tree()) if projectile_seen and not ranged_captured: await settle(10) await capture("07-ranged-action") ranged_captured = true await process_frame guard += 1 expect(contact_seen, "the horizontal lines visibly reach contact") expect(ranged_event_seen, "the recruited profession commits its role action") expect(projectile_seen, "the ranged role action exposes a visible projectile") expect(experience.service.state.phase == State.Phase.RESULT, "automatic battle reaches a bounded result") expect(experience.service.state.last_outcome == "victory", "the first Open Road line wins") experience._process(0.8) await settle(55) expect(find_control(experience, "P93BattleResult") != null, "battlefield keeps the visible result") await capture("08-result") await recording_hold(45) await tap("P93ReturnHallButton") expect(experience.service.state.phase == State.Phase.HALL, "result returns to the same Hall") expect(experience.service.state.company == ["Hearthguard", "Farstring"], "return preserves the hired line") expect(experience.service.state.coin == 20, "return preserves exact reward Coin") expect(find_control(experience, "P93ReturnReceipt") != null, "returned Hall shows the earned change") verify_touch_bounds(experience, Rect2(Vector2.ZERO, Vector2(VIEWPORT_SIZE))) await capture("09-returned-hall") await recording_hold(45) if recording_only: finish_test() return game.queue_free() await settle(90) finish_test() func finish_test() -> void: if failures.is_empty(): if recording_only: print("ABG_P9_3_REFERENCE_FIDELITY_RECORDING_OK viewport=844x390 touch=%d fallback=0 direct=0 title=true hall=true hire=true encounters=3 battle=true contact=true ranged=true reward=true return=true" % touch_count) else: print("ABG_P9_3_REFERENCE_FIDELITY_RUNTIME_OK viewport=844x390 touch=%d fallback=0 direct=0 title=true hall=true hire=true encounters=3 battle=true contact=true ranged=true reward=true return=true cards=0" % touch_count) quit(0) else: for failure in failures: push_error(failure) quit(1) func tap(node_name: String) -> void: var control: Control = find_control(experience if experience != null else game, node_name) if control == null or not control.is_visible_in_tree() or (control is BaseButton and control.disabled): failures.append("unavailable parsed-touch target %s" % node_name) return var position := root.get_final_transform() * control.get_global_rect().get_center() var down := InputEventScreenTouch.new() down.index = 0 down.position = position down.pressed = true Input.parse_input_event(down) await process_frame var up := InputEventScreenTouch.new() up.index = 0 up.position = position up.pressed = false Input.parse_input_event(up) touch_count += 1 await settle(6) func capture(name: String) -> void: if capture_root.is_empty(): return DirAccess.make_dir_recursive_absolute(capture_root) await settle(3) await RenderingServer.frame_post_draw var error := root.get_texture().get_image().save_png(capture_root.path_join(name + ".png")) expect(error == OK, "capture %s writes" % name) func verify_touch_bounds(node: Node, bounds: Rect2) -> void: if node is BaseButton and node.is_visible_in_tree(): var rect: Rect2 = node.get_global_rect() if rect.position.x < -0.5 or rect.position.y < -0.5 or rect.end.x > bounds.end.x + 0.5 or rect.end.y > bounds.end.y + 0.5: failures.append("button %s outside viewport: %s" % [node.name, rect]) if rect.size.x < 44.0 or rect.size.y < 44.0: failures.append("button %s below 44px: %s" % [node.name, rect.size]) for child in node.get_children(): verify_touch_bounds(child, bounds) func count_named_fragment(node: Node, fragment: String) -> int: var result := 1 if fragment in String(node.name) else 0 for child in node.get_children(): result += count_named_fragment(child, fragment) return result func find_control(node: Node, node_name: String) -> Control: if node.name == node_name and node is Control and not node.is_queued_for_deletion(): return node for child in node.get_children(): var found := find_control(child, node_name) if found != null: return found return null func settle(frames: int) -> void: for _frame in frames: await process_frame func recording_hold(frames: int) -> void: if recording_only: await settle(frames) func expect(condition: bool, message: String) -> void: if not condition: failures.append("P9.3 reference-fidelity runtime: %s" % message)