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

840 lines
32 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.
# InventoryUI (P2) —— 背包 + 装备窗(走 P1 uiscript 装载器)。
#
# var inv := preload("res://ui/inventory_ui.gd").new()
# add_child(inv)
# inv.setup(ui_manager, m2client, proto, assets_root)
# inv.toggle() # I 键
#
# 从 `assets/uiscript/inventorywindow.py` 建窗,用 M2Client.get_inventory/get_equipment
# 填格子,Metin2Proto 出名字,icon/item/<vnum>.tga 出图标。右键=use_item
# 拖到别的格=move_item。inventory_changed 信号来时刷新。
#
# 格子编号约定(uiscript):0..89 背包,90+ 装备(wear = index-90)。
# 新装备 / 腰带没有可靠的旧 uiscript 布局时,在背包右侧生成扩展面板。发包坐标见
# `_to_wire()`:旧 11 格 wearBODY..SHIELD)走 EQUIPMENT 窗(window=2, wear cell);
# costume/ring/beltwear >= 19)与 belt 背包走 INVENTORY 窗(window=1)的全局 cell
# 109..113 / 152+belt cell,见 GameType.h `c_Costume_Slot_Start` / `c_Equipment_Belt`)。
extends Node
# 右键点道具时发出(window, cell 为 wire 坐标)。P8 的商店 / 交易 / 仓库窗接它。
signal item_context(window: int, cell: int)
# 背包物品右键穿戴被 EquipRules 前置门拦下时发出(code 见 equip_rules.gdneed = limit 要求值)。
signal equip_rejected(code: String, need: int)
# 若设置:右键道具时先调它 (window, cell) -> bool;返回 true 表示已处理,不再 use_item。
var context_consumer: Callable = Callable()
const UiAssets = preload("res://ui/ui_assets.gd")
const EquipRules = preload("res://equip_rules.gd")
const ItemTooltip = preload("res://ui/item_tooltip.gd")
const EQUIP_BASE := 90 # uiscript EQUIPMENT_START_INDEX
const BELT_BASE := 200 # 仅 UI 内部索引,避免与旧 slot_index 相撞
const BELT_SLOT_START := 152 # GameType.c_Belt_Inventory_Slot_Start
const BELT_CELL_COUNT := 16
const WEAR_COUNT := 24 # get_equipment() 数组上限(server wear 保留位;实际用到 0..23)
# wear 位置编号(GameType.h `EWearPositions` / `c_Equipment_*`wear = 全局 cell - c_Equipment_Start(90))。
# 旧 11 格(BODY..SHIELD)在客户端走 EQUIPMENT 窗(window=2, wear cell);
# ENABLE_NEW_EQUIPMENT_SYSTEM 的 costume/ring/belt 仍留在 INVENTORY 窗(window=1)的
# 全局 cell 命名空间里 —— c_Costume_Slot_Start = 90+19, c_New_Equipment_Start = 90+21。
const WEAR_MAX_NUM := 11 # ItemData.h EWearPositions::WEAR_MAX_NUM
const WEAR_COSTUME_BODY := 19 # GameType.h c_Costume_Slot_Body - c_Equipment_Start
const WEAR_COSTUME_HAIR := 20 # c_Costume_Slot_Hair - c_Equipment_Start
const WEAR_RING1 := 21 # c_Equipment_Ring1 - c_Equipment_Start
const WEAR_RING2 := 22 # c_Equipment_Ring2 - c_Equipment_Start
const WEAR_BELT := 23 # c_Equipment_Belt - c_Equipment_Start
const NEW_EQUIP_WEAR_START := WEAR_COSTUME_BODY # >= 此值的 wear 位置走 INVENTORY 全局 cell
const WINDOW_INVENTORY := 1 # mtnet::WINDOW_INVENTORY
const WINDOW_EQUIPMENT := 2 # mtnet::WINDOW_EQUIPMENT(旧 11 格 wear
const WINDOW_SAFEBOX := 3 # global mouse payload source, not an inventory window
const WINDOW_MALL := 4 # global mouse payload source, not an inventory window
const BELT_RULES := [1, 2, 4, 6, 3, 3, 4, 6, 5, 5, 5, 6, 7, 7, 7, 7]
var ui: CanvasLayer # UiManager
var client: Node # M2Client
var proto: Node # Metin2Proto
var item_list: RefCounted # ItemList(可空;给了就用它的 icon 路径)
var item_tooltip: RefCounted
var assets_root := ""
var uiscript_dir := ""
var item_mouse: Node # MouseController(可空,兼容旧测试桩)
var _win: Dictionary = {} # { root, nodes }
var _cells := {} # ui_index:int -> Panel
var _drag_from := -1
var _combine_from := -1 # Shift 选中的“使用到物品”来源格
var _hint: Label
var _money_button: BaseButton
var _mobile_mode := false
var _mobile_selected_ui := -1
var _mobile_press_cell := -1
var _mobile_touch_index := -1
var _mobile_long_press_token := 0
var _mobile_detail_dialog: Control
var _mobile_dragging := false
var _mobile_drag_ui := -1
var _mobile_press_position_value := Vector2.ZERO
var _mobile_long_press_ready := false
const MOBILE_LONG_PRESS_SECONDS := 0.55
const MOBILE_DRAG_THRESHOLD := 14.0
func setup(ui_manager: CanvasLayer, m2client: Node, proto_node: Node, assets: String,
il: RefCounted = null) -> void:
ui = ui_manager
client = m2client
proto = proto_node
item_list = il
assets_root = assets
item_tooltip = ItemTooltip.new()
item_tooltip.setup(assets_root, "en", proto)
uiscript_dir = assets.path_join("uiscript/uiscript")
if not DirAccess.dir_exists_absolute(uiscript_dir):
uiscript_dir = assets.path_join("uiscript")
if client and client.has_signal("inventory_changed"):
client.inventory_changed.connect(_on_inv_changed)
set_process_input(true)
func is_open() -> bool:
return not _win.is_empty() and is_instance_valid(_win.get("root"))
func set_mobile_mode(enabled: bool) -> void:
_mobile_mode = enabled
mobile_selected_clear()
if enabled:
_set_hint("点按物品选择;再次点按使用,点另一格移动")
func mobile_selected() -> int:
return _mobile_selected_ui
func toggle() -> void:
if is_open(): close()
else: open()
func close() -> void:
if _mobile_detail_dialog and is_instance_valid(_mobile_detail_dialog) and ui:
ui.close(_mobile_detail_dialog)
_mobile_detail_dialog = null
_mobile_long_press_token += 1
_mobile_press_cell = -1
_mobile_touch_index = -1
_mobile_dragging = false
_mobile_drag_ui = -1
_mobile_press_position_value = Vector2.ZERO
_mobile_long_press_ready = 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()
if is_open():
ui.close(_win["root"])
_win = {}
_cells.clear()
_combine_from = -1
_mobile_selected_ui = -1
_hint = null
func open() -> void:
if is_open():
return
var path := uiscript_dir.path_join("inventorywindow.py")
if not FileAccess.file_exists(path):
push_warning("InventoryUI: no inventorywindow.py at " + path)
return
_win = ui.open_script(path, assets_root)
_index_cells(_win["root"])
_add_extended_cells(_win["root"])
_wire_cells()
_wire_money()
refresh()
# --- 填充 ---------------------------------------------------------------
func refresh() -> void:
if not is_open() or client == null:
return
for idx in _cells:
_clear_cell(_cells[idx])
for d in client.get_inventory():
var cell: Panel = _cells.get(int(d["cell"]), null)
if cell:
_fill_cell(cell, int(d["vnum"]), int(d["count"]), d)
var eq: Array = client.get_equipment()
for i in eq.size():
var it: Dictionary = eq[i]
var cell: Panel = _cells.get(EQUIP_BASE + i, null)
if cell and int(it.get("vnum", 0)) != 0:
_fill_cell(cell, int(it["vnum"]), int(it.get("count", 1)), it)
var belt_items: Array = client.get_belt_inventory() if client.has_method("get_belt_inventory") else []
for d in belt_items:
var cell: Panel = _cells.get(BELT_BASE + int(d["cell"]), null)
if cell:
_fill_cell(cell, int(d["vnum"]), int(d.get("count", 1)), d)
if is_instance_valid(_money_button):
_money_button.text = _format_gold(_current_gold())
_apply_belt_locks(eq)
func _on_inv_changed(_window: int, _cell: int) -> void:
refresh()
func _fill_cell(cell: Panel, vnum: int, count: int, instance_data: Dictionary = {}) -> void:
_clear_cell(cell)
cell.set_meta("vnum", vnum)
cell.set_meta("count", count)
cell.set_meta("item_data", instance_data.duplicate(true))
var name := "item %d" % vnum
var proto_data: Dictionary = {}
if proto:
proto_data = proto.item(vnum)
if not proto_data.is_empty():
name = String(proto_data.get("locale_name", proto_data.get("name", name)))
if item_tooltip:
cell.tooltip_text = item_tooltip.format(vnum, count, proto_data, instance_data)
else:
cell.tooltip_text = "%s\n#%d%s" % [name, vnum, (" x%d" % count) if count > 1 else ""]
var tex := _icon(vnum)
if tex:
var tr := TextureRect.new()
tr.name = "icon"
tr.texture = tex
tr.set_anchors_preset(Control.PRESET_FULL_RECT)
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
tr.stretch_mode = TextureRect.STRETCH_SCALE
tr.mouse_filter = Control.MOUSE_FILTER_IGNORE
cell.add_child(tr)
else:
var lb := Label.new()
lb.name = "icon"
lb.text = name.substr(0, 6)
lb.add_theme_font_size_override("font_size", 9)
lb.mouse_filter = Control.MOUSE_FILTER_IGNORE
cell.add_child(lb)
if count > 1:
var cnt := Label.new()
cnt.name = "count"
cnt.text = str(count)
cnt.position = Vector2(2, 16)
cnt.add_theme_font_size_override("font_size", 9)
cnt.mouse_filter = Control.MOUSE_FILTER_IGNORE
cell.add_child(cnt)
func _clear_cell(cell: Panel) -> void:
cell.set_meta("vnum", 0)
cell.set_meta("count", 0)
cell.set_meta("item_data", {})
cell.tooltip_text = ""
for c in cell.get_children():
c.queue_free()
func _icon(vnum: int) -> Texture2D:
var rel := ""
if item_list and item_list.has(vnum):
rel = item_list.icon(vnum)
if rel == "":
rel = "icon/item/%05d.tga" % ((vnum / 10) * 10) # basevnum 约定
return UiAssets.load_tex(assets_root, rel)
# --- 交互(右键用 / 拖动移动)---------------------------------------------
func _index_cells(root: Node) -> void:
_cells.clear()
for n in root.find_children("slot_*", "Panel", true, false):
if n.has_meta("slot_index"):
_cells[int(n.get_meta("slot_index"))] = n
func _wire_cells() -> void:
for idx in _cells:
var cell: Panel = _cells[idx]
var cell_idx: int = int(idx)
cell.gui_input.connect(func(e: InputEvent): _on_cell_input(cell_idx, e))
if item_mouse and item_mouse.has_method("register_target"):
item_mouse.register_target(cell, func(payload: Dictionary): return _drop_mouse_item(payload, cell_idx), self)
func _wire_money() -> void:
_money_button = _win.get("nodes", {}).get("Money_Slot", null)
if _money_button is BaseButton:
_money_button.pressed.connect(_on_money_pressed)
else:
_money_button = null
func _current_gold() -> int:
if client and client.has_method("get_points"):
return maxi(0, int(client.get_points().get("gold", 0)))
return 0
func _format_gold(gold: int) -> String:
return str(gold)
func _on_money_pressed() -> void:
if item_mouse == null or not item_mouse.has_method("attach_money"):
return
var gold := mini(_current_gold(), 9999999)
if gold <= 0:
return
_ask_count("金币数量", gold, func(amount: int): item_mouse.attach_money(amount, "inventory"))
func _add_extended_cells(root: Control) -> void:
# 旧 inventorywindow.py 只排出前 11 个传统装备位。剩余能力 / 时装 /
# 戒指 / 腰带与 4x4 腰带背包在同一扩展面板中保持可拖放。
var panel := Panel.new()
panel.name = "extended_equipment"
panel.position = Vector2(root.size.x + 8, 0)
panel.size = Vector2(204, 460)
var bg := StyleBoxFlat.new()
bg.bg_color = Color(0.04, 0.05, 0.07, 0.92)
bg.border_color = Color(0.45, 0.38, 0.2, 0.9)
bg.set_border_width_all(1)
panel.add_theme_stylebox_override("panel", bg)
root.add_child(panel)
# 旧 uiscript 只排 BODY..SHIELDwear 0..10)。EWearPositions 里 11..18 无位置,
# 扩展面板只补 costume(19/20) / ring(21/22) / belt(23) 这 5 个真实新装备位。
_add_grid(panel, "时装 / 戒指 / 腰带", WEAR_COSTUME_BODY, WEAR_BELT - WEAR_COSTUME_BODY + 1, EQUIP_BASE, Vector2(10, 30))
_add_grid(panel, "腰带背包", 0, BELT_CELL_COUNT, BELT_BASE, Vector2(10, 242))
_hint = Label.new()
_hint.position = Vector2(10, 430)
_hint.size = Vector2(184, 28)
_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_hint.add_theme_font_size_override("font_size", 10)
_hint.text = "Shift+左键:使用到物品\nCtrl+右键:赠与目标;Alt+右键:按数量丢弃"
panel.add_child(_hint)
func _add_grid(parent: Panel, title_text: String, first: int, count: int, base: int, pos: Vector2) -> void:
var title := Label.new()
title.text = title_text
title.position = pos - Vector2(0, 22)
title.add_theme_font_size_override("font_size", 13)
parent.add_child(title)
var grid := GridContainer.new()
grid.columns = 4
grid.position = pos
grid.add_theme_constant_override("h_separation", 4)
grid.add_theme_constant_override("v_separation", 4)
parent.add_child(grid)
for local in count:
var cell := Panel.new()
cell.name = "slot_%d" % (base + first + local)
cell.custom_minimum_size = Vector2(42, 42)
cell.set_meta("slot_index", base + first + local)
var style := StyleBoxFlat.new()
style.bg_color = Color(0.12, 0.13, 0.16, 0.95)
style.border_color = Color(0.35, 0.32, 0.22, 0.9)
style.set_border_width_all(1)
cell.add_theme_stylebox_override("panel", style)
grid.add_child(cell)
_cells[base + first + local] = cell
func _apply_belt_locks(eq: Array) -> void:
var grade := 0
if eq.size() > WEAR_BELT:
var belt: Dictionary = eq[WEAR_BELT]
if int(belt.get("vnum", 0)) != 0 and proto:
var data: Dictionary = proto.item(int(belt["vnum"]))
var values: Array = data.get("values", [])
if not values.is_empty():
grade = int(values[0])
for local in BELT_CELL_COUNT:
var cell: Panel = _cells.get(BELT_BASE + local, null)
if cell == null:
continue
var locked: bool = grade < int(BELT_RULES[local])
cell.set_meta("locked", locked)
cell.modulate = Color(0.45, 0.45, 0.45, 0.9) if locked else Color.WHITE
cell.mouse_filter = Control.MOUSE_FILTER_IGNORE if locked else Control.MOUSE_FILTER_STOP
if locked and int(cell.get_meta("vnum", 0)) == 0:
cell.tooltip_text = "需要腰带等级 %d" % BELT_RULES[local]
func _on_cell_input(ui_idx: int, e: InputEvent) -> void:
if _mobile_mode and e is InputEventScreenTouch:
var touch := e as InputEventScreenTouch
if touch.pressed:
_begin_mobile_cell_press(ui_idx, touch.index, touch.position)
elif touch.index == _mobile_touch_index:
_finish_mobile_cell_press(ui_idx, touch.index)
var cell: Panel = _cells.get(ui_idx, null)
if cell:
cell.accept_event()
return
if _mobile_mode and e is InputEventScreenDrag:
_handle_mobile_cell_drag(ui_idx, e as InputEventScreenDrag)
var drag_cell: Panel = _cells.get(ui_idx, null)
if drag_cell:
drag_cell.accept_event()
return
if not (e is InputEventMouseButton):
return
var cell: Panel = _cells[ui_idx]
if bool(cell.get_meta("locked", false)):
return
var vnum := int(cell.get_meta("vnum", 0))
if e.button_index == MOUSE_BUTTON_RIGHT and e.pressed and vnum != 0:
if e.shift_pressed:
_begin_combine(ui_idx)
return
if e.ctrl_pressed:
give_to_target(ui_idx)
return
if e.alt_pressed:
drop_count(ui_idx)
return
var w := _to_wire(ui_idx)
item_context.emit(w[0], w[1])
if context_consumer.is_valid() and bool(context_consumer.call(w[0], w[1])):
return
use(ui_idx)
elif e.button_index == MOUSE_BUTTON_LEFT:
if e.pressed and vnum != 0:
if e.shift_pressed:
if _item_count(ui_idx) > 1:
_split_stack(ui_idx)
else:
_begin_combine(ui_idx)
return
if _combine_from != -1:
if _combine_from != ui_idx:
_use_to_item(_combine_from, ui_idx)
return
if item_mouse and item_mouse.has_method("attach_item"):
var wire := _to_wire(ui_idx)
var metadata: Dictionary = {}
if client.has_method("get_item"):
metadata = client.get_item(wire[0], wire[1])
if item_mouse.attach_item(wire[0], wire[1], vnum, _item_count(ui_idx),
_icon(vnum), "inventory", metadata):
_drag_from = -1
return
_drag_from = ui_idx
elif not e.pressed and _drag_from != -1:
var over := _cell_under_mouse()
if over != -1 and over != _drag_from:
move_to(_drag_from, over, 1)
_drag_from = -1
func _mobile_cell_tap(ui_idx: int) -> void:
var cell: Panel = _cells.get(ui_idx, null)
if cell == null or bool(cell.get_meta("locked", false)):
_set_hint("该格当前不可用")
return
var vnum := int(cell.get_meta("vnum", 0))
if _mobile_selected_ui < 0:
if vnum == 0:
_set_hint("这里没有物品")
return
_mobile_selected_ui = ui_idx
_set_hint("已选择 %s;再次点按使用,点另一格移动" % _item_label(ui_idx))
return
if _mobile_selected_ui == ui_idx:
use(ui_idx)
mobile_selected_clear()
return
var from := _mobile_selected_ui
mobile_selected_clear()
if move_to(from, ui_idx, 1):
_set_hint("已发送移动请求")
else:
_set_hint("物品移动请求发送失败")
func _begin_mobile_cell_press(ui_idx: int, touch_index: int,
position := Vector2(-1, -1)) -> void:
var cell: Panel = _cells.get(ui_idx, null)
if cell == null or bool(cell.get_meta("locked", false)):
return
_mobile_press_cell = ui_idx
_mobile_touch_index = touch_index
_mobile_dragging = false
_mobile_drag_ui = -1
_mobile_long_press_ready = false
var center: Vector2 = _cells[ui_idx].get_global_rect().get_center()
_mobile_press_position_value = position if position.x >= 0.0 and position.y >= 0.0 else center
_mobile_long_press_token += 1
var token := _mobile_long_press_token
get_tree().create_timer(MOBILE_LONG_PRESS_SECONDS).timeout.connect(func():
if token != _mobile_long_press_token or _mobile_press_cell != ui_idx:
return
_mobile_long_press_ready = true
_show_mobile_detail(ui_idx))
func _finish_mobile_cell_press(ui_idx: int, touch_index: int) -> void:
if touch_index != _mobile_touch_index or ui_idx != _mobile_press_cell:
return
if _mobile_dragging:
_mobile_dragging = false
_mobile_drag_ui = -1
_mobile_press_cell = -1
_mobile_touch_index = -1
_mobile_press_position_value = Vector2.ZERO
_mobile_long_press_ready = false
return
_mobile_long_press_token += 1
_mobile_touch_index = -1
_mobile_press_cell = -1
_mobile_long_press_ready = false
if _mobile_detail_dialog == null:
_mobile_cell_tap(ui_idx)
func _handle_mobile_cell_drag(ui_idx: int, event: InputEventScreenDrag) -> void:
if event.index != _mobile_touch_index or ui_idx != _mobile_press_cell:
return
var distance := event.position.distance_to(_mobile_press_position())
if _mobile_dragging:
return
# A long press first exposes the item detail, as on the reference tooltip
# path. Continuing to drag that same finger converts the gesture into a
# real item drag, so a user can move across inventory/equipment targets
# without relying on right-click or a tiny desktop drag handle.
if not _mobile_long_press_ready or distance < MOBILE_DRAG_THRESHOLD:
return
_start_mobile_drag(ui_idx, event.index, event.position)
func _input(event: InputEvent) -> void:
# Once a long press has opened the detail card, the card can cover the
# source cell and steal GUI events. Keep the source touch at the feature
# level so dragging out of that card still reaches MouseController.
if not _mobile_mode or _mobile_touch_index < 0 or _mobile_press_cell < 0:
return
if event is InputEventScreenDrag and event.index == _mobile_touch_index:
_handle_mobile_cell_drag(_mobile_press_cell, event as InputEventScreenDrag)
elif event is InputEventScreenTouch and not event.pressed \
and event.index == _mobile_touch_index:
_finish_mobile_cell_press(_mobile_press_cell, event.index)
func _mobile_press_position() -> Vector2:
return _mobile_press_position_value
func _start_mobile_drag(ui_idx: int, touch_index: int, position: Vector2) -> void:
var cell: Panel = _cells.get(ui_idx, null)
if cell == null or int(cell.get_meta("vnum", 0)) == 0:
return
if item_mouse == null or not item_mouse.has_method("attach_item"):
_set_hint("当前窗口不支持拖放")
return
var wire := _to_wire(ui_idx)
var metadata: Dictionary = client.get_item(wire[0], wire[1]) \
if client and client.has_method("get_item") else {}
if _mobile_detail_dialog and is_instance_valid(_mobile_detail_dialog):
_close_mobile_detail()
_mobile_long_press_token += 1
_mobile_dragging = true
_mobile_drag_ui = ui_idx
item_mouse.attach_item(wire[0], wire[1], int(cell.get_meta("vnum", 0)),
_item_count(ui_idx), _icon(int(cell.get_meta("vnum", 0))), "inventory",
metadata, touch_index, position)
_set_hint("拖动中:松手放置到目标格")
func _show_mobile_detail(ui_idx: int) -> void:
var cell: Panel = _cells.get(ui_idx, null)
if cell == null or int(cell.get_meta("vnum", 0)) == 0 or ui == null:
return
if _mobile_detail_dialog and is_instance_valid(_mobile_detail_dialog):
ui.close(_mobile_detail_dialog)
var vnum := int(cell.get_meta("vnum", 0))
var count := int(cell.get_meta("count", 1))
var dialog := Panel.new()
dialog.name = "MobileItemDetail"
dialog.size = Vector2(360, 220)
dialog.set_meta("mobile_title", "物品详情")
dialog.set_meta("mobile_modal", true)
dialog.set_meta("mobile_modal_close", Callable(self, "_close_mobile_detail"))
var bg := StyleBoxFlat.new()
bg.bg_color = Color(0.04, 0.065, 0.1, 0.98)
bg.border_color = Color(0.72, 0.56, 0.25, 0.95)
bg.set_border_width_all(1)
bg.set_corner_radius_all(8)
dialog.add_theme_stylebox_override("panel", bg)
var title := Label.new()
title.text = _item_label(ui_idx)
title.position = Vector2(18, 52)
title.size = Vector2(324, 28)
title.add_theme_font_size_override("font_size", 15)
title.add_theme_color_override("font_color", Color(0.98, 0.84, 0.52))
dialog.add_child(title)
var detail := Label.new()
detail.text = "物品编号:%d\n数量:%d\n\n%s" % [vnum, count, String(cell.tooltip_text)]
detail.position = Vector2(18, 84)
detail.size = Vector2(324, 70)
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
detail.add_theme_font_size_override("font_size", 10)
detail.add_theme_color_override("font_color", Color(0.78, 0.86, 0.96))
dialog.add_child(detail)
var select := Button.new()
select.text = "选择"
select.position = Vector2(18, 168)
select.size = Vector2(100, 40)
select.pressed.connect(func():
_close_mobile_detail()
_mobile_cell_tap(ui_idx))
dialog.add_child(select)
var use_button := Button.new()
use_button.text = "使用"
use_button.position = Vector2(130, 168)
use_button.size = Vector2(100, 40)
use_button.pressed.connect(func():
_close_mobile_detail()
use(ui_idx)
mobile_selected_clear())
dialog.add_child(use_button)
var cancel := Button.new()
cancel.text = "关闭"
cancel.position = Vector2(242, 168)
cancel.size = Vector2(100, 40)
cancel.pressed.connect(_close_mobile_detail)
dialog.add_child(cancel)
_mobile_detail_dialog = dialog
ui.open(dialog, true)
func _close_mobile_detail() -> void:
var dialog := _mobile_detail_dialog
_mobile_detail_dialog = null
if dialog and is_instance_valid(dialog) and ui:
ui.close(dialog)
func _item_label(ui_idx: int) -> String:
var cell: Panel = _cells.get(ui_idx, null)
if cell == null:
return "物品"
var vnum := int(cell.get_meta("vnum", 0))
if proto and proto.has_method("item") and vnum != 0:
var data: Dictionary = proto.item(vnum)
var name := String(data.get("locale_name", data.get("name", "")))
if name != "":
return name
return "#%d" % vnum
func mobile_selected_clear() -> void:
_mobile_selected_ui = -1
func _drop_mouse_item(payload: Dictionary, target_ui: int) -> bool:
if client == null or payload.is_empty() or target_ui < 0:
return false
if bool(_cells.get(target_ui, null).get_meta("locked", false)) if _cells.get(target_ui, null) else true:
return false
var source_window := int(payload.get("window", -1))
var source_cell := int(payload.get("cell", -1))
if source_cell < 0:
return false
var target := _to_wire(target_ui)
if source_cell == int(target[1]) and source_window == int(target[0]):
return true
if source_window == WINDOW_SAFEBOX:
return target[0] == WINDOW_INVENTORY and client.has_method("safebox_checkout") \
and client.safebox_checkout(source_cell, target[0], target[1])
if source_window == WINDOW_MALL:
return target[0] == WINDOW_INVENTORY and client.has_method("mall_checkout") \
and client.mall_checkout(source_cell, target[0], target[1])
if source_window not in [WINDOW_INVENTORY, 2]:
return false
return move_wire(source_window, source_cell, int(target[0]), int(target[1]),
maxi(1, int(payload.get("count", 1))))
func move_wire(source_window: int, source_cell: int, target_window: int,
target_cell: int, count := 1) -> bool:
if client == null or not client.has_method("move_item"):
return false
return client.move_item(source_window, source_cell, target_window, target_cell, count)
# 把 ui 格子 from 的物品挪到 ui 格子 to(发 CG_ITEM_MOVE
func move_to(from_ui: int, to_ui: int, count := 1) -> bool:
if client == null:
return false
var a := _to_wire(from_ui)
var b := _to_wire(to_ui)
return client.move_item(a[0], a[1], b[0], b[1], count)
# 用掉 ui 格子里的物品(发 CG_ITEM_USE)。
# 对背包里的可穿戴物品(右键 = 穿),先跑 EquipRules.can_equip 前置门(§4.1 修改 2):
# 校验不过 → 发 equip_rejected 并不发包(服务器仍是最终仲裁)。
func use(ui_idx: int) -> bool:
if client == null:
return false
if ui_idx < EQUIP_BASE:
var cell: Panel = _cells.get(ui_idx, null)
var vnum := int(cell.get_meta("vnum", 0)) if cell else 0
if vnum != 0 and proto:
var pi: Dictionary = proto.item(vnum)
if EquipRules.is_equippable(pi):
var verdict := EquipRules.can_equip(pi, -1, _equip_ctx())
if not bool(verdict["ok"]):
_set_hint(_equip_reject_hint(verdict))
equip_rejected.emit(String(verdict["code"]), int(verdict["need"]))
return false
var w := _to_wire(ui_idx)
return client.use_item(w[0], w[1])
# 公开校验入口(tooltip / 拖放目标预览可用)。wear 传 -1 = 用物品默认槽。
func can_equip_item(vnum: int, wear := -1) -> Dictionary:
if vnum == 0 or proto == null:
return {"ok": false, "code": "NO_ITEM", "need": 0}
return EquipRules.can_equip(proto.item(vnum), wear, _equip_ctx())
# 组装 EquipRules.can_equip 需要的运行期上下文。属性点客户端未按名透出 → 传 -1(seam)。
func _equip_ctx() -> Dictionary:
var ctx := {"race": 0, "empire": 0, "level": 0, "st": -1, "dx": -1, "ht": -1, "iq": -1}
if client == null:
return ctx
if client.has_method("get_points"):
var p: Dictionary = client.get_points()
ctx["level"] = int(p.get("level", 0))
if client.has_method("get_entities"):
for e in client.get_entities():
if bool(e.get("is_main", false)):
ctx["race"] = int(e.get("race", 0))
ctx["empire"] = int(e.get("empire", 0))
if int(e.get("level", 0)) > 0:
ctx["level"] = int(e.get("level", 0))
break
return ctx
func _equip_reject_hint(verdict: Dictionary) -> String:
match String(verdict.get("code", "")):
"ANTI_SEX": return "该装备不符合你的性别"
"ANTI_JOB": return "该装备不符合你的职业"
"ANTI_EMPIRE": return "该装备不符合你的帝国"
"NOT_WEARABLE_HERE": return "该装备不能穿在这个位置"
"LIMIT_LEVEL": return "需要等级 %d" % int(verdict.get("need", 0))
"LIMIT_STR": return "需要力量 %d" % int(verdict.get("need", 0))
"LIMIT_DEX": return "需要敏捷 %d" % int(verdict.get("need", 0))
"LIMIT_INT": return "需要智力 %d" % int(verdict.get("need", 0))
"LIMIT_CON": return "需要体力 %d" % int(verdict.get("need", 0))
return "无法装备"
# Shift+左键(或 Shift+右键)选来源,再左键另一个非空格。
func _begin_combine(ui_idx: int) -> void:
if _combine_from == ui_idx:
_combine_from = -1
_set_hint("已取消物品对物品")
return
_combine_from = ui_idx
_drag_from = -1
_set_hint("请选择目标物品(左键);再次 Shift 点击来源格可取消")
func _split_stack(ui_idx: int) -> void:
var count := _item_count(ui_idx)
if count <= 1 or item_mouse == null or not item_mouse.has_method("attach_item"):
return
_ask_count("拾取数量", count, func(amount: int):
var wire := _to_wire(ui_idx)
var metadata: Dictionary = client.get_item(wire[0], wire[1]) if client.has_method("get_item") else {}
item_mouse.attach_item(wire[0], wire[1], int(_cells[ui_idx].get_meta("vnum", 0)),
amount, _icon(int(_cells[ui_idx].get_meta("vnum", 0))), "inventory", metadata))
func _use_to_item(source_ui: int, target_ui: int) -> bool:
var target: Panel = _cells.get(target_ui, null)
if target == null or int(target.get_meta("vnum", 0)) == 0:
_set_hint("目标格必须有物品")
return false
var source := _to_wire(source_ui)
var dest := _to_wire(target_ui)
_combine_from = -1
_set_hint("已发送物品对物品请求")
return client != null and client.use_item_to_item(source[0], source[1], dest[0], dest[1])
# Ctrl+右键把物品赠与当前选中的实体;数量由用户确认。
func give_to_target(ui_idx: int) -> void:
if client == null or not client.has_method("get_target"):
return
var target: Dictionary = client.get_target()
var target_vid := int(target.get("vid", 0))
if target_vid == 0:
_set_hint("请先选中要赠与的角色")
return
var wire := _to_wire(ui_idx)
_ask_count("赠与数量", _item_count(ui_idx), func(amount: int):
client.give_item(target_vid, wire[0], wire[1], amount))
# Alt+右键丢弃;非金币物品走 CG_ITEM_DROP2,精确保留用户填写的数量。
func drop_count(ui_idx: int) -> void:
if client == null:
return
var wire := _to_wire(ui_idx)
_ask_count("丢弃数量", _item_count(ui_idx), func(amount: int):
client.drop_item_count(wire[0], wire[1], 0, amount))
func _item_count(ui_idx: int) -> int:
var cell: Panel = _cells.get(ui_idx, null)
return max(1, int(cell.get_meta("count", 1))) if cell else 1
func _ask_count(title_text: String, max_count: int, on_confirm: Callable) -> void:
if ui == null:
return
var dialog := Panel.new()
dialog.position = Vector2(760, 420)
dialog.size = Vector2(360, 210) if _mobile_mode else Vector2(240, 132)
if _mobile_mode:
dialog.set_meta("mobile_title", title_text)
var bg := StyleBoxFlat.new()
bg.bg_color = Color(0.05, 0.06, 0.09, 0.97)
bg.border_color = Color(0.7, 0.55, 0.22, 0.9)
bg.set_border_width_all(1)
dialog.add_theme_stylebox_override("panel", bg)
var title := Label.new()
title.text = title_text
title.position = Vector2(18, 50) if _mobile_mode else Vector2(12, 10)
if _mobile_mode:
title.add_theme_font_size_override("font_size", 15)
dialog.add_child(title)
var amount_input: Control
if _mobile_mode:
var edit := LineEdit.new()
edit.name = "CountInput"
edit.text = "1"
edit.placeholder_text = "1 - %d" % max(1, max_count)
edit.max_length = 8
edit.virtual_keyboard_type = LineEdit.KEYBOARD_TYPE_NUMBER
edit.position = Vector2(18, 88)
edit.size = Vector2(324, 40)
amount_input = edit
dialog.add_child(edit)
else:
var spin := SpinBox.new()
spin.min_value = 1
spin.max_value = max(1, max_count)
spin.value = 1
spin.step = 1
spin.allow_greater = false
spin.position = Vector2(12, 42)
spin.size = Vector2(216, 28)
amount_input = spin
dialog.add_child(spin)
var ok := Button.new()
ok.text = "确认"
ok.position = Vector2(18, 150) if _mobile_mode else Vector2(76, 88)
ok.size = Vector2(154, 44) if _mobile_mode else Vector2(58, 28)
ok.pressed.connect(func():
var amount := int((amount_input as LineEdit).text) if amount_input is LineEdit else int((amount_input as SpinBox).value)
amount = clampi(amount, 1, max(1, max_count))
ui.close(dialog)
on_confirm.call(amount))
dialog.add_child(ok)
var cancel := Button.new()
cancel.text = "取消"
cancel.position = Vector2(188, 150) if _mobile_mode else Vector2(142, 88)
cancel.size = Vector2(154, 44) if _mobile_mode else Vector2(58, 28)
cancel.pressed.connect(func(): ui.close(dialog))
dialog.add_child(cancel)
ui.open(dialog, true)
func _set_hint(text: String) -> void:
if is_instance_valid(_hint):
_hint.text = text
func _cell_under_mouse() -> int:
for idx in _cells:
var cell: Panel = _cells[idx]
if cell.get_global_rect().has_point(cell.get_global_mouse_position()):
return int(idx)
return -1
# ui 格子编号 -> (window, cell)
func _to_wire(ui_idx: int) -> Array:
if ui_idx >= BELT_BASE:
return [WINDOW_INVENTORY, BELT_SLOT_START + ui_idx - BELT_BASE]
if ui_idx >= EQUIP_BASE:
var wear := ui_idx - EQUIP_BASE
# ENABLE_NEW_EQUIPMENT_SYSTEM 的 costume(19/20) / ring(21/22) / belt(23)
# 在客户端里没有独立窗,仍用 INVENTORY 窗的全局 cell109..113,见 GameType.h)。
if wear >= NEW_EQUIP_WEAR_START:
return [WINDOW_INVENTORY, EQUIP_BASE + wear]
# 旧 11 格(BODY..SHIELD)走 EQUIPMENT 窗(window=2+ 零基 wear cell。
return [WINDOW_EQUIPMENT, wear]
return [WINDOW_INVENTORY, ui_idx]