Files
python-tdd/src/templates/apps/gameboard/room.html
Disco DeDisco 71c00699a1 room Sig Select: unify with the my_sign card-stage apparatus (DRY stat-block, per-card stage image, --duoUser felt)
The in-room SIG_SELECT stage diverged from the polished GAME SIGN page:
a fixed dark-Gaussian modal over the hex, a stale label-only stat-block,
and no card imagery. This brings it in line with my_sign / my_sea.

A — Stat-block DRY: _sig_select_overlay.html now renders the shared
core/_partials/_stat_face.html (rank-chip + title + arcana + keywords)
instead of a reduced label-only copy; sig-select.js's updateStage() now
calls StageCard.populateStatExtras (the missing call that left those
fields blank). data-arcana-key added per card for title color-keying.

B — Per-card stage image: the stage card gains a .sig-stage-card-img
slot + data-image-url per thumbnail, so an image-equipped seat deck
(RWS / Minchiate) shows real card art on the preview. Thumbnails stay
glyph-only (rank + suit) at every deck — only the stage shows the image.
Keyed off each card's OWN deck_variant, so it auto-upgrades to mixed art
when the dubbodeck assembly lands. No backend change (cards already
carry a deck_variant via _room_deck_variant).

C — Felt-in-aperture: the stage renders INSIDE .room-hex-pane on edge-to-
edge green --duoUser felt (my_sea-style), replacing the hex content; the
old .sig-backdrop blur is gone. .sig-overlay absolute-fills the pane
(.room-hex-pane.has-sig-stage = positioning context); dismissing it
reveals the hex + waiting message behind. Scroll-down still reaches the
reelhouse carousel (untouched scroll pane).

Polishes:
- Image-mode bg escape: the levity 0,3,0 polarity rule
  (.sig-overlay/.my-sign-page[data-polarity="levity"] .sig-stage-card)
  hard-set a --secUser background that re-clothed image cards behind the
  transparent PNG. Added the &.sig-stage-card--image { background:
  transparent; border:0; overflow:visible } escape (parity w. the base +
  my-sea rules). Latent my_sign bug too. Monodeck-era assumption.
- FLIP .btn-reveal: non-polarized image decks get a FLIP that turns the
  preview to the deck card-back (my_sign parity) — back-img + reused
  .my-sign-flip-btn (shared positioning/hide/counter-position rules
  already cover .sig-stage-card) + a frozen-gated reveal scoped to
  .sig-overlay + sig-select.js _flipToBack (500ms Y-rotate, midpoint
  swap). SPIN now sets data-spinning so the btn hides mid-rotate.
- Reserved thumbs-up / hover cursors portal to a body-root fixed
  container, so they hung over the reelhouse on scroll. sig-select.js now
  toggles .cursors-hidden off the aperture scrollTop: instant hide the
  moment the scroll leaves the hex, 0.5s opacity ease-in on the full
  return. Tray intentionally kept.

TDD: SigSelectUnifiedStageTest (6 ITs) — DRY stat-face present, per-card
data-image-url + data-arcana-key, .sig-stage-card-img slot, image deck
non-empty face URL / text deck empty, has-sig-stage felt + overlay inside
the hex pane. 319 epic test_views ITs green; user-verified live on an RWS
room (no rect, FLIP works, thumb timing). Jasmine for the JS wiring +
the dubbodeck cross-deck assembly (per-seat segment cards, CARTE-solo
both-polarity case, per-card backs) are the tracked follow-on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 02:37:13 -04:00

233 lines
16 KiB
HTML

