A.5-polish FLIP-to-back for non-polarized image-equipped decks — TDD. User-spec'd feature 2026-05-25 PM after browser-verifying A.5: the FLIP button on my_sign.html cycles polarity for polarized decks (Earthman) — gravity/levity swap w. a 3D-spin animation, stat block updates to the new polarity's emanation/reversal qualifiers. For non-polarized decks (Minchiate today, future RWS-with-images, future classic-playing decks), polarity has no meaning — clicking FLIP just runs an animation that doesn't change anything content-wise. User wants FLIP repurposed for non-polarized decks: reveal the card-back image while leaving the stat block untouched, so the gesture has visible payoff w/o forcing a meaningless polarity-state change. Implementation thread: server-side page wrapper carries a new data-deck-polarized="{{ user.equipped_deck.is_polarized|yesno:'true,false' }}" attr so the in-page JS can branch on it without making an API call or guessing from card data; stage-card scaffold conditionally renders a hidden <img.sig-stage-card-back-img> element when equipped_deck.has_card_images AND NOT is_polarized (image-equipped polarized decks would still cycle polarity per existing flow — back-image element absent for them, no resource waste). JS branch in flipBtn.click: if (pageEl.dataset.deckPolarized === 'false') { stageCard.classList.toggle('is-flipped-to-back') } else { _flipPolarityAnimated() } — same .is-reversed class toggle on the btn itself so visual feedback is consistent across both modes (btn rotates to signal "flipped state on"). SCSS: .sig-stage-card-back-img joins the existing .sig-stage-card-img filter chain (same contour stroke + silhouette black shadow — back image gets identical visual treatment to the front so the flip reads as same-deck consistency); default display: none; .sig-stage-card.is-flipped-to-back flips visibility — hides front, shows back. Stat block + arcana-key stroke color stay put per user spec — FLIP for non-polarized is purely a visual reveal, no polarity-cycle or content swap. 3 new ITs in MySignViewTest: data-deck-polarized="true" for default Earthman; data-deck-polarized="false" + back-img element present w. correct v2-convention back asset URL when user switches to Minchiate; polarized deck omits the back-img element. No JS unit test (Jasmine spec) for the flipBtn branch — visual verify covers the hover/click interaction; the IT covers the server-side conditional render that determines whether the branch can fire. No FT (the existing my_sign FTs cover the polarized-flip flow already; non-polarized-flip is a CSS class toggle, low-risk for regression). Tests: 3 new green; 9/9 MySignViewTest class green; 1303/1303 IT+UT total green (71s; +3 from 82813e9's 1300). Out of scope: my_sea's central sig card doesn't have a FLIP btn (no analogous behavior to add there); room.html FLIP behavior will be covered in A.8 if applicable; Sea Stage modal FLIP behavior (if any) lands in the my-sea fetch-endpoint extension later in A.5
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -860,6 +860,49 @@ class MySignViewTest(TestCase):
|
|||||||
{"card_id": 999999, "reversed": "0"},
|
{"card_id": 999999, "reversed": "0"},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 403)
|
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
|
||||||
|
<img.sig-stage-card-back-img> 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.user.refresh_from_db()
|
||||||
self.assertIsNone(self.user.significator_id)
|
self.assertIsNone(self.user.significator_id)
|
||||||
|
|
||||||
|
|||||||
@@ -681,7 +681,8 @@ html:has(.sig-backdrop) {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sig-stage-card-img {
|
.sig-stage-card-img,
|
||||||
|
.sig-stage-card-back-img {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -707,6 +708,18 @@ html:has(.sig-backdrop) {
|
|||||||
drop-shadow( 0 -0.2rem 0 var(--img-stroke-color))
|
drop-shadow( 0 -0.2rem 0 var(--img-stroke-color))
|
||||||
drop-shadow( 1px 1px 2px rgba(0, 0, 0, 1));
|
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 ────────────────────────────
|
// ─── My Sign picker — sizing + state-gated reveal ────────────────────────────
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
data-save-url="{% url 'billboard:save_sign' %}"
|
data-save-url="{% url 'billboard:save_sign' %}"
|
||||||
{% if current_significator %}data-current-card-id="{{ current_significator.id }}"{% endif %}
|
{% if current_significator %}data-current-card-id="{{ current_significator.id }}"{% endif %}
|
||||||
data-current-reversed="{{ current_significator_reversed|yesno:'true,false' }}"
|
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 %}">
|
data-polarity="{% if current_significator_reversed %}levity{% else %}gravity{% endif %}">
|
||||||
|
|
||||||
{# Stage frame — always reserved at the top of the page; SAVE SIGN + #}
|
{# 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 #}
|
{# DeckVariant.has_card_images). Hidden by default; CSS shows it #}
|
||||||
{# when .sig-stage-card carries .sig-stage-card--image. #}
|
{# when .sig-stage-card carries .sig-stage-card--image. #}
|
||||||
<img class="sig-stage-card-img" alt="" style="display:none">
|
<img class="sig-stage-card-img" alt="" style="display:none">
|
||||||
|
{# 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 %}
|
||||||
|
<img class="sig-stage-card-back-img" alt=""
|
||||||
|
src="{{ request.user.equipped_deck.back_image_url }}"
|
||||||
|
style="display:none">
|
||||||
|
{% endif %}
|
||||||
<div class="fan-card-corner fan-card-corner--tl">
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
<span class="fan-corner-rank"></span>
|
<span class="fan-corner-rank"></span>
|
||||||
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
||||||
@@ -351,7 +362,20 @@
|
|||||||
if (flipBtn) {
|
if (flipBtn) {
|
||||||
flipBtn.addEventListener('click', function () {
|
flipBtn.addEventListener('click', function () {
|
||||||
if (!_currentCard) return;
|
if (!_currentCard) return;
|
||||||
|
// 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();
|
_flipPolarityAnimated();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (spinBtn) {
|
if (spinBtn) {
|
||||||
|
|||||||
Reference in New Issue
Block a user