stepstead-client/scripts/world.gd
jts 6a1706051d İlk commit: Stepstead Godot istemci (4.6 Mobile)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:31:35 +03:00

369 lines
13 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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