Commit
This commit is contained in:
238
api/routers/characters.py
Normal file
238
api/routers/characters.py
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user