İ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

47
autoload/api.gd Normal file
View file

@ -0,0 +1,47 @@
extends Node
## HTTP istemcisi — Stepstead API'sine istek atar.
signal request_completed(method: String, path: String, status: int, body: Variant)
func request(method: String, path: String, body: Variant = null, auth: bool = true) -> Dictionary:
var http := HTTPRequest.new()
add_child(http)
var url := Config.get_api_base() + path
var headers := ["Content-Type: application/json", "Accept: application/json"]
if auth and Auth.token != "":
headers.append("Authorization: Bearer " + Auth.token)
var method_id: int = HTTPClient.METHOD_GET
match method.to_upper():
"GET": method_id = HTTPClient.METHOD_GET
"POST": method_id = HTTPClient.METHOD_POST
"PUT": method_id = HTTPClient.METHOD_PUT
"DELETE": method_id = HTTPClient.METHOD_DELETE
"PATCH": method_id = HTTPClient.METHOD_PATCH
var body_str := ""
if body != null:
body_str = JSON.stringify(body)
var err := http.request(url, headers, method_id, body_str)
if err != OK:
http.queue_free()
return {"ok": false, "status": 0, "error": "request_failed", "body": null}
var result: Array = await http.request_completed
http.queue_free()
var status: int = result[1]
var raw_body: PackedByteArray = result[3]
var parsed: Variant = null
if raw_body.size() > 0:
var text := raw_body.get_string_from_utf8()
var json := JSON.new()
if json.parse(text) == OK:
parsed = json.data
else:
parsed = text
var ok := status >= 200 and status < 300
request_completed.emit(method.to_upper(), path, status, parsed)
return {"ok": ok, "status": status, "body": parsed}

1
autoload/api.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://c1wofkvtrtvaw

53
autoload/auth.gd Normal file
View file

@ -0,0 +1,53 @@
extends Node
## Auth durumu — JWT token saklama ve auth API çağrıları.
const SAVE_PATH := "user://auth.dat"
var token: String = ""
var user: Dictionary = {}
func _ready() -> void:
_load()
func _save() -> void:
var f := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if f:
f.store_var({"token": token, "user": user})
func _load() -> void:
if not FileAccess.file_exists(SAVE_PATH):
return
var f := FileAccess.open(SAVE_PATH, FileAccess.READ)
if f:
var data: Variant = f.get_var()
if data is Dictionary:
token = data.get("token", "")
user = data.get("user", {})
func is_logged_in() -> bool:
return token != ""
func clear() -> void:
token = ""
user = {}
_save()
func register(email: String, password: String, display_name: String = "") -> Dictionary:
var body := {"email": email, "password": password}
if display_name != "":
body["display_name"] = display_name
return await Api.request("POST", "/auth/register", body, false)
func login(email: String, password: String) -> Dictionary:
var res: Dictionary = await Api.request("POST", "/auth/login", {"email": email, "password": password}, false)
if res.ok and res.body is Dictionary and res.body.has("access_token"):
token = res.body["access_token"]
_save()
var me_res: Dictionary = await Api.request("GET", "/auth/me", null, true)
if me_res.ok:
user = me_res.body
_save()
return res
func request_password_reset(email: String) -> Dictionary:
return await Api.request("POST", "/auth/request-password-reset", {"email": email}, false)

1
autoload/auth.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://bse17dq853ush

63
autoload/config.gd Normal file
View file

@ -0,0 +1,63 @@
extends Node
## Global yapılandırma. API endpoint ve sabitler.
const API_BASE_URL := "https://api.stepstead.com"
const VERSION := "0.1.0"
const PLATFORM_ANDROID := "android"
const PLATFORM_IOS := "ios"
## Testler için runtime override (örn. headless E2E'de http://127.0.0.1:8002)
var override_base := ""
func get_api_base() -> String:
if override_base != "":
return override_base
return API_BASE_URL
func current_platform() -> String:
var name := OS.get_name().to_lower()
if name == "android":
return PLATFORM_ANDROID
if name == "ios":
return PLATFORM_IOS
return name
## --- Kullanıcı ayarları (Ayarlar paneli) ---
const _SETTINGS_PATH := "user://settings.cfg"
var sound_on := true
var music_on := true
var notif_on := true
func _ready() -> void:
load_settings()
func load_settings() -> void:
var cf := ConfigFile.new()
if cf.load(_SETTINGS_PATH) == OK:
sound_on = bool(cf.get_value("audio", "sound", true))
music_on = bool(cf.get_value("audio", "music", true))
notif_on = bool(cf.get_value("notify", "enabled", true))
_apply_audio()
func save_settings() -> void:
var cf := ConfigFile.new()
cf.set_value("audio", "sound", sound_on)
cf.set_value("audio", "music", music_on)
cf.set_value("notify", "enabled", notif_on)
cf.save(_SETTINGS_PATH)
_apply_audio()
func _apply_audio() -> void:
# "Sfx" / "Music" bus'ları varsa sustur; yoksa Master'ı ses ayarına bağla
var sfx := AudioServer.get_bus_index("Sfx")
if sfx != -1:
AudioServer.set_bus_mute(sfx, not sound_on)
var mus := AudioServer.get_bus_index("Music")
if mus != -1:
AudioServer.set_bus_mute(mus, not music_on)
if sfx == -1 and mus == -1:
var master := AudioServer.get_bus_index("Master")
if master != -1:
AudioServer.set_bus_mute(master, not (sound_on or music_on))

