İ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

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()