extends PanelContainer ## Satıcı dükkanı — bir NPC vendor'ın stoğu. Şehir haritasındaki pine tıklayınca açılır. ## İki sekme: 🛒 Satın Al (sabit stok + oyuncu konsinyeleri, ömür rozetli) / 💰 Sat. ## Kod ile kurulur. Node adı "VendorShop". Alt-sheet. const PANEL_HEIGHT := 640.0 const RARITY_COLOR := { "common": Color(0.8, 0.8, 0.8), "uncommon": Color(0.4, 0.85, 0.4), "rare": Color(0.35, 0.6, 1.0), "epic": Color(0.7, 0.45, 0.95), "legendary": Color(1.0, 0.65, 0.2), } var _npc_id := 0 var _npc_name := "" var _tab := "buy" # "buy" | "sell" var _title: Label var _info: Label var _tab_buy: Button var _tab_sell: Button var _list: VBoxContainer func _ready() -> void: name = "VendorShop" visible = false _build_ui() func _build_ui() -> void: set_anchors_preset(Control.PRESET_BOTTOM_WIDE) offset_top = -PANEL_HEIGHT offset_left = 8 offset_right = -8 offset_bottom = -8 var sb := StyleBoxFlat.new() sb.bg_color = Color(0.12, 0.11, 0.09, 0.98) sb.set_corner_radius_all(10) add_theme_stylebox_override("panel", sb) var margin := MarginContainer.new() margin.add_theme_constant_override("margin_left", 12) margin.add_theme_constant_override("margin_right", 12) margin.add_theme_constant_override("margin_top", 8) margin.add_theme_constant_override("margin_bottom", 8) add_child(margin) var vbox := VBoxContainer.new() vbox.add_theme_constant_override("separation", 6) margin.add_child(vbox) var top := HBoxContainer.new() _title = Label.new() _title.add_theme_font_size_override("font_size", 18) _title.size_flags_horizontal = Control.SIZE_EXPAND_FILL top.add_child(_title) var close_btn := Button.new() close_btn.text = "✕" close_btn.pressed.connect(func() -> void: visible = false) top.add_child(close_btn) vbox.add_child(top) _info = Label.new() _info.add_theme_font_size_override("font_size", 13) _info.add_theme_color_override("font_color", Color(0.95, 0.8, 0.35)) vbox.add_child(_info) var tabs := HBoxContainer.new() tabs.add_theme_constant_override("separation", 6) _tab_buy = _tab_btn(tabs, "🛒 Satın Al", "buy") _tab_sell = _tab_btn(tabs, "💰 Sat", "sell") vbox.add_child(tabs) var scroll := ScrollContainer.new() scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED vbox.add_child(scroll) _list = VBoxContainer.new() _list.size_flags_horizontal = Control.SIZE_EXPAND_FILL _list.add_theme_constant_override("separation", 6) scroll.add_child(_list) func _tab_btn(parent: HBoxContainer, label: String, key: String) -> Button: var b := Button.new() b.text = label b.toggle_mode = true b.size_flags_horizontal = Control.SIZE_EXPAND_FILL b.pressed.connect(func() -> void: _tab = key _sync_tabs() refresh()) parent.add_child(b) return b func _sync_tabs() -> void: _tab_buy.button_pressed = _tab == "buy" _tab_sell.button_pressed = _tab == "sell" func open(npc_id: int, npc_name: String = "") -> void: _npc_id = npc_id _npc_name = npc_name _tab = "buy" _sync_tabs() visible = true refresh() func refresh() -> void: _info.text = "🪙 %d" % int(GameState.character.get("gold", 0)) for c in _list.get_children(): c.queue_free() if _tab == "buy": await _render_buy() else: await _render_sell() func _muted(txt: String) -> Label: var l := Label.new() l.text = txt l.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6)) return l func _render_buy() -> void: var res: Dictionary = await GameState.npc_shop(_npc_id) if not res.ok or not (res.body is Dictionary): _list.add_child(_muted("Dükkan yüklenemedi.")) return var body: Dictionary = res.body _title.text = "🏪 %s" % str(body.get("npc_name", _npc_name)) var role := str(body.get("role", "")) if role != "": _title.text += " (%s)" % role var items: Array = body.get("items", []) if items.is_empty(): _list.add_child(_muted("Bu satıcının stoğu boş.")) return var gold := int(GameState.character.get("gold", 0)) for it in items: if it is Dictionary: _list.add_child(_buy_row(it, gold)) func _buy_row(it: Dictionary, gold: int) -> HBoxContainer: var row := HBoxContainer.new() row.add_theme_constant_override("separation", 8) var info := VBoxContainer.new() info.size_flags_horizontal = Control.SIZE_EXPAND_FILL var name_lbl := Label.new() name_lbl.text = str(it.get("item_name", "")) name_lbl.add_theme_font_size_override("font_size", 15) name_lbl.add_theme_color_override("font_color", RARITY_COLOR.get(str(it.get("rarity", "common")), Color.WHITE)) info.add_child(name_lbl) var meta := Label.new() var price := int(it.get("unit_price", 0)) var src := str(it.get("source", "base")) if src == "player": var exp := int(it.get("expires_in_h", 0)) var qty := int(it.get("qty", 0)) meta.text = "🪙 %d • kalan %d • ⏳ %ds (oyuncu malı)" % [price, qty, exp] meta.add_theme_color_override("font_color", Color(0.7, 0.75, 0.85)) else: meta.text = "🪙 %d • sınırsız" % price meta.add_theme_color_override("font_color", Color(0.6, 0.62, 0.58)) meta.add_theme_font_size_override("font_size", 12) info.add_child(meta) row.add_child(info) var qty_spin := SpinBox.new() qty_spin.min_value = 1 qty_spin.max_value = maxi(1, int(it.get("qty", 10))) if src == "player" else 10 qty_spin.value = 1 qty_spin.custom_minimum_size = Vector2(70, 0) row.add_child(qty_spin) var btn := Button.new() if gold < price: btn.text = "🪙 az" btn.disabled = true else: btn.text = "Al" btn.pressed.connect(_on_buy.bind(btn, str(it.get("item_code", "")), qty_spin)) row.add_child(btn) return row func _render_sell() -> void: var inv: Array = GameState.inventory var sellable := inv.filter(func(r: Variant) -> bool: return r is Dictionary and int(r.get("qty", 0)) > 0) if sellable.is_empty(): _list.add_child(_muted("Envanterin boş.")) return _list.add_child(_muted("Satıcıya sat → gold. (Satıcının normalde satmadığı ürün 24s dükkanına düşer.)")) for r in sellable: _list.add_child(_sell_row(r)) func _sell_row(r: Dictionary) -> HBoxContainer: var row := HBoxContainer.new() row.add_theme_constant_override("separation", 8) var name_lbl := Label.new() name_lbl.text = "%s ×%d" % [str(r.get("item_name", "")), int(r.get("qty", 0))] name_lbl.add_theme_font_size_override("font_size", 15) name_lbl.add_theme_color_override("font_color", RARITY_COLOR.get(str(r.get("rarity", "common")), Color.WHITE)) name_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(name_lbl) var qty_spin := SpinBox.new() qty_spin.min_value = 1 qty_spin.max_value = maxi(1, int(r.get("qty", 1))) qty_spin.value = 1 qty_spin.custom_minimum_size = Vector2(70, 0) row.add_child(qty_spin) var btn := Button.new() btn.text = "Sat" btn.pressed.connect(_on_sell.bind(btn, str(r.get("item_code", "")), qty_spin)) row.add_child(btn) return row func _on_buy(btn: Button, code: String, qty_spin: SpinBox) -> void: btn.disabled = true var res: Dictionary = await GameState.npc_buy(_npc_id, code, int(qty_spin.value)) _toast(res, "✅ Satın alındı", "🚫 Alınamadı") await refresh() func _on_sell(btn: Button, code: String, qty_spin: SpinBox) -> void: btn.disabled = true var res: Dictionary = await GameState.npc_sell(_npc_id, code, int(qty_spin.value)) _toast(res, "✅ Satıldı", "🚫 Satılamadı") await refresh() func _toast(res: Dictionary, ok_msg: String, fail_prefix: String) -> void: var toast := get_tree().get_first_node_in_group("toast") var msg := ok_msg var col := Color(0.6, 0.85, 0.5) if not res.ok: col = Color(0.95, 0.5, 0.4) msg = fail_prefix if res.body is Dictionary and res.body.has("detail"): msg = "%s: %s" % [fail_prefix, str(res.body["detail"])] if toast != null and toast.has_method("show_toast"): toast.call("show_toast", msg, col)