diff --git a/src/apps/epic/models.py b/src/apps/epic/models.py index 169db4d..6a450ab 100644 --- a/src/apps/epic/models.py +++ b/src/apps/epic/models.py @@ -624,7 +624,7 @@ class Character(models.Model): """A gamer's player-character for one seat in one game session. Lifecycle: - - Created (draft) when gamer opens PICK SKY overlay. + - Created (draft) when gamer opens CAST SKY overlay. - confirmed_at set on confirm → locked. - retired_at set on retirement → archived (seat may hold a new Character). @@ -647,7 +647,7 @@ class Character(models.Model): TableSeat, on_delete=models.CASCADE, related_name='characters', ) - # ── significator (set at PICK SKY) ──────────────────────────────────── + # ── significator (set at CAST SKY) ──────────────────────────────────── significator = models.ForeignKey( TarotCard, null=True, blank=True, on_delete=models.SET_NULL, related_name='character_significators', @@ -665,7 +665,7 @@ class Character(models.Model): # ── computed sky snapshot (full PySwiss response) ─────────────────── chart_data = models.JSONField(null=True, blank=True) - # ── celtic cross spread (added at PICK SEA) ─────────────────────────── + # ── celtic cross spread (added at DRAW SEA) ─────────────────────────── celtic_cross = models.JSONField(null=True, blank=True) # ── lifecycle ───────────────────────────────────────────────────────── diff --git a/src/apps/epic/static/apps/epic/sig-select.js b/src/apps/epic/static/apps/epic/sig-select.js index 2d5490d..94d222e 100644 --- a/src/apps/epic/static/apps/epic/sig-select.js +++ b/src/apps/epic/static/apps/epic/sig-select.js @@ -317,7 +317,7 @@ var SigSelect = (function () { }); } - // ── TAKE SIG / WAIT NVM button ───────────────────────────────────────── + // ── SAVE SIG / WAIT NVM button ───────────────────────────────────────── function _onTakeSigClick() { if (_isReady) { @@ -337,7 +337,7 @@ var SigSelect = (function () { _countdownTimer = null; if (_takeSigBtn) _takeSigBtn.style.fontSize = ''; } - if (_takeSigBtn) _takeSigBtn.textContent = 'TAKE SIG'; + if (_takeSigBtn) _takeSigBtn.textContent = 'SAVE SIG'; _stopWaitNoGlow(); _stopCountdownGlow(); } @@ -367,7 +367,7 @@ var SigSelect = (function () { _takeSigBtn.id = 'id_take_sig_btn'; _takeSigBtn.className = 'btn btn-primary sig-take-sig-btn'; _takeSigBtn.type = 'button'; - _takeSigBtn.textContent = 'TAKE SIG'; + _takeSigBtn.textContent = 'SAVE SIG'; _takeSigBtn.addEventListener('click', _onTakeSigClick); stage.appendChild(_takeSigBtn); } @@ -495,7 +495,7 @@ var SigSelect = (function () { function _showWaitingMsg(pendingPolarity) { if (document.getElementById('id_hex_waiting_msg')) return; // If the OTHER polarity finished before our tray sequence completed, - // pick_sky_available will already have fired and revealed PICK SKY. + // pick_sky_available will already have fired and revealed CAST SKY. // In that case skip the waiting msg so the two don't co-exist. var pickSkyBtn = document.getElementById('id_pick_sky_btn'); if (pickSkyBtn && pickSkyBtn.style.display !== 'none') return; @@ -539,7 +539,7 @@ var SigSelect = (function () { // the overlay vanishes; overlay dismissal + waiting msg run last. // User sees: stage card → tray slides in → sig fades into the tray // cell → tray slides out → 2s pause → overlay dismisses → table hex - // w. waiting msg (or PICK SKY btn if both polarities are done). + // w. waiting msg (or CAST SKY btn if both polarities are done). function _settle() { _dismissSigOverlay(); _showWaitingMsg(pendingPolarity); @@ -625,7 +625,7 @@ var SigSelect = (function () { userRole = overlay.dataset.userRole; userPolarity= overlay.dataset.polarity; - // PICK SKY btn is rendered hidden during SIG_SELECT; reveal on pick_sky_available + // CAST SKY btn is rendered hidden during SIG_SELECT; reveal on pick_sky_available var pickSkyBtn = document.getElementById('id_pick_sky_btn'); if (pickSkyBtn) { pickSkyBtn.addEventListener('click', function () { diff --git a/src/apps/epic/static/apps/epic/stage-card.js b/src/apps/epic/static/apps/epic/stage-card.js index a3b5ca8..682928d 100644 --- a/src/apps/epic/static/apps/epic/stage-card.js +++ b/src/apps/epic/static/apps/epic/stage-card.js @@ -143,26 +143,42 @@ var StageCard = (function () { if (qBelow) qBelow.textContent = isMajor ? qualifier : ''; } - // Reversal face — four cases: - // Polarity-split: full reversal title in qualifier slot (top-after-spin), name slot empty - // Major: title (with comma) in qualifier slot, qualifier in name slot - // Non-major + reversal_qual: reversal_qualifier in qualifier slot, title in name slot - // Non-major no reversal_qual: fall back to current polarity's qualifier - var rQual = stageCard.querySelector('.fan-card-reversal-qualifier'); - var rName = stageCard.querySelector('.fan-card-reversal-name'); - if (rQual && rName) { + // Reversal face — class always matches semantic content: + // .fan-card-reversal-name → title-like text + // .fan-card-reversal-qualifier → qualifier-like text + // DOM order controls visual layout after the 180° SPIN (DOM-second appears + // visually on top). Sig + sea skeletons render a fixed slot pair so we + // assign classes per-arcana here so each branch lands in the right slot: + // Major / polarity-split — title on top → .name carried by DOM-second + // Non-major — qualifier on top → .qualifier carried by DOM-second + var slots = stageCard.querySelectorAll('.fan-card-face-reversal > p'); + if (slots.length === 2) { + var bottomEl = slots[0]; // DOM-first → visually bottom after spin + var topEl = slots[1]; // DOM-second → visually top after spin + // Wipe any squeeze-class artifacts from a prior populate call before + // re-stamping the base class — keeps the slot's CSS predictable. + bottomEl.className = ''; + topEl.className = ''; + if (reversalOverride) { - _setTitle(rQual, reversalOverride, card); - rName.textContent = ''; + // Polarity-split: single-line title on TOP; qualifier slot empty. + topEl.className = 'fan-card-reversal-name'; + _setTitle(topEl, reversalOverride, card); + bottomEl.className = 'fan-card-reversal-qualifier'; + bottomEl.textContent = ''; } else if (isMajor) { - _setTitle(rQual, title + ',', card); - rName.textContent = qualifier; - } else if (reversalQualifier) { - rQual.textContent = reversalQualifier; - _setTitle(rName, title, card); + // Major: title-with-comma on TOP, qualifier on BOTTOM. + topEl.className = 'fan-card-reversal-name'; + _setTitle(topEl, title + ',', card); + bottomEl.className = 'fan-card-reversal-qualifier'; + bottomEl.textContent = qualifier; } else { - rQual.textContent = qualifier; - _setTitle(rName, title, card); + // Non-major: qualifier on TOP, title on BOTTOM (inverted from + // upright order by design — qualifier word reads first after spin). + topEl.className = 'fan-card-reversal-qualifier'; + topEl.textContent = reversalQualifier || qualifier; + bottomEl.className = 'fan-card-reversal-name'; + _setTitle(bottomEl, title, card); } } } diff --git a/src/apps/epic/tasks.py b/src/apps/epic/tasks.py index 9281ec7..8268f37 100644 --- a/src/apps/epic/tasks.py +++ b/src/apps/epic/tasks.py @@ -1,5 +1,5 @@ """ -Countdown scheduler for the polarity-room TAKE SIG gate. +Countdown scheduler for the polarity-room SAVE SIG gate. Uses threading.Timer so no separate Celery worker is needed in development. Single-process only — swap for a Celery task if production uses multiple diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py index 5af5660..d41a46e 100644 --- a/src/apps/epic/tests/integrated/test_views.py +++ b/src/apps/epic/tests/integrated/test_views.py @@ -879,7 +879,7 @@ class SelectRoleMultiSeatTest(TestCase): class RoomViewAllRolesFilledTest(TestCase): - """Room view in ROLE_SELECT with all seats assigned shows PICK SIGS button.""" + """Room view in ROLE_SELECT with all seats assigned shows SCAN SIGS button.""" def setUp(self): import lxml.html self.lxml = lxml.html @@ -1785,7 +1785,7 @@ class SigConfirmViewTest(TestCase): # ── SKY_SELECT rendering ────────────────────────────────────────────────────── class PickSkyRenderingTest(TestCase): - """Room page at SKY_SELECT renders PICK SKY btn and sig card in tray cell 2.""" + """Room page at SKY_SELECT renders CAST SKY btn and sig card in tray cell 2.""" def setUp(self): self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self) @@ -1865,7 +1865,7 @@ class PickSkyRenderingTest(TestCase): self.assertEqual(founder.sky_birth_tz, "America/New_York") def test_no_sky_delete_btn_in_blank_sky_select_modal(self): - """A fresh PICK SKY modal (no preview wheel rendered yet) must not + """A fresh CAST SKY modal (no preview wheel rendered yet) must not carry the DEL btn — it would otherwise float in the empty wheel area suggesting there's something to delete when the user has only seen the form. The JS schedulePreview success handler is the contract that @@ -1881,7 +1881,7 @@ class PickSkyRenderingTest(TestCase): # ── SEA_SELECT rendering ────────────────────────────────────────────────────── class PickSeaRenderingTest(TestCase): - """At SKY_SELECT, a confirmed Character swaps PICK SKY → PICK SEA + sea overlay.""" + """At SKY_SELECT, a confirmed Character swaps CAST SKY → DRAW SEA + sea overlay.""" def setUp(self): self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self) diff --git a/src/apps/epic/utils.py b/src/apps/epic/utils.py index aa92979..1f9c279 100644 --- a/src/apps/epic/utils.py +++ b/src/apps/epic/utils.py @@ -5,7 +5,7 @@ from apps.epic.models import Room, RoomInvite # ── Game-wide constants ──────────────────────────────────────────────────── # Reversal probability applied to any card pulled from a stack, anywhere in -# the game (PICK SEA initially; future phases — gameplay draws etc. — will +# the game (DRAW SEA initially; future phases — gameplay draws etc. — will # share this single source of truth). Stub for a future per-user profile # override: callers MUST go through stack_reversal_probability(user, room) # rather than referencing the constant directly so the user-config hookup is diff --git a/src/apps/epic/views.py b/src/apps/epic/views.py index 652b2d4..337f015 100644 --- a/src/apps/epic/views.py +++ b/src/apps/epic/views.py @@ -413,7 +413,7 @@ def room_view(request, room_id): ctx = _role_select_context(room, request.user) ctx["room"] = room ctx["page_class"] = "page-gameboard" - # Reversal-rate hint label under PICK SEA's SPREAD select — same helper as + # Reversal-rate hint label under DRAW SEA's SPREAD select — same helper as # sea_partial so the value tracks any future per-user override automatically. ctx["stack_reversal_pct"] = int(round(stack_reversal_probability(request.user, room) * 100)) return render(request, "apps/gameboard/room.html", ctx) @@ -1223,7 +1223,7 @@ def sky_save(request, room_id): @login_required def sky_delete(request, room_id): """Purge the requesting gamer's Character on this seat — both unconfirmed - drafts AND confirmed rows. The in-room PICK SKY DEL targets this so SAVE + drafts AND confirmed rows. The in-room CAST SKY DEL targets this so SAVE SKY → DEL → refresh truly drops the saved sky for the seat. The User model's sky_chart_data is intentionally untouched (Dashsky / My Sky applet's DEL handles that separately).""" @@ -1239,7 +1239,7 @@ def sky_delete(request, room_id): @login_required def sea_deck(request, room_id): - """Shuffled deck lists (levity + gravity halves) for PICK SEA draw. + """Shuffled deck lists (levity + gravity halves) for DRAW SEA draw. Excludes all Significators already claimed by seated gamers. Returns {levity: [{id, name, arcana, suit, number, levity_qualifier, diff --git a/src/functional_tests/management/commands/setup_sea_session.py b/src/functional_tests/management/commands/setup_sea_session.py index ed1bf08..87168db 100644 --- a/src/functional_tests/management/commands/setup_sea_session.py +++ b/src/functional_tests/management/commands/setup_sea_session.py @@ -1,8 +1,8 @@ """ -Management command for manual single-gamer PICK SEA testing. +Management command for manual single-gamer DRAW SEA testing. Creates a room at SKY_SELECT with one seated gamer whose sky is already -confirmed, so the PICK SEA overlay is immediately visible on page load. +confirmed, so the DRAW SEA overlay is immediately visible on page load. Usage: python src/manage.py setup_sea_session @@ -32,7 +32,7 @@ def _make_session(user): class Command(BaseCommand): - help = "Set up a SKY_SELECT room with sky confirmed and print a PICK SEA URL" + help = "Set up a SKY_SELECT room with sky confirmed and print a DRAW SEA URL" def add_arguments(self, parser): parser.add_argument("--base-url", default="http://localhost:8000") diff --git a/src/functional_tests/test_billboard.py b/src/functional_tests/test_billboard.py index a93b98e..dc55353 100644 --- a/src/functional_tests/test_billboard.py +++ b/src/functional_tests/test_billboard.py @@ -453,7 +453,7 @@ class BillscrollGearMenuTest(FunctionalTest): FT: the billscroll page has a gear menu that filters events by label. Frame = all regular (non-struck) drama entries. - Redact = struck-through (retracted) entries, e.g. a WAIT NVM after TAKE SIG. + Redact = struck-through (retracted) entries, e.g. a WAIT NVM after SAVE SIG. Scenario (one gamer, Role + Sig events): 1. Both labels checked by default — all events visible. diff --git a/src/functional_tests/test_game_room_select_role.py b/src/functional_tests/test_game_room_select_role.py index 0f021f1..3d4c329 100644 --- a/src/functional_tests/test_game_room_select_role.py +++ b/src/functional_tests/test_game_room_select_role.py @@ -843,12 +843,12 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest): )) # ------------------------------------------------------------------ # - # Test 7 — PICK SIGS appears + card stack removed on last role # + # Test 7 — SCAN SIGS appears + card stack removed on last role # # ------------------------------------------------------------------ # def test_pick_sigs_appears_and_card_stack_removed_on_last_role(self): """When the sixth and final role is confirmed, the all_roles_filled - WS event makes the PICK SIGS button visible and removes the card + WS event makes the SCAN SIGS button visible and removes the card stack from the DOM entirely.""" emails = [ "founder@test.io", "amigo@test.io", "bud@test.io", @@ -883,7 +883,7 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest): self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click() self.confirm_guard() - # PICK SIGS wrap must become visible via the all_roles_filled WS event. + # SCAN SIGS wrap must become visible via the all_roles_filled WS event. self.wait_for(lambda: self.assertFalse( self.browser.find_element(By.ID, "id_pick_sigs_wrap").get_attribute("style"), )) diff --git a/src/functional_tests/test_game_room_select_sea.py b/src/functional_tests/test_game_room_select_sea.py index f3ef437..f4fa907 100644 --- a/src/functional_tests/test_game_room_select_sea.py +++ b/src/functional_tests/test_game_room_select_sea.py @@ -1,4 +1,4 @@ -"""Functional tests for the PICK SEA overlay — Celtic Cross draw.""" +"""Functional tests for the DRAW SEA overlay — Celtic Cross draw.""" from django.test import tag from django.urls import reverse @@ -39,7 +39,7 @@ def _make_sky_confirmed_room(live_server_url, user, earthman): @tag("channels") class PickSeaAsyncTransitionTest(ChannelsFunctionalTest): """After sky confirm, the sky overlay closes and the room reloads to the - table hex w. the PICK SEA btn visible — the gamer must opt into the sea + table hex w. the DRAW SEA btn visible — the gamer must opt into the sea overlay rather than be auto-launched into it.""" def setUp(self): @@ -92,22 +92,22 @@ class PickSeaAsyncTransitionTest(ChannelsFunctionalTest): """) def test_pick_sea_btn_visible_after_sky_confirm(self): - """Confirming sky reloads the room to the hex w. PICK SEA replacing - PICK SKY; the sea overlay is NOT auto-opened.""" + """Confirming sky reloads the room to the hex w. DRAW SEA replacing + CAST SKY; the sea overlay is NOT auto-opened.""" self.create_pre_authenticated_session("founder@test.io") self.browser.get(self.room_url) - # Sky not yet confirmed — PICK SKY btn present. + # Sky not yet confirmed — CAST SKY btn present. self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn")) self._confirm_sky() - # Page reloads → hex shows PICK SEA in place of PICK SKY. + # Page reloads → hex shows DRAW SEA in place of CAST SKY. self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sea_btn")) self.assertEqual(self.browser.find_elements(By.ID, "id_pick_sky_btn"), []) # Sea overlay is NOT auto-opened — it only appears once the gamer - # clicks PICK SEA. + # clicks DRAW SEA. has_sea_open = self.browser.execute_script( "return document.documentElement.classList.contains('sea-open');" ) @@ -126,7 +126,7 @@ class PickSeaAsyncTransitionTest(ChannelsFunctionalTest): self.assertTrue(not sky or not sky[0].is_displayed()) def test_clicking_pick_sea_btn_opens_sea_overlay(self): - """The gamer's explicit click on PICK SEA is what opens the sea overlay.""" + """The gamer's explicit click on DRAW SEA is what opens the sea overlay.""" self.create_pre_authenticated_session("founder@test.io") self.browser.get(self.room_url) self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn")) @@ -134,7 +134,7 @@ class PickSeaAsyncTransitionTest(ChannelsFunctionalTest): self._confirm_sky() self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sea_btn")) - # On slow CI, the PICK SEA btn parses into the DOM before the inline + # On slow CI, the DRAW SEA btn parses into the DOM before the inline # `