完善客户端功能并同步差距文档

This commit is contained in:
shenlei
2026-09-03 18:40:01 +09:00
parent f93a172296
commit 13ccb8d02d
135 changed files with 21881 additions and 1259 deletions
+229 -45
View File
@@ -9,22 +9,38 @@
# 卖: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 _list: VBoxContainer
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
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())
@@ -44,13 +60,26 @@ func open() -> void:
_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()
@@ -59,10 +88,119 @@ func close_and_leave() -> void:
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)
amount = clampi(amount, 1, 200)
if client.has_method("shop_sell") and not client.shop_sell(inv_cell, amount):
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())
_root.add_child(dialog)
if dialog.is_inside_tree():
dialog.popup_centered()
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)
func _name_of(vnum: int) -> String:
if proto and proto.has_method("item"):
@@ -78,8 +216,8 @@ func refresh() -> void:
for c in _tabbar.get_children():
_tabbar.remove_child(c)
c.queue_free()
for c in _list.get_children():
_list.remove_child(c)
for c in _grid.get_children():
_grid.remove_child(c)
c.queue_free()
# START_EX 商店有多个货架(shop.GetTabCount);普通 START 商店 tabs 只 1 个。
@@ -108,47 +246,66 @@ func refresh() -> void:
_tabbar.visible = false
var items: Array = tabs[_active_tab].get("items", [])
if items.is_empty():
var e := Label.new()
e.text = "(无货物)"
_list.add_child(e)
return
# uishop.py: 买位置 = tabIdx * SHOP_SLOT_COUNT + slotPos
var base := _active_tab * SHOP_SLOT_COUNT
var by_pos := {}
for it in items:
_list.add_child(_row(it, base))
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))
func _row(it: Dictionary, pos_base := 0) -> Control:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(320, 0)
var nm := Label.new()
nm.text = _name_of(int(it.get("vnum", 0)))
nm.custom_minimum_size = Vector2(170, 0)
nm.add_theme_font_size_override("font_size", 12)
row.add_child(nm)
var pr := Label.new()
pr.text = "%d" % int(it.get("price", 0))
pr.custom_minimum_size = Vector2(90, 0)
pr.modulate = Color(0.95, 0.85, 0.5)
row.add_child(pr)
var buy := Button.new()
buy.text = ""
var pos := pos_base + int(it.get("pos", 0))
var stock := int(it.get("count", 0))
var quantity := SpinBox.new()
quantity.name = "Quantity"
quantity.min_value = 1
quantity.max_value = clampi(stock if stock > 0 else 200, 1, 200)
quantity.step = 1
quantity.value = 1
quantity.custom_minimum_size = Vector2(64, 0)
row.add_child(quantity)
buy.pressed.connect(func() -> void:
var amount := clampi(int(quantity.value), 1, int(quantity.max_value))
if not client.shop_buy(pos, amount):
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"))
row.add_child(buy)
return row
dialog.canceled.connect(func() -> void:
_buy_confirm = null
dialog.hide()
dialog.queue_free())
_root.add_child(dialog)
if dialog.is_inside_tree():
dialog.popup_centered()
else:
dialog.call_deferred("popup_centered")
func _on_error(kind: String) -> void:
var tbl := {
@@ -202,15 +359,42 @@ func _build(parent: Node) -> void:
_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)
_list = VBoxContainer.new()
_list.position = Vector2(12, 58)
_list.add_theme_constant_override("separation", 4)
_root.add_child(_list)
_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)
var close_btn := Button.new()
close_btn.text = "离开"
close_btn.position = Vector2(300, 360)