İ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

View file

@ -0,0 +1,26 @@
extends Control
@onready var name_input: LineEdit = $Panel/VBox/Name
@onready var create_btn: Button = $Panel/VBox/Create
@onready var logout_btn: Button = $Panel/VBox/Logout
@onready var error: Label = $Panel/VBox/Error
func _ready() -> void:
error.text = ""
create_btn.pressed.connect(_on_create)
logout_btn.pressed.connect(func():
Auth.clear()
get_tree().change_scene_to_file("res://scenes/login.tscn"))
func _on_create() -> void:
error.text = ""
create_btn.disabled = true
var res: Dictionary = await GameState.create_character(name_input.text.strip_edges())
create_btn.disabled = false
if not res.ok:
var detail := "Karakter oluşturulamadı"
if res.body is Dictionary and res.body.has("detail"):
detail = str(res.body["detail"])
error.text = detail
return
get_tree().change_scene_to_file("res://scenes/world.tscn")

View file

@ -0,0 +1 @@
uid://cv25ehw0rw6qf

179
scripts/chat_panel.gd Normal file
View file

@ -0,0 +1,179 @@
extends PanelContainer
## Sohbet paneli — kod ile kurulur (tscn'de yok), WsClient chat sinyallerini dinler.
## hud.gd oluşturup HUD CanvasLayer'ına ekler. Node adı "ChatPanel" (world.gd blokaj kontrolü).
const MAX_ROWS := 60
const PANEL_HEIGHT := 480.0
var _list: VBoxContainer
var _scroll: ScrollContainer
var _input: LineEdit
var _tab_global: Button
var _tab_local: Button
## Aktif kanal: "global" | "local". Mesajlar kanal başına bellekte tutulur,
## sekme değişince yeniden çizilir.
var _channel := "global"
var _msgs := {"global": [], "local": []}
func _ready() -> void:
name = "ChatPanel"
visible = false
_build_ui()
WsClient.chat_received.connect(_on_chat)
WsClient.chat_history.connect(_on_history)
WsClient.local_chat_history.connect(_on_local_history)
# Lokasyon değişince yerel geçmiş bayatlar
GameState.traveled.connect(func(_p: Dictionary) -> void:
_msgs["local"] = []
if visible and _channel == "local":
WsClient.request_local_history())
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.08, 0.1, 0.13, 0.95)
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()
var title := Label.new()
title.text = "💬 Sohbet"
title.add_theme_font_size_override("font_size", 18)
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
top.add_child(title)
_tab_global = Button.new()
_tab_global.text = "🌍 Genel"
_tab_global.toggle_mode = true
_tab_global.button_pressed = true
_tab_global.pressed.connect(func() -> void: _switch_channel("global"))
top.add_child(_tab_global)
_tab_local = Button.new()
_tab_local.text = "📍 Burada"
_tab_local.toggle_mode = true
_tab_local.pressed.connect(func() -> void: _switch_channel("local"))
top.add_child(_tab_local)
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)
_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", 4)
_scroll.add_child(_list)
var bottom := HBoxContainer.new()
bottom.add_theme_constant_override("separation", 8)
_input = LineEdit.new()
_input.placeholder_text = "Mesaj yaz…"
_input.max_length = 300
_input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_input.text_submitted.connect(func(_t: String) -> void: _send())
bottom.add_child(_input)
var send_btn := Button.new()
send_btn.text = "Gönder"
send_btn.pressed.connect(_send)
bottom.add_child(send_btn)
vbox.add_child(bottom)
func open() -> void:
visible = true
if _channel == "local" and (_msgs["local"] as Array).is_empty():
WsClient.request_local_history()
_scroll_to_bottom()
_input.grab_focus()
func _switch_channel(ch: String) -> void:
_channel = ch
_tab_global.button_pressed = ch == "global"
_tab_local.button_pressed = ch == "local"
_input.placeholder_text = "Mesaj yaz…" if ch == "global" else "Buradakilere yaz…"
if ch == "local":
WsClient.request_local_history()
_rerender()
func _send() -> void:
var msg := _input.text.strip_edges()
if msg == "":
return
WsClient.send_chat(msg, _channel)
_input.text = ""
_input.grab_focus()
func _on_history(messages: Array) -> void:
_msgs["global"] = messages.duplicate()
if _channel == "global":
_rerender()
func _on_local_history(messages: Array) -> void:
_msgs["local"] = messages.duplicate()
if _channel == "local":
_rerender()
func _on_chat(msg: Dictionary) -> void:
var ch := str(msg.get("channel", "global"))
if not _msgs.has(ch):
return
var arr: Array = _msgs[ch]
arr.append(msg)
while arr.size() > MAX_ROWS:
arr.pop_front()
if ch == _channel:
_append_row(msg)
while _list.get_child_count() > MAX_ROWS:
var oldest := _list.get_child(0)
_list.remove_child(oldest)
oldest.queue_free()
_scroll_to_bottom()
func _rerender() -> void:
for c in _list.get_children():
c.queue_free()
for m in (_msgs[_channel] as Array):
if m is Dictionary:
_append_row(m)
_scroll_to_bottom()
func _append_row(msg: Dictionary) -> void:
var own: bool = int(msg.get("char_id", 0)) == int(GameState.character.get("id", -1))
var row := RichTextLabel.new()
row.bbcode_enabled = true
row.fit_content = true
row.scroll_active = false
row.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var name_color := "#7ee08a" if own else "#e0c060"
row.text = "[color=%s]%s:[/color] %s" % [
name_color,
str(msg.get("name", "?")).replace("[", ""),
str(msg.get("text", "")).replace("[", ""),
]
_list.add_child(row)
func _scroll_to_bottom() -> void:
if not visible:
return
await get_tree().process_frame
_scroll.scroll_vertical = int(_scroll.get_v_scroll_bar().max_value)

View file

@ -0,0 +1 @@
uid://dn5l3bpgv7xdd

155
scripts/city_map.gd Normal file
View file

@ -0,0 +1,155 @@
extends CanvasLayer
## Şehir haritası — bir şehir location'ının içi. Prosedürel placeholder zemin
## (bina blokları + yollar) üzerinde vendor NPC pinleri. Pine tıkla → dükkan (VendorShop).
## "🗺 Dünya Haritası" ile kapanır. world.gd oluşturur; açıkken world input'u durur.
## Node adı "CityMap".
var _root: Control
var _canvas: Control
var _pins_box: Control
var _title: Label
var _shop: PanelContainer
var _vendors: Array = [] # {npc_id,name,role}
var _pin_pos: Array = [] # Vector2 ekran koordinatları (canvas _draw kullanır)
var _location_name := ""
func _ready() -> void:
name = "CityMap"
layer = 4
visible = false
_build_ui()
func is_open() -> bool:
return visible
func _build_ui() -> void:
_root = Control.new()
_root.set_anchors_preset(Control.PRESET_FULL_RECT)
_root.mouse_filter = Control.MOUSE_FILTER_STOP
add_child(_root)
# Prosedürel zemin (buildings + yollar) — pin konumlarına göre çizilir
_canvas = CityCanvas.new()
_canvas.set_anchors_preset(Control.PRESET_FULL_RECT)
_canvas.mouse_filter = Control.MOUSE_FILTER_IGNORE
_root.add_child(_canvas)
# Pin container (butonlar mutlak konumlu)
_pins_box = Control.new()
_pins_box.set_anchors_preset(Control.PRESET_FULL_RECT)
_pins_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
_root.add_child(_pins_box)
# Üst bar
var bar := PanelContainer.new()
bar.set_anchors_preset(Control.PRESET_TOP_WIDE)
bar.offset_bottom = 64
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.08, 0.09, 0.12, 0.95)
bar.add_theme_stylebox_override("panel", sb)
_root.add_child(bar)
var hb := HBoxContainer.new()
hb.add_theme_constant_override("separation", 10)
bar.add_child(hb)
var m := MarginContainer.new()
m.add_theme_constant_override("margin_left", 12)
m.add_theme_constant_override("margin_right", 12)
m.add_theme_constant_override("margin_top", 8)
m.add_theme_constant_override("margin_bottom", 8)
m.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hb.add_child(m)
_title = Label.new()
_title.add_theme_font_size_override("font_size", 18)
m.add_child(_title)
var back := Button.new()
back.text = "🗺 Dünya Haritası"
back.pressed.connect(_close)
hb.add_child(back)
# Satıcı dükkanı (alt-sheet) — pinlerin üstünde
_shop = preload("res://scripts/vendor_shop.gd").new()
add_child(_shop)
func open() -> void:
visible = true
_title.text = "🏙 Yükleniyor…"
var res: Dictionary = await GameState.city_vendors()
if not res.ok or not (res.body is Dictionary):
_title.text = "🏙 Şehir yüklenemedi"
_vendors = []
else:
_location_name = str(res.body.get("location_name", "Şehir"))
_vendors = res.body.get("vendors", [])
_title.text = "🏙 %s" % _location_name
_layout_pins()
func _close() -> void:
_shop.visible = false
visible = false
func _layout_pins() -> void:
for c in _pins_box.get_children():
c.queue_free()
_pin_pos.clear()
var size := get_viewport().get_visible_rect().size
var n := _vendors.size()
if n == 0:
_canvas.queue_redraw()
return
# Zikzak yerleşim: üst bar altından alt nav üstüne kadar dağıt
var top := 110.0
var bottom := size.y - 140.0
for i in range(n):
var t := float(i) / float(maxi(1, n - 1)) if n > 1 else 0.5
var y := lerpf(top, bottom, t)
var x := size.x * (0.30 if i % 2 == 0 else 0.68)
var pos := Vector2(x, y)
_pin_pos.append(pos)
_add_pin(_vendors[i], pos)
_canvas.pins = _pin_pos
_canvas.queue_redraw()
func _add_pin(v: Dictionary, pos: Vector2) -> void:
var btn := Button.new()
var role := str(v.get("role", ""))
btn.text = "🏪\n%s" % str(v.get("name", ""))
btn.tooltip_text = role
btn.add_theme_font_size_override("font_size", 13)
btn.custom_minimum_size = Vector2(150, 64)
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.18, 0.15, 0.10, 0.96)
sb.border_color = Color(0.85, 0.7, 0.35)
sb.set_border_width_all(2)
sb.set_corner_radius_all(8)
btn.add_theme_stylebox_override("normal", sb)
_pins_box.add_child(btn)
btn.size = btn.custom_minimum_size
btn.position = pos - btn.custom_minimum_size / 2.0
var npc_id := int(v.get("npc_id", 0))
var vname := str(v.get("name", ""))
btn.pressed.connect(func() -> void: _shop.open(npc_id, vname))
## Prosedürel şehir zemini — zemin, yollar, pin altı bina blokları.
class CityCanvas extends Control:
var pins: Array = []
func _draw() -> void:
var s := size
# Zemin (toprak/çim degrade hissi)
draw_rect(Rect2(Vector2.ZERO, s), Color(0.16, 0.18, 0.13))
draw_rect(Rect2(Vector2(0, s.y * 0.5), Vector2(s.x, s.y * 0.5)), Color(0.14, 0.15, 0.11))
# Ana yol (dikey)
var road_col := Color(0.30, 0.27, 0.22)
draw_rect(Rect2(Vector2(s.x * 0.5 - 22, 90), Vector2(44, s.y - 220)), road_col)
# Pin altı bina blokları
for p in pins:
var bp: Vector2 = p
draw_line(Vector2(s.x * 0.5, bp.y), bp, road_col, 14.0)
var bsize := Vector2(120, 90)
var top_left := bp - Vector2(bsize.x / 2.0, bsize.y + 20)
draw_rect(Rect2(top_left, bsize), Color(0.28, 0.24, 0.20))
draw_rect(Rect2(top_left, bsize), Color(0.5, 0.42, 0.30), false, 2.0)
var roof := PackedVector2Array([
top_left + Vector2(-8, 0), top_left + Vector2(bsize.x + 8, 0),
top_left + Vector2(bsize.x / 2.0, -34)])
draw_colored_polygon(roof, Color(0.45, 0.22, 0.18))

