Files
python-tdd/src/templates/apps/gameboard/_partials/_sea_overlay.html
Disco DeDisco cde556b178
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
SEED MAP felt: 2D d3-delaunay Voronoi/Delaunay dual graph (roadmap step 21, Step 1) — TDD
New inline --duoUser felt (sibling of CAST SKY / DRAW SEA) that opens on
#id_seed_map_btn once the 6-card sea hand completes (hand_complete); paints a
Voronoi cell layer (territory) + a Delaunay edge layer (adjacency) from
PLACEHOLDER seeds. Card-driven seeding (the 6 Celtic-Cross cards) is Step 2.

- voronoi-map.js: window.SeedMap.draw/drawPlaceholder/clear over the bundled
  d3.min.js (d3-delaunay ships in the v7.9.0 UMD bundle — no new dep); a
  ResizeObserver re-tessellates to fill the felt on resize; data-seed reads d3's
  ring.index (survives skipped/degenerate cells in Step 2)
- _seed_map_overlay.html felt + room.html include + has-seed-stage (gated on hand_complete)
- three-way felt close (T3): openSeed closes sky+sea; openSky/openSea reciprocally close seed
- .room-menu-seed gear NVM pane + room-views.js seed-open branch
- _sky.scss felt block (T1: no aperture-fill light; T2: chained selector; fills the pane) + _room.scss aperture pin
- VoronoiMapSpec Jasmine + game_room_seed_map FT + 5 felt ITs; CarteTray NVM count 2->3

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

