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
+303
View File
@@ -18,6 +18,7 @@ extends Node
const UiScript = preload("res://ui/uiscript.gd")
const UiBuild = preload("res://ui/ui_build.gd")
const UiAssets = preload("res://ui/ui_assets.gd")
const MobileTouchButton = preload("res://ui/mobile/mobile_touch_button.gd")
# --- EPointTypesm2dev Packet.h),只列本窗要用的 ---
const P_LEVEL := 1
@@ -65,6 +66,14 @@ var uiscript_dir := ""
var _win: Dictionary = {} # { root, nodes }
var _state := "STATUS"
var _mobile_mode := false
var _mobile_root: Control
var _mobile_status_page: Control
var _mobile_emote_page: Control
var _mobile_labels := {}
var _mobile_stats := {}
var _mobile_equipment_labels: Array[Label] = []
var _mobile_hint: Label
func setup(ui_manager: CanvasLayer, m2client: Node, assets := "") -> void:
ui = ui_manager
@@ -80,9 +89,19 @@ func setup(ui_manager: CanvasLayer, m2client: Node, assets := "") -> void:
if client and client.has_signal("entity_main_set"):
client.entity_main_set.connect(func(_v): if is_open(): _refresh())
func set_mobile_mode(enabled: bool) -> void:
if not enabled and _mobile_root != null:
var old_mode := _mobile_mode
_mobile_mode = true
close()
_mobile_mode = old_mode
_mobile_mode = enabled
# --- open / close -----------------------------------------------------
func is_open() -> bool:
if _mobile_mode:
return _mobile_root != null and is_instance_valid(_mobile_root)
return not _win.is_empty() and is_instance_valid(_win.get("root"))
func toggle() -> void:
@@ -90,6 +109,16 @@ func toggle() -> void:
else: open()
func close() -> void:
if _mobile_mode:
if _mobile_root and is_instance_valid(_mobile_root) and ui:
ui.close(_mobile_root)
_mobile_root = null
_mobile_status_page = null
_mobile_emote_page = null
_mobile_labels.clear()
_mobile_stats.clear()
_mobile_equipment_labels.clear()
return
if is_open():
ui.close(_win["root"])
_win = {}
@@ -97,6 +126,9 @@ func close() -> void:
func open(state := "") -> void:
if state != "" and state in STATES:
_state = state
if _mobile_mode:
_open_mobile()
return
if is_open():
_set_state(_state)
_refresh()
@@ -113,6 +145,277 @@ func open(state := "") -> void:
_set_state(_state)
_refresh()
func _open_mobile() -> void:
if is_open():
_mobile_set_state(_state)
_refresh_mobile()
return
_mobile_root = Control.new()
_mobile_root.name = "MobileCharacterWindow"
_mobile_root.size = Vector2(760, 340)
_mobile_root.set_meta("mobile_title", "角色")
_mobile_root.mouse_filter = Control.MOUSE_FILTER_STOP
_build_mobile_view()
ui.open(_mobile_root)
_mobile_set_state(_state)
_refresh_mobile()
func _build_mobile_view() -> void:
var panel := Panel.new()
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
panel.add_theme_stylebox_override("panel", _mobile_panel_style())
_mobile_root.add_child(panel)
var tabs := HBoxContainer.new()
tabs.position = Vector2(14, 48)
tabs.size = Vector2(250, 38)
tabs.add_theme_constant_override("separation", 6)
_mobile_root.add_child(tabs)
var status_tab := MobileTouchButton.new()
status_tab.setup("属性 / 装备", Color(0.65, 0.78, 0.98, 0.95))
status_tab.set_rect_style()
status_tab.custom_minimum_size = Vector2(118, 36)
status_tab.size = Vector2(118, 36)
status_tab.pressed.connect(func(): _mobile_set_state("STATUS"))
tabs.add_child(status_tab)
var emote_tab := MobileTouchButton.new()
emote_tab.setup("表情", Color(0.65, 0.78, 0.98, 0.95))
emote_tab.set_rect_style()
emote_tab.custom_minimum_size = Vector2(86, 36)
emote_tab.size = Vector2(86, 36)
emote_tab.pressed.connect(func(): _mobile_set_state("EMOTICON"))
tabs.add_child(emote_tab)
_mobile_status_page = Control.new()
_mobile_status_page.name = "StatusPage"
_mobile_status_page.position = Vector2(14, 92)
_mobile_status_page.size = Vector2(732, 236)
_mobile_root.add_child(_mobile_status_page)
_build_mobile_profile()
_build_mobile_stats()
_build_mobile_equipment()
_mobile_emote_page = Control.new()
_mobile_emote_page.name = "EmotePage"
_mobile_emote_page.position = Vector2(14, 92)
_mobile_emote_page.size = Vector2(732, 236)
_mobile_root.add_child(_mobile_emote_page)
_build_mobile_emotes()
func _build_mobile_profile() -> void:
var profile := Panel.new()
profile.position = Vector2(0, 0)
profile.size = Vector2(190, 236)
profile.add_theme_stylebox_override("panel", _mobile_panel_style(Color(0.035, 0.055, 0.09, 0.94), Color(0.28, 0.5, 0.78, 0.72)))
_mobile_status_page.add_child(profile)
var title := Label.new()
title.text = "角色信息"
title.position = Vector2(12, 10)
title.add_theme_font_size_override("font_size", 14)
title.add_theme_color_override("font_color", Color(0.95, 0.82, 0.54))
profile.add_child(title)
for data in [["name", "角色"], ["guild", "公会"], ["level", "等级"], ["exp", "经验"], ["hp", "生命"], ["sp", "法力"]]:
var row := HBoxContainer.new()
row.position = Vector2(12, 38 + _mobile_labels.size() * 29)
row.size = Vector2(166, 24)
var caption := Label.new()
caption.text = String(data[1])
caption.custom_minimum_size = Vector2(44, 22)
caption.add_theme_font_size_override("font_size", 11)
row.add_child(caption)
var value := Label.new()
value.name = String(data[0])
value.size_flags_horizontal = Control.SIZE_EXPAND_FILL
value.add_theme_font_size_override("font_size", 11)
value.add_theme_color_override("font_color", Color(0.82, 0.9, 1.0))
row.add_child(value)
profile.add_child(row)
_mobile_labels[String(data[0])] = value
func _build_mobile_stats() -> void:
var stats := Panel.new()
stats.position = Vector2(200, 0)
stats.size = Vector2(270, 236)
stats.add_theme_stylebox_override("panel", _mobile_panel_style(Color(0.035, 0.055, 0.09, 0.94), Color(0.28, 0.5, 0.78, 0.72)))
_mobile_status_page.add_child(stats)
var title := Label.new()
title.text = "属性"
title.position = Vector2(12, 10)
title.add_theme_font_size_override("font_size", 14)
title.add_theme_color_override("font_color", Color(0.95, 0.82, 0.54))
stats.add_child(title)
var points := Label.new()
points.name = "stat_points"
points.position = Vector2(146, 10)
points.size = Vector2(110, 22)
points.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
points.add_theme_font_size_override("font_size", 11)
points.add_theme_color_override("font_color", Color(0.96, 0.78, 0.35))
stats.add_child(points)
_mobile_labels["stat_points"] = points
for i in 4:
var key: String = ["STR", "HTH", "DEX", "INT"][i]
var row := HBoxContainer.new()
row.position = Vector2(12 + (i % 2) * 128, 44 + (i / 2) * 58)
row.size = Vector2(118, 48)
var caption := Label.new()
caption.text = key
caption.custom_minimum_size = Vector2(34, 42)
caption.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
caption.add_theme_font_size_override("font_size", 12)
row.add_child(caption)
var value := Label.new()
value.name = "value"
value.custom_minimum_size = Vector2(34, 42)
value.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
value.add_theme_font_size_override("font_size", 13)
row.add_child(value)
var minus := MobileTouchButton.new()
minus.setup("", Color(0.72, 0.74, 0.82, 0.9))
minus.set_rect_style()
minus.custom_minimum_size = Vector2(24, 42)
minus.size = Vector2(24, 42)
minus.pressed.connect(func(): _send_stat(MINUS_CMD[key]))
row.add_child(minus)
var plus := MobileTouchButton.new()
plus.setup("", Color(0.95, 0.72, 0.3, 0.95))
plus.set_rect_style()
plus.custom_minimum_size = Vector2(24, 42)
plus.size = Vector2(24, 42)
plus.pressed.connect(func(): _send_stat(PLUS_CMD[key]))
row.add_child(plus)
stats.add_child(row)
_mobile_stats[key] = {"value": value, "minus": minus, "plus": plus}
var hint := Label.new()
hint.text = "分配属性点会通过服务器确认"
hint.position = Vector2(12, 174)
hint.size = Vector2(245, 40)
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
hint.add_theme_font_size_override("font_size", 10)
hint.add_theme_color_override("font_color", Color(0.62, 0.72, 0.84))
stats.add_child(hint)
func _build_mobile_equipment() -> void:
var equipment := Panel.new()
equipment.position = Vector2(480, 0)
equipment.size = Vector2(252, 236)
equipment.add_theme_stylebox_override("panel", _mobile_panel_style(Color(0.035, 0.055, 0.09, 0.94), Color(0.28, 0.5, 0.78, 0.72)))
_mobile_status_page.add_child(equipment)
var title := Label.new()
title.text = "装备"
title.position = Vector2(12, 10)
title.add_theme_font_size_override("font_size", 14)
title.add_theme_color_override("font_color", Color(0.95, 0.82, 0.54))
equipment.add_child(title)
var grid := GridContainer.new()
grid.name = "equipment_grid"
grid.columns = 4
grid.position = Vector2(10, 38)
grid.size = Vector2(232, 188)
grid.add_theme_constant_override("h_separation", 5)
grid.add_theme_constant_override("v_separation", 5)
equipment.add_child(grid)
for i in 24:
var slot := Panel.new()
slot.custom_minimum_size = Vector2(53, 36)
var style := StyleBoxFlat.new()
style.bg_color = Color(0.08, 0.11, 0.17, 0.95)
style.border_color = Color(0.32, 0.45, 0.62, 0.8)
style.set_border_width_all(1)
style.set_corner_radius_all(4)
slot.add_theme_stylebox_override("panel", style)
var value := Label.new()
value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
value.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
value.set_anchors_preset(Control.PRESET_FULL_RECT)
value.add_theme_font_size_override("font_size", 9)
value.text = str(i + 1)
value.mouse_filter = Control.MOUSE_FILTER_IGNORE
slot.add_child(value)
grid.add_child(slot)
_mobile_equipment_labels.append(value)
func _build_mobile_emotes() -> void:
var title := Label.new()
title.text = "点击发送表情"
title.position = Vector2(8, 8)
title.add_theme_font_size_override("font_size", 14)
title.add_theme_color_override("font_color", Color(0.95, 0.82, 0.54))
_mobile_emote_page.add_child(title)
var grid := GridContainer.new()
grid.position = Vector2(8, 42)
grid.columns = 5
grid.add_theme_constant_override("h_separation", 8)
grid.add_theme_constant_override("v_separation", 8)
_mobile_emote_page.add_child(grid)
for i in 9:
var button := MobileTouchButton.new()
button.setup("表情 %d" % (i + 1), Color(0.45, 0.7, 0.95, 0.92))
button.set_rect_style()
button.custom_minimum_size = Vector2(112, 52)
button.size = Vector2(112, 52)
var emote_id := i + 1
button.pressed.connect(func():
if client and client.has_method("send_emoticon"):
client.send_emoticon(emote_id)
if _mobile_hint: _mobile_hint.text = "已发送表情 %d" % emote_id)
grid.add_child(button)
_mobile_hint = Label.new()
_mobile_hint.position = Vector2(8, 192)
_mobile_hint.size = Vector2(500, 28)
_mobile_hint.add_theme_font_size_override("font_size", 11)
_mobile_hint.add_theme_color_override("font_color", Color(0.7, 0.8, 0.94))
_mobile_emote_page.add_child(_mobile_hint)
func _mobile_set_state(state: String) -> void:
if state in STATES:
_state = state
if _mobile_status_page:
_mobile_status_page.visible = _state != "EMOTICON"
if _mobile_emote_page:
_mobile_emote_page.visible = _state == "EMOTICON"
func _refresh_mobile() -> void:
if not is_open() or client == null:
return
var pd: Dictionary = client.get_points() if client.has_method("get_points") else {}
var pts: Array = pd.get("points", [])
var get := func(i: int) -> int: return int(pts[i]) if i >= 0 and i < pts.size() else 0
_mobile_set_label("level", "Lv %d" % int(pd.get("level", get.call(P_LEVEL))))
_mobile_set_label("exp", "%d / %d" % [_u32(int(pd.get("exp", get.call(P_EXP)))), _u32(int(pd.get("next_exp", get.call(P_NEXT_EXP))))])
_mobile_set_label("hp", "%d / %d" % [int(pd.get("hp", get.call(P_HP))), int(pd.get("max_hp", get.call(P_MAX_HP)))])
_mobile_set_label("sp", "%d / %d" % [int(pd.get("sp", get.call(P_SP))), int(pd.get("max_sp", get.call(P_MAX_SP)))])
var vid := int(client.get_main_vid()) if client.has_method("get_main_vid") else 0
var ent: Dictionary = client.get_entity(vid) if vid != 0 and client.has_method("get_entity") else {}
_mobile_set_label("name", String(ent.get("name", "冒险者")))
var guild_id := int(ent.get("guild", 0))
var guild_name := String(client.get_guild_name(guild_id)) if guild_id != 0 and client.has_method("get_guild_name") else ""
_mobile_set_label("guild", guild_name if guild_name != "" else "")
_mobile_set_label("stat_points", "可用点数:%d" % max(0, get.call(P_STAT)))
for key in _mobile_stats:
var source: int = int({"STR": P_ST, "HTH": P_HT, "DEX": P_DX, "INT": P_IQ}.get(key, 0))
_mobile_stats[key]["value"].text = str(get.call(int(source)))
_mobile_stats[key]["plus"].visible = get.call(P_STAT) > 0
_mobile_stats[key]["minus"].visible = get.call(P_STAT) > 0
var equipment: Array = client.get_equipment() if client.has_method("get_equipment") else []
for i in _mobile_equipment_labels.size():
var item: Dictionary = equipment[i] if i < equipment.size() else {}
var vnum := int(item.get("vnum", 0))
_mobile_equipment_labels[i].text = ("#%d" % vnum) if vnum != 0 else str(i + 1)
func _mobile_set_label(key: String, value: String) -> void:
if _mobile_labels.has(key) and is_instance_valid(_mobile_labels[key]):
_mobile_labels[key].text = value
func _mobile_panel_style(bg := Color(0.045, 0.065, 0.1, 0.96), border := Color(0.52, 0.68, 0.9, 0.82)) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = bg
style.border_color = border
style.set_border_width_all(1)
style.set_corner_radius_all(8)
return style
func _node(nm: String) -> Control:
if _win.is_empty():
return null