İlk commit: Stepstead Godot istemci (4.6 Mobile)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jts 2026-07-11 11:31:35 +03:00
commit 6a1706051d
346 changed files with 14415 additions and 0 deletions

236
scripts/vendor_shop.gd Normal file
View file

@ -0,0 +1,236 @@
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)