feat: clash-royale card art (PR B)#68
Merged
Merged
Conversation
Two coordinated contract extensions that lay the groundwork for per-entity visual assets (Clash Royale card art, future world-airports flags, etc). contracts: - entity-schema.json: new optional 'icon_url' (uri) field on Entity. Backward compatible — no existing pack uses it. Domains that ship imagery store their per-entity URL there. - openapi.yaml: PlayResponse gains an optional 'entityId' field — the canonical id the resolver settled on. Saves the client from slugifying user-typed answers, which broke on accents and aliases. core: - Entity gains 'pub icon_url: Option<String>'. Engine ignores it; pure presentation metadata. Every Entity literal in the 20+ predicate family tests and scoring.rs is updated to add 'icon_url: None,'. server: - routes/games.rs::play now echoes the resolved entity_id back in PlayResponse (None on wrong answers, same as fame_score).
scripts/ingest/build_clash_royale_dataset.py - Persists Supercell's iconUrls.medium on each entity (newly allowed by entity-schema.json since the previous commit). - New download_icons pass writes web/public/cards/clash-royale/<id>.png from those URLs. Idempotent — skips when the file already exists at non-zero size. Failures are logged and non-fatal: CardIcon.svelte falls back to the entity name when an asset is missing. - CLI flags: --icons-dir <path> (default web/public/cards/clash-royale), --no-icons to skip the download pass (data-only ingest). domains/clash-royale/entities.json - 121 cards now carry icon_url. fame_score / attributes unchanged. - Generator stress test still passes 0/100 failures @ 1000 attempts. NOTE: the 18 MB of PNG assets land in web/public/ outside this commit — they are produced locally and shipped through the Docker image's web layer. Tracked in [[kalidoku-domain-clash-royale]] for future optimisation (sharp resize → webp would shave ~80% of the bundle).
Downloaded by the ingest script from Supercell's api-assets CDN. 285×420 PNG RGBA, ~150 KB each, 18 MB total. Stored under web/public so Astro's static build picks them up at /cards/clash-royale/<id>.png. CardIcon.svelte points at that path with loading='lazy' on the autocomplete list and 'eager' inside the end-game modal. Future optimisation: pipe through sharp at build time to produce WebP/AVIF at 96×96 (~25 KB each) which would shave ~80% of the bundle. Out of scope for this PR — happy to revisit if the bundle bloat becomes a measurable issue.
CardIcon.svelte — generic component that loads
/cards/<domain>/<entityId>.png with a graceful onerror fallback (the
<img> hides itself, callers render their own text fallback). Default
loading='lazy', opt-in 'eager' for already-visible contexts.
Three integration points, all gated on a DOMAINS_WITH_ART set so other
domains stay text-only:
- CellButton: card-art domains get a full-bleed image with the entity
name in a small caption strip. The plain text layout used by
paris-metro / rer is unchanged.
- AutocompleteModal: suggestion list flips from column to row layout
when the domain ships art, with a 36 px thumbnail to the left of the
highlighted name.
- EndGameModal: the solutions accordion shows a 28 px thumbnail next
to each candidate name + fame chip, eager-loaded since the user
expanded the section.
Plumbing:
- gameStore.CellAnswer gains an 'entityId' field, populated from the
/play response (new in this PR). Persists in localStorage so the
cell-art still works after a refresh.
- Grid.svelte forwards its domain prop to CellButton.
- share.test.ts fixtures updated for the new field.
The DOMAINS_WITH_ART set is currently {'clash-royale'} and is
duplicated in three components. A future world-airports domain
lights up automatically once its /cards/<id>.png files land — just
add the entry.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
PR B of the Clash Royale arc — adds card art alongside the data PR A shipped in #67.
CardIcon.sveltegeneric componentweb/public/cards/clash-royale/icon_urlin the entity payload (schema extension)entityIdechoed back inPlayResponseso the client doesn't have to slugify user inputDomains without art (paris-metro, rer) render unchanged.
Why
PR A landed the data + predicates but no visuals — Clash Royale cards are recognised primarily by their art, not their name. Without images the domain felt thin compared to the metro/RER experience.
How
Four commits in escalating scope.
4c9dca6 feat(core,server): expose icon_url + entityIdcontracts/entity-schema.json: optionalicon_url(uri) on Entity. Backward compatible.core::entity::Entity: matchingOption<String>. All 20+ predicate test literals updated (icon_url: None,).contracts/openapi.yaml+routes/games.rs::play:PlayResponsereturns the resolved canonicalentityIdon correct answers, so the client picks the right asset without slugifying the typed answer (which broke on accents/aliases).9dabc7a feat(domain-clash-royale): icon_url + download passscripts/ingest/build_clash_royale_dataset.py: persistsiconUrls.mediumon each entity, newdownload_icons()pass writes PNGs toweb/public/cards/clash-royale/<id>.png. Idempotent, throttled, failures non-fatal.--icons-dir,--no-iconsfor selective re-runs.domains/clash-royale/entities.json: 121 entries now carryicon_url.9376f77 feat(web): 121 PNG card assetsThe card art itself — 285×420 PNG RGBA from Supercell's
api-assets.clashroyale.comCDN, ~18 MB total. Stored underweb/public/cards/clash-royale/so Astro's static build serves them at/cards/clash-royale/<id>.png. Future optimisation: a sharp pass at build time to WebP at 96×96 would shave ~80% — out of scope for this PR.9d0f80e feat(web): CardIcon + UI integrationCardIcon.svelte— generic component.loading="lazy"by default; opt-ineagerfor already-visible contexts.onerrorhides the<img>so callers can rely on a sibling text fallback when an asset is missing.Three integration sites, all gated on
DOMAINS_WITH_ART = new Set(["clash-royale"]):Plumbing:
gameStore.CellAnswergainsentityId: string | null, persisted in localStorage so the cell-art survives a refresh.Grid.svelteforwards itsdomainprop toCellButton.Checklist
cargo fmt --check && clippy --all-targets -- -D warnings && test --workspacepassespnpm --dir web lint && typecheck && test && buildpasses (21 pages, 25 tests)unsafe, nounwrap()in non-test codeTest plan
/paris-metro/play→ text-only layout unchanged/clash-royale/play→ card art fills the cell with the name as caption