diff --git a/src/apps/epic/migrations/0012_rename_earthman_major_groups_and_pip_spellings.py b/src/apps/epic/migrations/0012_rename_earthman_major_groups_and_pip_spellings.py new file mode 100644 index 0000000..6d199eb --- /dev/null +++ b/src/apps/epic/migrations/0012_rename_earthman_major_groups_and_pip_spellings.py @@ -0,0 +1,162 @@ +""" +Data migration: + 1. Rename grouped Earthman major arcana to use group-relative ordinals + (e.g. "Virtue VI: Controlled Folly" → "Implicit Virtue 1: Controlled Folly"). + 2. Spell out Earthman minor arcana pip names 2–10 + (e.g. "2 of Wands" → "Two of Wands"). + +Corner ranks (Roman numerals of absolute card number) are a property on the model +and are unchanged — this only affects the stored name / slug fields. +""" +from django.db import migrations + + +# ── Major arcana: (new_name, new_slug) keyed by card number ───────────────── + +MAJOR_RENAMES = { + # Implicit Virtues (cards 6–9) + 6: ("Implicit Virtue 1: Controlled Folly", "implicit-virtue-1-controlled-folly"), + 7: ("Implicit Virtue 2: Not-Doing", "implicit-virtue-2-not-doing"), + 8: ("Implicit Virtue 3: Losing Self-Importance", "implicit-virtue-3-losing-self-importance"), + 9: ("Implicit Virtue 4: Erasing Personal History", "implicit-virtue-4-erasing-personal-history"), + # Explicit Virtues (cards 18–20) + 18: ("Explicit Virtue 1: Stalking", "explicit-virtue-1-stalking"), + 19: ("Explicit Virtue 2: Intent", "explicit-virtue-2-intent"), + 20: ("Explicit Virtue 3: Dreaming", "explicit-virtue-3-dreaming"), + # Classical Elements (cards 21–24) + 21: ("Classical Element 1: Fire", "classical-element-1-fire"), + 22: ("Classical Element 2: Earth", "classical-element-2-earth"), + 23: ("Classical Element 3: Air", "classical-element-3-air"), + 24: ("Classical Element 4: Water", "classical-element-4-water"), + # Zodiac (cards 25–36) + 25: ("Zodiac 1: Aries", "zodiac-1-aries"), + 26: ("Zodiac 2: Taurus", "zodiac-2-taurus"), + 27: ("Zodiac 3: Gemini", "zodiac-3-gemini"), + 28: ("Zodiac 4: Cancer", "zodiac-4-cancer"), + 29: ("Zodiac 5: Leo", "zodiac-5-leo"), + 30: ("Zodiac 6: Virgo", "zodiac-6-virgo"), + 31: ("Zodiac 7: Libra", "zodiac-7-libra"), + 32: ("Zodiac 8: Scorpio", "zodiac-8-scorpio"), + 33: ("Zodiac 9: Sagittarius", "zodiac-9-sagittarius"), + 34: ("Zodiac 10: Capricorn", "zodiac-10-capricorn"), + 35: ("Zodiac 11: Aquarius", "zodiac-11-aquarius"), + 36: ("Zodiac 12: Pisces", "zodiac-12-pisces"), + # Absolute Elements (cards 37–38) + 37: ("Absolute Element 1: Time", "absolute-element-1-time"), + 38: ("Absolute Element 2: Space", "absolute-element-2-space"), + # Wanderers (cards 39–49) + 39: ("Wanderer 1: The Polestar", "wanderer-1-polestar"), + 40: ("Wanderer 2: The Antichthon", "wanderer-2-antichthon"), + 41: ("Wanderer 3: The Corestar", "wanderer-3-corestar"), + 42: ("Wanderer 4: Mercury", "wanderer-4-mercury"), + 43: ("Wanderer 5: Venus", "wanderer-5-venus"), + 44: ("Wanderer 6: Mars", "wanderer-6-mars"), + 45: ("Wanderer 7: Jupiter", "wanderer-7-jupiter"), + 46: ("Wanderer 8: Saturn", "wanderer-8-saturn"), + 47: ("Wanderer 9: Uranus", "wanderer-9-uranus"), + 48: ("Wanderer 10: Neptune", "wanderer-10-neptune"), + 49: ("Wanderer 11: The King & Queen of Hades", "wanderer-11-king-queen-hades"), +} + +# Original (name, slug) pairs for reversal +MAJOR_ORIGINALS = { + 6: ("Virtue VI: Controlled Folly", "virtue-vi-controlled-folly"), + 7: ("Virtue VII: Not-Doing", "virtue-vii-not-doing"), + 8: ("Virtue VIII: Losing Self-Importance", "virtue-viii-losing-self-importance"), + 9: ("Virtue IX: Erasing Personal History", "virtue-ix-erasing-personal-history"), + 18: ("Virtue XVIII: Stalking", "virtue-xviii-stalking"), + 19: ("Virtue XIX: Intent", "virtue-xix-intent"), + 20: ("Virtue XX: Dreaming", "virtue-xx-dreaming"), + 21: ("Element XXI: Fire", "element-xxi-fire"), + 22: ("Element XXII: Earth", "element-xxii-earth"), + 23: ("Element XXIII: Air", "element-xxiii-air"), + 24: ("Element XXIV: Water", "element-xxiv-water"), + 25: ("Zodiac XXV: Aries", "zodiac-xxv-aries"), + 26: ("Zodiac XXVI: Taurus", "zodiac-xxvi-taurus"), + 27: ("Zodiac XXVII: Gemini", "zodiac-xxvii-gemini"), + 28: ("Zodiac XXVIII: Cancer", "zodiac-xxviii-cancer"), + 29: ("Zodiac XXIX: Leo", "zodiac-xxix-leo"), + 30: ("Zodiac XXX: Virgo", "zodiac-xxx-virgo"), + 31: ("Zodiac XXXI: Libra", "zodiac-xxxi-libra"), + 32: ("Zodiac XXXII: Scorpio", "zodiac-xxxii-scorpio"), + 33: ("Zodiac XXXIII: Sagittarius", "zodiac-xxxiii-sagittarius"), + 34: ("Zodiac XXXIV: Capricorn", "zodiac-xxxiv-capricorn"), + 35: ("Zodiac XXXV: Aquarius", "zodiac-xxxv-aquarius"), + 36: ("Zodiac XXXVI: Pisces", "zodiac-xxxvi-pisces"), + 37: ("Element XXXVII: Time", "element-xxxvii-time"), + 38: ("Element XXXVIII: Space", "element-xxxviii-space"), + 39: ("Wanderer XXXIX: The Polestar", "wanderer-xxxix-polestar"), + 40: ("Wanderer XL: The Antichthon", "wanderer-xl-antichthon"), + 41: ("Wanderer XLI: The Corestar", "wanderer-xli-corestar"), + 42: ("Wanderer XLII: Mercury", "wanderer-xlii-mercury"), + 43: ("Wanderer XLIII: Venus", "wanderer-xliii-venus"), + 44: ("Wanderer XLIV: Mars", "wanderer-xliv-mars"), + 45: ("Wanderer XLV: Jupiter", "wanderer-xlv-jupiter"), + 46: ("Wanderer XLVI: Saturn", "wanderer-xlvi-saturn"), + 47: ("Wanderer XLVII: Uranus", "wanderer-xlvii-uranus"), + 48: ("Wanderer XLVIII: Neptune", "wanderer-xlviii-neptune"), + 49: ("Wanderer XLIX: The King & Queen of Hades", "wanderer-xlix-king-queen-hades"), +} + +# Pip number → spelled-out word (slugs already use the word form, only name changes) +PIP_SPELLINGS = { + 2: "Two", 3: "Three", 4: "Four", 5: "Five", + 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine", 10: "Ten", +} + +SUITS = ["WANDS", "CUPS", "SWORDS", "COINS"] + + +def rename_forward(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 + + # 1. Rename grouped major arcana to group-relative ordinals + for number, (new_name, new_slug) in MAJOR_RENAMES.items(): + TarotCard.objects.filter( + deck_variant=earthman, arcana="MAJOR", number=number + ).update(name=new_name, slug=new_slug) + + # 2. Spell out pip names 2–10 + for number, word in PIP_SPELLINGS.items(): + for suit in SUITS: + TarotCard.objects.filter( + deck_variant=earthman, arcana="MINOR", suit=suit, number=number + ).update(name=f"{word} of {suit.capitalize()}") + + +def rename_reverse(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 + + # 1. Restore original major arcana names + for number, (old_name, old_slug) in MAJOR_ORIGINALS.items(): + TarotCard.objects.filter( + deck_variant=earthman, arcana="MAJOR", number=number + ).update(name=old_name, slug=old_slug) + + # 2. Restore numeric pip names (slugs unchanged) + for number, _word in PIP_SPELLINGS.items(): + for suit in SUITS: + TarotCard.objects.filter( + deck_variant=earthman, arcana="MINOR", suit=suit, number=number + ).update(name=f"{number} of {suit.capitalize()}") + + +class Migration(migrations.Migration): + + dependencies = [ + ("epic", "0011_rename_earthman_court_cards"), + ] + + operations = [ + migrations.RunPython(rename_forward, reverse_code=rename_reverse), + ] diff --git a/src/apps/epic/migrations/0013_earthman_coins_to_pentacles.py b/src/apps/epic/migrations/0013_earthman_coins_to_pentacles.py new file mode 100644 index 0000000..7c84e18 --- /dev/null +++ b/src/apps/epic/migrations/0013_earthman_coins_to_pentacles.py @@ -0,0 +1,55 @@ +""" +Data migration: rename Earthman 4th-suit cards from COINS → PENTACLES. + +Updates: + - suit field: "COINS" → "PENTACLES" + - name: "X of Coins" → "X of Pentacles" + - slug: "x-of-coins-em" → "x-of-pentacles-em" +""" +from django.db import migrations + + +def coins_to_pentacles(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 + + cards = TarotCard.objects.filter(deck_variant=earthman, suit="COINS") + for card in cards: + card.suit = "PENTACLES" + card.name = card.name.replace(" of Coins", " of Pentacles") + card.slug = card.slug.replace("-of-coins-em", "-of-pentacles-em") + card.save(update_fields=["suit", "name", "slug"]) + + +def pentacles_to_coins(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 + + # Only reverse cards that came from Earthman (identified by -em slug suffix) + cards = TarotCard.objects.filter( + deck_variant=earthman, suit="PENTACLES", slug__endswith="-em" + ) + for card in cards: + card.suit = "COINS" + card.name = card.name.replace(" of Pentacles", " of Coins") + card.slug = card.slug.replace("-of-pentacles-em", "-of-coins-em") + card.save(update_fields=["suit", "name", "slug"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("epic", "0012_rename_earthman_major_groups_and_pip_spellings"), + ] + + operations = [ + migrations.RunPython(coins_to_pentacles, reverse_code=pentacles_to_coins), + ] diff --git a/src/apps/epic/migrations/0014_rename_earthman_popes_arabic_ordinals.py b/src/apps/epic/migrations/0014_rename_earthman_popes_arabic_ordinals.py new file mode 100644 index 0000000..760d39a --- /dev/null +++ b/src/apps/epic/migrations/0014_rename_earthman_popes_arabic_ordinals.py @@ -0,0 +1,65 @@ +""" +Data migration: rename the five Pope cards to use Arabic group-relative ordinals, +matching the convention set for other grouped major arcana. + + "Pope I: President" → "Pope 1: President" + "Pope II: Tsar" → "Pope 2: Tsar" + etc. +""" +from django.db import migrations + + +POPE_RENAMES = { + 1: ("Pope 1: President", "pope-1-president"), + 2: ("Pope 2: Tsar", "pope-2-tsar"), + 3: ("Pope 3: Chairman", "pope-3-chairman"), + 4: ("Pope 4: Emperor", "pope-4-emperor"), + 5: ("Pope 5: Chancellor", "pope-5-chancellor"), +} + +POPE_ORIGINALS = { + 1: ("Pope I: President", "pope-i-president"), + 2: ("Pope II: Tsar", "pope-ii-tsar"), + 3: ("Pope III: Chairman", "pope-iii-chairman"), + 4: ("Pope IV: Emperor", "pope-iv-emperor"), + 5: ("Pope V: Chancellor", "pope-v-chancellor"), +} + + +def rename_forward(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 + + for number, (new_name, new_slug) in POPE_RENAMES.items(): + TarotCard.objects.filter( + deck_variant=earthman, arcana="MAJOR", number=number + ).update(name=new_name, slug=new_slug) + + +def rename_reverse(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 + + for number, (old_name, old_slug) in POPE_ORIGINALS.items(): + TarotCard.objects.filter( + deck_variant=earthman, arcana="MAJOR", number=number + ).update(name=old_name, slug=old_slug) + + +class Migration(migrations.Migration): + + dependencies = [ + ("epic", "0013_earthman_coins_to_pentacles"), + ] + + operations = [ + migrations.RunPython(rename_forward, reverse_code=rename_reverse), + ] diff --git a/src/apps/epic/migrations/0015_rename_classical_element_earth_to_stone.py b/src/apps/epic/migrations/0015_rename_classical_element_earth_to_stone.py new file mode 100644 index 0000000..d4b7b5e --- /dev/null +++ b/src/apps/epic/migrations/0015_rename_classical_element_earth_to_stone.py @@ -0,0 +1,42 @@ +""" +Data migration: rename Earthman card 22 from "Classical Element 2: Earth" +to "Classical Element 2: Stone" (Stone = Ossum, the Earthman name for Earth). +""" +from django.db import migrations + + +def rename_forward(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 + + TarotCard.objects.filter( + deck_variant=earthman, arcana="MAJOR", number=22 + ).update(name="Classical Element 2: Stone", slug="classical-element-2-stone") + + +def rename_reverse(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 + + TarotCard.objects.filter( + deck_variant=earthman, arcana="MAJOR", number=22 + ).update(name="Classical Element 2: Earth", slug="classical-element-2-earth") + + +class Migration(migrations.Migration): + + dependencies = [ + ("epic", "0014_rename_earthman_popes_arabic_ordinals"), + ] + + operations = [ + migrations.RunPython(rename_forward, reverse_code=rename_reverse), + ] diff --git a/src/apps/epic/models.py b/src/apps/epic/models.py index 753c0f6..0c97535 100644 --- a/src/apps/epic/models.py +++ b/src/apps/epic/models.py @@ -253,6 +253,20 @@ class TarotCard(models.Model): court = {11: 'M', 12: 'J', 13: 'Q', 14: 'K'} return court.get(self.number, str(self.number)) + @property + def name_group(self): + """Returns 'Group N:' prefix if the name contains ': ', else ''.""" + if ': ' in self.name: + return self.name.split(': ', 1)[0] + ':' + return '' + + @property + def name_title(self): + """Returns the title after 'Group N: ', or the full name if no colon.""" + if ': ' in self.name: + return self.name.split(': ', 1)[1] + return self.name + @property def suit_icon(self): if self.arcana == self.MAJOR: @@ -261,8 +275,8 @@ class TarotCard(models.Model): self.WANDS: 'fa-wand-sparkles', self.CUPS: 'fa-trophy', self.SWORDS: 'fa-gun', - self.COINS: 'fa-sack-dollar', - self.PENTACLES: 'fa-sack-dollar', + self.COINS: 'fa-star', + self.PENTACLES: 'fa-star', }.get(self.suit, '') def __str__(self): diff --git a/src/apps/gameboard/views.py b/src/apps/gameboard/views.py index 6610a9b..323d8a5 100644 --- a/src/apps/gameboard/views.py +++ b/src/apps/gameboard/views.py @@ -128,7 +128,7 @@ 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) - _suit_order = {"WANDS": 0, "CUPS": 1, "SWORDS": 2, "COINS": 3, "PENTACLES": 4} + _suit_order = {"WANDS": 0, "CUPS": 1, "SWORDS": 2, "PENTACLES": 3, "COINS": 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), diff --git a/src/static_src/scss/_game-kit.scss b/src/static_src/scss/_game-kit.scss index ce450f4..f6206a2 100644 --- a/src/static_src/scss/_game-kit.scss +++ b/src/static_src/scss/_game-kit.scss @@ -281,8 +281,9 @@ flex-direction: column; gap: 0.5rem; - .fan-card-number { font-size: 0.65rem; opacity: 0.5; } - .fan-card-name { font-size: 0.95rem; font-weight: bold; margin: 0; } + .fan-card-number { font-size: 0.65rem; opacity: 0.5; } + .fan-card-name-group { font-size: 0.65rem; opacity: 0.5; margin: 0; text-transform: uppercase; letter-spacing: 0.08em; } + .fan-card-name { font-size: 0.95rem; font-weight: bold; margin: 0; } .fan-card-arcana { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.1em; opacity: 0.6; } .fan-card-correspondence { font-size: 0.6rem; opacity: 0.45; font-style: italic; } } diff --git a/src/templates/apps/gameboard/_partials/_tarot_fan.html b/src/templates/apps/gameboard/_partials/_tarot_fan.html index edfca4e..b259d4f 100644 --- a/src/templates/apps/gameboard/_partials/_tarot_fan.html +++ b/src/templates/apps/gameboard/_partials/_tarot_fan.html @@ -5,7 +5,8 @@ {% if card.suit_icon %}{% endif %}
-

{{ card.name }}

+ {% if card.name_group %}

{{ card.name_group }}

{% endif %} +

{{ card.name_title }}

{{ card.get_arcana_display }}

{% if card.correspondence %}

{{ card.correspondence }}