232 lines
7.5 KiB
Python
232 lines
7.5 KiB
Python
"""
|
|
Character management router.
|
|
Handles character creation, selection, and deletion.
|
|
"""
|
|
from fastapi import APIRouter, HTTPException, Depends, status, Request
|
|
from fastapi.security import HTTPAuthorizationCredentials
|
|
|
|
from ..items import items_manager
|
|
from ..services.helpers import enrich_character_data, get_game_message
|
|
|
|
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": [
|
|
await enrich_character_data(char, items_manager)
|
|
for char in characters
|
|
]
|
|
}
|
|
|
|
|
|
@router.post("")
|
|
async def create_character_endpoint(
|
|
character: CharacterCreate,
|
|
request: Request,
|
|
credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
):
|
|
"""Create a new character"""
|
|
token = credentials.credentials
|
|
locale = request.headers.get('Accept-Language', 'en')
|
|
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": get_game_message('character_created', locale),
|
|
"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,
|
|
request: Request,
|
|
credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
):
|
|
"""Delete a character"""
|
|
token = credentials.credentials
|
|
locale = request.headers.get('Accept-Language', 'en')
|
|
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": get_game_message('character_deleted', locale, name=character['name'])
|
|
}
|