Files
mtgodot-poc/project/ui/private_shop_ui.gd
T

384 lines
13 KiB
GDScript
Raw 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.
# 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 里是 textLabel)—— 盖一个 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()