feat: complete mobile UI implementation
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# 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()
|
||||
Reference in New Issue
Block a user