1
scripts/city_map.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://8gu6e0tl5yf8

137
scripts/craft_panel.gd Normal file
View file

@ -0,0 +1,137 @@
extends PanelContainer
## Üretim paneli — tarif listeler, üretim yapar.
@onready var list: VBoxContainer = $Margin/VBox/Scroll/List
@onready var title_label: Label = $Margin/VBox/Title
@onready var close_btn: Button = $Margin/VBox/CloseBtn
@onready var filter_box: HBoxContainer = $Margin/VBox/FilterBox
const SKILL_LABEL := {
"mining": "Madencilik", "fishing": "Balıılık", "foraging": "Bitki Toplama",
"woodcutting": "Ağaç Kesme", "hunting": "Avcılık", "combat": "Dövüş",
"smithing": "Demircilik", "alchemy": "Simyacılık", "cooking": "Yemek Pişirme",
"construction": "İnşa",
}
var _recipes: Array = []
var _filter: String = ""
func _ready() -> void:
visible = false
close_btn.pressed.connect(func(): visible = false)
func open() -> void:
visible = true
title_label.text = "Üretim — yükleniyor..."
_recipes = await GameState.list_recipes()
_build_filters()
_render()
func _build_filters() -> void:
for c in filter_box.get_children():
c.queue_free()
var skills_seen: Dictionary = {}
for r in _recipes:
skills_seen[r["skill"]] = true
var all_btn := Button.new()
all_btn.text = "Tümü"
all_btn.toggle_mode = true
all_btn.button_pressed = _filter == ""
all_btn.pressed.connect(func():
_filter = ""
_render()
_build_filters())
filter_box.add_child(all_btn)
for sk in skills_seen.keys():
var b := Button.new()
b.text = SKILL_LABEL.get(sk, sk)
b.toggle_mode = true
b.button_pressed = _filter == sk
b.pressed.connect(func():
_filter = sk
_render()
_build_filters())
filter_box.add_child(b)
func _render() -> void:
for c in list.get_children():
c.queue_free()
var shown := 0
title_label.text = "Üretim"
for r in _recipes:
if _filter != "" and r["skill"] != _filter:
continue
shown += 1
list.add_child(_make_row(r))
if shown == 0:
var l := Label.new()
l.text = "Bu kategoride tarif yok."
l.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
list.add_child(l)
func _make_row(r: Dictionary) -> Control:
var card := PanelContainer.new()
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.16, 0.18, 0.20, 1) if r.get("craftable", false) else Color(0.12, 0.13, 0.14, 1)
sb.set_corner_radius_all(6)
sb.set_content_margin_all(10)
card.add_theme_stylebox_override("panel", sb)
var vb := VBoxContainer.new()
vb.add_theme_constant_override("separation", 4)
card.add_child(vb)
var top := HBoxContainer.new()
top.add_theme_constant_override("separation", 8)
vb.add_child(top)
var title := Label.new()
title.text = "%s%dx %s" % [str(r["name"]), int(r["output_qty"]), str(r["output_item_name"])]
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
title.add_theme_font_size_override("font_size", 15)
if not r.get("unlocked", false):
title.add_theme_color_override("font_color", Color(0.55, 0.55, 0.55))
top.add_child(title)
var btn := Button.new()
btn.text = "Üret"
btn.disabled = not r.get("craftable", false)
btn.pressed.connect(_on_craft.bind(str(r["code"])))
top.add_child(btn)
var meta := Label.new()
var skill_tr: String = SKILL_LABEL.get(r["skill"], r["skill"])
meta.text = "%s Lv %d%d adım • +%d XP" % [skill_tr, int(r["required_level"]), int(r["step_cost"]), int(r["xp_reward"])]
meta.add_theme_color_override("font_color", Color(0.7, 0.7, 0.75))
meta.add_theme_font_size_override("font_size", 12)
vb.add_child(meta)
for ing in r["ingredients"]:
var row := Label.new()
var have := int(ing["have"])
var need := int(ing["qty"])
var color: Color = Color(0.6, 0.85, 0.5) if have >= need else Color(0.85, 0.4, 0.4)
row.text = "%s %d/%d" % [str(ing["item_name"]), have, need]
row.add_theme_color_override("font_color", color)
row.add_theme_font_size_override("font_size", 12)
vb.add_child(row)
if not r.get("unlocked", false):
var lock := Label.new()
lock.text = " 🔒 %s Lv %d gerekli" % [skill_tr, int(r["required_level"])]
lock.add_theme_color_override("font_color", Color(0.9, 0.6, 0.3))
lock.add_theme_font_size_override("font_size", 12)
vb.add_child(lock)
return card
func _on_craft(code: String) -> void:
var res: Dictionary = await GameState.craft_recipe(code, 1)
if not res.ok:
var msg := "Üretim başarısız"
if res.body is Dictionary and res.body.has("detail"):
msg = str(res.body["detail"])
print("[craft fail] ", msg)
# Refresh
_recipes = await GameState.list_recipes()
_render()

View file

@ -0,0 +1 @@
uid://y2a48brb8qaw

113
scripts/daily_panel.gd Normal file
View file

@ -0,0 +1,113 @@
extends PanelContainer
## Günlük hedef + streak paneli — kod ile kurulur, hud.gd ekler. Node adı "DailyPanel".
var _title: Label
var _progress: ProgressBar
var _progress_lbl: Label
var _streak_lbl: Label
var _claim_btn: Button
var _result_lbl: Label
func _ready() -> void:
name = "DailyPanel"
visible = false
_build_ui()
func _build_ui() -> void:
set_anchors_preset(Control.PRESET_CENTER_TOP)
offset_top = 90
offset_left = -170
offset_right = 170
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.10, 0.12, 0.10, 0.97)
sb.border_color = Color(0.75, 0.62, 0.35)
sb.set_border_width_all(1)
sb.set_corner_radius_all(10)
sb.set_content_margin_all(14)
add_theme_stylebox_override("panel", sb)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 8)
add_child(vbox)
var top := HBoxContainer.new()
_title = Label.new()
_title.text = "🎁 Günlük Hedef"
_title.add_theme_font_size_override("font_size", 17)
_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)
_progress = ProgressBar.new()
_progress.custom_minimum_size = Vector2(0, 22)
_progress.show_percentage = false
vbox.add_child(_progress)
_progress_lbl = Label.new()
_progress_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_progress_lbl.add_theme_font_size_override("font_size", 13)
vbox.add_child(_progress_lbl)
_streak_lbl = Label.new()
_streak_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_streak_lbl.add_theme_font_size_override("font_size", 13)
_streak_lbl.add_theme_color_override("font_color", Color(0.9, 0.78, 0.5))
vbox.add_child(_streak_lbl)
_claim_btn = Button.new()
_claim_btn.text = "Ödülü Al"
_claim_btn.pressed.connect(_on_claim)
vbox.add_child(_claim_btn)
_result_lbl = Label.new()
_result_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_result_lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_result_lbl.add_theme_font_size_override("font_size", 13)
_result_lbl.visible = false
vbox.add_child(_result_lbl)
func open() -> void:
visible = true
_result_lbl.visible = false
await refresh()
func refresh() -> void:
var res: Dictionary = await GameState.daily_status()
if not res.ok or not (res.body is Dictionary):
_progress_lbl.text = "Yüklenemedi"
_claim_btn.disabled = true
return
var d: Dictionary = res.body
var goal := int(d.get("goal", 1))
var today := int(d.get("today_steps", 0))
_progress.max_value = goal
_progress.value = mini(today, goal)
_progress_lbl.text = "👣 %d / %d" % [today, goal]
_streak_lbl.text = "🔥 Streak: %d gün • Ödül: +%d👣 +%d🪙" % [
int(d.get("streak_days", 0)), int(d.get("bonus_steps", 0)), int(d.get("bonus_gold", 0))]
if bool(d.get("claimed_today", false)):
_claim_btn.text = "✅ Bugün alındı"
_claim_btn.disabled = true
elif bool(d.get("claimable", false)):
_claim_btn.text = "🎁 Ödülü Al"
_claim_btn.disabled = false
else:
_claim_btn.text = "Hedefe %d adım kaldı" % maxi(0, goal - today)
_claim_btn.disabled = true
func _on_claim() -> void:
_claim_btn.disabled = true
var res: Dictionary = await GameState.daily_claim()
if res.ok and res.body is Dictionary:
var b: Dictionary = res.body
_result_lbl.text = "🎉 +%d👣 +%d🪙 — %d günlük streak!" % [
int(b.get("bonus_steps", 0)), int(b.get("bonus_gold", 0)), int(b.get("streak_days", 0))]
_result_lbl.visible = true
elif res.body is Dictionary and res.body.has("detail"):
_result_lbl.text = "" + str(res.body["detail"])
_result_lbl.visible = true
await refresh()

