367 lines
12 KiB
GDScript
367 lines
12 KiB
GDScript
# 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
|
||
var _mobile_mode := false
|
||
var _box: VBoxContainer
|
||
var _recipe_scroll: ScrollContainer
|
||
var _material_scroll: ScrollContainer
|
||
|
||
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 get_mobile_window() -> Control:
|
||
return _root
|
||
|
||
func set_mobile_mode(enabled: bool) -> void:
|
||
_mobile_mode = enabled
|
||
_apply_mobile_layout()
|
||
|
||
func close() -> void:
|
||
if _root:
|
||
_root.visible = false
|
||
_sel = -1
|
||
_cube_slots.clear()
|
||
_craft_in_flight = false
|
||
|
||
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
|
||
_apply_mobile_layout()
|
||
_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 _apply_mobile_layout() -> void:
|
||
if _root == null or _box == null:
|
||
return
|
||
if _mobile_mode:
|
||
# Recipe selection and material/source-bag selection remain side by side
|
||
# in landscape. Both columns scroll independently so the craft action
|
||
# stays reachable when a recipe has many material alternatives.
|
||
_root.size = Vector2(720, 380)
|
||
_root.position = Vector2(-360, -190)
|
||
_box.position = Vector2(16, 48)
|
||
_box.size = Vector2(688, 318)
|
||
_box.custom_minimum_size = Vector2(688, 318)
|
||
_recipe_scroll.custom_minimum_size = Vector2(248, 254)
|
||
_material_scroll.custom_minimum_size = Vector2(430, 254)
|
||
_mat.custom_minimum_size = Vector2(414, 254)
|
||
else:
|
||
_root.size = Vector2(400, 380)
|
||
_root.position = Vector2(-200, -190)
|
||
_box.position = Vector2(16, 14)
|
||
_box.size = Vector2(368, 350)
|
||
_box.custom_minimum_size = Vector2(368, 350)
|
||
_recipe_scroll.custom_minimum_size = Vector2(180, 250)
|
||
_material_scroll.custom_minimum_size = Vector2(178, 250)
|
||
_mat.custom_minimum_size = Vector2(178, 250)
|
||
|
||
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)
|
||
_box = VBoxContainer.new()
|
||
var box := _box
|
||
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()
|
||
_recipe_scroll = lsc
|
||
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)
|
||
_material_scroll = ScrollContainer.new()
|
||
_material_scroll.custom_minimum_size = Vector2(178, 250)
|
||
_material_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
split.add_child(_material_scroll)
|
||
_mat = VBoxContainer.new()
|
||
_mat.custom_minimum_size = Vector2(178, 250)
|
||
_mat.add_theme_constant_override("separation", 3)
|
||
_material_scroll.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)
|
||
_apply_mobile_layout()
|