384 lines
13 KiB
GDScript
384 lines
13 KiB
GDScript
# PrivateShopUI —— 개인상점 개설창 (CG_MYSHOP).1:1 迁移 `assets/root/uiprivateshopbuilder.py`
|
||
# 的 `PrivateShopBuilder`(布局走真 `assets/uiscript/uiscript/privateshopbuilder.py`)。
|
||
#
|
||
# var pu := preload("res://ui/private_shop_ui.gd").new()
|
||
# add_child(pu)
|
||
# pu.setup(m2client, ui_manager, proto, assets_root)
|
||
# pu.toggle()
|
||
#
|
||
# 布局:`privateshopbuilder.py`(board + TitleBar + NameLine + ItemSlot 5×8=40 grid + Ok/Close)。
|
||
# 交互 = uiprivateshopbuilder.itemStock:左侧(挂在窗右边的附加面板)列背包候选,点一件
|
||
# “拿起”,再点右侧 40 格的空格 → 价格输入(`uiCommon.MoneyInputDialog`)→ 落位
|
||
# (`AddPrivateShopItemStock` + `itemStock[targetSlot]=(src)`);点已占用格 → 撤下
|
||
# (`OnSelectItemSlot` / `DelPrivateShopItemStock`)。OkButton(`BuildPrivateShop`):stock 按
|
||
# 格号排序、`display_pos = 格号`、上限 39(`PRIVATE_SHOP_ITEM_MAX_NUM`)打包成
|
||
# `M2Client.open_private_shop(sign, [{vnum,count,inv_cell,price,display_pos}])`
|
||
# (= `TPacketCGMyShop` + `TShopItemTable`×N)。40250 收摊用空的 `CG_MYSHOP`
|
||
# (服务端 `OpenMyShop` 会先关闭已有的个人摊位);`close_private_shop()` 已在
|
||
# M2Client classic 分支按该语义发送。
|
||
extends Node
|
||
|
||
class PrivateShopDragButton extends Button:
|
||
var drag_entry: Dictionary = {}
|
||
|
||
func _get_drag_data(_at_position: Vector2):
|
||
if drag_entry.is_empty():
|
||
return null
|
||
var preview := Label.new()
|
||
preview.text = String(text)
|
||
preview.add_theme_color_override("font_color", Color(1, 0.9, 0.55))
|
||
set_drag_preview(preview)
|
||
return {"private_shop_entry": drag_entry.duplicate(true)}
|
||
|
||
const PRIVATE_SHOP_ITEM_MAX := 39 # PRIVATE_SHOP_ITEM_MAX_NUM (TPacketCGMyShop::bCount 上限)
|
||
const SIGN_MAX := 25 # NameLine input_limit
|
||
|
||
var client: Node
|
||
var ui: CanvasLayer # UiManager
|
||
var proto: Node
|
||
var assets_root := ""
|
||
var uiscript_dir := ""
|
||
|
||
var _win: Dictionary = {}
|
||
var _sign: LineEdit
|
||
var _inv_list: VBoxContainer
|
||
var _cells: Dictionary = {} # 格号:int -> Panel
|
||
var _status: Label
|
||
var _stock: Dictionary = {} # 格号:int -> {cell, vnum, count, price}
|
||
var _picked = null # {cell, vnum, count} 等待落位
|
||
var _price_dialog: Control = null
|
||
var _server_prices: Dictionary = {} # vnum -> last price returned by MyShopPriceList
|
||
|
||
func setup(m2client: Node, ui_manager: CanvasLayer, proto_node: Node = null, assets := "") -> void:
|
||
client = m2client
|
||
ui = ui_manager
|
||
proto = proto_node
|
||
assets_root = assets
|
||
if assets_root == "" and ui and "assets_root" in ui:
|
||
assets_root = ui.assets_root
|
||
uiscript_dir = assets_root.path_join("uiscript/uiscript")
|
||
if not DirAccess.dir_exists_absolute(uiscript_dir):
|
||
uiscript_dir = assets_root.path_join("uiscript")
|
||
if client and client.has_signal("inventory_changed"):
|
||
client.inventory_changed.connect(func(_w, _c): if is_open(): _refresh())
|
||
if client and client.has_signal("my_shop_price_list"):
|
||
client.my_shop_price_list.connect(_on_server_price)
|
||
if client and client.has_signal("private_shop_open_requested"):
|
||
client.private_shop_open_requested.connect(open)
|
||
|
||
# --- open / close --------------------------------------------------
|
||
|
||
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:
|
||
_dismiss_price_dialog()
|
||
if is_open():
|
||
ui.close(_win["root"])
|
||
_win = {}
|
||
_cells.clear()
|
||
_stock.clear()
|
||
_picked = null
|
||
|
||
func open() -> void:
|
||
if is_open():
|
||
return
|
||
var path := uiscript_dir.path_join("privateshopbuilder.py")
|
||
if not FileAccess.file_exists(path):
|
||
push_warning("PrivateShopUI: no privateshopbuilder.py at " + path)
|
||
return
|
||
_stock.clear()
|
||
_picked = null
|
||
_win = ui.open_script(path, assets_root)
|
||
if not is_open():
|
||
return
|
||
var root: Control = _win["root"]
|
||
_index_cells(root)
|
||
_overlay_name_edit(root)
|
||
_add_inv_panel(root)
|
||
_wire_buttons(root)
|
||
_refresh()
|
||
|
||
func _node(nm: String) -> Control:
|
||
if _win.is_empty():
|
||
return null
|
||
var n = _win.get("nodes", {}).get(nm, null)
|
||
return n if n is Control else null
|
||
|
||
func _index_cells(root: Node) -> void:
|
||
_cells.clear()
|
||
for n in root.find_children("slot_*", "Panel", true, false):
|
||
if not n.has_meta("slot_index"):
|
||
continue
|
||
var slot := int(n.get_meta("slot_index"))
|
||
_cells[slot] = n
|
||
var idx := slot
|
||
n.gui_input.connect(func(e: InputEvent) -> void:
|
||
if e is InputEventMouseButton and e.button_index == MOUSE_BUTTON_LEFT and e.pressed:
|
||
_on_slot_clicked(idx))
|
||
# 同时支持从右侧背包候选区拖到空货位;点击选取仍保留给触屏/无鼠标场景。
|
||
n.set_drag_forwarding(Callable(self, "_no_drag_data"),
|
||
Callable(self, "_can_drop_slot").bind(idx),
|
||
Callable(self, "_drop_slot").bind(idx))
|
||
|
||
# NameLine 在 uiscript 里是 text(Label)—— 盖一个 LineEdit 上去
|
||
func _overlay_name_edit(root: Control) -> void:
|
||
var name_line := _node("NameLine")
|
||
_sign = LineEdit.new()
|
||
_sign.max_length = SIGN_MAX
|
||
_sign.placeholder_text = "상점 이름"
|
||
if name_line:
|
||
name_line.visible = false
|
||
var slot := name_line.get_parent()
|
||
_sign.position = name_line.position
|
||
_sign.size = Vector2(maxf(name_line.size.x, 150), 18)
|
||
(slot if slot is Control else root).add_child(_sign)
|
||
else:
|
||
_sign.position = Vector2(13, 35)
|
||
_sign.size = Vector2(157, 18)
|
||
root.add_child(_sign)
|
||
|
||
func _wire_buttons(root: Control) -> void:
|
||
var ok := _node("OkButton")
|
||
if ok is BaseButton:
|
||
ok.pressed.connect(_ok)
|
||
var cl := _node("CloseButton")
|
||
if cl is BaseButton:
|
||
cl.pressed.connect(close)
|
||
var tb := _node("TitleBar")
|
||
if tb:
|
||
for b in tb.find_children("*", "BaseButton", true, false):
|
||
b.pressed.connect(close)
|
||
|
||
# 원 클라는 메인 인벤토리에서 드래그 —— 여기선 창 오른쪽에 후보 패널을 붙임
|
||
func _add_inv_panel(root: Control) -> void:
|
||
var panel := Panel.new()
|
||
panel.name = "inv_candidates"
|
||
panel.position = Vector2(root.size.x + 6, 0)
|
||
panel.size = Vector2(220, root.size.y)
|
||
var bg := StyleBoxFlat.new()
|
||
bg.bg_color = Color(0.06, 0.06, 0.09, 0.95)
|
||
bg.border_color = Color(0.4, 0.36, 0.24, 0.9)
|
||
bg.set_border_width_all(1)
|
||
panel.add_theme_stylebox_override("panel", bg)
|
||
root.add_child(panel)
|
||
var title := Label.new()
|
||
title.text = "배낭 (클릭 → 집기)"
|
||
title.position = Vector2(10, 8)
|
||
title.add_theme_font_size_override("font_size", 12)
|
||
panel.add_child(title)
|
||
var sc := ScrollContainer.new()
|
||
sc.position = Vector2(8, 30)
|
||
sc.size = Vector2(204, root.size.y - 66)
|
||
panel.add_child(sc)
|
||
_inv_list = VBoxContainer.new()
|
||
_inv_list.add_theme_constant_override("separation", 3)
|
||
sc.add_child(_inv_list)
|
||
_status = Label.new()
|
||
_status.position = Vector2(10, root.size.y - 30)
|
||
_status.add_theme_font_size_override("font_size", 11)
|
||
_status.add_theme_color_override("font_color", Color(1, 0.9, 0.5))
|
||
panel.add_child(_status)
|
||
|
||
# --- 명칭 --------------------------------------------------------------
|
||
|
||
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:
|
||
_refresh_inv()
|
||
_refresh_grid()
|
||
|
||
func _refresh_inv() -> void:
|
||
if not is_instance_valid(_inv_list):
|
||
return
|
||
for c in _inv_list.get_children():
|
||
_inv_list.remove_child(c)
|
||
c.queue_free()
|
||
var used := {}
|
||
for s in _stock.values():
|
||
used[int(s["cell"])] = true
|
||
var inv: Array = client.get_inventory() if client and client.has_method("get_inventory") else []
|
||
var shown := 0
|
||
for it in inv:
|
||
var cell := int(it.get("cell", 0))
|
||
if used.has(cell):
|
||
continue
|
||
var vnum := int(it.get("vnum", 0))
|
||
var count := int(it.get("count", 1))
|
||
var b := PrivateShopDragButton.new()
|
||
b.text = "%s ×%d" % [_name_of(vnum), count]
|
||
b.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||
b.custom_minimum_size = Vector2(196, 22)
|
||
b.toggle_mode = true
|
||
var entry := {"cell": cell, "vnum": vnum, "count": count}
|
||
b.drag_entry = entry
|
||
b.pressed.connect(func() -> void: _pick(entry, b))
|
||
if _picked != null and int(_picked["cell"]) == cell:
|
||
b.button_pressed = true
|
||
_inv_list.add_child(b)
|
||
shown += 1
|
||
if shown == 0:
|
||
var l := Label.new()
|
||
l.text = "(배낭이 비어 있음)"
|
||
l.add_theme_font_size_override("font_size", 12)
|
||
_inv_list.add_child(l)
|
||
|
||
func _refresh_grid() -> void:
|
||
for slot in _cells.keys():
|
||
var cell: Panel = _cells[slot]
|
||
for ch in cell.get_children():
|
||
cell.remove_child(ch)
|
||
ch.queue_free()
|
||
cell.tooltip_text = ""
|
||
if _stock.has(slot):
|
||
var s: Dictionary = _stock[slot]
|
||
var nm := Label.new()
|
||
nm.text = _name_of(int(s["vnum"])).substr(0, 4)
|
||
nm.add_theme_font_size_override("font_size", 8)
|
||
nm.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
cell.add_child(nm)
|
||
var pr := Label.new()
|
||
pr.text = str(int(s["price"]))
|
||
pr.position = Vector2(1, 18)
|
||
pr.add_theme_font_size_override("font_size", 8)
|
||
pr.add_theme_color_override("font_color", Color(0.95, 0.85, 0.5))
|
||
pr.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
cell.add_child(pr)
|
||
cell.tooltip_text = "%s ×%d\n%d 냥" % [_name_of(int(s["vnum"])), int(s["count"]), int(s["price"])]
|
||
cell.modulate = Color(1, 1, 1)
|
||
else:
|
||
cell.modulate = Color(0.72, 0.72, 0.82) if _picked != null else Color(1, 1, 1)
|
||
|
||
# --- 상호작용 --------------------------------------------------------
|
||
|
||
func _pick(entry: Dictionary, _btn: Button) -> void:
|
||
if _picked != null and int(_picked["cell"]) == int(entry["cell"]):
|
||
_picked = null
|
||
else:
|
||
_picked = entry
|
||
_refresh_grid()
|
||
|
||
func _no_drag_data(_at_position: Vector2):
|
||
return null
|
||
|
||
func _can_drop_slot(_at_position: Vector2, data, slot: int) -> bool:
|
||
return slot >= 0 and slot < 40 and not _stock.has(slot) \
|
||
and data is Dictionary and data.has("private_shop_entry")
|
||
|
||
func _drop_slot(_at_position: Vector2, data, slot: int) -> void:
|
||
if not _can_drop_slot(Vector2.ZERO, data, slot):
|
||
return
|
||
var entry: Dictionary = data["private_shop_entry"] as Dictionary
|
||
_picked = entry.duplicate(true)
|
||
_ask_price(slot)
|
||
|
||
func _on_slot_clicked(slot: int) -> void:
|
||
if _stock.has(slot):
|
||
_stock.erase(slot)
|
||
if is_instance_valid(_status):
|
||
_status.text = ""
|
||
_refresh()
|
||
return
|
||
if _picked == null:
|
||
return
|
||
_ask_price(slot)
|
||
|
||
func _ask_price(slot: int) -> void:
|
||
_dismiss_price_dialog()
|
||
var dlg := Panel.new()
|
||
dlg.set_anchors_preset(Control.PRESET_CENTER)
|
||
dlg.position = Vector2(-120, -70)
|
||
dlg.size = Vector2(240, 128)
|
||
var sb := StyleBoxFlat.new()
|
||
sb.bg_color = Color(0.06, 0.06, 0.09, 0.98)
|
||
sb.border_color = Color(0.7, 0.55, 0.22, 0.9)
|
||
sb.set_border_width_all(1)
|
||
dlg.add_theme_stylebox_override("panel", sb)
|
||
var t := Label.new()
|
||
t.text = "판매 가격"
|
||
t.position = Vector2(12, 10)
|
||
dlg.add_child(t)
|
||
var spin := SpinBox.new()
|
||
spin.min_value = 1
|
||
spin.max_value = 2000000000
|
||
spin.step = 1
|
||
var default_price := int(_server_prices.get(int(_picked["vnum"]), 1))
|
||
if _stock.has(slot):
|
||
default_price = int(_stock[slot].get("price", default_price))
|
||
spin.value = default_price
|
||
spin.position = Vector2(12, 40)
|
||
spin.size = Vector2(216, 28)
|
||
dlg.add_child(spin)
|
||
var ok := Button.new()
|
||
ok.text = "확인"
|
||
ok.position = Vector2(60, 86)
|
||
ok.pressed.connect(func() -> void: _place(slot, int(spin.value)))
|
||
dlg.add_child(ok)
|
||
var cancel := Button.new()
|
||
cancel.text = "취소"
|
||
cancel.position = Vector2(130, 86)
|
||
cancel.pressed.connect(_dismiss_price_dialog)
|
||
dlg.add_child(cancel)
|
||
_win["root"].get_parent().add_child(dlg)
|
||
_price_dialog = dlg
|
||
|
||
func _dismiss_price_dialog() -> void:
|
||
if is_instance_valid(_price_dialog):
|
||
_price_dialog.queue_free()
|
||
_price_dialog = null
|
||
|
||
func _place(slot: int, price: int) -> void:
|
||
if slot < 0 or slot >= 40 or price <= 0 or price > 2000000000 or _picked == null:
|
||
_dismiss_price_dialog()
|
||
return
|
||
_stock[slot] = {"cell": int(_picked["cell"]), "vnum": int(_picked["vnum"]),
|
||
"count": int(_picked["count"]), "price": price}
|
||
_picked = null
|
||
_dismiss_price_dialog()
|
||
_refresh()
|
||
|
||
func _on_server_price(vnum: int, price: int) -> void:
|
||
if vnum > 0 and price >= 0:
|
||
_server_prices[vnum] = price
|
||
|
||
func _ok() -> void:
|
||
var sign := _sign.text.strip_edges()
|
||
if sign.is_empty() or sign.length() > SIGN_MAX:
|
||
if is_instance_valid(_status):
|
||
_status.text = "请输入 1~%d 个字符的摊位名称" % SIGN_MAX
|
||
return
|
||
if _stock.is_empty():
|
||
if is_instance_valid(_status):
|
||
_status.text = "판매할 아이템을 올리세요"
|
||
return
|
||
var slots := _stock.keys()
|
||
slots.sort()
|
||
var items: Array = []
|
||
for slot in slots:
|
||
if items.size() >= PRIVATE_SHOP_ITEM_MAX:
|
||
break
|
||
var s: Dictionary = _stock[slot]
|
||
items.append({"vnum": s["vnum"], "count": s["count"], "inv_cell": s["cell"],
|
||
"price": s["price"], "display_pos": slot})
|
||
if client and client.has_method("open_private_shop"):
|
||
if client.open_private_shop(sign, items):
|
||
close()
|
||
elif is_instance_valid(_status):
|
||
_status.text = "摆摊请求发送失败"
|
||
|
||
func _close_shop() -> void:
|
||
if client and client.has_method("close_private_shop"):
|
||
client.close_private_shop()
|