1
autoload/config.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://kmeam3or5l4c

281
autoload/game_state.gd Normal file
View file

@ -0,0 +1,281 @@
extends Node
## Oyun durumu — karakter, skill, envanter cache.
signal character_loaded(character: Dictionary)
signal step_balance_changed(new_value: int)
var character: Dictionary = {}
var skills: Array = []
var inventory: Array = []
signal inventory_changed
signal harvest_succeeded(payload: Dictionary)
signal level_up(kind: String, new_level: int) # "skill" veya "character"
signal item_used(payload: Dictionary)
func has_character() -> bool:
return character.has("id")
func load_character() -> Dictionary:
var res: Dictionary = await Api.request("GET", "/character/me", null, true)
if res.ok:
character = res.body
skills = character.get("skills", [])
character_loaded.emit(character)
return res
func create_character(name: String) -> Dictionary:
var res: Dictionary = await Api.request("POST", "/character", {"name": name}, true)
if res.ok:
character = res.body
skills = character.get("skills", [])
character_loaded.emit(character)
return res
func load_inventory() -> Dictionary:
var res: Dictionary = await Api.request("GET", "/world/inventory", null, true)
if res.ok:
inventory = res.body if res.body is Array else []
inventory_changed.emit()
return res
func list_recipes() -> Array:
var res: Dictionary = await Api.request("GET", "/craft/recipes", null, true)
return res.body if res.ok and res.body is Array else []
func craft_recipe(code: String, qty: int = 1) -> Dictionary:
var res: Dictionary = await Api.request("POST", "/craft", {"recipe_code": code, "qty": qty}, true)
if res.ok and res.body is Dictionary:
var b: Dictionary = res.body
character["step_balance"] = b["step_balance"]
character["level"] = b["character_level"]
character["total_xp"] = b.get("total_xp", character.get("total_xp", 0))
for s in skills:
if s.get("skill_name") == b["skill"]:
s["xp"] = s.get("xp", 0) + b["xp_gained"]
s["level"] = b["skill_level"]
break
character_loaded.emit(character)
await load_inventory()
# Toast yeniden kullan
harvest_succeeded.emit({
"item_name": "%dx %s" % [int(b["output_qty"]), b["output_item_name"]],
"xp_gained": b["xp_gained"],
"skill": b["skill"],
})
if b.get("skill_level_up", false):
level_up.emit("skill", b["skill_level"])
if b.get("character_level_up", false):
level_up.emit("character", b["character_level"])
return res
func use_item(item_code: String, qty: int = 1) -> Dictionary:
var res: Dictionary = await Api.request("POST", "/world/use-item",
{"item_code": item_code, "qty": qty}, true)
if res.ok and res.body is Dictionary:
var b: Dictionary = res.body
character["step_balance"] = b["step_balance"]
character_loaded.emit(character)
await load_inventory()
item_used.emit(b)
return res
# --- Lokasyon-graf dünya ---
signal map_loaded(map_data: Dictionary)
signal traveled(payload: Dictionary)
signal event_started(event: Dictionary)
signal event_resolved(result: Dictionary)
var map_data: Dictionary = {}
func load_map() -> Dictionary:
var res: Dictionary = await Api.request("GET", "/world/map", null, true)
if res.ok and res.body is Dictionary:
map_data = res.body
map_loaded.emit(map_data)
var pe: Variant = map_data.get("pending_event")
if pe is Dictionary:
event_started.emit(pe)
return res
func load_location() -> Dictionary:
return await Api.request("GET", "/world/location", null, true)
func travel(to_code: String) -> Dictionary:
var res: Dictionary = await Api.request("POST", "/world/travel", {"to_code": to_code}, true)
if res.ok and res.body is Dictionary:
var b: Dictionary = res.body
character["step_balance"] = b["step_balance"]
character["location_code"] = b["location_code"]
character["location_name"] = b["location_name"]
character_loaded.emit(character)
traveled.emit(b)
var ev: Variant = b.get("event")
if ev is Dictionary:
event_started.emit(ev)
return res
func gather(item_code: String) -> Dictionary:
var res: Dictionary = await Api.request("POST", "/world/gather", {"item_code": item_code}, true)
if res.ok and res.body is Dictionary:
var b: Dictionary = res.body
character["step_balance"] = b["step_balance"]
character["level"] = b["character_level"]
character["total_xp"] = b["total_xp"]
for s in skills:
if s.get("skill_name") == b["skill"]:
s["xp"] = s.get("xp", 0) + b["xp_gained"]
s["level"] = b["skill_level"]
break
character_loaded.emit(character)
await load_inventory()
harvest_succeeded.emit(b) # toast aynı formatı kullanır
if b.get("skill_level_up", false):
level_up.emit("skill", b["skill_level"])
if b.get("character_level_up", false):
level_up.emit("character", b["character_level"])
return res
func resolve_event(choice: int) -> Dictionary:
var res: Dictionary = await Api.request("POST", "/world/event/resolve", {"choice": choice}, true)
if res.ok and res.body is Dictionary:
var b: Dictionary = res.body
character["step_balance"] = b["step_balance"]
if b.has("gold"):
character["gold"] = b["gold"]
character_loaded.emit(character)
await load_inventory()
event_resolved.emit(b)
return res
# --- Pazar (market + vendor) ---
func market_listings(q: String = "", mine: bool = false) -> Array:
var path := "/market/listings?q=%s&mine=%s" % [q.uri_encode(), "true" if mine else "false"]
var res: Dictionary = await Api.request("GET", path, null, true)
return res.body if res.ok and res.body is Array else []
func market_list(item_code: String, qty: int, unit_price: int) -> Dictionary:
var res: Dictionary = await Api.request("POST", "/market/list",
{"item_code": item_code, "qty": qty, "unit_price": unit_price}, true)
if res.ok:
await load_inventory()
return res
func market_buy(listing_id: int, qty: int = 0) -> Dictionary:
var body := {"listing_id": listing_id}
if qty > 0:
body["qty"] = qty
var res: Dictionary = await Api.request("POST", "/market/buy", body, true)
if res.ok and res.body is Dictionary:
character["gold"] = res.body["gold"]
character_loaded.emit(character)
await load_inventory()
return res
func market_cancel(listing_id: int) -> Dictionary:
var res: Dictionary = await Api.request("POST", "/market/cancel", {"listing_id": listing_id}, true)
if res.ok:
await load_inventory()
return res
func vendor_prices() -> Array:
var res: Dictionary = await Api.request("GET", "/market/vendor", null, true)
return res.body if res.ok and res.body is Array else []
func vendor_sell(item_code: String, qty: int) -> Dictionary:
var res: Dictionary = await Api.request("POST", "/market/vendor/sell",
{"item_code": item_code, "qty": qty}, true)
if res.ok and res.body is Dictionary:
character["gold"] = res.body["gold"]
character_loaded.emit(character)
await load_inventory()
return res
func vendor_stock() -> Array:
var res: Dictionary = await Api.request("GET", "/market/vendor/stock", null, true)
return res.body if res.ok and res.body is Array else []
func vendor_buy(item_code: String, qty: int) -> Dictionary:
var res: Dictionary = await Api.request("POST", "/market/vendor/buy",
{"item_code": item_code, "qty": qty}, true)
if res.ok and res.body is Dictionary:
character["gold"] = res.body["gold"]
character_loaded.emit(character)
await load_inventory()
return res
# --- Şehir vendor'ları (şehir haritası) ---
func city_vendors() -> Dictionary:
return await Api.request("GET", "/market/city", null, true)
func npc_shop(npc_id: int) -> Dictionary:
return await Api.request("GET", "/market/vendor/%d/shop" % npc_id, null, true)
func npc_buy(npc_id: int, item_code: String, qty: int) -> Dictionary:
var res: Dictionary = await Api.request("POST", "/market/vendor/%d/buy" % npc_id,
{"item_code": item_code, "qty": qty}, true)
if res.ok and res.body is Dictionary:
character["gold"] = res.body["gold"]
character_loaded.emit(character)
await load_inventory()
return res
func npc_sell(npc_id: int, item_code: String, qty: int) -> Dictionary:
var res: Dictionary = await Api.request("POST", "/market/vendor/%d/sell" % npc_id,
{"item_code": item_code, "qty": qty}, true)
if res.ok and res.body is Dictionary:
character["gold"] = res.body["gold"]
character_loaded.emit(character)
await load_inventory()
return res
# --- Kamp ---
func build_camp() -> Dictionary:
var res: Dictionary = await Api.request("POST", "/world/camp/build", null, true)
if res.ok and res.body is Dictionary:
var b: Dictionary = res.body
character["step_balance"] = b["step_balance"]
character["level"] = b["character_level"]
for s in skills:
if s.get("skill_name") == "construction":
s["xp"] = s.get("xp", 0) + int(b["xp_gained"])
s["level"] = b["skill_level"]
break
character_loaded.emit(character)
await load_inventory()
harvest_succeeded.emit({
"item_name": "⛺ Kamp",
"xp_gained": b["xp_gained"],
"skill": "construction",
})
if b.get("skill_level_up", false):
level_up.emit("skill", b["skill_level"])
if b.get("character_level_up", false):
level_up.emit("character", b["character_level"])
return res
# --- Liderlik ---
func leaderboard(by: String = "steps") -> Dictionary:
return await Api.request("GET", "/leaderboard?by=" + by, null, true)
# --- Günlük hedef + streak ---
signal daily_claimed(result: Dictionary)
func daily_status() -> Dictionary:
return await Api.request("GET", "/world/daily", null, true)
func daily_claim() -> Dictionary:
var res: Dictionary = await Api.request("POST", "/world/daily/claim", null, true)
if res.ok and res.body is Dictionary:
var b: Dictionary = res.body
character["step_balance"] = b["step_balance"]
character["gold"] = b["gold"]
character_loaded.emit(character)
daily_claimed.emit(b)
return res

