diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py index b6a493d..8ad4a72 100644 --- a/src/apps/epic/tests/integrated/test_views.py +++ b/src/apps/epic/tests/integrated/test_views.py @@ -4162,6 +4162,19 @@ class PickSeaUnifiedFeltTest(TestCase): content = self.client.get(self.url).content.decode() self.assertIn("room-menu-sea", content) + def test_sea_stage_renders_back_img_for_image_deck(self): + """The sea-stage FLIP reveals the card back. The back-img now renders for + ANY image-equipped deck WITH a back image (polarized or not) — dropping + the old `not is_polarized` gate — so the Gravity/Levity draw can show a + polarity-tinted back (user-spec 2026-06-07).""" + minch = DeckVariant.objects.get(slug="minchiate-fiorentine-1860-1890") + founder = self.gamers[0] + founder.unlocked_decks.add(minch) + founder.equipped_deck = minch + founder.save(update_fields=["equipped_deck"]) + content = self.client.get(self.url).content.decode() + self.assertIn("sig-stage-card-back-img", content) + def test_always_two_deck_stacks_gravity_and_levity(self): """Unlike my_sea / Sig Select, the room Sea Select ALWAYS shows BOTH the Gravity + Levity stacks — the gamer draws from either populated half diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index 9c7deca..dd737a2 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -2328,6 +2328,36 @@ $_sea-title-els: '.fan-card-name, .sig-qualifier-above, .sig-qualifier-below, .f ); } +// ── FLIP-to-back polarity tint + border (user-spec 2026-06-07) ──────────────── +// The card-back art is identical for both polarities, so the FLIP reveal would +// be ambiguous. Distinguish the two stacks: a 0.3-alpha fill OVERLAY (the same +// deck fill colors) + a polarity border on the flipped card. +// gravity → --quiUser tint / --quaUser border +// levity → --terUser tint / --ninUser border +// Scoped to `.is-flipped-to-back` on the sea stage only, so my_sign / applet +// stages are untouched. The `&.sig-stage-card--image` chain (0,4,0) out-cascades +// the image-mode `border:0` / transparent rules above so the border + tint show. +.sea-stage-card.is-flipped-to-back { + position: relative; + &::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + border-radius: 0.4rem; + } +} +.sea-stage--gravity .sea-stage-card.is-flipped-to-back, +.sea-stage--gravity .sea-stage-card.sig-stage-card--image.is-flipped-to-back { + border: 0.18rem solid rgba(var(--quaUser), 1); + &::after { background: rgba(var(--quiUser), 0.3); } +} +.sea-stage--levity .sea-stage-card.is-flipped-to-back, +.sea-stage--levity .sea-stage-card.sig-stage-card--image.is-flipped-to-back { + border: 0.18rem solid rgba(var(--ninUser), 1); + &::after { background: rgba(var(--terUser), 0.3); } +} + // Sea stat block — reuses sig-select stat-block sizing, scoped to sea-stage. // `background` left blank here; the `.sea-stage--gravity` / `.sea-stage-- // levity` parent rules below set the polarity-aware bg (sig convention, diff --git a/src/templates/apps/gameboard/_partials/_sea_overlay.html b/src/templates/apps/gameboard/_partials/_sea_overlay.html index 6e12b1d..7b30de1 100644 --- a/src/templates/apps/gameboard/_partials/_sea_overlay.html +++ b/src/templates/apps/gameboard/_partials/_sea_overlay.html @@ -407,8 +407,34 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save. var pickSeaBtn = document.getElementById('id_pick_sea_btn'); if (pickSeaBtn) pickSeaBtn.addEventListener('click', openSea); - // ── Init — seed deck, restore saved-hand state, draw labels. - _fetchDeck(); + // Re-seed SeaDeal's `_seaHand` from the server-rendered saved slots so they + // stay clickable to RE-OPEN the stage after a refresh. Without this, SeaDeal's + // in-memory `_seaHand` is only populated by openStage/register during the live + // session → after a reload the overlay click handler short-circuits on + // `if (!_seaHand[pos]) return` and the saved slots silently no-op (user-reported + // 2026-06-07; same fix my_sea carries). The card payloads come from the deck + // fetch (looked up by `data-card-id`); `reversed`/polarity are DOM-sourced. + function _seedSavedHand() { + if (!window.SeaDeal || !window.SeaDeal.seedHand) return; + var byId = {}; + _levityPile.concat(_gravityPile).forEach(function (c) { byId[c.id] = c; }); + var seed = {}; + cross.querySelectorAll('.sea-card-slot.sea-card-slot--filled').forEach(function (slot) { + var posName = slot.dataset.posKey; + var card = byId[parseInt(slot.dataset.cardId, 10)]; + if (!posName || !card) return; + var slotCard = {}; + for (var k in card) if (Object.prototype.hasOwnProperty.call(card, k)) slotCard[k] = card[k]; + slotCard.reversed = slot.classList.contains('sea-card-slot--reversed'); + seed[posName] = { card: slotCard, isLevity: slot.classList.contains('sea-card-slot--levity') }; + }); + SeaDeal.seedHand(seed); + } + + // ── Init — bind SeaDeal to the (possibly injected) overlay, seed the deck, + // then re-seed the saved hand once the deck fetch resolves. + if (window.SeaDeal && window.SeaDeal.reinit) SeaDeal.reinit(); + _fetchDeck().then(_seedSavedHand); _filled = cross.querySelectorAll('.sea-card-slot.sea-card-slot--filled').length; // Already-drawn sea hand → "in Sea Select or beyond" (user-spec 2026-06-07, // condition 3): mark sea-entered on load so the sky-saved glow stays muted @@ -417,10 +443,6 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save. if (_filled >= _currentOrder().length) { _setComplete(true); _lockSpread(); _setHasDrawn(true); } else if (_filled > 0) { _lockSpread(); _setHasDrawn(true); } syncLabels(hidden.value); - - // Re-seed SeaDeal's _seaHand from server-rendered saved slots so they stay - // clickable to re-open the stage. (Card payloads come from the deck fetch.) - if (window.SeaDeal && window.SeaDeal.reinit) SeaDeal.reinit(); }()); diff --git a/src/templates/apps/gameboard/_partials/_sea_stage.html b/src/templates/apps/gameboard/_partials/_sea_stage.html index 001ac69..e7c246d 100644 --- a/src/templates/apps/gameboard/_partials/_sea_stage.html +++ b/src/templates/apps/gameboard/_partials/_sea_stage.html @@ -42,19 +42,19 @@ {% comment %} - Polish-6 — back-img + FLIP btn. The back-img mirrors the - my_sign.html / _applet-my-sign.html pattern: only renders - for non-polarized image-equipped decks (since back-img is - meaningless otherwise; src derives from user's equipped - deck). FLIP btn renders UNCONDITIONALLY per user-spec "allow - the FLIP btn everywhere"; sea.js's click handler no-ops - when no back-img sibling exists. Multi-user gameroom is a - known limitation here — the back-img src is the room - viewer's deck-back, not the drawing gamer's, which is wrong - when different gamers' decks have different backs. Parked - for a future multi-user polish pass. + back-img + FLIP btn. FLIP reveals the card back. Renders for ANY + image-equipped deck that has a back image — INCLUDING polarized decks + (user-spec 2026-06-07): the Sea Select phase tints the back per the + drawn card's polarity (`.sea-stage--gravity` / `--levity`, see + `_card-deck.scss`), so the same back-art reads distinctly for the two + stacks (was previously gated to non-polarized decks, where it never + rendered for the room's Gravity/Levity draw). The FLIP btn renders + unconditionally; sea.js's handler no-ops when no back-img sibling + exists (text decks). Multi-user gameroom limitation: the src is the + VIEWER's deck-back — correct for the sea (each gamer draws their own), + parked for the general multi-user case. {% endcomment %} - {% if request.user.is_authenticated and request.user.equipped_deck.has_card_images and not request.user.equipped_deck.is_polarized %} + {% if request.user.is_authenticated and request.user.equipped_deck.has_card_images and request.user.equipped_deck.back_image_url %} {% endif %}