İlk commit: Stepstead backend (FastAPI)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jts 2026-07-11 11:30:24 +03:00
commit ea049b8ccd
45 changed files with 6751 additions and 0 deletions

90
routers/character.py Normal file
View file

@ -0,0 +1,90 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession
from config import settings
from deps import get_current_user
from models import Character, Skill, User, get_db
router = APIRouter(prefix="/character", tags=["character"])
class CharacterCreateIn(BaseModel):
name: str = Field(min_length=3, max_length=32, pattern=r"^[A-Za-z0-9_]+$")
class SkillOut(BaseModel):
skill_name: str
xp: int
level: int
class CharacterOut(BaseModel):
id: int
name: str
level: int
total_xp: int
gold: int
step_balance: int
step_lifetime: int
location_code: Optional[str] = None
location_name: Optional[str] = None
has_pending_event: bool = False
skills: list[SkillOut]
class Config:
from_attributes = True
async def _with_location(db: AsyncSession, char: Character) -> CharacterOut:
out = CharacterOut.model_validate(char, from_attributes=True)
out.has_pending_event = char.pending_event is not None
if char.location_id is not None:
row = (await db.execute(text(
"SELECT code, name FROM locations WHERE id = :i"
), {"i": char.location_id})).first()
if row:
out.location_code, out.location_name = row[0], row[1]
return out
@router.post("", response_model=CharacterOut, status_code=status.HTTP_201_CREATED)
async def create_character(data: CharacterCreateIn, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
exists = (await db.execute(select(Character).where(Character.user_id == user.id))).scalar_one_or_none()
if exists:
raise HTTPException(409, "Zaten bir karakterin var")
name_taken = (await db.execute(select(Character).where(Character.name == data.name))).scalar_one_or_none()
if name_taken:
raise HTTPException(409, "Bu isim alınmış")
char = Character(user_id=user.id, name=data.name)
db.add(char)
await db.flush()
for skill_name in settings.SKILL_NAMES:
db.add(Skill(character_id=char.id, skill_name=skill_name, xp=0, level=1))
# Başlangıç lokasyonu: köy. Not: başlangıç tool'u YOK —
# ilk tool'lar köyde elle toplanan taş/sopa/sarmaşıktan craft edilir.
start = (await db.execute(text(
"SELECT id FROM locations WHERE type = 'village' ORDER BY id LIMIT 1"
))).first()
if start:
char.location_id = start[0]
await db.commit()
await db.refresh(char, ["skills"])
return await _with_location(db, char)
@router.get("/me", response_model=CharacterOut)
async def my_character(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, önce oluştur")
await db.refresh(char, ["skills"])
return await _with_location(db, char)