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>
608 lines
30 KiB
HTML
608 lines
30 KiB
HTML
{% 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 %}×{% 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 %}×{% 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 %}×{% 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 %}×{% 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>
|