Sig select: caution tooltip, FLIP/FYI stat block, keyword display

- TarotCard.cautions JSONField + cautions_json property; migrations
  0027–0029 seed The Schizo (number=1) with 4 rival-interaction cautions
  (Roman-numeral card refs: I. The Pervert / II. The Occultist, etc.)
- Sig-select overlay: FLIP (stat-block toggle) + FYI (caution tooltip)
  buttons; nav PRV/NXT portaled outside tooltip at bottom corners (z-70);
  caution tooltip covers stat block (inset:0, z-60, Gaussian blur);
  tooltip click dismisses; FLIP/FYI fully dead while btn-disabled;
  nav wraps circularly (4/4 → 1/4, 1/4 → 4/4)
- SCSS: btn-disabled specificity fix (!important); btn-nav-left/right
  classes; sig-caution-* layout; stat-face keyword lists
- Jasmine suite expanded: stat block + FLIP (5 specs), caution tooltip
  (16 specs) including wrap-around and disabled-button behaviour
- IT tests: TarotCardCautionsTest (5), SigSelectRenderingTest (8)
- Role-card SVG icons added to static/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-07 00:22:04 -04:00
parent e2cc38686f
commit 520fdf7862
24 changed files with 1936 additions and 54 deletions

View File

@@ -0,0 +1,154 @@
"""
Data migration — Earthman deck:
1. Rename three suit codes (and card names) for Earthman cards:
WANDS → BRANDS (Wands → Brands)
CUPS → GRAILS (Cups → Grails)
SWORDS → BLADES (Swords → Blades)
CROWNS stays CROWNS.
2. Copy keywords_upright / keywords_reversed from the Fiorentine Minchiate
deck to corresponding Earthman cards:
• Major: explicit number-to-number map based on card correspondences.
• Minor/Middle: same number, suit mapped (BRANDS→WANDS, GRAILS→CUPS,
BLADES→SWORDS, CROWNS→PENTACLES). Cards with no Fiorentine counterpart
stay with empty keyword lists.
"""
from django.db import migrations
# ── 1. Suit rename map ────────────────────────────────────────────────────────
SUIT_RENAMES = {
"WANDS": "BRANDS",
"CUPS": "GRAILS",
"SWORDS": "BLADES",
}
# ── 2. Major arcana: Earthman number → Fiorentine number ─────────────────────
# Cards without a Fiorentine counterpart are omitted (keywords stay empty).
MAJOR_KEYWORD_MAP = {
0: 0, # The Schiz → The Fool
1: 1, # Pope I (President) → The Magician
2: 2, # Pope II (Tsar) → The High Priestess
3: 3, # Pope III (Chairman) → The Empress
4: 4, # Pope IV (Emperor) → The Emperor
5: 5, # Pope V (Chancellor) → The Hierophant
6: 8, # Virtue VI (Controlled Folly) → Strength
7: 11, # Virtue VII (Not-Doing) → Justice
8: 14, # Virtue VIII (Losing Self-Importance) → Temperance
# 9: Prudence — no Fiorentine equivalent
10: 10, # Wheel of Fortune → Wheel of Fortune
11: 7, # The Junkboat → The Chariot
12: 12, # The Junkman → The Hanged Man
13: 13, # Death → Death
14: 15, # The Traitor → The Devil
15: 16, # Disco Inferno → The Tower
# 16: Torre Terrestre (Purgatory) — no equivalent
# 17: Fantasia Celestia (Paradise) — no equivalent
18: 6, # Virtue XVIII (Stalking) → The Lovers
# 19: Virtue XIX (Intent / Hope) — no equivalent
# 20: Virtue XX (Dreaming / Faith)— no equivalent
# 2138: Classical Elements + Zodiac — no equivalents
39: 17, # Wanderer XXXIX (Polestar) → The Star
40: 18, # Wanderer XL (Antichthon) → The Moon
41: 19, # Wanderer XLI (Corestar) → The Sun
# 4249: Planets + The Binary — no equivalents
50: 20, # The Eagle → Judgement
51: 21, # Divine Calculus → The World
}
# ── 3. Minor suit map: Earthman (post-rename) → Fiorentine ───────────────────
MINOR_SUIT_MAP = {
"BRANDS": "WANDS",
"GRAILS": "CUPS",
"BLADES": "SWORDS",
"CROWNS": "PENTACLES",
}
def forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
except DeckVariant.DoesNotExist:
return # decks not seeded — nothing to do
# ── Step 1: rename Earthman suit codes + card names ───────────────────────
for old_suit, new_suit in SUIT_RENAMES.items():
old_display = old_suit.capitalize() # e.g. "Wands"
new_display = new_suit.capitalize() # e.g. "Brands"
cards = TarotCard.objects.filter(deck_variant=earthman, suit=old_suit)
for card in cards:
card.name = card.name.replace(f" of {old_display}", f" of {new_display}")
card.suit = new_suit
card.save()
# ── Step 2: copy major arcana keywords ───────────────────────────────────
fio_major = {
card.number: card
for card in TarotCard.objects.filter(deck_variant=fiorentine, arcana="MAJOR")
}
for em_num, fio_num in MAJOR_KEYWORD_MAP.items():
fio_card = fio_major.get(fio_num)
if not fio_card:
continue
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=em_num
).update(
keywords_upright=fio_card.keywords_upright,
keywords_reversed=fio_card.keywords_reversed,
)
# ── Step 3: copy minor/middle arcana keywords ─────────────────────────────
for em_suit, fio_suit in MINOR_SUIT_MAP.items():
fio_by_number = {
card.number: card
for card in TarotCard.objects.filter(deck_variant=fiorentine, suit=fio_suit)
}
for em_card in TarotCard.objects.filter(deck_variant=earthman, suit=em_suit):
fio_card = fio_by_number.get(em_card.number)
if fio_card:
em_card.keywords_upright = fio_card.keywords_upright
em_card.keywords_reversed = fio_card.keywords_reversed
em_card.save()
def reverse(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
# Reverse suit renames
reverse_renames = {new: old for old, new in SUIT_RENAMES.items()}
for new_suit, old_suit in reverse_renames.items():
new_display = new_suit.capitalize()
old_display = old_suit.capitalize()
cards = TarotCard.objects.filter(deck_variant=earthman, suit=new_suit)
for card in cards:
card.name = card.name.replace(f" of {new_display}", f" of {old_display}")
card.suit = old_suit
card.save()
# Clear all Earthman keywords
TarotCard.objects.filter(deck_variant=earthman).update(
keywords_upright=[],
keywords_reversed=[],
)
class Migration(migrations.Migration):
dependencies = [
("epic", "0025_earthman_middle_arcana_and_major_icons"),
]
operations = [
migrations.RunPython(forward, reverse_code=reverse),
]

View File

@@ -0,0 +1,65 @@
"""
Schema + data migration:
1. Add `cautions` JSONField (list, default=[]) to TarotCard.
2. Seed The Schizo (Earthman MAJOR #1) with 4 rival-interaction cautions.
All other cards default to [] — the UI shows a placeholder when empty.
"""
from django.db import migrations, models
SCHIZO_CAUTIONS = [
'This card will reverse into <span class="card-ref">The Pervert</span> when it'
' comes under dominion of <span class="card-ref">The Occultist</span>, which in turn'
' reverses into <span class="card-ref">Pestilence</span>.',
'This card will reverse into <span class="card-ref">The Paranoiac</span> when it'
' comes under dominion of <span class="card-ref">The Despot</span>, which in turn'
' reverses into <span class="card-ref">War</span>.',
'This card will reverse into <span class="card-ref">The Neurotic</span> when it'
' comes under dominion of <span class="card-ref">The Capitalist</span>, which in turn'
' reverses into <span class="card-ref">Famine</span>.',
'This card will reverse into <span class="card-ref">The Suicidal</span> when it'
' comes under dominion of <span class="card-ref">The Fascist</span>, which in turn'
' reverses into <span class="card-ref">Death</span>.',
]
def seed_schizo_cautions(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=1
).update(cautions=SCHIZO_CAUTIONS)
def clear_schizo_cautions(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=1
).update(cautions=[])
class Migration(migrations.Migration):
dependencies = [
("epic", "0026_earthman_suit_renames_and_keywords"),
]
operations = [
migrations.AddField(
model_name="tarotcard",
name="cautions",
field=models.JSONField(default=list),
),
migrations.RunPython(seed_schizo_cautions, reverse_code=clear_schizo_cautions),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2026-04-07 03:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0027_tarotcard_cautions'),
]
operations = [
migrations.AlterField(
model_name='tarotcard',
name='suit',
field=models.CharField(blank=True, choices=[('WANDS', 'Wands'), ('CUPS', 'Cups'), ('SWORDS', 'Swords'), ('PENTACLES', 'Pentacles'), ('CROWNS', 'Crowns'), ('BRANDS', 'Brands'), ('GRAILS', 'Grails'), ('BLADES', 'Blades')], max_length=10, null=True),
),
]

View File

@@ -0,0 +1,61 @@
"""
Data fix: clear Schizo cautions from The Nomad (number=0) if present,
and ensure they land on The Schizo (number=1).
"""
from django.db import migrations
SCHIZO_CAUTIONS = [
'This card will reverse into <span class="card-ref">I. The Pervert</span> when it'
' comes under dominion of <span class="card-ref">II. The Occultist</span>, which in turn'
' reverses into <span class="card-ref">II. Pestilence</span>.',
'This card will reverse into <span class="card-ref">I. The Paranoiac</span> when it'
' comes under dominion of <span class="card-ref">III. The Despot</span>, which in turn'
' reverses into <span class="card-ref">III. War</span>.',
'This card will reverse into <span class="card-ref">I. The Neurotic</span> when it'
' comes under dominion of <span class="card-ref">IV. The Capitalist</span>, which in turn'
' reverses into <span class="card-ref">IV. Famine</span>.',
'This card will reverse into <span class="card-ref">I. The Suicidal</span> when it'
' comes under dominion of <span class="card-ref">V. The Fascist</span>, which in turn'
' reverses into <span class="card-ref">V. Death</span>.',
]
def forward(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=0
).update(cautions=[])
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=1
).update(cautions=SCHIZO_CAUTIONS)
def reverse(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=1
).update(cautions=[])
class Migration(migrations.Migration):
dependencies = [
("epic", "0028_alter_tarotcard_suit"),
]
operations = [
migrations.RunPython(forward, reverse_code=reverse),
]