View file

@ -0,0 +1 @@
uid://bd438ja6h0wv0

151
scripts/event_dialog.gd Normal file
View file

@ -0,0 +1,151 @@
extends Control
## D&D tarzı olay kartı — tam ekran karartma + kart. Seçim → sunucu çözer (d20 + skill).
## Kod ile kurulur, world.gd HUD'a ekler. Node adı "EventDialog".
var _card_title: Label
var _card_image: TextureRect
var _card_text: Label
var _choices_box: VBoxContainer
var _result_box: VBoxContainer
var _result_text: Label
var _check_text: Label
var _continue_btn: Button
func _ready() -> void:
name = "EventDialog"
visible = false
set_anchors_preset(Control.PRESET_FULL_RECT)
_build_ui()
func _build_ui() -> void:
var dim := ColorRect.new()
dim.color = Color(0, 0, 0, 0.65)
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(dim)
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(center)
var card := PanelContainer.new()
card.custom_minimum_size = Vector2(560, 0)
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.13, 0.11, 0.09, 0.98)
sb.border_color = Color(0.75, 0.62, 0.35)
sb.set_border_width_all(2)
sb.set_corner_radius_all(12)
sb.set_content_margin_all(20)
card.add_theme_stylebox_override("panel", sb)
center.add_child(card)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 12)
card.add_child(vbox)
_card_title = Label.new()
_card_title.add_theme_font_size_override("font_size", 22)
_card_title.add_theme_color_override("font_color", Color(0.9, 0.78, 0.5))
_card_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vbox.add_child(_card_title)
# Olay illüstrasyonu — assets/events/<code>.webp varsa gösterilir
_card_image = TextureRect.new()
_card_image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
_card_image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
_card_image.custom_minimum_size = Vector2(0, 280)
_card_image.visible = false
vbox.add_child(_card_image)
_card_text = Label.new()
_card_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_card_text.add_theme_font_size_override("font_size", 15)
vbox.add_child(_card_text)
_choices_box = VBoxContainer.new()
_choices_box.add_theme_constant_override("separation", 8)
vbox.add_child(_choices_box)
_result_box = VBoxContainer.new()
_result_box.add_theme_constant_override("separation", 8)
_result_box.visible = false
vbox.add_child(_result_box)
_check_text = Label.new()
_check_text.add_theme_font_size_override("font_size", 14)
_check_text.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_result_box.add_child(_check_text)
_result_text = Label.new()
_result_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_result_text.add_theme_font_size_override("font_size", 15)
_result_box.add_child(_result_text)
_continue_btn = Button.new()
_continue_btn.text = "Devam"
_continue_btn.pressed.connect(func() -> void: visible = false)
_result_box.add_child(_continue_btn)
func show_event(ev: Dictionary) -> void:
_card_title.text = "🎴 %s" % str(ev.get("name", "Olay"))
var img_path := "res://assets/events/%s.webp" % str(ev.get("code", ""))
if ev.has("code") and ResourceLoader.exists(img_path):
_card_image.texture = load(img_path)
_card_image.visible = true
else:
_card_image.visible = false
_card_text.text = str(ev.get("text", ""))
_result_box.visible = false
_choices_box.visible = true
for c in _choices_box.get_children():
c.queue_free()
var choices: Array = ev.get("choices", [])
for i in range(choices.size()):
var ch: Dictionary = choices[i]
var btn := Button.new()
btn.text = str(ch.get("text", "Seçenek %d" % (i + 1)))
btn.pressed.connect(_on_choice.bind(i))
_choices_box.add_child(btn)
visible = true
func _on_choice(index: int) -> void:
for c in _choices_box.get_children():
if c is Button:
(c as Button).disabled = true
var res: Dictionary = await GameState.resolve_event(index)
if not res.ok:
var msg := "Olay çözülemedi"
if res.body is Dictionary and res.body.has("detail"):
msg = str(res.body["detail"])
_result_text.text = "" + msg
_check_text.text = ""
_choices_box.visible = false
_result_box.visible = true
return
var b: Dictionary = res.body
var parts: Array[String] = []
var check: Variant = b.get("skill_check")
if check is Dictionary:
var ok: bool = bool(check.get("success", false))
_check_text.text = "🎲 %d + %d%d%s" % [
int(check.get("roll", 0)), int(check.get("skill_level", 0)),
int(check.get("dc", 0)), "BAŞARILI!" if ok else "BAŞARISIZ"]
_check_text.add_theme_color_override("font_color",
Color(0.5, 0.9, 0.5) if ok else Color(0.95, 0.5, 0.4))
else:
_check_text.text = ""
parts.append(str(b.get("result_text", "")))
var sd := int(b.get("steps_delta", 0))
if sd != 0:
parts.append("%s%d adım" % ["+" if sd > 0 else "", sd])
var gd := int(b.get("gold_delta", 0))
if gd != 0:
parts.append("%s%d 🪙 gold" % ["+" if gd > 0 else "", gd])
var gained: Array = b.get("items_gained", [])
for it in gained:
parts.append("+%d× %s" % [int(it.get("qty", 1)), str(it.get("name", ""))])
var xg: Variant = b.get("xp_gained")
if xg is Dictionary:
parts.append("+%d XP (%s)" % [int(xg.get("xp", 0)), str(xg.get("skill", ""))])
_result_text.text = "\n".join(parts)
_choices_box.visible = false
_result_box.visible = true

View file

@ -0,0 +1 @@
uid://ljumj6x4prg5

1
scripts/game.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://c3hrqbpw3015v

159
scripts/hud.gd Normal file
View file

@ -0,0 +1,159 @@
extends CanvasLayer
## HUD — üst bilgi çubuğu (karakter/altın/adım + ikincil ikonlar) ve
## alt navigasyon çubuğu (Harita/Çanta/Üretim/Market/Karakter/Ayarlar).
## Ana paneller kod ile kurulur; alt bar soldan kayarak girer.
@onready var char_label: Label = $TopBar/HBox/CharName
@onready var level_label: Label = $TopBar/HBox/Level
@onready var steps_label: Label = $TopBar/HBox/Steps
@onready var logout_btn: Button = $TopBar/HBox/Logout
@onready var add_steps_btn: Button = $TopBar/HBox/AddSteps
@onready var inventory_btn: Button = $TopBar/HBox/InventoryBtn
@onready var inventory_panel: PanelContainer = $InventoryPanel
@onready var craft_btn: Button = $TopBar/HBox/CraftBtn
@onready var craft_panel: PanelContainer = $CraftPanel
const NAV_HEIGHT := 96
var chat_panel: PanelContainer
var market_panel: PanelContainer
var daily_panel: PanelContainer
var skills_panel: PanelContainer
var leaderboard_panel: PanelContainer
var settings_panel: PanelContainer
var gold_label: Label
var _nav_layer: CanvasLayer
func _ready() -> void:
add_steps_btn.pressed.connect(_add_steps_debug)
# Eski üst-bar nav butonları alt çubuğa taşındı — gizle (tscn'e dokunmadan)
inventory_btn.visible = false
craft_btn.visible = false
logout_btn.visible = false
# Kod-ile-kurulan paneller
chat_panel = preload("res://scripts/chat_panel.gd").new()
add_child(chat_panel)
market_panel = preload("res://scripts/market_panel.gd").new()
add_child(market_panel)
daily_panel = preload("res://scripts/daily_panel.gd").new()
add_child(daily_panel)
skills_panel = preload("res://scripts/skills_panel.gd").new()
add_child(skills_panel)
leaderboard_panel = preload("res://scripts/leaderboard_panel.gd").new()
add_child(leaderboard_panel)
settings_panel = preload("res://scripts/settings_panel.gd").new()
add_child(settings_panel)
settings_panel.logout_requested.connect(_logout)
# Üst bar: altın etiketi + ikincil ikonlar (sohbet/lider/günlük)
var hbox: HBoxContainer = $TopBar/HBox
gold_label = Label.new()
gold_label.add_theme_color_override("font_color", Color(0.95, 0.8, 0.35))
hbox.add_child(gold_label)
hbox.move_child(gold_label, steps_label.get_index() + 1)
_top_icon(hbox, "💬", _open_chat)
_top_icon(hbox, "🏆", _open_leaderboard)
_top_icon(hbox, "🎁", _open_daily)
_build_bottom_nav()
_render()
GameState.character_loaded.connect(func(_c): _render())
func _top_icon(hbox: HBoxContainer, txt: String, cb: Callable) -> void:
var b := Button.new()
b.text = txt
hbox.add_child(b)
b.pressed.connect(cb)
# --- Alt navigasyon çubuğu ---
func _build_bottom_nav() -> void:
_nav_layer = CanvasLayer.new()
_nav_layer.layer = 3
add_child(_nav_layer)
var bar := PanelContainer.new()
bar.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
bar.offset_top = -NAV_HEIGHT
bar.offset_bottom = 0
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.09, 0.10, 0.13, 0.98)
sb.border_color = Color(0.2, 0.22, 0.28)
sb.border_width_top = 2
bar.add_theme_stylebox_override("panel", sb)
_nav_layer.add_child(bar)
var hbox := HBoxContainer.new()
hbox.add_theme_constant_override("separation", 0)
bar.add_child(hbox)
_nav_btn(hbox, "🗺", "Harita", close_all_panels)
_nav_btn(hbox, "🎒", "Çanta", func() -> void: _toggle(inventory_panel))
_nav_btn(hbox, "🛠", "Üretim", func() -> void: _toggle(craft_panel))
_nav_btn(hbox, "🏪", "Market", func() -> void: _toggle(market_panel))
_nav_btn(hbox, "👤", "Karakter", func() -> void: _toggle(skills_panel))
_nav_btn(hbox, "", "Ayarlar", func() -> void: _toggle(settings_panel))
# Soldan kaydırarak gir
_nav_layer.offset = Vector2(-720, 0)
var tw := create_tween()
tw.tween_property(_nav_layer, "offset", Vector2.ZERO, 0.35).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
func _nav_btn(hbox: HBoxContainer, icon: String, label: String, cb: Callable) -> void:
var b := Button.new()
b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
b.custom_minimum_size = Vector2(0, NAV_HEIGHT)
b.focus_mode = Control.FOCUS_NONE
b.flat = true
b.text = "%s\n%s" % [icon, label]
b.add_theme_font_size_override("font_size", 13)
b.autowrap_mode = TextServer.AUTOWRAP_OFF
hbox.add_child(b)
b.pressed.connect(cb)
func _all_panels() -> Array:
return [inventory_panel, craft_panel, market_panel, skills_panel,
daily_panel, leaderboard_panel, chat_panel, settings_panel]
func close_all_panels() -> void:
for p in _all_panels():
if p != null:
p.visible = false
# Bir paneli aç/kapat; açarken diğerlerini kapat (tek panel görünür)
func _toggle(panel: PanelContainer) -> void:
if panel == null:
return
if panel.visible:
panel.visible = false
return
close_all_panels()
panel.open()
func _open_chat() -> void: _toggle(chat_panel)
func _open_leaderboard() -> void: _toggle(leaderboard_panel)
func _open_daily() -> void: _toggle(daily_panel)
func _add_steps_debug() -> void:
# Gerçek pedometer yolunu test eder: simülasyon sayacına ekle + sunucuya sync
Pedometer.sim_add(100)
await Pedometer.sync_now()
_render()
func _render() -> void:
var c: Dictionary = GameState.character
if c.is_empty():
return
char_label.text = str(c.get("name", ""))
level_label.text = "Lv %d" % int(c.get("level", 1))
steps_label.text = "👣 %d" % int(c.get("step_balance", 0))
if gold_label != null:
gold_label.text = "🪙 %d" % int(c.get("gold", 0))
func _logout() -> void:
WsClient.stop()
Auth.clear()
GameState.character = {}
GameState.skills = []
get_tree().change_scene_to_file("res://scenes/login.tscn")

