369 lines
13 KiB
GDScript
369 lines
13 KiB
GDScript
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 ""
|