{% extends "core/base.html" %}
{% load static tooltip_tags %}
{% block title_text %}Gameboard{% endblock title_text %}
{# Two-word reel: GAME ROOM rests in view; GAME SCROLL is parked just below #}
{# in the title slot's bottom fade. room-scroll.js toggles `.is-scroll` on #}
{# the <h2> when the table-hex aperture snaps to the provenance feed, and #}
{# the CSS reel slides ROOM up & out / SCROLL up & in (reverses on scroll- #}
{# up). The window span carries `data-letters-split` so base.html's letter- #}
{# splitter skips it and splits the two inner `.gr-word`s instead. Only in #}
{# the table phase (a scroll exists to reveal); the gate phase stays plain. #}
{% comment %}
Title reel — two axes on NESTED elements so they never blend. ROOM
(`.gr-word--base`) + the outer `.gr-views-reel` ride the VERTICAL reel (hex ⇄
views, driven by `.is-scroll`). The inner `.gr-views-track` (the five view
words in VIEW_ORDER) rides the HORIZONTAL axis, keyed by `data-active-view`
(set by room-views.js) ALONE — so the active view's cell sits in the slot at
all times, including at the hex. Result: hex⇄views is a pure vertical reel
landing on wherever you left off; lateral nav is a pure horizontal slide
(old word out one side, new in from the other). base.html splits `.gr-word`.
{% endcomment %}{% block header_text %}<span>Game</span>{% if room.table_status %}<span class="gr-swap" data-letters-split="1"><span class="gr-word gr-word--base">room</span><span class="gr-views-reel"><span class="gr-views-track"><span class="gr-word gr-word--atlas">atlas</span><span class="gr-word gr-word--scroll">scroll</span><span class="gr-word gr-word--yarn">yarn</span><span class="gr-word gr-word--post">post</span><span class="gr-word gr-word--pulse">pulse</span></span></span></span>{% else %}<span>room</span>{% endif %}{% endblock header_text %}
{% block content %}
<div class="room-page" data-room-id="{{ room.id }}"
{% if room.table_status %}data-select-role-url="{% url 'epic:select_role' room.id %}"{% endif %}>
<div id="id_aperture_fill"></div>
{# Table-hex aperture — a binary scroll-snap viewport (mirrors my_sky's #}
{# wheel<->form swap): the hex pane is the default view; from Role #}
{# Select onwards a 2nd pane holds the room's provenance feed, revealed #}
{# by scrolling DOWN. `is-scrollable` engages scroll-snap (2 panes), so #}
{# the hex content is replaced entirely by the Scroll (no partial scroll #}
{# — `scroll-snap-stop: always`). DRY seam: my_sea reuses _room_scroll. #}
<div id="id_room_aperture" class="room-aperture{% if room.table_status %} is-scrollable{% endif %}">
{# `has-sig-stage` (active sig pick) makes the pane a positioning context #}
{# so the green-felt _sig_select_overlay fills it (my_sea-style), covering #}
{# the hex/seats behind. Dismissing the overlay (this gamer's sigs done) #}
{# reveals the hex + waiting message underneath. #}
<div class="room-pane room-hex-pane{% if room.table_status == "SIG_SELECT" and user_polarity and not polarity_done and viewer_cost_current %} has-sig-stage{% endif %}">
<div class="room-shell">
<div id="id_game_table" class="room-table">
{# 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 %}
<button id="id_pick_sigs_btn" type="submit" class="btn btn-primary">SCAN<br>SIGS</button>
</form>
</div>
{% endif %}
<div class="room-table-scene">
<div class="table-hex-border">
<div class="table-hex">
<div class="table-center">
{# 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 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>
</div>
</div>
{# Seats — fa-chair layout persists from ROLE_SELECT through SIG_SELECT #}
{% for pos in gate_positions %}
<div class="table-seat{% if pos.role_label in starter_roles %} role-confirmed{% endif %}"
data-slot="{{ pos.slot.slot_number }}" data-role="{{ pos.role_label }}">
<i class="fa-solid fa-chair"></i>
<span class="seat-role-label">{{ pos.role_label }}</span>
{% if pos.role_label in starter_roles %}
<i class="position-status-icon fa-solid fa-circle-check"></i>
{% else %}
<i class="position-status-icon fa-solid fa-ban"></i>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>{# /.room-shell #}
{# Sig Select stage — green-felt card stage + thumbnail grid, rendered #}
{# INSIDE the hex pane (absolute-fills it on --duoUser felt, my_sea- #}
{# style) for this gamer's polarity group. Suppressed once their polarity #}
{# sigs are assigned (then the hex/waiting-msg shows through). #}
{% 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 %}
{# Position circles scroll away WITH the hex (they live inside the hex #}
{# pane, not at room-page root). Neither the aperture nor the pane sets #}
{# z-index/transform, so the strip's z-130 still resolves in the root #}
{# stacking context — above the gate/sig overlays — as before. #}
{% if room.gate_status != "RENEWAL_DUE" and room.table_status != "SIG_SELECT" %}
{% include "apps/gameboard/_partials/_table_positions.html" %}
{% endif %}
</div>{# /.room-hex-pane #}
{% if room.table_status %}
<div class="room-pane room-scroll-pane">
{# Horizontal game-views carousel (ATLAS/SCROLL/POST/CHAT/PULSE), #}
{# landing on SCROLL — which still wraps _room_scroll.html, so the #}
{# binary vertical snap + the provenance feed behave as before. #}
{% include "apps/gameboard/_partials/_room_views.html" %}
</div>
{% endif %}
</div>{# /.room-aperture #}
{# Game-views icon strip — root-level (outside the aperture) so it #}
{# clears the scroll card's fade mask + the aperture's scroll clip. #}
{% if room.table_status %}
{% include "apps/gameboard/_partials/_room_views_strip.html" %}
{% endif %}
{# Phase overlays are gated on `viewer_cost_current` too: a lapsed gamer #}
{# gets GATE VIEW in the center, so the SKY/SEA modals (which embed #}
{# their trigger-btn ids in JS) must not render alongside it. (The Sig #}
{# Select stage now lives INSIDE the hex pane above — my_sea-style felt.) #}
{# Sky (Pick Sky) overlay — natal chart entry #}
{% 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 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 and viewer_cost_current %}
{% include "apps/gameboard/_partials/_sea_overlay.html" %}
{% endif %}
{# Gamer-needed stub — a seat lapsed past its renewal grace and was #}
{# auto-BYE'd, so the table no longer fills all six. Minimal stub #}
{# until the mid-game re-seat flow lands (_table_positions + #}
{# _gatekeeper are already suppressed for RENEWAL_DUE below). #}
{% if room.gate_status == "RENEWAL_DUE" %}
<div id="id_gamer_needed" class="hex-stub">
<p>A seat opened — awaiting a gamer.</p>
</div>
{% endif %}
{% if not room.table_status and room.gate_status != "RENEWAL_DUE" %}
{% include "apps/gameboard/_partials/_gatekeeper.html" %}
{# Owner-only invite affordance: handshake btn at the upper-right #}
{# of the right sidebar w. slide-out + autocomplete. Replaces the #}
{# legacy inline `<form action="invite_gamer">` panel. #}
{% if request.user == room.owner %}
{% include "apps/billboard/_partials/_bud_invite_panel.html" %}
{% endif %}
{% endif %}
{% if room.table_status %}
<div id="id_tray_wrap"{% if room.table_status == "ROLE_SELECT" and starter_roles|length < 6 %} class="role-select-phase"{% endif %}>
<div id="id_tray_handle">
<div id="id_tray_grip"></div>
<button id="id_tray_btn" aria-label="Open seat tray">
<i class="fa-solid fa-dice-d20"></i>
</button>
</div>
<div id="id_tray" style="display:none"><div id="id_tray_grid" data-role-icons-url="{% static 'apps/epic/icons/cards-roles/' %}">{% if my_tray_role %}<div class="tray-cell tray-role-card" data-role="{{ my_tray_role }}"><img src="{% static my_tray_scrawl_static_path %}" alt="{{ my_tray_role }}">{% tooltip my_tray_role_tooltip %}</div>{% else %}<div class="tray-cell"></div>{% endif %}{% if my_tray_sig %}<div class="tray-cell tray-sig-card"><div class="sig-stage-card sea-sig-card" aria-label="{{ my_tray_sig.name }}" data-energies="{{ my_tray_sig.energies_json }}" data-operations="{{ my_tray_sig.operations_json }}"><span class="fan-corner-rank">{{ my_tray_sig.corner_rank }}</span>{% if my_tray_sig.suit_icon %}<i class="fa-solid {{ my_tray_sig.suit_icon }}"></i>{% endif %}</div></div>{% else %}<div class="tray-cell"></div>{% endif %}{% for i in "345678" %}<div class="tray-cell"></div>{% endfor %}</div></div>
</div>
{% endif %}
{% comment %}
Tray-tooltip portal — sibling of the tray so it sits at room-page root,
not inside the tray's overflow:hidden / mask-image clip. JS populates
innerHTML on hover of .tray-role-card > img (and Phase 2: sig card).
{% endcomment %}
{% if room.table_status %}
<div id="id_tooltip_portal" class="tt" style="display:none;"></div>
{% endif %}
{# Position-circle tooltip portal — rendered whenever the circles can #}
{# (gatekeeper + role-select; the SIG_SELECT phase hides the strip). #}
{% include "apps/gameboard/_partials/_position_tooltip.html" %}
{# scroll_filter (table phase only) adds the Frame/Redact gear pane that #}
{# room-scroll.js swaps in when the aperture is scrolled to the feed. #}
{% include "apps/gameboard/_partials/_room_gear.html" with scroll_filter=room.table_status %}
{% include "apps/gameboard/_partials/_burger.html" %}
</div>
{% endblock content %}
{% block scripts %}
{# Brief module — needed by _bud_invite_panel's OK handler so the #}
{# slide-down banner shows up on a successful gatekeeper invite. #}
<script src="{% static 'apps/dashboard/note.js' %}"></script>
<script src="{% static 'apps/epic/room.js' %}"></script>
<script src="{% static 'apps/epic/gatekeeper.js' %}"></script>
<script src="{% static 'apps/epic/role-select.js' %}"></script>
<script src="{% static 'apps/epic/stage-card.js' %}"></script>
<script src="{% static 'apps/epic/combobox.js' %}"></script>
<script src="{% static 'apps/epic/sig-select.js' %}"></script>
<script src="{% static 'apps/epic/sea.js' %}"></script>
<script src="{% static 'apps/epic/tray.js' %}"></script>
<script src="{% static 'apps/epic/tray-tooltip.js' %}"></script>
<script src="{% static 'apps/epic/position-tooltip.js' %}"></script>
<script src="{% static 'apps/epic/burger-btn.js' %}"></script>
<script src="{% static 'apps/epic/room-scroll.js' %}"></script>
<script src="{% static 'apps/epic/room-views.js' %}"></script>
{% endblock scripts %}