From 6d75b9541ffd838c805e147ca3dd01aa6191215b Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Tue, 28 Apr 2026 23:30:07 -0400 Subject: [PATCH] =?UTF-8?q?PICK=20SEA=20styling:=20deck=20backs,=20card=20?= =?UTF-8?q?rank+icon=20display,=20fa-hand-dots=20Major=20Arcana=20?= =?UTF-8?q?=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - migration 0010: icon='fa-hand-dots' for all Earthman Major Arcana number >= 2 (Nomad/Schizo kept empty for distinct icons later) - sea_deck view: switch from .values() to model instances; serializes corner_rank + suit_icon computed properties alongside DB fields - sea overlay JS: _fillPos() renders + HTML; tracks levity/gravity source via sea-card-slot--levity/gravity class; _reset() strips polarity classes; _showOk/_hideOk toggle sea-deck-stack--active - template: gravity deck before levity; OK btn inside .sea-stack-face (absolute center); DECKS label (vertical-rl CCW) on stacks left; Gravity/Levity names under each pile - _card-deck.scss: .sea-stacks-label (vertical-rl); .sea-stack-ok (absolute center on face); .sea-stack-name w. --quaUser/--terUser; glow on hover+:active+--active class — --ninUser for levity, --quaUser for gravity; sea-sig-card compact rank+icon display - sea_partial view: ctx['room'] fix carried in from Sprint B Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Sonnet 4.6 --- .../0010_major_arcana_hand_dots_icon.py | 49 +++++++ src/apps/epic/views.py | 23 +++- src/static_src/scss/_card-deck.scss | 120 +++++++++++++++--- .../gameboard/_partials/_sea_overlay.html | 45 ++++--- 4 files changed, 190 insertions(+), 47 deletions(-) create mode 100644 src/apps/epic/migrations/0010_major_arcana_hand_dots_icon.py diff --git a/src/apps/epic/migrations/0010_major_arcana_hand_dots_icon.py b/src/apps/epic/migrations/0010_major_arcana_hand_dots_icon.py new file mode 100644 index 0000000..d7ee1dd --- /dev/null +++ b/src/apps/epic/migrations/0010_major_arcana_hand_dots_icon.py @@ -0,0 +1,49 @@ +"""Assign fa-hand-dots icon to all Earthman Major Arcana cards with number >= 2. + +Cards 0 (The Nomad) and 1 (The Schizo) keep their existing icon value so they +can receive distinct icons later. All other Major Arcana groups (Popes, Implicit +Virtues, Elements, Realms, Explicit Virtues, Zodiac, Lunars, Planets, Inner Rings, +polarity-split finals) default to fa-hand-dots until per-group icons are assigned. +""" +from django.db import migrations + + +def assign_hand_dots(apps, schema_editor): + TarotCard = apps.get_model("epic", "TarotCard") + DeckVariant = apps.get_model("epic", "DeckVariant") + try: + earthman = DeckVariant.objects.get(slug="earthman") + except DeckVariant.DoesNotExist: + return + TarotCard.objects.filter( + deck_variant=earthman, + arcana="MAJOR", + number__gte=2, + icon="", + ).update(icon="fa-hand-dots") + + +def clear_hand_dots(apps, schema_editor): + TarotCard = apps.get_model("epic", "TarotCard") + DeckVariant = apps.get_model("epic", "DeckVariant") + try: + earthman = DeckVariant.objects.get(slug="earthman") + except DeckVariant.DoesNotExist: + return + TarotCard.objects.filter( + deck_variant=earthman, + arcana="MAJOR", + number__gte=2, + icon="fa-hand-dots", + ).update(icon="") + + +class Migration(migrations.Migration): + + dependencies = [ + ("epic", "0009_schizo_card_ref_spans"), + ] + + operations = [ + migrations.RunPython(assign_hand_dots, reverse_code=clear_hand_dots), + ] diff --git a/src/apps/epic/views.py b/src/apps/epic/views.py index 5877d25..a9c827e 100644 --- a/src/apps/epic/views.py +++ b/src/apps/epic/views.py @@ -1131,15 +1131,28 @@ def sea_deck(request, room_id): .values_list('significator_id', flat=True) ) + def _card_dict(c): + return { + 'id': c.id, + 'name': c.name, + 'arcana': c.arcana, + 'suit': c.suit, + 'number': c.number, + 'corner_rank': c.corner_rank, + 'suit_icon': c.suit_icon, + 'levity_qualifier': c.levity_qualifier, + 'gravity_qualifier': c.gravity_qualifier, + } + available = list( - TarotCard.objects.filter(deck_variant=deck) - .exclude(id__in=sig_ids) - .values('id', 'name', 'arcana', 'suit', 'number', - 'levity_qualifier', 'gravity_qualifier') + TarotCard.objects.filter(deck_variant=deck).exclude(id__in=sig_ids) ) _random.shuffle(available) mid = len(available) // 2 - return JsonResponse({'levity': available[:mid], 'gravity': available[mid:]}) + return JsonResponse({ + 'levity': [_card_dict(c) for c in available[:mid]], + 'gravity': [_card_dict(c) for c in available[mid:]], + }) @login_required diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index 5af2a53..a339b64 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -801,14 +801,29 @@ $sea-card-h: 6.5rem; .sea-card-slot--filled { border-style: solid; - border-color: rgba(var(--secUser), 0.6); + flex-direction: column; + gap: 0.2rem; + + .fan-corner-rank { + font-size: 1.15rem; + font-weight: 700; + line-height: 1; + } + i { font-size: 0.9rem; } +} + +// Levity drawn card — standard polarity (priUser bg, secUser text) +.sea-card-slot--filled.sea-card-slot--levity { background: rgba(var(--priUser), 1); - color: rgba(var(--terUser), 1); - font-size: 0.55rem; - font-weight: 600; - text-align: center; - padding: 0.2rem; - line-height: 1.2; + border-color: rgba(var(--secUser), 0.6); + color: rgba(var(--secUser), 0.85); +} + +// Gravity drawn card — inverted polarity (secUser bg, priUser text) +.sea-card-slot--filled.sea-card-slot--gravity { + background: rgba(var(--secUser), 0.9); + border-color: rgba(var(--priUser), 0.4); + color: rgba(var(--priUser), 0.9); } // Cover + Cross — absolutely overlaid on the Sig card in .sea-pos-center @@ -828,6 +843,23 @@ $sea-card-h: 6.5rem; .sea-pos-cross .sea-card-slot { transform: rotate(90deg); } +// Sig card in center slot — compact rank + icon display +.sea-sig-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.2rem; + + .fan-corner-rank { + font-size: 1.2rem; + font-weight: 700; + line-height: 1; + color: rgba(var(--secUser), 0.85); + } + i { font-size: 1rem; color: rgba(var(--secUser), 0.75); } +} + // .sig-stage-card is normally scoped inside .sig-stage — re-apply the card shell // here so it renders correctly outside that context. .sea-cross .sig-stage-card { @@ -896,40 +928,86 @@ $sea-card-h: 6.5rem; option { background: rgba(var(--priUser), 1); } } -// Deck stacks — two face-down piles side by side +// Deck stacks — DECKS label + gravity + levity piles .sea-stacks { display: flex; - gap: 1rem; - justify-content: center; + align-items: center; + gap: 0.75rem; margin: 1rem 0; } +.sea-stacks-label { + writing-mode: vertical-rl; + transform: rotate(180deg); + text-transform: uppercase; + font-size: 0.6rem; + letter-spacing: 0.15em; + opacity: 0.45; + white-space: nowrap; + flex-shrink: 0; +} + .sea-deck-stack { position: relative; display: flex; flex-direction: column; align-items: center; - gap: 0.4rem; + gap: 0.35rem; cursor: pointer; } .sea-stack-face { + position: relative; width: $sea-card-w; height: $sea-card-h; border-radius: 0.3rem; - background: rgba(var(--duoUser), 0.8); - border: 0.12rem solid rgba(var(--secUser), 0.5); - box-shadow: 0 2px 0 rgba(0,0,0,0.2), 0 4px 0 rgba(var(--duoUser),0.7), 0 5px 0 rgba(0,0,0,0.15); + border: 0.15rem solid; + display: flex; + align-items: center; + justify-content: center; transition: box-shadow 0.15s; - - .sea-deck-stack:hover & { - box-shadow: 0 2px 0 rgba(0,0,0,0.2), 0 4px 0 rgba(var(--duoUser),0.7), 0 5px 0 rgba(0,0,0,0.15), - 0 0 0.5rem rgba(var(--terUser), 0.4); - } } -.sea-deck-stack--levity .sea-stack-face { border-color: rgba(var(--terUser), 0.5); } -.sea-deck-stack--gravity .sea-stack-face { border-color: rgba(var(--quaUser), 0.5); } +.sea-stack-ok { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 5; +} + +.sea-stack-name { + font-size: 0.6rem; + letter-spacing: 0.06em; + text-transform: uppercase; + font-weight: 600; +} +.sea-deck-stack--gravity .sea-stack-name { color: rgba(var(--quaUser), 1); } +.sea-deck-stack--levity .sea-stack-name { color: rgba(var(--terUser), 1); } + +// Deck backs — face-down pile colour identifies polarity +$_sea-shadow: 1px 2px 0 rgba(0,0,0,0.7), 0 4px 0 rgba(0,0,0,0.18), 2px 5px 5px rgba(0,0,0,0.5); +$_glow-levity: 0 0 0.8rem 0.15rem rgba(var(--ninUser), 0.6); +$_glow-gravity: 0 0 0.8rem 0.15rem rgba(var(--quaUser), 0.6); + +.sea-deck-stack--levity .sea-stack-face { + background: rgba(var(--terUser), 0.88); + border-color: rgba(var(--ninUser), 0.65); + box-shadow: $_sea-shadow; +} +.sea-deck-stack--gravity .sea-stack-face { + background: rgba(var(--quiUser), 0.88); + border-color: rgba(var(--quaUser), 0.65); + box-shadow: $_sea-shadow; +} + +// Glow on hover, :active, and while OK is showing (--active class set by JS) +.sea-deck-stack--levity:hover .sea-stack-face, +.sea-deck-stack--levity:active .sea-stack-face, +.sea-deck-stack--levity.sea-deck-stack--active .sea-stack-face { box-shadow: $_sea-shadow, $_glow-levity; } +.sea-deck-stack--gravity:hover .sea-stack-face, +.sea-deck-stack--gravity:active .sea-stack-face, +.sea-deck-stack--gravity.sea-deck-stack--active .sea-stack-face { box-shadow: $_sea-shadow, $_glow-gravity; } // Form action row — LOCK HAND + DEL side by side at the bottom .sea-form-actions { diff --git a/src/templates/apps/gameboard/_partials/_sea_overlay.html b/src/templates/apps/gameboard/_partials/_sea_overlay.html index b4c0b85..a32d116 100644 --- a/src/templates/apps/gameboard/_partials/_sea_overlay.html +++ b/src/templates/apps/gameboard/_partials/_sea_overlay.html @@ -30,17 +30,10 @@ {# Center — Significator (always placed) + Cover + Cross overlaid #}
-
+
{% if my_tray_sig %} -
- {% if my_tray_sig.arcana == "MAJOR" %} -

