PICK SEA Sprint B: deck stacks, OK btn, card draw, LOCK HAND/DEL — TDD
- _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 <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,4 +28,5 @@ urlpatterns = [
|
|||||||
path('room/<uuid:room_id>/natus/preview', views.natus_preview, name='natus_preview'),
|
path('room/<uuid:room_id>/natus/preview', views.natus_preview, name='natus_preview'),
|
||||||
path('room/<uuid:room_id>/natus/save', views.natus_save, name='natus_save'),
|
path('room/<uuid:room_id>/natus/save', views.natus_save, name='natus_save'),
|
||||||
path('room/<uuid:room_id>/sea/partial', views.sea_partial, name='sea_partial'),
|
path('room/<uuid:room_id>/sea/partial', views.sea_partial, name='sea_partial'),
|
||||||
|
path('room/<uuid:room_id>/sea/deck', views.sea_deck, name='sea_deck'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1108,6 +1108,40 @@ def natus_save(request, room_id):
|
|||||||
return JsonResponse({'id': char.id, 'confirmed': char.is_confirmed})
|
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
|
@login_required
|
||||||
def sea_partial(request, room_id):
|
def sea_partial(request, room_id):
|
||||||
"""Return the rendered sea overlay partial for in-page injection after sky confirm."""
|
"""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)
|
ctx = _role_select_context(room, request.user)
|
||||||
if not ctx.get('sky_confirmed'):
|
if not ctx.get('sky_confirmed'):
|
||||||
return HttpResponse(status=403)
|
return HttpResponse(status=403)
|
||||||
|
ctx['room'] = room
|
||||||
return render(request, 'apps/gameboard/_partials/_sea_overlay.html', ctx)
|
return render(request, 'apps/gameboard/_partials/_sea_overlay.html', ctx)
|
||||||
|
|
||||||
|
|||||||
@@ -799,6 +799,35 @@ $sea-card-h: 6.5rem;
|
|||||||
height: $sea-card-w;
|
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
|
// .sig-stage-card is normally scoped inside .sig-stage — re-apply the card shell
|
||||||
// here so it renders correctly outside that context.
|
// here so it renders correctly outside that context.
|
||||||
.sea-cross .sig-stage-card {
|
.sea-cross .sig-stage-card {
|
||||||
@@ -867,8 +896,48 @@ $sea-card-h: 6.5rem;
|
|||||||
option { background: rgba(var(--priUser), 1); }
|
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;
|
margin-top: auto;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NVM button — same positioning as .natus-modal-wrap > .btn-cancel
|
// NVM button — same positioning as .natus-modal-wrap > .btn-cancel
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
{# Layout is the reverse of PICK SKY: cards left (transparent), form right #}
|
{# Layout is the reverse of PICK SKY: cards left (transparent), form right #}
|
||||||
|
|
||||||
<div class="sea-backdrop"></div>
|
<div class="sea-backdrop"></div>
|
||||||
<div class="sea-overlay" id="id_sea_overlay">
|
<div class="sea-overlay" id="id_sea_overlay"
|
||||||
|
data-sea-deck-url="{% url 'epic:sea_deck' room.id %}">
|
||||||
|
|
||||||
<div class="sea-modal-wrap">
|
<div class="sea-modal-wrap">
|
||||||
<div class="sea-modal">
|
<div class="sea-modal">
|
||||||
@@ -19,15 +20,15 @@
|
|||||||
{# ── Cards column (transparent) ───────────────────────────── #}
|
{# ── Cards column (transparent) ───────────────────────────── #}
|
||||||
<div class="sea-cards-col">
|
<div class="sea-cards-col">
|
||||||
<div class="sea-cross">
|
<div class="sea-cross">
|
||||||
{# Crown — position 3 #}
|
{# Crown — CC pos 3 / EV pos 5 #}
|
||||||
<div class="sea-cross-cell sea-pos-crown">
|
<div class="sea-cross-cell sea-pos-crown">
|
||||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
</div>
|
</div>
|
||||||
{# Past — position 4 #}
|
{# Beneath (past) — CC pos 4 / EV pos 3 #}
|
||||||
<div class="sea-cross-cell sea-pos-past">
|
<div class="sea-cross-cell sea-pos-past">
|
||||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
</div>
|
</div>
|
||||||
{# Center — Significator (already placed) #}
|
{# Center — Significator (always placed) + Cover + Cross overlaid #}
|
||||||
<div class="sea-cross-cell sea-pos-center">
|
<div class="sea-cross-cell sea-pos-center">
|
||||||
<div class="sig-stage-card" style="--sig-card-w: 4rem">
|
<div class="sig-stage-card" style="--sig-card-w: 4rem">
|
||||||
{% if my_tray_sig %}
|
{% if my_tray_sig %}
|
||||||
@@ -42,16 +43,23 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{# Future — position 5 #}
|
{# Before (future) — CC pos 5 / EV pos 6 #}
|
||||||
<div class="sea-cross-cell sea-pos-future">
|
<div class="sea-cross-cell sea-pos-future">
|
||||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
</div>
|
</div>
|
||||||
{# Root — position 1 #}
|
{# Behind (root) — CC pos 6 / EV pos 4 #}
|
||||||
<div class="sea-cross-cell sea-pos-root">
|
<div class="sea-cross-cell sea-pos-root">
|
||||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
</div>
|
</div>
|
||||||
{# Crossing — position 2 (rotated) deferred; re-add once layout is finalized #}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -71,11 +79,28 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Two face-down deck piles — tap to proffer OK #}
|
||||||
|
<div class="sea-stacks">
|
||||||
|
<div class="sea-deck-stack sea-deck-stack--levity">
|
||||||
|
<div class="sea-stack-face"></div>
|
||||||
|
<button class="btn btn-confirm sea-stack-ok" type="button">OK</button>
|
||||||
|
</div>
|
||||||
|
<div class="sea-deck-stack sea-deck-stack--gravity">
|
||||||
|
<div class="sea-stack-face"></div>
|
||||||
|
<button class="btn btn-confirm sea-stack-ok" type="button">OK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" id="id_sea_deal" class="btn btn-primary" disabled>
|
<div class="sea-form-actions">
|
||||||
Deal
|
<button type="button" id="id_sea_lock_hand" class="btn btn-primary" disabled>
|
||||||
</button>
|
LOCK HAND
|
||||||
|
</button>
|
||||||
|
<button type="button" id="id_sea_del" class="btn btn-danger">
|
||||||
|
DEL
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -107,5 +132,104 @@
|
|||||||
if (pickSeaBtn) pickSeaBtn.addEventListener('click', openSea);
|
if (pickSeaBtn) pickSeaBtn.addEventListener('click', openSea);
|
||||||
cancelBtn.addEventListener('click', closeSea);
|
cancelBtn.addEventListener('click', closeSea);
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) 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();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user