PICK SEA: modal w. Celtic Cross layout + spread select; PICK SKY swaps to PICK SEA after sky save — TDD
- _role_select_context: at SKY_SELECT, compute sky_confirmed (confirmed Character exists for seat) + user_polarity - room.html: PICK SEA btn + _sea_overlay.html when sky_confirmed; PICK SKY + natus overlay otherwise - _sea_overlay.html: transparent cards col (6-position cross, Sig at center) left; priUser form col (spread select) right; NVM cancel; JS open/close via html.sea-open - _natus.scss: .sea-* rules mirror natus layout w. reversed columns; crossing slot rotated; dotted empty slots; sig slot solid; width/max-width replaces min() to avoid rem+vw unit mix - select defaults: "Celtic Cross, Waite-Smith" for levity (PC/NC/SC); "Celtic Cross, Escape Velocity" for gravity (EC/AC/BC) 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:
@@ -8,7 +8,7 @@ from django.utils import timezone
|
|||||||
from apps.drama.models import GameEvent
|
from apps.drama.models import GameEvent
|
||||||
from apps.lyric.models import Token, User
|
from apps.lyric.models import Token, User
|
||||||
from apps.epic.models import (
|
from apps.epic.models import (
|
||||||
DeckVariant, GateSlot, Room, RoomInvite, SigReservation, TableSeat, TarotCard,
|
Character, DeckVariant, GateSlot, Room, RoomInvite, SigReservation, TableSeat, TarotCard,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1652,6 +1652,69 @@ class PickSkyRenderingTest(TestCase):
|
|||||||
self.assertContains(response, 'style="display:none"')
|
self.assertContains(response, 'style="display:none"')
|
||||||
|
|
||||||
|
|
||||||
|
# ── SEA_SELECT rendering ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class PickSeaRenderingTest(TestCase):
|
||||||
|
"""At SKY_SELECT, a confirmed Character swaps PICK SKY → PICK SEA + sea overlay."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self)
|
||||||
|
self.room.table_status = Room.SKY_SELECT
|
||||||
|
self.room.save()
|
||||||
|
self.sig_card = TarotCard.objects.get(
|
||||||
|
deck_variant=self.earthman, arcana="MIDDLE", suit="BRANDS", number=11
|
||||||
|
)
|
||||||
|
self.pc_seat = TableSeat.objects.get(room=self.room, role="PC")
|
||||||
|
self.pc_seat.significator = self.sig_card
|
||||||
|
self.pc_seat.save()
|
||||||
|
self.url = reverse("epic:room", kwargs={"room_id": self.room.id})
|
||||||
|
|
||||||
|
def _confirm_sky(self, seat=None):
|
||||||
|
target = seat or self.pc_seat
|
||||||
|
return Character.objects.create(seat=target, confirmed_at=timezone.now())
|
||||||
|
|
||||||
|
def test_sky_confirmed_false_when_no_character(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertFalse(response.context["sky_confirmed"])
|
||||||
|
|
||||||
|
def test_sky_confirmed_true_when_character_confirmed(self):
|
||||||
|
self._confirm_sky()
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertTrue(response.context["sky_confirmed"])
|
||||||
|
|
||||||
|
def test_pick_sea_btn_shown_when_sky_confirmed(self):
|
||||||
|
self._confirm_sky()
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertContains(response, "id_pick_sea_btn")
|
||||||
|
|
||||||
|
def test_pick_sky_btn_shown_when_sky_not_confirmed(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertContains(response, "id_pick_sky_btn")
|
||||||
|
|
||||||
|
def test_sea_overlay_included_when_sky_confirmed(self):
|
||||||
|
self._confirm_sky()
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertContains(response, "id_sea_overlay")
|
||||||
|
|
||||||
|
def test_sea_overlay_select_defaults_to_waite_smith_for_levity(self):
|
||||||
|
self._confirm_sky()
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertContains(response, "Celtic Cross, Waite-Smith")
|
||||||
|
|
||||||
|
def test_sea_overlay_select_defaults_to_escape_velocity_for_gravity(self):
|
||||||
|
ec_gamer = self.gamers[2] # EC — gravity
|
||||||
|
self.client.force_login(ec_gamer)
|
||||||
|
ec_seat = TableSeat.objects.get(room=self.room, role="EC")
|
||||||
|
self._confirm_sky(seat=ec_seat)
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertContains(response, "Celtic Cross, Escape Velocity")
|
||||||
|
|
||||||
|
def test_user_polarity_in_context_at_sky_select(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertIn("user_polarity", response.context)
|
||||||
|
self.assertEqual(response.context["user_polarity"], "levity") # PC is levity
|
||||||
|
|
||||||
|
|
||||||
# ── select_role GET redirect ──────────────────────────────────────────────────
|
# ── select_role GET redirect ──────────────────────────────────────────────────
|
||||||
|
|
||||||
class SelectRoleGetRedirectTest(TestCase):
|
class SelectRoleGetRedirectTest(TestCase):
|
||||||
|
|||||||
@@ -342,6 +342,24 @@ def _role_select_context(room, user):
|
|||||||
ctx["sig_cards"] = gravity_sig_cards(room)
|
ctx["sig_cards"] = gravity_sig_cards(room)
|
||||||
else:
|
else:
|
||||||
ctx["sig_cards"] = []
|
ctx["sig_cards"] = []
|
||||||
|
|
||||||
|
if room.table_status == Room.SKY_SELECT:
|
||||||
|
user_role = _canonical_seat.role if _canonical_seat else None
|
||||||
|
user_polarity = None
|
||||||
|
if user_role in _LEVITY_ROLES:
|
||||||
|
user_polarity = 'levity'
|
||||||
|
elif user_role in _GRAVITY_ROLES:
|
||||||
|
user_polarity = 'gravity'
|
||||||
|
ctx["user_polarity"] = user_polarity
|
||||||
|
sky_confirmed = bool(
|
||||||
|
_canonical_seat and Character.objects.filter(
|
||||||
|
seat=_canonical_seat,
|
||||||
|
confirmed_at__isnull=False,
|
||||||
|
retired_at__isnull=True,
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
ctx["sky_confirmed"] = sky_confirmed
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -934,3 +934,193 @@ body.page-sky {
|
|||||||
z-index: 90;
|
z-index: 90;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── PICK SEA overlay ─────────────────────────────────────────────────────────
|
||||||
|
// Mirrors .natus-* structure but with columns reversed:
|
||||||
|
// left = transparent (Celtic Cross card positions)
|
||||||
|
// right = rgba(--priUser) opaque (spread select)
|
||||||
|
|
||||||
|
.sea-backdrop {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.sea-open .sea-backdrop { display: block; }
|
||||||
|
|
||||||
|
.sea-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 201;
|
||||||
|
overflow-y: auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.sea-open .sea-overlay { display: flex; }
|
||||||
|
|
||||||
|
.sea-modal-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 60rem;
|
||||||
|
max-height: 90vh;
|
||||||
|
margin: auto;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(1.5rem);
|
||||||
|
transition: opacity 0.25s, transform 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.sea-open .sea-modal-wrap {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-modal {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-modal-header {
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
background: rgba(var(--priUser), 1);
|
||||||
|
|
||||||
|
h2 { font-size: 1.4rem; margin: 0; }
|
||||||
|
p { margin: 0.2rem 0 0; font-size: 0.85rem; opacity: 0.8; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-modal-body {
|
||||||
|
display: flex;
|
||||||
|
min-height: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cards column (transparent / left) ────────────────────────────────────────
|
||||||
|
|
||||||
|
.sea-cards-col {
|
||||||
|
flex: 1 1 55%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-cross {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
". crown . "
|
||||||
|
"past center future "
|
||||||
|
". root . "
|
||||||
|
". crossing . ";
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
grid-template-rows: auto auto auto auto;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-cross-cell { display: flex; align-items: center; justify-content: center; }
|
||||||
|
.sea-pos-crown { grid-area: crown; }
|
||||||
|
.sea-pos-past { grid-area: past; }
|
||||||
|
.sea-pos-center { grid-area: center; }
|
||||||
|
.sea-pos-future { grid-area: future; }
|
||||||
|
.sea-pos-root { grid-area: root; }
|
||||||
|
.sea-pos-crossing { grid-area: crossing; }
|
||||||
|
|
||||||
|
$sea-card-w: 4rem;
|
||||||
|
$sea-card-h: 6.5rem;
|
||||||
|
|
||||||
|
.sea-card-slot {
|
||||||
|
width: $sea-card-w;
|
||||||
|
height: $sea-card-h;
|
||||||
|
border: 0.15rem dashed rgba(var(--terUser), 0.45);
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: rgba(var(--terUser), 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-card-slot--sig {
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgba(var(--priUser), 0.9);
|
||||||
|
background: rgba(var(--priUser), 0.15);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.25rem;
|
||||||
|
color: rgba(var(--terUser), 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-card-slot--crossing {
|
||||||
|
width: $sea-card-h; // rotated — swap w/h
|
||||||
|
height: $sea-card-w;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-sig-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Form column (priUser / opaque / right) ────────────────────────────────────
|
||||||
|
|
||||||
|
.sea-form-col {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 16rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1.25rem;
|
||||||
|
background: rgba(var(--priUser), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-form-main {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-select {
|
||||||
|
background: rgba(var(--duoUser), 0.6);
|
||||||
|
border: 1px solid rgba(var(--terUser), 0.3);
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0.4rem 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
option { background: rgba(var(--priUser), 1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-form-col > #id_sea_deal {
|
||||||
|
margin-top: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NVM button — same positioning as .natus-modal-wrap > .btn-cancel
|
||||||
|
.sea-modal-wrap > .btn-cancel {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.75rem;
|
||||||
|
right: -0.75rem;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
html.sea-open body .container .navbar,
|
||||||
|
html.sea-open body #id_footer {
|
||||||
|
z-index: 90;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
106
src/templates/apps/gameboard/_partials/_sea_overlay.html
Normal file
106
src/templates/apps/gameboard/_partials/_sea_overlay.html
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{% load static %}
|
||||||
|
{# PICK SEA overlay — Celtic Cross spread entry #}
|
||||||
|
{# Included in room.html when table_status == "SKY_SELECT" and sky_confirmed #}
|
||||||
|
{# Layout is the reverse of PICK SKY: cards left (transparent), form right #}
|
||||||
|
|
||||||
|
<div class="sea-backdrop"></div>
|
||||||
|
<div class="sea-overlay" id="id_sea_overlay">
|
||||||
|
|
||||||
|
<div class="sea-modal-wrap">
|
||||||
|
<div class="sea-modal">
|
||||||
|
|
||||||
|
<header class="sea-modal-header">
|
||||||
|
<h2>PICK <span>SEA</span></h2>
|
||||||
|
<p>Choose your spread and draw your Celtic Cross.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="sea-modal-body">
|
||||||
|
|
||||||
|
{# ── Cards column (transparent) ───────────────────────────── #}
|
||||||
|
<div class="sea-cards-col">
|
||||||
|
<div class="sea-cross">
|
||||||
|
{# Crown — position 3 #}
|
||||||
|
<div class="sea-cross-cell sea-pos-crown">
|
||||||
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
|
</div>
|
||||||
|
{# Past — position 4 #}
|
||||||
|
<div class="sea-cross-cell sea-pos-past">
|
||||||
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
|
</div>
|
||||||
|
{# Center — Significator (already placed) #}
|
||||||
|
<div class="sea-cross-cell sea-pos-center">
|
||||||
|
<div class="sea-card-slot sea-card-slot--sig">
|
||||||
|
{% if my_tray_sig %}
|
||||||
|
<span class="sea-sig-name">{{ my_tray_sig.name_title }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{# Future — position 5 #}
|
||||||
|
<div class="sea-cross-cell sea-pos-future">
|
||||||
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
|
</div>
|
||||||
|
{# Root — position 1 #}
|
||||||
|
<div class="sea-cross-cell sea-pos-root">
|
||||||
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
|
</div>
|
||||||
|
{# Crossing — position 2 (rotated across center) #}
|
||||||
|
<div class="sea-cross-cell sea-pos-crossing">
|
||||||
|
<div class="sea-card-slot sea-card-slot--empty sea-card-slot--crossing"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ── Form column (priUser / opaque) ───────────────────────── #}
|
||||||
|
<div class="sea-form-col">
|
||||||
|
|
||||||
|
<div class="sea-form-main">
|
||||||
|
<div class="sea-field">
|
||||||
|
<label for="id_sea_spread">Spread</label>
|
||||||
|
<select id="id_sea_spread" name="spread" class="sea-select">
|
||||||
|
{% if user_polarity == "levity" %}
|
||||||
|
<option value="waite-smith" selected>Celtic Cross, Waite-Smith</option>
|
||||||
|
<option value="escape-velocity">Celtic Cross, Escape Velocity</option>
|
||||||
|
{% else %}
|
||||||
|
<option value="escape-velocity" selected>Celtic Cross, Escape Velocity</option>
|
||||||
|
<option value="waite-smith">Celtic Cross, Waite-Smith</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" id="id_sea_deal" class="btn btn-primary" disabled>
|
||||||
|
Deal
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</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 #}
|
||||||
|
</div>{# /.sea-overlay #}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const overlay = document.getElementById('id_sea_overlay');
|
||||||
|
const cancelBtn = document.getElementById('id_sea_cancel');
|
||||||
|
|
||||||
|
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);
|
||||||
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeSea(); });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -35,7 +35,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if room.table_status == "SKY_SELECT" %}
|
{% if room.table_status == "SKY_SELECT" %}
|
||||||
|
{% if sky_confirmed %}
|
||||||
|
<button id="id_pick_sea_btn" class="btn btn-primary">PICK<br>SEA</button>
|
||||||
|
{% else %}
|
||||||
<button id="id_pick_sky_btn" class="btn btn-primary">PICK<br>SKY</button>
|
<button id="id_pick_sky_btn" class="btn btn-primary">PICK<br>SKY</button>
|
||||||
|
{% endif %}
|
||||||
{% elif room.table_status == "SIG_SELECT" %}
|
{% elif room.table_status == "SIG_SELECT" %}
|
||||||
<button id="id_pick_sky_btn" class="btn btn-primary" style="display:none">PICK<br>SKY</button>
|
<button id="id_pick_sky_btn" class="btn btn-primary" style="display:none">PICK<br>SKY</button>
|
||||||
{% if polarity_done %}
|
{% if polarity_done %}
|
||||||
@@ -68,15 +72,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Natus (Pick Sky) overlay — natal chart entry #}
|
{# Natus (Pick Sky) overlay — natal chart entry #}
|
||||||
{% if room.table_status == "SKY_SELECT" %}
|
{% if room.table_status == "SKY_SELECT" and not sky_confirmed %}
|
||||||
{% include "apps/gameboard/_partials/_natus_overlay.html" %}
|
{% include "apps/gameboard/_partials/_natus_overlay.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# Natus tooltip: sibling of .natus-overlay, not inside .natus-modal-wrap (which has transform) #}
|
{# Natus tooltip: sibling of .natus-overlay, not inside .natus-modal-wrap (which has transform) #}
|
||||||
{% if room.table_status == "SKY_SELECT" %}
|
{% if room.table_status == "SKY_SELECT" and not sky_confirmed %}
|
||||||
<div id="id_natus_tooltip" class="tt" style="display:none;"></div>
|
<div id="id_natus_tooltip" class="tt" style="display:none;"></div>
|
||||||
<div id="id_natus_tooltip_2" class="tt" style="display:none;"></div>
|
<div id="id_natus_tooltip_2" class="tt" style="display:none;"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{# Sea (Pick Sea) overlay — Celtic Cross spread entry #}
|
||||||
|
{% if room.table_status == "SKY_SELECT" and sky_confirmed %}
|
||||||
|
{% include "apps/gameboard/_partials/_sea_overlay.html" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if room.gate_status != "RENEWAL_DUE" and room.table_status != "SIG_SELECT" %}
|
{% if room.gate_status != "RENEWAL_DUE" and room.table_status != "SIG_SELECT" %}
|
||||||
{% include "apps/gameboard/_partials/_table_positions.html" %}
|
{% include "apps/gameboard/_partials/_table_positions.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user