The room scroll pane now matches scroll.html: the feed sits in a .applet-scroll %applet-box card with the rotated room-name title; dropped the special --duoUser pane bg (the dark card sits on the room-page bg). Gear menu is now view-aware. #id_room_menu carries two panes: .room-menu-default (the existing NVM/DEL/BYE) + .room-menu-scroll (a Frame/Redact #id_scroll_filter_form, rendered only when the gear include gets scroll_filter — room.html passes scroll_filter=room.table_status). room-scroll.js (NEW) runs an IntersectionObserver on .room-scroll-pane (root=#id_room_aperture): scrolled to the feed -> show the filter pane; back on the hex -> show the default. The filter mirrors scroll.html (per-room localStorage, toggles .drama-event[data-label] display). Buffer-dots animation moved from the inline partial script into room-scroll.js. Other views keep their own menus, as asked: GATE VIEW (room_gate.html) includes _room_gear.html with nvm_url only (no scroll_filter, no room-scroll.js) -> NVM(->hex)/DEL/BYE; the cross/spread phase is a modal over the hex (scrollTop 0) -> default pane. Traps: applets.js caches gear.dataset.menuTarget at bind time, so you can't swap a gear's target to a 2nd menu — both panes live in ONE #id_room_menu and JS toggles visibility. .room-menu-default is display:contents so wrapping the existing controls doesn't change their layout (JS toggles none<->contents, not ''). Tests: +3 ITs (RoomScrollOfEventsTest — .applet-scroll card + room-name title, filter pane renders in table phase, filter absent in gate phase); +2 FTs (test_game_room_scroll — gear swaps to filter when scrolled to feed, unchecking Redact+OK hides struck rows). 8 scroll ITs + 4 scroll FTs green; 554 epic ITs/UTs green; gatekeeper DEL+BYE gear FTs green (the .room-menu-default wrap is layout-neutral). [[project-room-scroll-of-events]] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
199 lines
13 KiB
HTML
199 lines
13 KiB
HTML
{% extends "core/base.html" %}
|
|
{% load static tooltip_tags %}
|
|
|
|
{% block title_text %}Gameboard{% endblock title_text %}
|
|
{% block header_text %}<span>Game</span><span>room</span>{% 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 %}">
|
|
<div class="room-pane room-hex-pane">
|
|
<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 #}
|
|
{# 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">
|
|
{% include "apps/gameboard/_partials/_room_scroll.html" %}
|
|
</div>
|
|
{% endif %}
|
|
</div>{# /.room-aperture #}
|
|
|
|
{# 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 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 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>
|
|
{% endblock scripts %}
|