İlk commit: Stepstead Godot istemci (4.6 Mobile)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
6a1706051d
346 changed files with 14415 additions and 0 deletions
26
scripts/character_create.gd
Normal file
26
scripts/character_create.gd
Normal 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")
|
||||
1
scripts/character_create.gd.uid
Normal file
1
scripts/character_create.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cv25ehw0rw6qf
|
||||
179
scripts/chat_panel.gd
Normal file
179
scripts/chat_panel.gd
Normal 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)
|
||||
1
scripts/chat_panel.gd.uid
Normal file
1
scripts/chat_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dn5l3bpgv7xdd
|
||||
155
scripts/city_map.gd
Normal file
155
scripts/city_map.gd
Normal 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
1
scripts/city_map.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://8gu6e0tl5yf8
|
||||
137
scripts/craft_panel.gd
Normal file
137
scripts/craft_panel.gd
Normal 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ıkçı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()
|
||||
1
scripts/craft_panel.gd.uid
Normal file
1
scripts/craft_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://y2a48brb8qaw
|
||||
113
scripts/daily_panel.gd
Normal file
113
scripts/daily_panel.gd
Normal 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()
|
||||
1
scripts/daily_panel.gd.uid
Normal file
1
scripts/daily_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bd438ja6h0wv0
|
||||
151
scripts/event_dialog.gd
Normal file
151
scripts/event_dialog.gd
Normal 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
|
||||
1
scripts/event_dialog.gd.uid
Normal file
1
scripts/event_dialog.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ljumj6x4prg5
|
||||
1
scripts/game.gd.uid
Normal file
1
scripts/game.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c3hrqbpw3015v
|
||||
159
scripts/hud.gd
Normal file
159
scripts/hud.gd
Normal 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
1
scripts/hud.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b1mucbwfpp41x
|
||||
66
scripts/inventory_panel.gd
Normal file
66
scripts/inventory_panel.gd
Normal 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
|
||||
1
scripts/inventory_panel.gd.uid
Normal file
1
scripts/inventory_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bdgv55r7u52uy
|
||||
117
scripts/leaderboard_panel.gd
Normal file
117
scripts/leaderboard_panel.gd
Normal 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
|
||||
1
scripts/leaderboard_panel.gd.uid
Normal file
1
scripts/leaderboard_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d0ce3a7plnqyr
|
||||
210
scripts/location_panel.gd
Normal file
210
scripts/location_panel.gd
Normal 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ıkçı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()
|
||||
1
scripts/location_panel.gd.uid
Normal file
1
scripts/location_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://kp3wsy87dnj1
|
||||
32
scripts/login.gd
Normal file
32
scripts/login.gd
Normal 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
1
scripts/login.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://0drmet5cxbp4
|
||||
293
scripts/market_panel.gd
Normal file
293
scripts/market_panel.gd
Normal 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("Açı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
|
||||
1
scripts/market_panel.gd.uid
Normal file
1
scripts/market_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ckdntvohya4hp
|
||||
28
scripts/register.gd
Normal file
28
scripts/register.gd
Normal 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
1
scripts/register.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://p2qk5fny80mq
|
||||
119
scripts/settings_panel.gd
Normal file
119
scripts/settings_panel.gd
Normal 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 "—")
|
||||
1
scripts/settings_panel.gd.uid
Normal file
1
scripts/settings_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c71hdvgrbu5eh
|
||||
116
scripts/skills_panel.gd
Normal file
116
scripts/skills_panel.gd
Normal 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ıkçı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
|
||||
1
scripts/skills_panel.gd.uid
Normal file
1
scripts/skills_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://fah27k5pkhe2
|
||||
32
scripts/splash.gd
Normal file
32
scripts/splash.gd
Normal 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
1
scripts/splash.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://deo1am2bu5ted
|
||||
45
scripts/toast.gd
Normal file
45
scripts/toast.gd
Normal 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ıkçı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
1
scripts/toast.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c0rfvgjj6kt1p
|
||||
236
scripts/vendor_shop.gd
Normal file
236
scripts/vendor_shop.gd
Normal 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)
|
||||
1
scripts/vendor_shop.gd.uid
Normal file
1
scripts/vendor_shop.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://eeoyqkrgnk16
|
||||
369
scripts/world.gd
Normal file
369
scripts/world.gd
Normal 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ı açı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
1
scripts/world.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cwfuwbnjp4yv5
|
||||
Loading…
Add table
Add a link
Reference in a new issue