From 71c00699a1b390435139100c952a9a58dfba6d7b Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Wed, 3 Jun 2026 02:37:13 -0400 Subject: [PATCH] room Sig Select: unify with the my_sign card-stage apparatus (DRY stat-block, per-card stage image, --duoUser felt) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The in-room SIG_SELECT stage diverged from the polished GAME SIGN page: a fixed dark-Gaussian modal over the hex, a stale label-only stat-block, and no card imagery. This brings it in line with my_sign / my_sea. A — Stat-block DRY: _sig_select_overlay.html now renders the shared core/_partials/_stat_face.html (rank-chip + title + arcana + keywords) instead of a reduced label-only copy; sig-select.js's updateStage() now calls StageCard.populateStatExtras (the missing call that left those fields blank). data-arcana-key added per card for title color-keying. B — Per-card stage image: the stage card gains a .sig-stage-card-img slot + data-image-url per thumbnail, so an image-equipped seat deck (RWS / Minchiate) shows real card art on the preview. Thumbnails stay glyph-only (rank + suit) at every deck — only the stage shows the image. Keyed off each card's OWN deck_variant, so it auto-upgrades to mixed art when the dubbodeck assembly lands. No backend change (cards already carry a deck_variant via _room_deck_variant). C — Felt-in-aperture: the stage renders INSIDE .room-hex-pane on edge-to- edge green --duoUser felt (my_sea-style), replacing the hex content; the old .sig-backdrop blur is gone. .sig-overlay absolute-fills the pane (.room-hex-pane.has-sig-stage = positioning context); dismissing it reveals the hex + waiting message behind. Scroll-down still reaches the reelhouse carousel (untouched scroll pane). Polishes: - Image-mode bg escape: the levity 0,3,0 polarity rule (.sig-overlay/.my-sign-page[data-polarity="levity"] .sig-stage-card) hard-set a --secUser background that re-clothed image cards behind the transparent PNG. Added the &.sig-stage-card--image { background: transparent; border:0; overflow:visible } escape (parity w. the base + my-sea rules). Latent my_sign bug too. Monodeck-era assumption. - FLIP .btn-reveal: non-polarized image decks get a FLIP that turns the preview to the deck card-back (my_sign parity) — back-img + reused .my-sign-flip-btn (shared positioning/hide/counter-position rules already cover .sig-stage-card) + a frozen-gated reveal scoped to .sig-overlay + sig-select.js _flipToBack (500ms Y-rotate, midpoint swap). SPIN now sets data-spinning so the btn hides mid-rotate. - Reserved thumbs-up / hover cursors portal to a body-root fixed container, so they hung over the reelhouse on scroll. sig-select.js now toggles .cursors-hidden off the aperture scrollTop: instant hide the moment the scroll leaves the hex, 0.5s opacity ease-in on the full return. Tray intentionally kept. TDD: SigSelectUnifiedStageTest (6 ITs) — DRY stat-face present, per-card data-image-url + data-arcana-key, .sig-stage-card-img slot, image deck non-empty face URL / text deck empty, has-sig-stage felt + overlay inside the hex pane. 319 epic test_views ITs green; user-verified live on an RWS room (no rect, FLIP works, thumb timing). Jasmine for the JS wiring + the dubbodeck cross-deck assembly (per-seat segment cards, CARTE-solo both-polarity case, per-card backs) are the tracked follow-on. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/apps/epic/static/apps/epic/sig-select.js | 65 ++++++++++++++++- src/apps/epic/tests/integrated/test_views.py | 63 ++++++++++++++++ src/static_src/scss/_card-deck.scss | 73 ++++++++++++++----- .../_partials/_sig_select_overlay.html | 46 +++++++++--- src/templates/apps/gameboard/room.html | 22 ++++-- 5 files changed, 231 insertions(+), 38 deletions(-) diff --git a/src/apps/epic/static/apps/epic/sig-select.js b/src/apps/epic/static/apps/epic/sig-select.js index df81ca1..5a68a49 100644 --- a/src/apps/epic/static/apps/epic/sig-select.js +++ b/src/apps/epic/static/apps/epic/sig-select.js @@ -98,6 +98,11 @@ var SigSelect = (function () { uprightSel: '#id_stat_keywords_upright', reversedSel: '#id_stat_keywords_reversed', }); + // Fill the rank-chip + title + arcana of the DRY _stat_face.html block + // (unified w. my_sign 2026-06-03) — previously the sig stat-block was a + // reduced label-only copy, so this call was absent and those fields + // stayed blank. + StageCard.populateStatExtras(statBlock, card); stageCard.style.display = ''; stage.classList.add('sig-stage--active'); @@ -113,6 +118,32 @@ var SigSelect = (function () { updateStage(cardEl); } + // ── FLIP — reveal the deck card-back (non-polarized image decks) ────── + // + // my_sign parity (its `_flipToBackAnimated`): a 500ms Y-axis half-rotation + // that swaps the front face for the deck's card-back at the midpoint. The + // back-img + .sig-flip-btn render only for non-polarized image decks + // (template gate), so this no-ops on Earthman/text decks. SPIN orientation + // (.stage-card--reversed) is preserved through the flip. `data-flipping` + // hides the in-card FLIP btn mid-rotate (shared SCSS rule). + function _flipToBack() { + if (!stageCard || stageCard.dataset.flipping) return; + stageCard.dataset.flipping = '1'; + var spin = stageCard.classList.contains('stage-card--reversed') + ? ' rotate(180deg)' : ''; + var rest = 'translateX(0px) rotateY(0deg) scale(1)' + spin; + var mid = rest + ' rotateY(90deg)'; + stageCard.animate([ + { transform: rest }, + { transform: mid, offset: 0.5 }, + { transform: rest }, + ], { duration: 500, easing: 'ease' }); + setTimeout(function () { + stageCard.classList.toggle('is-flipped-to-back'); + }, 250); + setTimeout(function () { delete stageCard.dataset.flipping; }, 500); + } + // ── Hover events ────────────────────────────────────────────────────── function onCardEnter(e) { @@ -482,8 +513,9 @@ var SigSelect = (function () { function _dismissSigOverlay() { _hideCountdown(); _hideTakeSigBtn(); - var backdrop = document.querySelector('.sig-backdrop'); - if (backdrop) backdrop.remove(); + // Removing the felt overlay reveals the hex + waiting message that sit + // behind it in the pane (no separate dark backdrop element anymore — + // the felt lives on .sig-overlay itself since the 2026-06-03 unify). if (overlay) { overlay.remove(); overlay = null; } // Remove all floating cursors (hover + thumbs-up) from the portal Object.keys(_reservedFloats).forEach(function (role) { @@ -594,9 +626,38 @@ var SigSelect = (function () { _flipBtn.addEventListener('click', function () { if (_flipBtn.classList.contains('btn-disabled')) return; statBlock.classList.toggle('is-reversed'); + // data-spinning hides the in-card FLIP btn for the 0.4s CSS rotate + // (shared `.sig-stage-card[data-spinning] .my-sign-flip-btn` rule) + // so it doesn't ride the spin between bottom-left + top-right. + stageCard.dataset.spinning = '1'; stageCard.classList.toggle('stage-card--reversed'); + setTimeout(function () { delete stageCard.dataset.spinning; }, 400); }); + // FLIP-to-back (non-polarized image decks only — btn renders just then). + var sigFlipBtn = stageCard.querySelector('.sig-flip-btn'); + if (sigFlipBtn) { + sigFlipBtn.addEventListener('click', _flipToBack); + } + + // Reserved thumbs-up + hover cursors portal to a body-root fixed + // container, so they'd hang over the reelhouse when the aperture scroll- + // snaps down to it. Drive their visibility straight off the aperture's + // scrollTop: 0 == snapped on the hex (show); any non-zero == the user + // has begun scrolling toward the reelhouse (hide). This vanishes them + // the INSTANT the scroll starts and restores them only once it lands + // fully back on the hex — user-spec timing 2026-06-03. + var _aperture = document.getElementById('id_room_aperture'); + if (_aperture) { + _aperture.addEventListener('scroll', function () { + var portal = document.getElementById('id_sig_cursor_portal'); + // .cursors-hidden hides INSTANTLY (transition:none) at scroll + // start; removing it on the return eases opacity 0→1 over 0.5s + // via the portal's base transition. + if (portal) portal.classList.toggle('cursors-hidden', _aperture.scrollTop > 0); + }, { passive: true }); + } + cautionEl = stage.querySelector('.sig-info'); cautionEffect = cautionEl.querySelector('.sig-info-effect'); cautionTitle = cautionEl.querySelector('.sig-info-title'); diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py index 212029a..eb9b142 100644 --- a/src/apps/epic/tests/integrated/test_views.py +++ b/src/apps/epic/tests/integrated/test_views.py @@ -1494,6 +1494,69 @@ class SigSelectRenderingTest(TestCase): self.assertContains(response, "fyi-next") +class SigSelectUnifiedStageTest(TestCase): + """Sig Select stage unified with the my_sign card-stage apparatus: + the shared DRY _stat_face.html stat-block (rank-chip + title + arcana, + not just keywords), per-card face-image plumbing, and the green + --duoUser felt that replaces the hex-pane content (my_sea-style) instead + of the old fixed dark-Gaussian modal. Founder is PC (levity), mid-pick.""" + + def setUp(self): + self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self) + self.url = reverse("epic:room", kwargs={"room_id": self.room.id}) + + # ── Workstream A — DRY stat-block ─────────────────────────────────────── + def test_stat_block_uses_dry_stat_face_partial(self): + # The overlay must render the SHARED _stat_face.html (rank-chip + + # title + arcana), not the old reduced label-only stat-face. + content = self.client.get(self.url).content.decode() + self.assertIn("stat-face-title", content) + self.assertIn("stat-chip-rank", content) + self.assertIn("stat-face-arcana", content) + # Keyword