Implement 40250 classic client port

This commit is contained in:
shenlei
2026-09-01 18:49:28 +09:00
parent 2277b1dd60
commit 8f61990a83
481 changed files with 169826 additions and 293 deletions
+158 -17
View File
@@ -9,7 +9,9 @@
# cube_changed → 刷新配方 / 材料 / 金币
# cube_result(vnum,count,ok) → 结果提示
# cube_closed → 隐藏
# 选中一个配方 → [制作] → M2Client.cube_make(index)。
# 选中一个配方、把背包物品放入材料槽 → [制作] → M2Client.cube_make(index)。
# 40250 服务端按材料槽匹配配方,`result_index` 只保留给 m2dev 后端;classic
# 会安全地忽略它并执行一次 `/cube make`。
extends Node
var client: Node
@@ -21,6 +23,9 @@ 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
@@ -34,12 +39,25 @@ func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
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 = ""
@@ -48,10 +66,15 @@ func _on_open(npc_vnum: int) -> void:
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"):
@@ -65,6 +88,9 @@ 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():
@@ -105,21 +131,123 @@ func _refresh_materials() -> void:
c.queue_free()
var cube: Dictionary = client.get_cube()
var recipes: Array = cube.get("recipes", [])
_make_btn.disabled = _sel < 0
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))
return
var r: Dictionary = recipes[_sel]
_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))
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()
@@ -165,10 +293,23 @@ func _build(parent: Node) -> void:
_make_btn.text = "제작"
_make_btn.disabled = true
_make_btn.pressed.connect(func() -> void:
if _sel >= 0 and client.has_method("cube_make"):
client.cube_make(_sel))
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: _root.visible = false)
close.pressed.connect(func() -> void:
if client.has_method("cube_close"):
client.cube_close()
_root.visible = false)
brow.add_child(close)