İ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
47
autoload/api.gd
Normal file
47
autoload/api.gd
Normal 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
1
autoload/api.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c1wofkvtrtvaw
|
||||
53
autoload/auth.gd
Normal file
53
autoload/auth.gd
Normal 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
1
autoload/auth.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bse17dq853ush
|
||||
63
autoload/config.gd
Normal file
63
autoload/config.gd
Normal 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
1
autoload/config.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://kmeam3or5l4c
|
||||
281
autoload/game_state.gd
Normal file
281
autoload/game_state.gd
Normal 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
|
||||
1
autoload/game_state.gd.uid
Normal file
1
autoload/game_state.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://0bu7lg8ddk2y
|
||||
196
autoload/pedometer.gd
Normal file
196
autoload/pedometer.gd
Normal 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()
|
||||
1
autoload/pedometer.gd.uid
Normal file
1
autoload/pedometer.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c6o6yfjjvhgpw
|
||||
1
autoload/world_state.gd.uid
Normal file
1
autoload/world_state.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://l0n5h4ijuubn
|
||||
117
autoload/ws_client.gd
Normal file
117
autoload/ws_client.gd
Normal 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\"}")
|
||||
1
autoload/ws_client.gd.uid
Normal file
1
autoload/ws_client.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://h776ig7kb06j
|
||||
Loading…
Add table
Add a link
Reference in a new issue