feat: complete mobile UI implementation

This commit is contained in:
shenlei
2026-09-04 19:54:33 +09:00
parent 9a4a044da5
commit b19d5b5dd3
132 changed files with 6643 additions and 180 deletions
+73 -13
View File
@@ -55,6 +55,7 @@ var _price_dialog: Control = null
var _server_prices: Dictionary = {} # vnum -> last price returned by MyShopPriceList
var item_mouse: Node
const INVENTORY_WINDOW := 1
var _mobile_mode := false
func setup(m2client: Node, ui_manager: CanvasLayer, proto_node: Node = null, assets := "") -> void:
client = m2client
@@ -80,6 +81,15 @@ func setup(m2client: Node, ui_manager: CanvasLayer, proto_node: Node = null, ass
func is_open() -> bool:
return not _win.is_empty() and is_instance_valid(_win.get("root"))
func get_mobile_window() -> Control:
return _win.get("root") if not _win.is_empty() else null
func set_mobile_mode(enabled: bool) -> void:
_mobile_mode = enabled
if is_open():
_add_mobile_width(_win["root"])
_refresh()
func toggle() -> void:
if is_open(): close()
else: open()
@@ -91,7 +101,14 @@ func close() -> void:
if item_mouse.has_method("is_attached") and item_mouse.is_attached():
item_mouse.cancel()
if is_open():
ui.close(_win["root"])
var root: Control = _win["root"]
# An external mobile window is owned by UiManager's presentation stack;
# calling ui.close() here would call this method again through its close
# callback. Hide it and let UiManager restore the original parent.
if _mobile_mode and bool(root.get_meta("mobile_external", false)):
root.visible = false
else:
ui.close(root)
_win = {}
_cells.clear()
_stock.clear()
@@ -113,6 +130,8 @@ func open() -> void:
_index_cells(root)
_overlay_name_edit(root)
_add_inv_panel(root)
if _mobile_mode and ui and ui.has_method("reflow_mobile_window"):
ui.reflow_mobile_window(root)
_wire_buttons(root)
_refresh()
@@ -132,6 +151,8 @@ func _index_cells(root: Node) -> void:
var idx := slot
n.gui_input.connect(func(e: InputEvent) -> void:
if e is InputEventMouseButton and e.button_index == MOUSE_BUTTON_LEFT and e.pressed:
_on_slot_clicked(idx)
elif e is InputEventScreenTouch and e.pressed:
_on_slot_clicked(idx))
# 同时支持从右侧背包候选区拖到空货位;点击选取仍保留给触屏/无鼠标场景。
n.set_drag_forwarding(Callable(self, "_no_drag_data"),
@@ -201,6 +222,7 @@ func _add_inv_panel(root: Control) -> void:
bg.set_border_width_all(1)
panel.add_theme_stylebox_override("panel", bg)
root.add_child(panel)
_add_mobile_width(root)
var title := Label.new()
title.text = "배낭 (클릭 → 집기)"
title.position = Vector2(10, 8)
@@ -219,6 +241,13 @@ func _add_inv_panel(root: Control) -> void:
_status.add_theme_color_override("font_color", Color(1, 0.9, 0.5))
panel.add_child(_status)
func _add_mobile_width(root: Control) -> void:
if not _mobile_mode or root == null:
return
# Keep the candidate list inside the hosted/scaled root instead of leaving
# it as an overflow panel outside the mobile safe area.
root.size.x = maxf(root.size.x, 674.0)
# --- 명칭 --------------------------------------------------------------
func _name_of(vnum: int) -> String:
@@ -375,9 +404,16 @@ func _on_slot_clicked(slot: int) -> void:
func _ask_price(slot: int) -> void:
_dismiss_price_dialog()
var dlg := Panel.new()
dlg.set_anchors_preset(Control.PRESET_CENTER)
dlg.position = Vector2(-120, -70)
dlg.set_anchors_preset(Control.PRESET_TOP_LEFT)
dlg.size = Vector2(240, 128)
dlg.position = (_win["root"].size - dlg.size) * 0.5
dlg.z_index = 200
dlg.mouse_filter = Control.MOUSE_FILTER_STOP
dlg.set_meta("mobile_modal", _mobile_mode)
# UiManager closes mobile modals before the feature window itself. Keep the
# controller's reference in sync when Android back/ESC hides this custom
# panel instead of going through the Cancel button.
dlg.set_meta("mobile_modal_close", Callable(self, "_dismiss_price_dialog"))
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.06, 0.06, 0.09, 0.98)
sb.border_color = Color(0.7, 0.55, 0.22, 0.9)
@@ -387,30 +423,54 @@ func _ask_price(slot: int) -> void:
t.text = "판매 가격"
t.position = Vector2(12, 10)
dlg.add_child(t)
var spin := SpinBox.new()
spin.min_value = 1
spin.max_value = 2000000000
spin.step = 1
var default_price := int(_server_prices.get(int(_picked["vnum"]), 1))
if _stock.has(slot):
default_price = int(_stock[slot].get("price", default_price))
spin.value = default_price
spin.position = Vector2(12, 40)
spin.size = Vector2(216, 28)
dlg.add_child(spin)
var price_input: Control
if _mobile_mode:
var edit := LineEdit.new()
edit.name = "PriceInput"
edit.text = str(default_price)
edit.placeholder_text = "输入价格"
edit.max_length = 10
edit.virtual_keyboard_type = LineEdit.KEYBOARD_TYPE_NUMBER
edit.position = Vector2(12, 40)
edit.size = Vector2(216, 32)
edit.text_submitted.connect(func(_text: String) -> void:
_place(slot, _parse_price(edit)))
price_input = edit
else:
var spin := SpinBox.new()
spin.min_value = 1
spin.max_value = 2000000000
spin.step = 1
spin.value = default_price
spin.position = Vector2(12, 40)
spin.size = Vector2(216, 28)
price_input = spin
dlg.add_child(price_input)
var ok := Button.new()
ok.text = "확인"
ok.position = Vector2(60, 86)
ok.pressed.connect(func() -> void: _place(slot, int(spin.value)))
ok.pressed.connect(func() -> void:
_place(slot, _parse_price(price_input)))
dlg.add_child(ok)
var cancel := Button.new()
cancel.text = "취소"
cancel.position = Vector2(130, 86)
cancel.pressed.connect(_dismiss_price_dialog)
dlg.add_child(cancel)
_win["root"].get_parent().add_child(dlg)
_win["root"].add_child(dlg)
_price_dialog = dlg
func _parse_price(input: Control) -> int:
if input is SpinBox:
return int((input as SpinBox).value)
if input is LineEdit:
var raw := (input as LineEdit).text.strip_edges()
return int(raw) if raw.is_valid_int() else 0
return 0
func _dismiss_price_dialog() -> void:
if is_instance_valid(_price_dialog):
_price_dialog.queue_free()