Files
mtgodot-poc/project/ui/shop_ui.gd
T

684 lines
23 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.
# ShopUI (P8) —— NPC 商店窗(1:1 严格对齐 40250 `root/uishop.py` 与 `uiscript/shopdialog.py`)。
#
# var su := preload("res://ui/shop_ui.gd").new()
# add_child(su)
# su.setup(m2client, canvas_parent, proto, item_list) # proto / item_list 可空
#
# 窗口尺寸严格对齐 40250: 184 × 328。
# 边框: 经典羊皮纸金属 Board 边框(UiKit.board)。
# 顶部: 40250 官方 TitleBar (8, 8, width=169),标题“商店”,带右上角关闭按钮。
# 货架: 5 列 × 8 行(40 格),位置 (12, 34),单格 32×32,使用 Slot_Base.sub 底图。
# 底部: 购买/出售中按钮(middle_button_01.sub,宽 61 高 21),或多货架 Tab。
# 交互: 右键商品直接购买;左键在购买模式下弹窗确认;背包中右键或拖拽道具到商店出售。
extends Node
const CursorManager = preload("res://ui/cursor_manager.gd")
const ItemTooltip = preload("res://ui/item_tooltip.gd")
const ItemTooltipView = preload("res://ui/item_tooltip_view.gd")
const UiAssets = preload("res://ui/ui_assets.gd")
const UiKit = preload("res://ui_kit.gd")
const UiBuild = preload("res://ui/ui_build.gd")
const SHOP_SLOT_COUNT := 40 # shop.SHOP_SLOT_COUNT (== SHOP_HOST_ITEM_MAX_NUM)
const ITEM_ANTIFLAG_SELL := 1 << 8
const ITEM_FLAG_COUNT_PER_1GOLD := 1 << 3
const USE_SHOP_LIMIT_RANGE_M := 10.0 # 40250 USE_SHOP_LIMIT_RANGE = 1000cm (uishop.py:483)
var client: Node
var proto: Node
var item_list # ItemListDB (RefCounted)
var _assets_root := ""
var _tooltip_builder: RefCounted
var _tooltip_view: Control = null
var _root: Control
var _tabbar: HBoxContainer
var _grid: GridContainer
var _err: Label
var _sell_quantity: SpinBox
var _active_tab := 0
var _open_char_pos := Vector3.ZERO
var _has_open_pos := false
var item_mouse: Node
var cursor_manager: Node
var audio: Node
var _mode := 1 # 1=BUY, 2=SELL
var _buy_mode_button: Button
var _sell_mode_button: Button
var _sell_drop_target: Button
var _sell_confirm: ConfirmationDialog
var _buy_confirm: ConfirmationDialog
var _mobile_mode := false
var _mobile_sell_scroll: ScrollContainer
var _mobile_sell_list: VBoxContainer
func setup(m2client: Node, parent: Node, proto_node: Node = null, ilist: RefCounted = null) -> void:
client = m2client
proto = proto_node
item_list = ilist
_assets_root = AssetRoot.path()
_tooltip_builder = ItemTooltip.new()
_tooltip_builder.setup(_assets_root, "en", proto)
_build(parent)
if client.has_signal("shop_opened"):
client.shop_opened.connect(func(_v): open())
if client.has_signal("shop_closed"):
client.shop_closed.connect(_close)
if client.has_signal("shop_updated"):
client.shop_updated.connect(_on_shop_slot_updated)
if client.has_signal("shop_error"):
client.shop_error.connect(_on_error)
func is_open() -> bool:
return _root != null and _root.visible
func set_mobile_mode(enabled: bool) -> void:
_mobile_mode = enabled
if _root and _mobile_mode:
_set_mode(_mode)
func get_mobile_window() -> Control:
return _root
func close() -> void:
if client and client.has_method("shop_close") and is_open():
client.shop_close()
_close()
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_SHOP_LIMIT_RANGE_M:
close()
func _icon(vnum: int) -> Texture2D:
if vnum <= 0:
return null
var rel := ""
if item_list and item_list.has_method("has") and item_list.has(vnum):
rel = item_list.icon(vnum)
elif item_list and item_list.has_method("icon"):
rel = item_list.icon(vnum)
if rel != "":
return UiAssets.load_tex(_assets_root, rel)
var exact_rel := "icon/item/%05d.tga" % vnum
var tex := UiAssets.load_tex(_assets_root, exact_rel)
if tex != null:
return tex
var fallback_rel := "icon/item/%05d.tga" % ((vnum / 10) * 10)
return UiAssets.load_tex(_assets_root, fallback_rel)
func open() -> void:
_root.visible = true
_err.text = ""
_sell_quantity.value = 1
_active_tab = 0
_set_mode(1)
var p = _get_main_char_pos()
if p is Vector3:
_open_char_pos = p
_has_open_pos = true
else:
_has_open_pos = false
refresh()
func _close() -> void:
_hide_slot_tooltip()
_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()
_root.visible = false
_err.text = ""
if _sell_quantity:
_sell_quantity.value = 1
if is_instance_valid(_sell_confirm):
_sell_confirm.queue_free()
_sell_confirm = null
if is_instance_valid(_buy_confirm):
_buy_confirm.hide()
_buy_confirm.queue_free()
_buy_confirm = null
_set_mode(1)
func close_and_leave() -> void:
client.shop_close()
_close()
func sell(inv_cell: int, count: int = -1) -> void:
if not is_open() or inv_cell < 0:
return
var item := _inventory_item(inv_cell)
var amount := count if count > 0 else int(_sell_quantity.value)
if not item.is_empty():
amount = clampi(amount, 1, int(item.get("count", amount)))
else:
amount = clampi(amount, 1, 200)
var payload := item.duplicate(true)
payload["window"] = 1
payload["cell"] = inv_cell
payload["vnum"] = int(item.get("vnum", 0))
payload["count"] = int(item.get("count", amount))
_request_sell(payload, amount)
func _drop_to_sell(payload: Dictionary) -> bool:
if _mode != 2 and not (item_mouse and item_mouse.has_method("is_attached") and item_mouse.is_attached()):
# 在购买模式下若直接拖拽道具到商店窗口,同样允许触发卖出
pass
if payload.is_empty() or int(payload.get("window", -1)) != 1:
return false
var cell := int(payload.get("cell", -1))
if cell < 0 or not client.has_method("shop_sell"):
return false
var source_count := maxi(1, int(payload.get("count", 1)))
var amount := clampi(int(_sell_quantity.value), 1, mini(200, source_count))
return _request_sell(payload, amount)
func _inventory_item(cell: int) -> Dictionary:
if client == null or not client.has_method("get_inventory"):
return {}
for item in client.get_inventory():
if int(item.get("cell", -1)) == cell:
return item
return {}
func _proto_item(vnum: int) -> Dictionary:
if vnum > 0 and proto and proto.has_method("item"):
return proto.item(vnum)
return {}
func _sell_price(vnum: int, count: int, item_data: Dictionary = {}) -> int:
var d := item_data if not item_data.is_empty() else _proto_item(vnum)
var unit := maxi(0, int(d.get("sell_price", 0)))
var flags := int(d.get("flags", 0))
var raw := (count / maxi(1, unit)) if (flags & ITEM_FLAG_COUNT_PER_1GOLD) != 0 else unit * count
return maxi(0, int(raw) / 5)
func _is_valuable(payload: Dictionary, item_data: Dictionary) -> bool:
if int(item_data.get("sell_price", 0)) > 5000:
return true
var sockets: Array = payload.get("sockets", [])
for value in sockets:
if int(value) != 0:
return true
return false
func _request_sell(payload: Dictionary, amount: int) -> bool:
var vnum := int(payload.get("vnum", 0))
var d := _proto_item(vnum)
var anti_flags := int(payload.get("anti_flags", 0)) | int(d.get("anti_flags", 0))
if (anti_flags & ITEM_ANTIFLAG_SELL) != 0:
_on_error("CANNOT_SELL")
_play_ui("loginfail.wav")
return false
if _is_valuable(payload, d):
_ask_sell_confirmation(payload, amount, _sell_price(vnum, amount, d))
return true
return _send_sell(int(payload.get("cell", -1)), amount)
func _send_sell(cell: int, amount: int) -> bool:
if cell < 0 or not client.has_method("shop_sell"):
return false
if not client.shop_sell(cell, amount):
_on_error("SEND_FAILED")
return false
_play_ui("money.wav")
return true
func _ask_sell_confirmation(payload: Dictionary, amount: int, price: int) -> void:
if is_instance_valid(_sell_confirm):
_sell_confirm.queue_free()
var vnum := int(payload.get("vnum", 0))
_sell_confirm = ConfirmationDialog.new()
_sell_confirm.title = "确认出售"
_sell_confirm.dialog_text = "确定以 %d 金币出售 %s ×%d" % [
price,
_name_of(vnum),
amount,
]
_sell_confirm.ok_button_text = "出售"
_sell_confirm.cancel_button_text = "取消"
var dialog := _sell_confirm
dialog.confirmed.connect(func() -> void:
_sell_confirm = null
dialog.hide()
dialog.queue_free()
_send_sell(int(payload.get("cell", -1)), amount))
dialog.canceled.connect(func() -> void:
_sell_confirm = null
dialog.hide()
dialog.queue_free())
_prepare_mobile_confirmation(dialog, "确认出售")
_root.add_child(dialog)
if dialog.is_inside_tree():
_popup_confirmation(dialog)
else:
if _mobile_mode:
dialog.call_deferred("popup_centered", Vector2i(480, 220))
else:
dialog.call_deferred("popup_centered")
func _play_ui(name: String) -> void:
if audio and audio.has_method("play_ui"):
audio.play_ui(name)
func _set_mode(mode: int) -> void:
_mode = 2 if mode == 2 else 1
if _buy_mode_button:
_buy_mode_button.button_pressed = _mode == 1
if _sell_mode_button:
_sell_mode_button.button_pressed = _mode == 2
if _sell_drop_target:
_sell_drop_target.visible = _mode == 2
if cursor_manager and cursor_manager.has_method("set_cursor"):
cursor_manager.set_cursor(CursorManager.SELL if _mode == 2 else CursorManager.BUY)
if _mobile_sell_scroll:
_mobile_sell_scroll.visible = _mobile_mode and _mode == 2
_grid.visible = not (_mobile_mode and _mode == 2)
_refresh_mobile_sell()
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 refresh() -> void:
if not is_open() or client == null:
return
for c in _tabbar.get_children():
_tabbar.remove_child(c)
c.queue_free()
for c in _grid.get_children():
_grid.remove_child(c)
c.queue_free()
# START_EX 商店有多个货架(shop.GetTabCount);普通 START 商店 tabs 只 1 个。
var tabs: Array = []
if client.has_method("get_shop"):
tabs = client.get_shop().get("tabs", [])
if tabs.is_empty():
tabs = [{"name": "", "items": client.get_shop_items()}]
_active_tab = clampi(_active_tab, 0, tabs.size() - 1)
if tabs.size() > 1:
_tabbar.visible = true
_buy_mode_button.visible = false
_sell_mode_button.visible = false
var tab_w := 61 if tabs.size() <= 2 else 43
var tex_prefix := "d:/ymir work/ui/public/middle_button_" if tabs.size() <= 2 else "d:/ymir work/ui/public/small_button_"
for i in tabs.size():
var tn := String(tabs[i].get("name", ""))
var tb_text := tn if tn != "" else "货架 %d" % (i + 1)
var tb := _create_toggle_button(tb_text, Vector2.ZERO, Vector2(tab_w, 21), tex_prefix)
tb.button_pressed = (i == _active_tab)
var idx := i
tb.pressed.connect(func() -> void:
_active_tab = idx
refresh())
_tabbar.add_child(tb)
else:
_tabbar.visible = false
_buy_mode_button.visible = true
_sell_mode_button.visible = true
var items: Array = tabs[_active_tab].get("items", [])
# uishop.py: 买位置 = tabIdx * SHOP_SLOT_COUNT + slotPos
var base := _active_tab * SHOP_SLOT_COUNT
var by_pos := {}
for it in items:
var slot := int(it.get("pos", 0))
if slot >= 0 and slot < SHOP_SLOT_COUNT:
by_pos[slot] = it
for slot in SHOP_SLOT_COUNT:
var it: Dictionary = by_pos.get(slot, {})
_grid.add_child(_slot(it, base + slot))
_refresh_mobile_sell()
func _refresh_mobile_sell() -> void:
if not _mobile_mode or _mobile_sell_list == null:
return
for child in _mobile_sell_list.get_children():
child.queue_free()
var inv: Array = client.get_inventory() if client and client.has_method("get_inventory") else []
if inv.is_empty():
var empty := Label.new()
empty.text = "(背包为空)"
_mobile_sell_list.add_child(empty)
return
for item in inv:
var cell := int(item.get("cell", -1))
var vnum := int(item.get("vnum", 0))
if cell < 0 or vnum <= 0:
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(160, 30)
button.pressed.connect(func(): sell(cell, int(_sell_quantity.value)))
_mobile_sell_list.add_child(button)
# 40250 ItemSlot 单元格:32×32,带 Slot_Base.sub 底图,图标居中,右下角堆叠数量
func _slot(it: Dictionary, pos: int) -> Button:
var slot := Button.new()
slot.custom_minimum_size = Vector2(32, 32)
slot.size = Vector2(32, 32)
slot.clip_text = true
var slot_base_tex := UiAssets.load_tex(_assets_root, "d:/ymir work/ui/public/Slot_Base.sub")
if slot_base_tex:
slot.add_theme_stylebox_override("normal", UiBuild._sb_tex(slot_base_tex))
slot.add_theme_stylebox_override("hover", UiBuild._sb_tex(slot_base_tex))
slot.add_theme_stylebox_override("pressed", UiBuild._sb_tex(slot_base_tex))
slot.add_theme_stylebox_override("disabled", UiBuild._sb_tex(slot_base_tex))
slot.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
var vnum := int(it.get("vnum", 0))
if vnum > 0:
var count := int(it.get("count", 0))
slot.text = _name_of(vnum) # 保持 text 属性满足原有测试检索
var tex := _icon(vnum)
if tex:
slot.icon = tex
slot.expand_icon = true
slot.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
slot.vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER
# 堆叠数量标签(右下角)
if count > 1:
var count_lbl := Label.new()
count_lbl.name = "Count"
count_lbl.position = Vector2(0, 16)
count_lbl.size = Vector2(30, 14)
count_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
count_lbl.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
count_lbl.add_theme_font_size_override("font_size", 10)
count_lbl.add_theme_color_override("font_color", Color(1, 1, 1))
count_lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0))
count_lbl.add_theme_constant_override("outline_size", 2)
count_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
count_lbl.text = str(count)
slot.add_child(count_lbl)
if _tooltip_builder:
var pd: Dictionary = proto.item(vnum) if proto and proto.has_method("item") else {}
slot.tooltip_text = _tooltip_builder.format(vnum, maxi(1, count), pd, it)
slot.tooltip_text += "\n价格:%d" % int(it.get("price", 0))
slot.mouse_entered.connect(func(): _show_slot_tooltip(vnum, maxi(1, count), it))
slot.mouse_exited.connect(func(): _hide_slot_tooltip())
slot.pressed.connect(func() -> void: _buy_slot(pos, it))
slot.gui_input.connect(func(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
_direct_buy(pos, it)
)
else:
slot.disabled = true
return slot
func _get_tooltip_view() -> Control:
if _root and _root.get_parent() and _root.get_parent().has_method("get_item_tooltip_view"):
return _root.get_parent().get_item_tooltip_view(_assets_root, proto, item_list)
if _tooltip_view == null or not is_instance_valid(_tooltip_view):
_tooltip_view = ItemTooltipView.new()
_tooltip_view.setup(_assets_root, proto, item_list, _tooltip_builder)
_tooltip_view.visible = false
_tooltip_view.z_index = 200
if _root and is_instance_valid(_root):
_root.add_child(_tooltip_view)
return _tooltip_view
func _show_slot_tooltip(vnum: int, count: int, item_data: Dictionary) -> void:
if vnum <= 0:
return
var pd: Dictionary = proto.item(vnum) if proto and proto.has_method("item") else {}
var tv := _get_tooltip_view()
if tv:
tv.show_item(vnum, count, pd, item_data)
func _hide_slot_tooltip() -> void:
var tv := _get_tooltip_view()
if tv:
tv.hide_tooltip()
func _direct_buy(pos: int, item: Dictionary) -> void:
if _mode != 1 or item.is_empty() or pos < 0:
return
if client and client.has_method("shop_buy"):
if not client.shop_buy(pos, 1):
_on_error("SEND_FAILED")
func _on_shop_slot_updated(_pos: int) -> void:
if not is_open():
return
refresh()
func _buy_slot(pos: int, item: Dictionary) -> void:
if item_mouse and item_mouse.has_method("is_attached") and item_mouse.is_attached():
var payload: Dictionary = item_mouse.call("get_payload") if item_mouse.has_method("get_payload") else {}
if not payload.is_empty():
_drop_to_sell(payload)
item_mouse.cancel()
return
if _mode != 1:
_set_mode(1)
return
if item.is_empty() or pos < 0:
return
if is_instance_valid(_buy_confirm):
_buy_confirm.hide()
_buy_confirm.queue_free()
_buy_confirm = ConfirmationDialog.new()
_buy_confirm.title = "确认购买"
_buy_confirm.dialog_text = "购买 %s\n价格:%d 金币" % [_name_of(int(item.get("vnum", 0))), int(item.get("price", 0))]
_buy_confirm.ok_button_text = "购买"
_buy_confirm.cancel_button_text = "取消"
var dialog := _buy_confirm
dialog.confirmed.connect(func() -> void:
_buy_confirm = null
dialog.hide()
dialog.queue_free()
if not client.shop_buy(pos, 1):
_on_error("SEND_FAILED"))
dialog.canceled.connect(func() -> void:
_buy_confirm = null
dialog.hide()
dialog.queue_free())
_prepare_mobile_confirmation(dialog, "确认购买")
_root.add_child(dialog)
if dialog.is_inside_tree():
_popup_confirmation(dialog)
else:
if _mobile_mode:
dialog.call_deferred("popup_centered", Vector2i(480, 220))
else:
dialog.call_deferred("popup_centered")
func _prepare_mobile_confirmation(dialog: ConfirmationDialog, title: String) -> void:
if not _mobile_mode:
return
dialog.set_meta("mobile_modal", true)
dialog.set_meta("mobile_title", title)
dialog.min_size = Vector2i(480, 220)
dialog.size = Vector2i(480, 220)
func _popup_confirmation(dialog: ConfirmationDialog) -> void:
if _mobile_mode:
dialog.popup_centered(Vector2i(480, 220))
else:
dialog.popup_centered()
func _on_error(kind: String) -> void:
var tbl := {
"NOT_ENOUGH_MONEY": "金币不足",
"SOLDOUT": "已售罄",
"INVENTORY_FULL": "背包已满",
"INVALID_POS": "位置无效",
"NOT_ENOUGH_ITEM": "物品数量不足",
"CANNOT_BUY": "当前物品不可购买",
"CANNOT_SELL": "当前物品不可出售",
"SHOP_BUSY": "商店正在处理上一笔交易",
"SEND_FAILED": "交易请求发送失败",
}
_err.text = str(tbl.get(kind, kind))
func _create_toggle_button(btn_text: String, pos: Vector2, sz: Vector2, tex_prefix: String) -> Button:
var btn := Button.new()
btn.text = btn_text
btn.toggle_mode = true
btn.position = pos
btn.size = sz
btn.custom_minimum_size = sz
btn.clip_text = true
btn.add_theme_font_size_override("font_size", 12)
btn.add_theme_color_override("font_color", Color(1.0, 0.9, 0.7))
btn.add_theme_color_override("font_pressed_color", Color(1.0, 1.0, 1.0))
btn.add_theme_color_override("font_hover_color", Color(1.0, 1.0, 0.8))
var nrm := UiAssets.load_tex(_assets_root, tex_prefix + "01.sub")
var ovr := UiAssets.load_tex(_assets_root, tex_prefix + "02.sub")
var dwn := UiAssets.load_tex(_assets_root, tex_prefix + "03.sub")
if nrm:
btn.add_theme_stylebox_override("normal", UiBuild._sb_tex(nrm))
btn.add_theme_stylebox_override("hover", UiBuild._sb_tex(ovr if ovr else nrm))
btn.add_theme_stylebox_override("pressed", UiBuild._sb_tex(dwn if dwn else nrm))
btn.add_theme_stylebox_override("disabled", UiBuild._sb_tex(nrm))
btn.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
btn.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
return btn
# 40250 shopdialog.py 严格 184 × 328 官方 UI 布局构建
func _build(parent: Node) -> void:
_root = Control.new()
_root.name = "ShopWindow"
_root.size = Vector2(184, 328)
_root.custom_minimum_size = Vector2(184, 328)
_root.visible = false
_root.set_meta("is_titlebar", true)
parent.add_child(_root)
var vp_size := Vector2(800.0, 600.0)
if _root.is_inside_tree() and _root.get_viewport():
vp_size = _root.get_viewport_rect().size
# 40250 uiscript/shopdialog.py: x = SCREEN_WIDTH - 400, y = 10
_root.position = Vector2(maxf(10.0, vp_size.x - 400.0), 10.0)
# 1. 经典羊皮纸金属 Board 边框(184 × 328
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.08, 0.07, 0.06, 0.97)
sb.set_corner_radius_all(4)
panel.add_theme_stylebox_override("panel", sb)
_root.add_child(panel)
# 2. 官方 TitleBar (8, 8, width=169)
var titlebar := UiBuild._titlebar(_assets_root, 169, {})
titlebar.name = "TitleBar"
titlebar.position = Vector2(8, 8)
_root.add_child(titlebar)
var title_lbl := Label.new()
title_lbl.name = "TitleName"
title_lbl.text = "商店"
title_lbl.size = Vector2(169, 23)
title_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title_lbl.add_theme_color_override("font_color", Color(1.0, 0.9, 0.6))
title_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
titlebar.add_child(title_lbl)
var close_btn: Button = titlebar.find_child("CloseButton", true, false)
if close_btn:
close_btn.pressed.connect(close_and_leave)
# 错误提示悬浮字(红字)
_err = Label.new()
_err.position = Vector2(12, 30)
_err.size = Vector2(160, 16)
_err.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_err.modulate = Color(1, 0.3, 0.3)
_err.add_theme_font_size_override("font_size", 11)
_err.mouse_filter = Control.MOUSE_FILTER_IGNORE
_root.add_child(_err)
# 3. 40250 5×8 货架 ItemSlot (12, 34)
# 40 个格子,每格 32×32,使用 Slot_Base.sub 底图
_grid = GridContainer.new()
_grid.name = "ItemSlot"
_grid.columns = 5
_grid.position = Vector2(12, 34)
_grid.size = Vector2(160, 256)
_grid.add_theme_constant_override("h_separation", 0)
_grid.add_theme_constant_override("v_separation", 0)
_root.add_child(_grid)
# 4. 底部按钮(y=295
# BuyButton (21, 295, 61x21)
_buy_mode_button = _create_toggle_button("购买", Vector2(21, 295), Vector2(61, 21), "d:/ymir work/ui/public/middle_button_")
_buy_mode_button.name = "BuyButton"
_buy_mode_button.pressed.connect(func() -> void: _set_mode(1))
_root.add_child(_buy_mode_button)
# SellButton (104, 295, 61x21)
_sell_mode_button = _create_toggle_button("出售", Vector2(104, 295), Vector2(61, 21), "d:/ymir work/ui/public/middle_button_")
_sell_mode_button.name = "SellButton"
_sell_mode_button.pressed.connect(func() -> void: _set_mode(2))
_root.add_child(_sell_mode_button)
# 多货架 Tabbary=295 处覆盖显示)
_tabbar = HBoxContainer.new()
_tabbar.name = "TabBar"
_tabbar.position = Vector2(21, 295)
_tabbar.size = Vector2(144, 21)
_tabbar.add_theme_constant_override("separation", 4)
_tabbar.visible = false
_root.add_child(_tabbar)
# 保持 API 兼容性字段(无视界面渲染)
_sell_quantity = SpinBox.new()
_sell_quantity.name = "SellQuantity"
_sell_quantity.value = 1
_sell_quantity.visible = false
_root.add_child(_sell_quantity)
_sell_drop_target = Button.new()
_sell_drop_target.name = "SellDropTarget"
_sell_drop_target.visible = false
_root.add_child(_sell_drop_target)
# 移动端兼容
_mobile_sell_scroll = ScrollContainer.new()
_mobile_sell_scroll.position = Vector2(12, 34)
_mobile_sell_scroll.size = Vector2(160, 256)
_mobile_sell_scroll.visible = false
_root.add_child(_mobile_sell_scroll)
_mobile_sell_list = VBoxContainer.new()
_mobile_sell_list.add_theme_constant_override("separation", 4)
_mobile_sell_scroll.add_child(_mobile_sell_list)