extends SceneTree const Anchor = preload("res://scripts/hud_anchor.gd") var failures := 0 func _initialize() -> void: call_deferred("run") func check(value: bool, message: String) -> void: if not value: failures += 1 push_error(message) func _check_clearance(scene: Node3D, message: String) -> void: var ship := Anchor.transformed_bounds(scene._player_hud_local_bounds(), scene.player_ship.global_transform) var panel := Anchor.panel_bounds(scene.hud_surface.global_transform, scene.hud_surface.surface_size) check(not Anchor.overlaps_ship(scene.hud_surface.global_transform, scene.hud_surface.surface_size, scene.player_ship.global_transform, scene._player_hud_local_bounds(), Anchor.CLEARANCE - 0.001), message) if scene.hud_anchor.blocked: check(panel.position.y >= ship.end.y + Anchor.CLEARANCE - 0.001, "A blocked HUD must stop entirely above the transformed ship/shield volume") func run() -> void: var scene := load("res://tools/input_harness.gd").new() as Node3D root.add_child(scene) await process_frame scene.set_process(false) scene.hud_surface.set_process(false) scene.demo_mode = false scene.xr_active = true scene.origin.transform = Transform3D.IDENTITY scene.camera.transform = Transform3D(Basis.IDENTITY, Vector3(0, 1.7, 1.3)) scene.hud_state = {"ready": true, "ui_mode": "game", "paused": true} scene._recenter() scene._update_hud() scene._position_hud(0.0, true) check(scene.hud_surface.get_parent() == scene and scene.gameplay_hud, "Gameplay HUD must permit collision-clamped headset-relative placement") check(scene.hud_surface.source_rect == Rect2(0, 0, 872, 510), "Gameplay must retain upper status and complete crew roster while excluding the entire bottom HUD") check(is_equal_approx(scene.hud_surface.global_basis.x.length(), 1.0) and is_equal_approx(scene.hud_surface.surface_size.x, 1.10), "HUD readability must use fixed world metres, independent of tabletop scale") check(scene.pause_label.get_parent() == scene.hud_surface and scene.pause_label.visible and scene.pause_label.position.y > scene.hud_surface.surface_size.y / 2, "Pause must remain visible above the compact HUD after the lower UI is removed") check(scene.tracking_label.get_parent() == scene.camera and scene.tracking_label.position.x > 0 and scene.tracking_label.position.y > -.5, "Modifier/controller status must stay accessible at the lower right of the view") _check_clearance(scene, "Initial HUD must clear the hull and shields") check(scene.hud_surface.global_position.z < scene.camera.global_position.z, "Initial HUD should remain in front of the headset") # Move the table out of the viewer's path. The head-relative HUD must follow # position, pitch, yaw and roll instead of staying anchored to that table. scene.tabletop_root.position = Vector3(8, 4, 8) scene.hud_anchor.reset() scene._position_hud(0.0, true) check(not scene.hud_anchor.blocked and scene.hud_surface.global_position.is_equal_approx(scene.camera.get_camera_transform() * Anchor.HEAD_OFFSET), "Empty space must use the headset-relative pose even if a distant ship is higher") scene.camera.position += Vector3(.02, .01, .01) scene.camera.rotation = Vector3(-.2, .25, .12) scene._position_hud(1.0 / 90) check(scene.hud_surface.global_position.is_equal_approx(scene.camera.get_camera_transform() * Anchor.HEAD_OFFSET), "Unobstructed HUD must follow headset motion immediately") check(scene.hud_surface.global_basis.is_equal_approx(scene.camera.get_camera_transform().basis), "Unobstructed HUD rotation must preserve headset-relative framing") scene.camera.rotation = Vector3.ZERO scene._recenter() scene.camera.rotation.x = -.85 scene._position_hud(0.0, true) check(scene.hud_anchor.blocked and scene.hud_anchor.lift > 0, "Looking down toward the ship must engage the HUD height stop") _check_clearance(scene, "Looking down must not tunnel the HUD through the ship") var clearance_lift: float = scene.hud_anchor.lift scene.camera.rotation.y = PI scene._position_hud(1.0 / 90) check(not scene.hud_anchor.blocked and scene.hud_anchor.lift > 0 and scene.hud_anchor.lift < clearance_lift, "Turning away must release the height stop smoothly") for frame in range(180): scene._position_hud(1.0 / 90) check(scene.hud_anchor.lift == 0 and scene.hud_surface.global_position.is_equal_approx(scene.camera.get_camera_transform() * Anchor.HEAD_OFFSET), "Once clear, the HUD must return fully to its headset anchor") scene.camera.rotation = Vector3.ZERO scene._recenter() for scale_value in [.2, .35, .8, 1.5]: for rotation_value in [Vector3.ZERO, Vector3(0, 1.2, 0), Vector3(.4, .7, .3)]: scene.tabletop_root.basis = Basis.from_euler(rotation_value).scaled(Vector3.ONE * scale_value) for height in [-.6, .0, .8]: scene.tabletop_root.position += Vector3(.3, height, -.1) for frame in range(10): scene._position_hud(1.0 / 90) _check_clearance(scene, "Scaled/rotated/grabbed ship must never intersect the HUD during smoothing") check(scene.hud_surface.global_transform.is_finite(), "Extreme placement fallback must stay finite") # Cropping changes geometry only, preserving every retained native pixel. var image := Image.create(1280, 720, false, Image.FORMAT_RGBA8) image.fill(Color.WHITE) scene.hud_surface.canvas.frame_image = image for native in [Vector2i(35, 30), Vector2i(580, 50), Vector2i(50, 470)]: var world: Vector3 = scene.hud_surface.pixel_world(native) var normal: Vector3 = scene.hud_surface.global_basis.z.normalized() check(scene.hud_surface.ray_pixel(world + normal, -normal) == native, "Top/roster ray picks must preserve original native coordinates under rotation") var removed_world: Vector3 = scene.hud_surface.pixel_world(Vector2i(400, 670)) check(scene.hud_surface.ray_pixel(removed_world + scene.hud_surface.global_basis.z, -scene.hud_surface.global_basis.z) == null, "Removed lower HUD must not intercept pointing at the world") var event_crop: Rect2 = scene.event_surface.source_rect var panel_crop: Rect2 = scene.world_surface.source_rect scene.hud_state.event_open = true scene.hud_state.panel_open = true scene._sync_hud_layout() check(scene.gameplay_hud and scene.event_surface.source_rect == event_crop and scene.world_surface.source_rect == panel_crop, "Gameplay top crop must not alter full native dialog/store/ship panel content") scene.hud_state = {"ready": false, "ui_mode": "menu"} scene._sync_hud_layout() scene._position_hud(0.0) check(not scene.gameplay_hud and scene.hud_surface.source_rect == Rect2(0, 0, 1280, 720), "Main menus must retain the entire original native screen") var expected: Vector3 = scene.camera.get_camera_transform() * Vector3(0, -.16, -1.8) check(scene.hud_surface.global_position.is_equal_approx(expected), "Full main menus must retain the original viewer-relative readable framing") scene.xr_active = false for mode in ["map_open", "tactical"]: scene.hud_state = {"ready": true, "ui_mode": "game", mode: true} scene._sync_hud_layout() check(not scene.gameplay_hud and scene.hud_surface.source_rect.size == Vector2(1280, 720), "Desktop native map/Tactical display must preserve full content and picking") scene.xr_active = true scene.hud_state = {"ready": true, "ui_mode": "game", "tactical": true} scene._sync_hud_layout() check(scene.gameplay_hud, "VR Tactical screen must retain the headset HUD while its hand screen shows full Tactical content") if DisplayServer.get_name() != "headless": await _capture(scene) scene.queue_free() print("HUD_LAYOUT_TESTS ", "PASS" if failures == 0 else "FAIL %d" % failures) quit(0 if failures == 0 else 1) func rendered() -> void: await process_frame await RenderingServer.frame_post_draw func _capture(scene: Node3D) -> void: scene.camera.rotation.x = -.28 scene._recenter() scene._position_hud(0.0, true) scene.pause_label.visible = false scene.hud_surface.canvas.use_original_frame = false # Color bands make a removed lower HUD an objective GPU check, and retained # roster/native positions independently test the crop rather than its math. var image := Image.create(1280, 720, false, Image.FORMAT_RGBA8) image.fill(Color.TRANSPARENT) image.fill_rect(Rect2i(0, 0, 872, 100), Color.GREEN) image.fill_rect(Rect2i(0, 150, 100, 340), Color.BLUE) image.fill_rect(Rect2i(0, 550, 1280, 170), Color.RED) scene.hud_surface.canvas._accept_frame(image, "hud-layout-fixture") await rendered() var output: Image = scene.hud_surface.viewport.get_texture().get_image() check(output.get_pixel(600, 50).g > .8 and output.get_pixel(30, 650).b > .8, "GPU top crop must preserve both upper status and the long crew roster") var red_pixels := 0 for y in range(0, output.get_height(), 5): for x in range(0, output.get_width(), 5): var pixel := output.get_pixel(x, y) if pixel.r > .8 and pixel.g < .2 and pixel.b < .2: red_pixels += 1 check(red_pixels == 0, "No lower HUD pixels may leak into the cropped GPU texture") scene.hud_state = {"ready": false, "ui_mode": "menu"} scene._sync_hud_layout() await rendered() output = scene.hud_surface.viewport.get_texture().get_image() check(output.get_pixel(600, 650).r > .8, "Full native menus must retain their lower content after returning from gameplay crop") scene.hud_state = {"ready": true, "ui_mode": "game"} scene._sync_hud_layout() scene._position_hud(0.0, true) # Prefer an immutable native QA snapshot so a concurrent bridge frame cannot # change the fixture between its load and visual inspection. var native_path := "res://local_game_data/speed-tactical-return-hud_frame.png" if FileAccess.file_exists(native_path): var native := Image.new() native.load_png_from_buffer(FileAccess.get_file_as_bytes(native_path)) scene.hud_surface.canvas._accept_frame(native, "hud-layout-owned-native") await rendered() scene.hud_surface.viewport.get_texture().get_image().save_png("res://local_game_data/hud-top-spatial-native.png") root.get_texture().get_image().save_png("res://local_game_data/hud-top-spatial-scene.png") # The fallback/demo uses the same crop; bottom weapon/reactor rows stay out. scene.hud_surface.canvas.original_frame = null scene.hud_surface.canvas.set_state({"crew": [{"name": "Roster test", "health": 100}], "paused": false}) scene.hud_surface.canvas.request_redraw() await rendered() output = scene.hud_surface.viewport.get_texture().get_image() output.save_png("res://local_game_data/hud-top-spatial-demo.png") check(output.get_pixel(30, 690).a < .1, "Fallback/demo bottom power/reactor must also disappear under the native crop")