Compare commits
3 Commits
da57106d7a
...
8b0ad545c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b0ad545c9 | ||
|
|
3410f073f0 | ||
|
|
c264b6e3ee |
0
src/apps/epic/management/__init__.py
Normal file
0
src/apps/epic/management/__init__.py
Normal file
0
src/apps/epic/management/commands/__init__.py
Normal file
0
src/apps/epic/management/commands/__init__.py
Normal file
110
src/apps/epic/management/commands/reset_staging_db.py
Normal file
110
src/apps/epic/management/commands/reset_staging_db.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Wipe the configured database and re-run all migrations from scratch.
|
||||
|
||||
Intended for ephemeral environments (staging) where losing every user, room,
|
||||
billpost, token, etc. is acceptable. Refuses to run when DEBUG=False unless
|
||||
the operator explicitly confirms with --i-mean-it, and always prints the
|
||||
DB host before doing anything destructive.
|
||||
|
||||
Typical staging usage from the deploy host:
|
||||
|
||||
docker exec -it gamearray python manage.py reset_staging_db --i-mean-it
|
||||
|
||||
Locally (sqlite, DEBUG=True) the safety prompt is skipped:
|
||||
|
||||
python src/manage.py reset_staging_db
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import connection
|
||||
|
||||
|
||||
PROD_HOST_FRAGMENTS = ("earthmanrpg.me",)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Drop every table in the default DB and re-run migrations. Destructive."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--i-mean-it",
|
||||
action="store_true",
|
||||
help="Required when DEBUG=False. Bypasses the interactive confirmation.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-superuser",
|
||||
action="store_true",
|
||||
help="Skip the post-migrate ensure_superuser call.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **opts):
|
||||
db_settings = settings.DATABASES["default"]
|
||||
engine = db_settings.get("ENGINE", "")
|
||||
host = db_settings.get("HOST") or db_settings.get("NAME") or "(unknown)"
|
||||
|
||||
# Refuse outright if the host name suggests production
|
||||
if any(frag in str(host) for frag in PROD_HOST_FRAGMENTS) and not host.startswith("staging"):
|
||||
if "staging" not in str(host):
|
||||
raise CommandError(
|
||||
f"Refusing to reset DB at host={host!r} — looks like production. "
|
||||
"Edit PROD_HOST_FRAGMENTS in this command if you really mean it."
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.WARNING(
|
||||
f"\nAbout to wipe DB:\n ENGINE: {engine}\n HOST/NAME: {host}\n"
|
||||
))
|
||||
|
||||
if not settings.DEBUG and not opts["i_mean_it"]:
|
||||
raise CommandError(
|
||||
"DEBUG=False — pass --i-mean-it to confirm. "
|
||||
"(This is the staging-safety check; it does not bypass the prod-host refusal above.)"
|
||||
)
|
||||
|
||||
if settings.DEBUG and not opts["i_mean_it"]:
|
||||
answer = input("Type the DB host/name to confirm: ").strip()
|
||||
if answer != str(host):
|
||||
raise CommandError(f"Got {answer!r}, expected {str(host)!r}. Aborting.")
|
||||
|
||||
# Drop schema. Postgres + sqlite both honor `flush --no-input`'s
|
||||
# truncate-tables-in-place model, but for a *fresh* migration run we
|
||||
# need the migration history wiped too. For Postgres the cleanest
|
||||
# route is `DROP SCHEMA public CASCADE; CREATE SCHEMA public;` —
|
||||
# for sqlite, deleting the file is simpler but Django's connection
|
||||
# has it open. So: introspect the connection and drop all tables.
|
||||
self.stdout.write("Dropping all tables…")
|
||||
self._drop_all_tables()
|
||||
|
||||
self.stdout.write("Running migrate from scratch…")
|
||||
call_command("migrate", verbosity=1, interactive=False)
|
||||
|
||||
if not opts["no_superuser"]:
|
||||
try:
|
||||
call_command("ensure_superuser", verbosity=1)
|
||||
except Exception as exc: # pragma: no cover - depends on env vars
|
||||
self.stdout.write(self.style.WARNING(
|
||||
f"ensure_superuser skipped/failed: {exc}"
|
||||
))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("\nDB reset complete."))
|
||||
|
||||
def _drop_all_tables(self):
|
||||
vendor = connection.vendor
|
||||
with connection.cursor() as cursor:
|
||||
if vendor == "postgresql":
|
||||
cursor.execute("DROP SCHEMA public CASCADE;")
|
||||
cursor.execute("CREATE SCHEMA public;")
|
||||
cursor.execute("GRANT ALL ON SCHEMA public TO public;")
|
||||
elif vendor == "sqlite":
|
||||
cursor.execute("PRAGMA foreign_keys = OFF;")
|
||||
cursor.execute(
|
||||
"SELECT name FROM sqlite_master "
|
||||
"WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
||||
)
|
||||
tables = [row[0] for row in cursor.fetchall()]
|
||||
for table in tables:
|
||||
cursor.execute(f'DROP TABLE IF EXISTS "{table}"')
|
||||
cursor.execute("PRAGMA foreign_keys = ON;")
|
||||
else:
|
||||
raise CommandError(
|
||||
f"reset_staging_db only knows postgresql + sqlite, got {vendor!r}"
|
||||
)
|
||||
200
src/apps/epic/migrations/0007_finalize_earthman_deck.py
Normal file
200
src/apps/epic/migrations/0007_finalize_earthman_deck.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""Collapse of old migrations 0007–0022 into a single end-state finalize.
|
||||
|
||||
Schema:
|
||||
- mechanisms → energies (was 0008)
|
||||
- articulations → operations (was 0008)
|
||||
- reversal → reversal_qualifier (was 0014)
|
||||
- +italic_word (was 0018)
|
||||
|
||||
Data (operates on the Earthman deck seeded in 0004; idempotent against the
|
||||
current schema, which is the post-rename state thanks to the operations
|
||||
above running first):
|
||||
- Middle court reversal_qualifier per suit (was 0007)
|
||||
- Schizo energies + operations w/ .card-ref (was 0008 + 0009)
|
||||
- fa-hand-dots fallback for empty MAJOR icons (was 0010; only card 41
|
||||
in fresh-seed state)
|
||||
- Card 49 polarity-split reversal titles (was 0015 + 0016)
|
||||
- Castanedan Virtues: trumps 6-9 + 19-21 (was 0017)
|
||||
- italic_word for trumps 19-21 (was 0019)
|
||||
- Trump 8 rename + non-breaking hyphen (was 0020 + 0021)
|
||||
- Trump 9 non-breaking space (was 0021)
|
||||
- Pip cards (number 1-10) MIDDLE → MINOR arcana (was 0022)
|
||||
|
||||
Skipped (all no-ops against fresh 0004 seed):
|
||||
- 0011 (nomad/schizo icons already correct in 0004)
|
||||
- 0012 (no PENTACLES seeded for Earthman in 0004)
|
||||
- 0013 (nomad icon already fa-hat-cowboy-side in 0004)
|
||||
"""
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
# ── Schizo energies + operations ─────────────────────────────────────────────
|
||||
CR = '<span class="card-ref">{}</span>'
|
||||
|
||||
SCHIZO_ENERGIES = [
|
||||
{"type": "LIBIDO", "effect": f'When encountering territorial Libido, may convert Emanation into {CR.format("1. The Priest")}.'},
|
||||
{"type": "NUMEN", "effect": f'When encountering despotic Numen, may convert Emanation into {CR.format("1. The Powerful")}.'},
|
||||
{"type": "VOLUPTAS", "effect": f'When encountering axiomatic Voluptas, may convert Emanation into {CR.format("1. The Normal")}.'},
|
||||
{"type": "VOLUPTAS", "effect": f'When encountering annihilating Voluptas, may convert Emanation into {CR.format("1. The Surrendered")}.'},
|
||||
]
|
||||
|
||||
SCHIZO_OPERATIONS = [
|
||||
{"type": "COVER", "effect": f'When covering {CR.format("2. The Occultist")} she may choose, by converting her own Reversal into {CR.format("2. Pestilence")}, to convert this Reversal into {CR.format("1. The Pervert")}.'},
|
||||
{"type": "CROWN", "effect": f'When crowning {CR.format("3. The Despot")} she may choose, by converting her own Reversal into {CR.format("3. War")}, to convert this Reversal into {CR.format("1. The Paranoiac")}.'},
|
||||
{"type": "BEHIND", "effect": f'When behind {CR.format("4. The Capitalist")} he may choose, by converting his own Reversal into {CR.format("4. Famine")}, to convert this Reversal into {CR.format("1. The Neurotic")}.'},
|
||||
{"type": "BEFORE", "effect": f'When before {CR.format("5. The Fascist")} he may choose, by converting his own Reversal into {CR.format("5. Death")}, to convert this Reversal into {CR.format("1. The Suicidal")}.'},
|
||||
]
|
||||
|
||||
# ── Middle court suit reversals ──────────────────────────────────────────────
|
||||
SUIT_REVERSAL_QUALIFIER = {
|
||||
"BRANDS": "Seething",
|
||||
"GRAILS": "Gloomy",
|
||||
"BLADES": "Nervous",
|
||||
"CROWNS": "Vacant",
|
||||
}
|
||||
COURT_NUMBERS = [11, 12, 13, 14]
|
||||
|
||||
# ── Castanedan Virtues ───────────────────────────────────────────────────────
|
||||
IMPLICIT_VIRTUES = [
|
||||
# (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_VIRTUES = [
|
||||
# (number, levity_emanation, gravity_emanation, levity_reversal, gravity_reversal, italic_word)
|
||||
(19, "The Hunter's Stalking", "The Hunter's Stalking", "The Sleeper's Stalking", "The Quarry's Stalking", "Stalking"),
|
||||
(20, "The Dreamer's Dreaming", "The Dreamer's Dreaming", "The Sleeper's Dreaming", "The Dreamed's Dreaming", "Dreaming"),
|
||||
(21, "The Warrior's Intent", "The Warrior's Intent", "The Sleeper's Intent", "The Predator's Intent", "Intent"),
|
||||
]
|
||||
|
||||
|
||||
def finalize(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
|
||||
|
||||
# Middle court suit reversals
|
||||
for suit, qualifier in SUIT_REVERSAL_QUALIFIER.items():
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MIDDLE", suit=suit,
|
||||
number__in=COURT_NUMBERS,
|
||||
).update(reversal_qualifier=qualifier)
|
||||
|
||||
# Schizo: clear stray reversal_qualifier (0004 seeds 'Territoriality') +
|
||||
# populate energies/operations
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MAJOR", number=1,
|
||||
).update(
|
||||
reversal_qualifier="",
|
||||
energies=SCHIZO_ENERGIES,
|
||||
operations=SCHIZO_OPERATIONS,
|
||||
)
|
||||
|
||||
# fa-hand-dots fallback for empty MAJOR icons (number ≥ 2)
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MAJOR", number__gte=2, icon="",
|
||||
).update(icon="fa-hand-dots")
|
||||
|
||||
# Card 49 polarity reversal titles
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MAJOR", number=49,
|
||||
).update(
|
||||
levity_reversal="The Vibrational Mould of Man",
|
||||
gravity_reversal="The Bestowing Eagle",
|
||||
)
|
||||
|
||||
# Castanedan Virtues — implicit (trumps 6-9): trump 7 name canonicalize +
|
||||
# qualifiers + reversal titles
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MAJOR", number=7,
|
||||
).update(name="Not-Doing")
|
||||
for number, lvty, grav, rev in IMPLICIT_VIRTUES:
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MAJOR", number=number,
|
||||
).update(
|
||||
levity_qualifier=lvty,
|
||||
gravity_qualifier=grav,
|
||||
levity_reversal=rev,
|
||||
gravity_reversal=rev,
|
||||
)
|
||||
|
||||
# Castanedan Virtues — explicit (trumps 19-21): polarity-split titles +
|
||||
# italic_word for the agency stem
|
||||
for number, le, ge, lr, gr, word in EXPLICIT_VIRTUES:
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MAJOR", number=number,
|
||||
).update(
|
||||
levity_emanation=le,
|
||||
gravity_emanation=ge,
|
||||
levity_reversal=lr,
|
||||
gravity_reversal=gr,
|
||||
italic_word=word,
|
||||
)
|
||||
|
||||
# Trump 8: "Losing Self-Importance" → "Self‑Unimportance" w/ U+2011
|
||||
# non-breaking hyphen (keeps title on one line above qualifier)
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MAJOR", number=8,
|
||||
).update(name="Self‑Unimportance", slug="self-unimportance")
|
||||
|
||||
# Trump 9: insert U+00A0 between "Personal" and "History" so they wrap as
|
||||
# a single unit ("Erasing / Personal History, / Sublimating")
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MAJOR", number=9,
|
||||
).update(name="Erasing Personal History")
|
||||
|
||||
# Pip cards (number 1-10) → MINOR arcana; courts (11-14) stay MIDDLE
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MIDDLE", number__lte=10,
|
||||
).update(arcana="MINOR")
|
||||
|
||||
|
||||
def revert(apps, schema_editor):
|
||||
"""Reverse just enough to restore 0006 schema state. Data reverts to the
|
||||
raw 0004 seed shape (without any of the post-seed tweaks)."""
|
||||
TarotCard = apps.get_model("epic", "TarotCard")
|
||||
DeckVariant = apps.get_model("epic", "DeckVariant")
|
||||
try:
|
||||
earthman = DeckVariant.objects.get(slug="earthman")
|
||||
except DeckVariant.DoesNotExist:
|
||||
return
|
||||
# Pip arcana
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MINOR", number__lte=10,
|
||||
).update(arcana="MIDDLE")
|
||||
# Trump 8 + 9 names back
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MAJOR", number=8,
|
||||
).update(name="Losing Self-Importance", slug="losing-self-importance")
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MAJOR", number=9,
|
||||
).update(name="Erasing Personal History")
|
||||
# Trump 7 name back
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MAJOR", number=7,
|
||||
).update(name="Not Doing")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("epic", "0006_add_deck_variant_to_tableseat"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField("TarotCard", "mechanisms", "energies"),
|
||||
migrations.RenameField("TarotCard", "articulations", "operations"),
|
||||
migrations.RenameField("TarotCard", "reversal", "reversal_qualifier"),
|
||||
migrations.AddField(
|
||||
model_name="tarotcard",
|
||||
name="italic_word",
|
||||
field=models.CharField(blank=True, default="", max_length=50),
|
||||
),
|
||||
migrations.RunPython(finalize, reverse_code=revert),
|
||||
]
|
||||
@@ -1,73 +0,0 @@
|
||||
"""Populate TarotCard.reversal for Earthman Middle Arcana court cards.
|
||||
|
||||
Each suit has a fixed reversal qualifier that replaces the polarity qualifier
|
||||
(Elevated/Graven) when the card is spun to its reversed face:
|
||||
Brands → Seething Grails → Gloomy Blades → Nervous Crowns → Vacant
|
||||
|
||||
Also clears the incorrectly inherited reversal on The Schizo (card 1), which
|
||||
mistakenly carried 'Territoriality' from The Occultist (card 2).
|
||||
"""
|
||||
from django.db import migrations
|
||||
|
||||
SUIT_REVERSAL_QUALIFIER = {
|
||||
"BRANDS": "Seething",
|
||||
"GRAILS": "Gloomy",
|
||||
"BLADES": "Nervous",
|
||||
"CROWNS": "Vacant",
|
||||
}
|
||||
|
||||
RANK_NAMES = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
|
||||
|
||||
|
||||
def populate_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
|
||||
|
||||
# Middle Arcana court cards
|
||||
for suit, qualifier in SUIT_REVERSAL_QUALIFIER.items():
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman,
|
||||
arcana="MIDDLE",
|
||||
suit=suit,
|
||||
number__in=list(RANK_NAMES.keys()),
|
||||
).update(reversal=qualifier)
|
||||
|
||||
# Clear The Schizo's incorrectly inherited reversal (belongs to The Occultist)
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman,
|
||||
arcana="MAJOR",
|
||||
number=1,
|
||||
).update(reversal="")
|
||||
|
||||
|
||||
def clear_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="MIDDLE",
|
||||
suit__in=list(SUIT_REVERSAL_QUALIFIER.keys()),
|
||||
number__in=list(RANK_NAMES.keys()),
|
||||
).update(reversal="")
|
||||
TarotCard.objects.filter(deck_variant=earthman, arcana="MAJOR", number=1).update(
|
||||
reversal="Territoriality"
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("epic", "0006_add_deck_variant_to_tableseat"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(populate_reversals, reverse_code=clear_reversals),
|
||||
]
|
||||
@@ -1,57 +0,0 @@
|
||||
"""Rename mechanisms→energies and articulations→operations on TarotCard;
|
||||
seed The Schizo (Earthman major arcana card 1) with Energy and Operation entries.
|
||||
"""
|
||||
from django.db import migrations
|
||||
|
||||
CR = '<span class="card-ref">{}</span>'
|
||||
|
||||
SCHIZO_ENERGIES = [
|
||||
{"type": "LIBIDO", "effect": f'When encountering territorial Libido, may convert Emanation into {CR.format("1. The Priest")}.'},
|
||||
{"type": "NUMEN", "effect": f'When encountering despotic Numen, may convert Emanation into {CR.format("1. The Powerful")}.'},
|
||||
{"type": "VOLUPTAS", "effect": f'When encountering axiomatic Voluptas, may convert Emanation into {CR.format("1. The Normal")}.'},
|
||||
{"type": "VOLUPTAS", "effect": f'When encountering annihilating Voluptas, may convert Emanation into {CR.format("1. The Surrendered")}.'},
|
||||
]
|
||||
|
||||
SCHIZO_OPERATIONS = [
|
||||
{"type": "COVER", "effect": f'When covering {CR.format("2. The Occultist")} she may choose, by converting her own Reversal into {CR.format("2. Pestilence")}, to convert this Reversal into {CR.format("1. The Pervert")}.'},
|
||||
{"type": "CROWN", "effect": f'When crowning {CR.format("3. The Despot")} she may choose, by converting her own Reversal into {CR.format("3. War")}, to convert this Reversal into {CR.format("1. The Paranoiac")}.'},
|
||||
{"type": "BEHIND", "effect": f'When behind {CR.format("4. The Capitalist")} he may choose, by converting his own Reversal into {CR.format("4. Famine")}, to convert this Reversal into {CR.format("1. The Neurotic")}.'},
|
||||
{"type": "BEFORE", "effect": f'When before {CR.format("5. The Fascist")} he may choose, by converting his own Reversal into {CR.format("5. Death")}, to convert this Reversal into {CR.format("1. The Suicidal")}.'},
|
||||
]
|
||||
|
||||
|
||||
def seed_schizo(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(energies=SCHIZO_ENERGIES, operations=SCHIZO_OPERATIONS)
|
||||
|
||||
|
||||
def clear_schizo(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(energies=[], operations=[])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("epic", "0007_populate_middle_arcana_reversals"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField("TarotCard", "mechanisms", "energies"),
|
||||
migrations.RenameField("TarotCard", "articulations", "operations"),
|
||||
migrations.RunPython(seed_schizo, reverse_code=clear_schizo),
|
||||
]
|
||||
@@ -1,53 +0,0 @@
|
||||
"""Re-seed The Schizo's energies and operations with .card-ref HTML spans."""
|
||||
from django.db import migrations
|
||||
|
||||
CR = '<span class="card-ref">{}</span>'
|
||||
|
||||
SCHIZO_ENERGIES = [
|
||||
{"type": "LIBIDO", "effect": f'When encountering territorial Libido, may convert Emanation into {CR.format("1. The Priest")}.'},
|
||||
{"type": "NUMEN", "effect": f'When encountering despotic Numen, may convert Emanation into {CR.format("1. The Powerful")}.'},
|
||||
{"type": "VOLUPTAS", "effect": f'When encountering axiomatic Voluptas, may convert Emanation into {CR.format("1. The Normal")}.'},
|
||||
{"type": "VOLUPTAS", "effect": f'When encountering annihilating Voluptas, may convert Emanation into {CR.format("1. The Surrendered")}.'},
|
||||
]
|
||||
|
||||
SCHIZO_OPERATIONS = [
|
||||
{"type": "COVER", "effect": f'When covering {CR.format("2. The Occultist")} she may choose, by converting her own Reversal into {CR.format("2. Pestilence")}, to convert this Reversal into {CR.format("1. The Pervert")}.'},
|
||||
{"type": "CROWN", "effect": f'When crowning {CR.format("3. The Despot")} she may choose, by converting her own Reversal into {CR.format("3. War")}, to convert this Reversal into {CR.format("1. The Paranoiac")}.'},
|
||||
{"type": "BEHIND", "effect": f'When behind {CR.format("4. The Capitalist")} he may choose, by converting his own Reversal into {CR.format("4. Famine")}, to convert this Reversal into {CR.format("1. The Neurotic")}.'},
|
||||
{"type": "BEFORE", "effect": f'When before {CR.format("5. The Fascist")} he may choose, by converting his own Reversal into {CR.format("5. Death")}, to convert this Reversal into {CR.format("1. The Suicidal")}.'},
|
||||
]
|
||||
|
||||
|
||||
def seed_schizo(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(energies=SCHIZO_ENERGIES, operations=SCHIZO_OPERATIONS)
|
||||
|
||||
|
||||
def clear_schizo(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(energies=[], operations=[])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("epic", "0008_rename_energies_operations_seed_schizo"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(seed_schizo, reverse_code=clear_schizo),
|
||||
]
|
||||
@@ -1,49 +0,0 @@
|
||||
"""Assign fa-hand-dots icon to all Earthman Major Arcana cards with number >= 2.
|
||||
|
||||
Cards 0 (The Nomad) and 1 (The Schizo) keep their existing icon value so they
|
||||
can receive distinct icons later. All other Major Arcana groups (Popes, Implicit
|
||||
Virtues, Elements, Realms, Explicit Virtues, Zodiac, Lunars, Planets, Inner Rings,
|
||||
polarity-split finals) default to fa-hand-dots until per-group icons are assigned.
|
||||
"""
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def assign_hand_dots(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__gte=2,
|
||||
icon="",
|
||||
).update(icon="fa-hand-dots")
|
||||
|
||||
|
||||
def clear_hand_dots(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__gte=2,
|
||||
icon="fa-hand-dots",
|
||||
).update(icon="")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("epic", "0009_schizo_card_ref_spans"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(assign_hand_dots, reverse_code=clear_hand_dots),
|
||||
]
|
||||
@@ -1,43 +0,0 @@
|
||||
"""Assign individual icons to The Nomad (0) and The Schizo (1).
|
||||
|
||||
All other Major Arcana already have fa-hand-dots from migration 0010.
|
||||
"""
|
||||
from django.db import migrations
|
||||
|
||||
ICONS = {0: 'fa-hat-cowboy-side', 1: 'fa-hat-wizard'}
|
||||
|
||||
|
||||
def assign_icons(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, icon in ICONS.items():
|
||||
TarotCard.objects.filter(
|
||||
deck_variant=earthman, arcana="MAJOR", number=number
|
||||
).update(icon=icon)
|
||||
|
||||
|
||||
def clear_icons(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(ICONS.keys())
|
||||
).update(icon="")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("epic", "0010_major_arcana_hand_dots_icon"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(assign_icons, reverse_code=clear_icons),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
"""Delete 4 stray PENTACLES court cards from the Earthman deck.
|
||||
|
||||
These survived the migration collapse; the Earthman deck uses
|
||||
BRANDS/GRAILS/BLADES/CROWNS only.
|
||||
"""
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def delete_pentacles(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, suit="PENTACLES").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("epic", "0011_nomad_schizo_icons"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(delete_pentacles, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Fix The Nomad icon: fa-hat-cowboy → fa-hat-cowboy-side."""
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def fix_nomad_icon(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, icon="fa-hat-cowboy"
|
||||
).update(icon="fa-hat-cowboy-side")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("epic", "0012_delete_stray_pentacles"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(fix_nomad_icon, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
"""Rename TarotCard.reversal → TarotCard.reversal_qualifier.
|
||||
|
||||
Symmetric naming with levity_qualifier / gravity_qualifier; disambiguates the
|
||||
qualifier-text field from the reversal *axis* state and the keywords_reversed
|
||||
list. Pure column rename — no data movement.
|
||||
"""
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("epic", "0013_fix_nomad_icon"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="tarotcard",
|
||||
old_name="reversal",
|
||||
new_name="reversal_qualifier",
|
||||
),
|
||||
]
|
||||
@@ -1,60 +0,0 @@
|
||||
"""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),
|
||||
]
|
||||
@@ -1,37 +0,0 @@
|
||||
"""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),
|
||||
]
|
||||
@@ -1,114 +0,0 @@
|
||||
"""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),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -1,51 +0,0 @@
|
||||
"""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),
|
||||
]
|
||||
@@ -211,8 +211,8 @@ class DeckVariant(models.Model):
|
||||
|
||||
class TarotCard(models.Model):
|
||||
MAJOR = "MAJOR"
|
||||
MINOR = "MINOR"
|
||||
MIDDLE = "MIDDLE" # Earthman court cards (M/J/Q/K)
|
||||
MINOR = "MINOR" # pip cards (numbers 1-10)
|
||||
MIDDLE = "MIDDLE" # Earthman court cards (M/J/Q/K, numbers 11-14)
|
||||
ARCANA_CHOICES = [
|
||||
(MAJOR, "Major Arcana"),
|
||||
(MINOR, "Minor Arcana"),
|
||||
@@ -323,6 +323,14 @@ class TarotCard(models.Model):
|
||||
return self.name.split(': ', 1)[1]
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def title_squeeze_class(self):
|
||||
"""No-op kept for template compatibility. Title fit is now handled by
|
||||
a smaller base `font-size` on `.fan-card-name`/`.fan-card-reversal-*`
|
||||
plus `text-wrap: balance` (see `_card-deck.scss`) — every long-title
|
||||
card fits naturally without per-card CSS hacks."""
|
||||
return ''
|
||||
|
||||
@property
|
||||
def suit_icon(self):
|
||||
if self.icon:
|
||||
|
||||
@@ -25,9 +25,12 @@ var SeaDeal = (function () {
|
||||
_infoData = StageCard.buildInfoData(card);
|
||||
_infoIdx = 0;
|
||||
|
||||
// Reset SPIN
|
||||
stageCard.classList.remove('stage-card--reversed');
|
||||
statBlock.classList.remove('is-reversed');
|
||||
// Sync SPIN state to the card's reversal axis — `card.reversed` is set
|
||||
// server-side at deck-fetch time (apps.epic.utils.stack_reversal_probability)
|
||||
// and persisted in `_seaHand`, so re-clicking a deposited slot must
|
||||
// restore that state, not reset to upright.
|
||||
stageCard.classList.toggle('stage-card--reversed', !!card.reversed);
|
||||
statBlock.classList.toggle('is-reversed', !!card.reversed);
|
||||
_closeInfo();
|
||||
}
|
||||
|
||||
@@ -64,6 +67,10 @@ var SeaDeal = (function () {
|
||||
slot.classList.remove('sea-card-slot--empty');
|
||||
slot.classList.add('sea-card-slot--filled');
|
||||
slot.classList.add(isLevity ? 'sea-card-slot--levity' : 'sea-card-slot--gravity');
|
||||
if (card.reversed) slot.classList.add('sea-card-slot--reversed');
|
||||
// Long Roman numerals (XVIII / XXIII / XXVIII / XXXIII / XXXVIII /
|
||||
// XLIII / XLVIII) need horizontal squeezing to fit the slot — see SCSS.
|
||||
if ((card.corner_rank || '').length >= 5) slot.classList.add('sea-card-slot--rank-long');
|
||||
slot.dataset.cardId = String(card.id);
|
||||
slot.dataset.posKey = posSelector;
|
||||
slot.innerHTML =
|
||||
|
||||
@@ -84,6 +84,15 @@ var StageCard = (function () {
|
||||
return a === 'MAJOR' || a === 'MAJOR ARCANA';
|
||||
}
|
||||
|
||||
// Map either form (model code or display) to the rendered label.
|
||||
function _arcanaDisplay(card) {
|
||||
var a = (card.arcana || '').toUpperCase();
|
||||
if (a === 'MAJOR' || a === 'MAJOR ARCANA') return 'Major Arcana';
|
||||
if (a === 'MINOR' || a === 'MINOR ARCANA') return 'Minor Arcana';
|
||||
if (a === 'MIDDLE' || a === 'MIDDLE ARCANA') return 'Middle Arcana';
|
||||
return '';
|
||||
}
|
||||
|
||||
// Paint the stage-card's upright + reversal faces from a normalized card
|
||||
// object + the active polarity ('levity' | 'gravity'). Reversal-qualifier
|
||||
// falls back to the current polarity's qualifier when blank (6F behavior).
|
||||
@@ -117,7 +126,7 @@ var StageCard = (function () {
|
||||
if (nameGroupEl) nameGroupEl.textContent = emanationOverride ? '' : (card.name_group || '');
|
||||
|
||||
var arcanaEl = stageCard.querySelector('.fan-card-arcana');
|
||||
if (arcanaEl) arcanaEl.textContent = isMajor ? 'Major Arcana' : 'Middle Arcana';
|
||||
if (arcanaEl) arcanaEl.textContent = _arcanaDisplay(card);
|
||||
|
||||
var nameEl = stageCard.querySelector('.fan-card-name');
|
||||
var qAbove = stageCard.querySelector('.sig-qualifier-above');
|
||||
|
||||
@@ -3,6 +3,28 @@ from django.db.models import Q
|
||||
from apps.epic.models import Room, RoomInvite
|
||||
|
||||
|
||||
# ── Game-wide constants ────────────────────────────────────────────────────
|
||||
# Reversal probability applied to any card pulled from a stack, anywhere in
|
||||
# the game (PICK SEA initially; future phases — gameplay draws etc. — will
|
||||
# share this single source of truth). Stub for a future per-user profile
|
||||
# override: callers MUST go through stack_reversal_probability(user, room)
|
||||
# rather than referencing the constant directly so the user-config hookup is
|
||||
# a one-line change inside the helper.
|
||||
STACK_REVERSAL_PROBABILITY = 0.25
|
||||
|
||||
|
||||
def stack_reversal_probability(user=None, room=None):
|
||||
"""Reversal probability for a draw stack in this user's context.
|
||||
|
||||
Current behavior: returns the module default for everyone. Plumbing point
|
||||
for a forthcoming per-user setting — when that lands, swap the body to
|
||||
something like `return getattr(user.profile, 'reversal_rate', STACK_REVERSAL_PROBABILITY)`
|
||||
and every call site picks up the per-user value automatically.
|
||||
"""
|
||||
return STACK_REVERSAL_PROBABILITY
|
||||
|
||||
|
||||
|
||||
def _planet_house(degree, cusps):
|
||||
"""Return 1-based house number for a planet at ecliptic degree.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from apps.epic.models import (
|
||||
active_sig_seat, debit_token, levity_sig_cards, gravity_sig_cards,
|
||||
select_token, sig_deck_cards,
|
||||
)
|
||||
from apps.epic.utils import _compute_distinctions, _planet_house
|
||||
from apps.epic.utils import _compute_distinctions, _planet_house, stack_reversal_probability
|
||||
from apps.lyric.models import Token
|
||||
|
||||
|
||||
@@ -402,6 +402,9 @@ def room_view(request, room_id):
|
||||
ctx = _role_select_context(room, request.user)
|
||||
ctx["room"] = room
|
||||
ctx["page_class"] = "page-gameboard"
|
||||
# Reversal-rate hint label under PICK SEA's SPREAD select — same helper as
|
||||
# sea_partial so the value tracks any future per-user override automatically.
|
||||
ctx["stack_reversal_pct"] = int(round(stack_reversal_probability(request.user, room) * 100))
|
||||
return render(request, "apps/gameboard/room.html", ctx)
|
||||
|
||||
|
||||
@@ -1132,6 +1135,11 @@ def sea_deck(request, room_id):
|
||||
.values_list('significator_id', flat=True)
|
||||
)
|
||||
|
||||
# Roll reversal eagerly during the shuffle — the deck order is fully
|
||||
# determined at phase start, so the reversal axis should be too. Future
|
||||
# per-user-profile config rides this same helper.
|
||||
reversal_prob = stack_reversal_probability(request.user, room)
|
||||
|
||||
def _card_dict(c):
|
||||
return {
|
||||
'id': c.id,
|
||||
@@ -1157,6 +1165,8 @@ def sea_deck(request, room_id):
|
||||
'keywords_reversed': c.keywords_reversed,
|
||||
'energies': c.energies,
|
||||
'operations': c.operations,
|
||||
# Pre-rolled reversal axis — server-deterministic, client just reads
|
||||
'reversed': _random.random() < reversal_prob,
|
||||
}
|
||||
|
||||
available = list(
|
||||
@@ -1178,5 +1188,10 @@ def sea_partial(request, room_id):
|
||||
if not ctx.get('sky_confirmed'):
|
||||
return HttpResponse(status=403)
|
||||
ctx['room'] = room
|
||||
# Reversal-rate hint label under SPREAD — both the percentage AND the raw
|
||||
# probability flow from the same helper, so when per-user config lands we
|
||||
# only swap the helper body and every render picks it up.
|
||||
_prob = stack_reversal_probability(request.user, room)
|
||||
ctx['stack_reversal_pct'] = int(round(_prob * 100))
|
||||
return render(request, 'apps/gameboard/_partials/_sea_overlay.html', ctx)
|
||||
|
||||
|
||||
@@ -33,14 +33,14 @@ function initGameKitTooltips() {
|
||||
if (portal.classList.contains('active') && activeToken) {
|
||||
const tokenRect = activeToken.getBoundingClientRect();
|
||||
const portalRect = portal.getBoundingClientRect();
|
||||
// Expand left to cover button overflow outside portal edge
|
||||
const expandedPortalRect = {
|
||||
left: portalRect.left - 24,
|
||||
top: portalRect.top,
|
||||
right: portalRect.right,
|
||||
bottom: portalRect.bottom,
|
||||
};
|
||||
const rects = [tokenRect, expandedPortalRect];
|
||||
const rects = [tokenRect, portalRect];
|
||||
// Include the DON/DOFF button group's actual bounding rect so the
|
||||
// portions of those buttons that hang past the portal's left edge
|
||||
// (and above its top edge) stay inside the hover-tolerance region.
|
||||
// Was previously a hardcoded 24px left expansion which didn't
|
||||
// cover top overhang and underestimated wider button labels.
|
||||
const equipBtns = portal.querySelector('.tt-equip-btns');
|
||||
if (equipBtns) rects.push(equipBtns.getBoundingClientRect());
|
||||
if (miniPortal.classList.contains('active')) rects.push(miniPortal.getBoundingClientRect());
|
||||
const left = Math.min(...rects.map(r => r.left));
|
||||
const top = Math.min(...rects.map(r => r.top));
|
||||
@@ -244,7 +244,19 @@ function initGameKitTooltips() {
|
||||
const tokenRect = token.getBoundingClientRect();
|
||||
const halfW = portal.offsetWidth / 2;
|
||||
const rawLeft = tokenRect.left + tokenRect.width / 2;
|
||||
const clampedLeft = Math.max(halfW + 8, Math.min(rawLeft, window.innerWidth - halfW - 8));
|
||||
// Extra left clearance — the DON/DOFF button group is absolute-
|
||||
// positioned with `left: -1rem` inside the portal and spills further
|
||||
// left by its own width. Measure the actual overhang so the clamp
|
||||
// keeps the buttons inside the viewport rather than just the portal.
|
||||
let leftOverhang = 0;
|
||||
const equipBtns = portal.querySelector('.tt-equip-btns');
|
||||
if (equipBtns) {
|
||||
const portalRect = portal.getBoundingClientRect();
|
||||
const btnsRect = equipBtns.getBoundingClientRect();
|
||||
leftOverhang = Math.max(0, portalRect.left - btnsRect.left);
|
||||
}
|
||||
const minLeft = halfW + 8 + leftOverhang;
|
||||
const clampedLeft = Math.max(minLeft, Math.min(rawLeft, window.innerWidth - halfW - 8));
|
||||
portal.style.left = Math.round(clampedLeft) + 'px';
|
||||
|
||||
// Show above when token is in lower viewport half; below when in upper half
|
||||
|
||||
@@ -350,21 +350,34 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: calc(var(--fan-card-w) * 0.007);
|
||||
// Ghost-line: reserve at least two title-line-heights of vertical space
|
||||
// on each face so emanation + reversal stay symmetric even when one
|
||||
// side has a single-line title (e.g. trumps 6–9 reversal "Indulged
|
||||
// Folly" vs upright "Losing Self-Importance, / Sublimating").
|
||||
min-height: calc(var(--fan-card-w) * 0.21);
|
||||
}
|
||||
|
||||
// Qualifier shares the name's typography — same line, different content.
|
||||
// Sizes scale with --fan-card-w so they stay proportional on mobile.
|
||||
// `text-wrap: balance` distributes lines evenly so a borderline-long title
|
||||
// breaks at the natural midpoint instead of greedy first-fit (e.g. trump
|
||||
// 9 wraps as "Erasing / Personal History," instead of "Erasing Personal /
|
||||
// History,"). Base size lowered from 0.1 → 0.087 (~13%) so all the long
|
||||
// titles (trumps 8/9/18/36/41 + Queen of Crowns) fit without per-card
|
||||
// hacks and without asymmetry between upright (h3) and reversal (p).
|
||||
.sig-qualifier-above,
|
||||
.sig-qualifier-below,
|
||||
.fan-card-reversal-qualifier,
|
||||
.fan-card-reversal-name,
|
||||
.fan-card-name {
|
||||
font-size: calc(var(--fan-card-w) * 0.1);
|
||||
font-size: calc(var(--fan-card-w) * 0.087);
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
color: rgba(var(--terUser), 1);
|
||||
transition: opacity 0.2s;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
// Reversal-face spans pre-rotated so they read forward once the card spins
|
||||
@@ -547,12 +560,14 @@ html:has(.sig-backdrop) {
|
||||
.fan-card-face-upright { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; }
|
||||
.fan-card-face-reversal { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; padding-top: 0.1rem; }
|
||||
.fan-card-name-group { font-size: calc(var(--sig-card-w, 120px) * 0.073); opacity: 0.6; }
|
||||
// Upright qualifier + name share sizing/weight/color with their reversed counterparts
|
||||
// Upright qualifier + name share sizing/weight/color with their reversed counterparts.
|
||||
// text-wrap: balance distributes lines evenly so longer titles wrap symmetrically;
|
||||
// base size 0.08 (was 0.093) gives long titles room to fit without per-card hacks.
|
||||
.sig-qualifier-above,
|
||||
.sig-qualifier-below,
|
||||
.fan-card-reversal-qualifier { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; }
|
||||
.fan-card-reversal-qualifier { font-size: calc(var(--sig-card-w, 120px) * 0.08); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; text-wrap: balance; }
|
||||
.fan-card-name,
|
||||
.fan-card-reversal-name { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; }
|
||||
.fan-card-reversal-name { font-size: calc(var(--sig-card-w, 120px) * 0.08); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; text-wrap: balance; }
|
||||
.fan-card-arcana { font-size: calc(var(--sig-card-w, 120px) * 0.067); text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.5; }
|
||||
.fan-card-correspondence{ display: none; } // Minchiate equivalence shown in game-kit only
|
||||
// Reversed face elements — pre-rotated so they read forward after card spins
|
||||
@@ -1053,6 +1068,28 @@ $sea-card-h: 6.5rem;
|
||||
border-color: rgba(var(--secUser), 0.6);
|
||||
}
|
||||
|
||||
// Reversed — pre-rolled by sea_deck server-side. Rotate the whole slot
|
||||
// (background + border + content) so the rank/icon stacking order also
|
||||
// flips (rank-top + icon-bottom upright → icon-top + rank-bottom reversed),
|
||||
// not just each character upside-down in place.
|
||||
.sea-card-slot--reversed { transform: rotate(180deg); }
|
||||
|
||||
// Cross-position adds 90° already; reversed cross combines to 270°. Higher
|
||||
// specificity than the .sea-pos-cross .sea-card-slot rule so it wins.
|
||||
.sea-pos-cross .sea-card-slot--reversed { transform: rotate(270deg); }
|
||||
|
||||
// Long Roman numerals (≥ 5 chars: XVIII, XXIII, XXVIII, XXXIII, XXXVIII,
|
||||
// XLIII, XLVIII) — squeeze horizontally via scaleX so they fit the slot
|
||||
// without dropping font-size (height stays the same). Class added in
|
||||
// _fillSlot when card.corner_rank.length >= 5. Slot-level reversed rotation
|
||||
// already carries the rank along, so scaleX is the only inner transform
|
||||
// regardless of reversal state.
|
||||
.sea-card-slot--rank-long .fan-corner-rank {
|
||||
display: inline-block;
|
||||
transform: scaleX(0.7);
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
// Deposited — fully opaque by default; Cover/Cross are semi-transparent
|
||||
.sea-card-slot--visible { opacity: 1; transition: opacity 1s ease, box-shadow 0.15s ease; }
|
||||
|
||||
@@ -1126,8 +1163,9 @@ $sea-card-h: 6.5rem;
|
||||
}
|
||||
|
||||
// .sig-stage-card is normally scoped inside .sig-stage — re-apply the card shell
|
||||
// here so it renders correctly outside that context.
|
||||
.sea-cross .sig-stage-card {
|
||||
// here so it renders correctly outside that context. Class-based selector so it
|
||||
// also applies in the tray (.tray-sig-card .sig-stage-card.sea-sig-card).
|
||||
.sig-stage-card.sea-sig-card {
|
||||
flex-shrink: 0;
|
||||
width: var(--sig-card-w, #{$sea-card-w});
|
||||
height: auto;
|
||||
@@ -1188,6 +1226,16 @@ $sea-card-h: 6.5rem;
|
||||
label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; }
|
||||
}
|
||||
|
||||
// Forthcoming-feature hint between SPREAD label and the combobox; rendered
|
||||
// value comes from apps.epic.utils.stack_reversal_probability via the view
|
||||
// context.
|
||||
.sea-reversal-hint {
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.55;
|
||||
margin: -0.1rem 0 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
// Custom combobox replacement for native <select>. See combobox.js for the
|
||||
// expected markup; SCSS owns all visuals because the OS-native dropdown ignored
|
||||
// option background/color anyway.
|
||||
@@ -1466,9 +1514,9 @@ $_glow-gravity: 0 0 0.8rem 0.15rem rgba(var(--quaUser), 0.6);
|
||||
.fan-card-name-group { font-size: calc(var(--sig-card-w, 140px) * 0.073); opacity: 0.6; }
|
||||
.sig-qualifier-above,
|
||||
.sig-qualifier-below,
|
||||
.fan-card-reversal-qualifier { font-size: calc(var(--sig-card-w, 140px) * 0.093); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; }
|
||||
.fan-card-reversal-qualifier { font-size: calc(var(--sig-card-w, 140px) * 0.08); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; text-wrap: balance; }
|
||||
.fan-card-name,
|
||||
.fan-card-reversal-name { font-size: calc(var(--sig-card-w, 140px) * 0.093); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; }
|
||||
.fan-card-reversal-name { font-size: calc(var(--sig-card-w, 140px) * 0.08); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; text-wrap: balance; }
|
||||
.fan-card-arcana { font-size: calc(var(--sig-card-w, 140px) * 0.067); text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.5; }
|
||||
.fan-card-reversal-qualifier,
|
||||
.fan-card-reversal-name { transform: rotate(180deg); opacity: 0.25; }
|
||||
|
||||
@@ -147,18 +147,18 @@ $handle-r: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Hosts the same compact rank-+-icon Sig card used in the Sea Select center
|
||||
// (.sig-stage-card.sea-sig-card). Width is sized so the 5:8-aspect card
|
||||
// height ≈ tray cell height.
|
||||
.tray-sig-card {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
transform: scale(1.4); // crop SVG's internal margins
|
||||
.sig-stage-card.sea-sig-card {
|
||||
--sig-card-w: calc(var(--tray-cell-size, 48px) * 5 / 8);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<header class="sea-modal-header">
|
||||
<h2>PICK <span>SEA</span></h2>
|
||||
<p>Draw cards to circumscribe your character's influences and seed the Voronoi map.</p>
|
||||
<p>Draw +6 cards to describe your character's influences and seed the map.</p>
|
||||
</header>
|
||||
|
||||
<div class="sea-modal-body">
|
||||
@@ -63,6 +63,13 @@
|
||||
<div class="sea-form-main">
|
||||
<div class="sea-field">
|
||||
<label for="id_sea_spread" id="id_sea_spread_label">Spread</label>
|
||||
{% comment %}
|
||||
Reversal-rate hint — `stack_reversal_pct` flows from
|
||||
apps.epic.utils.stack_reversal_probability via the
|
||||
view. Currently a module default; placeholder UI for
|
||||
a forthcoming per-user setting.
|
||||
{% endcomment %}
|
||||
<p class="sea-reversal-hint">{{ stack_reversal_pct|default:25 }}% reversals</p>
|
||||
{% comment %}
|
||||
Custom combobox — native <select> dropdowns ignore most CSS on
|
||||
Firefox/Chrome (OS-rendered list); this gives full styling control.
|
||||
@@ -144,7 +151,7 @@
|
||||
<div class="fan-card-face-upright">
|
||||
<p class="fan-card-name-group"></p>
|
||||
<p class="sig-qualifier-above"></p>
|
||||
<h3 class="fan-card-name"></h3>
|
||||
<p class="fan-card-name"></p>
|
||||
<p class="sig-qualifier-below"></p>
|
||||
</div>
|
||||
<p class="fan-card-arcana"></p>
|
||||
|
||||
@@ -26,7 +26,7 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
|
||||
<div class="fan-card-face-upright">
|
||||
<p class="fan-card-name-group"></p>
|
||||
<p class="sig-qualifier-above"></p>
|
||||
<h3 class="fan-card-name"></h3>
|
||||
<p class="fan-card-name"></p>
|
||||
<p class="sig-qualifier-below"></p>
|
||||
</div>
|
||||
<p class="fan-card-arcana"></p>
|
||||
|
||||
@@ -28,13 +28,13 @@
|
||||
<div class="fan-card-face-upright">
|
||||
{% if card.levity_emanation %}
|
||||
{# Polarity-split title (cards 48-49 + trumps 19-21); no qualifier slots — qualifier is baked into the title between "The" and the proper noun #}
|
||||
<h3 class="fan-card-name">{{ card.levity_emanation|italicize:card.italic_word }}</h3>
|
||||
<p class="fan-card-name {{ card.title_squeeze_class }}">{{ card.levity_emanation|italicize:card.italic_word }}</p>
|
||||
{% else %}
|
||||
{% if card.name_group %}<p class="fan-card-name-group">{{ card.name_group }}</p>{% endif %}
|
||||
{% if card.arcana != "MAJOR" and card.levity_qualifier %}
|
||||
<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>
|
||||
<p class="fan-card-name {{ card.title_squeeze_class }}">{{ card.name_title|italicize:card.italic_word }}{% if card.arcana == "MAJOR" and card.levity_qualifier %},{% endif %}</p>
|
||||
{% if card.arcana == "MAJOR" and card.levity_qualifier %}
|
||||
<p class="sig-qualifier-below">{{ card.levity_qualifier }}</p>
|
||||
{% endif %}
|
||||
@@ -49,12 +49,12 @@
|
||||
{% 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>
|
||||
<p class="fan-card-reversal-qualifier {{ card.title_squeeze_class }}">{{ 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-qualifier">{{ card.name_title|italicize:card.italic_word }}{% if card.levity_qualifier %},{% endif %}</p>
|
||||
<p class="fan-card-reversal-qualifier {{ card.title_squeeze_class }}">{{ card.name_title|italicize:card.italic_word }}{% if card.levity_qualifier %},{% endif %}</p>
|
||||
{% else %}
|
||||
<p class="fan-card-reversal-name">{{ card.name_title|italicize:card.italic_word }}</p>
|
||||
<p class="fan-card-reversal-name {{ card.title_squeeze_class }}">{{ card.name_title|italicize:card.italic_word }}</p>
|
||||
<p class="fan-card-reversal-qualifier">{{ card.reversal_qualifier|default:card.gravity_qualifier }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
<i class="fa-solid fa-dice-d20"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="id_tray" style="display:none"><div id="id_tray_grid" data-role-icons-url="{% static 'apps/epic/icons/cards-roles/' %}">{% if my_tray_role %}<div class="tray-cell tray-role-card" data-role="{{ my_tray_role }}"><img src="{% static my_tray_scrawl_static_path %}" alt="{{ my_tray_role }}"></div>{% else %}<div class="tray-cell"></div>{% endif %}{% if my_tray_sig %}<div class="tray-cell tray-sig-card"><img src="{% static 'apps/epic/icons/cards-sigs/Blank.svg' %}" alt="{{ my_tray_sig.name }}"></div>{% else %}<div class="tray-cell"></div>{% endif %}{% for i in "345678" %}<div class="tray-cell"></div>{% endfor %}</div></div>
|
||||
<div id="id_tray" style="display:none"><div id="id_tray_grid" data-role-icons-url="{% static 'apps/epic/icons/cards-roles/' %}">{% if my_tray_role %}<div class="tray-cell tray-role-card" data-role="{{ my_tray_role }}"><img src="{% static my_tray_scrawl_static_path %}" alt="{{ my_tray_role }}"></div>{% else %}<div class="tray-cell"></div>{% endif %}{% if my_tray_sig %}<div class="tray-cell tray-sig-card"><div class="sig-stage-card sea-sig-card" aria-label="{{ my_tray_sig.name }}"><span class="fan-corner-rank">{{ my_tray_sig.corner_rank }}</span>{% if my_tray_sig.suit_icon %}<i class="fa-solid {{ my_tray_sig.suit_icon }}"></i>{% endif %}</div></div>{% else %}<div class="tray-cell"></div>{% endif %}{% for i in "345678" %}<div class="tray-cell"></div>{% endfor %}</div></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include "apps/gameboard/_partials/_room_gear.html" %}
|
||||
|
||||
Reference in New Issue
Block a user