# mobile_ui_test —— mobile presentation smoke test without a server or assets. # godot --headless --path project --script mobile_ui_test.gd extends SceneTree const UiProfile := preload("res://ui/ui_profile.gd") const MobileHud := preload("res://ui/mobile/mobile_hud.gd") const MobileUiRoot := preload("res://ui/mobile/mobile_ui_root.gd") const MobileMenuDrawer := preload("res://ui/mobile/mobile_menu_drawer.gd") const MobileWindowHost := preload("res://ui/mobile/mobile_window_host.gd") const PlayerCtl := preload("res://player_controller.gd") class FakeClient extends Node: signal party_changed signal vitals_changed(vid: int) var main := 7 var entity := {"vid": 7, "name": "测试角色", "race": 0, "level": 12} var party := [{"vid": 7, "name": "测试角色", "hp_pct": 100, "leader": true}] func get_main_vid() -> int: return main func get_entity(_vid: int) -> Dictionary: return entity func get_party() -> Array: return party func set_target(_vid: int) -> bool: return true var _fail := 0 func _ck(condition: bool, message: String) -> void: if not condition: _fail += 1 printerr("FAIL: " + message) func _init() -> void: await _run() if _fail == 0: print("PASS: mobile_ui_test (profile / landscape HUD / menu / controls)") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) func _run() -> void: await process_frame _ck(UiProfile.parse("mobile") == UiProfile.Mode.MOBILE, "profile parses mobile") _ck(UiProfile.parse("desktop") == UiProfile.Mode.DESKTOP, "profile parses desktop") _ck(UiProfile.parse("auto") == UiProfile.Mode.AUTO, "profile parses auto") _ck(MobileUiRoot.requires_landscape(Vector2(844, 390)) == false, "landscape design size is accepted") _ck(MobileUiRoot.requires_landscape(Vector2(390, 844)), "portrait viewport is rejected") var safe := MobileWindowHost.safe_rect_for(Vector2(844, 390), Rect2(20, 8, 804, 374)) _ck(safe == Rect2(20, 8, 804, 374), "reported safe area is preserved") _ck(MobileWindowHost.safe_rect_for(Vector2(844, 390), Rect2(0, 0, 1920, 1080)) == Rect2(0, 0, 844, 390), "physical-screen safe area is rejected for a logical viewport") var client := FakeClient.new() get_root().add_child(client) var root := MobileUiRoot.new() get_root().add_child(root) root.setup(null, null, client) root.bind_controls({"client": client}) await process_frame _ck(root.hud_view is MobileHud, "mobile root owns mobile HUD") _ck(root.input_overlay != null, "mobile root owns touch overlay") _ck(root.hud_view._player_name.text == "测试角色", "avatar card uses server player name") root.set_vitals(50, 100, 20, 40) _ck(root.hud_view._hp_bar.value == 50 and root.hud_view._mp_bar.value == 20, "mobile HUD updates HP/SP through compatibility setters") root.set_target("目标", 35) _ck(root.hud_view._target_panel.visible and root.hud_view._target_bar.value == 35, "mobile HUD shows target status") root.menu_drawer.open() _ck(root.menu_drawer.is_open(), "mobile menu opens") _ck(MobileMenuDrawer.CATEGORIES.size() == 5, "mobile menu has five top-level categories") _ck(not MobileMenuDrawer.ITEMS["社交"].has("队伍"), "social menu has no party entry") root.menu_drawer.close() var pc := PlayerCtl.new() get_root().add_child(pc) pc.player = Node3D.new() get_root().add_child(pc.player) root.input_overlay.bind_controls(pc, null, null, null) await process_frame var touch_pos: Vector2 = root.input_overlay._joystick.get_global_rect().get_center() var ui_down := InputEventScreenTouch.new() ui_down.index = 21 ui_down.pressed = true ui_down.position = touch_pos root.input_overlay._input(ui_down) _ck(pc._mobile_ui_touches.has(21), "UI touch is registered by world-touch arbiter") var world_down := InputEventScreenTouch.new() world_down.index = 22 world_down.pressed = true world_down.position = Vector2(600, 210) pc._on_touch(world_down) var world_up := InputEventScreenTouch.new() world_up.index = 22 world_up.pressed = false world_up.position = world_down.position pc._on_touch(world_up) _ck(pc._multi_gesture, "joystick plus world touch cannot become tap-to-move") var ui_up := InputEventScreenTouch.new() ui_up.index = 21 ui_up.pressed = false ui_up.position = touch_pos root.input_overlay._input(ui_up) _ck(not pc._mobile_ui_touches.has(21), "UI touch arbiter clears on release") pc.set_mobile_axis(Vector2(2, 0)) _ck(pc._mobile_axis == Vector2(1, 0), "mobile movement axis is normalized") pc.clear_mobile_input() _ck(pc._mobile_axis == Vector2.ZERO, "mobile movement axis clears on cancellation") root.queue_free() pc.queue_free() client.queue_free()