tarot card icons + ranks; sig fallback for pre-sync Characters; DECKS label sizing — TDD
- migration 0011: The Nomad (0) → fa-hat-cowboy; The Schizo (1) → fa-hat-wizard - corner_rank: non-MAJOR pip card 1 → 'A' (Ace); court unchanged (M/J/Q/K); TDD - 17 unit model tests for corner_rank + suit_icon - _role_select_context: my_tray_sig falls back to seat.significator when confirmed_char.significator is None (Characters created before natus_save sync) - _card-deck.scss: DECKS label bigger (1rem, 0.32em letter-spacing) to fill stack height; sea-stack-name: opacity 0.6, scaleY(1.5), margin-top -0.4rem partially under face; sea-stack-face z-index:1 Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
43
src/apps/epic/migrations/0011_nomad_schizo_icons.py
Normal file
43
src/apps/epic/migrations/0011_nomad_schizo_icons.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Assign individual icons to The Nomad (0) and The Schizo (1).
|
||||
|
||||
All other Major Arcana already have fa-hand-dots from migration 0010.
|
||||
"""
|
||||
from django.db import migrations
|
||||
|
||||
ICONS = {0: 'fa-hat-cowboy', 1: 'fa-hat-wizard'}
|
||||
|
||||
|
||||
def assign_icons(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
|
||||
for number, icon in ICONS.items():
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MAJOR", number=number
|
||||
).update(icon=icon)
|
||||
|
||||
|
||||
def clear_icons(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__in=list(ICONS.keys())
|
||||
).update(icon="")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("epic", "0010_major_arcana_hand_dots_icon"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(assign_icons, reverse_code=clear_icons),
|
||||
]
|
||||
@@ -285,7 +285,9 @@ class TarotCard(models.Model):
|
||||
if self.arcana == self.MAJOR:
|
||||
return self._to_roman(self.number)
|
||||
court = {11: 'M', 12: 'J', 13: 'Q', 14: 'K'}
|
||||
return court.get(self.number, str(self.number))
|
||||
if self.number in court:
|
||||
return court[self.number]
|
||||
return 'A' if self.number == 1 else str(self.number)
|
||||
|
||||
def emanation_for(self, polarity):
|
||||
"""Return the upright title for a given polarity ('levity' or 'gravity').
|
||||
|
||||
@@ -1855,6 +1855,16 @@ class PickSeaRenderingTest(TestCase):
|
||||
self.assertIn("user_polarity", response.context)
|
||||
self.assertEqual(response.context["user_polarity"], "levity") # PC is levity
|
||||
|
||||
def test_my_tray_sig_falls_back_to_seat_when_char_sig_is_none(self):
|
||||
"""Characters created before the sig-sync fix have significator=None; fall back to seat."""
|
||||
Character.objects.create(
|
||||
seat=self.pc_seat,
|
||||
significator=None,
|
||||
confirmed_at=timezone.now(),
|
||||
)
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.context["my_tray_sig"], self.sig_card)
|
||||
|
||||
def test_my_tray_sig_comes_from_character_significator_when_confirmed(self):
|
||||
"""When sky_confirmed, my_tray_sig reads from Character.significator (not TableSeat)."""
|
||||
char = Character.objects.create(
|
||||
|
||||
72
src/apps/epic/tests/unit/test_models.py
Normal file
72
src/apps/epic/tests/unit/test_models.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from apps.epic.models import TarotCard
|
||||
|
||||
|
||||
def _card(arcana, number, suit='', icon=''):
|
||||
c = TarotCard()
|
||||
c.arcana = arcana
|
||||
c.number = number
|
||||
c.suit = suit
|
||||
c.icon = icon
|
||||
return c
|
||||
|
||||
|
||||
class TarotCardCornerRankTest(SimpleTestCase):
|
||||
"""TarotCard.corner_rank — alphanumeric display labels."""
|
||||
|
||||
def test_major_arcana_0_gives_0(self):
|
||||
self.assertEqual(_card('MAJOR', 0).corner_rank, '0')
|
||||
|
||||
def test_major_arcana_1_gives_roman_I(self):
|
||||
self.assertEqual(_card('MAJOR', 1).corner_rank, 'I')
|
||||
|
||||
def test_major_arcana_2_gives_roman_II(self):
|
||||
self.assertEqual(_card('MAJOR', 2).corner_rank, 'II')
|
||||
|
||||
def test_non_major_pip_1_gives_A(self):
|
||||
"""Ace — pip card number 1 should show 'A', not '1'."""
|
||||
self.assertEqual(_card('MIDDLE', 1, 'BRANDS').corner_rank, 'A')
|
||||
|
||||
def test_non_major_pip_2_gives_2(self):
|
||||
self.assertEqual(_card('MIDDLE', 2, 'BRANDS').corner_rank, '2')
|
||||
|
||||
def test_non_major_pip_10_gives_10(self):
|
||||
self.assertEqual(_card('MIDDLE', 10, 'CROWNS').corner_rank, '10')
|
||||
|
||||
def test_court_maid_gives_M(self):
|
||||
self.assertEqual(_card('MIDDLE', 11, 'GRAILS').corner_rank, 'M')
|
||||
|
||||
def test_court_jack_gives_J(self):
|
||||
self.assertEqual(_card('MIDDLE', 12, 'BLADES').corner_rank, 'J')
|
||||
|
||||
def test_court_queen_gives_Q(self):
|
||||
self.assertEqual(_card('MIDDLE', 13, 'BRANDS').corner_rank, 'Q')
|
||||
|
||||
def test_court_king_gives_K(self):
|
||||
self.assertEqual(_card('MIDDLE', 14, 'CROWNS').corner_rank, 'K')
|
||||
|
||||
|
||||
class TarotCardSuitIconTest(SimpleTestCase):
|
||||
"""TarotCard.suit_icon — icon class resolution."""
|
||||
|
||||
def test_major_with_icon_returns_icon(self):
|
||||
self.assertEqual(_card('MAJOR', 0, icon='fa-hat-cowboy').suit_icon, 'fa-hat-cowboy')
|
||||
|
||||
def test_major_without_icon_returns_empty(self):
|
||||
self.assertEqual(_card('MAJOR', 5).suit_icon, '')
|
||||
|
||||
def test_brands_returns_wand_sparkles(self):
|
||||
self.assertEqual(_card('MIDDLE', 11, 'BRANDS').suit_icon, 'fa-wand-sparkles')
|
||||
|
||||
def test_grails_returns_trophy(self):
|
||||
self.assertEqual(_card('MIDDLE', 11, 'GRAILS').suit_icon, 'fa-trophy')
|
||||
|
||||
def test_blades_returns_gun(self):
|
||||
self.assertEqual(_card('MIDDLE', 11, 'BLADES').suit_icon, 'fa-gun')
|
||||
|
||||
def test_crowns_returns_crown(self):
|
||||
self.assertEqual(_card('MIDDLE', 11, 'CROWNS').suit_icon, 'fa-crown')
|
||||
|
||||
def test_icon_override_takes_priority_over_suit(self):
|
||||
self.assertEqual(_card('MIDDLE', 11, 'CROWNS', icon='fa-star').suit_icon, 'fa-star')
|
||||
@@ -371,7 +371,8 @@ def _role_select_context(room, user):
|
||||
ctx["sky_confirmed"] = sky_confirmed
|
||||
ctx["user_seat_role"] = _canonical_seat.role if _canonical_seat else ''
|
||||
if sky_confirmed:
|
||||
ctx["my_tray_sig"] = confirmed_char.significator
|
||||
# Fall back to seat.significator for Characters created before the sync was added
|
||||
ctx["my_tray_sig"] = confirmed_char.significator or _canonical_seat.significator
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -940,11 +940,14 @@ $sea-card-h: 6.5rem;
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.6rem;
|
||||
letter-spacing: 0.15em;
|
||||
opacity: 0.45;
|
||||
// Fill the full card height ($sea-card-h: 6.5rem) with 5 letters
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.32em;
|
||||
font-weight: 700;
|
||||
opacity: 0.5;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.sea-deck-stack {
|
||||
@@ -966,6 +969,7 @@ $sea-card-h: 6.5rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: box-shadow 0.15s;
|
||||
z-index: 1; // sits above the name label
|
||||
}
|
||||
|
||||
.sea-stack-ok {
|
||||
@@ -976,11 +980,19 @@ $sea-card-h: 6.5rem;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.sea-deck-stack { gap: 0; } // remove gap so name slides under the face
|
||||
|
||||
.sea-stack-name {
|
||||
font-size: 0.6rem;
|
||||
letter-spacing: 0.06em;
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
opacity: 0.6;
|
||||
// Pull top of label partially under the stack face
|
||||
// margin-top: -0.1rem;
|
||||
transform: scaleY(1.2);
|
||||
transform-origin: top center;
|
||||
z-index: 0;
|
||||
}
|
||||
.sea-deck-stack--gravity .sea-stack-name { color: rgba(var(--quaUser), 1); }
|
||||
.sea-deck-stack--levity .sea-stack-name { color: rgba(var(--terUser), 1); }
|
||||
|
||||
Reference in New Issue
Block a user