# 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/.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 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) func is_open() -> bool: return not _win.is_empty() and is_instance_valid(_win.get("root")) func toggle() -> void: if is_open(): close() 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 = {} _cells.clear() _combine_from = -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 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 _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(240, 132) 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(12, 10) dialog.add_child(title) 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) dialog.add_child(spin) var ok := Button.new() ok.text = "确认" ok.position = Vector2(76, 88) ok.pressed.connect(func(): var amount := int(spin.value) ui.close(dialog) on_confirm.call(amount)) dialog.add_child(ok) var cancel := Button.new() cancel.text = "取消" cancel.position = Vector2(142, 88) 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]