From 7fcb6f307cc3ffb855f19093e4f9ab6a2baa4756 Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Sun, 26 Apr 2026 21:30:27 -0400 Subject: [PATCH] =?UTF-8?q?PICK=20SEA:=20modal=20w.=20Celtic=20Cross=20lay?= =?UTF-8?q?out=20+=20spread=20select;=20PICK=20SKY=20swaps=20to=20PICK=20S?= =?UTF-8?q?EA=20after=20sky=20save=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _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 Git commit message Co-Authored-By: Claude Sonnet 4.6 --- src/apps/epic/tests/integrated/test_views.py | 65 +++++- src/apps/epic/views.py | 18 ++ src/static_src/scss/_natus.scss | 190 ++++++++++++++++++ .../gameboard/_partials/_sea_overlay.html | 106 ++++++++++ src/templates/apps/gameboard/room.html | 15 +- 5 files changed, 390 insertions(+), 4 deletions(-) create mode 100644 src/templates/apps/gameboard/_partials/_sea_overlay.html 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 #} + +
+
+ +
+
+ +
+

PICK SEA

+

Choose your spread and draw your Celtic Cross.

+
+ +
+ + {# ── Cards column (transparent) ───────────────────────────── #} +
+
+ {# Crown — position 3 #} +
+
+
+ {# Past — position 4 #} +
+
+
+ {# Center — Significator (already placed) #} +
+
+ {% if my_tray_sig %} + {{ my_tray_sig.name_title }} + {% endif %} +
+
+ {# Future — position 5 #} +
+
+
+ {# Root — position 1 #} +
+
+
+ {# Crossing — position 2 (rotated across center) #} +
+
+
+
+
+ + {# ── Form column (priUser / opaque) ───────────────────────── #} +
+ +
+
+ + +
+
+ + + +
+ +
{# /.sea-modal-body #} + +
{# /.sea-modal #} + + + +
{# /.sea-modal-wrap #} +
{# /.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 %}