1
scripts/hud.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://b1mucbwfpp41x

View file

@ -0,0 +1,66 @@
extends PanelContainer
## Envanter paneli — açıldığında /world/inventory yükler, listede gösterir.
@onready var list: VBoxContainer = $Margin/VBox/Scroll/List
@onready var title: Label = $Margin/VBox/Title
@onready var close_btn: Button = $Margin/VBox/CloseBtn
const RARITY_COLOR := {
"common": Color(0.85, 0.85, 0.85),
"uncommon": Color(0.4, 0.85, 0.4),
"rare": Color(0.4, 0.6, 1.0),
"epic": Color(0.7, 0.4, 1.0),
"legendary": Color(1.0, 0.7, 0.2),
}
func _ready() -> void:
visible = false
close_btn.pressed.connect(func(): visible = false)
GameState.inventory_changed.connect(_render)
func open() -> void:
visible = true
await GameState.load_inventory()
_render()
func _render() -> void:
for c in list.get_children():
c.queue_free()
title.text = "Envanter (%d)" % GameState.inventory.size()
if GameState.inventory.is_empty():
var l := Label.new()
l.text = "Boş. Bir node'dan kaynak topla."
l.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
list.add_child(l)
return
for it in GameState.inventory:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 12)
var name := Label.new()
name.text = str(it.get("item_name", ""))
name.add_theme_color_override("font_color", RARITY_COLOR.get(str(it.get("rarity", "common")), Color.WHITE))
name.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(name)
var qty := Label.new()
qty.text = "x%d" % int(it.get("qty", 1))
row.add_child(qty)
# Consumable ise "Kullan" butonu (effect backend'den gelir, örn. +50 adım)
if str(it.get("item_type", "")) == "consumable" and it.get("effect") != null:
var effect: Dictionary = it["effect"]
var btn := Button.new()
btn.text = "Kullan (+%d)" % int(effect.get("amount", 0))
var code: String = str(it.get("item_code", ""))
btn.pressed.connect(_on_use_item.bind(btn, code))
row.add_child(btn)
list.add_child(row)
func _on_use_item(btn: Button, code: String) -> void:
btn.disabled = true
var res: Dictionary = await GameState.use_item(code, 1)
if not res.ok:
var msg := "Kullanılamadı"
if res.body is Dictionary and res.body.has("detail"):
msg = str(res.body["detail"])
print("[use-item fail] ", msg)
btn.disabled = false
# Başarıda inventory_changed sinyali paneli zaten yeniden çizer

View file

@ -0,0 +1 @@
uid://bdgv55r7u52uy

View file

@ -0,0 +1,117 @@
extends PanelContainer
## Liderlik tablosu — 👣 adım / ⭐ level / 🪙 gold sekmeleri, top 20 + kendi sıran.
## Kod ile kurulur. Node adı "LeaderboardPanel".
const PANEL_HEIGHT := 560.0
var _list: VBoxContainer
var _tabs := {}
var _my_rank_lbl: Label
var _by := "steps"
func _ready() -> void:
name = "LeaderboardPanel"
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.10, 0.13, 0.97)
sb.set_corner_radius_all(10)
add_theme_stylebox_override("panel", sb)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 14)
margin.add_theme_constant_override("margin_right", 14)
margin.add_theme_constant_override("margin_top", 10)
margin.add_theme_constant_override("margin_bottom", 10)
add_child(margin)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 6)
margin.add_child(vbox)
var top := HBoxContainer.new()
var title := Label.new()
title.text = "🏆 Liderlik"
title.add_theme_font_size_override("font_size", 18)
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
top.add_child(title)
for entry in [["steps", "👣"], ["level", ""], ["gold", "🪙"]]:
var btn := Button.new()
btn.text = str(entry[1])
btn.toggle_mode = true
var key := str(entry[0])
btn.pressed.connect(func() -> void: _switch(key))
top.add_child(btn)
_tabs[key] = btn
(_tabs["steps"] as Button).button_pressed = true
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)
_my_rank_lbl = Label.new()
_my_rank_lbl.add_theme_font_size_override("font_size", 13)
_my_rank_lbl.add_theme_color_override("font_color", Color(0.75, 0.7, 0.55))
vbox.add_child(_my_rank_lbl)
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", 4)
scroll.add_child(_list)
func open() -> void:
visible = true
await _render()
func _switch(by: String) -> void:
_by = by
for key in _tabs:
(_tabs[key] as Button).button_pressed = key == by
_render()
func _render() -> void:
for c in _list.get_children():
c.queue_free()
var res: Dictionary = await GameState.leaderboard(_by)
if not res.ok or not (res.body is Dictionary):
_my_rank_lbl.text = "Yüklenemedi"
return
var d: Dictionary = res.body
_my_rank_lbl.text = "Senin sıran: #%d" % int(d.get("my_rank", 0))
var unit := {"steps": "👣", "level": "", "gold": "🪙"}.get(_by, "")
for r in d.get("rows", []):
if r is Dictionary:
_list.add_child(_row(r, str(unit)))
func _row(r: Dictionary, unit: String) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
var me: bool = bool(r.get("me", false))
var rank := int(r.get("rank", 0))
var rank_lbl := Label.new()
rank_lbl.text = ["🥇", "🥈", "🥉"][rank - 1] if rank <= 3 else "#%d" % rank
rank_lbl.custom_minimum_size = Vector2(44, 0)
row.add_child(rank_lbl)
var name_lbl := Label.new()
name_lbl.text = "%s (Lv %d)" % [str(r.get("name", "")), int(r.get("level", 1))]
name_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
if me:
name_lbl.add_theme_color_override("font_color", Color(0.5, 0.9, 0.5))
row.add_child(name_lbl)
var val_lbl := Label.new()
val_lbl.text = "%s %d" % [unit, int(r.get("value", 0))]
row.add_child(val_lbl)
return row

View file

@ -0,0 +1 @@
uid://d0ce3a7plnqyr

210
scripts/location_panel.gd Normal file
View file

