image tree refactor: cards-faces/<family>/<variant>/ + RWS deck import (78 cards + back, renamed + pngquant'd)
Two related sub-changes, bundled because the new image_url path structure has to land in the same commit as the actual file relocations to keep `manage.py runserver` resolvable at every revision. **(1) `DeckVariant.variant_dir_slug` + image-path tree restructure** — `apps/epic/models.py`. New `variant_dir_slug` property on DeckVariant returns the subdirectory name under `cards-faces/<family>/` for this deck's images. Mapping locked in 2026-05-26: - earthman family → "default" (single-canonical today, locks the variant tier now so future Earthman editions slot in at `earthman/<variant>/` w.o. a path migration) - slug startswith "tarot-" → strips that prefix (RWS slug `tarot-rider-waite-smith` → `rider-waite-smith`; "tarot-" is redundant under family=english) - otherwise → uses slug as-is (italian/minchiate-fiorentine-1860-1890) Both `DeckVariant.back_image_url` + `TarotCard.image_url` updated from `cards-faces/<slug>/<filename>` to `cards-faces/<family>/<variant_dir_slug>/<filename>`. Flat → 2-tier tree groups by tarot tradition (italian/english/playing/earthman) rather than scattering 20+ deck dirs at the top level — payoff is most visible when adding multi-variant decks within a family (e.g., future RWS Centennial Edition, Pamela-A pristine scans, both land alongside the original at `english/<variant>/`). Why this naming over alternatives the user considered: - `western-tarot/` — too broad (Italian Minchiate is also western tarot, defeats the partition) - `hermetic-dawn/` — too narrow (RWS lineage but doesn't generalize to pre-GD Marseille or non-RWS English decks) - `english/` — matches the existing `DeckVariant.FAMILY_CHOICES` field verbatim (source of truth, no new enum) No tests assert on `image_url` paths (only on `image_filename` — the bare PNG names, which are unchanged). No JS references `cards-faces/` directly — sea.js + stage-card.js + utils.py all consume `image_url` server-rendered. **(2) Minchiate Fiorentine 1860-1890 dir move** — 98 PNGs relocated from `cards-faces/minchiate-fiorentine-1860-1890/` to `cards-faces/italian/minchiate-fiorentine-1860-1890/`. Initially used `git mv source/ italian/` which Windows-flattened the move (files landed directly in italian/ instead of the nested variant subdir) — recovered by creating the variant subdir explicitly + `git mv *.png variant/`. Worth remembering for future deck imports: on Windows, `git mv dir/ existing_parent_dir/` does NOT auto-nest when the destination has existing entries. **(3) RWS deck import** — 78 card images + 1 card-back PNG, dropped into `cards-faces/english/rider-waite-smith/`. Source: Wikipedia Commons (Public domain, attributable to Pamela Colman Smith). All scraped at 960px width per the size-vs-quality tradeoff conversation (matches the contour-stroke filter chain's largest CSS-display surface w. retina headroom; full-resolution 2100×3600 was 11.68MB/card → would balloon the page weight). Filename normalization via one-shot `d:/tmp/rename_rws.py`: - Wikipedia patterns: `960px-Ace_of_Cups_(Rider-Waite_Smith_tarot_deck).png` → `tarot-rider-waite-smith-cups-01.png` - Trumps: `960px-The_Fool_(...)` → `tarot-rider-waite-smith-majors-00-the-fool.png` (English family uses "majors" not "trumps" per `_TRUMP_CATEGORY_BY_FAMILY` mapping) - Courts: `Page/Knight/Queen/King_of_<Suit>` → ranks 11/12/13/14 w. court-name suffix (e.g., `-cups-13-queen.png`) - Special: Aces of Pentacles + Aces of Swords Wikipedia-named as "One_of_..." instead of "Ace_of_..." (RANK_BY_WORD dict handles both) - Special: "Wheel_of_Fortune" major initially matched the MINOR_RE regex (Wheel + of + Fortune); fixed by adding both-rank-and-suit-in-known-vocab guard so non-real-suit "of" patterns fall through to MAJOR_RE - Card back: `Waite-Smith_Tarot_Roses_and_Lilies.png` → `tarot-rider-waite-smith-back.png` Also: Queen of Cups was missing from the initial Wikipedia batch (caught by per-suit count audit: cups=13, others=14); user grabbed + dropped it in separately, scripted rename was rerun for that single file. pngquant pass: `--quality=65-85 --speed=1 --strip --skip-if-larger --ext=.png --force` — 219MB → 76MB across the 78 cards (~65% reduction, ~975 KB/card average). Queen-of-Cups single-file pass: 2.4MB → 856KB. Tests: 834/834 green across epic + gameboard + billboard (and 181/181 epic-isolated post-rename + collectstatic). collectstatic recopied all 176 PNGs (98 minchiate + 78 RWS) into the build dir; manifest hashes refresh. Tomorrow: A.8 room.html sprint can now proceed w. RWS image-equipped (`has_card_images=True`) the same way Minchiate already does — image-mode SCSS already in place from A.5-A.7 polish. Future Shop applet entries: user mentioned a few decks slated as exclusively-purchasable via wallet shop (paid-only deck variants). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -259,6 +259,28 @@ class DeckVariant(models.Model):
|
||||
has_card_images = models.BooleanField(default=True)
|
||||
is_polarized = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def variant_dir_slug(self):
|
||||
"""Subdirectory under `cards-faces/<family>/` for this deck's images.
|
||||
Strips family-implied prefixes from `slug` (e.g., RWS slug is
|
||||
`tarot-rider-waite-smith` but lives at `english/rider-waite-smith/` —
|
||||
the "tarot-" is redundant under family=english). Earthman is special-
|
||||
cased to "default" per user-locked spec 2026-05-26: even though it's
|
||||
currently a single canonical deck, we lock in the variant tier now
|
||||
so future Earthman editions slot in alongside as `earthman/<variant>/`
|
||||
w.o. a path migration.
|
||||
|
||||
Mapping today:
|
||||
earthman / earthman → earthman/default
|
||||
italian / minchiate-... → italian/minchiate-fiorentine-1860-1890
|
||||
english / tarot-rws → english/rider-waite-smith (strip "tarot-")
|
||||
"""
|
||||
if self.family == self.EARTHMAN:
|
||||
return "default"
|
||||
if self.slug.startswith("tarot-"):
|
||||
return self.slug[len("tarot-"):]
|
||||
return self.slug
|
||||
|
||||
@property
|
||||
def back_image_url(self):
|
||||
"""Full static-asset URL for this deck's card-back image, or empty
|
||||
@@ -270,7 +292,7 @@ class DeckVariant(models.Model):
|
||||
return ""
|
||||
from django.templatetags.static import static
|
||||
return static(
|
||||
f"apps/epic/images/cards-faces/{self.slug}/{self.slug}-back.png"
|
||||
f"apps/epic/images/cards-faces/{self.family}/{self.variant_dir_slug}/{self.slug}-back.png"
|
||||
)
|
||||
|
||||
def suit_slug(self, canonical_suit):
|
||||
@@ -565,12 +587,18 @@ class TarotCard(models.Model):
|
||||
"""Full static-asset URL for the card image, or empty string if the
|
||||
deck has no images (legacy text-only mode). Constructed via Django's
|
||||
`static` helper so STATIC_URL prefix + manifest-versioning (when
|
||||
WhiteNoise compressed manifest is active) flow through."""
|
||||
WhiteNoise compressed manifest is active) flow through.
|
||||
|
||||
Path structure: `cards-faces/<family>/<variant_dir_slug>/<filename>`
|
||||
per the family-grouped tree convention (user spec 2026-05-26). See
|
||||
`DeckVariant.variant_dir_slug` for the variant subdir mapping.
|
||||
"""
|
||||
if not self.deck_variant.has_card_images:
|
||||
return ""
|
||||
from django.templatetags.static import static
|
||||
deck = self.deck_variant
|
||||
return static(
|
||||
f"apps/epic/images/cards-faces/{self.deck_variant.slug}/{self.image_filename}"
|
||||
f"apps/epic/images/cards-faces/{deck.family}/{deck.variant_dir_slug}/{self.image_filename}"
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
Reference in New Issue
Block a user