room center: GATE VIEW supersedes SCAN SIGS / CAST SKY / DRAW SEA / sig overlay when token cost lapses — ROLE pick survives grace — TDD

Phase 3 of the room GATE VIEW + seat-renewal sprint. When the viewer's
own FILLED gate-slot cost has lapsed (filled_at past the cost-current
window), the center hex shows a GATE VIEW button (→ room gate-view)
instead of the phase affordances, so they must renew before advancing.

- _role_select_context: adds viewer_cost_current / viewer_in_grace from
  the viewer's FILLED slot (no slot → current, defensive)
- room.html: the ROLE card-stack renders OUTSIDE the cost gate (the
  gamer's own role pick survives the renewal grace — deposit privilege);
  GATE VIEW supersedes the rest of .table-center; #id_pick_sigs_wrap
  (SCAN SIGS, advancing the whole table) is gated on viewer_cost_current;
  the SIG/SKY/SEA overlays are gated too (they embed their trigger-btn
  ids in JS, so they must not render alongside GATE VIEW)
- per user-spec: only the ROLE pick stays in grace; SCAN SIGS + every
  later phase get GATE VIEW

Tests: RoomCenterSupersessionTest (9) — GATE VIEW supersedes sig overlay
/ CAST SKY / DRAW SEA / SCAN SIGS when lapsed, normal buttons when
current; RoomRoleStackGraceTest (1) — card-stack (eligible) kept
alongside GATE VIEW when lapsed. 838 epic+gameboard ITs green.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-31 23:29:43 -04:00
parent e78ba730e3
commit 4b3dc91e7f
3 changed files with 172 additions and 27 deletions

View File

@@ -345,6 +345,19 @@ def _role_select_context(room, user):
"gate_positions": _gate_positions(room),
"slots": room.gate_slots.order_by("slot_number"),
}
# Viewer's seat token-cost state — drives the center-hex GATE VIEW
# supersession (room.html). When the viewer's FILLED slot's cost has
# lapsed (filled_at past the cost-current window), GATE VIEW replaces
# SCAN SIGS / CAST SKY / DRAW SEA / the sig overlay; the gamer's own
# ROLE card-stack pick survives the renewal grace. No filled slot →
# treated as current (defensive — non-seated viewers see the normal UI).
viewer_slot = (
room.gate_slots.filter(gamer=user, status=GateSlot.FILLED).first()
if user.is_authenticated else None
)
ctx["viewer_cost_current"] = viewer_slot.cost_current if viewer_slot else True
ctx["viewer_in_grace"] = viewer_slot.in_renewal_grace if viewer_slot else False
# Tray cell 2: sig card (set once polarity group confirms)
_canonical_seat = _canonical_user_seat(room, user) if user.is_authenticated else None
ctx["my_tray_sig"] = _canonical_seat.significator if _canonical_seat else None