Compare commits
2 Commits
2f039559e6
...
da57106d7a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da57106d7a | ||
|
|
270e48ab2c |
@@ -0,0 +1,60 @@
|
|||||||
|
"""Populate card 49's polarity-split reversal titles.
|
||||||
|
|
||||||
|
The Earthman deck's last two cards (48–49) carry distinct titles per polarity
|
||||||
|
(stored in `levity_emanation` / `gravity_emanation` / `levity_reversal` /
|
||||||
|
`gravity_reversal`) rather than a shared title + qualifier.
|
||||||
|
|
||||||
|
Card 48 had its full set seeded in migration 0004:
|
||||||
|
levity: Father Sky → reversal: The Storm
|
||||||
|
gravity: Mother Sea → reversal: The Flood
|
||||||
|
|
||||||
|
Card 49 had only emanations seeded; this migration fills the reversals:
|
||||||
|
levity: The Effulgent Mould of Man → reversal: The Vibrational Mould of Man
|
||||||
|
gravity: The Devouring Eagle → reversal: The All-Bestowing Eagle
|
||||||
|
|
||||||
|
The "qualifier" (Effulgent / Vibrational / Devouring / All-Bestowing) is baked
|
||||||
|
into the title between "The" and the title-proper rather than rendered as a
|
||||||
|
separate qualifier slot — the per-polarity title strings are stored verbatim.
|
||||||
|
"""
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def populate_card49_reversals(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=49,
|
||||||
|
).update(
|
||||||
|
levity_reversal="The Vibrational Mould of Man",
|
||||||
|
gravity_reversal="The All-Bestowing Eagle",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_card49_reversals(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=49,
|
||||||
|
).update(
|
||||||
|
levity_reversal="",
|
||||||
|
gravity_reversal="",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("epic", "0014_rename_reversal_to_reversal_qualifier"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(populate_card49_reversals, reverse_code=clear_card49_reversals),
|
||||||
|
]
|
||||||
37
src/apps/epic/migrations/0016_card49_bestowing_eagle.py
Normal file
37
src/apps/epic/migrations/0016_card49_bestowing_eagle.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""Tweak card 49 gravity_reversal: 'All-Bestowing Eagle' → 'Bestowing Eagle'."""
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
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=49,
|
||||||
|
).update(gravity_reversal="The Bestowing Eagle")
|
||||||
|
|
||||||
|
|
||||||
|
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=49,
|
||||||
|
).update(gravity_reversal="The All-Bestowing Eagle")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("epic", "0015_card49_polarity_reversal_titles"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse_code=reverse),
|
||||||
|
]
|
||||||
114
src/apps/epic/migrations/0017_castanedan_virtues.py
Normal file
114
src/apps/epic/migrations/0017_castanedan_virtues.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
"""Populate the seven Castanedan Virtues — trumps 6–9 (Implicit) + 19–21 (Explicit).
|
||||||
|
|
||||||
|
Implicit Virtues (6–9): emanation qualifier differs by polarity (Sublimating /
|
||||||
|
Sedimentary), name is shared. Reversal is a single full string shared across
|
||||||
|
both polarities (the agency word — Controlled / Not / Losing / Erasing —
|
||||||
|
flips to Indulged / Indulgent / Self-Indulgence / Indulging). We fill the
|
||||||
|
standard `levity_qualifier` / `gravity_qualifier` slots so the major-arcana
|
||||||
|
upright renders "Controlled Folly,\nSublimating" via the existing template
|
||||||
|
branch; we fill BOTH `levity_reversal` + `gravity_reversal` with the same
|
||||||
|
string so a FLIP'd reversal still picks up the override (an empty side falls
|
||||||
|
through to the default major-arcana rendering).
|
||||||
|
|
||||||
|
Explicit Virtues (19–21): emanation is shared across polarities (e.g. "The
|
||||||
|
Hunter's Stalking" — no qualifier + stem decomposition), reversal differs by
|
||||||
|
polarity. All four polarity-split title fields filled.
|
||||||
|
|
||||||
|
Also canonicalizes trump 7's name from "Not Doing" to "Not-Doing" per the spec
|
||||||
|
doc (slug "not-doing" already correct).
|
||||||
|
"""
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
IMPLICIT = [
|
||||||
|
# (number, levity_qualifier, gravity_qualifier, reversal_title)
|
||||||
|
(6, "Sublimating", "Sedimentary", "Indulged Folly"),
|
||||||
|
(7, "Sublimating", "Sedimentary", "Indulgent Doing"),
|
||||||
|
(8, "Sublimating", "Sedimentary", "Self-Indulgence"),
|
||||||
|
(9, "Sublimating", "Sedimentary", "Indulging Personal History"),
|
||||||
|
]
|
||||||
|
|
||||||
|
EXPLICIT = [
|
||||||
|
# (number, levity_emanation, gravity_emanation, levity_reversal, gravity_reversal)
|
||||||
|
(19, "The Hunter's Stalking", "The Hunter's Stalking", "The Sleeper's Stalking", "The Quarry's Stalking"),
|
||||||
|
(20, "The Dreamer's Dreaming", "The Dreamer's Dreaming", "The Sleeper's Dreaming", "The Dreamed's Dreaming"),
|
||||||
|
(21, "The Warrior's Intent", "The Warrior's Intent", "The Sleeper's Intent", "The Predator's Intent"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Trump 7 name canonicalization
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=earthman, arcana="MAJOR", number=7,
|
||||||
|
).update(name="Not-Doing")
|
||||||
|
|
||||||
|
for number, lvty, grav, rev in IMPLICIT:
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=earthman, arcana="MAJOR", number=number,
|
||||||
|
).update(
|
||||||
|
levity_qualifier=lvty,
|
||||||
|
gravity_qualifier=grav,
|
||||||
|
levity_reversal=rev,
|
||||||
|
gravity_reversal=rev,
|
||||||
|
)
|
||||||
|
|
||||||
|
for number, le, ge, lr, gr in EXPLICIT:
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=earthman, arcana="MAJOR", number=number,
|
||||||
|
).update(
|
||||||
|
levity_emanation=le,
|
||||||
|
gravity_emanation=ge,
|
||||||
|
levity_reversal=lr,
|
||||||
|
gravity_reversal=gr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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=7,
|
||||||
|
).update(name="Not Doing")
|
||||||
|
|
||||||
|
for number, _lvty, _grav, _rev in IMPLICIT:
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=earthman, arcana="MAJOR", number=number,
|
||||||
|
).update(
|
||||||
|
levity_qualifier="",
|
||||||
|
gravity_qualifier="",
|
||||||
|
levity_reversal="",
|
||||||
|
gravity_reversal="",
|
||||||
|
)
|
||||||
|
|
||||||
|
for number, _le, _ge, _lr, _gr in EXPLICIT:
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=earthman, arcana="MAJOR", number=number,
|
||||||
|
).update(
|
||||||
|
levity_emanation="",
|
||||||
|
gravity_emanation="",
|
||||||
|
levity_reversal="",
|
||||||
|
gravity_reversal="",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("epic", "0016_card49_bestowing_eagle"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse_code=reverse),
|
||||||
|
]
|
||||||
18
src/apps/epic/migrations/0018_add_italic_word.py
Normal file
18
src/apps/epic/migrations/0018_add_italic_word.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-05-01 03:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epic', '0017_castanedan_virtues'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tarotcard',
|
||||||
|
name='italic_word',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=50),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""Set TarotCard.italic_word for trumps 19-21 (Stalking / Dreaming / Intent).
|
||||||
|
|
||||||
|
Each of these three Castanedan virtues has its title key-word italicized
|
||||||
|
across every emanation/reversal slot ("The Hunter's *Stalking*", "The
|
||||||
|
Sleeper's *Stalking*", etc.). Storing the word in a single field lets the
|
||||||
|
renderer wrap it in <em> at display time without HTML in the data.
|
||||||
|
"""
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
WORDS = {
|
||||||
|
19: "Stalking",
|
||||||
|
20: "Dreaming",
|
||||||
|
21: "Intent",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
for number, word in WORDS.items():
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=earthman, arcana="MAJOR", number=number,
|
||||||
|
).update(italic_word=word)
|
||||||
|
|
||||||
|
|
||||||
|
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__in=list(WORDS),
|
||||||
|
).update(italic_word="")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("epic", "0018_add_italic_word"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse_code=reverse),
|
||||||
|
]
|
||||||
@@ -257,6 +257,7 @@ class TarotCard(models.Model):
|
|||||||
gravity_emanation = models.CharField(max_length=200, blank=True, default='')
|
gravity_emanation = models.CharField(max_length=200, blank=True, default='')
|
||||||
levity_reversal = models.CharField(max_length=200, blank=True, default='') # polarity-split reversal (card 48)
|
levity_reversal = models.CharField(max_length=200, blank=True, default='') # polarity-split reversal (card 48)
|
||||||
gravity_reversal = models.CharField(max_length=200, blank=True, default='')
|
gravity_reversal = models.CharField(max_length=200, blank=True, default='')
|
||||||
|
italic_word = models.CharField(max_length=50, blank=True, default='') # word(s) inside any title slot to wrap in <em> at render time (e.g. "Stalking" for trumps 19-21)
|
||||||
energies = models.JSONField(default=list) # list of {type, effect} dicts — Energy interactions
|
energies = models.JSONField(default=list) # list of {type, effect} dicts — Energy interactions
|
||||||
operations = models.JSONField(default=list) # list of {type, effect} dicts — Operation interactions
|
operations = models.JSONField(default=list) # list of {type, effect} dicts — Operation interactions
|
||||||
keywords_upright = models.JSONField(default=list)
|
keywords_upright = models.JSONField(default=list)
|
||||||
@@ -291,21 +292,22 @@ class TarotCard(models.Model):
|
|||||||
|
|
||||||
def emanation_for(self, polarity):
|
def emanation_for(self, polarity):
|
||||||
"""Return the upright title for a given polarity ('levity' or 'gravity').
|
"""Return the upright title for a given polarity ('levity' or 'gravity').
|
||||||
Falls back to name for cards without a polarity split."""
|
Falls back to name_title (group prefix stripped) for cards without a
|
||||||
|
polarity split."""
|
||||||
if polarity == 'levity' and self.levity_emanation:
|
if polarity == 'levity' and self.levity_emanation:
|
||||||
return self.levity_emanation
|
return self.levity_emanation
|
||||||
if polarity == 'gravity' and self.gravity_emanation:
|
if polarity == 'gravity' and self.gravity_emanation:
|
||||||
return self.gravity_emanation
|
return self.gravity_emanation
|
||||||
return self.name
|
return self.name_title
|
||||||
|
|
||||||
def reversal_for(self, polarity):
|
def reversal_for(self, polarity):
|
||||||
"""Return the reversed title for a given polarity.
|
"""Return the reversed title for a given polarity.
|
||||||
Falls back to reversal (blank = same as emanation_for)."""
|
Falls back to reversal_qualifier (blank = same as emanation_for)."""
|
||||||
if polarity == 'levity' and self.levity_reversal:
|
if polarity == 'levity' and self.levity_reversal:
|
||||||
return self.levity_reversal
|
return self.levity_reversal
|
||||||
if polarity == 'gravity' and self.gravity_reversal:
|
if polarity == 'gravity' and self.gravity_reversal:
|
||||||
return self.gravity_reversal
|
return self.gravity_reversal
|
||||||
return self.reversal or self.emanation_for(polarity)
|
return self.reversal_qualifier or self.emanation_for(polarity)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name_group(self):
|
def name_group(self):
|
||||||
|
|||||||
@@ -31,9 +31,51 @@ var StageCard = (function () {
|
|||||||
levity_qualifier: el.dataset.levityQualifier || '',
|
levity_qualifier: el.dataset.levityQualifier || '',
|
||||||
gravity_qualifier: el.dataset.gravityQualifier || '',
|
gravity_qualifier: el.dataset.gravityQualifier || '',
|
||||||
reversal_qualifier: el.dataset.reversalQualifier || '',
|
reversal_qualifier: el.dataset.reversalQualifier || '',
|
||||||
|
// Polarity-split title overrides — non-blank for cards 48-49 only,
|
||||||
|
// where each polarity (and within each polarity, each axis state)
|
||||||
|
// has a fully distinct title rather than a shared name + qualifier.
|
||||||
|
levity_emanation: el.dataset.levityEmanation || '',
|
||||||
|
gravity_emanation: el.dataset.gravityEmanation || '',
|
||||||
|
levity_reversal: el.dataset.levityReversal || '',
|
||||||
|
gravity_reversal: el.dataset.gravityReversal || '',
|
||||||
|
// Word(s) inside any title slot to wrap in <em> at render time
|
||||||
|
// (e.g. "Stalking" for trumps 19-21). Blank for most cards.
|
||||||
|
italic_word: el.dataset.italicWord || '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _escape(s) {
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap every occurrence of `word` in `text` with <em>...</em>, returning
|
||||||
|
// an HTML-safe string. Both inputs are escaped before splicing.
|
||||||
|
function _italicize(text, word) {
|
||||||
|
if (!text) return '';
|
||||||
|
var safeText = _escape(text);
|
||||||
|
if (!word) return safeText;
|
||||||
|
var safeWord = _escape(word);
|
||||||
|
if (safeText.indexOf(safeWord) === -1) return safeText;
|
||||||
|
return safeText.split(safeWord).join('<em>' + safeWord + '</em>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set element text — uses innerHTML if the card has an italic_word that
|
||||||
|
// appears in the string, otherwise textContent. Caller passes the full
|
||||||
|
// card so we can pick up `italic_word` without re-passing it everywhere.
|
||||||
|
function _setTitle(el, text, card) {
|
||||||
|
if (!el) return;
|
||||||
|
if (card.italic_word && text && text.indexOf(card.italic_word) !== -1) {
|
||||||
|
el.innerHTML = _italicize(text, card.italic_word);
|
||||||
|
} else {
|
||||||
|
el.textContent = text || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Decide whether a card object represents Major Arcana — sig sources from
|
// Decide whether a card object represents Major Arcana — sig sources from
|
||||||
// `data-arcana` (Django's `get_arcana_display`, e.g. "Major Arcana"), sea
|
// `data-arcana` (Django's `get_arcana_display`, e.g. "Major Arcana"), sea
|
||||||
// from card.arcana (model code, e.g. "MAJOR"). Accept both.
|
// from card.arcana (model code, e.g. "MAJOR"). Accept both.
|
||||||
@@ -47,11 +89,17 @@ var StageCard = (function () {
|
|||||||
// falls back to the current polarity's qualifier when blank (6F behavior).
|
// falls back to the current polarity's qualifier when blank (6F behavior).
|
||||||
function populateCard(stageCard, card, polarity) {
|
function populateCard(stageCard, card, polarity) {
|
||||||
if (!stageCard) return;
|
if (!stageCard) return;
|
||||||
var isLevity = polarity === 'levity';
|
var isLevity = polarity === 'levity';
|
||||||
var qualifier = isLevity ? (card.levity_qualifier || '') : (card.gravity_qualifier || '');
|
var qualifier = isLevity ? (card.levity_qualifier || '') : (card.gravity_qualifier || '');
|
||||||
var isMajor = _isMajor(card);
|
var isMajor = _isMajor(card);
|
||||||
var title = card.name_title || '';
|
var title = card.name_title || '';
|
||||||
var reversalQualifier = card.reversal_qualifier || '';
|
var reversalQualifier = card.reversal_qualifier || '';
|
||||||
|
// Polarity-split overrides (cards 48-49). When set, the full title
|
||||||
|
// string already incorporates whatever qualifier the card carries
|
||||||
|
// ("The Effulgent Mould of Man" etc.) — render as a single line and
|
||||||
|
// leave the upright qualifier slots empty.
|
||||||
|
var emanationOverride = isLevity ? (card.levity_emanation || '') : (card.gravity_emanation || '');
|
||||||
|
var reversalOverride = isLevity ? (card.levity_reversal || '') : (card.gravity_reversal || '');
|
||||||
|
|
||||||
stageCard.querySelectorAll('.fan-corner-rank').forEach(function (el) {
|
stageCard.querySelectorAll('.fan-corner-rank').forEach(function (el) {
|
||||||
el.textContent = card.corner_rank || '';
|
el.textContent = card.corner_rank || '';
|
||||||
@@ -66,35 +114,46 @@ var StageCard = (function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
var nameGroupEl = stageCard.querySelector('.fan-card-name-group');
|
var nameGroupEl = stageCard.querySelector('.fan-card-name-group');
|
||||||
if (nameGroupEl) nameGroupEl.textContent = card.name_group || '';
|
if (nameGroupEl) nameGroupEl.textContent = emanationOverride ? '' : (card.name_group || '');
|
||||||
|
|
||||||
var arcanaEl = stageCard.querySelector('.fan-card-arcana');
|
var arcanaEl = stageCard.querySelector('.fan-card-arcana');
|
||||||
if (arcanaEl) arcanaEl.textContent = isMajor ? 'Major Arcana' : 'Middle Arcana';
|
if (arcanaEl) arcanaEl.textContent = isMajor ? 'Major Arcana' : 'Middle Arcana';
|
||||||
|
|
||||||
var nameEl = stageCard.querySelector('.fan-card-name');
|
var nameEl = stageCard.querySelector('.fan-card-name');
|
||||||
if (nameEl) nameEl.textContent = isMajor ? title + ',' : title;
|
|
||||||
|
|
||||||
var qAbove = stageCard.querySelector('.sig-qualifier-above');
|
var qAbove = stageCard.querySelector('.sig-qualifier-above');
|
||||||
if (qAbove) qAbove.textContent = isMajor ? '' : qualifier;
|
|
||||||
var qBelow = stageCard.querySelector('.sig-qualifier-below');
|
var qBelow = stageCard.querySelector('.sig-qualifier-below');
|
||||||
if (qBelow) qBelow.textContent = isMajor ? qualifier : '';
|
|
||||||
|
|
||||||
// Reversal face — three cases:
|
if (emanationOverride) {
|
||||||
// Major: title (with comma) in qualifier slot, qualifier in name slot
|
// Cards 48-49 + trumps 19-21 — single-line title, no qualifier slots.
|
||||||
|
_setTitle(nameEl, emanationOverride, card);
|
||||||
|
if (qAbove) qAbove.textContent = '';
|
||||||
|
if (qBelow) qBelow.textContent = '';
|
||||||
|
} else {
|
||||||
|
_setTitle(nameEl, isMajor ? title + ',' : title, card);
|
||||||
|
if (qAbove) qAbove.textContent = isMajor ? '' : qualifier;
|
||||||
|
if (qBelow) qBelow.textContent = isMajor ? qualifier : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reversal face — four cases:
|
||||||
|
// Polarity-split: full reversal title in qualifier slot (top-after-spin), name slot empty
|
||||||
|
// Major: title (with comma) in qualifier slot, qualifier in name slot
|
||||||
// Non-major + reversal_qual: reversal_qualifier in qualifier slot, title in name slot
|
// Non-major + reversal_qual: reversal_qualifier in qualifier slot, title in name slot
|
||||||
// Non-major no reversal_qual: fall back to current polarity's qualifier
|
// Non-major no reversal_qual: fall back to current polarity's qualifier
|
||||||
var rQual = stageCard.querySelector('.fan-card-reversal-qualifier');
|
var rQual = stageCard.querySelector('.fan-card-reversal-qualifier');
|
||||||
var rName = stageCard.querySelector('.fan-card-reversal-name');
|
var rName = stageCard.querySelector('.fan-card-reversal-name');
|
||||||
if (rQual && rName) {
|
if (rQual && rName) {
|
||||||
if (isMajor) {
|
if (reversalOverride) {
|
||||||
rQual.textContent = title + ',';
|
_setTitle(rQual, reversalOverride, card);
|
||||||
|
rName.textContent = '';
|
||||||
|
} else if (isMajor) {
|
||||||
|
_setTitle(rQual, title + ',', card);
|
||||||
rName.textContent = qualifier;
|
rName.textContent = qualifier;
|
||||||
} else if (reversalQualifier) {
|
} else if (reversalQualifier) {
|
||||||
rQual.textContent = reversalQualifier;
|
rQual.textContent = reversalQualifier;
|
||||||
rName.textContent = title;
|
_setTitle(rName, title, card);
|
||||||
} else {
|
} else {
|
||||||
rQual.textContent = qualifier;
|
rQual.textContent = qualifier;
|
||||||
rName.textContent = title;
|
_setTitle(rName, title, card);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
src/apps/epic/templatetags/__init__.py
Normal file
0
src/apps/epic/templatetags/__init__.py
Normal file
24
src/apps/epic/templatetags/tarot_filters.py
Normal file
24
src/apps/epic/templatetags/tarot_filters.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""Template filters for tarot card rendering."""
|
||||||
|
from django import template
|
||||||
|
from django.utils.html import escape
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='italicize')
|
||||||
|
def italicize(text, word):
|
||||||
|
"""Wrap every occurrence of `word` in `text` with <em>...</em>.
|
||||||
|
|
||||||
|
Both `text` and `word` are escape()d before splicing the <em> in, so the
|
||||||
|
output is safe to mark `mark_safe` regardless of input.
|
||||||
|
"""
|
||||||
|
if not text or not word:
|
||||||
|
return text
|
||||||
|
text = str(text)
|
||||||
|
word = str(word)
|
||||||
|
if word not in text:
|
||||||
|
return text
|
||||||
|
safe_text = escape(text)
|
||||||
|
safe_word = escape(word)
|
||||||
|
return mark_safe(safe_text.replace(safe_word, '<em>{}</em>'.format(safe_word)))
|
||||||
@@ -1146,6 +1146,13 @@ def sea_deck(request, room_id):
|
|||||||
'levity_qualifier': c.levity_qualifier,
|
'levity_qualifier': c.levity_qualifier,
|
||||||
'gravity_qualifier': c.gravity_qualifier,
|
'gravity_qualifier': c.gravity_qualifier,
|
||||||
'reversal_qualifier': c.reversal_qualifier,
|
'reversal_qualifier': c.reversal_qualifier,
|
||||||
|
# Polarity-split full-title overrides (cards 48-49 + trumps 19-21)
|
||||||
|
'levity_emanation': c.levity_emanation,
|
||||||
|
'gravity_emanation': c.gravity_emanation,
|
||||||
|
'levity_reversal': c.levity_reversal,
|
||||||
|
'gravity_reversal': c.gravity_reversal,
|
||||||
|
# Word inside any title slot to wrap in <em> at render time
|
||||||
|
'italic_word': c.italic_word,
|
||||||
'keywords_upright': c.keywords_upright,
|
'keywords_upright': c.keywords_upright,
|
||||||
'keywords_reversed': c.keywords_reversed,
|
'keywords_reversed': c.keywords_reversed,
|
||||||
'energies': c.energies,
|
'energies': c.energies,
|
||||||
|
|||||||
@@ -424,15 +424,17 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: rgba(var(--secUser), 0.6);
|
text-shadow: 0 0 1px rgba(0, 0, 0, 1);
|
||||||
|
color: rgba(var(--terUser), 0.6);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
transition: color 0.15s;
|
transition: color 0.15s;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
&:hover { color: rgba(var(--secUser), 1); }
|
&:hover { color: rgba(var(--ninUser), 1); }
|
||||||
// Suppress browser focus ring on mouse/touch clicks; retain it for keyboard nav
|
// Suppress browser focus ring on mouse/touch clicks; retain it for keyboard nav
|
||||||
&:focus:not(:focus-visible) { outline: none; box-shadow: none; }
|
|
||||||
&--prev { left: 1rem; }
|
&--prev { left: 1rem; }
|
||||||
&--next { right: 1rem; }
|
&--next { right: 1rem; }
|
||||||
}
|
}
|
||||||
@@ -1392,6 +1394,12 @@ $_glow-gravity: 0 0 0.8rem 0.15rem rgba(var(--quaUser), 0.6);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sea-stage-content {
|
.sea-stage-content {
|
||||||
|
// Card width drives the stage-card AND the stat block (which reuses
|
||||||
|
// --sig-card-w via the shared stat-block-shared mixin's calc rules).
|
||||||
|
// Override per breakpoint below to keep the stage from blowing up on small
|
||||||
|
// screens.
|
||||||
|
--sig-card-w: 180px;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1401,6 +1409,15 @@ $_glow-gravity: 0 0 0.8rem 0.15rem rgba(var(--quaUser), 0.6);
|
|||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sea-stage mobile breakpoints — mirror the fan modal's portrait/landscape
|
||||||
|
// trigger thresholds so behavior across staging surfaces stays consistent.
|
||||||
|
@media (orientation: portrait) and (max-width: 480px) {
|
||||||
|
.sea-stage-content { --sig-card-w: 130px; }
|
||||||
|
}
|
||||||
|
@media (orientation: landscape) and (max-height: 500px) {
|
||||||
|
.sea-stage-content { --sig-card-w: 130px; }
|
||||||
|
}
|
||||||
|
|
||||||
// Stage card — size matches sig-select stage (--sig-card-w driven by inline style)
|
// Stage card — size matches sig-select stage (--sig-card-w driven by inline style)
|
||||||
.sea-stage-card {
|
.sea-stage-card {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
@@ -134,7 +134,7 @@
|
|||||||
{# ── Sea stage — big card viewer ─────────────────────────────────────────── #}
|
{# ── Sea stage — big card viewer ─────────────────────────────────────────── #}
|
||||||
<div class="sea-stage" id="id_sea_stage" style="display:none">
|
<div class="sea-stage" id="id_sea_stage" style="display:none">
|
||||||
<div class="sea-stage-backdrop"></div>
|
<div class="sea-stage-backdrop"></div>
|
||||||
<div class="sea-stage-content" style="--sig-card-w:180px">
|
<div class="sea-stage-content">
|
||||||
<div class="sig-stage-card sea-stage-card">
|
<div class="sig-stage-card sea-stage-card">
|
||||||
<div class="fan-card-corner fan-card-corner--tl">
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
<span class="fan-corner-rank"></span>
|
<span class="fan-corner-rank"></span>
|
||||||
|
|||||||
@@ -81,7 +81,12 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
|
|||||||
data-operations="{{ card.operations_json }}"
|
data-operations="{{ card.operations_json }}"
|
||||||
data-levity-qualifier="{{ card.levity_qualifier }}"
|
data-levity-qualifier="{{ card.levity_qualifier }}"
|
||||||
data-gravity-qualifier="{{ card.gravity_qualifier }}"
|
data-gravity-qualifier="{{ card.gravity_qualifier }}"
|
||||||
data-reversal-qualifier="{{ card.reversal_qualifier }}">
|
data-reversal-qualifier="{{ card.reversal_qualifier }}"
|
||||||
|
data-levity-emanation="{{ card.levity_emanation }}"
|
||||||
|
data-gravity-emanation="{{ card.gravity_emanation }}"
|
||||||
|
data-levity-reversal="{{ card.levity_reversal }}"
|
||||||
|
data-gravity-reversal="{{ card.gravity_reversal }}"
|
||||||
|
data-italic-word="{{ card.italic_word }}">
|
||||||
<div class="fan-card-corner fan-card-corner--tl">
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||||
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{% load tarot_filters %}
|
||||||
{% for card in cards %}
|
{% for card in cards %}
|
||||||
<div class="fan-card"
|
<div class="fan-card"
|
||||||
data-index="{{ forloop.counter0 }}"
|
data-index="{{ forloop.counter0 }}"
|
||||||
@@ -13,29 +14,47 @@
|
|||||||
data-operations="{{ card.operations_json }}"
|
data-operations="{{ card.operations_json }}"
|
||||||
data-levity-qualifier="{{ card.levity_qualifier }}"
|
data-levity-qualifier="{{ card.levity_qualifier }}"
|
||||||
data-gravity-qualifier="{{ card.gravity_qualifier }}"
|
data-gravity-qualifier="{{ card.gravity_qualifier }}"
|
||||||
data-reversal-qualifier="{{ card.reversal_qualifier }}">
|
data-reversal-qualifier="{{ card.reversal_qualifier }}"
|
||||||
|
data-levity-emanation="{{ card.levity_emanation }}"
|
||||||
|
data-gravity-emanation="{{ card.gravity_emanation }}"
|
||||||
|
data-levity-reversal="{{ card.levity_reversal }}"
|
||||||
|
data-gravity-reversal="{{ card.gravity_reversal }}"
|
||||||
|
data-italic-word="{{ card.italic_word }}">
|
||||||
<div class="fan-card-corner fan-card-corner--tl">
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||||
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="fan-card-face">
|
<div class="fan-card-face">
|
||||||
<div class="fan-card-face-upright">
|
<div class="fan-card-face-upright">
|
||||||
{% if card.name_group %}<p class="fan-card-name-group">{{ card.name_group }}</p>{% endif %}
|
{% if card.levity_emanation %}
|
||||||
{% if card.arcana != "MAJOR" and card.levity_qualifier %}
|
{# Polarity-split title (cards 48-49 + trumps 19-21); no qualifier slots — qualifier is baked into the title between "The" and the proper noun #}
|
||||||
<p class="sig-qualifier-above">{{ card.levity_qualifier }}</p>
|
<h3 class="fan-card-name">{{ card.levity_emanation|italicize:card.italic_word }}</h3>
|
||||||
{% endif %}
|
{% else %}
|
||||||
<h3 class="fan-card-name">{{ card.name_title }}{% if card.arcana == "MAJOR" and card.levity_qualifier %},{% endif %}</h3>
|
{% if card.name_group %}<p class="fan-card-name-group">{{ card.name_group }}</p>{% endif %}
|
||||||
{% if card.arcana == "MAJOR" and card.levity_qualifier %}
|
{% if card.arcana != "MAJOR" and card.levity_qualifier %}
|
||||||
<p class="sig-qualifier-below">{{ card.levity_qualifier }}</p>
|
<p class="sig-qualifier-above">{{ card.levity_qualifier }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<h3 class="fan-card-name">{{ card.name_title|italicize:card.italic_word }}{% if card.arcana == "MAJOR" and card.levity_qualifier %},{% endif %}</h3>
|
||||||
|
{% if card.arcana == "MAJOR" and card.levity_qualifier %}
|
||||||
|
<p class="sig-qualifier-below">{{ card.levity_qualifier }}</p>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<p class="fan-card-arcana">{{ card.get_arcana_display }}</p>
|
<p class="fan-card-arcana">{{ card.get_arcana_display }}</p>
|
||||||
<div class="fan-card-face-reversal">
|
<div class="fan-card-face-reversal">
|
||||||
{% if card.arcana == "MAJOR" %}
|
{% comment %}
|
||||||
|
DOM order: reversal-name first, reversal-qualifier second.
|
||||||
|
After SPIN's 180° rotation DOM-second appears visually on top.
|
||||||
|
{% endcomment %}
|
||||||
|
{% if card.levity_reversal %}
|
||||||
|
{# Polarity-split reversal title — single line, qualifier slot empty. Title goes in the qualifier slot so it visually lands on top after spin. #}
|
||||||
|
<p class="fan-card-reversal-name"></p>
|
||||||
|
<p class="fan-card-reversal-qualifier">{{ card.levity_reversal|italicize:card.italic_word }}</p>
|
||||||
|
{% elif card.arcana == "MAJOR" %}
|
||||||
<p class="fan-card-reversal-name">{{ card.levity_qualifier|default:card.gravity_qualifier }}</p>
|
<p class="fan-card-reversal-name">{{ card.levity_qualifier|default:card.gravity_qualifier }}</p>
|
||||||
<p class="fan-card-reversal-qualifier">{{ card.name_title }}{% if card.levity_qualifier %},{% endif %}</p>
|
<p class="fan-card-reversal-qualifier">{{ card.name_title|italicize:card.italic_word }}{% if card.levity_qualifier %},{% endif %}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="fan-card-reversal-name">{{ card.name_title }}</p>
|
<p class="fan-card-reversal-name">{{ card.name_title|italicize:card.italic_word }}</p>
|
||||||
<p class="fan-card-reversal-qualifier">{{ card.reversal_qualifier|default:card.gravity_qualifier }}</p>
|
<p class="fan-card-reversal-qualifier">{{ card.reversal_qualifier|default:card.gravity_qualifier }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user