Character portrait
Crop-and-zoom picture editor on the Concept page, stored (re-encoded to a 256 px WebP data URL) inside the character; shown on the sheet header, in the lobby player bar and as token icon on the map. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
This commit is contained in:
+18
-2
@@ -9,7 +9,7 @@ from pathlib import Path
|
||||
|
||||
import asyncio
|
||||
from fastapi import APIRouter, Body, Depends, FastAPI, HTTPException, Request, Response, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.responses import FileResponse, JSONResponse, Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from . import auth
|
||||
@@ -17,6 +17,7 @@ from .derive import compute
|
||||
from . import rules_edit
|
||||
from . import maps as maps_mod
|
||||
from . import sounds as sounds_mod
|
||||
from . import portrait as portrait_mod
|
||||
from .realtime import hub
|
||||
from .export import to_docx, to_pdf
|
||||
|
||||
@@ -386,6 +387,7 @@ def my_characters(user: dict = Depends(current_user)):
|
||||
|
||||
@api.post("/characters")
|
||||
def create_character(ch: dict = Body(...), user: dict = Depends(current_user)):
|
||||
portrait_mod.clean(ch)
|
||||
check_size(ch)
|
||||
with db() as con:
|
||||
if con.execute("SELECT COUNT(*) FROM characters WHERE owner_id=?", (user["id"],)).fetchone()[0] >= MAX_CHARACTERS:
|
||||
@@ -437,6 +439,7 @@ def remove_share(cid: str, username: str, user: dict = Depends(current_user)):
|
||||
|
||||
@api.put("/characters/{cid}")
|
||||
def update_character(cid: str, ch: dict = Body(...), user: dict = Depends(current_user)):
|
||||
portrait_mod.clean(ch)
|
||||
check_size(ch)
|
||||
with db() as con:
|
||||
_owned(con, "characters", cid, user)
|
||||
@@ -533,7 +536,7 @@ def _card(data: str, rules: dict) -> dict:
|
||||
prof = next((p for p in rules["professions"] if p["id"] == ch.get("profession")), None)
|
||||
return {"name": ch.get("name") or "Nameless", "concept": str(ch.get("concept") or "")[:120],
|
||||
"class": (cls or {}).get("name", ""), "subclass": (sub or {}).get("name", ""),
|
||||
"profession": (prof or {}).get("name", "")}
|
||||
"profession": (prof or {}).get("name", ""), "portrait": portrait_mod.tag(ch.get("portrait"))}
|
||||
|
||||
|
||||
@api.get("/lobbies")
|
||||
@@ -590,6 +593,19 @@ def join_lobby(request: Request, body: dict = Body(...), user: dict = Depends(cu
|
||||
return {"id": lobby["id"]}
|
||||
|
||||
|
||||
@api.get("/lobbies/{lid}/portrait/{username}")
|
||||
def lobby_portrait(lid: str, username: str, user: dict = Depends(current_user)):
|
||||
"""The portrait of a player's character in this lobby (icon on the map and in the player bar). Members only."""
|
||||
with db() as con:
|
||||
_lobby_for(con, lid, user)
|
||||
row = con.execute("""SELECT c.data FROM lobby_members m JOIN users u ON u.id = m.user_id
|
||||
JOIN characters c ON c.id = m.character_id WHERE m.lobby_id=? AND u.username=?""", (lid, username)).fetchone()
|
||||
img = portrait_mod.image(json.loads(row["data"]).get("portrait")) if row else None
|
||||
if not img:
|
||||
raise HTTPException(404, "No portrait")
|
||||
return Response(img, media_type="image/webp", headers={"Cache-Control": "private, max-age=86400"})
|
||||
|
||||
|
||||
@api.get("/lobbies/{lid}")
|
||||
def read_lobby(lid: str, user: dict = Depends(current_user)):
|
||||
rules = load_rules()
|
||||
|
||||
Reference in New Issue
Block a user