- 装备校验:can_equip 按 CanEquipNow→FindEquipCell→EquipItem 精确重排, 新增 find_equip_cell() 1:1 复刻(RING/UNIQUE 占用翻转、COSTUME/BELT/DS), 帝国 antiflag 降级为 tooltip 提示,LIMIT_PCBANG/REAL_TIME* 语义与倒计时文案 - 快捷栏:死亡全槽禁用+激活守卫、变身(affect 220)技能置灰、 物品槽失配置灰(服务器回收/换武器后刷新) - 公会:GUILD_AUTH_* 权限位门控踢人/公告/技能升级,/gskillup、 被宣战应答弹窗(/war·/nowar)、资金存取行(40250 服务端已禁用仍保留 1:1) - 好友/情侣:messenger 三固定组(好友/公会/情侣),lover_login/logout/ near/far/divorce 命令事件→在线/在身边状态与离婚隐藏 - 交易/仓库:ExchangeState.last_notice(ALREADY/LESS_GOLD)结束浮层提示; 仓库改密 /safebox_change_password 三段输入;关窗补 /safebox_close·/mall_close; 仓库金钱经核实 40250 无 wire 协议,UI 定界只读 - app_flow:重连时 guild_marks_ready 重复连接加 is_connected 守卫 - 相机 look_at 零向量守卫(首帧/传送落点) - 测试:equip/love/guild 断言更新,79 项中 78 绿(game_camera_test 为 HEAD 既有失败);真服冒烟 LIVE_SMOKE PASS(192.168.21.203 全链路) - docs/CLIENT-GAP.md 同步增量 129 Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018kUFvQGYusuY3dkfGitmAe
879 lines
34 KiB
GDScript
879 lines
34 KiB
GDScript
# 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 格 wear(BODY..SHIELD)走 EQUIPMENT 窗(window=2, wear cell);
|
||
# costume/ring/belt(wear >= 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.gd,need = 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..SHIELD(wear 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())
|
||
|
||
# EPointTypes(服务器 char.h):ST=12 HT=13 DX=14 IQ=15;MOUNT=109(骑乘中坐骑 vnum)。
|
||
const POINT_ST := 12
|
||
const POINT_HT := 13
|
||
const POINT_DX := 14
|
||
const POINT_IQ := 15
|
||
const POINT_MOUNT := 109
|
||
|
||
# 组装 EquipRules.can_equip 需要的运行期上下文(服务器真实链所需字段全集)。
|
||
# 属性四维从 get_points()["points"] 全量数组取;worn 是 {wear_cell: vnum} 穿戴表,
|
||
# 供 RING/UNIQUE 占用翻转与 UNIQUE 同组去重;polymorphed/riding 供 EquipItem 后置门。
|
||
func _equip_ctx() -> Dictionary:
|
||
var ctx := {"race": 0, "empire": 0, "level": 0, "st": -1, "dx": -1, "ht": -1, "iq": -1,
|
||
"worn": {}, "polymorphed": false, "riding": false}
|
||
if client == null:
|
||
return ctx
|
||
if client.has_method("get_points"):
|
||
var p: Dictionary = client.get_points()
|
||
ctx["level"] = int(p.get("level", 0))
|
||
var vals: Array = p.get("points", [])
|
||
if vals.size() > POINT_MOUNT:
|
||
ctx["st"] = int(vals[POINT_ST])
|
||
ctx["ht"] = int(vals[POINT_HT])
|
||
ctx["dx"] = int(vals[POINT_DX])
|
||
ctx["iq"] = int(vals[POINT_IQ])
|
||
ctx["riding"] = int(vals[POINT_MOUNT]) > 0
|
||
# 穿戴表 {wear_cell: vnum}:旧 11 格来自 get_equipment()(下标即 wear cell);
|
||
# 新装备槽 19..23(costume/ring/belt)留在 INVENTORY 全局 cell 90+wear(见上方常量)。
|
||
var worn := {}
|
||
if client.has_method("get_equipment"):
|
||
var eq: Array = client.get_equipment()
|
||
for i in eq.size():
|
||
var vnum := int(eq[i].get("vnum", 0)) if eq[i] is Dictionary else 0
|
||
if vnum != 0:
|
||
worn[i] = vnum
|
||
if client.has_method("get_inventory"):
|
||
for it in client.get_inventory():
|
||
if it is Dictionary:
|
||
var cell := int(it.get("cell", -1))
|
||
if cell >= EQUIP_BASE + WEAR_COSTUME_BODY and cell < EQUIP_BASE + WEAR_COUNT:
|
||
worn[cell - EQUIP_BASE] = int(it.get("vnum", 0))
|
||
ctx["worn"] = worn
|
||
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))
|
||
ctx["polymorphed"] = int(e.get("polymorph", 0)) != 0 \
|
||
or int(e.get("poly_vnum", 0)) != 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 "该装备不符合你的帝国"
|
||
"UNIQUE_DUP": return "同类特殊装备只能佩戴一件"
|
||
"POLYMORPH": return "变身状态下无法更换装备"
|
||
"RIDE_TUXEDO": return "骑乘状态下无法穿礼服"
|
||
"RIDE_ITEM": 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 窗的全局 cell(109..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]
|