Sea Select: post-completion cascade — felt eases out → DRAW SEA → SEED MAP — TDD
Mirror CAST SKY's post-save cascade for the sea phase. When the 6-card spread completes (live FLIP of the 6th card / AUTO DRAW finishing): linger ~3s on the felt → the felt eases OUT (`.sea-page--cascade-out`, revealing the table-hex) → DRAW SEA gives way to SEED MAP + the sea glow fires on the burger (handoff → sea_btn) → +3s → SEED MAP eases IN. Same shape as CAST SKY → sky-btn glow → DRAW SEA. - `_room_hex_center.html`: SEED MAP joins the hex-phase-stack; DRAW SEA goes --out once `hand_complete`, SEED MAP --out until then (a reload of a complete sea lands on SEED MAP server-side = the cascade's end-state). SEED MAP → the Voronoi map (roadmap step 21) is a stub — it only needs to APPEAR here - `_sea_overlay.html`: `_setComplete(on, live)` runs `_startSeaCascade()` on the LIVE completion (FLIP / AUTO DRAW pass `live=true`; init does not, so a reload doesn't re-animate). The completion-glow IIFE no longer self-starts on the data-state transition — the cascade adds `glow-handoff` to the burger; the IIFE keeps only the burger → sea_btn → .sea-select handoff - `.sea-page--cascade-out` SCSS (mirrors `.sky-page--cascade-out`) - ITs: SEED MAP --out pre-completion (DRAW SEA in); SEED MAP in + DRAW SEA --out when hand_complete. 952 epic+gameboard ITs + PickSeaAsyncTransitionTest(3) green Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4211,6 +4211,33 @@ class PickSeaUnifiedFeltTest(TestCase):
|
||||
self.assertNotIn("sea-deck-stack--single", content)
|
||||
self.assertNotIn("sea-stacks--single", content)
|
||||
|
||||
def test_seed_map_btn_out_until_hand_complete(self):
|
||||
"""SEED MAP joins the hex phase stack (the post-completion cascade eases
|
||||
it IN, mirroring CAST SKY → DRAW SEA). Pre-completion it renders --out
|
||||
while DRAW SEA is in (user-spec 2026-06-07)."""
|
||||
import lxml.html
|
||||
parsed = lxml.html.fromstring(self.client.get(self.url).content)
|
||||
[seed] = parsed.cssselect("#id_seed_map_btn")
|
||||
self.assertIn("hex-phase-btn--out", seed.get("class", ""))
|
||||
[sea] = parsed.cssselect("#id_pick_sea_btn")
|
||||
self.assertNotIn("hex-phase-btn--out", sea.get("class", ""))
|
||||
|
||||
def test_seed_map_in_and_draw_sea_out_when_hand_complete(self):
|
||||
"""A reload of an already-complete sea lands on SEED MAP server-side:
|
||||
SEED MAP in, DRAW SEA --out (the cascade's end-state, no animation)."""
|
||||
card_id = TarotCard.objects.filter(deck_variant=self.earthman).first().id
|
||||
char = Character.objects.get(seat=self.pc_seat, confirmed_at__isnull=False)
|
||||
char.celtic_cross = {"spread": "waite-smith", "hand": [
|
||||
{"position": p, "card_id": card_id, "reversed": False, "polarity": "gravity"}
|
||||
for p in ["cover", "cross", "crown", "lay", "loom", "leave"]]}
|
||||
char.save(update_fields=["celtic_cross"])
|
||||
import lxml.html
|
||||
parsed = lxml.html.fromstring(self.client.get(self.url).content)
|
||||
[seed] = parsed.cssselect("#id_seed_map_btn")
|
||||
self.assertNotIn("hex-phase-btn--out", seed.get("class", ""))
|
||||
[sea] = parsed.cssselect("#id_pick_sea_btn")
|
||||
self.assertIn("hex-phase-btn--out", sea.get("class", ""))
|
||||
|
||||
|
||||
class CarteSeatSwitchSkySeaTest(TestCase):
|
||||
"""A CARTE owner (one gamer owns all 6 seats) must drive EACH seat through
|
||||
|
||||
@@ -103,6 +103,15 @@ html.sea-open .position-strip {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
// Post-completion cascade ease-out — once the 6-card spread is complete the felt
|
||||
// fades to transparent over ~0.5s (the JS _SEA_FELT_FADE timer) BEFORE sea-open
|
||||
// is removed, revealing the table-hex with a soft dissolve (mirrors the CAST SKY
|
||||
// post-save cascade's .sky-page--cascade-out).
|
||||
.sea-page.sea-page--cascade-out {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
// ── Backdrop ──────────────────────────────────────────────────────────────────
|
||||
|
||||
.sky-backdrop {
|
||||
|
||||
@@ -29,14 +29,17 @@ sky_confirmed, polarity_done, user_polarity, current_slot, …).
|
||||
onclick="window.location.href='{% url 'epic:room_gate' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}'">GATE<br>VIEW</button>
|
||||
{% else %}
|
||||
{% if room.table_status == "SKY_SELECT" %}
|
||||
{# Both phase btns render so the post-save cascade can cross-fade CAST #}
|
||||
{# SKY → DRAW SEA in place (no reload). The server sets --out on the #}
|
||||
{# inactive one; sky-select.js's cascade swaps them. On a confirmed #}
|
||||
{# reload the server lands DRAW SEA visible + CAST SKY out, same as the #}
|
||||
{# cascade end. #}
|
||||
{# All three phase btns render so the cascades can cross-fade in place #}
|
||||
{# (no reload): CAST SKY → DRAW SEA on sky-save, then DRAW SEA → SEED MAP #}
|
||||
{# on the 6-card spread completing. The server sets --out on the inactive #}
|
||||
{# ones; the sky/sea cascades swap them. On a reload the server lands the #}
|
||||
{# right one visible (DRAW SEA once sky_confirmed; SEED MAP once the sea #}
|
||||
{# hand is complete), same as each cascade's end. SEED MAP → the Voronoi #}
|
||||
{# map (roadmap step 21) is a stub for now — it only needs to APPEAR here. #}
|
||||
<div class="hex-phase-stack">
|
||||
<button id="id_pick_sky_btn" class="btn btn-primary hex-phase-btn{% if sky_confirmed %} hex-phase-btn--out{% endif %}">CAST<br>SKY</button>
|
||||
<button id="id_pick_sea_btn" class="btn btn-primary hex-phase-btn{% if not sky_confirmed %} hex-phase-btn--out{% endif %}">DRAW<br>SEA</button>
|
||||
<button id="id_pick_sea_btn" class="btn btn-primary hex-phase-btn{% if not sky_confirmed or hand_complete %} hex-phase-btn--out{% endif %}">DRAW<br>SEA</button>
|
||||
<button id="id_seed_map_btn" class="btn btn-primary hex-phase-btn{% if not hand_complete %} hex-phase-btn--out{% endif %}">SEED<br>MAP</button>
|
||||
</div>
|
||||
{% elif room.table_status == "SIG_SELECT" %}
|
||||
<button id="id_pick_sky_btn" class="btn btn-primary" style="display:none">CAST<br>SKY</button>
|
||||
|
||||
@@ -222,7 +222,7 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
|
||||
function _setHasDrawn(on) {
|
||||
if (delBtn) { delBtn.classList.toggle('btn-disabled', !on); delBtn.innerHTML = on ? 'DEL' : '×'; }
|
||||
}
|
||||
function _setComplete(on) {
|
||||
function _setComplete(on, live) {
|
||||
_locked = on;
|
||||
overlay.classList.toggle('my-sea-picker--locked', on);
|
||||
overlay.querySelectorAll('.sea-deck-stack .sea-stack-ok').forEach(function (btn) {
|
||||
@@ -234,6 +234,45 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
|
||||
actionBtn.classList.toggle('btn-disabled', on);
|
||||
}
|
||||
_hideOk();
|
||||
// Live completion (manual FLIP of the 6th card / AUTO DRAW finishing) → run
|
||||
// the post-completion cascade. NOT on init (a reload of an already-complete
|
||||
// sea lands on the SEED MAP hex server-side, no animation).
|
||||
if (on && live) _startSeaCascade();
|
||||
}
|
||||
|
||||
// ── Post-completion cascade (mirrors CAST SKY's _startSaveCascade) ──────────
|
||||
// The 6-card spread completes → linger ~3s on the felt → the felt eases OUT
|
||||
// (revealing the table-hex) → DRAW SEA gives way to SEED MAP + the sea glow
|
||||
// fires (burger → sea_btn handoff) → +3s → SEED MAP eases IN. Same shape as
|
||||
// the CAST SKY → sky-btn glow → DRAW SEA sequence (user-spec 2026-06-07).
|
||||
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'); // CSS fades the felt 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();
|
||||
// DRAW SEA is stale → ease it out; keep the sea_btn active (reopen/review)
|
||||
// + start the sea glow handoff on the burger, concurrent w. the hex reveal.
|
||||
var seaPhaseBtn = document.getElementById('id_pick_sea_btn');
|
||||
if (seaPhaseBtn) seaPhaseBtn.classList.add('hex-phase-btn--out');
|
||||
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'); // eases in
|
||||
}
|
||||
|
||||
function _collectHandFromDom() {
|
||||
@@ -288,7 +327,7 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
|
||||
_filled++;
|
||||
if (_filled === 1) { _lockSpread(); _setHasDrawn(true); }
|
||||
_postLock(_collectHandFromDom());
|
||||
if (_filled >= order.length) _setComplete(true);
|
||||
if (_filled >= order.length) _setComplete(true, true); // live → cascade
|
||||
}
|
||||
_hideOk();
|
||||
});
|
||||
@@ -320,7 +359,7 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
|
||||
_setHasDrawn(true);
|
||||
var idx = 0;
|
||||
function placeNext() {
|
||||
if (idx >= autoEntries.length) { _setComplete(true); return; }
|
||||
if (idx >= autoEntries.length) { _setComplete(true, true); return; } // live → cascade
|
||||
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).
|
||||
@@ -493,12 +532,12 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
|
||||
}());
|
||||
</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. #}
|
||||
{# ── 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 review their sea. User-spec #}
|
||||
{# 2026-06-07. #}
|
||||
<script>
|
||||
(function () {
|
||||
var burgerBtn = document.getElementById('id_burger_btn');
|
||||
@@ -507,16 +546,9 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
|
||||
if (!burgerBtn || !seaBtn || !modal) return;
|
||||
|
||||
var seaSelect = modal.querySelector('.sea-select');
|
||||
var actionBtn = document.getElementById('id_sea_action_btn');
|
||||
if (!seaSelect || !actionBtn) return;
|
||||
if (!seaSelect) return;
|
||||
|
||||
var started = false, glowDone = false;
|
||||
|
||||
function startGlow() {
|
||||
if (started || glowDone) return;
|
||||
started = true;
|
||||
burgerBtn.classList.add('glow-handoff');
|
||||
}
|
||||
var glowDone = false;
|
||||
function endGlow() {
|
||||
glowDone = true;
|
||||
burgerBtn.classList.remove('glow-handoff');
|
||||
@@ -524,7 +556,8 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
|
||||
seaSelect.classList.remove('glow-handoff');
|
||||
}
|
||||
|
||||
// Handoff on clicks: burger → sea_btn → .sea-select → end.
|
||||
// Handoff on clicks: burger → sea_btn → .sea-select → end. (The cascade adds
|
||||
// `glow-handoff` to the burger to begin the sequence.)
|
||||
burgerBtn.addEventListener('click', function () {
|
||||
if (glowDone || !burgerBtn.classList.contains('glow-handoff')) return;
|
||||
burgerBtn.classList.remove('glow-handoff');
|
||||
@@ -539,14 +572,5 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
|
||||
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