194 lines
7 KiB
Python
194 lines
7 KiB
Python
"""
|
||
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,
|
||
)
|