# CubeUI (첫 버전) —— 도깨비 방주 / 제작 (Cube) 窗。 # # var cu := preload("res://ui/cube_ui.gd").new() # add_child(cu) # cu.setup(m2client, canvas_parent, proto) # proto 可空 # # 服务器通过 GC_CHAT/COMMAND 的 "cube ..." 行驱动(见 EntityStore::apply_server_command): # cube_opened(npc) → 显示 + 请求配方列表 # cube_changed → 刷新配方 / 材料 / 金币 # cube_result(vnum,count,ok) → 结果提示 # cube_closed → 隐藏 # 选中一个配方、把背包物品放入材料槽 → [制作] → M2Client.cube_make(index)。 # 40250 服务端按材料槽匹配配方,`result_index` 只保留给 m2dev 后端;classic # 会安全地忽略它并执行一次 `/cube make`。 extends Node var client: Node var proto: Node var _root: Control var _title: Label var _status: Label var _list: VBoxContainer var _mat: VBoxContainer var _make_btn: Button var _sel := -1 var _cube_slots: Dictionary = {} # cube index:int -> inventory cell:int var _craft_in_flight := false var _cube_npc_vnum := 0 func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void: client = m2client proto = proto_node _build(parent) if client.has_signal("cube_opened"): client.cube_opened.connect(_on_open) if client.has_signal("cube_closed"): client.cube_closed.connect(func() -> void: _root.visible = false) if client.has_signal("cube_changed"): client.cube_changed.connect(refresh) if client.has_signal("cube_result"): client.cube_result.connect(_on_result) if client.has_signal("inventory_changed"): client.inventory_changed.connect(func(_window, _cell): refresh()) func is_open() -> bool: return _root != null and _root.visible func _on_open(npc_vnum: int) -> void: if npc_vnum <= 0: return var current: Dictionary = client.get_cube() if client.has_method("get_cube") else {} if current.has("open") and not bool(current.get("open", false)): return var current_npc := int(current.get("npc_vnum", 0)) if current_npc > 0 and current_npc != npc_vnum: return _sel = -1 _cube_slots.clear() _craft_in_flight = false _cube_npc_vnum = npc_vnum _root.visible = true _title.text = "제작 (Cube) · NPC #%d" % npc_vnum _status.text = "" if client.has_method("cube_request_result_list"): client.cube_request_result_list(npc_vnum) refresh() func _on_result(vnum: int, count: int, ok: bool) -> void: _craft_in_flight = false if ok: _status.text = "제작 성공: %s ×%d" % [_name_of(vnum), count] # 40250 在成功后消耗材料;等待 inventory/cube_changed 的最终状态前, # 先清掉本地映射,避免旧背包格继续显示为可用材料。 _cube_slots.clear() else: _status.text = "제작 실패" refresh() func _name_of(vnum: int) -> String: if proto and proto.has_method("item"): var d: Dictionary = proto.item(vnum) var n := String(d.get("locale_name", d.get("name", ""))) if n != "": return n return "#%d" % vnum func refresh() -> void: if client == null or _root == null or not _root.visible: return var cube: Dictionary = client.get_cube() if not cube.get("open", true): _root.visible = false return var recipes: Array = cube.get("recipes", []) var results: Array = cube.get("results", []) for c in _list.get_children(): c.queue_free() var n: int = maxi(recipes.size(), results.size()) if n == 0: var e := Label.new() e.text = "(이 NPC의 제작 목록 없음)" _list.add_child(e) for i in n: var vnum := 0 if i < recipes.size(): vnum = int((recipes[i] as Dictionary).get("result_vnum", 0)) if vnum == 0 and i < results.size(): vnum = int((results[i] as Dictionary).get("vnum", 0)) var b := Button.new() b.toggle_mode = true b.button_pressed = (i == _sel) b.text = "%d. %s" % [i + 1, _name_of(vnum)] var idx := i b.pressed.connect(func() -> void: _select(idx)) _list.add_child(b) _refresh_materials() func _select(i: int) -> void: _sel = i for k in _list.get_child_count(): var b := _list.get_child(k) if b is Button: b.button_pressed = (k == i) # 材料信息可能未拉过 → 请求 if client.has_method("cube_request_materials"): client.cube_request_materials(i, 1) _refresh_materials() func _refresh_materials() -> void: for c in _mat.get_children(): c.queue_free() var cube: Dictionary = client.get_cube() var recipes: Array = cube.get("recipes", []) var recipe: Dictionary = _selected_recipe() _make_btn.disabled = _sel < 0 or recipe.is_empty() or _craft_in_flight \ or not _recipe_can_craft(recipe) if _sel < 0 or _sel >= recipes.size(): _mat.add_child(_lbl("재료 정보를 불러오는 중…", 12)) else: var r: Dictionary = recipe _mat.add_child(_lbl("필요 골드: %d" % int(r.get("gold", 0)), 12)) var groups: Array = r.get("material_groups", []) if groups.is_empty(): _mat.add_child(_lbl("(재료 없음)", 12)) for g in groups: var parts: Array = [] for s in (g as Array): parts.append("%s ×%d" % [_name_of(int((s as Dictionary).get("vnum", 0))), int((s as Dictionary).get("count", 1))]) _mat.add_child(_lbl("• " + " / ".join(parts), 12)) if not _recipe_can_craft(r): _mat.add_child(_lbl("(材料不足或未放入材料槽)", 11)) elif client.has_method("get_points") and int(r.get("gold", 0)) > int(client.get_points().get("gold", 0)): _make_btn.disabled = true _mat.add_child(_lbl("(金币不足)", 11)) _mat.add_child(HSeparator.new()) _mat.add_child(_lbl("재료 슬롯 (클릭하면 추가/삭제)", 12)) for slot in _cube_slots.keys(): var cell := int(_cube_slots[slot]) var row := HBoxContainer.new() var label := _lbl("cube[%d] ← bag[%d]" % [int(slot), cell], 11) row.add_child(label) var remove := Button.new() remove.text = "삭제" remove.pressed.connect(func() -> void: if client.has_method("cube_delete_item") and client.cube_delete_item(int(slot)): _cube_slots.erase(slot) refresh()) row.add_child(remove) _mat.add_child(row) _mat.add_child(_lbl("背包物品:", 12)) var used_cells := {} for cell in _cube_slots.values(): used_cells[int(cell)] = true var inventory: Array = client.get_inventory() if client.has_method("get_inventory") else [] var added := 0 for item in inventory: var inv_cell := int(item.get("cell", -1)) var item_vnum := int(item.get("vnum", 0)) if used_cells.has(inv_cell): continue if not _recipe_accepts_vnum(recipe, item_vnum): continue var free_slot := _first_free_cube_slot() if free_slot < 0: break var add := Button.new() add.text = "+ bag[%d] %s ×%d" % [inv_cell, _name_of(item_vnum), int(item.get("count", 1))] add.alignment = HORIZONTAL_ALIGNMENT_LEFT add.pressed.connect(func() -> void: var target := _first_free_cube_slot() if target >= 0 and client.has_method("cube_add_item") and client.cube_add_item(target, inv_cell): _cube_slots[target] = inv_cell refresh()) _mat.add_child(add) added += 1 if added == 0: _mat.add_child(_lbl("(可用背包物品 없음)", 11)) func _selected_recipe() -> Dictionary: if client == null or not client.has_method("get_cube"): return {} var recipes: Array = client.get_cube().get("recipes", []) if _sel < 0 or _sel >= recipes.size(): return {} return recipes[_sel] as Dictionary func _recipe_accepts_vnum(recipe: Dictionary, vnum: int) -> bool: if vnum <= 0: return false for group in recipe.get("material_groups", []): for option in group as Array: if int((option as Dictionary).get("vnum", 0)) == vnum: return true return false func _recipe_can_craft(recipe: Dictionary) -> bool: if recipe.is_empty(): return false var provided := {} if client.has_method("get_inventory"): var inventory: Array = client.get_inventory() for cell in _cube_slots.values(): for item in inventory: if int(item.get("cell", -1)) == int(cell): var vnum := int(item.get("vnum", 0)) provided[vnum] = int(provided.get(vnum, 0)) + maxi(1, int(item.get("count", 1))) break for group in recipe.get("material_groups", []): var matched := false for option in group as Array: var material: Dictionary = option as Dictionary var vnum := int(material.get("vnum", 0)) var need := maxi(1, int(material.get("count", 1))) if int(provided.get(vnum, 0)) >= need: provided[vnum] = int(provided[vnum]) - need matched = true break if not matched: return false return true func _first_free_cube_slot() -> int: for i in 24: if not _cube_slots.has(i): return i return -1 func _lbl(t: String, sz: int) -> Label: var l := Label.new() l.text = t l.add_theme_font_size_override("font_size", sz) return l func _build(parent: Node) -> void: _root = Panel.new() _root.set_anchors_preset(Control.PRESET_CENTER) _root.position = Vector2(-200, -190) _root.custom_minimum_size = Vector2(400, 380) _root.size = Vector2(400, 380) _root.visible = false parent.add_child(_root) var box := VBoxContainer.new() box.position = Vector2(16, 14) box.custom_minimum_size = Vector2(368, 0) box.add_theme_constant_override("separation", 6) _root.add_child(box) _title = _lbl("제작 (Cube)", 16) box.add_child(_title) var split := HBoxContainer.new() split.add_theme_constant_override("separation", 10) box.add_child(split) var lsc := ScrollContainer.new() lsc.custom_minimum_size = Vector2(180, 250) split.add_child(lsc) _list = VBoxContainer.new() _list.add_theme_constant_override("separation", 3) lsc.add_child(_list) _mat = VBoxContainer.new() _mat.custom_minimum_size = Vector2(178, 250) _mat.add_theme_constant_override("separation", 3) split.add_child(_mat) _status = _lbl("", 12) _status.add_theme_color_override("font_color", Color(1, 0.9, 0.5)) box.add_child(_status) var brow := HBoxContainer.new() brow.add_theme_constant_override("separation", 12) box.add_child(brow) _make_btn = Button.new() _make_btn.text = "제작" _make_btn.disabled = true _make_btn.pressed.connect(func() -> void: var recipe := _selected_recipe() if _sel < 0 or recipe.is_empty(): _status.text = "请先选择配方" return if not _recipe_can_craft(recipe): _status.text = "材料不足或未放入材料槽" refresh() return if client.has_method("cube_make") and client.cube_make(_sel): _craft_in_flight = true _status.text = "制作中…" _make_btn.disabled = true) brow.add_child(_make_btn) var close := Button.new() close.text = "닫기" close.pressed.connect(func() -> void: if client.has_method("cube_close"): client.cube_close() _root.visible = false) brow.add_child(close)