# 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)。철수 `close_private_shop()` = SHOP_CG_END。 extends Node 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 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()) # --- 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)) # 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 := Button.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.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 _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 spin.value = int(_stock.get(slot, {}).get("price", 1)) 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 price <= 0 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 _ok() -> void: 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"): client.open_private_shop(_sign.text.strip_edges(), items) close() func _close_shop() -> void: if client and client.has_method("close_private_shop"): client.close_private_shop()