İ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
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 ""
|
||||
Loading…
Add table
Add a link
Reference in a new issue