diff --git a/game.go b/game.go index 7641ad3..71e1e8d 100644 --- a/game.go +++ b/game.go @@ -270,28 +270,19 @@ func dataHandler(w http.ResponseWriter, r *http.Request) { } return case "prompt": - // host-only poll: while a prompt challenge is live, hand the host - // the spinner's name, the prompt text, and how many seconds have - // already elapsed so their popup can sync its countdown and enable - // the "fail" choice on time. if state.Game.StateID != statePrompt { w.WriteHeader(http.StatusNoContent) return } - if !state.isHost(cookieKey) { - w.WriteHeader(http.StatusNoContent) - return - } spin, err := queries.SpinPendingModifier(r.Context(), gameID) if err != nil || spin.Type != "prompt" || !spin.PlayerID.Valid { - log.Debug("no pending prompt for host poll", + log.Debug("no pending prompt for poll", "game_id", gameID, "error", err) w.WriteHeader(http.StatusNoContent) return } - elapsed, err := queries.SpinLatestElapsedSeconds(r.Context(), gameID) - if err != nil { - log.Error("measure prompt elapsed", "error", err, "game_id", gameID) + // the spinner already has the card via the newPrompt HX-Trigger + if spin.PlayerID.Int32 == int32(state.CallerID) { w.WriteHeader(http.StatusNoContent) return } @@ -303,13 +294,26 @@ func dataHandler(w http.ResponseWriter, r *http.Request) { } } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]any{ - "spin_id": spin.ID, - "spinner": spinnerName, - "prompt": spin.Front, - "elapsed": elapsed, - "window": promptSeconds, - }) + if state.isHost(cookieKey) { + elapsed, err := queries.SpinLatestElapsedSeconds(r.Context(), gameID) + if err != nil { + log.Error("measure prompt elapsed", "error", err, "game_id", gameID) + w.WriteHeader(http.StatusNoContent) + return + } + json.NewEncoder(w).Encode(map[string]any{ + "spin_id": spin.ID, + "spinner": spinnerName, + "prompt": spin.Front, + "elapsed": elapsed, + "window": promptSeconds, + }) + } else { + json.NewEncoder(w).Encode(map[string]any{ + "spinner": spinnerName, + "prompt": spin.Front, + }) + } return case "infraction": if state.Game.StateID != stateChallenge { diff --git a/static/html/tmpl.game.html b/static/html/tmpl.game.html index fc84fe2..bcc4452 100644 --- a/static/html/tmpl.game.html +++ b/static/html/tmpl.game.html @@ -99,6 +99,14 @@

prompt challenge!

+ +
+

+
+ +
+
+

prompt challenge

