Files

367 lines
12 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()