Files
mtgodot-poc/project/ui/safebox_ui.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

766 lines
26 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SafeboxUI (P8) —— 仓库窗(1:1 对齐 40250 `root/uisafebox.py` 与 `uiscript/safeboxwindow.py`)。
#
# var bu := preload("res://ui/safebox_ui.gd").new()
# add_child(bu)
# bu.setup(m2client, canvas_parent, proto)
#
# 采用 40250 经典 176×373 羊皮纸金属 Board 边框与 TitleBar。
# 5×9 格子(45 格/页),使用 Slot_Base.sub 底图。
# 底部单选分页按键(I / II / III)、修改密码与关闭大按钮。
# 右键格子道具直接取出至背包;在背包中右键直接存入仓库;支持鼠标拖放。
extends Node
const ItemTooltip = preload("res://ui/item_tooltip.gd")
const UiKit = preload("res://ui_kit.gd")
const UiBuild = preload("res://ui/ui_build.gd")
const UiAssets = preload("res://ui/ui_assets.gd")
var client: Node
var proto: Node
var _tooltip_builder: RefCounted
var _assets_root := ""
var _root: Control
var _board_panel: Control
var _list: VBoxContainer
var _gold: Label
var _title: Label
var _status: Label
var _password_dialog: ConfirmationDialog
var _grid_container: Control
var _cells: Dictionary = {}
var _page_buttons: Array[Button] = []
var _page_button_container: HBoxContainer
var _change_pw_btn: Button
var _exit_btn: Button
var _page_label: Label
var _page := 0
var item_mouse: Node
var _mobile_mode := false
var _mobile_inventory_title: Label
var _mobile_inventory_scroll: ScrollContainer
var _mobile_inventory_list: VBoxContainer
var _mobile_deposit_pending := {}
var _change_pw_dialog: ConfirmationDialog
const SAFE_PAGE_SLOTS := 45
const SAFE_WINDOW := 3
const INVENTORY_WINDOW := 1
const USE_SAFEBOX_LIMIT_RANGE_M := 10.0
var ui: CanvasLayer
var _open_char_pos := Vector3.ZERO
var _has_open_pos := false
func setup(m2client: Node, parent: Node, proto_node: Node = null, assets_override := "") -> void:
client = m2client
proto = proto_node
ui = parent as CanvasLayer
_assets_root = assets_override if assets_override != "" else AssetRoot.path()
_tooltip_builder = ItemTooltip.new()
_tooltip_builder.setup(_assets_root, "en", proto)
_build(parent)
if client and client.has_signal("safebox_opened"):
client.safebox_opened.connect(func(_size): open())
if client and client.has_signal("safebox_closed"):
client.safebox_closed.connect(_close_local)
if client and client.has_signal("safebox_changed"):
client.safebox_changed.connect(refresh)
if client and client.has_signal("inventory_changed"):
client.inventory_changed.connect(_refresh_inventory_candidates)
if client and client.has_signal("safebox_password_required"):
client.safebox_password_required.connect(func(): _ask_password("safebox"))
if client and client.has_signal("safebox_wrong_password"):
client.safebox_wrong_password.connect(func():
if is_instance_valid(_status):
_status.text = "仓库密码错误")
if ui and ui.has_signal("window_closed"):
ui.window_closed.connect(func(w):
if w == _root and is_open():
close()
)
func is_open() -> bool:
return _root != null and _root.visible
func _get_main_char_pos() -> Variant:
if client == null:
return null
var vid := 0
if client.has_method("get_main_vid"):
vid = int(client.get_main_vid())
if vid != 0 and client.has_method("get_entity"):
var e: Dictionary = client.get_entity(vid)
if not e.is_empty() and e.has("pos"):
return e["pos"]
return null
func _process(_delta: float) -> void:
if not is_open() or not _has_open_pos:
return
var cur = _get_main_char_pos()
if cur is Vector3:
if _open_char_pos.distance_to(cur) > USE_SAFEBOX_LIMIT_RANGE_M:
close()
func open() -> void:
if _root:
var vp_size := Vector2(800.0, 600.0)
if _root.is_inside_tree():
vp_size = _root.get_viewport_rect().size
elif ui and ui.is_inside_tree():
vp_size = ui.get_viewport().get_visible_rect().size
_root.position.x = maxf(0.0, (vp_size.x - _root.size.x) / 2.0)
_root.position.y = maxf(0.0, (vp_size.y - _root.size.y) / 2.0)
_root.visible = true
if ui and ui.has_method("open"):
ui.open(_root)
var p = _get_main_char_pos()
if p is Vector3:
_open_char_pos = p
_has_open_pos = true
refresh()
func toggle_window() -> void:
if is_open():
close()
else:
open()
func set_mobile_mode(enabled: bool) -> void:
_mobile_mode = enabled
_apply_mobile_layout()
_refresh_inventory_candidates()
func get_mobile_window() -> Control:
return _root
func close() -> void:
# 对齐原版 SafeboxWindow.Close():先发 /safebox_close 让服务器同步关仓
if client and client.has_method("safebox_close") and is_open():
client.safebox_close()
_close_local()
func _close_local() -> void:
_has_open_pos = false
if item_mouse and item_mouse.has_method("unregister_owner"):
item_mouse.unregister_owner(self)
if item_mouse.has_method("is_attached") and item_mouse.is_attached():
item_mouse.cancel()
if _root:
_root.visible = false
if ui and "_stack" in ui:
ui._stack.erase(_root)
if ui.has_method("_restack"):
ui._restack()
_page = 0
_mobile_deposit_pending.clear()
if is_instance_valid(_password_dialog):
_password_dialog.queue_free()
_password_dialog = null
func deposit(inv_window: int, inv_cell: int) -> void:
if not is_open() or inv_window < 0 or inv_cell < 0:
return
if inv_window == INVENTORY_WINDOW and _mobile_deposit_pending.has(inv_cell):
if is_instance_valid(_status):
_status.text = "该物品的存入请求已发送"
return
var safe_pos := _next_free_slot()
if safe_pos < 0:
if is_instance_valid(_status):
_status.text = "仓库已满"
return
if not client.has_method("safebox_checkin"):
if is_instance_valid(_status):
_status.text = "存入请求发送失败"
return
if not client.safebox_checkin(safe_pos, inv_window, inv_cell):
if is_instance_valid(_status):
_status.text = "存入请求发送失败"
return
if inv_window == INVENTORY_WINDOW:
_mobile_deposit_pending[inv_cell] = true
if is_instance_valid(_status):
_status.text = "已发送存入请求"
func _next_free_slot() -> int:
var used := {}
if client and client.has_method("get_safebox_items"):
for it in client.get_safebox_items():
used[int(it.get("cell", -1))] = true
var cap: int = maxi(1, int(client.get_safebox_size()) if client and client.has_method("get_safebox_size") else 1) * 45
for i in range(cap):
if not used.has(i):
return i
return -1
func _name_of(vnum: int) -> String:
if proto and proto.has_method("item"):
var d: Dictionary = proto.item(vnum)
var n := String(d.get("locale_name", d.get("name", "")))
if n != "":
return n
return "#%d" % vnum
func _tooltip_for(item: Dictionary) -> String:
var vnum := int(item.get("vnum", 0))
if _tooltip_builder:
var pd: Dictionary = proto.item(vnum) if proto and proto.has_method("item") else {}
return _tooltip_builder.format(vnum, int(item.get("count", 1)), pd, item)
return "%s\n#%d ×%d" % [_name_of(vnum), vnum, int(item.get("count", 1))]
func refresh() -> void:
if client == null:
return
_root.visible = client.is_safebox_open() if client.has_method("is_safebox_open") else true
if not _root.visible:
return
if is_instance_valid(_status):
_status.text = ""
var pages := maxi(1, int(client.get_safebox_size()))
_page = clampi(_page, 0, pages - 1)
if is_instance_valid(_title):
_title.text = "仓库" if not _mobile_mode else "仓库(第 %d/%d 页)" % [_page + 1, pages]
if is_instance_valid(_page_label):
_page_label.text = "%d / %d" % [_page + 1, pages]
if is_instance_valid(_gold):
_gold.text = "仓库金币: %d" % client.get_safebox_gold()
for i in _page_buttons.size():
var btn: Button = _page_buttons[i]
btn.visible = i < pages
btn.set_pressed_no_signal(i == _page)
_render_grid()
for c in _list.get_children():
c.queue_free()
var items: Array = client.get_safebox_items() if client and client.has_method("get_safebox_items") else []
if items.is_empty():
var e := Label.new()
e.text = "(空)"
_list.add_child(e)
else:
for it in items:
_list.add_child(_row(it))
_refresh_inventory_candidates()
func _render_grid() -> void:
for pos in _cells:
var cell: Control = _cells[pos]
_clear_grid_cell(cell)
var items: Array = client.get_safebox_items() if client and client.has_method("get_safebox_items") else []
for it in items:
var absolute_pos := int(it.get("cell", -1))
var pos := absolute_pos - _page * SAFE_PAGE_SLOTS
if not _cells.has(pos):
continue
var cell: Control = _cells[pos]
_fill_grid_cell(cell, it)
func _clear_grid_cell(cell: Control) -> void:
cell.tooltip_text = ""
cell.set_meta("vnum", 0)
cell.set_meta("count", 0)
for c in cell.get_children():
c.queue_free()
func _fill_grid_cell(cell: Control, item: Dictionary) -> void:
var vnum := int(item.get("vnum", 0))
var count := int(item.get("count", 1))
cell.set_meta("vnum", vnum)
cell.set_meta("count", count)
cell.tooltip_text = _tooltip_for(item)
var tex := _icon(vnum)
if tex != null:
var tr := TextureRect.new()
tr.name = "Icon"
tr.texture = tex
tr.set_anchors_preset(Control.PRESET_FULL_RECT)
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
tr.mouse_filter = Control.MOUSE_FILTER_IGNORE
cell.add_child(tr)
else:
var lbl := Label.new()
lbl.name = "Icon"
lbl.text = _name_of(vnum).substr(0, 4)
lbl.add_theme_font_size_override("font_size", 9)
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
cell.add_child(lbl)
if count > 1:
var cnt_lbl := Label.new()
cnt_lbl.name = "Count"
cnt_lbl.text = str(count)
cnt_lbl.position = Vector2(2, 16)
cnt_lbl.size = Vector2(28, 14)
cnt_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
cnt_lbl.add_theme_font_size_override("font_size", 10)
cnt_lbl.add_theme_color_override("font_color", Color(1.0, 1.0, 0.8))
cnt_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
cell.add_child(cnt_lbl)
func _icon(vnum: int) -> Texture2D:
if _assets_root == "":
return null
var rel := "icon/item/%05d.tga" % vnum
var tex := UiAssets.load_tex(_assets_root, rel)
if tex == null:
rel = "icon/item/%05d.tga" % ((vnum / 10) * 10)
tex = UiAssets.load_tex(_assets_root, rel)
return tex
func _drop_to_slot(payload: Dictionary, local_pos: int) -> bool:
if payload.is_empty() or not _cells.has(local_pos):
return false
var safe_pos := _page * SAFE_PAGE_SLOTS + local_pos
var cell: Control = _cells[local_pos]
if int(cell.get_meta("vnum", 0)) != 0:
return false
var source_window := int(payload.get("window", -1))
var source_cell := int(payload.get("cell", -1))
if source_cell < 0:
return false
if source_window in [1, 2] and client.has_method("safebox_checkin"):
return client.safebox_checkin(safe_pos, source_window, source_cell)
if source_window == SAFE_WINDOW and client.has_method("safebox_move"):
return client.safebox_move(source_cell, safe_pos, 1)
return false
func _on_cell_input(local_pos: int, ev: InputEvent) -> void:
if not (ev is InputEventMouseButton and ev.pressed):
return
if ev.button_index == MOUSE_BUTTON_RIGHT:
var safe_pos := _page * SAFE_PAGE_SLOTS + local_pos
var target := _first_free_inv()
if target < 0:
if is_instance_valid(_status): _status.text = "背包已满"
return
if client and client.has_method("safebox_checkout"):
client.safebox_checkout(safe_pos, 1, target)
elif ev.button_index == MOUSE_BUTTON_LEFT:
if _mobile_mode:
_on_mobile_grid_tap(local_pos)
return
if item_mouse and item_mouse.has_method("attach_item"):
var cell: Control = _cells.get(local_pos, null)
if cell:
var vnum := int(cell.get_meta("vnum", 0))
if vnum > 0:
item_mouse.attach_item(SAFE_WINDOW, _page * SAFE_PAGE_SLOTS + local_pos, vnum,
int(cell.get_meta("count", 1)), null, "safebox")
func _on_mobile_grid_tap(local_pos: int) -> void:
if client == null or not _cells.has(local_pos):
return
var cell: Control = _cells[local_pos]
if int(cell.get_meta("vnum", 0)) == 0 or not client.has_method("safebox_checkout"):
return
var target := _first_free_inv()
if target < 0:
if is_instance_valid(_status):
_status.text = "背包已满"
return
if not client.safebox_checkout(_page * SAFE_PAGE_SLOTS + local_pos, 1, target):
if is_instance_valid(_status):
_status.text = "取出请求发送失败"
func _select_page(index: int) -> void:
_page = index
for i in _page_buttons.size():
var btn: Button = _page_buttons[i]
btn.set_pressed_no_signal(i == _page)
if _page_label:
_page_label.text = "%d / 3" % (_page + 1)
if _title:
_title.text = "仓库" if not _mobile_mode else "仓库(第 %d/3 页)" % (_page + 1)
_render_grid()
func _change_page(delta: int) -> void:
var pages := maxi(1, int(client.get_safebox_size()) if client and client.has_method("get_safebox_size") else 3)
_select_page(posmod(_page + delta, pages))
func _apply_mobile_layout() -> void:
if _root == null:
return
if _mobile_mode:
_root.size = Vector2(688, 400)
_root.position = Vector2(-344, -200)
if _grid_container:
_grid_container.position = Vector2(12, 54)
if _mobile_inventory_title:
_mobile_inventory_title.visible = true
_mobile_inventory_title.position = Vector2(350, 48)
if _mobile_inventory_scroll:
_mobile_inventory_scroll.visible = true
_mobile_inventory_scroll.position = Vector2(350, 76)
_mobile_inventory_scroll.size = Vector2(326, 292)
if _gold:
_gold.visible = true
_gold.position = Vector2(12, 350)
if _change_pw_btn:
_change_pw_btn.position = Vector2(12, 316)
if _exit_btn:
_exit_btn.position = Vector2(174, 316)
else:
# 桌面端也显示背包候选面板(右侧),方便存入物品。
_root.size = Vector2(530, 418)
if _grid_container:
_grid_container.position = Vector2(8, 35)
if _mobile_inventory_title:
_mobile_inventory_title.visible = true
_mobile_inventory_title.position = Vector2(184, 35)
if _mobile_inventory_scroll:
_mobile_inventory_scroll.visible = true
_mobile_inventory_scroll.position = Vector2(184, 56)
_mobile_inventory_scroll.size = Vector2(336, 296)
if _gold:
_gold.visible = false
if _page_button_container:
_page_button_container.position = Vector2(10, 333)
if _change_pw_btn:
_change_pw_btn.position = Vector2(51, 360)
if _exit_btn:
_exit_btn.position = Vector2(51, 381)
func _refresh_inventory_candidates() -> void:
if _mobile_inventory_list == null or not is_open():
return
for child in _mobile_inventory_list.get_children():
child.queue_free()
var inventory: Array = client.get_inventory() if client and client.has_method("get_inventory") else []
var shown := 0
for item in inventory:
var cell := int(item.get("cell", -1))
var vnum := int(item.get("vnum", 0))
if cell < 0 or vnum <= 0 or _mobile_deposit_pending.has(cell):
continue
var button := Button.new()
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.text = "%s ×%d · 点按存入" % [_name_of(vnum), int(item.get("count", 1))]
button.custom_minimum_size = Vector2(316, 34)
button.tooltip_text = _tooltip_for(item)
var captured_cell := cell
button.pressed.connect(func() -> void: deposit(INVENTORY_WINDOW, captured_cell))
_mobile_inventory_list.add_child(button)
shown += 1
if shown == 0:
var empty := Label.new()
empty.text = "(没有可存入物品)"
empty.add_theme_font_size_override("font_size", 12)
_mobile_inventory_list.add_child(empty)
func _ask_password(kind: String) -> void:
if is_instance_valid(_password_dialog):
_password_dialog.queue_free()
_password_dialog = ConfirmationDialog.new()
_password_dialog.title = "商城密码" if kind == "mall" else "仓库密码"
_password_dialog.dialog_text = "请输入 16 位密码"
_password_dialog.ok_button_text = "确认"
_password_dialog.cancel_button_text = "取消"
var edit := LineEdit.new()
edit.secret = true
edit.max_length = 6
edit.placeholder_text = "密码"
edit.virtual_keyboard_type = LineEdit.KEYBOARD_TYPE_NUMBER
edit.custom_minimum_size = Vector2(360, 44) if _mobile_mode else Vector2(220, 28)
_password_dialog.add_child(edit)
_password_dialog.confirmed.connect(func() -> void:
var sent: bool = client.safebox_password(edit.text) if kind == "safebox" else client.mall_password(edit.text)
if not sent and is_instance_valid(_status):
_status.text = "密码格式无效"
_password_dialog.queue_free()
_password_dialog = null)
_password_dialog.canceled.connect(func() -> void:
_password_dialog.queue_free()
_password_dialog = null)
if _mobile_mode:
_password_dialog.set_meta("mobile_modal", true)
_password_dialog.set_meta("mobile_title", "商城密码" if kind == "mall" else "仓库密码")
_password_dialog.min_size = Vector2i(460, 230)
_password_dialog.size = Vector2i(460, 230)
_root.add_child(_password_dialog)
if _mobile_mode:
_password_dialog.popup_centered(Vector2i(460, 230))
else:
_password_dialog.popup_centered()
func _ask_change_password() -> void:
if not is_open():
return
if is_instance_valid(_change_pw_dialog):
_change_pw_dialog.queue_free()
_change_pw_dialog = ConfirmationDialog.new()
_change_pw_dialog.title = "修改仓库密码"
_change_pw_dialog.dialog_text = "旧密码 / 新密码(1~6 位)/ 再输入一次新密码"
_change_pw_dialog.ok_button_text = "确认"
_change_pw_dialog.cancel_button_text = "取消"
var box := VBoxContainer.new()
var edits: Array[LineEdit] = []
for hint in ["旧密码", "新密码", "确认新密码"]:
var row := HBoxContainer.new()
var lbl := Label.new()
lbl.text = hint
lbl.custom_minimum_size = Vector2(96, 0)
row.add_child(lbl)
var edit := LineEdit.new()
edit.secret = true
edit.max_length = 6
edit.placeholder_text = hint
edit.virtual_keyboard_type = LineEdit.KEYBOARD_TYPE_NUMBER
edit.custom_minimum_size = Vector2(360, 44) if _mobile_mode else Vector2(160, 28)
row.add_child(edit)
box.add_child(row)
edits.append(edit)
_change_pw_dialog.add_child(box)
_change_pw_dialog.confirmed.connect(func() -> void:
var old_pw := edits[0].text
var new_pw := edits[1].text
if new_pw != edits[2].text:
if is_instance_valid(_status):
_status.text = "两次输入的新密码不一致"
elif old_pw.is_empty() or new_pw.is_empty() or new_pw.length() > 6 or old_pw.length() > 6:
if is_instance_valid(_status):
_status.text = "密码须为 16 位"
elif not client.safebox_change_password(old_pw, new_pw):
if is_instance_valid(_status):
_status.text = "修改密码请求发送失败"
else:
if is_instance_valid(_status):
_status.text = "已提交修改密码请求"
_change_pw_dialog.queue_free()
_change_pw_dialog = null)
_change_pw_dialog.canceled.connect(func() -> void:
_change_pw_dialog.queue_free()
_change_pw_dialog = null)
if _mobile_mode:
_change_pw_dialog.set_meta("mobile_modal", true)
_change_pw_dialog.set_meta("mobile_title", "修改仓库密码")
_change_pw_dialog.min_size = Vector2i(560, 320)
_change_pw_dialog.size = Vector2i(560, 320)
_root.add_child(_change_pw_dialog)
if _mobile_mode:
_change_pw_dialog.popup_centered(Vector2i(560, 320))
else:
_change_pw_dialog.popup_centered()
func _row(it: Dictionary) -> Control:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(300, 0)
var nm := Label.new()
nm.text = "%s ×%d" % [_name_of(int(it.get("vnum", 0))), int(it.get("count", 1))]
nm.custom_minimum_size = Vector2(220, 0)
nm.add_theme_font_size_override("font_size", 12)
row.add_child(nm)
var out := Button.new()
out.text = "取出"
var cell := int(it.get("cell", 0))
out.pressed.connect(func() -> void:
var target := _first_free_inv()
if target < 0:
if is_instance_valid(_status): _status.text = "背包已满"
return
if not client.safebox_checkout(cell, 1, target):
if is_instance_valid(_status): _status.text = "取出请求发送失败")
row.add_child(out)
return row
func _first_free_inv() -> int:
var used := {}
if client and client.has_method("get_inventory"):
for it in client.get_inventory():
used[int(it.get("cell", -1))] = true
for i in range(90):
if not used.has(i):
return i
return -1
func _build(parent: Node) -> void:
_root = Control.new()
_root.name = "SafeboxWindow"
_root.size = Vector2(176, 418)
parent.add_child(_root)
var vp_size := Vector2(800.0, 600.0)
if _root.is_inside_tree():
vp_size = _root.get_viewport_rect().size
elif ui and ui.is_inside_tree():
vp_size = ui.get_viewport().get_visible_rect().size
_root.position.x = maxf(0.0, (vp_size.x - _root.size.x) / 2.0)
_root.position.y = maxf(0.0, (vp_size.y - _root.size.y) / 2.0)
_root.visible = false
_root.set_meta("is_titlebar", true)
# 1. 经典羊皮纸金属 Board 边框
var board_np := UiKit.board(_assets_root, "board", 32, 128)
if board_np and board_np.texture:
board_np.set_anchors_preset(Control.PRESET_FULL_RECT)
board_np.mouse_filter = Control.MOUSE_FILTER_IGNORE
_root.add_child(board_np)
else:
var panel := Panel.new()
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.06, 0.08, 0.09, 0.97)
sb.set_corner_radius_all(4)
panel.add_theme_stylebox_override("panel", sb)
_root.add_child(panel)
# 2. 官方 TitleBar
var titlebar := UiBuild._titlebar(_assets_root, 161, {})
titlebar.name = "TitleBar"
titlebar.position = Vector2(8, 7)
_root.add_child(titlebar)
_title = Label.new()
_title.name = "TitleName"
_title.text = "仓库"
_title.position = Vector2(0, 0)
_title.size = Vector2(161, 23)
_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_title.add_theme_color_override("font_color", Color(1.0, 0.9, 0.6))
_title.mouse_filter = Control.MOUSE_FILTER_IGNORE
titlebar.add_child(_title)
var close_btn: Button = titlebar.find_child("CloseButton", true, false)
if close_btn:
close_btn.pressed.connect(close)
# 3. 5×9 GridSlotWindow (8, 35)
_grid_container = Control.new()
_grid_container.name = "wndItem"
_grid_container.position = Vector2(8, 35)
_grid_container.size = Vector2(160, 288)
_root.add_child(_grid_container)
var slot_base_tex := UiAssets.load_tex(_assets_root, "ETC/ymir work/ui/public/Slot_Base.sub")
for pos in SAFE_PAGE_SLOTS:
var col := pos % 5
var row := pos / 5
var cell := Panel.new()
cell.name = "slot_%d" % pos
cell.position = Vector2(col * 32, row * 32)
cell.custom_minimum_size = Vector2(32, 32)
cell.size = Vector2(32, 32)
cell.set_meta("safe_pos", pos)
cell.mouse_filter = Control.MOUSE_FILTER_STOP
if slot_base_tex:
var sbt := StyleBoxTexture.new()
sbt.texture = slot_base_tex
cell.add_theme_stylebox_override("panel", sbt)
else:
var sbf := StyleBoxFlat.new()
sbf.bg_color = Color(0.12, 0.12, 0.15)
cell.add_theme_stylebox_override("panel", sbf)
cell.gui_input.connect(func(e: InputEvent): _on_cell_input(pos, e))
if item_mouse and item_mouse.has_method("register_target"):
item_mouse.register_target(cell,
func(payload: Dictionary): return _drop_to_slot(payload, pos), self)
_grid_container.add_child(cell)
_cells[pos] = cell
# 4. 单选分页按键 (I, II, III) - 40250 原版在 y = 333 (418 - 85)
_page_button_container = HBoxContainer.new()
_page_button_container.name = "PageButtons"
_page_button_container.position = Vector2(10, 333)
_page_button_container.size = Vector2(156, 20)
_page_button_container.alignment = BoxContainer.ALIGNMENT_CENTER
_page_button_container.add_theme_constant_override("separation", 6)
_root.add_child(_page_button_container)
for p_idx in 3:
var p_btn := Button.new()
p_btn.name = "PageBtn_%d" % p_idx
p_btn.text = ["I", "II", "III"][p_idx]
p_btn.custom_minimum_size = Vector2(44, 18)
p_btn.toggle_mode = true
var t1 := UiAssets.load_tex(_assets_root, "ETC/ymir work/ui/game/windows/tab_button_middle_01.sub")
var t2 := UiAssets.load_tex(_assets_root, "ETC/ymir work/ui/game/windows/tab_button_middle_02.sub")
var t3 := UiAssets.load_tex(_assets_root, "ETC/ymir work/ui/game/windows/tab_button_middle_03.sub")
if t1:
var sb1 := StyleBoxTexture.new(); sb1.texture = t1
p_btn.add_theme_stylebox_override("normal", sb1)
if t2:
var sb2 := StyleBoxTexture.new(); sb2.texture = t2
p_btn.add_theme_stylebox_override("hover", sb2)
if t3:
var sb3 := StyleBoxTexture.new(); sb3.texture = t3
p_btn.add_theme_stylebox_override("pressed", sb3)
p_btn.pressed.connect(func(): _select_page(p_idx))
_page_button_container.add_child(p_btn)
_page_buttons.append(p_btn)
# 5. 修改密码与关闭大按钮 - 40250 原版居中纵向排列
_change_pw_btn = _create_large_button("ChangePasswordButton", "修改密码", Vector2(51, 360), func(): _ask_change_password())
_change_pw_btn.size = Vector2(74, 21)
_root.add_child(_change_pw_btn)
_exit_btn = _create_large_button("ExitButton", "关闭", Vector2(51, 381), func(): close())
_exit_btn.size = Vector2(74, 21)
_root.add_child(_exit_btn)
# 兼容字段(测试与移动端使用)
_gold = Label.new()
_gold.name = "Gold"
_gold.visible = false
_root.add_child(_gold)
_page_label = Label.new()
_page_label.name = "PageLabel"
_page_label.visible = false
_root.add_child(_page_label)
_status = Label.new()
_status.name = "Status"
_status.visible = false
_root.add_child(_status)
_list = VBoxContainer.new()
_list.name = "List"
_list.visible = false
_root.add_child(_list)
# 移动端适配容器
_mobile_inventory_title = Label.new()
_mobile_inventory_title.text = "背包候选 · 点按存入"
_mobile_inventory_title.position = Vector2(350, 48)
_mobile_inventory_title.visible = false
_root.add_child(_mobile_inventory_title)
_mobile_inventory_scroll = ScrollContainer.new()
_mobile_inventory_scroll.position = Vector2(350, 76)
_mobile_inventory_scroll.size = Vector2(326, 292)
_mobile_inventory_scroll.visible = false
_root.add_child(_mobile_inventory_scroll)
_mobile_inventory_list = VBoxContainer.new()
_mobile_inventory_list.add_theme_constant_override("separation", 6)
_mobile_inventory_scroll.add_child(_mobile_inventory_list)
_select_page(0)
_apply_mobile_layout()
func _create_large_button(btn_name: String, text: String, pos: Vector2, on_click: Callable) -> Button:
var btn := Button.new()
btn.name = btn_name
btn.text = text
btn.position = pos
btn.size = Vector2(74, 22)
btn.custom_minimum_size = Vector2(74, 22)
btn.add_theme_font_size_override("font_size", 11)
var t1 := UiAssets.load_tex(_assets_root, "ETC/ymir work/ui/public/large_button_01.sub")
var t2 := UiAssets.load_tex(_assets_root, "ETC/ymir work/ui/public/large_button_02.sub")
var t3 := UiAssets.load_tex(_assets_root, "ETC/ymir work/ui/public/large_button_03.sub")
if t1:
var sb1 := StyleBoxTexture.new(); sb1.texture = t1
btn.add_theme_stylebox_override("normal", sb1)
if t2:
var sb2 := StyleBoxTexture.new(); sb2.texture = t2
btn.add_theme_stylebox_override("hover", sb2)
if t3:
var sb3 := StyleBoxTexture.new(); sb3.texture = t3
btn.add_theme_stylebox_override("pressed", sb3)
btn.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
btn.pressed.connect(on_click)
return btn