diff --git a/src/apps/epic/migrations/0011_rename_earthman_court_cards.py b/src/apps/epic/migrations/0011_rename_earthman_court_cards.py new file mode 100644 index 0000000..dd09bd2 --- /dev/null +++ b/src/apps/epic/migrations/0011_rename_earthman_court_cards.py @@ -0,0 +1,82 @@ +""" +Data migration: rename Earthman court cards at positions 11 and 12. + +Old naming (from 0010): Jack (11) / Cavalier (12) +New naming: Maid (11) / Jack (12) + +Must rename 11 → Maid first so the "jack-of-*-em" slugs are free +before the 12s claim them. +""" +from django.db import migrations + + +SUITS = ["Wands", "Cups", "Swords", "Coins"] + + +def rename_court_cards(apps, schema_editor): + TarotCard = apps.get_model("epic", "TarotCard") + DeckVariant = apps.get_model("epic", "DeckVariant") + + earthman = DeckVariant.objects.filter(slug="earthman").first() + if not earthman: + return + + # Step 1: Jack (11) → Maid — frees up jack-of-*-em slugs + for suit in SUITS: + suit_slug = suit.lower() + TarotCard.objects.filter( + deck_variant=earthman, number=11, slug=f"jack-of-{suit_slug}-em" + ).update( + name=f"Maid of {suit}", + slug=f"maid-of-{suit_slug}-em", + ) + + # Step 2: Cavalier (12) → Jack — takes the now-free jack-of-*-em slugs + for suit in SUITS: + suit_slug = suit.lower() + TarotCard.objects.filter( + deck_variant=earthman, number=12, slug=f"cavalier-of-{suit_slug}-em" + ).update( + name=f"Jack of {suit}", + slug=f"jack-of-{suit_slug}-em", + ) + + +def reverse_court_cards(apps, schema_editor): + TarotCard = apps.get_model("epic", "TarotCard") + DeckVariant = apps.get_model("epic", "DeckVariant") + + earthman = DeckVariant.objects.filter(slug="earthman").first() + if not earthman: + return + + # Step 1: Jack (12) → Cavalier — frees up jack-of-*-em slugs + for suit in SUITS: + suit_slug = suit.lower() + TarotCard.objects.filter( + deck_variant=earthman, number=12, slug=f"jack-of-{suit_slug}-em" + ).update( + name=f"Cavalier of {suit}", + slug=f"cavalier-of-{suit_slug}-em", + ) + + # Step 2: Maid (11) → Jack + for suit in SUITS: + suit_slug = suit.lower() + TarotCard.objects.filter( + deck_variant=earthman, number=11, slug=f"maid-of-{suit_slug}-em" + ).update( + name=f"Jack of {suit}", + slug=f"jack-of-{suit_slug}-em", + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("epic", "0010_seed_deck_variants_and_earthman"), + ] + + operations = [ + migrations.RunPython(rename_court_cards, reverse_code=reverse_court_cards), + ] diff --git a/src/apps/epic/models.py b/src/apps/epic/models.py index e7cccc0..753c0f6 100644 --- a/src/apps/epic/models.py +++ b/src/apps/epic/models.py @@ -233,6 +233,38 @@ class TarotCard(models.Model): ordering = ["deck_variant", "arcana", "suit", "number"] unique_together = [("deck_variant", "slug")] + @staticmethod + def _to_roman(n): + if n == 0: + return '0' + val = [50, 40, 10, 9, 5, 4, 1] + syms = ['L','XL','X','IX','V','IV','I'] + result = '' + for v, s in zip(val, syms): + while n >= v: + result += s + n -= v + return result + + @property + def corner_rank(self): + 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)) + + @property + def suit_icon(self): + if self.arcana == self.MAJOR: + return '' + return { + self.WANDS: 'fa-wand-sparkles', + self.CUPS: 'fa-trophy', + self.SWORDS: 'fa-gun', + self.COINS: 'fa-sack-dollar', + self.PENTACLES: 'fa-sack-dollar', + }.get(self.suit, '') + def __str__(self): return self.name diff --git a/src/apps/gameboard/views.py b/src/apps/gameboard/views.py index b1541aa..6610a9b 100644 --- a/src/apps/gameboard/views.py +++ b/src/apps/gameboard/views.py @@ -128,7 +128,11 @@ def tarot_fan(request, deck_id): deck = get_object_or_404(DeckVariant, pk=deck_id) if not request.user.unlocked_decks.filter(pk=deck_id).exists(): return HttpResponse(status=403) - cards = list(TarotCard.objects.filter(deck_variant=deck).order_by("arcana", "number")) + _suit_order = {"WANDS": 0, "CUPS": 1, "SWORDS": 2, "COINS": 3, "PENTACLES": 4} + cards = sorted( + TarotCard.objects.filter(deck_variant=deck), + key=lambda c: (0 if c.arcana == "MAJOR" else 1, _suit_order.get(c.suit or "", 9), c.number), + ) return render(request, "apps/gameboard/_partials/_tarot_fan.html", { "deck": deck, "cards": cards, diff --git a/src/static_src/scss/_game-kit.scss b/src/static_src/scss/_game-kit.scss index 01ffd38..ce450f4 100644 --- a/src/static_src/scss/_game-kit.scss +++ b/src/static_src/scss/_game-kit.scss @@ -254,6 +254,26 @@ } } +.fan-card-corner { + position: absolute; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.15rem; + line-height: 1; + color: rgba(var(--secUser), 0.75); + + &--tl { top: 0.4rem; left: 0.4rem; } + &--br { bottom: 0.4rem; right: 0.4rem; transform: rotate(180deg); } + + .fan-corner-rank { + font-size: 1.5rem; + font-weight: bold; + padding: 0.18rem 0; + } + i { font-size: 1.5rem; } +} + .fan-card-face { padding: 1.25rem; text-align: center; diff --git a/src/templates/apps/gameboard/_partials/_tarot_fan.html b/src/templates/apps/gameboard/_partials/_tarot_fan.html index 642eda5..edfca4e 100644 --- a/src/templates/apps/gameboard/_partials/_tarot_fan.html +++ b/src/templates/apps/gameboard/_partials/_tarot_fan.html @@ -1,15 +1,19 @@ {% for card in cards %}
{{ card.number }}
{{ card.get_arcana_display }}
{% if card.correspondence %}{{ card.correspondence }}
{% endif %} - {% if card.suit %} -{{ card.suit }}
- {% endif %} +