This commit is contained in:
Joan
2025-11-27 16:27:01 +01:00
parent 33cc9586c2
commit 81f8912059
304 changed files with 56149 additions and 10122 deletions

238
api/routers/characters.py Normal file
View File

@@ -0,0 +1,238 @@
"""
Character management router.
Handles character creation, selection, and deletion.
"""
from fastapi import APIRouter, HTTPException, Depends, status
from fastapi.security import HTTPAuthorizationCredentials
from ..core.security import decode_token, create_access_token, security
from ..services.models import CharacterCreate, CharacterSelect
from .. import database as db
router = APIRouter(prefix="/api/characters", tags=["characters"])
@router.get("")
async def list_characters(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""List all characters for the logged-in account"""
token = credentials.credentials
payload = decode_token(token)
account_id = payload.get("account_id")
if not account_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
characters = await db.get_characters_by_account_id(account_id)
return {
"characters": [
{
"id": char["id"],
"name": char["name"],
"level": char["level"],
"xp": char["xp"],
"hp": char["hp"],
"max_hp": char["max_hp"],
"stamina": char["stamina"],
"max_stamina": char["max_stamina"],
"avatar_data": char.get("avatar_data"),
"location_id": char["location_id"],
"created_at": char["created_at"],
"last_played_at": char.get("last_played_at"),
}
for char in characters
]
}
@router.post("")
async def create_character_endpoint(
character: CharacterCreate,
credentials: HTTPAuthorizationCredentials = Depends(security)
):
"""Create a new character"""
token = credentials.credentials
payload = decode_token(token)
account_id = payload.get("account_id")
if not account_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
# Check if account can create more characters
can_create, error_msg = await db.can_create_character(account_id)
if not can_create:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=error_msg
)
# Validate character name
if len(character.name) < 3 or len(character.name) > 20:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Character name must be between 3 and 20 characters"
)
# Check if name is unique
existing = await db.get_character_by_name(character.name)
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Character name already taken"
)
# Validate stat allocation (must total 20 points)
total_stats = character.strength + character.agility + character.endurance + character.intellect
if total_stats != 20:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Must allocate exactly 20 stat points (you allocated {total_stats})"
)
# Validate each stat is >= 0
if any(stat < 0 for stat in [character.strength, character.agility, character.endurance, character.intellect]):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Stats cannot be negative"
)
# Create character
new_character = await db.create_character(
account_id=account_id,
name=character.name,
strength=character.strength,
agility=character.agility,
endurance=character.endurance,
intellect=character.intellect,
avatar_data=character.avatar_data
)
if not new_character:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create character"
)
return {
"message": "Character created successfully",
"character": {
"id": new_character["id"],
"name": new_character["name"],
"level": new_character["level"],
"strength": new_character["strength"],
"agility": new_character["agility"],
"endurance": new_character["endurance"],
"intellect": new_character["intellect"],
"hp": new_character["hp"],
"max_hp": new_character["max_hp"],
"stamina": new_character["stamina"],
"max_stamina": new_character["max_stamina"],
"location_id": new_character["location_id"],
"avatar_data": new_character.get("avatar_data"),
}
}
@router.post("/select")
async def select_character(
selection: CharacterSelect,
credentials: HTTPAuthorizationCredentials = Depends(security)
):
"""Select a character to play"""
token = credentials.credentials
payload = decode_token(token)
account_id = payload.get("account_id")
if not account_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
# Verify character belongs to account
character = await db.get_character_by_id(selection.character_id)
if not character:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Character not found"
)
if character["account_id"] != account_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Character does not belong to this account"
)
# Update last played timestamp
await db.update_character_last_played(selection.character_id)
# Create new token with character_id
access_token = create_access_token({
"account_id": account_id,
"character_id": selection.character_id
})
return {
"access_token": access_token,
"token_type": "bearer",
"character": {
"id": character["id"],
"name": character["name"],
"level": character["level"],
"xp": character["xp"],
"hp": character["hp"],
"max_hp": character["max_hp"],
"stamina": character["stamina"],
"max_stamina": character["max_stamina"],
"strength": character["strength"],
"agility": character["agility"],
"endurance": character["endurance"],
"intellect": character["intellect"],
"location_id": character["location_id"],
"avatar_data": character.get("avatar_data"),
}
}
@router.delete("/{character_id}")
async def delete_character_endpoint(
character_id: int,
credentials: HTTPAuthorizationCredentials = Depends(security)
):
"""Delete a character"""
token = credentials.credentials
payload = decode_token(token)
account_id = payload.get("account_id")
if not account_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
# Verify character belongs to account
character = await db.get_character_by_id(character_id)
if not character:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Character not found"
)
if character["account_id"] != account_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Character does not belong to this account"
)
# Delete character
await db.delete_character(character_id)
return {
"message": f"Character '{character['name']}' deleted successfully"
}