diff --git a/src/apps/epic/models.py b/src/apps/epic/models.py index 3ad8814..e29d826 100644 --- a/src/apps/epic/models.py +++ b/src/apps/epic/models.py @@ -817,7 +817,7 @@ def _court_cards(deck_variant, suits): arcana__in=[TarotCard.MIDDLE, TarotCard.MINOR], suit__in=suits, number__in=[11, 12, 13, 14], - )) + ).select_related("deck_variant")) def _major_cards(deck_variant): @@ -826,7 +826,7 @@ def _major_cards(deck_variant): return [] return list(TarotCard.objects.filter( deck_variant=deck_variant, arcana=TarotCard.MAJOR, number__in=[0, 1], - )) + ).select_related("deck_variant")) def _polarity_sig_cards(room, polarity): diff --git a/src/apps/epic/static/apps/epic/stage-card.js b/src/apps/epic/static/apps/epic/stage-card.js index 24a2aa8..70f72a8 100644 --- a/src/apps/epic/static/apps/epic/stage-card.js +++ b/src/apps/epic/static/apps/epic/stage-card.js @@ -55,6 +55,10 @@ var StageCard = (function () { // border-color CSS var (--terUser for major, --quiUser for the rest). image_url: el.dataset.imageUrl || '', arcana_key: el.dataset.arcanaKey || '', + // Per-card FLIP back (dubbodeck) — the focused card's OWN deck back. + // Empty for backless (text-only / polarized) decks → _setImageMode + // hides the FLIP affordance for that card in a mixed pile. + back_image_url: el.dataset.backImageUrl || '', }; } @@ -135,6 +139,24 @@ var StageCard = (function () { img.removeAttribute('src'); } } + // Per-card FLIP back (dubbodeck): point the stage back-img at the FOCUSED + // card's own deck back + show/hide the FLIP affordance for cards whose + // deck has no back (a mixed pile can hold backless cards next to image + // ones). Guarded on the back-img element existing — only rendered when the + // pile has backable cards (`sig_pile_has_backs`), so my_sign's single + // server-rendered back (no per-card data) is untouched. + var backImg = stageCard.querySelector('.sig-stage-card-back-img'); + if (backImg) { + var flipBtn = stageCard.querySelector('.sig-flip-btn'); + if (card.back_image_url) { + backImg.src = card.back_image_url; + if (flipBtn) flipBtn.style.display = ''; + } else { + // Backless focused card — drop any flipped state + hide FLIP. + stageCard.classList.remove('is-flipped-to-back'); + if (flipBtn) flipBtn.style.display = 'none'; + } + } } // Paint the stage-card's upright + reversal faces from a normalized card diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py index 8fc1cbc..5cb20fd 100644 --- a/src/apps/epic/tests/integrated/test_views.py +++ b/src/apps/epic/tests/integrated/test_views.py @@ -1798,6 +1798,41 @@ class SigSelectUnifiedStageTest(TestCase): content = self.client.get(self.url).content.decode() self.assertIn('data-image-url=""', content) + # ── Per-card FLIP-to-back (dubbodeck) ─────────────────────────────────── + def test_backable_deck_renders_per_card_back_url_and_flip(self): + # Non-polarized image deck → each sig card carries its OWN deck back + + # the stage renders the FLIP affordance (sig_pile_has_backs True), so a + # mixed pile can FLIP each card to its own deck's back via stage-card.js. + self.earthman.has_card_images = True + self.earthman.is_polarized = False + self.earthman.save(update_fields=["has_card_images", "is_polarized"]) + resp = self.client.get(self.url) + content = resp.content.decode() + self.assertIn('data-back-image-url="/static', content) + self.assertTrue(resp.context["sig_pile_has_backs"]) + self.assertIn("sig-stage-card-back-img", content) + self.assertIn("sig-flip-btn", content) + + def test_polarized_deck_omits_per_card_back_and_flip(self): + # Polarized decks don't get a FLIP-to-back (polarity IS their flip) → + # empty per-card back + no stage back-img/FLIP element. + self.earthman.has_card_images = True + self.earthman.is_polarized = True + self.earthman.save(update_fields=["has_card_images", "is_polarized"]) + resp = self.client.get(self.url) + content = resp.content.decode() + self.assertIn('data-back-image-url=""', content) + self.assertFalse(resp.context["sig_pile_has_backs"]) + self.assertNotIn("sig-stage-card-back-img", content) + + def test_textonly_deck_omits_per_card_back(self): + # Glyph-only deck → empty per-card back + no FLIP element. + self.earthman.has_card_images = False + self.earthman.save(update_fields=["has_card_images"]) + resp = self.client.get(self.url) + self.assertIn('data-back-image-url=""', resp.content.decode()) + self.assertFalse(resp.context["sig_pile_has_backs"]) + # ── Workstream C — felt replaces hex-pane content (my_sea-style) ──────── def test_sig_stage_on_felt_in_hex_pane_no_dark_backdrop(self): content = self.client.get(self.url).content.decode() diff --git a/src/apps/epic/views.py b/src/apps/epic/views.py index 5650220..b25eea6 100644 --- a/src/apps/epic/views.py +++ b/src/apps/epic/views.py @@ -648,6 +648,17 @@ def _role_select_context(room, user, seat_param=None): ctx["sig_cards"] = gravity_sig_cards(room, user) else: ctx["sig_cards"] = [] + # Per-card FLIP-to-back (dubbodeck): the stage back-img + FLIP affordance + # render when ANY card in the (possibly mixed-deck) pile has a + # non-polarized image-deck back; stage-card.js then points the back-img at + # the FOCUSED card's OWN deck. Was gated on the viewer's seat deck only — + # wrong once a pile mixes decks (an RWS card in a PC-earthman viewer's pile + # could never FLIP). select_related on the assembly keeps this N+1-free. + ctx["sig_pile_has_backs"] = any( + c.deck_variant_id and c.deck_variant.has_card_images + and not c.deck_variant.is_polarized + for c in ctx["sig_cards"] + ) if room.table_status == Room.SKY_SELECT: # CARTE seat-switch: the sky/sea state is per-SEAT (Character.seat), so key diff --git a/src/static_src/scss/rootvars.scss b/src/static_src/scss/rootvars.scss index b5cc5b9..3e31876 100644 --- a/src/static_src/scss/rootvars.scss +++ b/src/static_src/scss/rootvars.scss @@ -17,26 +17,26 @@ /* Precious Metal Hues */ // nickel --priNi: 141, 142, 140; - --secNi: 118, 120, 118; - --terNi: 93, 95, 94; - --terNi: 0, 0, 0; - --quaNi: 0, 0, 0; - --quiNi: 0, 0, 0; - --sixNi: 0, 0, 0; + --secNi: 108, 120, 108; + --terNi: 80, 108, 80; + --quaNi: 63, 95, 64; + --quiNi: 43, 75, 44; + --sixNi: 23, 56, 26; + // palladium - --priPd: 188, 193, 165; - --secPd: 155, 160, 138; - --terPd: 124, 129, 111; - --quaPd: 0, 0, 0; - --quiPd: 0, 0, 0; - --sixPd: 0, 0, 0; + --priPd: 218, 223, 181; + --secPd: 188, 193, 165; + --terPd: 155, 160, 138; + --quaPd: 124, 129, 111; + --quiPd: 97, 99, 76; + --sixPd: 72, 75, 0; // platinum --priPt: 229, 228, 226; - --secPt: 189, 190, 189; - --terPt: 152, 153, 153; - --quaPt: 0, 0, 0; - --quiPt: 0, 0, 0; - --sixPt: 0, 0, 0; + --secPt: 191, 190, 188; + --terPt: 152, 151, 149; + --quaPt: 122, 121, 119; + --quiPt: 92, 91, 89; + --sixPt: 62, 61, 59; // titanium --priTi: 38, 57, 69; --secTi: 57, 79, 94; @@ -350,16 +350,18 @@ --secBlt: 137, 107, 32; // white --terBlt: 255, 255, 255; - // --quaBlt: ; + // red + --quaBlt: 178, 12, 18; // black --quiBlt: 0, 0, 0; --sixBlt: 162, 170, 173; // purple --sepBlt: 50, 30, 95; + // red again --octBlt: 157, 34, 53; // orange - --ninBlt: 221, 73, 38; - --decBlt: 181, 57, 30; + --ninBlt: 200, 63, 28; + --decBlt: 121, 39, 22; // Felt values --undUser: var(--priFor); @@ -452,11 +454,11 @@ .palette-baltimore { --priUser: var(--sepBlt); --secUser: var(--sixBlt); - --terUser: var(--ninBlt); + --terUser: var(--quaBlt); --quaUser: var(--priBlt); --quiUser: var(--secBlt); --sixUser: var(--ninBlt); - --sepUser: var(--quiBlt); + --sepUser: var(--quaBlt); --octUser: var(--decBlt); --ninUser: var(--terBlt); --decUser: var(--quiBlt); @@ -466,12 +468,12 @@ .palette-maryland { --priUser: var(--quiBlt); --secUser: var(--sixBlt); - --terUser: var(--secBlt); - --quaUser: var(--priYl); - --quiUser: var(--octBlt); - --sixUser: var(--quiBlt); - --sepUser: var(--quiBlt); - --octUser: var(--quiBlt); + --terUser: var(--priYl); + --quaUser: var(--priBlt); + --quiUser: var(--quaBlt); + --sixUser: var(--octBlt); + --sepUser: var(--sepBlt); + --octUser: var(--decBlt); --ninUser: var(--priRd); --decUser: var(--quiBlt); } @@ -483,9 +485,9 @@ --terUser: var(--sixAg); /* 240,240,240 — bright white accent */ --quaUser: var(--sixAg); /* 240,240,240 — active/interactive */ --quiUser: var(--secAg); /* 133,133,133 — secondary action */ - --sixUser: var(--quaAg); /* 100,100,100 — subtle mid */ - --sepUser: var(--priPt); /* 60,60,60 — deep subtle */ - --octUser: var(--quiPt); /* 189,190,189 — links (cooler silver) */ + --sixUser: var(--secFe); /* 100,100,100 — subtle mid */ + --sepUser: var(--quiPd); /* 60,60,60 — deep subtle */ + --octUser: var(--priFe); /* 189,190,189 — links (cooler silver) */ --ninUser: var(--sixAg); /* 240,240,240 — glow highlight */ --decUser: var(--terAg); /* 100,100,100 — mid tone */ } diff --git a/src/templates/apps/gameboard/_partials/_sig_select_overlay.html b/src/templates/apps/gameboard/_partials/_sig_select_overlay.html index c2b679a..6cae219 100644 --- a/src/templates/apps/gameboard/_partials/_sig_select_overlay.html +++ b/src/templates/apps/gameboard/_partials/_sig_select_overlay.html @@ -29,14 +29,16 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_ {# once the dubbodeck assembly lands (each card keeps its deck). #} {# Non-polarized image decks (RWS / Minchiate): a FLIP .btn-reveal #} - {# turns the preview to the deck's card-back (my_sign parity). The #} + {# turns the preview to the FOCUSED card's deck card-back. The #} {# back-img defaults display:none via SCSS; .is-flipped-to-back #} {# (toggled by sig-select.js) reveals it. The shared #} {# .my-sign-flip-btn rules already anchor/hide/counter-position on #} - {# any .sig-stage-card. Gated on the SEAT deck for now — per-card #} - {# backs arrive with the dubbodeck assembly (mixed-deck piles). #} - {% if user_seat.deck_variant.has_card_images and not user_seat.deck_variant.is_polarized %} - + {# any .sig-stage-card. Rendered when ANY pile card is backable #} + {# (`sig_pile_has_backs`); stage-card.js `_setImageMode` repoints #} + {# the src per focused card + hides FLIP for backless cards — so a #} + {# mixed dubbodeck pile FLIPs each card to its OWN deck's back. #} + {% if sig_pile_has_backs %} + {% endif %}
@@ -91,6 +93,10 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_ data-arcana="{{ card.get_arcana_display }}" data-arcana-key="{{ card.arcana }}" data-image-url="{{ card.image_url }}" + {# Per-card FLIP back (dubbodeck): the card's OWN deck back, only #} + {# for non-polarized image decks (others → empty → stage-card.js #} + {# hides the FLIP affordance for that card). #} + data-back-image-url="{% if card.deck_variant.has_card_images and not card.deck_variant.is_polarized %}{{ card.deck_variant.back_image_url }}{% endif %}" data-correspondence="{{ card.correspondence|default:'' }}" data-keywords-upright="{{ card.keywords_upright|join:',' }}" data-keywords-reversed="{{ card.keywords_reversed|join:',' }}"