diff --git a/src/apps/billboard/tests/integrated/test_views.py b/src/apps/billboard/tests/integrated/test_views.py index 73d3ccb..7ac59c3 100644 --- a/src/apps/billboard/tests/integrated/test_views.py +++ b/src/apps/billboard/tests/integrated/test_views.py @@ -860,6 +860,49 @@ class MySignViewTest(TestCase): {"card_id": 999999, "reversed": "0"}, ) self.assertEqual(response.status_code, 403) + + def test_page_carries_data_deck_polarized_attr(self): + """Sprint A.5-polish — the my_sign page wrapper exposes the equipped + deck's `is_polarized` state via `data-deck-polarized` so the FLIP-btn + JS can branch: polarized decks cycle polarity (existing behavior); + non-polarized decks flip to the deck card-back (new).""" + import lxml.html + # Default Earthman = is_polarized=True per A.0 migration. + response = self.client.get(reverse("billboard:my_sign")) + parsed = lxml.html.fromstring(response.content) + [page] = parsed.cssselect(".my-sign-page") + self.assertEqual(page.get("data-deck-polarized"), "true") + + def test_image_deck_renders_back_img_in_stage_scaffold(self): + """Image-equipped non-polarized decks (Minchiate) render a hidden + inside the stage card; toggled visible + by the FLIP-btn JS handler via the .is-flipped-to-back class.""" + from apps.epic.models import DeckVariant + import lxml.html + minchiate = DeckVariant.objects.get(slug="minchiate-fiorentine-1860-1890") + self.user.unlocked_decks.add(minchiate) + self.user.equipped_deck = minchiate + self.user.save(update_fields=["equipped_deck"]) + response = self.client.get(reverse("billboard:my_sign")) + parsed = lxml.html.fromstring(response.content) + [page] = parsed.cssselect(".my-sign-page") + self.assertEqual(page.get("data-deck-polarized"), "false") + [back_img] = parsed.cssselect(".sig-stage-card .sig-stage-card-back-img") + self.assertIn( + "minchiate-fiorentine-1860-1890-back.png", + back_img.get("src", ""), + ) + + def test_polarized_deck_omits_back_img(self): + """Earthman (polarized) keeps the existing polarity-cycle FLIP — no + back-image element needed in the scaffold.""" + import lxml.html + response = self.client.get(reverse("billboard:my_sign")) + parsed = lxml.html.fromstring(response.content) + self.assertEqual( + len(parsed.cssselect(".sig-stage-card .sig-stage-card-back-img")), 0, + "Polarized deck must not render the back-image element", + ) self.user.refresh_from_db() self.assertIsNone(self.user.significator_id) diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index 8ed3be7..e1a021e 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -681,7 +681,8 @@ html:has(.sig-backdrop) { display: none; } - .sig-stage-card-img { + .sig-stage-card-img, + .sig-stage-card-back-img { display: block; width: 100%; height: 100%; @@ -707,6 +708,18 @@ html:has(.sig-backdrop) { drop-shadow( 0 -0.2rem 0 var(--img-stroke-color)) drop-shadow( 1px 1px 2px rgba(0, 0, 0, 1)); } + .sig-stage-card-back-img { display: none; } // shown only when flipped + + // Sprint A.5 — FLIP-to-back behavior for non-polarized image-equipped + // decks (Minchiate today). When `.is-flipped-to-back` is toggled by + // my_sign's flip-btn handler, the front face img hides + the deck + // card-back img shows. Stat block + arcana-key stroke color stay put — + // FLIP is purely a visual reveal of the card's back, no polarity-cycle + // or content swap. User spec 2026-05-25 PM. + &.is-flipped-to-back { + .sig-stage-card-img { display: none; } + .sig-stage-card-back-img { display: block; } + } } // ─── My Sign picker — sizing + state-gated reveal ──────────────────────────── diff --git a/src/templates/apps/billboard/my_sign.html b/src/templates/apps/billboard/my_sign.html index 4787ebf..1e14e01 100644 --- a/src/templates/apps/billboard/my_sign.html +++ b/src/templates/apps/billboard/my_sign.html @@ -19,6 +19,7 @@ data-save-url="{% url 'billboard:save_sign' %}" {% if current_significator %}data-current-card-id="{{ current_significator.id }}"{% endif %} data-current-reversed="{{ current_significator_reversed|yesno:'true,false' }}" + data-deck-polarized="{{ request.user.equipped_deck.is_polarized|yesno:'true,false' }}" data-polarity="{% if current_significator_reversed %}levity{% else %}gravity{% endif %}"> {# Stage frame — always reserved at the top of the page; SAVE SIGN + #} @@ -35,6 +36,16 @@ {# DeckVariant.has_card_images). Hidden by default; CSS shows it #} {# when .sig-stage-card carries .sig-stage-card--image. #} + {# Sprint A.5 — for non-polarized image-equipped decks (Minchiate, #} + {# future RWS-with-images), the FLIP btn flips the card to its #} + {# back instead of cycling polarity (which has no meaning for #} + {# non-polarized decks). Pre-rendered back-image element; CSS #} + {# toggles visibility via `.sig-stage-card.is-flipped-to-back`. #} + {% if request.user.equipped_deck.has_card_images and not request.user.equipped_deck.is_polarized %} + + {% endif %}
@@ -351,7 +362,20 @@ if (flipBtn) { flipBtn.addEventListener('click', function () { if (!_currentCard) return; - _flipPolarityAnimated(); + // Sprint A.5 — non-polarized decks (Minchiate, RWS): FLIP + // shows the deck card-back instead of cycling polarity (no + // gravity/levity to toggle on these decks). Stat block stays + // unchanged — user spec 2026-05-25 PM. Polarized decks + // (Earthman) keep the existing polarity-flip animation. + if (pageEl.dataset.deckPolarized === 'false') { + stageCard.classList.toggle('is-flipped-to-back'); + if (flipBtn) flipBtn.classList.toggle( + 'is-reversed', + stageCard.classList.contains('is-flipped-to-back') + ); + } else { + _flipPolarityAnimated(); + } }); } if (spinBtn) {