diff --git a/static/js/prompt.js b/static/js/prompt.js index 844e9d4..3be2227 100644 --- a/static/js/prompt.js +++ b/static/js/prompt.js @@ -30,6 +30,17 @@ // a fresh challenge: show the countdown view, clear any prior outcome. spinnerActive = true; + var self = document.getElementById("self"); + if (self) { + var myName = self.textContent.trim(); + var items = document.querySelectorAll('#event-log .event[data-event-type="prompt"]'); + for (var i = items.length - 1; i >= 0; i--) { + if (items[i].getAttribute("data-target") === myName) { + lastOutcomeId = items[i].getAttribute("data-event-id"); + break; + } + } + } if (title) title.textContent = "prompt challenge!"; if (waiting) waiting.hidden = true; if (outcome) outcome.hidden = true; @@ -206,17 +217,28 @@ } }); - // handlePrompt runs after every host prompt-poll. 200 = a live challenge to - // rule on; 204 = none (close any stale dialog). + // ---- spectator side: read-only card view ---- + + var spectatePrompt = null; // prompt text currently shown to spectators + + function closeSpectateDialog() { + var dialog = document.getElementById("prompt-spectate-dialog"); + if (dialog && dialog.open) dialog.close(); + spectatePrompt = null; + } + + // handlePrompt runs after every prompt-poll. 200 = a live challenge; + // 204 = none (close any stale dialog). The payload shape determines + // the role: spin_id present → host; absent → spectator. window.handlePrompt = function (e) { - var dialog = document.getElementById("prompt-decide-dialog"); - if (!dialog) return; + var decideDialog = document.getElementById("prompt-decide-dialog"); if (e.detail.xhr.status !== 200) { - if (dialog.open && hostSpinId !== null) { + if (decideDialog && decideDialog.open && hostSpinId !== null) { clearHostTimer(); - dialog.close(); + decideDialog.close(); hostSpinId = null; } + closeSpectateDialog(); return; } var data; @@ -225,44 +247,57 @@ } catch (err) { return; } - if (String(data.spin_id) === String(lastRuledSpinId)) return; // already ruled - if (String(data.spin_id) === String(hostSpinId)) return; // already showing - // a new challenge: populate and open. anchor the countdown to the spin - // time the server reported (now minus the elapsed seconds) so it stays - // accurate however late this host opened the popup. - hostSpinId = data.spin_id; - var spinnerEl = document.getElementById("prompt-decide-spinner"); - var contentEl = document.getElementById("prompt-decide-content"); - if (spinnerEl) spinnerEl.textContent = data.spinner + "'s challenge:"; - if (contentEl) contentEl.textContent = data.prompt; + if (spinnerActive) return; - var window_ = data.window || 60; - var anchor = Date.now() - (data.elapsed || 0) * 1000; - var countdown = document.getElementById("prompt-decide-countdown"); - var failBtn = document.getElementById("prompt-fail-btn"); - if (failBtn) failBtn.disabled = true; + if (data.spin_id != null) { + // host path + if (String(data.spin_id) === String(lastRuledSpinId)) return; + if (String(data.spin_id) === String(hostSpinId)) return; - clearHostTimer(); - function tick() { - var remaining = Math.ceil((window_ * 1000 - (Date.now() - anchor)) / 1000); - if (remaining > 0) { - if (countdown) { - countdown.textContent = remaining; - countdown.classList.remove("prompt-countdown-done"); - } - if (failBtn) failBtn.disabled = true; - } else { - if (countdown) { - countdown.textContent = "0"; - countdown.classList.add("prompt-countdown-done"); + hostSpinId = data.spin_id; + var spinnerEl = document.getElementById("prompt-decide-spinner"); + var contentEl = document.getElementById("prompt-decide-content"); + if (spinnerEl) spinnerEl.textContent = data.spinner + "'s challenge:"; + if (contentEl) contentEl.textContent = data.prompt; + + var window_ = data.window || 60; + var anchor = Date.now() - (data.elapsed || 0) * 1000; + var countdown = document.getElementById("prompt-decide-countdown"); + var failBtn = document.getElementById("prompt-fail-btn"); + if (failBtn) failBtn.disabled = true; + + clearHostTimer(); + function tick() { + var remaining = Math.ceil((window_ * 1000 - (Date.now() - anchor)) / 1000); + if (remaining > 0) { + if (countdown) { + countdown.textContent = remaining; + countdown.classList.remove("prompt-countdown-done"); + } + if (failBtn) failBtn.disabled = true; + } else { + if (countdown) { + countdown.textContent = "0"; + countdown.classList.add("prompt-countdown-done"); + } + if (failBtn) failBtn.disabled = false; + clearHostTimer(); } - if (failBtn) failBtn.disabled = false; - clearHostTimer(); } + tick(); + hostTimer = setInterval(tick, 250); + if (!decideDialog.open) decideDialog.showModal(); + } else { + // spectator path + if (data.prompt === spectatePrompt) return; + spectatePrompt = data.prompt; + var dialog = document.getElementById("prompt-spectate-dialog"); + var spinnerEl = document.getElementById("prompt-spectate-spinner"); + var contentEl = document.getElementById("prompt-spectate-content"); + if (spinnerEl) spinnerEl.textContent = data.spinner + "’s challenge"; + if (contentEl) contentEl.textContent = data.prompt; + if (dialog && !dialog.open) dialog.showModal(); } - tick(); - hostTimer = setInterval(tick, 250); - if (!dialog.open) dialog.showModal(); }; })();