@ -0,0 +1,210 @@
extends PanelContainer
## Lokasyon paneli — bulunduğun yerin kaynakları (topla butonları), buradaki oyuncular.
## Kod ile kurulur, world.gd HUD'a ekler. Node adı "LocationPanel".
const PANEL_HEIGHT := 620.0
signal city_requested
var _city_btn: Button
var _title: Label
var _desc: Label
var _players: Label
var _list: VBoxContainer
var _scroll: ScrollContainer
var _header: TextureRect
func _ready() -> void:
name = "LocationPanel"
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.10, 0.12, 0.10, 0.97)
sb.set_corner_radius_all(10)
add_theme_stylebox_override("panel", sb)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 14)
margin.add_theme_constant_override("margin_right", 14)
margin.add_theme_constant_override("margin_top", 10)
margin.add_theme_constant_override("margin_bottom", 10)
add_child(margin)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 6)
margin.add_child(vbox)
# Lokasyon başlık görseli (assets/locations/<code>.webp — varsa)
_header = TextureRect.new()
_header.custom_minimum_size = Vector2(0, 190)
_header.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
_header.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
_header.clip_contents = true
_header.visible = false
vbox.add_child(_header)
var top := HBoxContainer.new()
_title = Label.new()
_title.add_theme_font_size_override("font_size", 20)
_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
top.add_child(_title)
_city_btn = Button.new()
_city_btn.text = "🏙 Şehir Haritası"
_city_btn.visible = false
_city_btn.pressed.connect(func() -> void:
visible = false
city_requested.emit())
top.add_child(_city_btn)
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)
_desc = Label.new()
_desc.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_desc.add_theme_font_size_override("font_size", 13)
_desc.add_theme_color_override("font_color", Color(0.7, 0.72, 0.65))
vbox.add_child(_desc)
_players = Label.new()
_players.add_theme_font_size_override("font_size", 13)
_players.add_theme_color_override("font_color", Color(0.55, 0.75, 0.95))
vbox.add_child(_players)
_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 open() -> void:
visible = true
await refresh()
func refresh() -> void:
var res: Dictionary = await GameState.load_location()
if not res.ok or not (res.body is Dictionary):
_title.text = "Lokasyon yüklenemedi"
return
var loc: Dictionary = res.body
_title.text = "📍 %s" % str(loc.get("name", ""))
_city_btn.visible = str(loc.get("type", "")) in ["city", "village"]
_desc.text = str(loc.get("description", ""))
var img_path := "res://assets/locations/%s.webp" % str(loc.get("code", ""))
if ResourceLoader.exists(img_path):
_header.texture = load(img_path)
_header.visible = true
else:
_header.visible = false
var names: Array = loc.get("players_here", [])
_players.text = "👥 Burada: %s" % ", ".join(names) if not names.is_empty() else "👥 Burada başka kimse yok"
for c in _list.get_children():
c.queue_free()
_list.add_child(_camp_row(loc))
var resources: Array = loc.get("resources", [])
if resources.is_empty():
var l := Label.new()
l.text = "Burada toplanacak kaynak yok."
l.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
_list.add_child(l)
return
var bal := int(GameState.character.get("step_balance", 0))
for r in resources:
if r is Dictionary:
_list.add_child(_resource_row(r, bal))
const SKILL_TR := {"mining": "Madencilik", "fishing": "Balıılık", "foraging": "Toplama",
"woodcutting": "Ağaç Kesme", "hunting": "Avcılık"}
func _resource_row(r: Dictionary, bal: int) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
var info := VBoxContainer.new()
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var name_lbl := Label.new()
name_lbl.text = str(r.get("item_name", ""))
name_lbl.add_theme_font_size_override("font_size", 15)
info.add_child(name_lbl)
var meta := Label.new()
var skill: String = str(r.get("skill", ""))
meta.text = "%s%d👣 • +%dXP" % [SKILL_TR.get(skill, skill), int(r.get("step_cost", 0)), int(r.get("xp", 0))]
meta.add_theme_font_size_override("font_size", 12)
meta.add_theme_color_override("font_color", Color(0.6, 0.62, 0.58))
info.add_child(meta)
row.add_child(info)
var btn := Button.new()
var cd := int(r.get("cooldown_remaining", 0))
var tool_ok: bool = bool(r.get("tool_ok", true))
var cost := int(r.get("step_cost", 0))
if not tool_ok:
btn.text = "🔒 %s gerekli" % str(r.get("tool_needed", "Tool"))
btn.disabled = true
elif cd > 0:
btn.text = "%ds" % cd
btn.disabled = true
elif bal < cost:
btn.text = "👣 yetersiz"
btn.disabled = true
else:
btn.text = "Topla"
var code := str(r.get("item_code", ""))
btn.pressed.connect(_on_gather.bind(btn, code))
row.add_child(btn)
return row
const CAMP_ITEM_TR := {"log_oak": "Meşe Odunu", "stone_small": "Küçük Taş", "vine": "Sarmaşık"}
func _camp_row(loc: Dictionary) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
var lbl := Label.new()
lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
lbl.add_theme_font_size_override("font_size", 13)
if bool(loc.get("has_camp", false)):
lbl.text = "⛺ Kampın var — toplama maliyeti -%25"
lbl.add_theme_color_override("font_color", Color(0.6, 0.85, 0.6))
row.add_child(lbl)
return row
var cost_items: Dictionary = loc.get("camp_cost_items", {})
var parts: Array[String] = []
for code in cost_items:
parts.append("%d %s" % [int(cost_items[code]), str(CAMP_ITEM_TR.get(code, code))])
lbl.text = "⛺ Kamp: %s + %d👣 (-%%25 maliyet)" % [", ".join(parts), int(loc.get("camp_step_cost", 100))]
lbl.add_theme_color_override("font_color", Color(0.75, 0.7, 0.55))
row.add_child(lbl)
var btn := Button.new()
btn.text = "Kur"
btn.pressed.connect(_on_build_camp.bind(btn))
row.add_child(btn)
return row
func _on_build_camp(btn: Button) -> void:
btn.disabled = true
var res: Dictionary = await GameState.build_camp()
if not res.ok and res.body is Dictionary and res.body.has("detail"):
print("[camp fail] ", str(res.body["detail"]))
await refresh()
func _on_gather(btn: Button, code: String) -> void:
btn.disabled = true
var res: Dictionary = await GameState.gather(code)
if not res.ok:
var msg := "Toplanamadı"
if res.body is Dictionary and res.body.has("detail"):
msg = str(res.body["detail"])
print("[gather fail] ", msg)
await refresh()

View file

@ -0,0 +1 @@
uid://kp3wsy87dnj1

32
scripts/login.gd Normal file
View file

@ -0,0 +1,32 @@
extends Control
@onready var email_input: LineEdit = $Panel/VBox/Email
@onready var pw_input: LineEdit = $Panel/VBox/Password
@onready var login_btn: Button = $Panel/VBox/LoginBtn
@onready var register_btn: Button = $Panel/VBox/RegisterBtn
@onready var error_label: Label = $Panel/VBox/Error
func _ready() -> void:
error_label.text = ""
login_btn.pressed.connect(_on_login)
register_btn.pressed.connect(_on_goto_register)
func _on_login() -> void:
error_label.text = ""
login_btn.disabled = true
var res: Dictionary = await Auth.login(email_input.text.strip_edges(), pw_input.text)
login_btn.disabled = false
if not res.ok:
var detail := "Giriş başarısız"
if res.body is Dictionary and res.body.has("detail"):
detail = str(res.body["detail"])
error_label.text = detail
return
var char_res: Dictionary = await GameState.load_character()
if char_res.ok:
get_tree().change_scene_to_file("res://scenes/world.tscn")
else:
get_tree().change_scene_to_file("res://scenes/character_create.tscn")
func _on_goto_register() -> void:
get_tree().change_scene_to_file("res://scenes/register.tscn")

1
scripts/login.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://0drmet5cxbp4

293
scripts/market_panel.gd Normal file
View file

@ -0,0 +1,293 @@
extends PanelContainer
## Pazar paneli — oyuncu ilanları + NPC vendor. Kod ile kurulur, hud.gd ekler.
## Node adı "MarketPanel" (world.gd blokaj kontrolü).
## Görüntüleme her yerden; ilan verme / satın alma / vendor satışı sadece pazar'da
## (sunucu zaten doğrular, UI da butonları kilitler).
const PANEL_HEIGHT := 640.0
const MARKET_LOCATION := "pazar"
var _tab_listings: Button
var _tab_sell: Button
var _tab_stock: Button
var _list: VBoxContainer
var _scroll: ScrollContainer
var _search: LineEdit
var _info: Label
var _tab := "listings" # "listings" | "sell" | "stock"
func _ready() -> void:
name = "MarketPanel"
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.13, 0.11, 0.08, 0.97)
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()
var title := Label.new()
title.text = "🏪 Pazar"
title.add_theme_font_size_override("font_size", 18)
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
top.add_child(title)
_tab_listings = Button.new()
_tab_listings.text = "📜 İlanlar"
_tab_listings.toggle_mode = true
_tab_listings.button_pressed = true
_tab_listings.pressed.connect(func() -> void: _switch("listings"))
top.add_child(_tab_listings)
_tab_sell = Button.new()
_tab_sell.text = "🎒 Sat"
_tab_sell.toggle_mode = true
_tab_sell.pressed.connect(func() -> void: _switch("sell"))
top.add_child(_tab_sell)
_tab_stock = Button.new()
_tab_stock.text = "🧰 Vendor"
_tab_stock.toggle_mode = true
_tab_stock.pressed.connect(func() -> void: _switch("stock"))
top.add_child(_tab_stock)
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.75, 0.7, 0.55))
vbox.add_child(_info)
_search = LineEdit.new()
_search.placeholder_text = "🔍 Ara…"
_search.text_submitted.connect(func(_t: String) -> void: refresh())
vbox.add_child(_search)
_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 _at_market() -> bool:
return str(GameState.character.get("location_code", "")) == MARKET_LOCATION
func open() -> void:
visible = true
refresh()
func _switch(tab: String) -> void:
_tab = tab
_tab_listings.button_pressed = tab == "listings"
_tab_sell.button_pressed = tab == "sell"
_tab_stock.button_pressed = tab == "stock"
_search.visible = tab == "listings"
refresh()
func refresh() -> void:
_info.text = ("🪙 %d" % int(GameState.character.get("gold", 0))) + \
("" if _at_market() else " • ⚠ İşlem için Pazar'a git (sadece vitrin)")
for c in _list.get_children():
c.queue_free()
if _tab == "listings":
await _render_listings()
elif _tab == "sell":
await _render_sell()
else:
await _render_stock()
func _render_listings() -> void:
var rows: Array = await GameState.market_listings(_search.text.strip_edges())
if rows.is_empty():
_list.add_child(_muted("ık ilan yok."))
return
for r in rows:
if r is Dictionary:
_list.add_child(_listing_row(r))
func _listing_row(r: Dictionary) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
var info := VBoxContainer.new()
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
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)
info.add_child(name_lbl)
var meta := Label.new()
meta.text = "🪙 %d/adet • %s" % [int(r.get("unit_price", 0)), str(r.get("seller_name", ""))]
meta.add_theme_font_size_override("font_size", 12)
meta.add_theme_color_override("font_color", Color(0.6, 0.58, 0.5))
info.add_child(meta)
row.add_child(info)
var btn := Button.new()
if bool(r.get("mine", false)):
btn.text = "İptal"
btn.pressed.connect(_on_cancel.bind(btn, int(r.get("id", 0))))
else:
var total: int = int(r.get("qty", 0)) * int(r.get("unit_price", 0))
btn.text = "Al (🪙 %d)" % total
if not _at_market():
btn.disabled = true
elif int(GameState.character.get("gold", 0)) < total:
btn.text = "🪙 yetersiz"
btn.disabled = true
else:
btn.pressed.connect(_on_buy.bind(btn, int(r.get("id", 0))))
row.add_child(btn)
return row
func _render_sell() -> void:
var rows: Array = await GameState.vendor_prices()
if rows.is_empty():
_list.add_child(_muted("Envanterin boş."))
return
var at_market := _at_market()
for r in rows:
if r is Dictionary:
_list.add_child(_sell_row(r, at_market))
func _sell_row(r: Dictionary, at_market: bool) -> VBoxContainer:
var box := VBoxContainer.new()
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.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 = int(r.get("qty", 1))
qty_spin.value = 1
row.add_child(qty_spin)
var vend_btn := Button.new()
vend_btn.text = "Vendor 🪙 %d" % int(r.get("unit_price", 0))
vend_btn.disabled = not at_market
vend_btn.pressed.connect(_on_vendor_sell.bind(vend_btn, str(r.get("item_code", "")), qty_spin))
row.add_child(vend_btn)
box.add_child(row)
var row2 := HBoxContainer.new()
row2.add_theme_constant_override("separation", 8)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row2.add_child(spacer)
var price_lbl := Label.new()
price_lbl.text = "İlan fiyatı:"
price_lbl.add_theme_font_size_override("font_size", 12)
row2.add_child(price_lbl)
var price_spin := SpinBox.new()
price_spin.min_value = 1
price_spin.max_value = 1000000
price_spin.value = maxi(1, int(r.get("unit_price", 1)) * 2)
row2.add_child(price_spin)
var list_btn := Button.new()
list_btn.text = "📜 İlan Ver"
list_btn.disabled = not at_market
list_btn.pressed.connect(_on_list.bind(list_btn, str(r.get("item_code", "")), qty_spin, price_spin))
row2.add_child(list_btn)
box.add_child(row2)
return box
func _render_stock() -> void:
var rows: Array = await GameState.vendor_stock()
if rows.is_empty():
_list.add_child(_muted("Vendor stoku yüklenemedi."))
return
var at_market := _at_market()
var gold := int(GameState.character.get("gold", 0))
for r in rows:
if r is Dictionary:
_list.add_child(_stock_row(r, at_market, gold))
func _stock_row(r: Dictionary, at_market: bool, gold: int) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
var name_lbl := Label.new()
name_lbl.text = str(r.get("item_name", ""))
name_lbl.add_theme_font_size_override("font_size", 15)
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 = 10
qty_spin.value = 1
row.add_child(qty_spin)
var price := int(r.get("unit_price", 0))
var btn := Button.new()
btn.text = "Al 🪙 %d" % price
if not at_market:
btn.disabled = true
elif gold < price:
btn.text = "🪙 yetersiz"
btn.disabled = true
else:
btn.pressed.connect(_on_vendor_buy.bind(btn, str(r.get("item_code", "")), qty_spin))
row.add_child(btn)
return row
func _on_vendor_buy(btn: Button, code: String, qty_spin: SpinBox) -> void:
btn.disabled = true
var res: Dictionary = await GameState.vendor_buy(code, int(qty_spin.value))
_report(res)
refresh()
func _on_buy(btn: Button, listing_id: int) -> void:
btn.disabled = true
var res: Dictionary = await GameState.market_buy(listing_id)
_report(res)
refresh()
func _on_cancel(btn: Button, listing_id: int) -> void:
btn.disabled = true
var res: Dictionary = await GameState.market_cancel(listing_id)
_report(res)
refresh()
func _on_vendor_sell(btn: Button, code: String, qty_spin: SpinBox) -> void:
btn.disabled = true
var res: Dictionary = await GameState.vendor_sell(code, int(qty_spin.value))
_report(res)
refresh()
func _on_list(btn: Button, code: String, qty_spin: SpinBox, price_spin: SpinBox) -> void:
btn.disabled = true
var res: Dictionary = await GameState.market_list(code, int(qty_spin.value), int(price_spin.value))
_report(res)
refresh()
func _report(res: Dictionary) -> void:
if not res.ok and res.body is Dictionary and res.body.has("detail"):
_info.text = "" + str(res.body["detail"])
func _muted(text: String) -> Label:
var l := Label.new()
l.text = text
l.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
return l

