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

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
+194 -21
View File
@@ -10,39 +10,63 @@
# 拖到别的格=move_item。inventory_changed 信号来时刷新。
#
# 格子编号约定(uiscript):0..89 背包,90+ 装备(wear = index-90)。
# 新装备 / 腰带没有可靠的旧 uiscript 布局时,在背包右侧生成扩展面板发包
# 使用原客户端的 INVENTORY 全局 cell90+wear / 152+belt cell
# 新装备 / 腰带没有可靠的旧 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
const WEAR_BELT := 23
const WINDOW_INVENTORY := 1 # mtnet::WINDOW_*
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
func setup(ui_manager: CanvasLayer, m2client: Node, proto_node: Node, assets: String,
il: RefCounted = null) -> void:
@@ -51,6 +75,8 @@ func setup(ui_manager: CanvasLayer, m2client: Node, proto_node: Node, assets: St
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")
@@ -65,6 +91,10 @@ func toggle() -> void:
else: open()
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 is_open():
ui.close(_win["root"])
_win = {}
@@ -83,6 +113,7 @@ func open() -> void:
_index_cells(_win["root"])
_add_extended_cells(_win["root"])
_wire_cells()
_wire_money()
refresh()
# --- 填充 ---------------------------------------------------------------
@@ -95,34 +126,40 @@ func refresh() -> void:
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"]))
_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)))
for d in client.get_belt_inventory():
_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)))
_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) -> void:
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 itype := 0
var proto_data: Dictionary = {}
if proto:
var pd: Dictionary = proto.item(vnum)
if not pd.is_empty():
name = String(pd.get("locale_name", pd.get("name", name)))
itype = int(pd.get("type", 0))
cell.tooltip_text = "%s\n#%d%s" % [name, vnum, (" x%d" % count) if count > 1 else ""]
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()
@@ -152,6 +189,7 @@ func _fill_cell(cell: Panel, vnum: int, count: int) -> void:
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()
@@ -175,7 +213,33 @@ func _index_cells(root: Node) -> void:
func _wire_cells() -> void:
for idx in _cells:
var cell: Panel = _cells[idx]
cell.gui_input.connect(func(e: InputEvent): _on_cell_input(int(idx), e))
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 个传统装备位。剩余能力 / 时装 /
@@ -190,7 +254,9 @@ func _add_extended_cells(root: Control) -> void:
bg.set_border_width_all(1)
panel.add_theme_stylebox_override("panel", bg)
root.add_child(panel)
_add_grid(panel, "扩展装备", 11, WEAR_COUNT, EQUIP_BASE, Vector2(10, 30))
# 旧 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)
@@ -270,12 +336,24 @@ func _on_cell_input(ui_idx: int, e: InputEvent) -> void:
elif e.button_index == MOUSE_BUTTON_LEFT:
if e.pressed and vnum != 0:
if e.shift_pressed:
_begin_combine(ui_idx)
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()
@@ -283,6 +361,35 @@ func _on_cell_input(ui_idx: int, e: InputEvent) -> void:
move_to(_drag_from, over, 1)
_drag_from = -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:
@@ -291,13 +398,63 @@ func move_to(from_ui: int, to_ui: int, count := 1) -> bool:
var b := _to_wire(to_ui)
return client.move_item(a[0], a[1], b[0], b[1], count)
# 用掉 ui 格子里的物品(发 CG_ITEM_USE
# 用掉 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:
@@ -308,6 +465,16 @@ func _begin_combine(ui_idx: int) -> void:
_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:
@@ -399,5 +566,11 @@ 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:
return [WINDOW_INVENTORY, ui_idx]
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]