684 lines
23 KiB
GDScript
684 lines
23 KiB
GDScript
# 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)
|