Compare commits
3 Commits
9698d70164
...
db1608fa38
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db1608fa38 | ||
|
|
4728cde771 | ||
|
|
2f6fc1ff20 |
@@ -9,6 +9,15 @@ const initialize = (inputSelector) => {
|
||||
};
|
||||
};
|
||||
|
||||
const bindPaletteWheel = () => {
|
||||
document.querySelectorAll('.palette-scroll').forEach(el => {
|
||||
el.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
el.scrollLeft += e.deltaY;
|
||||
}, { passive: false });
|
||||
});
|
||||
};
|
||||
|
||||
const bindPaletteForms = () => {
|
||||
document.querySelectorAll('form[action*="set_palette"]').forEach(form => {
|
||||
form.addEventListener("submit", async (e) => {
|
||||
|
||||
82
src/apps/epic/migrations/0011_rename_earthman_court_cards.py
Normal file
82
src/apps/epic/migrations/0011_rename_earthman_court_cards.py
Normal file
@@ -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),
|
||||
]
|
||||
@@ -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),
|
||||
]
|
||||
55
src/apps/epic/migrations/0013_earthman_coins_to_pentacles.py
Normal file
55
src/apps/epic/migrations/0013_earthman_coins_to_pentacles.py
Normal file
@@ -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),
|
||||
]
|
||||
@@ -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),
|
||||
]
|
||||
@@ -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),
|
||||
]
|
||||
@@ -233,6 +233,52 @@ 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 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:
|
||||
return ''
|
||||
return {
|
||||
self.WANDS: 'fa-wand-sparkles',
|
||||
self.CUPS: 'fa-trophy',
|
||||
self.SWORDS: 'fa-gun',
|
||||
self.COINS: 'fa-star',
|
||||
self.PENTACLES: 'fa-star',
|
||||
}.get(self.suit, '')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@@ -84,9 +84,10 @@ function initGameKitPage() {
|
||||
updateFan();
|
||||
}
|
||||
|
||||
// Click on the dialog background (outside .tarot-fan-wrap) closes the modal
|
||||
// Click on the dark backdrop (the dialog or fan-wrap itself, not on any card child) closes
|
||||
var fanWrap = dialog.querySelector('.tarot-fan-wrap');
|
||||
dialog.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.tarot-fan-wrap')) closeFan();
|
||||
if (e.target === dialog || e.target === fanWrap) closeFan();
|
||||
});
|
||||
|
||||
// Arrow key navigation
|
||||
@@ -95,6 +96,16 @@ function initGameKitPage() {
|
||||
if (e.key === 'ArrowLeft') navigate(-1);
|
||||
});
|
||||
|
||||
// Mousewheel navigation — throttled so each detent advances one card
|
||||
var lastWheel = 0;
|
||||
dialog.addEventListener('wheel', function(e) {
|
||||
e.preventDefault();
|
||||
var now = Date.now();
|
||||
if (now - lastWheel < 150) return;
|
||||
lastWheel = now;
|
||||
navigate(e.deltaY > 0 ? 1 : -1);
|
||||
}, { passive: false });
|
||||
|
||||
prevBtn.addEventListener('click', function() { navigate(-1); });
|
||||
nextBtn.addEventListener('click', function() { navigate(1); });
|
||||
|
||||
|
||||
@@ -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, "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),
|
||||
)
|
||||
return render(request, "apps/gameboard/_partials/_tarot_fan.html", {
|
||||
"deck": deck,
|
||||
"cards": cards,
|
||||
|
||||
@@ -453,18 +453,15 @@ class GameKitPageTest(FunctionalTest):
|
||||
# Test 12 — clicking outside the modal closes it #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_clicking_outside_fan_closes_modal(self):
|
||||
def test_pressing_escape_closes_fan_modal(self):
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
|
||||
).click()
|
||||
dialog = self.browser.find_element(By.ID, "id_tarot_fan_dialog")
|
||||
self.wait_for(lambda: self.assertTrue(dialog.is_displayed()))
|
||||
# Dispatch a click directly on the dialog element (simulates clicking the dark backdrop)
|
||||
self.browser.execute_script(
|
||||
"document.getElementById('id_tarot_fan_dialog')"
|
||||
".dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}))"
|
||||
)
|
||||
dialog.send_keys(Keys.ESCAPE)
|
||||
self.wait_for(lambda: self.assertFalse(dialog.is_displayed()))
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -484,11 +481,9 @@ class GameKitPageTest(FunctionalTest):
|
||||
saved_index = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active").get_attribute("data-index")
|
||||
)
|
||||
# Close
|
||||
self.browser.execute_script(
|
||||
"document.getElementById('id_tarot_fan_dialog')"
|
||||
".dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}))"
|
||||
)
|
||||
# Close via ESC
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
self.browser.find_element(By.ID, "id_tarot_fan_dialog").send_keys(Keys.ESCAPE)
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
self.browser.find_element(By.ID, "id_tarot_fan_dialog").is_displayed()
|
||||
|
||||
@@ -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;
|
||||
@@ -261,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; }
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
window.onload = () => {
|
||||
initialize("#id_text");
|
||||
bindPaletteForms();
|
||||
bindPaletteWheel();
|
||||
};
|
||||
</script>
|
||||
@@ -1,15 +1,20 @@
|
||||
{% for card in cards %}
|
||||
<div class="fan-card" data-index="{{ forloop.counter0 }}">
|
||||
<div class="fan-card-corner fan-card-corner--tl">
|
||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
||||
</div>
|
||||
<div class="fan-card-face">
|
||||
<p class="fan-card-number">{{ card.number }}</p>
|
||||
<h3 class="fan-card-name">{{ card.name }}</h3>
|
||||
{% if card.name_group %}<p class="fan-card-name-group">{{ card.name_group }}</p>{% endif %}
|
||||
<h3 class="fan-card-name">{{ card.name_title }}</h3>
|
||||
<p class="fan-card-arcana">{{ card.get_arcana_display }}</p>
|
||||
{% if card.correspondence %}
|
||||
<p class="fan-card-correspondence">{{ card.correspondence }}</p>
|
||||
{% endif %}
|
||||
{% if card.suit %}
|
||||
<p class="fan-card-suit">{{ card.suit }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fan-card-corner fan-card-corner--br">
|
||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user