diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py index 3abc03c..588327b 100644 --- a/src/apps/epic/tests/integrated/test_views.py +++ b/src/apps/epic/tests/integrated/test_views.py @@ -8,7 +8,7 @@ from django.utils import timezone from apps.drama.models import GameEvent from apps.lyric.models import Token, User 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"') +# ── 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 ────────────────────────────────────────────────── class SelectRoleGetRedirectTest(TestCase): diff --git a/src/apps/epic/views.py b/src/apps/epic/views.py index 335ad5f..9fd2e92 100644 --- a/src/apps/epic/views.py +++ b/src/apps/epic/views.py @@ -342,6 +342,24 @@ def _role_select_context(room, user): ctx["sig_cards"] = gravity_sig_cards(room) else: 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 diff --git a/src/static_src/scss/_natus.scss b/src/static_src/scss/_natus.scss index b527443..4392fc0 100644 --- a/src/static_src/scss/_natus.scss +++ b/src/static_src/scss/_natus.scss @@ -934,3 +934,193 @@ body.page-sky { 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; + } +} diff --git a/src/templates/apps/gameboard/_partials/_sea_overlay.html b/src/templates/apps/gameboard/_partials/_sea_overlay.html new file mode 100644 index 0000000..4a58ce4 --- /dev/null +++ b/src/templates/apps/gameboard/_partials/_sea_overlay.html @@ -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 #} + +
+{# /.sea-overlay #} + + diff --git a/src/templates/apps/gameboard/room.html b/src/templates/apps/gameboard/room.html index 3722b6e..0c70677 100644 --- a/src/templates/apps/gameboard/room.html +++ b/src/templates/apps/gameboard/room.html @@ -35,7 +35,11 @@ {% endif %} {% endif %} {% if room.table_status == "SKY_SELECT" %} - + {% if sky_confirmed %} + + {% else %} + + {% endif %} {% elif room.table_status == "SIG_SELECT" %} {% if polarity_done %} @@ -68,15 +72,20 @@ {% endif %} {# 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" %} {% endif %} {# 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 %} {% 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" %} {% include "apps/gameboard/_partials/_table_positions.html" %} {% endif %}