{{ my_tray_sig.name_title }}

-

{% if user_polarity == "levity" %}{{ my_tray_sig.levity_qualifier }}{% else %}{{ my_tray_sig.gravity_qualifier }}{% endif %}

- {% else %} -

{% if user_polarity == "levity" %}{{ my_tray_sig.levity_qualifier }}{% else %}{{ my_tray_sig.gravity_qualifier }}{% endif %}

-

{{ my_tray_sig.name_title }}

- {% endif %} -
+ {{ my_tray_sig.corner_rank }} + {% if my_tray_sig.suit_icon %}{% endif %} {% endif %}
{# Cover — CC/EV pos 1, stacked face-up on Sig #} @@ -82,13 +75,18 @@ {# Two face-down deck piles — tap to proffer OK #}
-
-
- -
+ DECKS
-
- +
+ +
+ Gravity +
+
+
+ +
+ Levity
@@ -163,6 +161,7 @@ if (_activeStack) { const ok = _activeStack.querySelector('.sea-stack-ok'); if (ok) ok.style.display = 'none'; + _activeStack.classList.remove('sea-deck-stack--active'); _activeStack = null; } } @@ -170,19 +169,23 @@ function _showOk(stack) { _hideOk(); _activeStack = stack; + stack.classList.add('sea-deck-stack--active'); const ok = stack.querySelector('.sea-stack-ok'); if (ok) ok.style.display = ''; } - function _fillPos(sel, card) { + function _fillPos(sel, card, isLevity) { const cell = overlay.querySelector(sel); if (!cell) return; const slot = cell.querySelector('.sea-card-slot'); if (!slot) return; slot.classList.remove('sea-card-slot--empty'); slot.classList.add('sea-card-slot--filled'); + slot.classList.add(isLevity ? 'sea-card-slot--levity' : 'sea-card-slot--gravity'); slot.dataset.cardId = String(card.id); - slot.textContent = card.name; + slot.innerHTML = + `${card.corner_rank}` + + (card.suit_icon ? `` : ''); _filled++; if (lockBtn) lockBtn.disabled = (_filled < 6); } @@ -191,9 +194,9 @@ _filled = 0; _hideOk(); overlay.querySelectorAll('.sea-card-slot').forEach(s => { - s.classList.remove('sea-card-slot--filled'); + s.classList.remove('sea-card-slot--filled', 'sea-card-slot--levity', 'sea-card-slot--gravity'); s.classList.add('sea-card-slot--empty'); - s.textContent = ''; + s.innerHTML = ''; delete s.dataset.cardId; }); if (lockBtn) lockBtn.disabled = true; @@ -221,7 +224,7 @@ const pile = isLevity ? levityPile : gravityPile; const card = pile.length ? pile.shift() : null; const pos = _nextPosSelector(); - if (card && pos) _fillPos(pos, card); + if (card && pos) _fillPos(pos, card, isLevity); _hideOk(); }); }