From 1c672f0ab8d943cdc1c634dce4f524119281c790 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 13:32:57 +0000 Subject: [PATCH] Lobby: private rolls and checks for players, DM tab with lobby actions Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K --- backend/app/main.py | 9 +++++---- frontend/src/Lobbies.jsx | 43 ++++++++++++++++++++++++---------------- frontend/src/styles.css | 2 ++ 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 4bc8ee7..0ae5d7d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -634,15 +634,15 @@ def set_member_mod(lid: str, username: str, body: dict = Body(...), user: dict = def _recipient(con, lobby, user: dict, to): - """User id to whisper to (the DM or a member, not yourself), None for everybody.""" + """User id to whisper to (the DM, a member or yourself for private rolls and notes), None for everybody.""" to = str(to or "").strip() if not to: return None target = con.execute("""SELECT u.id FROM users u WHERE u.username=? AND (u.id=? OR EXISTS (SELECT 1 FROM lobby_members m WHERE m.lobby_id=? AND m.user_id=u.id))""", (to, lobby["owner_id"], lobby["id"])).fetchone() - if not target or target["id"] == user["id"]: - raise HTTPException(422, "You can only whisper to other people in this lobby") + if not target: + raise HTTPException(422, "You can only whisper to people in this lobby") return target["id"] @@ -796,7 +796,8 @@ def roll_check(lid: str, body: dict = Body(...), user: dict = Depends(current_us payload = {"kind": kind, "what": what, "base": base, "mod": lobby["diff_mod"], "cmod": member["char_mod"], "diff": lobby["diff_label"], "target": target, "roll": roll, "margin": target - roll, "outcome": outcome, "hidden": bool(lobby["diff_hidden"]), "manual": bool(manual)} - mid = _post(con, lid, user["id"], None, "check", payload) + private = str(body.get("to") or "") == user["username"] # a private check is only seen by the roller + mid = _post(con, lid, user["id"], user["id"] if private else None, "check", payload) return {"id": mid, "roll": roll, "outcome": outcome} diff --git a/frontend/src/Lobbies.jsx b/frontend/src/Lobbies.jsx index b635c24..c38e4d0 100644 --- a/frontend/src/Lobbies.jsx +++ b/frontend/src/Lobbies.jsx @@ -177,7 +177,7 @@ function Chat({ me, people, msgs, poll, id, to, setTo }) { const who = (m) => ( <> {m.mine ? 'You' : m.from || '?'} - {m.to && {m.to === m.from ? '(only visible to you, the DM)' : m.mine ? `to ${m.to} (private)` : 'to you (private)'}} + {m.to && {m.to === m.from ? '(private, only you)' : m.mine ? `to ${m.to} (private)` : 'to you (private)'}} ) @@ -216,6 +216,7 @@ function Chat({ me, people, msgs, poll, id, to, setTo }) {
setText(e.target.value)} placeholder={to ? `Whisper to ${to} …` : 'Message …'} maxLength={500} aria-label="Message" autoComplete="off" /> @@ -243,7 +244,7 @@ function DieIcon({ sides }) { const MODES = [['mystery', 'Mystery', 'Players see grey dice, but not the result'], ['hidden', 'Hidden', 'Only you see the roll'], ['public', 'Public', 'Everybody sees the roll']] -function DiceBar({ to, isDm, count, setCount, mode, setMode, own, setOwn, onRoll, busy }) { +function DiceBar({ to, me, isDm, count, setCount, mode, setMode, own, setOwn, onRoll, busy }) { return (
) } @@ -346,7 +347,7 @@ function DiceTray({ rolls }) { const who = {m.mine ? 'You' : m.from} if (m.kind === 'check') { const c = m.check - return
{who} – {c.what}{c.manual ? ' · own dice' : ''}: {OUTCOME[c.outcome]}{c.target !== undefined && ({c.roll} vs {c.target})}
+ return
{who} – {c.what}{c.manual ? ' · own dice' : ''}{m.to ? ' · only you' : ''}: {OUTCOME[c.outcome]}{c.target !== undefined && ({c.roll} vs {c.target})}
} const t = rollText(m) return
{who} rolled {t.dice}{m.to ? (m.to === m.from ? ' · only you' : ` · private${m.mine ? ` to ${m.to}` : ''}`) : ''}{m.roll.manual ? ' · own dice' : ''} @@ -429,14 +430,14 @@ function CharTab({ cid, rules, onRoll }) { } // lobby settings: join code, rename, colour, character switch, members, leave / delete -function Settings({ lobby, id, mine, act, setInfo, onClose }) { +function Settings({ lobby, id, mine, act, setInfo, onClose, inline }) { const [rename, setRename] = useState('') const [pick, setPick] = useState('') const me = lobby.members.find((m) => m.me) const taken = new Set(lobby.members.filter((m) => !m.me).map((m) => m.color)) return ( -
-

Lobby

+
+ {!inline &&

Lobby

} {lobby.is_dm && ( <>
@@ -505,6 +506,7 @@ function LobbyView({ id, rules, user }) { const [busy, setBusy] = useState(false) const [mErr, setMErr] = useState('') const [showSettings, setShowSettings] = useState(false) + const [leftTab, setLeftTab] = useState('sheet') // the DM's left column: character sheet or lobby actions const [sheetOpen, setSheetOpen] = useState(() => store('aw_sheetopen') !== '0') const setOwn = (v) => { setOwnState(v); store('aw_owndice', v ? '1' : '0') } @@ -560,10 +562,10 @@ function LobbyView({ id, rules, user }) { const check = (kind, cid, name) => { if (busy) return if (own) return setPending({ type: 'check', kind, id: cid, name }) - post(`/api/lobbies/${id}/checks`, { kind, id: cid }) + post(`/api/lobbies/${id}/checks`, { kind, id: cid, to: dest === user ? user : '' }) } const submitManual = (vals) => { - if (pending.type === 'check') post(`/api/lobbies/${id}/checks`, { kind: pending.kind, id: pending.id, manual: vals[0] }, () => setPending(null)) + if (pending.type === 'check') post(`/api/lobbies/${id}/checks`, { kind: pending.kind, id: pending.id, manual: vals[0], to: dest === user ? user : '' }, () => setPending(null)) else post(`/api/lobbies/${id}/rolls`, { sides: pending.sides, count: pending.count, to: dest, manual: vals, ...(lobby.is_dm ? { mode } : {}) }, () => setPending(null)) } @@ -574,7 +576,7 @@ function LobbyView({ id, rules, user }) {

{lobby.name}

DM: {lobby.dm}{lobby.is_dm ? ' (you)' : ''} - +
{err &&

{err}

} @@ -584,26 +586,33 @@ function LobbyView({ id, rules, user }) { {sheetOpen && (
- {cur ? cur.character.name : 'Character sheet'} - + {lobby.is_dm ? ( + + + + + ) : {cur ? cur.character.name : 'Character sheet'}} + {(!lobby.is_dm || leftTab === 'sheet') && }
- {cur ? - :

{lobby.is_dm ? 'Pick a character in the bar to look at its sheet.' : 'You have no character in this lobby.'}

} + {lobby.is_dm && leftTab === 'lobby' + ? + : cur ? + :

{lobby.is_dm ? 'Pick a character in the bar to look at its sheet.' : 'You have no character in this lobby.'}

}
)}
{!sheetOpen && } - + { setSel(u); setLeftTab('sheet') }} /> act(`/api/lobbies/${id}/difficulty`, 'PUT', b)} />
- {showSettings && setShowSettings(false)} />} + {showSettings && !lobby.is_dm && setShowSettings(false)} />} {canCheck && !pending &&

Click an attribute or skill on your sheet to roll it.

} {pending && { setPending(null); setMErr('') }} err={mErr} />} {!pending && mErr &&

{mErr}

}
- +