Files

472 lines
15 KiB
GDScript
Raw Permalink 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 商店窗。
#
# var su := preload("res://ui/shop_ui.gd").new()
# add_child(su)
# su.setup(m2client, canvas_parent, proto, item_list) # proto / item_list 可空
#
# `shop_opened(vid)` → 打开并列出货物(名字 + 价格 + [买])。
# `shop_closed` → 关闭。`shop_error(kind)` → 顶部红字提示。
# 卖:inventory_ui 在商店开着时右键道具 → 调 shop_ui.sell(cell)。
extends Node
const CursorManager = preload("res://ui/cursor_manager.gd")
const ItemTooltip = preload("res://ui/item_tooltip.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
var client: Node
var proto: Node
var item_list # ItemListDB (RefCounted)
var _tooltip_builder: RefCounted
var _root: Control
var _tabbar: HBoxContainer
var _grid: GridContainer
var _err: Label
var _sell_quantity: SpinBox
var _active_tab := 0
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
_tooltip_builder = ItemTooltip.new()
_tooltip_builder.setup(AssetRoot.path(), "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_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:
_close()
func open() -> void:
_root.visible = true
_err.text = ""
_sell_quantity.value = 1
_active_tab = 0
_set_mode(1)
refresh()
func _close() -> void:
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 or 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.hide()
_sell_confirm.queue_free()
_sell_confirm = ConfirmationDialog.new()
_sell_confirm.title = "确认出售"
_sell_confirm.dialog_text = "出售 %d 个物品,获得 %d 金币?" % [amount, price]
_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():
# 兼容没有 get_shop 的旧桩
tabs = [{"name": "", "items": client.get_shop_items()}]
_active_tab = clampi(_active_tab, 0, tabs.size() - 1)
if tabs.size() > 1:
_tabbar.visible = true
for i in tabs.size():
var tb := Button.new()
tb.toggle_mode = true
var tn := String(tabs[i].get("name", ""))
tb.text = tn if tn != "" else "货架 %d" % (i + 1)
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
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(330, 30)
button.pressed.connect(func(): sell(cell, int(_sell_quantity.value)))
_mobile_sell_list.add_child(button)
func _slot(it: Dictionary, pos: int) -> Button:
var slot := Button.new()
slot.custom_minimum_size = Vector2(66, 30)
slot.clip_text = true
var vnum := int(it.get("vnum", 0))
if vnum > 0:
var count := int(it.get("count", 0))
slot.text = _name_of(vnum).substr(0, 7)
if count > 1:
slot.text += " ×%d" % count
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.pressed.connect(func() -> void: _buy_slot(pos, it))
else:
slot.disabled = true
return slot
func _buy_slot(pos: int, item: Dictionary) -> void:
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 _build(parent: Node) -> void:
_root = Control.new()
_root.set_anchors_preset(Control.PRESET_CENTER)
_root.position = Vector2(-190, -200)
_root.size = Vector2(380, 400)
_root.visible = false
_root.set_meta("is_titlebar", true)
parent.add_child(_root)
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)
var title := Label.new()
title.text = "商店"
title.position = Vector2(12, 8)
_root.add_child(title)
_err = Label.new()
_err.position = Vector2(60, 10)
_err.modulate = Color(1, 0.4, 0.4)
_err.add_theme_font_size_override("font_size", 12)
_root.add_child(_err)
var sell_hint := Label.new()
sell_hint.text = "出售数量(背包右键):"
sell_hint.position = Vector2(12, 360)
sell_hint.add_theme_font_size_override("font_size", 11)
_root.add_child(sell_hint)
_sell_quantity = SpinBox.new()
_sell_quantity.name = "SellQuantity"
_sell_quantity.min_value = 1
_sell_quantity.max_value = 200
_sell_quantity.value = 1
_sell_quantity.step = 1
_sell_quantity.position = Vector2(160, 356)
_sell_quantity.size = Vector2(70, 26)
_root.add_child(_sell_quantity)
_sell_drop_target = Button.new()
_sell_drop_target.name = "SellDropTarget"
_sell_drop_target.text = "拖动物品到此出售"
_sell_drop_target.position = Vector2(12, 325)
_sell_drop_target.size = Vector2(210, 26)
_sell_drop_target.mouse_filter = Control.MOUSE_FILTER_STOP
_sell_drop_target.visible = false
_root.add_child(_sell_drop_target)
if item_mouse and item_mouse.has_method("register_target"):
item_mouse.register_target(_sell_drop_target, _drop_to_sell, self)
_buy_mode_button = Button.new()
_buy_mode_button.text = "购买"
_buy_mode_button.toggle_mode = true
_buy_mode_button.position = Vector2(235, 325)
_buy_mode_button.size = Vector2(62, 26)
_buy_mode_button.pressed.connect(func() -> void: _set_mode(1))
_root.add_child(_buy_mode_button)
_sell_mode_button = Button.new()
_sell_mode_button.text = "出售"
_sell_mode_button.toggle_mode = true
_sell_mode_button.position = Vector2(300, 325)
_sell_mode_button.size = Vector2(62, 26)
_sell_mode_button.pressed.connect(func() -> void: _set_mode(2))
_root.add_child(_sell_mode_button)
_tabbar = HBoxContainer.new()
_tabbar.position = Vector2(12, 30)
_tabbar.add_theme_constant_override("separation", 4)
_tabbar.visible = false
_root.add_child(_tabbar)
_grid = GridContainer.new()
_grid.columns = 5
_grid.position = Vector2(12, 58)
_grid.size = Vector2(350, 252)
_grid.add_theme_constant_override("h_separation", 3)
_grid.add_theme_constant_override("v_separation", 2)
_root.add_child(_grid)
_mobile_sell_scroll = ScrollContainer.new()
_mobile_sell_scroll.position = Vector2(12, 58)
_mobile_sell_scroll.size = Vector2(350, 252)
_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)
var close_btn := Button.new()
close_btn.text = "离开"
close_btn.position = Vector2(300, 360)
close_btn.pressed.connect(close_and_leave)
_root.add_child(close_btn)