View file

@ -0,0 +1 @@
uid://ckdntvohya4hp

28
scripts/register.gd Normal file
View file

@ -0,0 +1,28 @@
extends Control
@onready var email: LineEdit = $Panel/VBox/Email
@onready var pw: LineEdit = $Panel/VBox/Password
@onready var name_input: LineEdit = $Panel/VBox/DisplayName
@onready var submit_btn: Button = $Panel/VBox/Submit
@onready var back_btn: Button = $Panel/VBox/Back
@onready var error: Label = $Panel/VBox/Error
func _ready() -> void:
error.text = ""
submit_btn.pressed.connect(_on_submit)
back_btn.pressed.connect(func(): get_tree().change_scene_to_file("res://scenes/login.tscn"))
func _on_submit() -> void:
error.text = ""
submit_btn.disabled = true
var res: Dictionary = await Auth.register(email.text.strip_edges(), pw.text, name_input.text.strip_edges())
submit_btn.disabled = false
if not res.ok:
var detail := "Kayıt başarısız"
if res.body is Dictionary and res.body.has("detail"):
detail = str(res.body["detail"])
error.text = detail
return
error.text = "Doğrulama e-postası gönderildi. Şimdi giriş yapabilirsin."
await get_tree().create_timer(1.5).timeout
get_tree().change_scene_to_file("res://scenes/login.tscn")

1
scripts/register.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://p2qk5fny80mq

119
scripts/settings_panel.gd Normal file
View file

@ -0,0 +1,119 @@
extends PanelContainer
## Ayarlar paneli — ses/müzik/bildirim toggle, hesap bilgisi, çıkış, sürüm.
## Kod ile kurulur. Node adı "SettingsPanel". Alt-sheet (skills_panel deseni).
const PANEL_HEIGHT := 520.0
signal logout_requested
var _sound_chk: CheckButton
var _music_chk: CheckButton
var _notif_chk: CheckButton
var _email_lbl: Label
func _ready() -> void:
name = "SettingsPanel"
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.10, 0.10, 0.13, 0.97)
sb.set_corner_radius_all(10)
add_theme_stylebox_override("panel", sb)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 16)
margin.add_theme_constant_override("margin_right", 16)
margin.add_theme_constant_override("margin_top", 12)
margin.add_theme_constant_override("margin_bottom", 12)
add_child(margin)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 10)
margin.add_child(vbox)
var top := HBoxContainer.new()
var title := Label.new()
title.text = "⚙ Ayarlar"
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)
vbox.add_child(_section("🔊 Ses"))
_sound_chk = _toggle_row(vbox, "Ses efektleri", Config.sound_on, func(v: bool) -> void:
Config.sound_on = v
Config.save_settings())
_music_chk = _toggle_row(vbox, "Müzik", Config.music_on, func(v: bool) -> void:
Config.music_on = v
Config.save_settings())
vbox.add_child(_section("🔔 Bildirim"))
_notif_chk = _toggle_row(vbox, "Bildirimlere izin ver", Config.notif_on, func(v: bool) -> void:
Config.notif_on = v
Config.save_settings())
vbox.add_child(_section("👤 Hesap"))
_email_lbl = Label.new()
_email_lbl.add_theme_color_override("font_color", Color(0.75, 0.8, 0.88))
vbox.add_child(_email_lbl)
var spacer := Control.new()
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
vbox.add_child(spacer)
var logout_btn := Button.new()
logout_btn.text = "🚪 Çıkış Yap"
var lb_sb := StyleBoxFlat.new()
lb_sb.bg_color = Color(0.55, 0.18, 0.16)
lb_sb.set_corner_radius_all(6)
logout_btn.add_theme_stylebox_override("normal", lb_sb)
logout_btn.pressed.connect(func() -> void:
visible = false
logout_requested.emit())
vbox.add_child(logout_btn)
var ver := Label.new()
ver.text = "Stepstead • sürüm %s" % Config.VERSION
ver.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
ver.add_theme_font_size_override("font_size", 11)
ver.add_theme_color_override("font_color", Color(1, 1, 1, 0.4))
vbox.add_child(ver)
func _section(txt: String) -> Label:
var l := Label.new()
l.text = txt
l.add_theme_font_size_override("font_size", 14)
l.add_theme_color_override("font_color", Color(0.9, 0.78, 0.5))
return l
func _toggle_row(parent: VBoxContainer, label: String, value: bool, cb: Callable) -> CheckButton:
var row := HBoxContainer.new()
var lbl := Label.new()
lbl.text = label
lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(lbl)
var chk := CheckButton.new()
chk.button_pressed = value
chk.toggled.connect(cb)
row.add_child(chk)
parent.add_child(row)
return chk
func open() -> void:
visible = true
_sound_chk.button_pressed = Config.sound_on
_music_chk.button_pressed = Config.music_on
_notif_chk.button_pressed = Config.notif_on
var email := str(Auth.user.get("email", ""))
_email_lbl.text = "E-posta: %s" % (email if email != "" else "")

View file

@ -0,0 +1 @@
uid://c71hdvgrbu5eh

116
scripts/skills_panel.gd Normal file
View file

@ -0,0 +1,116 @@
extends PanelContainer
## Skill paneli — 10 skill, level + bir sonraki seviyeye XP ilerlemesi.
## Eğri sunucuyla aynı: level N eşiği = 100 × N² XP. Kod ile kurulur. Node adı "SkillsPanel".
const PANEL_HEIGHT := 560.0
const SKILL_TR := {
"mining": "⛏ Madencilik", "fishing": "🎣 Balıılık", "foraging": "🌿 Toplama",
"woodcutting": "🪓 Ağaç Kesme", "hunting": "🏹 Avcılık", "combat": "⚔ Savaş",
"smithing": "🔨 Demircilik", "alchemy": "⚗ Simya", "cooking": "🍳 Aşçılık",
"construction": "🏠 İnşaat",
}
var _list: VBoxContainer
func _ready() -> void:
name = "SkillsPanel"
visible = false
_build_ui()
GameState.character_loaded.connect(func(_c: Dictionary) -> void:
if visible:
_render())
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.10, 0.10, 0.13, 0.97)
sb.set_corner_radius_all(10)
add_theme_stylebox_override("panel", sb)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 14)
margin.add_theme_constant_override("margin_right", 14)
margin.add_theme_constant_override("margin_top", 10)
margin.add_theme_constant_override("margin_bottom", 10)
add_child(margin)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 6)
margin.add_child(vbox)
var top := HBoxContainer.new()
var title := Label.new()
title.text = "📊 Yetenekler"
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)
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", 8)
scroll.add_child(_list)
func open() -> void:
visible = true
_render()
func _render() -> void:
for c in _list.get_children():
c.queue_free()
var skills: Array = GameState.skills.duplicate()
skills.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return int(a.get("xp", 0)) > int(b.get("xp", 0)))
for s in skills:
if s is Dictionary:
_list.add_child(_skill_row(s))
func _skill_row(s: Dictionary) -> VBoxContainer:
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 2)
var skill_name := str(s.get("skill_name", ""))
var lvl := int(s.get("level", 1))
var xp := int(s.get("xp", 0))
var top := HBoxContainer.new()
var name_lbl := Label.new()
name_lbl.text = str(SKILL_TR.get(skill_name, skill_name))
name_lbl.add_theme_font_size_override("font_size", 15)
name_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
top.add_child(name_lbl)
var lvl_lbl := Label.new()
lvl_lbl.add_theme_font_size_override("font_size", 14)
lvl_lbl.add_theme_color_override("font_color", Color(0.9, 0.78, 0.5))
top.add_child(lvl_lbl)
box.add_child(top)
var bar := ProgressBar.new()
bar.custom_minimum_size = Vector2(0, 14)
bar.show_percentage = false
if lvl >= 50:
lvl_lbl.text = "Lv %d (MAX)" % lvl
bar.max_value = 1
bar.value = 1
else:
# level N eşiği = 100×N²; sonraki seviye eşiği 100×(N+1)²
var cur_floor := 100 * lvl * lvl
var next_need := 100 * (lvl + 1) * (lvl + 1)
lvl_lbl.text = "Lv %d%d / %d XP" % [lvl, xp, next_need]
bar.max_value = next_need - cur_floor
bar.value = clampi(xp - cur_floor, 0, next_need - cur_floor)
box.add_child(bar)
return box

