Sig Select gate-view: CONT GAME/NVM keep the acting seat; restore the live countdown numeral on load — TDD
Two Sig-Select seat-switch follow-ups from the 2026-06-05 countdown sprint.
(1) CONT GAME + gear NVM pos-1 shuttle.
The GATE VIEW (room_gate.html) CONT GAME button and the gear-menu NVM both
linked to epic:room with NO ?seat, so a CARTE multi-seat gamer acting at pos 4
got shuttled back to owned[0] (pos 1) instead of staying on his acting seat —
the same gap bedc489 fixed for the GATE VIEW nav buttons. room_gate now
computes a single table_url (epic:room + ?seat=<current_slot> when seated) and
feeds it to both the CONT GAME onclick and the NVM include. The table view
already reads ?seat, so the gamer lands on the seat he was viewing. Added
`reverse` import to epic/views.py. 2 ITs in CarteTrayFollowsSelectedSeatTest
(cont-game + nvm carry the acting seat; default targets lowest owned); updated
RoomGateViewTest.test_nvm_returns_to_room_hex to expect the seat-carrying href.
(2) Live countdown numeral not restored on a fresh seat view mid-countdown.
countdown_start is a ONE-SHOT WS broadcast: a gamer (esp. a CARTE owner
switching to an already-ready seat) who loads the view after it fired saw a
static WAIT NVM, never the 12s flashing numeral — the redirect still fired, so
it was a visual-restore-on-load gap. The cache entry now stores the absolute
deadline alongside the timer token ({token, deadline} dict, was a bare token
string); _fire's token guard reads either shape defensively so a stale string
from an older deploy can't crash the callback. New tasks.countdown_remaining()
derives the seconds-left from the deadline (None when no countdown / elapsed).
The room view seeds ctx["countdown_remaining"] for the acting polarity;
_sig_select_overlay.html carries data-countdown-remaining; sig-select.js's
_replayReservations restores _showCountdown(remaining) on load when a count is
live, else falls back to WAIT NVM. 4 unit tests (CountdownRemainingTest), 2 ITs
(SigSelectRenderingTest), 3 Jasmine specs (SigSelectSpec countdown-restore).
922 epic+gameboard ITs + 19 task UTs + Jasmine SpecRunner all green.
Trap [[feedback-ws-cursor-group-must-match-acting-seat]].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -713,12 +713,23 @@ var SigSelect = (function () {
|
||||
Object.keys(existing).forEach(function (cardId) {
|
||||
applyReservation(cardId, existing[cardId], true);
|
||||
});
|
||||
// Restore WAIT NVM state if gamer was already ready before page load
|
||||
// Restore the ready-state button if the gamer was already
|
||||
// ready before this page load.
|
||||
if (overlay.dataset.ready === 'true' && _takeSigBtn) {
|
||||
_isReady = true;
|
||||
// If a polarity countdown is ALREADY running (its
|
||||
// one-shot countdown_start WS event fired before this
|
||||
// view loaded — e.g. a CARTE owner switching to an
|
||||
// already-ready seat), restore the live flashing numeral
|
||||
// from the server-seeded seconds-left. Else WAIT NVM.
|
||||
var _rem = parseInt(overlay.dataset.countdownRemaining, 10);
|
||||
if (_rem > 0) {
|
||||
_showCountdown(_rem);
|
||||
} else {
|
||||
_takeSigBtn.textContent = 'WAIT NVM';
|
||||
_startWaitNoGlow();
|
||||
}
|
||||
}
|
||||
};
|
||||
if (document.readyState === 'complete') {
|
||||
_replayReservations();
|
||||
|
||||
@@ -6,6 +6,7 @@ Single-process only — swap for a Celery task if production uses multiple
|
||||
web workers (gunicorn -w N with N > 1).
|
||||
"""
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
@@ -29,8 +30,13 @@ def _group_send(room_id, msg):
|
||||
|
||||
def _fire(room_id, polarity, token):
|
||||
"""Callback run by threading.Timer after the countdown expires."""
|
||||
# Token guard: if cancelled or superseded, cache entry will differ
|
||||
if cache.get(_cache_key(room_id, polarity)) != token:
|
||||
# Token guard: if cancelled or superseded, cache entry will differ. The
|
||||
# entry is now a {token, deadline} dict (was a bare token string before the
|
||||
# restore-on-load sprint) — read either shape defensively so a stale plain
|
||||
# string left by an older deploy doesn't crash the timer callback.
|
||||
entry = cache.get(_cache_key(room_id, polarity))
|
||||
stored_token = entry.get('token') if isinstance(entry, dict) else entry
|
||||
if stored_token != token:
|
||||
return
|
||||
|
||||
from apps.epic.models import Room, SigReservation
|
||||
@@ -79,7 +85,15 @@ def schedule_polarity_confirm(room_id, polarity, seconds):
|
||||
cancel_polarity_confirm(room_id, polarity)
|
||||
|
||||
token = str(uuid.uuid4())
|
||||
cache.set(_cache_key(room_id, polarity), token, timeout=int(seconds) + 60)
|
||||
# Store the absolute deadline alongside the token so a gamer loading a
|
||||
# fresh seat view mid-countdown can derive the seconds left (the flashing
|
||||
# numeral) instead of falling back to a static WAIT NVM. See
|
||||
# countdown_remaining() + sig-select.js's restore-on-load.
|
||||
cache.set(
|
||||
_cache_key(room_id, polarity),
|
||||
{'token': token, 'deadline': time.time() + seconds},
|
||||
timeout=int(seconds) + 60,
|
||||
)
|
||||
|
||||
timer = threading.Timer(seconds, _fire, args=[str(room_id), polarity, token])
|
||||
timer.daemon = True
|
||||
@@ -93,3 +107,16 @@ def cancel_polarity_confirm(room_id, polarity):
|
||||
if timer:
|
||||
timer.cancel()
|
||||
cache.delete(_cache_key(room_id, polarity))
|
||||
|
||||
|
||||
def countdown_remaining(room_id, polarity):
|
||||
"""Seconds left on the live polarity countdown, or None when no countdown
|
||||
is running (or it has already elapsed). Lets a fresh page load restore the
|
||||
flashing numeral mid-countdown instead of showing a static WAIT NVM — the
|
||||
`countdown_start` WS broadcast is one-shot, so a view that loads after it
|
||||
fired has nothing to seed `_showCountdown` with otherwise."""
|
||||
entry = cache.get(_cache_key(room_id, polarity))
|
||||
if not isinstance(entry, dict) or 'deadline' not in entry:
|
||||
return None
|
||||
remaining = int(round(entry['deadline'] - time.time()))
|
||||
return remaining if remaining > 0 else None
|
||||
|
||||
@@ -919,6 +919,37 @@ class CarteTrayFollowsSelectedSeatTest(TestCase):
|
||||
self.assertTrue(by_slot[1]["is_me_also"])
|
||||
self.assertEqual(by_slot[1]["state_class"], "tt-pos-me-also")
|
||||
|
||||
def test_gate_view_cont_game_and_nvm_carry_acting_seat(self):
|
||||
# Regression (CONT GAME pos-1 shuttle): the gate-view CONT GAME btn +
|
||||
# the gear NVM both linked to epic:room with NO ?seat, so a CARTE
|
||||
# multi-seat gamer acting at pos 4 got shuttled back to owned[0] (pos 1)
|
||||
# instead of staying on his acting seat. Both must carry the acting
|
||||
# ?seat through to the table (same fix shape as the GATE VIEW nav btns).
|
||||
gate_url = reverse("epic:room_gate", kwargs={"room_id": self.room.id})
|
||||
response = self.client.get(gate_url + "?seat=4")
|
||||
self.assertEqual(response.context["current_slot"], 4)
|
||||
# CONT GAME btn returns to the table hex at the acting seat.
|
||||
self.assertContains(
|
||||
response,
|
||||
f"window.location.href='{self.room_url}?seat=4'",
|
||||
)
|
||||
# Gear NVM (back to the hex) carries the acting seat too.
|
||||
self.assertContains(
|
||||
response,
|
||||
f'href="{self.room_url}?seat=4" class="btn btn-cancel"',
|
||||
)
|
||||
|
||||
def test_gate_view_cont_game_default_targets_lowest_owned(self):
|
||||
# No ?seat on the gate → CONT GAME + NVM carry the lowest owned slot
|
||||
# (current_slot), keeping the gate + table views in agreement.
|
||||
gate_url = reverse("epic:room_gate", kwargs={"room_id": self.room.id})
|
||||
response = self.client.get(gate_url)
|
||||
self.assertEqual(response.context["current_slot"], 1)
|
||||
self.assertContains(
|
||||
response,
|
||||
f"window.location.href='{self.room_url}?seat=1'",
|
||||
)
|
||||
|
||||
|
||||
class PickRolesViewTest(TestCase):
|
||||
def setUp(self):
|
||||
@@ -1630,6 +1661,37 @@ class SigSelectRenderingTest(TestCase):
|
||||
self.assertContains(response, "fyi-prev")
|
||||
self.assertContains(response, "fyi-next")
|
||||
|
||||
def test_overlay_carries_live_countdown_remaining_on_load(self):
|
||||
# Restore-on-load: a gamer landing on his seat view mid-countdown must
|
||||
# get the live flashing numeral, not a static WAIT NVM. countdown_start
|
||||
# is a one-shot WS event, so the seconds-left are seeded server-side
|
||||
# from the timer deadline stored in the cache.
|
||||
from django.core.cache import cache
|
||||
from apps.epic.tasks import _cache_key
|
||||
import time
|
||||
founder = self.gamers[0] # PC → levity
|
||||
seat = self.room.table_seats.get(gamer=founder, role="PC")
|
||||
card = TarotCard.objects.get(
|
||||
deck_variant=self.earthman, arcana="MIDDLE", suit="BRANDS", number=11)
|
||||
SigReservation.objects.create(
|
||||
room=self.room, gamer=founder, card=card,
|
||||
polarity=SigReservation.LEVITY, seat=seat, ready=True)
|
||||
cache.set(
|
||||
_cache_key(str(self.room.id), SigReservation.LEVITY),
|
||||
{"token": "t", "deadline": time.time() + 10}, 60)
|
||||
response = self.client.get(self.url)
|
||||
self.assertTrue(8 <= response.context["countdown_remaining"] <= 10)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'data-countdown-remaining="{response.context["countdown_remaining"]}"')
|
||||
|
||||
def test_overlay_countdown_remaining_zero_when_no_countdown(self):
|
||||
# No live countdown → context None → attribute renders "0" (JS falls
|
||||
# back to WAIT NVM on load).
|
||||
response = self.client.get(self.url)
|
||||
self.assertIsNone(response.context["countdown_remaining"])
|
||||
self.assertContains(response, 'data-countdown-remaining="0"')
|
||||
|
||||
|
||||
class SigSelectUnifiedStageTest(TestCase):
|
||||
"""Sig Select stage unified with the my_sign card-stage apparatus:
|
||||
@@ -3146,9 +3208,13 @@ class RoomGateViewTest(TestCase):
|
||||
self.assertNotContains(response, "id_room_cont_game_btn")
|
||||
|
||||
def test_nvm_returns_to_room_hex(self):
|
||||
# NVM lands the gamer back on the table hex at his acting seat (?seat),
|
||||
# not owned[0] — so a CARTE multi-seat gamer keeps his switched seat.
|
||||
# The owner holds slot 1, so current_slot=1.
|
||||
response = self.client.get(self.url)
|
||||
self.assertContains(
|
||||
response, f'href="{reverse("epic:room", args=[self.room.id])}"')
|
||||
response,
|
||||
f'href="{reverse("epic:room", args=[self.room.id])}?seat=1"')
|
||||
|
||||
def test_page_class_carries_page_room(self):
|
||||
response = self.client.get(self.url)
|
||||
|
||||
@@ -193,6 +193,51 @@ class SchedulePolarityConfirmTest(TestCase):
|
||||
second_timer.cancel()
|
||||
|
||||
|
||||
class CountdownRemainingTest(TestCase):
|
||||
"""The polarity countdown stores a deadline alongside its timer token so a
|
||||
gamer landing on a fresh seat view mid-countdown can restore the live
|
||||
flashing numeral (not just WAIT NVM). `countdown_remaining` derives the
|
||||
seconds left from that deadline."""
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="cd@tasks.io")
|
||||
self.room = Room.objects.create(name="R", owner=self.user)
|
||||
|
||||
def tearDown(self):
|
||||
cancel_polarity_confirm(str(self.room.id), SigReservation.LEVITY)
|
||||
|
||||
def test_schedule_stores_deadline_alongside_token(self):
|
||||
from django.core.cache import cache
|
||||
schedule_polarity_confirm(str(self.room.id), SigReservation.LEVITY, 12)
|
||||
entry = cache.get(_cache_key(str(self.room.id), SigReservation.LEVITY))
|
||||
self.assertIsInstance(entry, dict)
|
||||
self.assertIn("token", entry)
|
||||
self.assertIn("deadline", entry)
|
||||
|
||||
def test_countdown_remaining_returns_seconds_left(self):
|
||||
from apps.epic.tasks import countdown_remaining
|
||||
schedule_polarity_confirm(str(self.room.id), SigReservation.LEVITY, 12)
|
||||
remaining = countdown_remaining(str(self.room.id), SigReservation.LEVITY)
|
||||
# Just-scheduled 12s countdown → ~12s left (allow scheduling slack).
|
||||
self.assertTrue(10 <= remaining <= 12, remaining)
|
||||
|
||||
def test_countdown_remaining_none_when_no_entry(self):
|
||||
from apps.epic.tasks import countdown_remaining
|
||||
self.assertIsNone(
|
||||
countdown_remaining(str(self.room.id), SigReservation.LEVITY))
|
||||
|
||||
def test_countdown_remaining_none_when_deadline_passed(self):
|
||||
from apps.epic.tasks import countdown_remaining
|
||||
from django.core.cache import cache
|
||||
import time
|
||||
cache.set(
|
||||
_cache_key(str(self.room.id), SigReservation.LEVITY),
|
||||
{"token": "t", "deadline": time.time() - 5}, 60,
|
||||
)
|
||||
self.assertIsNone(
|
||||
countdown_remaining(str(self.room.id), SigReservation.LEVITY))
|
||||
|
||||
|
||||
class GroupSendTest(TestCase):
|
||||
@patch("apps.epic.tasks.async_to_sync")
|
||||
def test_group_send_calls_async_to_sync(self, mock_a2s):
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -610,8 +611,15 @@ def _role_select_context(room, user, seat_param=None):
|
||||
str(res.card_id): res.role
|
||||
for res in room.sig_reservations.filter(polarity=polarity_const)
|
||||
}
|
||||
# Seconds left on a live polarity countdown, so a gamer landing on
|
||||
# this seat view mid-countdown restores the flashing numeral instead
|
||||
# of a static WAIT NVM (countdown_start is a one-shot WS broadcast).
|
||||
# None when no countdown is running → template renders 0.
|
||||
from apps.epic.tasks import countdown_remaining
|
||||
ctx["countdown_remaining"] = countdown_remaining(room.id, polarity_const)
|
||||
else:
|
||||
reservations = {}
|
||||
ctx["countdown_remaining"] = None
|
||||
ctx["sig_reservations_json"] = json.dumps(reservations)
|
||||
|
||||
if user_polarity == 'levity':
|
||||
@@ -789,8 +797,16 @@ def room_gate(request, room_id):
|
||||
# partial's slot conditionals read. The renewal modal's own keys override
|
||||
# below.
|
||||
ctx = _gate_context(room, request.user, request.GET.get("seat"))
|
||||
# CONT GAME + the gear NVM both return to the table hex — but they must land
|
||||
# the gamer on his ACTING seat (the ?seat-selected pos-circle), not owned[0].
|
||||
# Without the ?seat a CARTE multi-seat gamer acting at pos 4 got shuttled
|
||||
# back to pos 1. Mirrors the GATE VIEW nav buttons (bedc489).
|
||||
table_url = reverse("epic:room", args=[room.id])
|
||||
if ctx.get("current_slot"):
|
||||
table_url += f"?seat={ctx['current_slot']}"
|
||||
ctx.update({
|
||||
"room": room,
|
||||
"table_url": table_url,
|
||||
"cost_current": user_slot.cost_current if user_slot else True,
|
||||
"deposited_count": room.gate_slots.filter(status=GateSlot.FILLED).count(),
|
||||
"page_class": "page-gameboard page-room page-room-gate",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
describe("SigSelect", () => {
|
||||
let testDiv, stageCard, card, statBlock;
|
||||
|
||||
function makeFixture({ reservations = '{}', polarity = 'levity', userRole = 'PC' } = {}) {
|
||||
function makeFixture({ reservations = '{}', polarity = 'levity', userRole = 'PC',
|
||||
ready = false, countdownRemaining = 0 } = {}) {
|
||||
testDiv = document.createElement("div");
|
||||
testDiv.innerHTML = `
|
||||
<div class="sig-overlay"
|
||||
@@ -10,6 +11,8 @@ describe("SigSelect", () => {
|
||||
data-reserve-url="/epic/room/test/sig-reserve"
|
||||
data-ready-url="/epic/room/test/sig-ready"
|
||||
|
||||
data-ready="${ready}"
|
||||
data-countdown-remaining="${countdownRemaining}"
|
||||
data-reservations="${reservations.replace(/"/g, '"')}">
|
||||
<div class="sig-modal">
|
||||
<div class="sig-stage">
|
||||
@@ -863,6 +866,42 @@ describe("SigSelect", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Countdown restore on load (mid-countdown page load) ────────────────── //
|
||||
//
|
||||
// The 12s polarity countdown's `countdown_start` is a ONE-SHOT WS event: a
|
||||
// gamer (esp. a CARTE multi-seat owner switching to an already-ready seat)
|
||||
// who loads the view after it fired never received it, so the button showed
|
||||
// a static WAIT NVM instead of the live flashing numeral. The view now
|
||||
// seeds data-countdown-remaining from the server-side timer deadline; on
|
||||
// load _replayReservations restores _showCountdown(remaining) when a count
|
||||
// is live, else falls back to WAIT NVM.
|
||||
|
||||
describe("countdown restore on load", () => {
|
||||
let takeSigBtn;
|
||||
|
||||
beforeEach(() => jasmine.clock().install());
|
||||
afterEach(() => jasmine.clock().uninstall());
|
||||
|
||||
it("shows the live numeral (not WAIT NVM) when a countdown is running at load", () => {
|
||||
makeFixture({ reservations: '{"42":"PC"}', ready: true, countdownRemaining: 8 });
|
||||
takeSigBtn = document.getElementById("id_take_sig_btn");
|
||||
expect(takeSigBtn.textContent).toBe("8");
|
||||
});
|
||||
|
||||
it("the restored numeral counts down each second", () => {
|
||||
makeFixture({ reservations: '{"42":"PC"}', ready: true, countdownRemaining: 8 });
|
||||
takeSigBtn = document.getElementById("id_take_sig_btn");
|
||||
jasmine.clock().tick(1000);
|
||||
expect(takeSigBtn.textContent).toBe("7");
|
||||
});
|
||||
|
||||
it("falls back to WAIT NVM when ready but no countdown is live (remaining 0)", () => {
|
||||
makeFixture({ reservations: '{"42":"PC"}', ready: true, countdownRemaining: 0 });
|
||||
takeSigBtn = document.getElementById("id_take_sig_btn");
|
||||
expect(takeSigBtn.textContent).toBe("WAIT NVM");
|
||||
});
|
||||
});
|
||||
|
||||
// ── polarity_room_done → tray sequence ─────────────────────────────────── //
|
||||
//
|
||||
// After all 3 gamers in the user's polarity confirm SAVE SIG and the
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
describe("SigSelect", () => {
|
||||
let testDiv, stageCard, card, statBlock;
|
||||
|
||||
function makeFixture({ reservations = '{}', polarity = 'levity', userRole = 'PC' } = {}) {
|
||||
function makeFixture({ reservations = '{}', polarity = 'levity', userRole = 'PC',
|
||||
ready = false, countdownRemaining = 0 } = {}) {
|
||||
testDiv = document.createElement("div");
|
||||
testDiv.innerHTML = `
|
||||
<div class="sig-overlay"
|
||||
@@ -10,6 +11,8 @@ describe("SigSelect", () => {
|
||||
data-reserve-url="/epic/room/test/sig-reserve"
|
||||
data-ready-url="/epic/room/test/sig-ready"
|
||||
|
||||
data-ready="${ready}"
|
||||
data-countdown-remaining="${countdownRemaining}"
|
||||
data-reservations="${reservations.replace(/"/g, '"')}">
|
||||
<div class="sig-modal">
|
||||
<div class="sig-stage">
|
||||
@@ -863,6 +866,42 @@ describe("SigSelect", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Countdown restore on load (mid-countdown page load) ────────────────── //
|
||||
//
|
||||
// The 12s polarity countdown's `countdown_start` is a ONE-SHOT WS event: a
|
||||
// gamer (esp. a CARTE multi-seat owner switching to an already-ready seat)
|
||||
// who loads the view after it fired never received it, so the button showed
|
||||
// a static WAIT NVM instead of the live flashing numeral. The view now
|
||||
// seeds data-countdown-remaining from the server-side timer deadline; on
|
||||
// load _replayReservations restores _showCountdown(remaining) when a count
|
||||
// is live, else falls back to WAIT NVM.
|
||||
|
||||
describe("countdown restore on load", () => {
|
||||
let takeSigBtn;
|
||||
|
||||
beforeEach(() => jasmine.clock().install());
|
||||
afterEach(() => jasmine.clock().uninstall());
|
||||
|
||||
it("shows the live numeral (not WAIT NVM) when a countdown is running at load", () => {
|
||||
makeFixture({ reservations: '{"42":"PC"}', ready: true, countdownRemaining: 8 });
|
||||
takeSigBtn = document.getElementById("id_take_sig_btn");
|
||||
expect(takeSigBtn.textContent).toBe("8");
|
||||
});
|
||||
|
||||
it("the restored numeral counts down each second", () => {
|
||||
makeFixture({ reservations: '{"42":"PC"}', ready: true, countdownRemaining: 8 });
|
||||
takeSigBtn = document.getElementById("id_take_sig_btn");
|
||||
jasmine.clock().tick(1000);
|
||||
expect(takeSigBtn.textContent).toBe("7");
|
||||
});
|
||||
|
||||
it("falls back to WAIT NVM when ready but no countdown is live (remaining 0)", () => {
|
||||
makeFixture({ reservations: '{"42":"PC"}', ready: true, countdownRemaining: 0 });
|
||||
takeSigBtn = document.getElementById("id_take_sig_btn");
|
||||
expect(takeSigBtn.textContent).toBe("WAIT NVM");
|
||||
});
|
||||
});
|
||||
|
||||
// ── polarity_room_done → tray sequence ─────────────────────────────────── //
|
||||
//
|
||||
// After all 3 gamers in the user's polarity confirm SAVE SIG and the
|
||||
|
||||
@@ -13,6 +13,7 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
|
||||
data-ready-url="{% url 'epic:sig_ready' room.id %}{% if user_seat %}?seat={{ user_seat.slot_number }}{% endif %}"
|
||||
|
||||
data-ready="{{ user_ready|yesno:'true,false' }}"
|
||||
data-countdown-remaining="{{ countdown_remaining|default:0 }}"
|
||||
data-reservations="{{ sig_reservations_json }}">
|
||||
|
||||
<div class="sig-modal">
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<button type="button"
|
||||
id="id_room_cont_game_btn"
|
||||
class="launch-game-btn btn btn-primary"
|
||||
onclick="window.location.href='{% url 'epic:room' room.id %}'">CONT<br>GAME</button>
|
||||
onclick="window.location.href='{{ table_url }}'">CONT<br>GAME</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,9 +90,9 @@
|
||||
{% include "apps/gameboard/_partials/_table_positions.html" with persist_circles=True %}
|
||||
{% include "apps/gameboard/_partials/_position_tooltip.html" %}
|
||||
|
||||
{# NVM nav-backs one step to the table hex (not out to /gameboard/). #}
|
||||
{% url 'epic:room' room.id as nvm_url %}
|
||||
{% include "apps/gameboard/_partials/_room_gear.html" with nvm_url=nvm_url %}
|
||||
{# NVM nav-backs one step to the table hex (not out to /gameboard/), #}
|
||||
{# landing on the acting seat (?seat) like CONT GAME above. #}
|
||||
{% include "apps/gameboard/_partials/_room_gear.html" with nvm_url=table_url %}
|
||||
{% include "apps/gameboard/_partials/_burger.html" %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
Reference in New Issue
Block a user