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

@@ -10,7 +10,10 @@
<div id="id_aperture_fill"></div>
<div class="room-shell">
<div id="id_game_table" class="room-table">
{% if room.table_status == "ROLE_SELECT" %}
{# SCAN SIGS advances the whole table past role-select — gated on #}
{# the viewer's token cost being current (a lapsed gamer gets GATE #}
{# VIEW in the center instead; only their own ROLE pick survives). #}
{% if room.table_status == "ROLE_SELECT" and viewer_cost_current %}
<div id="id_pick_sigs_wrap"{% if starter_roles|length < 6 %} style="display:none"{% endif %}>
<form method="POST" action="{% url 'epic:pick_sigs' room.id %}">
{% csrf_token %}
@@ -22,29 +25,45 @@
<div class="table-hex-border">
<div class="table-hex">
<div class="table-center">
{% if room.table_status == "ROLE_SELECT" %}
{% if card_stack_state %}
<div class="card-stack" data-state="{{ card_stack_state }}"
data-starter-roles="{{ starter_roles|join:',' }}"
data-user-slots="{{ user_slots|join:',' }}"
data-active-slot="{{ active_slot }}"
data-equipped-deck="{{ equipped_deck_id|default:'' }}">
{% if card_stack_state == "ineligible" %}
<i class="fa-solid fa-ban"></i>
{% endif %}
</div>
{% endif %}
{# ROLE card-stack — the gamer's own role pick stays #}
{# available even when their token cost has lapsed #}
{# (deposit-privilege grace, 7d), so it renders OUTSIDE #}
{# the cost gate below. #}
{% if room.table_status == "ROLE_SELECT" and card_stack_state %}
<div class="card-stack" data-state="{{ card_stack_state }}"
data-starter-roles="{{ starter_roles|join:',' }}"
data-user-slots="{{ user_slots|join:',' }}"
data-active-slot="{{ active_slot }}"
data-equipped-deck="{{ equipped_deck_id|default:'' }}">
{% if card_stack_state == "ineligible" %}
<i class="fa-solid fa-ban"></i>
{% endif %}
</div>
{% endif %}
{% if room.table_status == "SKY_SELECT" %}
{% if sky_confirmed %}
<button id="id_pick_sea_btn" class="btn btn-primary">DRAW<br>SEA</button>
{% else %}
<button id="id_pick_sky_btn" class="btn btn-primary">CAST<br>SKY</button>
{% endif %}
{% elif room.table_status == "SIG_SELECT" %}
<button id="id_pick_sky_btn" class="btn btn-primary" style="display:none">CAST<br>SKY</button>
{% if polarity_done %}
<p id="id_hex_waiting_msg">{% if user_polarity == "levity" %}Gravity settling . . .{% else %}Levity appraising . . .{% endif %}</p>
{% if not viewer_cost_current %}
{# Token cost lapsed → GATE VIEW supersedes SCAN SIGS #}
{# / CAST SKY / DRAW SEA / the sig waiting msg. The #}
{# gamer keeps their seat through the renewal grace; #}
{# GATE VIEW routes to the renewal gate-view. Only #}
{# the ROLE pick (above) survives. `<button>` + #}
{# onclick (not `<a>`) — `.btn` doesn't reset serif #}
{# font on anchors. [[feedback-btn-vs-anchor-font- #}
{# family]] #}
<button id="id_room_gate_view_btn" type="button"
class="btn btn-primary"
onclick="window.location.href='{% url 'epic:room_gate' room.id %}'">GATE<br>VIEW</button>
{% else %}
{% if room.table_status == "SKY_SELECT" %}
{% if sky_confirmed %}
<button id="id_pick_sea_btn" class="btn btn-primary">DRAW<br>SEA</button>
{% else %}
<button id="id_pick_sky_btn" class="btn btn-primary">CAST<br>SKY</button>
{% endif %}
{% elif room.table_status == "SIG_SELECT" %}
<button id="id_pick_sky_btn" class="btn btn-primary" style="display:none">CAST<br>SKY</button>
{% if polarity_done %}
<p id="id_hex_waiting_msg">{% if user_polarity == "levity" %}Gravity settling . . .{% else %}Levity appraising . . .{% endif %}</p>
{% endif %}
{% endif %}
{% endif %}
</div>
@@ -67,23 +86,26 @@
</div>
</div>
{# Phase overlays are gated on `viewer_cost_current` too: a lapsed gamer #}
{# gets GATE VIEW in the center, so the SIG/SKY/SEA modals (which embed #}
{# their trigger-btn ids in JS) must not render alongside it. #}
{# Sig Select overlay — suppressed once this gamer's polarity sigs are assigned #}
{% if room.table_status == "SIG_SELECT" and user_polarity and not polarity_done %}
{% if room.table_status == "SIG_SELECT" and user_polarity and not polarity_done and viewer_cost_current %}
{% include "apps/gameboard/_partials/_sig_select_overlay.html" %}
{% endif %}
{# Sky (Pick Sky) overlay — natal chart entry #}
{% if room.table_status == "SKY_SELECT" and not sky_confirmed %}
{% if room.table_status == "SKY_SELECT" and not sky_confirmed and viewer_cost_current %}
{% include "apps/gameboard/_partials/_sky_overlay.html" %}
{% endif %}
{# Sky tooltip: sibling of .sky-overlay, not inside .sky-modal-wrap (which has transform) #}
{% if room.table_status == "SKY_SELECT" and not sky_confirmed %}
{% if room.table_status == "SKY_SELECT" and not sky_confirmed and viewer_cost_current %}
<div id="id_sky_tooltip" class="tt" style="display:none;"></div>
<div id="id_sky_tooltip_2" class="tt" style="display:none;"></div>
{% endif %}
{# Sea (Pick Sea) overlay — Celtic Cross spread entry #}
{% if room.table_status == "SKY_SELECT" and sky_confirmed %}
{% if room.table_status == "SKY_SELECT" and sky_confirmed and viewer_cost_current %}
{% include "apps/gameboard/_partials/_sea_overlay.html" %}
{% endif %}