Lobby: maps, map editor and tokens

DM map library (upload re-encoded with Pillow), map editor with prepared
enemies/NPCs, map on the lobby table with personal pan/zoom, draggable
tokens (players move their own, DM all), full/player view, live sync via
the WebSocket.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
This commit is contained in:
Claude
2026-10-23 13:59:21 +00:00
parent 162d8c9224
commit e8755582d5
8 changed files with 679 additions and 9 deletions
+9
View File
@@ -15,6 +15,7 @@ from fastapi.staticfiles import StaticFiles
from . import auth
from .derive import compute
from . import rules_edit
from . import maps as maps_mod
from .realtime import hub
from .export import to_docx, to_pdf
@@ -100,6 +101,7 @@ def db() -> sqlite3.Connection:
("lobby_members", "char_mod", "INTEGER NOT NULL DEFAULT 0")):
if col not in [r["name"] for r in con.execute(f"PRAGMA table_info({table})")]:
con.execute(f"ALTER TABLE {table} ADD COLUMN {col} {ddl}")
maps_mod.schema(con)
for table in ("characters", "presets"): # older databases: add the owner column
if "owner_id" not in [r["name"] for r in con.execute(f"PRAGMA table_info({table})")]:
con.execute(f"ALTER TABLE {table} ADD COLUMN owner_id INTEGER")
@@ -581,6 +583,7 @@ def join_lobby(request: Request, body: dict = Body(...), user: dict = Depends(cu
con.execute("""INSERT INTO lobby_members (lobby_id, user_id, character_id, joined, color) VALUES (?,?,?,?,?)
ON CONFLICT(lobby_id, user_id) DO UPDATE SET character_id=excluded.character_id""",
(lobby["id"], user["id"], body["character_id"], time.time(), _free_color(con, lobby["id"])))
hub.notify(lobby["id"], "map")
_event(con, lobby["id"], f"{user['username']} {'switched to' if known else 'joined as'} {_char_name(con, body['character_id'])}", user["id"])
return {"id": lobby["id"]}
@@ -836,6 +839,7 @@ def delete_lobby(lid: str, user: dict = Depends(current_user)):
_dm_only(_lobby_for(con, lid, user), user)
con.execute("DELETE FROM lobby_members WHERE lobby_id=?", (lid,))
con.execute("DELETE FROM lobby_messages WHERE lobby_id=?", (lid,))
con.execute("DELETE FROM lobby_tokens WHERE lobby_id=?", (lid,))
con.execute("DELETE FROM lobbies WHERE id=?", (lid,))
hub.close_lobby(lid)
return {"ok": True}
@@ -884,6 +888,7 @@ def leave_lobby(lid: str, user: dict = Depends(current_user)):
if gone:
_event(con, lid, f"{user['username']} left the lobby", user["id"])
if gone:
hub.notify(lid, "map")
hub.close_user(lid, user["id"])
return {"ok": True}
@@ -898,6 +903,7 @@ def kick_member(lid: str, username: str, user: dict = Depends(current_user)):
_event(con, lid, f"{username} was removed by the DM", user["id"])
gone_id = con.execute("SELECT id FROM users WHERE username=?", (username,)).fetchone()["id"]
if gone:
hub.notify(lid, "map")
hub.close_user(lid, gone_id)
return {"ok": True}
@@ -1011,6 +1017,9 @@ def editor_reset(section: str, confirm: bool = False, user: dict = Depends(admin
return {"ok": True}
maps_mod.install(api, db, current_user, _lobby_for, _dm_only, hub, _event, Path(DB_PATH).parent / "maps")
@app.websocket("/api/lobbies/{lid}/ws")
async def lobby_socket(websocket: WebSocket, lid: str):
"""One socket per open lobby page. Only notes go out ("messages", "lobby", "sheet"), the data is fetched through the API."""