608 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% load static %}
{% comment %}
DRAW SEA — Sea Select felt (2026-06-07), mirroring Sky Select's form→wheel
scroll-snap. The felt starts with the spread OPTIONS (.sea-options-col, page 1):
the .sea-select combobox (the two Celtic Cross spreads only) + a mini shape
preview + OK + AUTO DRAW + DEL. Clicking OK confirms the spread → the options
shunt DOWN and the spread CROSS (.sea-cross-col, w. the Gravity/Levity deck
stacks) takes page 1 via scroll-snap (scroll down to find the options again).
NO modal, NO corner NVM. The burger #id_sea_btn is the post-completion REOPEN
affordance (active only once the spread is complete, like the sky btn) — NOT
active during drawing. Each draw persists onto the seat's Character.celtic_cross
via epic:sea_save. `?seat` threads the CARTE-selected seat onto the action URLs.
{% endcomment %}
<div class="sea-page sea-page--room{% if saved_by_position %} sea-spread-chosen{% endif %}" id="id_sea_page">
<div id="id_sea_overlay" class="sea-overlay-content"
data-sea-deck-url="{% url 'epic:sea_deck' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}"
data-sea-save-url="{% url 'epic:sea_save' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}"
data-sea-delete-url="{% url 'epic:sea_delete' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}"
data-sea-user-polarity="{{ user_polarity }}">
{# ── OPTIONS section — page 1 initially; shunts to page 2 on OK. The select #}
{# form + preview + the OK / AUTO DRAW / DEL actions. NO deck stacks here. #}
<div class="sea-options-col">
<div class="sea-form-col">
<div class="sea-form-main">
<div class="sea-field">
<label for="id_sea_spread" id="id_sea_spread_label">Spread</label>
<p class="sea-reversal-hint">{{ stack_reversal_pct|default:25 }}% reversals</p>
{# Two Celtic Cross 6-card spreads only (user-spec 2026-06-07). #}
<input type="hidden" id="id_sea_spread" name="spread" autocomplete="off"
value="{{ sea_default_spread }}">
{# Combobox + OK on one row — OK confirms the spread → shunts. #}
{# OK gains .btn-disabled + × the moment the first card is drawn #}
{# (the spread is then locked, the select disables, DEL enables). #}
<div class="sea-select-row">
<div class="sea-select"
data-combobox
data-combobox-target="id_sea_spread"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="id_sea_spread_label"
tabindex="0">
<span class="sea-select-current">{% if sea_default_spread == "escape-velocity" %}Celtic Cross,<br>Escape Velocity{% else %}Celtic Cross,<br>Rider-Waite-Smith{% endif %}</span>
<span class="sea-select-arrow" aria-hidden="true"></span>
<ul class="sea-select-list" role="listbox">
{% if user_polarity == "levity" %}
<li role="option" data-value="waite-smith" aria-selected="{% if sea_default_spread == 'escape-velocity' %}false{% else %}true{% endif %}">Celtic Cross,<br>Rider-Waite-Smith</li>
<li role="option" data-value="escape-velocity" aria-selected="{% if sea_default_spread == 'escape-velocity' %}true{% else %}false{% endif %}">Celtic Cross,<br>Escape Velocity</li>
{% else %}
<li role="option" data-value="escape-velocity" aria-selected="{% if sea_default_spread == 'waite-smith' %}false{% else %}true{% endif %}">Celtic Cross,<br>Escape Velocity</li>
<li role="option" data-value="waite-smith" aria-selected="{% if sea_default_spread == 'waite-smith' %}true{% else %}false{% endif %}">Celtic Cross,<br>Rider-Waite-Smith</li>
{% endif %}
</ul>
</div>
<button type="button" id="id_sea_confirm_spread" class="btn btn-confirm{% if saved_by_position %} btn-disabled{% endif %}">{% if saved_by_position %}&times;{% else %}OK{% endif %}</button>
</div>
</div>
{# Miniaturized spread preview — shape only, never dealt to. Shows #}
{# the chosen spread's shape before OK reveals the real cross. #}
{% include "apps/gameboard/_partials/_sea_spread_preview.html" with preview_spread=sea_default_spread sig=my_tray_sig %}
</div>
<div class="sea-form-actions">
{# AUTO DRAW commits the remaining hand in one POST + animates onto #}
{# the cross (confirms the spread first if not yet). DEL clears. #}
<button type="button"
id="id_sea_action_btn"
class="btn btn-primary{% if hand_complete %} btn-disabled{% endif %}"
data-state="{% if hand_complete %}complete{% else %}auto-draw{% endif %}">AUTO<br>DRAW</button>
<button type="button"
id="id_sea_del"
class="btn btn-danger{% if not saved_by_position %} btn-disabled{% endif %}">{% if saved_by_position %}DEL{% else %}&times;{% endif %}</button>
</div>
</div>
</div>
{# ── CROSS section — hidden until OK; then order:-1 takes page 1. The REAL #}
{# deal target (.my-sea-cross) + the Gravity/Levity deck stacks (FLIP). #}
<div class="sea-cross-col">
<div class="sea-cards-col">
<div class="sea-cross my-sea-cross" data-spread="{{ sea_default_spread }}">
<div class="sea-crucifix-cell sea-pos-crown">
<span class="sea-pos-label" data-position="crown"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="crown" saved=saved_by_position.crown crossing=False %}
</div>
<div class="sea-crucifix-cell sea-pos-leave">
<span class="sea-pos-label" data-position="leave"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="leave" saved=saved_by_position.leave crossing=False %}
</div>
<div class="sea-crucifix-cell sea-pos-core">
{# Center significator — supply the card-FACE image when the sig's #}
{# deck has one (mirrors my_sea); else the corner-rank + suit-icon #}
{# text thumbnail. my_tray_sig.deck_variant is the card's OWN deck #}
{# (Sig Select picks from the Role Select contributed deck). #}
<div class="sig-stage-card sea-sig-card{% if my_tray_sig.deck_variant.has_card_images %} sig-stage-card--image{% endif %}" style="--sig-card-w: 4rem"{% if my_tray_sig %} data-card-id="{{ my_tray_sig.id }}" data-arcana-key="{{ my_tray_sig.arcana }}"{% endif %}>
{% if my_tray_sig %}
{% if my_tray_sig.deck_variant.has_card_images %}
<img class="sig-stage-card-img" src="{{ my_tray_sig.image_url }}" alt="{{ my_tray_sig.name }}">
{% else %}
<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 %}
{% endif %}
{% endif %}
</div>
<div class="sea-pos-cover">
<span class="sea-pos-label" data-position="cover"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="cover" saved=saved_by_position.cover crossing=False %}
</div>
<div class="sea-pos-cross">
<span class="sea-pos-label" data-position="cross"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="cross" saved=saved_by_position.cross crossing=True %}
</div>
</div>
<div class="sea-crucifix-cell sea-pos-loom">
<span class="sea-pos-label" data-position="loom"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="loom" saved=saved_by_position.loom crossing=False %}
</div>
<div class="sea-crucifix-cell sea-pos-lay">
<span class="sea-pos-label" data-position="lay"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="lay" saved=saved_by_position.lay crossing=False %}
</div>
</div>
</div>
{# ALWAYS two stacks (Gravity + Levity) — the gamer draws from EITHER #}
{# populated half (sea_deck returns both), even a CARTE monodeck. #}
<div class="my-sea-stacks-wrap">
<div class="sea-stacks">
<span class="sea-stacks-label">DECKS</span>
<div class="sea-deck-stack sea-deck-stack--gravity">
<div class="sea-stack-face">
<button class="btn btn-reveal sea-stack-flip{% if hand_complete %} btn-disabled{% endif %}" type="button">{% if hand_complete %}&times;{% else %}FLIP{% endif %}</button>
</div>
<span class="sea-stack-name">Gravity</span>
</div>
<div class="sea-deck-stack sea-deck-stack--levity">
<div class="sea-stack-face">
<button class="btn btn-reveal sea-stack-flip{% if hand_complete %} btn-disabled{% endif %}" type="button">{% if hand_complete %}&times;{% else %}FLIP{% endif %}</button>
</div>
<span class="sea-stack-name">Levity</span>
</div>
</div>
</div>
</div>
{# Sea stage — portaled big-card viewer (position:fixed, escapes the snap). #}
{# `sea_stage_dubbo` flags the gameroom's cloned two-toned dubbodeck so the #}
{# FLIP-back polarity tint applies HERE only (my_sea/visit draw a monodeck #}
{# that is never cloned — no tint there). See _card-deck.scss. #}
{% include "apps/gameboard/_partials/_sea_stage.html" with sea_stage_dubbo=True %}
</div>
</div>
<script>
(function () {
'use strict';
var page = document.getElementById('id_sea_page');
var overlay = document.getElementById('id_sea_overlay');
if (!page || !overlay) return;
// ── Per-spread draw order + labels — the two Celtic Cross variants only.
var DRAW_ORDER = {
'waite-smith': ['cover', 'cross', 'crown', 'lay', 'loom', 'leave'],
'escape-velocity': ['cover', 'cross', 'lay', 'leave', 'crown', 'loom'],
};
var POSITION_LABELS = {
'waite-smith': { crown: 'Crown', leave: 'Behind', cover: 'Cover', cross: 'Cross', loom: 'Before', lay: 'Beneath' },
'escape-velocity': { crown: 'Crown', leave: 'Leave', cover: 'Cover', cross: 'Cross', loom: 'Loom', lay: 'Lay' },
};
var hidden = overlay.querySelector('#id_sea_spread');
var cross = overlay.querySelector('.my-sea-cross');
var preview = overlay.querySelector('.sea-cross--preview');
var actionBtn = overlay.querySelector('#id_sea_action_btn');
var delBtn = overlay.querySelector('#id_sea_del');
var okBtn = overlay.querySelector('#id_sea_confirm_spread');
var seaSelect = overlay.querySelector('[data-combobox][data-combobox-target="id_sea_spread"]');
if (!hidden || !cross) return;
var SEA_DECK_URL = overlay.dataset.seaDeckUrl;
var SEA_SAVE_URL = overlay.dataset.seaSaveUrl;
var SEA_DELETE_URL = overlay.dataset.seaDeleteUrl;
var _levityPile = [], _gravityPile = [];
var _filled = 0;
var _activeStack = null;
var _locked = false;
function _csrf() {
var m = document.cookie.match(/(?:^|; )csrftoken=([^;]+)/);
return m ? decodeURIComponent(m[1]) : '';
}
function _currentOrder() { return DRAW_ORDER[hidden.value] || DRAW_ORDER['waite-smith']; }
function _fetchDeck() {
return fetch(SEA_DECK_URL, { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (data) { _levityPile = data.levity || []; _gravityPile = data.gravity || []; })
.catch(function () {});
}
function _hideOk() {
if (_activeStack) { _activeStack.classList.remove('sea-deck-stack--active'); _activeStack = null; }
}
function _showOk(stack) { _hideOk(); _activeStack = stack; stack.classList.add('sea-deck-stack--active'); }
function _lockSpread() {
if (seaSelect) { seaSelect.classList.add('sea-select--locked'); seaSelect.setAttribute('aria-disabled', 'true'); }
}
// ── Confirm the chosen spread → shunt the options DOWN + reveal the cross.
// Mirrors Sky Select's form→wheel: the felt page gets `sea-spread-chosen` → the
// SCSS engages the scroll-snap (the cross takes page 1 via order:-1, the options
// shunt to page 2); lock the combobox; ease the scroller up to the cross.
// Idempotent. Triggered by OK and (defensively) by AUTO DRAW.
var _spreadChosen = page.classList.contains('sea-spread-chosen');
function _chooseSpread(slideIn) {
if (!_spreadChosen) {
_spreadChosen = true;
page.classList.add('sea-spread-chosen');
slideIn = true; // the first reveal always slides in from below
}
// NB: the combobox is NOT locked here — the spread stays changeable (scroll
// up) until the FIRST card is drawn, when _lockSpread() + _setHasDrawn(true)
// disable the select + OK together (user-spec 2026-06-07).
// `slideIn` (OK reveal) pins to the options (page 2) first so the cross
// slides in from above. AUTO DRAW fired from a scrolled-down options page
// skips the pin + just eases UP to the cross (so the user watches the draw).
if (slideIn) {
var opts = overlay.querySelector('.sea-options-col');
if (opts) page.scrollTop = opts.offsetTop;
}
_scrollToCross();
}
function _scrollToCross() {
// Ease the scroller UP to the cross (page 1, scrollTop 0) from wherever it is.
if (page.scrollTop === 0) return;
var start = page.scrollTop, t0 = null, DUR = 320;
function step(now) {
if (t0 === null) t0 = now;
var t = Math.min(1, (now - t0) / DUR);
var u = 1 - t, e = 1 - u * u * u;
page.scrollTop = Math.max(0, start * (1 - e));
if (t < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
if (okBtn) okBtn.addEventListener('click', function () {
if (okBtn.classList.contains('btn-disabled')) return;
_chooseSpread(true);
});
// First card drawn → DEL un-disables ("DEL") + OK disables ("×"), simultaneous
// with the combobox locking (`_lockSpread`). The two btns are inverse states.
function _setHasDrawn(on) {
if (delBtn) { delBtn.classList.toggle('btn-disabled', !on); delBtn.innerHTML = on ? 'DEL' : '×'; }
if (okBtn) { okBtn.classList.toggle('btn-disabled', on); okBtn.innerHTML = on ? '×' : 'OK'; }
}
function _setComplete(on, live) {
_locked = on;
overlay.querySelectorAll('.sea-deck-stack .sea-stack-flip').forEach(function (btn) {
btn.classList.toggle('btn-disabled', on);
btn.innerHTML = on ? '×' : 'FLIP';
});
if (actionBtn) {
actionBtn.dataset.state = on ? 'complete' : 'auto-draw';
actionBtn.classList.toggle('btn-disabled', on);
}
_hideOk();
if (on && live) _startSeaCascade();
}
// ── Post-completion cascade (mirrors CAST SKY's _startSaveCascade) ──────────
// 6 cards down → linger ~3s → the felt eases OUT (revealing the table-hex) →
// DRAW SEA gives way to SEED MAP + the sea glow fires (burger → sea_btn) → +3s
// → SEED MAP eases IN. Same shape as CAST SKY → sky-btn glow → DRAW SEA.
var _SEA_CASCADE_LINGER = 3000, _SEA_FELT_FADE = 500, _SEED_DELAY = 3000;
var _cascadeRun = false;
function _startSeaCascade() {
if (_cascadeRun) return;
_cascadeRun = true;
setTimeout(_cascadeFeltOut, _SEA_CASCADE_LINGER);
}
function _cascadeFeltOut() {
page.classList.add('sea-page--cascade-out');
setTimeout(function () {
document.documentElement.classList.remove('sea-open');
page.classList.remove('sea-page--cascade-out');
_restorePhaseBtns();
if (window.RoomViews && window.RoomViews.syncGear) window.RoomViews.syncGear();
var seaPhaseBtn = document.getElementById('id_pick_sea_btn');
if (seaPhaseBtn) seaPhaseBtn.classList.add('hex-phase-btn--out');
// The sea btn becomes the reopen/review affordance (active only NOW — the
// post-completion cue, like the sky btn) + the sea glow fires on the burger.
var seaBtn = document.getElementById('id_sea_btn');
if (seaBtn) seaBtn.classList.add('active');
var burgerBtn = document.getElementById('id_burger_btn');
if (burgerBtn) burgerBtn.classList.add('glow-handoff');
setTimeout(_cascadeSeedMapIn, _SEED_DELAY);
}, _SEA_FELT_FADE);
}
function _cascadeSeedMapIn() {
var seedBtn = document.getElementById('id_seed_map_btn');
if (seedBtn) seedBtn.classList.remove('hex-phase-btn--out');
}
function _collectHandFromDom() {
var byPos = {};
cross.querySelectorAll('.sea-card-slot.sea-card-slot--filled').forEach(function (slot) {
var pos = slot.dataset.posKey || '';
if (!pos) return;
byPos[pos] = {
position: pos,
card_id: parseInt(slot.dataset.cardId, 10),
reversed: /sea-card-slot--reversed\b/.test(slot.className),
polarity: /sea-card-slot--levity\b/.test(slot.className) ? 'levity' : 'gravity',
};
});
var hand = [];
_currentOrder().forEach(function (pos) { if (byPos[pos]) hand.push(byPos[pos]); });
return hand;
}
function _postLock(hand) {
if (!hand.length) return Promise.resolve(null);
return fetch(SEA_SAVE_URL, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': _csrf() },
body: JSON.stringify({ spread: hidden.value, hand: hand }),
}).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; });
}
// ── Deck-stack FLIP → deposit one card onto the felt cross.
overlay.querySelectorAll('.sea-deck-stack').forEach(function (stack) {
stack.addEventListener('click', function (e) {
e.stopPropagation();
if (_activeStack === stack) _hideOk(); else _showOk(stack);
});
var ok = stack.querySelector('.sea-stack-flip');
if (ok) {
ok.addEventListener('click', function (e) {
e.stopPropagation();
if (_locked) { _hideOk(); return; }
var isLevity = stack.classList.contains('sea-deck-stack--levity');
var pile = isLevity ? _levityPile : _gravityPile;
if (!pile.length) { pile = isLevity ? _gravityPile : _levityPile; isLevity = !isLevity; }
var card = pile.length ? pile.shift() : null;
var order = _currentOrder();
var posName = order[_filled];
if (card && posName) {
if (window.SeaDeal && window.SeaDeal.openStage) {
SeaDeal.openStage(card, '.sea-pos-' + posName, isLevity);
}
_filled++;
if (_filled === 1) { _lockSpread(); _setHasDrawn(true); }
_postLock(_collectHandFromDom());
if (_filled >= order.length) _setComplete(true, true); // live → cascade
}
_hideOk();
});
}
});
overlay.addEventListener('click', _hideOk);
// ── AUTO DRAW — confirm the spread (reveal the cross) then commit + animate.
function _autoDraw() {
// Reveal the cross if not yet, AND scroll back UP to it even when the user
// already OK'd + scrolled down to the options page — so he watches the cards
// land one-by-one (user-spec 2026-06-08). `_chooseSpread(false)` skips the
// slide-in pin when already chosen, just easing up to the cross.
_chooseSpread(false);
var order = _currentOrder();
var remaining = order.length - _filled;
if (remaining <= 0) return;
var autoEntries = [];
for (var i = 0; i < remaining; i++) {
var isLevity = false;
var pile = _gravityPile;
if (_levityPile.length && (!_gravityPile.length || Math.round(Math.random()))) { isLevity = true; pile = _levityPile; }
if (!pile.length) { isLevity = !isLevity; pile = isLevity ? _levityPile : _gravityPile; }
if (!pile.length) break;
autoEntries.push({ card: pile.shift(), posName: order[_filled + i], isLevity: isLevity });
}
var fullHand = _collectHandFromDom();
autoEntries.forEach(function (e) {
fullHand.push({ position: e.posName, card_id: e.card.id, reversed: !!e.card.reversed, polarity: e.isLevity ? 'levity' : 'gravity' });
});
_postLock(fullHand).then(function (body) {
if (!body || !body.ok) return;
if (_filled === 0) _lockSpread();
_setHasDrawn(true);
var idx = 0;
function placeNext() {
if (idx >= autoEntries.length) { _setComplete(true, true); return; } // live → cascade
var e = autoEntries[idx++];
var stack = overlay.querySelector('.sea-deck-stack--' + (e.isLevity ? 'levity' : 'gravity'));
if (stack) _showOk(stack);
setTimeout(function () {
if (window.SeaDeal && window.SeaDeal.register) {
SeaDeal.register(e.card, '.sea-pos-' + e.posName, e.isLevity);
}
var sl = cross.querySelector('.sea-pos-' + e.posName + ' .sea-card-slot');
if (sl) sl.classList.add('sea-card-slot--visible');
_filled++;
_hideOk();
setTimeout(placeNext, 250);
}, 350);
}
placeNext();
});
}
// ── DEL — guard → clear the seat's celtic_cross → reload.
if (delBtn) {
delBtn.addEventListener('click', function (e) {
e.stopPropagation();
if (delBtn.classList.contains('btn-disabled')) return;
if (!window.showGuard) return;
window.showGuard(delBtn, 'Are you sure?', function () {
fetch(SEA_DELETE_URL, {
method: 'POST', credentials: 'same-origin', headers: { 'X-CSRFToken': _csrf() },
}).then(function (r) { if (r.ok) window.location.reload(); });
});
});
}
// ── AUTO DRAW btn → guard portal.
if (actionBtn) {
actionBtn.addEventListener('click', function (e) {
e.preventDefault();
if (actionBtn.dataset.state !== 'auto-draw') return;
if (!window.showGuard) return;
window.showGuard(actionBtn, 'Auto Draw cards?', _autoDraw);
});
}
// ── Spread switch (pre-OK) → re-layout the felt cross + the preview + labels.
function syncLabels(spread) {
var labels = POSITION_LABELS[spread] || {};
cross.querySelectorAll('.sea-pos-label').forEach(function (el) {
el.textContent = labels[el.dataset.position] || '';
});
}
function sync() {
cross.setAttribute('data-spread', hidden.value);
if (preview) preview.setAttribute('data-spread', hidden.value);
syncLabels(hidden.value);
}
hidden.addEventListener('change', sync);
// ── Open / close the felt (DRAW SEA ⇄ gear NVM nav). On open: disable ONLY the
// burger Text sub-btn (its swipe machine would drive into a scroll-locked
// reelhouse). The SKY sub-btn is left .active (lit) — symmetric with Sky Select,
// which leaves the sea btn lit while the sky felt is up (user-spec 2026-06-08):
// both completed phases stay one click apart. The sea btn itself is NOT
// activated here — it's the post-completion reopen affordance (see the cascade
// + the reopen binding). Clicking the lit sky btn from here SWAPS felts cleanly
// (openSea/openSky close each other — see openSea below).
var _disabledBtns = [];
function _disablePhaseBtns() {
_disabledBtns = [];
['id_text_btn'].forEach(function (id) {
var b = document.getElementById(id);
if (b && b.classList.contains('active')) { b.classList.remove('active'); _disabledBtns.push(b); }
});
}
function _restorePhaseBtns() {
_disabledBtns.forEach(function (b) { b.classList.add('active'); });
_disabledBtns = [];
}
function openSea() {
// Clean SWAP, not a double-open: if the sky felt is up (its lit sea btn was
// just clicked) close it first — both felts are z-5 siblings that otherwise
// stack (sea paints over sky), and neither open path drops the other's
// open-class on its own. Mirrored by openSky closing the sea felt.
if (document.documentElement.classList.contains('sky-open') && window.closeSkyFelt) {
window.closeSkyFelt();
}
// SEED MAP is a third equal-z felt sibling — close it too (trap T3).
if (document.documentElement.classList.contains('seed-open') && window.closeSeedMapFelt) {
window.closeSeedMapFelt();
}
document.documentElement.classList.add('sea-open');
document.documentElement.classList.add('sea-entered');
_disablePhaseBtns();
if (window.RoomViews && window.RoomViews.syncGear) window.RoomViews.syncGear();
}
function closeSea() {
document.documentElement.classList.remove('sea-open');
_restorePhaseBtns();
if (window.RoomViews && window.RoomViews.syncGear) window.RoomViews.syncGear();
}
window.openSeaFelt = openSea;
window.closeSeaFelt = closeSea;
var pickSeaBtn = document.getElementById('id_pick_sea_btn');
if (pickSeaBtn) pickSeaBtn.addEventListener('click', openSea);
// ── Sea sub-btn = post-completion REOPEN (active only once the spread is
// complete, like the sky btn). An ACTIVE click re-shows the felt to review the
// saved spread (window.openSeaFelt); inactive clicks fall to burger-btn.js's
// --priRd flash. DEFERRED to DOMContentLoaded: #id_sea_btn lives in
// _burger.html, which room.html includes AFTER this overlay — a parse-time
// getElementById returns null → the binding would silently no-op (sea btn
// dead). Bind once the burger fan exists.
function _bindSeaReopen() {
var seaSubBtn = document.getElementById('id_sea_btn');
if (!seaSubBtn) return;
seaSubBtn.addEventListener('click', function () {
if (!seaSubBtn.classList.contains('active')) return;
// Reel the aperture UP to the hex FIRST when down in the reelhouse (else the
// felt opens stranded off-screen + the aperture scroll-locks). scrollToHex
// fires synchronously when already at the hex, so an in-room click is unchanged.
if (window.RoomViews && window.RoomViews.scrollToHex) {
window.RoomViews.scrollToHex(openSea);
} else {
openSea();
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', _bindSeaReopen);
} else {
_bindSeaReopen();
}
// Re-seed SeaDeal's `_seaHand` from the server-rendered saved slots so they
// stay clickable to RE-OPEN the stage after a refresh.
function _seedSavedHand() {
if (!window.SeaDeal || !window.SeaDeal.seedHand) return;
var byId = {};
_levityPile.concat(_gravityPile).forEach(function (c) { byId[c.id] = c; });
var seed = {};
cross.querySelectorAll('.sea-card-slot.sea-card-slot--filled').forEach(function (slot) {
var posName = slot.dataset.posKey;
var card = byId[parseInt(slot.dataset.cardId, 10)];
if (!posName || !card) return;
var slotCard = {};
for (var k in card) if (Object.prototype.hasOwnProperty.call(card, k)) slotCard[k] = card[k];
slotCard.reversed = slot.classList.contains('sea-card-slot--reversed');
seed[posName] = { card: slotCard, isLevity: slot.classList.contains('sea-card-slot--levity') };
});
SeaDeal.seedHand(seed);
}
// ── Init — bind SeaDeal, seed the deck, then re-seed the saved hand.
if (window.SeaDeal && window.SeaDeal.reinit) SeaDeal.reinit();
_fetchDeck().then(_seedSavedHand);
_filled = cross.querySelectorAll('.sea-card-slot.sea-card-slot--filled').length;
if (_filled > 0) document.documentElement.classList.add('sea-entered');
if (_filled >= _currentOrder().length) { _setComplete(true); _lockSpread(); _setHasDrawn(true); }
else if (_filled > 0) { _lockSpread(); _setHasDrawn(true); }
syncLabels(hidden.value);
}());
</script>
{# ── Sea glow handoff — the SEA glow-machine does NOT fire while drawing. The #}
{# post-completion CASCADE (in the picker IIFE above) STARTS it on the burger #}
{# as the felt eases out (DRAW SEA → SEED MAP), mirroring CAST SKY's burger → #}
{# sky-btn cue. This IIFE only carries the handoff: burger → sea_btn → #}
{# .sea-select → end, so the user is led to reopen + review their sea. #}
<script>
(function () {
// DEFERRED to DOMContentLoaded: #id_burger_btn + #id_sea_btn live in
// _burger.html, which room.html includes AFTER this overlay — capturing them at
// parse time yields null and the early-return below would skip every listener
// (burger never sheds .glow-handoff → stuck --priId; the chain never advances).
// Bind once the burger fan exists.
function _bindGlowHandoff() {
var burgerBtn = document.getElementById('id_burger_btn');
var seaBtn = document.getElementById('id_sea_btn');
var seaSelect = document.querySelector('#id_sea_overlay .sea-select');
if (!burgerBtn || !seaBtn || !seaSelect) return;
var glowDone = false;
function endGlow() {
glowDone = true;
burgerBtn.classList.remove('glow-handoff');
seaBtn.classList.remove('glow-handoff');
seaSelect.classList.remove('glow-handoff');
}
burgerBtn.addEventListener('click', function () {
if (glowDone || !burgerBtn.classList.contains('glow-handoff')) return;
burgerBtn.classList.remove('glow-handoff');
seaBtn.classList.add('glow-handoff');
});
seaBtn.addEventListener('click', function () {
if (glowDone || !seaBtn.classList.contains('glow-handoff')) return;
seaBtn.classList.remove('glow-handoff');
seaSelect.classList.add('glow-handoff');
});
seaSelect.addEventListener('click', function () {
if (glowDone) return;
endGlow();
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', _bindGlowHandoff);
} else {
_bindGlowHandoff();
}
}());
</script>