View file

@ -0,0 +1 @@
uid://0bu7lg8ddk2y

196
autoload/pedometer.gd Normal file
View file

@ -0,0 +1,196 @@
extends Node
## Pedometer soyutlama katmanı.
##
## Platforma göre kaynak:
## - Android: "StepCounter" singleton (TYPE_STEP_COUNTER, reboot'tan beri kümülatif)
## - iOS: "Pedometer" singleton (CMPedometer, oturum kümülatif)
## - Editör / masaüstü: simülasyon (manuel/otomatik adım üretir)
##
## Native plugin sözleşmesi (her iki platform da aynı):
## bool is_available()
## void start()
## void stop()
## int get_step_count() # kümülatif sayaç (artar, reboot'ta sıfırlanabilir)
##
## İstemci kümülatif sayaçtan delta hesaplar, son senkronize değeri kalıcı
## tutar (user://pedometer.cfg) ve sunucuya yalnızca delta gönderir.
## Otorite sunucuda — sunucu dakika başı tavan uygular (anti-cheat).
signal steps_synced(accepted: int, rejected: int, balance: int)
signal raw_step_count(total: int)
const SAVE_PATH := "user://pedometer.cfg"
const POLL_INTERVAL := 5.0 # sn — sayaç okuma
const SYNC_INTERVAL := 30.0 # sn — sunucuya gönderme
# Editör/masaüstü simülasyonu: otomatik yürüyüş hızı (adım/dakika).
# 0 = kapalı (sadece sim_add ile manuel). ~100/dk normal yürüyüş.
var sim_walk_per_min := 0
var _plugin: Object = null
var _platform := ""
var _last_total := 0 # en son sunucuya senkronize edilen kümülatif değer
var _current_total := 0 # cihazdan okunan güncel kümülatif değer
var _pending := 0 # henüz gönderilmemiş delta
var _running := false
var _sim_total := 0 # editör simülasyon sayacı
var _poll_timer: Timer
var _sync_timer: Timer
func _ready() -> void:
_platform = Config.current_platform()
_load_state()
_init_plugin()
_poll_timer = Timer.new()
_poll_timer.wait_time = POLL_INTERVAL
_poll_timer.timeout.connect(_on_poll)
add_child(_poll_timer)
_sync_timer = Timer.new()
_sync_timer.wait_time = SYNC_INTERVAL
_sync_timer.timeout.connect(_on_sync_tick)
add_child(_sync_timer)
func _init_plugin() -> void:
var singleton_name := ""
if _platform == Config.PLATFORM_ANDROID:
singleton_name = "StepCounter"
elif _platform == Config.PLATFORM_IOS:
singleton_name = "Pedometer"
if singleton_name != "" and Engine.has_singleton(singleton_name):
_plugin = Engine.get_singleton(singleton_name)
print("[Pedometer] native plugin yüklendi: ", singleton_name)
else:
_plugin = null
print("[Pedometer] native plugin yok — simülasyon modu (", _platform, ")")
func is_native() -> bool:
return _plugin != null and _plugin.has_method("is_available") and _plugin.is_available()
## Adım dinlemeyi başlat (login + karakter yüklendikten sonra çağrılır).
func start() -> void:
if _running:
return
_running = true
# Android 10+ çalışma zamanı izni
if _platform == Config.PLATFORM_ANDROID:
if not OS.request_permission("android.permission.ACTIVITY_RECOGNITION"):
push_warning("[Pedometer] ACTIVITY_RECOGNITION izni reddedildi/bekliyor")
if is_native() and _plugin.has_method("start"):
_plugin.start()
_poll_timer.start()
_sync_timer.start()
_on_poll()
func stop() -> void:
if not _running:
return
_running = false
if is_native() and _plugin.has_method("stop"):
_plugin.stop()
_poll_timer.stop()
_sync_timer.stop()
func _read_device_total() -> int:
if is_native() and _plugin.has_method("get_step_count"):
return int(_plugin.get_step_count())
# simülasyon — otomatik yürüyüş açıksa her poll'da hız oranında ekle
if sim_walk_per_min > 0:
_sim_total += int(round(sim_walk_per_min * POLL_INTERVAL / 60.0))
return _sim_total
## Editör/test: sürekli yürüyüş simülasyonu aç/kapat (adım/dakika).
## Örn. Pedometer.sim_walk(100) → dakikada ~100 adım, otomatik sync'lenir.
func sim_walk(steps_per_min: int) -> void:
sim_walk_per_min = max(0, steps_per_min)
## Editör/test: manuel adım enjekte et (simülasyon sayacını ilerletir).
func sim_add(steps: int) -> void:
_sim_total += max(0, steps)
_on_poll()
func _on_poll() -> void:
var total := _read_device_total()
# reboot tespiti: sayaç geriye gittiyse delta'yı ham total kabul et
if total < _last_total:
_last_total = 0
_current_total = total
_pending = max(0, _current_total - _last_total)
raw_step_count.emit(_current_total)
func _on_sync_tick() -> void:
if _pending > 0:
await sync_now()
## Bekleyen delta'yı sunucuya gönder. last_sync sonrası kalıcı kaydet.
func sync_now() -> Dictionary:
_on_poll()
if _pending <= 0:
return {"ok": true, "accepted": 0}
if Auth.token == "":
return {"ok": false, "error": "no_auth"}
var delta := _pending
var src := _platform if is_native() else "sim"
var res: Dictionary = await Api.request("POST", "/world/sync-steps",
{"delta": delta, "source": src}, true)
if res.ok and res.body is Dictionary:
var b: Dictionary = res.body
# gönderdiğimiz delta'yı senkronize sayılan total'a işle
_last_total += delta
_pending = max(0, _current_total - _last_total)
_save_state()
if GameState.has_character():
GameState.character["step_balance"] = int(b["step_balance"])
GameState.character["step_lifetime"] = int(b.get("step_lifetime", 0))
GameState.character_loaded.emit(GameState.character)
var acc: int = int(b.get("accepted", 0))
var rej: int = int(b.get("rejected", 0))
steps_synced.emit(acc, rej, int(b["step_balance"]))
if rej > 0:
push_warning("[Pedometer] %d adım reddedildi (tavan). reason=%s" % [rej, str(b.get("reason"))])
return {"ok": true, "accepted": acc, "rejected": rej}
return {"ok": false, "error": "sync_failed", "status": res.get("status", 0)}
func _load_state() -> void:
var cfg := ConfigFile.new()
if cfg.load(SAVE_PATH) == OK:
_last_total = int(cfg.get_value("pedometer", "last_total", 0))
_sim_total = int(cfg.get_value("pedometer", "sim_total", 0))
func _save_state() -> void:
var cfg := ConfigFile.new()
cfg.set_value("pedometer", "last_total", _last_total)
cfg.set_value("pedometer", "sim_total", _sim_total)
cfg.save(SAVE_PATH)
# Uygulama arka plandan döndüğünde hemen senkronize et (background adımları toparla)
func _notification(what: int) -> void:
if what == NOTIFICATION_APPLICATION_RESUMED:
if _running:
await sync_now()
elif what == NOTIFICATION_APPLICATION_PAUSED:
if _running:
await sync_now()

