""" World: pedometer adım senkronizasyonu + günlük hedef/streak. NOT (2026-07-04): Tile/chunk sistemi kaldırıldı — dünya artık lokasyon-graf (bkz. routers/travel.py: /world/map, /world/travel, /world/gather, /world/event/resolve). """ from datetime import date, datetime, timezone from typing import Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from sqlalchemy import select, text from sqlalchemy.ext.asyncio import AsyncSession from deps import get_current_user from models import Character, User, get_db router = APIRouter(prefix="/world", tags=["world"]) # Pedometer adım senkronizasyonu (gerçek cihaz) STEP_CAP_PER_MIN = 250 # dakika başı kabul edilen maks adım (koşu dahil) STEP_SYNC_GRACE = 500 # her sync'te elapsed-tavanına eklenen sabit pay STEP_FIRST_SYNC_MAX = 5000 # ilk sync / last_sync_at boşken kabul tavanı STEP_SYNC_MIN_INTERVAL = 10 # iki sync arası min saniye (spam koruması) # Günlük hedef + streak DAILY_GOAL = 3000 # günlük gerçek adım hedefi DAILY_BONUS_BASE = 200 # hedef ödülü (adım bakiyesi) DAILY_BONUS_PER_STREAK = 50 # streak günü başına ek (7 günde tavan) DAILY_STREAK_CAP = 7 DAILY_GOLD_BONUS = 5 # hedef ödülü (gold) def _roll_daily(char: Character, today: date) -> None: """Gün değiştiyse günlük sayacı sıfırla; ödül dünden alınmadıysa streak kırılır.""" if char.daily_date != today: char.daily_date = today char.daily_steps = 0 if char.last_claim_date is not None and (today - char.last_claim_date).days > 1: char.streak_days = 0 class SyncStepsIn(BaseModel): delta: int = Field(ge=0, le=200000) # son sync'ten beri eklenen adım source: str = Field("unknown", max_length=32) class SyncStepsOut(BaseModel): step_balance: int step_lifetime: int accepted: int rejected: int reason: Optional[str] = None @router.post("/sync-steps", response_model=SyncStepsOut) async def sync_steps(d: SyncStepsIn, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): char = (await db.execute(select(Character).where(Character.user_id == user.id))).scalar_one_or_none() if not char: raise HTTPException(404, "Karakter yok") now = datetime.now(timezone.utc) last = char.last_sync_at reason = None # tavan hesabı if last is None: cap = STEP_FIRST_SYNC_MAX else: elapsed = (now - last).total_seconds() if elapsed < STEP_SYNC_MIN_INTERVAL and d.delta > 0: # çok sık sync — yine de kabul et ama küçük tavan uygula elapsed = STEP_SYNC_MIN_INTERVAL cap = int(elapsed / 60.0 * STEP_CAP_PER_MIN) + STEP_SYNC_GRACE accepted = min(d.delta, cap) rejected = d.delta - accepted if rejected > 0: reason = "cap_exceeded: delta=%d cap=%d" % (d.delta, cap) _roll_daily(char, now.date()) if accepted > 0: char.step_balance += accepted char.step_lifetime += accepted char.daily_steps += accepted char.last_sync_at = now src = (d.source or "unknown")[:32] if d.delta > 0: await db.execute(text( "INSERT INTO step_log (character_id, steps_delta, source, accepted, reason) " "VALUES (:cid, :sd, :src, :acc, :rsn)" ), {"cid": char.id, "sd": d.delta, "src": src, "acc": rejected == 0, "rsn": reason}) await db.commit() return SyncStepsOut( step_balance=char.step_balance, step_lifetime=char.step_lifetime, accepted=accepted, rejected=rejected, reason=reason, ) # DEBUG: adım yükleme (pedometer gelene kadar test için) class DebugAddStepsIn(BaseModel): steps: int = Field(gt=0, le=10000) @router.post("/debug/add-steps", response_model=dict) async def debug_add_steps(d: DebugAddStepsIn, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): char = (await db.execute(select(Character).where(Character.user_id == user.id))).scalar_one_or_none() if not char: raise HTTPException(404, "Karakter yok") _roll_daily(char, datetime.now(timezone.utc).date()) char.step_balance += d.steps char.step_lifetime += d.steps char.daily_steps += d.steps await db.commit() return {"step_balance": char.step_balance, "step_lifetime": char.step_lifetime} # ---------- Günlük hedef + streak ---------- class DailyOut(BaseModel): goal: int today_steps: int streak_days: int claimable: bool claimed_today: bool bonus_steps: int # şu anki streak'e göre bugünkü ödül bonus_gold: int def _bonus_for(streak_after_claim: int) -> int: return DAILY_BONUS_BASE + DAILY_BONUS_PER_STREAK * min(streak_after_claim, DAILY_STREAK_CAP) def _daily_out(char: Character, today: date) -> DailyOut: claimed_today = char.last_claim_date == today claimable = (not claimed_today) and char.daily_steps >= DAILY_GOAL next_streak = char.streak_days if claimed_today else char.streak_days + 1 return DailyOut( goal=DAILY_GOAL, today_steps=char.daily_steps, streak_days=char.streak_days, claimable=claimable, claimed_today=claimed_today, bonus_steps=_bonus_for(next_streak), bonus_gold=DAILY_GOLD_BONUS, ) @router.get("/daily", response_model=DailyOut) async def daily_status(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): char = (await db.execute(select(Character).where(Character.user_id == user.id))).scalar_one_or_none() if not char: raise HTTPException(404, "Karakter yok") today = datetime.now(timezone.utc).date() _roll_daily(char, today) await db.commit() return _daily_out(char, today) class DailyClaimOut(BaseModel): streak_days: int bonus_steps: int bonus_gold: int step_balance: int gold: int @router.post("/daily/claim", response_model=DailyClaimOut) async def daily_claim(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): char = (await db.execute(select(Character).where(Character.user_id == user.id))).scalar_one_or_none() if not char: raise HTTPException(404, "Karakter yok") today = datetime.now(timezone.utc).date() _roll_daily(char, today) if char.last_claim_date == today: raise HTTPException(400, "Bugünkü ödülü zaten aldın") if char.daily_steps < DAILY_GOAL: raise HTTPException(400, f"Hedef dolmadı: {char.daily_steps}/{DAILY_GOAL}") char.streak_days += 1 char.last_claim_date = today bonus = _bonus_for(char.streak_days) char.step_balance += bonus char.gold += DAILY_GOLD_BONUS await db.execute(text( "INSERT INTO step_log (character_id, steps_delta, source, accepted, reason) " "VALUES (:cid, :sd, 'daily_bonus', true, :rsn)" ), {"cid": char.id, "sd": bonus, "rsn": f"streak={char.streak_days}"}) await db.commit() return DailyClaimOut( streak_days=char.streak_days, bonus_steps=bonus, bonus_gold=DAILY_GOLD_BONUS, step_balance=char.step_balance, gold=char.gold, )