From 132e60864e321325e641be55c445363685fc4cdf Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Tue, 28 Apr 2026 23:02:49 -0400 Subject: [PATCH] =?UTF-8?q?PICK=20SEA=20Sprint=20B:=20deck=20stacks,=20OK?= =?UTF-8?q?=20btn,=20card=20draw,=20LOCK=20HAND/DEL=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _sea_overlay.html: DEAL btn replaced by two .sea-deck-stack--levity/gravity piles; .sea-pos-cover + .sea-pos-cross overlaid on center sig slot; LOCK HAND (disabled) + DEL (.btn-danger) in .sea-form-actions; data-sea-deck-url attr - sea overlay inline JS: _fetchDeck() loads shuffled piles from sea_deck endpoint; stack click → _showOk(); click elsewhere → _hideOk(); OK click → _fillPos() in next spread-order position; DEL → _reset(); LOCK HAND enables at 6 fills - SPREAD_ORDER constants for waite-smith + escape-velocity spread types - sea_deck view: shuffles full equipped deck minus all seated Significators, splits into levity (first half) + gravity (second half) JSON arrays - epic:sea_deck URL registered - sea_partial view: ctx['room'] = room added (fixes NoReverseMatch for sea_deck URL) - _card-deck.scss: .sea-card-slot--filled; .sea-pos-cover/cross absolute overlay; .sea-deck-stack + .sea-stack-face; .sea-form-actions layout; removed old DEAL rule - 9 Sprint B FTs green; 3 Sprint A FTs green; 730 ITs green Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Sonnet 4.6 --- src/apps/epic/urls.py | 1 + src/apps/epic/views.py | 35 +++++ src/static_src/scss/_card-deck.scss | 71 ++++++++- .../gameboard/_partials/_sea_overlay.html | 144 ++++++++++++++++-- 4 files changed, 240 insertions(+), 11 deletions(-) diff --git a/src/apps/epic/urls.py b/src/apps/epic/urls.py index 4e73432..e7e6d42 100644 --- a/src/apps/epic/urls.py +++ b/src/apps/epic/urls.py @@ -28,4 +28,5 @@ urlpatterns = [ path('room//natus/preview', views.natus_preview, name='natus_preview'), path('room//natus/save', views.natus_save, name='natus_save'), path('room//sea/partial', views.sea_partial, name='sea_partial'), + path('room//sea/deck', views.sea_deck, name='sea_deck'), ] diff --git a/src/apps/epic/views.py b/src/apps/epic/views.py index e0e306e..5877d25 100644 --- a/src/apps/epic/views.py +++ b/src/apps/epic/views.py @@ -1108,6 +1108,40 @@ def natus_save(request, room_id): return JsonResponse({'id': char.id, 'confirmed': char.is_confirmed}) +@login_required +def sea_deck(request, room_id): + """Shuffled deck lists (levity + gravity halves) for PICK SEA draw. + + Excludes all Significators already claimed by seated gamers. + Returns {levity: [{id, name, arcana, suit, number, levity_qualifier, + gravity_qualifier}], gravity: [...]} + """ + import random as _random + room = Room.objects.get(id=room_id) + seat = _canonical_user_seat(room, request.user) + if seat is None: + return HttpResponse(status=403) + + deck = seat.deck_variant + if not deck: + return JsonResponse({'levity': [], 'gravity': []}) + + sig_ids = set( + room.table_seats.exclude(significator__isnull=True) + .values_list('significator_id', flat=True) + ) + + available = list( + TarotCard.objects.filter(deck_variant=deck) + .exclude(id__in=sig_ids) + .values('id', 'name', 'arcana', 'suit', 'number', + 'levity_qualifier', 'gravity_qualifier') + ) + _random.shuffle(available) + mid = len(available) // 2 + return JsonResponse({'levity': available[:mid], 'gravity': available[mid:]}) + + @login_required def sea_partial(request, room_id): """Return the rendered sea overlay partial for in-page injection after sky confirm.""" @@ -1115,5 +1149,6 @@ def sea_partial(request, room_id): ctx = _role_select_context(room, request.user) if not ctx.get('sky_confirmed'): return HttpResponse(status=403) + ctx['room'] = room return render(request, 'apps/gameboard/_partials/_sea_overlay.html', ctx) diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index bb24a11..5af2a53 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -799,6 +799,35 @@ $sea-card-h: 6.5rem; height: $sea-card-w; } +.sea-card-slot--filled { + border-style: solid; + border-color: rgba(var(--secUser), 0.6); + background: rgba(var(--priUser), 1); + color: rgba(var(--terUser), 1); + font-size: 0.55rem; + font-weight: 600; + text-align: center; + padding: 0.2rem; + line-height: 1.2; +} + +// Cover + Cross — absolutely overlaid on the Sig card in .sea-pos-center +.sea-pos-center { position: relative; } + +.sea-pos-cover, +.sea-pos-cross { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + + .sea-card-slot { pointer-events: auto; } +} + +.sea-pos-cross .sea-card-slot { transform: rotate(90deg); } + // .sig-stage-card is normally scoped inside .sig-stage — re-apply the card shell // here so it renders correctly outside that context. .sea-cross .sig-stage-card { @@ -867,8 +896,48 @@ $sea-card-h: 6.5rem; option { background: rgba(var(--priUser), 1); } } -.sea-form-col > #id_sea_deal { +// Deck stacks — two face-down piles side by side +.sea-stacks { + display: flex; + gap: 1rem; + justify-content: center; + margin: 1rem 0; +} + +.sea-deck-stack { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.4rem; + cursor: pointer; +} + +.sea-stack-face { + width: $sea-card-w; + height: $sea-card-h; + border-radius: 0.3rem; + background: rgba(var(--duoUser), 0.8); + border: 0.12rem solid rgba(var(--secUser), 0.5); + box-shadow: 0 2px 0 rgba(0,0,0,0.2), 0 4px 0 rgba(var(--duoUser),0.7), 0 5px 0 rgba(0,0,0,0.15); + transition: box-shadow 0.15s; + + .sea-deck-stack:hover & { + box-shadow: 0 2px 0 rgba(0,0,0,0.2), 0 4px 0 rgba(var(--duoUser),0.7), 0 5px 0 rgba(0,0,0,0.15), + 0 0 0.5rem rgba(var(--terUser), 0.4); + } +} + +.sea-deck-stack--levity .sea-stack-face { border-color: rgba(var(--terUser), 0.5); } +.sea-deck-stack--gravity .sea-stack-face { border-color: rgba(var(--quaUser), 0.5); } + +// Form action row — LOCK HAND + DEL side by side at the bottom +.sea-form-actions { + display: flex; + gap: 0.5rem; margin-top: auto; + padding-top: 0.75rem; + } // NVM button — same positioning as .natus-modal-wrap > .btn-cancel diff --git a/src/templates/apps/gameboard/_partials/_sea_overlay.html b/src/templates/apps/gameboard/_partials/_sea_overlay.html index 4c91085..b4c0b85 100644 --- a/src/templates/apps/gameboard/_partials/_sea_overlay.html +++ b/src/templates/apps/gameboard/_partials/_sea_overlay.html @@ -4,7 +4,8 @@ {# Layout is the reverse of PICK SKY: cards left (transparent), form right #}
-
+
@@ -19,15 +20,15 @@ {# ── Cards column (transparent) ───────────────────────────── #}
- {# Crown — position 3 #} + {# Crown — CC pos 3 / EV pos 5 #}
- {# Past — position 4 #} + {# Beneath (past) — CC pos 4 / EV pos 3 #}
- {# Center — Significator (already placed) #} + {# Center — Significator (always placed) + Cover + Cross overlaid #}
{% if my_tray_sig %} @@ -42,16 +43,23 @@
{% endif %}
+ {# Cover — CC/EV pos 1, stacked face-up on Sig #} +
+
+
+ {# Cross — CC/EV pos 2, rotated 90° on Cover #} +
+
+
- {# Future — position 5 #} + {# Before (future) — CC pos 5 / EV pos 6 #}
- {# Root — position 1 #} + {# Behind (root) — CC pos 6 / EV pos 4 #}
- {# Crossing — position 2 (rotated) deferred; re-add once layout is finalized #}
@@ -71,11 +79,28 @@ {% endif %}
+ + {# Two face-down deck piles — tap to proffer OK #} +
+
+
+ +
+
+
+ +
+
- +
+ + +
@@ -107,5 +132,104 @@ if (pickSeaBtn) pickSeaBtn.addEventListener('click', openSea); cancelBtn.addEventListener('click', closeSea); overlay.addEventListener('click', (e) => { if (e.target === overlay) 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-root', '.sea-pos-future', '.sea-pos-past'], + 'escape-velocity': ['.sea-pos-cover', '.sea-pos-cross', '.sea-pos-root', '.sea-pos-past', '.sea-pos-crown', '.sea-pos-future'], + }; + + let levityPile = [], gravityPile = []; + let _filled = 0; + let _activeStack = null; + + const spreadSel = overlay.querySelector('#id_sea_spread'); + const lockBtn = overlay.querySelector('#id_sea_lock_hand'); + const delBtn = overlay.querySelector('#id_sea_del'); + + 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 = null; + } + } + + function _showOk(stack) { + _hideOk(); + _activeStack = stack; + const ok = stack.querySelector('.sea-stack-ok'); + if (ok) ok.style.display = ''; + } + + function _fillPos(sel, card) { + const cell = overlay.querySelector(sel); + if (!cell) return; + const slot = cell.querySelector('.sea-card-slot'); + if (!slot) return; + slot.classList.remove('sea-card-slot--empty'); + slot.classList.add('sea-card-slot--filled'); + slot.dataset.cardId = String(card.id); + slot.textContent = card.name; + _filled++; + if (lockBtn) lockBtn.disabled = (_filled < 6); + } + + function _reset() { + _filled = 0; + _hideOk(); + overlay.querySelectorAll('.sea-card-slot').forEach(s => { + s.classList.remove('sea-card-slot--filled'); + s.classList.add('sea-card-slot--empty'); + s.textContent = ''; + delete s.dataset.cardId; + }); + if (lockBtn) lockBtn.disabled = true; + _fetchDeck(); + } + + function _fetchDeck() { + fetch(SEA_DECK_URL, { credentials: 'same-origin' }) + .then(r => r.json()) + .then(data => { levityPile = data.levity || []; gravityPile = data.gravity || []; }) + .catch(() => {}); + } + + overlay.querySelectorAll('.sea-deck-stack').forEach(stack => { + stack.addEventListener('click', e => { + e.stopPropagation(); + _activeStack === stack ? _hideOk() : _showOk(stack); + }); + const ok = stack.querySelector('.sea-stack-ok'); + if (ok) { + ok.style.display = 'none'; + ok.addEventListener('click', 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) _fillPos(pos, card); + _hideOk(); + }); + } + }); + + overlay.addEventListener('click', _hideOk); + if (delBtn) delBtn.addEventListener('click', _reset); + + _fetchDeck(); })();