Sea Select: rebuild as a felt + Gaussian spread modal, unify w. my_sea — TDD
Hollow out the gameroom DRAW SEA dark modal into the two surfaces my_sea
uses: a --duoUser felt (.sea-page--room) filling the hex pane where the
Celtic Cross deals, + a Gaussian spread modal (#id_sea_spread_modal: the
.sea-select combobox w. the two Celtic Cross 6-card opts ONLY, AUTO DRAW/DEL,
a mini preview, + a corner #id_sea_cancel NVM). Opened by DRAW SEA
(html.sea-open); the room gear's NVM (room-menu-sea) returns to the hex;
#id_text_btn + #id_sky_btn go inert while the felt is open.
- persist: epic:sea_save / sea_delete upsert the seat's Character.celtic_cross
(none of my_sea's MySeaDraw quota/Brief machinery); room ctx adds
saved_by_position + saved_sea_spread + sea_default_spread + hand_complete so
a reload re-renders the filled cross. celtic_cross field already existed (no
migration)
- mini spread preview (_sea_spread_preview.html) in BOTH the gameroom + my_sea
modals — shape only, NEVER dealt to: SeaDeal scopes its slot queries to
.sea-cross:not(.sea-cross--preview)
- always TWO deck stacks (Gravity + Levity) in the room Sea Select — the gamer
draws from either populated half (sea_deck split), even a CARTE monodeck;
unlike my_sea / Sig Select's polarization collapse
- glow coordination: the sky-saved glow is muted in the sky/sea phases
(html.sky-open / sea-open / sea-entered); sea glow color --priYl -> --priId
(distinct from sky's --priTk); the sea glow-machine fires at hand-COMPLETION
(mirrors Sky Select), not during drawing
- guard copy "Auto deal cards?" -> "Auto Draw cards?" (match the AUTO DRAW btn)
- fix: drop the stale `html.sea-open #id_aperture_fill { opacity:1 }` — it
painted the opaque z-90 fill over the z-5 felt so the spread flashed then
vanished (same trap as the CAST SKY felt); removed the dead .sea-backdrop /
.sea-overlay / .sea-modal-* SCSS
- tests: epic PickSeaPersistTest (7) + PickSeaUnifiedFeltTest (6) ITs; SeaDeal
preview-scoping + BurgerSpec sky-glow-mute Jasmine specs; my_sea sig-card
ITs scoped to .my-sea-cross (the preview adds a 2nd .sea-sig-card)
[[feedback-felt-aperture-fill-covers-felt]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,84 +1,107 @@
|
||||
{% load static %}
|
||||
{# DRAW SEA overlay — Celtic Cross spread entry #}
|
||||
{# Included in room.html when table_status == "SKY_SELECT" and sky_confirmed #}
|
||||
{# Layout is the reverse of CAST SKY: cards left (transparent), form right #}
|
||||
|
||||
<div class="sea-backdrop"></div>
|
||||
<div class="sea-overlay" id="id_sea_overlay"
|
||||
{% comment %}
|
||||
DRAW SEA — Sea Select felt + Gaussian spread modal, unified with my_sea.html
|
||||
(2026-06-07). Replaces the legacy dark single-modal. TWO surfaces, mirroring
|
||||
my_sea:
|
||||
• Sea Select FELT (.sea-page--room) — a --duoUser fill of the hex pane; the
|
||||
real Celtic-Cross spread deals here + the deck stacks. Opened by
|
||||
#id_pick_sea_btn (html.sea-open); the room gear's NVM returns to the hex.
|
||||
• Gaussian spread MODAL (#id_sea_spread_modal) — the .sea-select combobox
|
||||
(the two Celtic Cross 6-card options ONLY, per user-spec 2026-06-07) +
|
||||
AUTO DRAW + DEL + a miniaturized shape preview + a corner #id_sea_cancel
|
||||
NVM (closes back to the felt). Opened via the burger #id_sea_btn.
|
||||
Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
|
||||
{% endcomment %}
|
||||
<div class="sea-page sea-page--room" id="id_sea_page">
|
||||
<div class="my-sea-picker{% if hand_complete %} my-sea-picker--locked{% endif %}" id="id_sea_overlay"
|
||||
data-sea-deck-url="{% url 'epic:sea_deck' room.id %}"
|
||||
data-sea-save-url="{% url 'epic:sea_save' room.id %}"
|
||||
data-sea-delete-url="{% url 'epic:sea_delete' room.id %}"
|
||||
data-sea-user-polarity="{{ user_polarity }}">
|
||||
|
||||
<div class="sea-modal-wrap">
|
||||
<div class="sea-modal">
|
||||
|
||||
<header class="sea-modal-header">
|
||||
<h2>SEA <span>SELECT</span></h2>
|
||||
<p>Draw +6 cards to describe your character's influences and seed the map.</p>
|
||||
</header>
|
||||
|
||||
<div class="sea-modal-body">
|
||||
|
||||
{# ── Cards column (transparent) ───────────────────────────── #}
|
||||
<div class="sea-cards-col">
|
||||
<div class="sea-cross">
|
||||
{# Crown — CC pos 3 / EV pos 5 #}
|
||||
<div class="sea-crucifix-cell sea-pos-crown">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
{# Behind (past) — CC pos 6 / EV pos 4 #}
|
||||
<div class="sea-crucifix-cell sea-pos-leave">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
{# Center — Significator (always placed) + Cover + Cross overlaid #}
|
||||
<div class="sea-crucifix-cell sea-pos-core">
|
||||
<div class="sig-stage-card sea-sig-card" style="--sig-card-w: 4rem">
|
||||
{% if my_tray_sig %}
|
||||
<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 %}
|
||||
</div>
|
||||
{# Cover — CC/EV pos 1, stacked face-up on Sig #}
|
||||
<div class="sea-pos-cover">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
{# Cross — CC/EV pos 2, rotated 90° on Cover #}
|
||||
<div class="sea-pos-cross">
|
||||
<div class="sea-card-slot sea-card-slot--empty sea-card-slot--crossing"></div>
|
||||
</div>
|
||||
</div>
|
||||
{# Before (future) — CC pos 5 / EV pos 6 #}
|
||||
<div class="sea-crucifix-cell sea-pos-loom">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
{# Beneath (root) — CC pos 4 / EV pos 3 #}
|
||||
<div class="sea-crucifix-cell sea-pos-lay">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
{# ── Felt cross — the REAL deal target (.my-sea-cross). Sig pins core; the #}
|
||||
{# six CC positions render w. labels + saved-hand pre-fill. The default #}
|
||||
{# spread is the polarity-matched Celtic Cross (or the saved one). #}
|
||||
<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">
|
||||
<div class="sig-stage-card sea-sig-card" style="--sig-card-w: 4rem">
|
||||
{% if my_tray_sig %}
|
||||
<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 %}
|
||||
</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>
|
||||
|
||||
{# ── Deck stacks — pinned bottom-right of the felt, so FLIP stays usable #}
|
||||
{# while the spread modal is closed. ALWAYS two stacks (Gravity + Levity), #}
|
||||
{# unlike my_sea / Sig Select: in the room's Sea Select phase the gamer may #}
|
||||
{# draw from EITHER populated half (sea_deck returns both), even a CARTE #}
|
||||
{# user whose monodeck is presented across the two polarity stacks #}
|
||||
{# (user-spec 2026-06-07). So the polarization-conditional shared partial #}
|
||||
{# is intentionally NOT used here. #}
|
||||
<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-ok{% 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-ok{% 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>
|
||||
|
||||
{# ── Gaussian spread modal — opens on the burger #id_sea_btn. #}
|
||||
<div id="id_sea_spread_modal" class="my-sea-spread-modal" hidden>
|
||||
<div class="my-sea-spread-modal__backdrop"></div>
|
||||
<div class="my-sea-spread-modal__panel">
|
||||
{# Corner NVM (top-right) — closes the modal back to the felt. #}
|
||||
<button type="button" id="id_sea_cancel" class="btn btn-cancel btn-sm sea-modal-nvm">NVM</button>
|
||||
|
||||
{# Miniaturized spread preview — shape only, never dealt to. #}
|
||||
{% include "apps/gameboard/_partials/_sea_spread_preview.html" with preview_spread=sea_default_spread sig=my_tray_sig %}
|
||||
|
||||
{# ── Form column (priUser / opaque) ───────────────────────── #}
|
||||
<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>
|
||||
{% comment %}
|
||||
Reversal-rate hint — `stack_reversal_pct` flows from
|
||||
apps.epic.utils.stack_reversal_probability via the
|
||||
view. Currently a module default; placeholder UI for
|
||||
a forthcoming per-user setting.
|
||||
{% endcomment %}
|
||||
<p class="sea-reversal-hint">{{ stack_reversal_pct|default:25 }}% reversals</p>
|
||||
{% comment %}
|
||||
Custom combobox — native <select> dropdowns ignore most CSS on
|
||||
Firefox/Chrome (OS-rendered list); this gives full styling control.
|
||||
combobox.js wires up the keyboard nav, click-outside-to-close, and
|
||||
writes the chosen value to the hidden <input id="id_sea_spread"> so
|
||||
sea.js's existing `spreadSel.value` read still works.
|
||||
{% endcomment %}
|
||||
<input type="hidden" id="id_sea_spread" name="spread"
|
||||
value="{% if user_polarity == 'levity' %}waite-smith{% else %}escape-velocity{% endif %}">
|
||||
{# 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 }}">
|
||||
<div class="sea-select"
|
||||
data-combobox
|
||||
data-combobox-target="id_sea_spread"
|
||||
@@ -87,176 +110,411 @@
|
||||
aria-haspopup="listbox"
|
||||
aria-labelledby="id_sea_spread_label"
|
||||
tabindex="0">
|
||||
<span class="sea-select-current">{% if user_polarity == "levity" %}Celtic Cross, Waite-Smith{% else %}Celtic Cross, Escape Velocity{% endif %}</span>
|
||||
<span class="sea-select-current">{% if sea_default_spread == "escape-velocity" %}Celtic Cross, Escape Velocity{% else %}Celtic Cross, 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="true">Celtic Cross, Waite-Smith</li>
|
||||
<li role="option" data-value="escape-velocity" aria-selected="false">Celtic Cross, Escape Velocity</li>
|
||||
<li role="option" data-value="waite-smith" aria-selected="{% if sea_default_spread == 'escape-velocity' %}false{% else %}true{% endif %}">Celtic Cross, Waite-Smith</li>
|
||||
<li role="option" data-value="escape-velocity" aria-selected="{% if sea_default_spread == 'escape-velocity' %}true{% else %}false{% endif %}">Celtic Cross, Escape Velocity</li>
|
||||
{% else %}
|
||||
<li role="option" data-value="escape-velocity" aria-selected="true">Celtic Cross, Escape Velocity</li>
|
||||
<li role="option" data-value="waite-smith" aria-selected="false">Celtic Cross, Waite-Smith</li>
|
||||
<li role="option" data-value="escape-velocity" aria-selected="{% if sea_default_spread == 'waite-smith' %}false{% else %}true{% endif %}">Celtic Cross, Escape Velocity</li>
|
||||
<li role="option" data-value="waite-smith" aria-selected="{% if sea_default_spread == 'waite-smith' %}true{% else %}false{% endif %}">Celtic Cross, Waite-Smith</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Two face-down deck piles — tap to proffer OK #}
|
||||
<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-ok" type="button">FLIP</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-ok" type="button">FLIP</button>
|
||||
</div>
|
||||
<span class="sea-stack-name">Levity</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sea-form-actions">
|
||||
<button type="button" id="id_sea_lock_hand" class="btn btn-primary" disabled>
|
||||
LOCK HAND
|
||||
</button>
|
||||
<button type="button" id="id_sea_del" class="btn btn-danger">
|
||||
DEL
|
||||
</button>
|
||||
{# AUTO DRAW (mid-draw) — commits the remaining hand in one POST + #}
|
||||
{# animates placement onto the felt. Disabled once complete (the #}
|
||||
{# room has no GATE VIEW yet — character creation ends at the #}
|
||||
{# Voronoi map, roadmap step 21). #}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
</div>{# /.sea-modal-body #}
|
||||
|
||||
</div>{# /.sea-modal #}
|
||||
|
||||
<button type="button" id="id_sea_cancel" class="btn btn-cancel btn-sm">NVM</button>
|
||||
|
||||
</div>{# /.sea-modal-wrap #}
|
||||
|
||||
{# ── Sea stage — big card viewer ─────────────────────────────────────────── #}
|
||||
{# Extracted to a shared partial so the my-sea picker (Sprint 5 iter 4-bugs) #}
|
||||
{# reuses the same DOM contract that SeaDeal binds to. #}
|
||||
{# Sea stage — portaled big-card viewer (shared w. my_sea). #}
|
||||
{% include "apps/gameboard/_partials/_sea_stage.html" %}
|
||||
|
||||
</div>{# /.sea-overlay #}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const overlay = document.getElementById('id_sea_overlay');
|
||||
const cancelBtn = document.getElementById('id_sea_cancel');
|
||||
var page = document.getElementById('id_sea_page');
|
||||
var overlay = document.getElementById('id_sea_overlay');
|
||||
if (!page || !overlay) return;
|
||||
|
||||
function openSea() {
|
||||
document.documentElement.classList.add('sea-open');
|
||||
}
|
||||
|
||||
function closeSea() {
|
||||
document.documentElement.classList.remove('sea-open');
|
||||
}
|
||||
|
||||
const pickSeaBtn = document.getElementById('id_pick_sea_btn');
|
||||
if (pickSeaBtn) pickSeaBtn.addEventListener('click', openSea);
|
||||
cancelBtn.addEventListener('click', closeSea);
|
||||
|
||||
// ── Deck draw ──────────────────────────────────────────────────────────────
|
||||
|
||||
const SEA_DECK_URL = overlay.dataset.seaDeckUrl;
|
||||
|
||||
const SPREAD_ORDER = {
|
||||
'waite-smith': ['.sea-pos-cover', '.sea-pos-cross', '.sea-pos-crown', '.sea-pos-lay', '.sea-pos-loom', '.sea-pos-leave'],
|
||||
'escape-velocity': ['.sea-pos-cover', '.sea-pos-cross', '.sea-pos-lay', '.sea-pos-leave', '.sea-pos-crown', '.sea-pos-loom'],
|
||||
// ── Per-spread draw order + labels — the two Celtic Cross variants only.
|
||||
// WS / EV share the 6 positions but differ in draw order + diagonal labels.
|
||||
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' },
|
||||
};
|
||||
|
||||
let levityPile = [], gravityPile = [];
|
||||
let _filled = 0;
|
||||
let _activeStack = null;
|
||||
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 seaSelect = overlay.querySelector('[data-combobox][data-combobox-target="id_sea_spread"]');
|
||||
if (!hidden || !cross) return;
|
||||
|
||||
const spreadSel = overlay.querySelector('#id_sea_spread');
|
||||
const lockBtn = overlay.querySelector('#id_sea_lock_hand');
|
||||
const delBtn = overlay.querySelector('#id_sea_del');
|
||||
var SEA_DECK_URL = overlay.dataset.seaDeckUrl;
|
||||
var SEA_SAVE_URL = overlay.dataset.seaSaveUrl;
|
||||
var SEA_DELETE_URL = overlay.dataset.seaDeleteUrl;
|
||||
|
||||
function _spreadKey() {
|
||||
return spreadSel ? spreadSel.value : 'waite-smith';
|
||||
}
|
||||
|
||||
function _nextPosSelector() {
|
||||
const order = SPREAD_ORDER[_spreadKey()] || SPREAD_ORDER['waite-smith'];
|
||||
return order[_filled] || null;
|
||||
}
|
||||
|
||||
function _hideOk() {
|
||||
if (_activeStack) {
|
||||
const ok = _activeStack.querySelector('.sea-stack-ok');
|
||||
if (ok) ok.style.display = 'none';
|
||||
_activeStack.classList.remove('sea-deck-stack--active');
|
||||
_activeStack = null;
|
||||
}
|
||||
}
|
||||
|
||||
function _showOk(stack) {
|
||||
_hideOk();
|
||||
_activeStack = stack;
|
||||
stack.classList.add('sea-deck-stack--active');
|
||||
const ok = stack.querySelector('.sea-stack-ok');
|
||||
if (ok) ok.style.display = '';
|
||||
}
|
||||
|
||||
function _reset() {
|
||||
_filled = 0;
|
||||
_hideOk();
|
||||
overlay.querySelectorAll('.sea-card-slot').forEach(s => {
|
||||
s.classList.remove('sea-card-slot--filled', 'sea-card-slot--visible', 'sea-card-slot--focused', 'sea-card-slot--levity', 'sea-card-slot--gravity');
|
||||
s.classList.add('sea-card-slot--empty');
|
||||
s.innerHTML = '';
|
||||
delete s.dataset.cardId;
|
||||
delete s.dataset.posKey;
|
||||
});
|
||||
if (lockBtn) lockBtn.disabled = true;
|
||||
if (window.SeaDeal) SeaDeal.resetHand();
|
||||
_fetchDeck();
|
||||
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() {
|
||||
fetch(SEA_DECK_URL, { credentials: 'same-origin' })
|
||||
.then(r => r.json())
|
||||
.then(data => { levityPile = data.levity || []; gravityPile = data.gravity || []; })
|
||||
.catch(() => {});
|
||||
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 () {});
|
||||
}
|
||||
|
||||
overlay.querySelectorAll('.sea-deck-stack').forEach(stack => {
|
||||
stack.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
_activeStack === stack ? _hideOk() : _showOk(stack);
|
||||
// ── FLIP affordance (CSS-driven via .sea-deck-stack--active).
|
||||
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'); }
|
||||
}
|
||||
function _unlockSpread() {
|
||||
if (seaSelect) { seaSelect.classList.remove('sea-select--locked'); seaSelect.removeAttribute('aria-disabled'); }
|
||||
}
|
||||
|
||||
function _setHasDrawn(on) {
|
||||
if (delBtn) { delBtn.classList.toggle('btn-disabled', !on); delBtn.innerHTML = on ? 'DEL' : '×'; }
|
||||
}
|
||||
function _setComplete(on) {
|
||||
_locked = on;
|
||||
overlay.classList.toggle('my-sea-picker--locked', on);
|
||||
overlay.querySelectorAll('.sea-deck-stack .sea-stack-ok').forEach(function (btn) {
|
||||
btn.classList.toggle('btn-disabled', on);
|
||||
btn.innerHTML = on ? '×' : 'FLIP';
|
||||
});
|
||||
const ok = stack.querySelector('.sea-stack-ok');
|
||||
if (actionBtn) {
|
||||
actionBtn.dataset.state = on ? 'complete' : 'auto-draw';
|
||||
actionBtn.classList.toggle('btn-disabled', on);
|
||||
}
|
||||
_hideOk();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ── Persist — upsert the full current hand onto the seat's Character.
|
||||
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-ok');
|
||||
if (ok) {
|
||||
ok.style.display = 'none';
|
||||
ok.addEventListener('click', e => {
|
||||
ok.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
const isLevity = stack.classList.contains('sea-deck-stack--levity');
|
||||
const pile = isLevity ? levityPile : gravityPile;
|
||||
const card = pile.length ? pile.shift() : null;
|
||||
const pos = _nextPosSelector();
|
||||
if (card && pos) {
|
||||
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 (lockBtn) lockBtn.disabled = (_filled < 6);
|
||||
if (window.SeaDeal) SeaDeal.openStage(card, pos, isLevity);
|
||||
if (_filled === 1) { _lockSpread(); _setHasDrawn(true); }
|
||||
_postLock(_collectHandFromDom());
|
||||
if (_filled >= order.length) _setComplete(true);
|
||||
}
|
||||
_hideOk();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
overlay.addEventListener('click', _hideOk);
|
||||
if (delBtn) delBtn.addEventListener('click', _reset);
|
||||
|
||||
// ── AUTO DRAW — commit remaining hand in ONE POST, then animate placement.
|
||||
function _autoDraw() {
|
||||
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); return; }
|
||||
var e = autoEntries[idx++];
|
||||
// Gameroom Sea Select always has BOTH polarity stacks (no single-stack
|
||||
// fallback — that's a my_sea monodeck concern, not here).
|
||||
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 → 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 the
|
||||
// burger Text + Sky sub-btns (Text's swipe machine would drive into the
|
||||
// scroll-locked reelhouse; Sky would reopen the wheel mid-draw), light the
|
||||
// sea_btn, mark html.sea-entered (mutes the sky-saved glow henceforth), and
|
||||
// start the burger glow handoff. Server baseline restored on close.
|
||||
var _disabledBtns = [];
|
||||
function _disablePhaseBtns() {
|
||||
_disabledBtns = [];
|
||||
['id_text_btn', 'id_sky_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() {
|
||||
document.documentElement.classList.add('sea-open');
|
||||
// Once DRAW SEA is engaged the sky-saved glow stays muted (user-spec
|
||||
// 2026-06-07, conditions 2 & 3). The SEA glow-machine does NOT fire here —
|
||||
// mirroring Sky Select, it holds until the hand is complete (see the
|
||||
// completion-glow IIFE below), so drawing stays quiet.
|
||||
document.documentElement.classList.add('sea-entered');
|
||||
_disablePhaseBtns();
|
||||
var seaBtn = document.getElementById('id_sea_btn');
|
||||
if (seaBtn) seaBtn.classList.add('active');
|
||||
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;
|
||||
var pickSeaBtn = document.getElementById('id_pick_sea_btn');
|
||||
if (pickSeaBtn) pickSeaBtn.addEventListener('click', openSea);
|
||||
|
||||
// ── Init — seed deck, restore saved-hand state, draw labels.
|
||||
_fetchDeck();
|
||||
if (window.SeaDeal) SeaDeal.reinit();
|
||||
})();
|
||||
_filled = cross.querySelectorAll('.sea-card-slot.sea-card-slot--filled').length;
|
||||
// Already-drawn sea hand → "in Sea Select or beyond" (user-spec 2026-06-07,
|
||||
// condition 3): mark sea-entered on load so the sky-saved glow stays muted
|
||||
// across reloads, not just after a live DRAW SEA click.
|
||||
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);
|
||||
|
||||
// Re-seed SeaDeal's _seaHand from server-rendered saved slots so they stay
|
||||
// clickable to re-open the stage. (Card payloads come from the deck fetch.)
|
||||
if (window.SeaDeal && window.SeaDeal.reinit) SeaDeal.reinit();
|
||||
}());
|
||||
</script>
|
||||
|
||||
{# ── Spread modal open/close — opens on the burger #id_sea_btn (active while #}
|
||||
{# the felt is up). Closes on the corner NVM (#id_sea_cancel), the Gaussian #}
|
||||
{# backdrop, Escape, or a guard-OK (so AUTO DRAW / DEL run against a closed #}
|
||||
{# modal). Mirrors my_sea's Phase-2 IIFE. #}
|
||||
<script>
|
||||
(function () {
|
||||
var seaBtn = document.getElementById('id_sea_btn');
|
||||
var modal = document.getElementById('id_sea_spread_modal');
|
||||
if (!seaBtn || !modal) return;
|
||||
|
||||
var backdrop = modal.querySelector('.my-sea-spread-modal__backdrop');
|
||||
var cancelBtn = modal.querySelector('#id_sea_cancel');
|
||||
|
||||
function openModal() { modal.removeAttribute('hidden'); }
|
||||
function closeModal() { modal.setAttribute('hidden', ''); }
|
||||
|
||||
seaBtn.addEventListener('click', function () {
|
||||
// Inactive sub-btn click is the delegated --priRd flash (burger-btn.js);
|
||||
// only open the modal when the Sea sub-btn is active (felt is up).
|
||||
if (!seaBtn.classList.contains('active')) return;
|
||||
openModal();
|
||||
});
|
||||
if (backdrop) backdrop.addEventListener('click', closeModal);
|
||||
if (cancelBtn) cancelBtn.addEventListener('click', closeModal); // corner NVM → felt
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && !modal.hasAttribute('hidden')) closeModal();
|
||||
});
|
||||
// Modal stays up through the AUTO DRAW / DEL guard portal (so it can anchor
|
||||
// against the visible btn); close on guard-OK so the draw / delete proceeds
|
||||
// against an already-closed modal (the spread animates onto the felt).
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!e.target.closest('#id_guard_portal .guard-yes')) return;
|
||||
if (!modal.hasAttribute('hidden')) closeModal();
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
|
||||
{# ── Completion glow cue (mirrors Sky Select) — the SEA glow-machine does NOT #}
|
||||
{# fire while drawing. It starts only once ALL slots are drawn (the action btn #}
|
||||
{# flips to data-state="complete", DRAW SEA gives way to SEED MAP), then hands #}
|
||||
{# off burger → sea_btn → .sea-select so the user can review their sea. The #}
|
||||
{# cue starts on the live completion TRANSITION (manual FLIP or AUTO DRAW), not #}
|
||||
{# on a reload of an already-complete sea. User-spec 2026-06-07. #}
|
||||
<script>
|
||||
(function () {
|
||||
var burgerBtn = document.getElementById('id_burger_btn');
|
||||
var seaBtn = document.getElementById('id_sea_btn');
|
||||
var modal = document.getElementById('id_sea_spread_modal');
|
||||
if (!burgerBtn || !seaBtn || !modal) return;
|
||||
|
||||
var seaSelect = modal.querySelector('.sea-select');
|
||||
var actionBtn = document.getElementById('id_sea_action_btn');
|
||||
if (!seaSelect || !actionBtn) return;
|
||||
|
||||
var started = false, glowDone = false;
|
||||
|
||||
function startGlow() {
|
||||
if (started || glowDone) return;
|
||||
started = true;
|
||||
burgerBtn.classList.add('glow-handoff');
|
||||
}
|
||||
function endGlow() {
|
||||
glowDone = true;
|
||||
burgerBtn.classList.remove('glow-handoff');
|
||||
seaBtn.classList.remove('glow-handoff');
|
||||
seaSelect.classList.remove('glow-handoff');
|
||||
}
|
||||
|
||||
// Handoff on clicks: burger → sea_btn → .sea-select → end.
|
||||
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();
|
||||
});
|
||||
|
||||
// Start the cue on the live completion transition (data-state → "complete").
|
||||
var obs = new MutationObserver(function (mutations) {
|
||||
for (var i = 0; i < mutations.length; i++) {
|
||||
if (mutations[i].attributeName !== 'data-state') continue;
|
||||
if (actionBtn.dataset.state === 'complete') startGlow();
|
||||
}
|
||||
});
|
||||
obs.observe(actionBtn, { attributes: true, attributeFilter: ['data-state'] });
|
||||
}());
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user