From 3e67aba75c0e819fbd2b3b7c827d398fa66cbabd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 13:20:55 +0000 Subject: [PATCH] Lobby: dice are thrown onto the table and rest at random spots; fix duplicate dice for the roller Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K --- frontend/src/Lobbies.jsx | 76 ++++++++++++++++++++++++---------------- frontend/src/styles.css | 9 +++++ 2 files changed, 55 insertions(+), 30 deletions(-) diff --git a/frontend/src/Lobbies.jsx b/frontend/src/Lobbies.jsx index aec83c4..b635c24 100644 --- a/frontend/src/Lobbies.jsx +++ b/frontend/src/Lobbies.jsx @@ -116,11 +116,14 @@ function useFeed(id, onEvent, onRoll) { const live = useRef(false) // false until the history is loaded: old rolls are not animated again const cb = useRef({}) cb.current = { onEvent, onRoll } + const busy = useRef(false) // two overlapping polls (timer + right after a roll) would both deliver the same entries const poll = useCallback(async () => { - if (document.hidden) return + if (document.hidden || busy.current) return + busy.current = true const r = await fetch(`/api/lobbies/${id}/messages?after=${last.current}`).catch(() => null) + busy.current = false if (!r || !r.ok) return - const fresh = await r.json() + const fresh = (await r.json()).filter((m) => m.id > last.current) const wasLive = live.current live.current = true if (!fresh.length) return @@ -306,37 +309,50 @@ function Die({ sides, value, delay, mystery }) { ) } -// the tray is the middle of the table: dice in the colour of whoever threw them +// dice are thrown onto the table from a random side and come to rest at a random spot, in the colour of whoever threw them +const placed = [] // spots of the dice still lying on the table, so new ones do not land on top of them +function spots(n) { + const out = [] + for (let i = 0; i < n; i++) { + let p, tries = 0 + do { p = { x: 12 + Math.random() * 76, y: 22 + Math.random() * 58 }; tries++ } while (tries < 40 && [...placed, ...out].some((q) => Math.hypot((q.x - p.x) * 1.6, q.y - p.y) < 14)) + placed.push(p); if (placed.length > 12) placed.shift() + const a = Math.random() * Math.PI * 2 + out.push({ ...p, sx: Math.round(Math.cos(a) * 700), sy: Math.round(Math.sin(a) * 450), sr: Math.round(Math.random() * 720 - 360), er: Math.round(Math.random() * 50 - 25) }) + } + return out +} + +function ThrownDice({ m }) { + const sec = m.kind === 'roll' && m.roll.mystery && !m.mine + const n = m.kind === 'check' ? 1 : m.roll.n + const [pos] = useState(() => spots(n)) + return pos.map((p, i) => ( + + {m.kind === 'check' ? + : } + + )) +} + function DiceTray({ rolls }) { if (!rolls.length) return null return ( ) } @@ -505,7 +521,7 @@ function LobbyView({ id, rules, user }) { const onRoll = useCallback((m) => { const key = `${m.id}` setTray((t) => [...t.slice(-2), { ...m, key }]) - setTimeout(() => setTray((t) => t.filter((x) => x.key !== key)), 9000) + setTimeout(() => setTray((t) => t.filter((x) => x.key !== key)), 20000) }, []) const feed = useFeed(id, load, onRoll) diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 9605f26..3694e56 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -375,3 +375,12 @@ body:has(.lobbychat) main { padding: .5rem calc(var(--chatw) + .5rem) 0 .5rem; } .lobbychat { position: static; width: auto; height: 28rem; border-width: 1px; border-radius: var(--radius); margin-top: 1rem; } .lobbygrid { grid-template-columns: minmax(0, 1fr); } .playarea { order: -1; height: 32rem; position: static; } .sheetcol { position: static; max-height: none; } } + +/* the table: dice fly in from a random side and rest at a random spot */ +.table { overflow: hidden; } +.dicetray { inset: 0; top: 0; left: 0; transform: none; display: block; max-width: none; } +.thrown { position: absolute; width: 2.9rem; height: 2.9rem; margin: -1.45rem 0 0 -1.45rem; transform: rotate(var(--er)); animation: throwin .8s cubic-bezier(.15, .7, .3, 1) both; filter: drop-shadow(0 4px 4px rgba(0, 0, 0, .35)); } +@keyframes throwin { from { transform: translate(var(--sx), var(--sy)) rotate(var(--sr)) scale(1.3); opacity: .2; } to { transform: rotate(var(--er)); opacity: 1; } } +.tablelog { position: absolute; left: .6rem; bottom: .5rem; display: grid; gap: .25rem; max-width: 70%; } +.tablelog .trayroll { padding: .2rem .6rem; border-radius: 8px; font-size: .85rem; background: color-mix(in srgb, var(--panel) 88%, transparent); border: 1px solid var(--line); border-top: 3px solid var(--line); box-shadow: none; } +@media (prefers-reduced-motion: reduce) { .thrown { animation: none; } }