View file

@ -0,0 +1 @@
uid://fah27k5pkhe2

32
scripts/splash.gd Normal file
View file

@ -0,0 +1,32 @@
extends Control
@onready var status_label: Label = $VBox/Status
func _ready() -> void:
status_label.text = "Bağlanılıyor..."
await _check_session()
func _check_session() -> void:
if not Auth.is_logged_in():
_go_login()
return
var res: Dictionary = await Api.request("GET", "/auth/me", null, true)
if not res.ok:
Auth.clear()
_go_login()
return
Auth.user = res.body
var char_res: Dictionary = await GameState.load_character()
if char_res.ok:
_go_game()
else:
_go_character_create()
func _go_login() -> void:
get_tree().change_scene_to_file("res://scenes/login.tscn")
func _go_character_create() -> void:
get_tree().change_scene_to_file("res://scenes/character_create.tscn")
func _go_game() -> void:
get_tree().change_scene_to_file("res://scenes/world.tscn")

1
scripts/splash.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://deo1am2bu5ted

45
scripts/toast.gd Normal file
View file

@ -0,0 +1,45 @@
extends CanvasLayer
## Ekranın altında geçici bildirim.
@onready var container: VBoxContainer = $Anchor/VBox
func _ready() -> void:
add_to_group("toast")
GameState.harvest_succeeded.connect(_on_harvest)
GameState.level_up.connect(_on_level_up)
GameState.item_used.connect(_on_item_used)
func _on_item_used(p: Dictionary) -> void:
show_toast("🍵 %s kullanıldı +%d adım" % [p.get("item_name", ""), int(p.get("effect_amount", 0))], Color(0.5, 0.85, 1.0))
func _on_harvest(p: Dictionary) -> void:
var skill_tr := {"mining": "Madencilik", "fishing": "Balıılık", "foraging": "Bitki Toplama",
"woodcutting": "Ağaç Kesme", "hunting": "Avcılık"}
var skill: String = str(skill_tr.get(p.get("skill", ""), str(p.get("skill", ""))))
show_toast("+1 %s +%d %s XP" % [p.get("item_name", ""), int(p.get("xp_gained", 0)), skill], Color(0.4, 0.8, 0.4))
func _on_level_up(kind: String, lvl: int) -> void:
if kind == "character":
show_toast("🎉 Karakter Level %d!" % lvl, Color(1.0, 0.85, 0.2))
else:
show_toast("⭐ Skill Level %d!" % lvl, Color(0.5, 0.85, 1.0))
func show_toast(text: String, color: Color = Color.WHITE, dur: float = 2.5) -> void:
var panel := PanelContainer.new()
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.1, 0.12, 0.15, 0.92)
sb.set_corner_radius_all(8)
sb.set_content_margin_all(12)
panel.add_theme_stylebox_override("panel", sb)
var label := Label.new()
label.text = text
label.add_theme_color_override("font_color", color)
label.add_theme_font_size_override("font_size", 16)
panel.add_child(label)
container.add_child(panel)
panel.modulate.a = 0.0
var tw := create_tween()
tw.tween_property(panel, "modulate:a", 1.0, 0.15)
tw.tween_interval(dur)
tw.tween_property(panel, "modulate:a", 0.0, 0.3)
tw.tween_callback(panel.queue_free)

1
scripts/toast.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://c0rfvgjj6kt1p

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)

View file

@ -0,0 +1 @@
uid://eeoyqkrgnk16

369
scripts/world.gd Normal file
View file

