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/.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 ""