View file

@ -0,0 +1 @@
uid://c6o6yfjjvhgpw

View file

@ -0,0 +1 @@
uid://l0n5h4ijuubn

117
autoload/ws_client.gd Normal file
View file

@ -0,0 +1,117 @@
extends Node
## Canlı multiplayer istemcisi — diğer oyuncuların pozisyonlarını WebSocket ile alır.
## Sunucu eventleri: online_list (sadece bağlanınca), player_joined, player_moved,
## player_left, pong. Kopunca 5 sn'de bir yeniden bağlanır.
signal player_joined(p: Dictionary)
signal player_moved(p: Dictionary)
signal player_left(char_id: int)
signal online_list(players: Array)
signal chat_received(msg: Dictionary)
signal chat_history(messages: Array) # global geçmiş (bağlanınca)
signal local_chat_history(messages: Array) # bulunulan lokasyonun geçmişi (istek üzerine)
signal connection_changed(is_connected: bool)
const PING_INTERVAL := 25.0 # sunucu presence TTL 120sn, ping ile yenilenir
const RECONNECT_DELAY := 5.0
var _ws: WebSocketPeer = null
var _open := false
var _ping_accum := 0.0
var _reconnect_accum := 0.0
var _should_run := false
func start() -> void:
if _should_run:
return
_should_run = true
_connect_ws()
func stop() -> void:
_should_run = false
if _ws != null:
_ws.close()
_ws = null
if _open:
_open = false
connection_changed.emit(false)
func _connect_ws() -> void:
if Auth.token == "":
return
var url := Config.get_api_base().replace("https://", "wss://").replace("http://", "ws://") + "/ws?token=" + Auth.token
_ws = WebSocketPeer.new()
var err := _ws.connect_to_url(url)
if err != OK:
push_warning("WS bağlantı hatası: %d" % err)
_ws = null
_ping_accum = 0.0
func _process(delta: float) -> void:
if not _should_run:
return
if _ws == null:
_reconnect_accum += delta
if _reconnect_accum >= RECONNECT_DELAY:
_reconnect_accum = 0.0
_connect_ws()
return
_ws.poll()
var state := _ws.get_ready_state()
if state == WebSocketPeer.STATE_OPEN:
if not _open:
_open = true
connection_changed.emit(true)
while _ws.get_available_packet_count() > 0:
_handle_packet(_ws.get_packet().get_string_from_utf8())
_ping_accum += delta
if _ping_accum >= PING_INTERVAL:
_ping_accum = 0.0
_ws.send_text("{\"type\":\"ping\"}")
elif state == WebSocketPeer.STATE_CLOSED:
_ws = null
_reconnect_accum = 0.0
if _open:
_open = false
connection_changed.emit(false)
func _handle_packet(text: String) -> void:
var json := JSON.new()
if json.parse(text) != OK:
return
if not (json.data is Dictionary):
return
var msg: Dictionary = json.data
match str(msg.get("type", "")):
"online_list":
var players: Array = msg.get("players", [])
online_list.emit(players)
"player_joined":
player_joined.emit(msg)
"player_moved":
player_moved.emit(msg)
"player_left":
player_left.emit(int(msg.get("char_id", 0)))
"chat":
chat_received.emit(msg)
"chat_history":
var messages: Array = msg.get("messages", [])
if str(msg.get("channel", "global")) == "local":
local_chat_history.emit(messages)
else:
chat_history.emit(messages)
"pong":
pass
func send_chat(text: String, channel: String = "global") -> void:
if _ws == null or _ws.get_ready_state() != WebSocketPeer.STATE_OPEN:
return
var trimmed := text.strip_edges()
if trimmed == "":
return
_ws.send_text(JSON.stringify({"type": "chat", "text": trimmed, "channel": channel}))
func request_local_history() -> void:
if _ws == null or _ws.get_ready_state() != WebSocketPeer.STATE_OPEN:
return
_ws.send_text("{\"type\":\"get_local_history\"}")

View file

@ -0,0 +1 @@
uid://h776ig7kb06j