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>
233 lines
16 KiB
HTML
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 %}
|