Prop library page, DM notes on tokens and props, fog/sight without walls, finer explored mask

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
This commit is contained in:
2026-10-24 20:23:23 +00:00
co-authored by Claude Sonnet 5.5
parent 6880b58de2
commit 1405381096
6 changed files with 122 additions and 20 deletions
+32 -14
View File
@@ -62,7 +62,7 @@ def schema(con):
("lobby_tokens", "show_cond", "INTEGER NOT NULL DEFAULT 0"),
("lobby_members", "vgroup", "INTEGER NOT NULL DEFAULT 0"),
("lobby_tokens", "prop", "TEXT"), ("lobby_tokens", "rot", "REAL NOT NULL DEFAULT 0"), ("lobby_tokens", "size_h", "REAL"),
("lobbies", "scene_id", "TEXT")):
("lobbies", "scene_id", "TEXT"), ("lobby_tokens", "note", "TEXT")):
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}")
@@ -107,6 +107,7 @@ def clean_token(t: dict, w: int, h: int, keep_id=True) -> dict:
tid = str(t.get("id", "")) if keep_id else ""
npc = str(t.get("npc_id") or "")
hp_max, hp = clean_int(t.get("hp_max"), 1, 9999), clean_int(t.get("hp"), -999, 9999)
note = str(t.get("note") or "")[:2000]
prop = str(t.get("prop") or "")
prop = prop if re.fullmatch(r"[bc]:[a-z0-9_]{1,24}", prop) else ""
try:
@@ -117,7 +118,7 @@ def clean_token(t: dict, w: int, h: int, keep_id=True) -> dict:
return {"id": tid if tid.isalnum() and len(tid) <= 16 else uuid.uuid4().hex[:12], "name": name, "x": x, "y": y,
"size": size, "color": clean_color(t.get("color")), "hidden": bool(t.get("hidden")),
"npc_id": npc if npc.isalnum() and len(npc) <= 16 else "", "hp": hp, "hp_max": hp_max,
"prop": prop, "rot": rot, "size_h": size_h if prop else None}
"prop": prop, "rot": rot, "size_h": size_h if prop else None, "note": note}
def clean_int(v, lo, hi):
@@ -402,6 +403,15 @@ def install(api, db, current_user, lobby_for, dm_only, hub, event, store_dir: Pa
con.execute("INSERT INTO user_props (id, owner_id, name, created) VALUES (?,?,?,?)", (pid, user["id"], name.strip()[:30] or "Prop", time.time()))
return {"id": pid}
@api.put("/props/{pid}")
def rename_prop(pid: str, body: dict = Body(...), user: dict = Depends(current_user)):
name = str(body.get("name", "")).strip()[:30]
with db() as con:
if not name or not con.execute("SELECT 1 FROM user_props WHERE id=? AND owner_id=?", (pid, user["id"])).fetchone():
raise HTTPException(404, "Prop not found")
con.execute("UPDATE user_props SET name=? WHERE id=?", (name, pid))
return {"ok": True}
@api.delete("/props/{pid}")
def delete_prop(pid: str, user: dict = Depends(current_user)):
with db() as con:
@@ -443,13 +453,13 @@ def install(api, db, current_user, lobby_for, dm_only, hub, event, store_dir: Pa
def put_token(con, lid, t, npc=None, tid=None):
"""Inserts a (cleaned or snapshot) token row for the lobby."""
con.execute("""INSERT INTO lobby_tokens (id, lobby_id, name, x, y, color, size, hidden, npc_id, hp, hp_max, prop, rot, size_h)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
con.execute("""INSERT INTO lobby_tokens (id, lobby_id, name, x, y, color, size, hidden, npc_id, hp, hp_max, prop, rot, size_h, note)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(tid or uuid.uuid4().hex[:12], lid, t["name"], t["x"], t["y"], t["color"], t["size"], int(bool(t["hidden"])), npc,
t.get("hp") if t.get("hp") is not None else t.get("hp_max"), t.get("hp_max"), t.get("prop") or None, t.get("rot") or 0, t.get("size_h")))
t.get("hp") if t.get("hp") is not None else t.get("hp_max"), t.get("hp_max"), t.get("prop") or None, t.get("rot") or 0, t.get("size_h"), t.get("note") or None))
def mask_dims(m):
f = max(1, -(-max(m["w"], m["h"]) // 512))
f = max(1, -(-max(m["w"], m["h"]) // 1024))
return f, -(-m["w"] // f), -(-m["h"] // f)
def explore(con, lobby, viewer, m, polys, key):
@@ -466,7 +476,7 @@ def install(api, db, current_user, lobby_for, dm_only, hub, event, store_dir: Pa
old = Image.open(io.BytesIO(row["data"]))
old.load()
if old.size != (mw, mh):
old = None
old = old.resize((mw, mh), Image.BILINEAR) # memory made at another resolution is kept
base = old or Image.new("L", (mw, mh), 0)
cur = Image.new("L", (mw, mh), 0)
for poly in polys:
@@ -487,6 +497,11 @@ def install(api, db, current_user, lobby_for, dm_only, hub, event, store_dir: Pa
def lobby_map(con, lobby):
return con.execute("SELECT * FROM maps WHERE id=?", (lobby["map_id"],)).fetchone() if lobby["map_id"] else None
def vision_on(lobby, m) -> bool:
"""Players only see what their token sees: needs walls to block the view or a sight range to limit it."""
sight = lobby["sight"] if lobby["sight"] is not None else m["sight"]
return bool(lobby["los"] and (json.loads(m["walls"]) or sight > 0))
def view_ctx(con, lobby, m, user, as_name=None):
"""Whose eyes the table is shown through. Players: their own token. DM: everything, or (as=name) one player's view."""
is_dm = lobby["owner_id"] == user["id"]
@@ -506,7 +521,7 @@ def install(api, db, current_user, lobby_for, dm_only, hub, event, store_dir: Pa
if viewer is None:
return ctx
ctx["rev"] = "open:" + m["id"]
if not (lobby["los"] and walls):
if not vision_on(lobby, m):
return ctx
ctx["los"] = True
blocking = [(w["x1"], w["y1"], w["x2"], w["y2"]) for w in walls if w["kind"] == "wall" or not doors.get(w["id"])]
@@ -598,6 +613,8 @@ def install(api, db, current_user, lobby_for, dm_only, hub, event, store_dir: Pa
status.update(show_hp=bool(r["show_hp"]), show_cond=bool(r["show_cond"]), hp_hint=health(src) if src else None)
if r["prop"]:
status = {}
if full and not player:
status["note"] = r["note"] or ""
out.append({"id": r["id"], "kind": "player" if player else "prop" if r["prop"] else "npc", "prop": r["prop"], "rot": r["rot"], "size_h": r["size_h"], "username": r["username"], "name": name, "portrait": pt, **status,
"x": r["x"], "y": r["y"], "size": r["size"], "hidden": bool(r["hidden"]),
"color": (r["mcolor"] or "#999999") if player else r["color"],
@@ -774,7 +791,7 @@ def install(api, db, current_user, lobby_for, dm_only, hub, event, store_dir: Pa
# ---- scenes: saved states of the table (map + tokens + doors + reveals + explored ground + combat)
TOKEN_COLS = ("id", "name", "x", "y", "color", "size", "hidden", "npc_id", "hp", "hp_max", "conds", "show_hp", "show_cond", "prop", "rot", "size_h")
TOKEN_COLS = ("id", "name", "x", "y", "color", "size", "hidden", "npc_id", "hp", "hp_max", "conds", "show_hp", "show_cond", "prop", "rot", "size_h", "note")
def snapshot(con, lid) -> str:
lobby = con.execute("SELECT * FROM lobbies WHERE id=?", (lid,)).fetchone()
@@ -983,22 +1000,23 @@ def install(api, db, current_user, lobby_for, dm_only, hub, event, store_dir: Pa
t = con.execute("SELECT * FROM lobby_tokens WHERE id=? AND lobby_id=?", (tid, lid)).fetchone()
if not t or (not is_dm and t["user_id"] != user["id"]):
raise HTTPException(404, "Token not found")
if not is_dm and t["x"] is None and lobby["los"] and json.loads(m["walls"]):
if not is_dm and t["x"] is None and vision_on(lobby, m):
raise HTTPException(403, "The DM puts your token on the map") # with line of sight on you could not see where
merged = {"name": t["name"], "x": t["x"], "y": t["y"], "color": t["color"], "size": t["size"], "hidden": t["hidden"],
"prop": t["prop"], "rot": t["rot"], "size_h": t["size_h"]}
"prop": t["prop"], "rot": t["rot"], "size_h": t["size_h"], "note": t["note"] or ""}
if "x" in body or "y" in body:
merged["x"], merged["y"] = body.get("x"), body.get("y")
if is_dm and t["user_id"] is None:
for k in ("name", "color", "size", "hidden", "rot", "size_h"):
for k in ("name", "color", "size", "hidden", "rot", "size_h", "note"):
if k in body:
merged[k] = body[k]
elif is_dm and "size" in body:
merged["size"] = body["size"]
c = clean_token(merged, m["w"], m["h"])
con.execute("UPDATE lobby_tokens SET name=?, x=?, y=?, color=?, size=?, hidden=?, rot=?, size_h=? WHERE id=?",
con.execute("UPDATE lobby_tokens SET name=?, x=?, y=?, color=?, size=?, hidden=?, rot=?, size_h=?, note=? WHERE id=?",
(c["name"] if t["user_id"] is None else t["name"], c["x"], c["y"], c["color"], c["size"], int(c["hidden"]),
c["rot"] if t["user_id"] is None else 0, c["size_h"] if t["user_id"] is None else None, tid))
c["rot"] if t["user_id"] is None else 0, c["size_h"] if t["user_id"] is None else None,
(c["note"] or None) if t["user_id"] is None else None, tid))
if is_dm and any(k in body for k in ("hp", "hp_max", "conds", "show_hp", "show_cond")):
hp_max, hp = t["hp_max"], t["hp"]
if "hp_max" in body: