İlk commit: Stepstead Godot istemci (4.6 Mobile)

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

369
scripts/world.gd Normal file
View file

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