@ -0,0 +1,369 @@
extends Node2D
## Dünya haritası — lokasyon-graf modeli (2026-07-04 dönüşümü).
## Statik harita: lokasyon pinleri + yollar. Tıkla → seyahat (adım maliyeti) ya da
## bulunduğun yerin panelini aç (kaynak toplama). Yolda D&D tarzı olay kartları çıkar.
## Eski tile/chunk/fog sistemi kaldırıldı; sahnedeki TileLayer/NodeLayer katmanları
## yol/pin çizimi için yeniden kullanılıyor.
## Harita görseli 1536×1024 (3:2). Ekrana yüksekliğe göre sığdırılır (1100px),
## genişlik 1650px olur → kamera sadece yatay kaydırılır (drag).
## DB koordinatları 0-1000 grid'dir ve görselin tamamına oranlanır.
const MAP_ORIGIN := Vector2(0, 150)
const MAP_SIZE := Vector2(1650, 1100)
const CAM_Y := 640.0
const CAM_X_MIN := 360.0
const CAM_X_MAX := MAP_SIZE.x - 360.0
const PIN_TAP_RADIUS := 34.0
const CLICK_DRAG_THRESHOLD := 12.0
@onready var road_layer: Node2D = $TileLayer
@onready var pin_layer: Node2D = $NodeLayer
@onready var player: Sprite2D = $Player
@onready var pan_camera: Camera2D = $PanCamera
@onready var confirm: ConfirmationDialog = $HUD/MoveConfirm
@onready var recenter_btn: Button = $HUD/RecenterBtn
const TYPE_COLOR := {
"village": Color(0.95, 0.80, 0.30),
"city": Color(0.95, 0.55, 0.25),
"forest": Color(0.30, 0.65, 0.35),
"mine": Color(0.62, 0.62, 0.68),
"lake": Color(0.35, 0.60, 0.90),
"meadow": Color(0.55, 0.80, 0.45),
"hunting": Color(0.75, 0.45, 0.30),
}
const TYPE_LABEL := {
"village": "Köy", "city": "Şehir", "forest": "Orman", "mine": "Maden",
"lake": "Göl/Kıyı", "meadow": "Çayır", "hunting": "Av Sahası",
}
var _pins: Dictionary = {} # code -> Node2D
var _locs: Dictionary = {} # code -> location dict
var _roads: Array = []
var _pending_travel := ""
var _others_loc: Dictionary = {} # char_id -> location_code
var _press_pos := Vector2.ZERO
var _pressing := false
var location_panel: PanelContainer
var event_dialog: Control
var city_map: CanvasLayer
class MapPin extends Node2D:
var color := Color.WHITE
var is_here := false
var tex: Texture2D = null # assets/pins/<code>.webp — varsa çizim yerine bu
const TEX_H := 56.0 # ekranda pin yüksekliği (ucu lokasyon noktasında)
func _draw() -> void:
if tex != null:
var s := TEX_H / float(tex.get_height())
var w := tex.get_width() * s
draw_set_transform(Vector2(-w / 2.0, -TEX_H), 0.0, Vector2(s, s))
draw_texture(tex, Vector2.ZERO)
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
if is_here:
draw_arc(Vector2(0, -TEX_H * 0.64), TEX_H * 0.42, 0.0, TAU, 32, Color(1, 1, 1, 0.9), 3.0)
return
draw_circle(Vector2(2, 3), 13.0, Color(0, 0, 0, 0.30))
draw_circle(Vector2.ZERO, 12.0, color)
draw_arc(Vector2.ZERO, 12.0, 0.0, TAU, 24, Color(0.15, 0.15, 0.15, 0.8), 2.0)
if is_here:
draw_arc(Vector2.ZERO, 18.0, 0.0, TAU, 24, Color(1, 1, 1, 0.9), 3.0)
func _ready() -> void:
if not GameState.has_character():
await GameState.load_character()
# Eski sistemin kalıntıları: fog katmanı ve recenter gizli, kamera sabit
var void_layer: CanvasLayer = get_node_or_null("Void")
if void_layer != null:
void_layer.visible = false
recenter_btn.visible = false
pan_camera.position = Vector2(CAM_X_MIN, CAM_Y)
pan_camera.zoom = Vector2(1, 1)
pan_camera.make_current()
player.scale = Vector2(0.9, 0.9)
# Bayrak işareti varsa oyuncu sprite'ı onu kullanır; direğin dibi anchor noktası
var flag_path := "res://assets/sprites/player_flag.webp"
if ResourceLoader.exists(flag_path):
player.texture = load(flag_path)
player.centered = false
player.offset = Vector2(-7.5, -76.0) # direk dibi (webp içi piksel) → origin
confirm.confirmed.connect(_on_travel_confirmed)
# Kod ile kurulan paneller
location_panel = preload("res://scripts/location_panel.gd").new()
$HUD.add_child(location_panel)
event_dialog = preload("res://scripts/event_dialog.gd").new()
$HUD.add_child(event_dialog)
# Şehir haritası (kendi CanvasLayer'ında) — lokasyon panelindeki butondan açılır
city_map = preload("res://scripts/city_map.gd").new()
add_child(city_map)
location_panel.city_requested.connect(func() -> void: city_map.open())
GameState.map_loaded.connect(_draw_map)
GameState.traveled.connect(_on_traveled)
GameState.event_started.connect(func(ev: Dictionary) -> void: event_dialog.call("show_event", ev))
WsClient.online_list.connect(_on_ws_online_list)
WsClient.player_joined.connect(_on_ws_player)
WsClient.player_moved.connect(_on_ws_player)
WsClient.player_left.connect(_on_ws_player_left)
WsClient.start()
await GameState.load_inventory()
await GameState.load_map()
Pedometer.start()
# --- Harita çizimi ---
func _map_pos(loc: Dictionary) -> Vector2:
return MAP_ORIGIN + Vector2(float(loc["map_x"]), float(loc["map_y"])) / 1000.0 * MAP_SIZE
func _draw_map(md: Dictionary) -> void:
for c in road_layer.get_children():
c.queue_free()
for c in pin_layer.get_children():
c.queue_free()
_pins.clear()
_locs.clear()
# Harita görseli (assets/world_map.png, 1536×1024) — MAP_SIZE alanına ölçeklenir,
# pinler DB'deki 0-1000 koordinatlarıyla üstüne oturur
var bg := Sprite2D.new()
bg.texture = load("res://assets/world_map.png")
bg.centered = false
bg.position = MAP_ORIGIN
if bg.texture != null:
bg.scale = MAP_SIZE / Vector2(bg.texture.get_size())
road_layer.add_child(bg)
var locs: Array = md.get("locations", [])
_roads = md.get("roads", [])
for l in locs:
_locs[str(l["code"])] = l
# Yollar — editörde çizilen ara noktalardan (normalize 0-1) geçen kıvrımlı hat
for r in _roads:
var la: Dictionary = _locs.get(str(r["a"]), {})
var lb: Dictionary = _locs.get(str(r["b"]), {})
if la.is_empty() or lb.is_empty():
continue
var pts: Array[Vector2] = [_map_pos(la)]
for w in r.get("waypoints", []):
if w is Array and w.size() == 2:
pts.append(MAP_ORIGIN + Vector2(float(w[0]), float(w[1])) * MAP_SIZE)
pts.append(_map_pos(lb))
var line := Line2D.new()
line.width = 4.0
line.default_color = Color(0.55, 0.48, 0.38, 0.7)
line.joint_mode = Line2D.LINE_JOINT_ROUND
line.round_precision = 4
for p in pts:
line.add_point(p)
road_layer.add_child(line)
var cost := Label.new()
cost.text = "%d" % int(r["steps"])
cost.add_theme_font_size_override("font_size", 11)
cost.add_theme_color_override("font_color", Color(0.2, 0.15, 0.1))
cost.add_theme_color_override("font_outline_color", Color(1, 0.95, 0.8, 0.9))
cost.add_theme_constant_override("outline_size", 4)
cost.position = pts[pts.size() / 2] - Vector2(12, 8)
road_layer.add_child(cost)
# Pinler
for l in locs:
var code := str(l["code"])
var pin := MapPin.new()
pin.color = TYPE_COLOR.get(str(l["type"]), Color.WHITE)
pin.position = _map_pos(l)
var pin_tex := "res://assets/pins/%s.webp" % code
if ResourceLoader.exists(pin_tex):
pin.tex = load(pin_tex)
var name_lbl := Label.new()
name_lbl.text = str(l["name"])
name_lbl.add_theme_font_size_override("font_size", 12)
name_lbl.add_theme_color_override("font_color", Color(0.15, 0.15, 0.15))
name_lbl.add_theme_color_override("font_outline_color", Color(1, 1, 1, 0.8))
name_lbl.add_theme_constant_override("outline_size", 4)
name_lbl.custom_minimum_size = Vector2(120, 0)
name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_lbl.position = Vector2(-60, 16)
pin.add_child(name_lbl)
var badge := Label.new()
badge.name = "Badge"
badge.text = ""
badge.add_theme_font_size_override("font_size", 12)
badge.add_theme_color_override("font_color", Color(0.1, 0.3, 0.7))
badge.position = Vector2(10, -26)
pin.add_child(badge)
pin_layer.add_child(pin)
_pins[code] = pin
_update_marker()
_refresh_presence()
func _update_marker() -> void:
var my_code := str(GameState.character.get("location_code", GameState.map_data.get("my_location", "")))
for code in _pins:
var pin: MapPin = _pins[code]
pin.is_here = code == my_code
pin.queue_redraw()
if _pins.has(my_code):
var my_pin: MapPin = _pins[my_code]
var pin_pos := my_pin.position
# Görselli pinde bayrak direği pin tepesine oturur, çizim pininde eski yerine
player.position = pin_pos + (Vector2(0, -MapPin.TEX_H) if my_pin.tex != null else Vector2(0, -26))
# Kamerayı bulunduğun pine ortala (yatay)
pan_camera.position.x = clampf(pin_pos.x, CAM_X_MIN, CAM_X_MAX)
player.visible = _pins.has(my_code)
# --- Girdi: pin tıklama ---
func _input(event: InputEvent) -> void:
# Şehir haritasııkken dünya haritası girdisini yok say (pan/tık çakışmasın)
if city_map != null and city_map.is_open():
return
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
_press_pos = event.position
_pressing = not _is_over_hud(event.position)
else:
var was_pressing := _pressing
_pressing = false
if event.position.distance_to(_press_pos) > CLICK_DRAG_THRESHOLD:
return
if not was_pressing or _is_over_hud(event.position):
return
_handle_tap(event.position)
elif event is InputEventMouseMotion and _pressing:
# Yatay harita kaydırma (harita ekrandan geniş)
pan_camera.position.x = clampf(pan_camera.position.x - event.relative.x, CAM_X_MIN, CAM_X_MAX)
func _screen_to_world(screen_pos: Vector2) -> Vector2:
return get_viewport().get_canvas_transform().affine_inverse() * screen_pos
func _handle_tap(screen_pos: Vector2) -> void:
var wp := _screen_to_world(screen_pos)
var best_code := ""
var best_d := PIN_TAP_RADIUS
for code in _pins:
var d: float = (_pins[code] as Node2D).position.distance_to(wp)
if d < best_d:
best_d = d
best_code = str(code)
if best_code != "":
_on_pin_tapped(best_code)
func _is_over_hud(pos: Vector2) -> bool:
if pos.y < 70.0: # TopBar
return true
if pos.y > get_viewport().get_visible_rect().size.y - 96.0: # alt nav çubuğu
return true
if _has_blocking_panel():
return true
return false
func _has_blocking_panel() -> bool:
var hud := $HUD
for n in [hud.get_node_or_null("InventoryPanel"), hud.get_node_or_null("CraftPanel"),
hud.get_node_or_null("ChatPanel"), hud.get_node_or_null("LocationPanel"),
hud.get_node_or_null("EventDialog"), hud.get_node_or_null("MarketPanel"),
hud.get_node_or_null("DailyPanel"), hud.get_node_or_null("SkillsPanel"),
hud.get_node_or_null("LeaderboardPanel"), hud.get_node_or_null("SettingsPanel")]:
if n != null and n.visible:
return true
if confirm.visible:
return true
return false
# --- Seyahat ---
func _on_pin_tapped(code: String) -> void:
var my_code := str(GameState.character.get("location_code", ""))
if code == my_code:
location_panel.call("open")
return
# Yol var mı?
var road: Dictionary = {}
for r in _roads:
if (str(r["a"]) == my_code and str(r["b"]) == code) or (str(r["b"]) == my_code and str(r["a"]) == code):
road = r
break
var loc: Dictionary = _locs.get(code, {})
if road.is_empty():
$Toast.call("show_toast", "🚫 %s: buradan doğrudan yol yok" % str(loc.get("name", code)), Color(0.95, 0.6, 0.4))
return
var cost := int(road["steps"])
var bal := int(GameState.character.get("step_balance", 0))
var lines: Array[String] = []
lines.append("%s%s" % [str(_locs.get(my_code, {}).get("name", my_code)), str(loc.get("name", code))])
lines.append("%s%s" % [TYPE_LABEL.get(str(loc.get("type", "")), ""), str(loc.get("description", ""))])
lines.append("Yol: %d adım • Bakiye: %d" % [cost, bal])
if bal < cost:
lines.append("%d adım eksik. Daha yürümen gerek." % (cost - bal))
confirm.dialog_text = "\n".join(lines)
confirm.get_ok_button().text = "Tamam"
confirm.get_cancel_button().text = "Kapat"
_pending_travel = ""
else:
confirm.dialog_text = "\n".join(lines)
confirm.get_ok_button().text = "Yola Çık"
confirm.get_cancel_button().text = "İptal"
_pending_travel = code
confirm.popup_centered()
func _on_travel_confirmed() -> void:
if _pending_travel == "":
return
var dest := _pending_travel
_pending_travel = ""
var res: Dictionary = await GameState.travel(dest)
if not res.ok:
var msg := "Seyahat başarısız"
if res.body is Dictionary and res.body.has("detail"):
msg = str(res.body["detail"])
$Toast.call("show_toast", "🚫 " + msg, Color(0.95, 0.5, 0.4))
func _on_traveled(b: Dictionary) -> void:
_update_marker()
$Toast.call("show_toast", "🥾 %s'e vardın (-%d adım)" % [str(b.get("location_name", "")), int(b.get("step_cost", 0))], Color(0.6, 0.85, 0.5))
# Olay yoksa lokasyon panelini aç
if not (b.get("event") is Dictionary):
location_panel.call("open")
# --- Multiplayer presence ---
func _on_ws_online_list(players: Array) -> void:
for p in players:
if p is Dictionary:
_others_loc[int(p.get("char_id", 0))] = str(p.get("location_code", ""))
_refresh_presence()
func _on_ws_player(p: Dictionary) -> void:
_others_loc[int(p.get("char_id", 0))] = str(p.get("location_code", ""))
_refresh_presence()
func _on_ws_player_left(char_id: int) -> void:
_others_loc.erase(char_id)
_refresh_presence()
func _refresh_presence() -> void:
var counts: Dictionary = {}
var my_id := int(GameState.character.get("id", -1))
for cid in _others_loc:
if int(cid) == my_id:
continue
var code: String = _others_loc[cid]
counts[code] = int(counts.get(code, 0)) + 1
for code in _pins:
var badge: Label = (_pins[code] as Node2D).get_node("Badge")
var n := int(counts.get(code, 0))
badge.text = "👤%d" % n if n > 0 else ""

1
scripts/world.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://cwfuwbnjp4yv5