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

385 lines
12 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.
# ExchangeUI (P8) —— 交易 / 换货窗。
#
# var xu := preload("res://ui/exchange_ui.gd").new()
# add_child(xu)
# xu.setup(m2client, canvas_parent, proto) # proto 可空
#
# 两栏:我方 / 对方,各列道具行 + 金币。底部:金币输入 + [放金币]、[接受]、[取消]。
# `exchange_changed` 刷新(active=false 时自动关闭)。
# inventory_ui 在交易开着时右键道具 → 调 xu.offer(win, cell)。
extends Node
const ItemTooltip = preload("res://ui/item_tooltip.gd")
var client: Node
var proto: Node
var _tooltip_builder: RefCounted
var _root: Control
var _self_box: VBoxContainer
var _peer_box: VBoxContainer
var _self_gold: Label
var _peer_gold: Label
var _status: Label
var _accept_btn: Button
var _gold_input: LineEdit
var _next_display := 0
var _offered_cells := {}
var _self_grid: GridContainer
var _exchange_cells: Dictionary = {}
var item_mouse: Node
const EXCHANGE_SLOT_COUNT := 12
const INVENTORY_WINDOW := 1
const EXCHANGE_LIMIT_RANGE := 1000.0
const ITEM_ANTIFLAG_GIVE := 1 << 13
var _accept_pending := false
var _anchor := Vector2.ZERO
var _anchor_valid := false
func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
client = m2client
proto = proto_node
_tooltip_builder = ItemTooltip.new()
_tooltip_builder.setup(AssetRoot.path(), "en", proto)
_build(parent)
if client.has_signal("exchange_changed"):
client.exchange_changed.connect(refresh)
func is_open() -> bool:
return _root != null and _root.visible
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()
if _root:
_root.visible = false
_next_display = 0
_offered_cells.clear()
_accept_pending = false
_anchor_valid = false
if _gold_input:
_gold_input.text = ""
func offer(inv_window: int, inv_cell: int) -> void:
if not is_open() or inv_window < 0 or inv_cell < 0:
return
var key := "%d:%d" % [inv_window, inv_cell]
if _offered_cells.has(key):
_status.text = "该物品已经放入交易"
return
if not _can_give(_item_at(inv_window, inv_cell)):
return
var x: Dictionary = client.get_exchange()
var used := {}
for item in x.get("self_items", []):
used[int(item.get("slot", -1))] = true
for i in 12:
if not used.has(i):
_next_display = i
break
if used.size() >= 12:
_status.text = "交易物品栏已满"
return
if client.exchange_add_item(inv_window, inv_cell, _next_display):
_offered_cells[key] = true
_next_display = (_next_display + 1) % 12
_status.text = ""
else:
_status.text = "交易请求发送失败"
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 _tooltip_for(item: Dictionary) -> String:
var vnum := int(item.get("vnum", 0))
if _tooltip_builder:
var pd: Dictionary = proto.item(vnum) if proto and proto.has_method("item") else {}
return _tooltip_builder.format(vnum, int(item.get("count", 1)), pd, item)
return "%s\n#%d ×%d" % [_name_of(vnum), vnum, int(item.get("count", 1))]
func refresh() -> void:
if client == null:
return
var x: Dictionary = client.get_exchange()
if not x.get("active", false):
_root.visible = false
_next_display = 0
_offered_cells.clear()
_accept_pending = false
_anchor_valid = false
_gold_input.text = ""
_status.text = ""
return
_root.visible = true
_capture_anchor()
_render_self_grid(x.get("self_items", []))
_fill(_self_box, x.get("self_items", []))
_fill(_peer_box, x.get("peer_items", []))
_self_gold.text = "金币: %d" % int(x.get("self_gold", 0))
_peer_gold.text = "金币: %d" % int(x.get("peer_gold", 0))
var me := bool(x.get("self_accept", false))
var peer := bool(x.get("peer_accept", false))
# The server clears self_accept when either side changes the offer. Do not
# invent a local transaction state: mirror the authoritative bit exactly.
_accept_pending = me
_accept_btn.text = "已接受 ✓" if me else "接受"
_accept_btn.disabled = me
_accept_btn.modulate = Color(0.5, 1, 0.5) if me else Color(1, 1, 1)
_root.get_node("PeerAccept").text = "对方: 已接受" if peer else "对方: 未接受"
_status.text = ""
func _item_at(inv_window: int, inv_cell: int) -> Dictionary:
if client == null:
return {}
if client.has_method("get_item"):
var direct: Dictionary = client.get_item(inv_window, inv_cell)
if not direct.is_empty():
return direct
if inv_window == INVENTORY_WINDOW and client.has_method("get_inventory"):
for item in client.get_inventory():
if int(item.get("cell", -1)) == inv_cell:
return item
if inv_window == 2 and client.has_method("get_equipment"):
var equipment: Array = client.get_equipment()
if inv_cell >= 0 and inv_cell < equipment.size():
return equipment[inv_cell]
return {}
func _can_give(item_data: Dictionary) -> bool:
var vnum := int(item_data.get("vnum", 0))
var proto_data: Dictionary = proto.item(vnum) if proto and proto.has_method("item") else {}
var flags := int(item_data.get("anti_flags", 0)) | int(proto_data.get("anti_flags", 0))
if (flags & ITEM_ANTIFLAG_GIVE) != 0:
_status.text = "该物品不可交易"
return false
return true
func _render_self_grid(rows: Array) -> void:
if _self_grid == null:
return
for pos in _exchange_cells:
var cell: Button = _exchange_cells[pos]
cell.text = ""
cell.tooltip_text = ""
cell.set_meta("vnum", 0)
cell.set_meta("count", 0)
for row in rows:
var pos := int(row.get("slot", -1))
if not _exchange_cells.has(pos):
continue
var vnum := int(row.get("vnum", 0))
var count := int(row.get("count", 1))
var cell: Button = _exchange_cells[pos]
cell.text = "%s%s" % [_name_of(vnum).substr(0, 6), (" ×%d" % count) if count > 1 else ""]
cell.tooltip_text = _tooltip_for(row)
cell.set_meta("vnum", vnum)
cell.set_meta("count", count)
func _drop_to_slot(payload: Dictionary, display_pos: int) -> bool:
if payload.is_empty() or display_pos < 0 or display_pos >= EXCHANGE_SLOT_COUNT:
return false
if bool(payload.get("money", false)):
if int(client.get_exchange().get("self_gold", 0)) > 0:
_status.text = "金币已经放入,不能修改"
return false
var amount := int(payload.get("count", 0))
if amount <= 0 or not client.has_method("exchange_add_gold"):
return false
return client.exchange_add_gold(amount)
var source_window := int(payload.get("window", -1))
var source_cell := int(payload.get("cell", -1))
if source_window not in [INVENTORY_WINDOW, 2] or source_cell < 0:
return false
var item_data := payload.duplicate(true)
var source_data := _item_at(source_window, source_cell)
for key in source_data:
if not item_data.has(key):
item_data[key] = source_data[key]
if not _can_give(item_data):
return false
for row in client.get_exchange().get("self_items", []):
if int(row.get("slot", -1)) == display_pos:
return false
var key := "%d:%d" % [source_window, source_cell]
if _offered_cells.has(key):
return false
if not client.has_method("exchange_add_item") or not client.exchange_add_item(
source_window, source_cell, display_pos):
_status.text = "交易请求发送失败"
return false
_offered_cells[key] = true
return true
func _fill(box: VBoxContainer, rows: Array) -> void:
for c in box.get_children():
c.queue_free()
for r in rows:
var l := Label.new()
l.text = "%s ×%d" % [_name_of(int(r.get("vnum", 0))), int(r.get("count", 1))]
l.tooltip_text = _tooltip_for(r)
l.add_theme_font_size_override("font_size", 12)
box.add_child(l)
func _on_put_gold() -> void:
var current := int(client.get_exchange().get("self_gold", 0))
if current > 0:
_status.text = "金币已经放入,不能修改"
return
var raw := _gold_input.text.strip_edges()
if raw == "" or not raw.is_valid_int():
_status.text = "请输入有效金币数量"
return
var g := int(raw)
if g <= 0 or g > 9999999:
_status.text = "金币数量必须在 19999999 之间"
return
if client.has_method("exchange_add_gold") and client.exchange_add_gold(g):
_gold_input.text = ""
_status.text = ""
else:
_status.text = "金币请求发送失败"
func _on_accept() -> void:
if _accept_pending or _accept_btn.disabled:
return
if client.has_method("exchange_accept") and client.exchange_accept():
_accept_pending = true
_accept_btn.disabled = true
else:
_status.text = "接受请求发送失败"
func _capture_anchor() -> void:
if _anchor_valid or client == null or not client.has_method("get_main_vid") \
or not client.has_method("get_entity"):
return
var entity: Dictionary = client.get_entity(int(client.get_main_vid()))
var p = entity.get("pos_cm", null)
if p is Vector3:
_anchor = Vector2(p.x, p.y)
_anchor_valid = true
elif entity.has("x") and entity.has("y"):
_anchor = Vector2(float(entity.get("x", 0)), float(entity.get("y", 0)))
_anchor_valid = true
func _current_position() -> Variant:
if client == null or not client.has_method("get_main_vid") or not client.has_method("get_entity"):
return null
var entity: Dictionary = client.get_entity(int(client.get_main_vid()))
var p = entity.get("pos_cm", null)
if p is Vector3:
return Vector2(p.x, p.y)
if entity.has("x") and entity.has("y"):
return Vector2(float(entity.get("x", 0)), float(entity.get("y", 0)))
return null
func _process(_dt: float) -> void:
if not is_open() or not _anchor_valid:
return
var current = _current_position()
if current is Vector2 and (absf(current.x - _anchor.x) > EXCHANGE_LIMIT_RANGE \
or absf(current.y - _anchor.y) > EXCHANGE_LIMIT_RANGE):
if client.has_method("exchange_cancel"):
client.exchange_cancel()
close()
func _build(parent: Node) -> void:
_root = Control.new()
_root.set_anchors_preset(Control.PRESET_CENTER)
_root.position = Vector2(-220, -180)
_root.size = Vector2(440, 360)
_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.07, 0.08, 0.1, 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)
var sl := Label.new()
sl.text = "我方"
sl.position = Vector2(20, 34)
_root.add_child(sl)
_self_box = VBoxContainer.new()
_self_box.position = Vector2(20, 56)
_self_box.custom_minimum_size = Vector2(200, 0)
_root.add_child(_self_box)
_self_grid = GridContainer.new()
_self_grid.columns = 6
_self_grid.position = Vector2(20, 138)
_self_grid.add_theme_constant_override("h_separation", 3)
_self_grid.add_theme_constant_override("v_separation", 3)
_root.add_child(_self_grid)
for pos in EXCHANGE_SLOT_COUNT:
var cell := Button.new()
cell.custom_minimum_size = Vector2(52, 32)
cell.add_theme_font_size_override("font_size", 8)
_self_grid.add_child(cell)
_exchange_cells[pos] = cell
if item_mouse and item_mouse.has_method("register_target"):
item_mouse.register_target(cell,
func(payload: Dictionary): return _drop_to_slot(payload, pos), self)
_self_gold = Label.new()
_self_gold.text = "金币: 0"
_self_gold.position = Vector2(20, 250)
_root.add_child(_self_gold)
var pl := Label.new()
pl.text = "对方"
pl.position = Vector2(240, 34)
_root.add_child(pl)
_peer_box = VBoxContainer.new()
_peer_box.position = Vector2(240, 56)
_peer_box.custom_minimum_size = Vector2(180, 0)
_root.add_child(_peer_box)
_peer_gold = Label.new()
_peer_gold.text = "金币: 0"
_peer_gold.position = Vector2(240, 250)
_root.add_child(_peer_gold)
_status = Label.new()
_status.position = Vector2(20, 276)
_status.add_theme_font_size_override("font_size", 11)
_status.add_theme_color_override("font_color", Color(1, 0.65, 0.55))
_root.add_child(_status)
var pa := Label.new()
pa.name = "PeerAccept"
pa.text = "对方: 未接受"
pa.position = Vector2(240, 272)
pa.add_theme_font_size_override("font_size", 12)
_root.add_child(pa)
var bottom := HBoxContainer.new()
bottom.position = Vector2(20, 310)
_root.add_child(bottom)
_gold_input = LineEdit.new()
_gold_input.placeholder_text = "金币"
_gold_input.custom_minimum_size = Vector2(90, 0)
bottom.add_child(_gold_input)
var put := Button.new()
put.text = "放金币"
put.pressed.connect(_on_put_gold)
bottom.add_child(put)
_accept_btn = Button.new()
_accept_btn.text = "接受"
_accept_btn.pressed.connect(_on_accept)
bottom.add_child(_accept_btn)
var cancel := Button.new()
cancel.text = "取消"
cancel.pressed.connect(func() -> void: client.exchange_cancel())
bottom.add_child(cancel)