# 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) # 多货架 Tabbar(y=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)