Skip to content

feat: clash-royale card art (PR B)#68

Merged
Calixteair merged 4 commits into
mainfrom
feat/clash-royale-card-images
May 14, 2026
Merged

feat: clash-royale card art (PR B)#68
Calixteair merged 4 commits into
mainfrom
feat/clash-royale-card-images

Conversation

@Calixteair
Copy link
Copy Markdown
Owner

What

PR B of the Clash Royale arc — adds card art alongside the data PR A shipped in #67.

  • New CardIcon.svelte generic component
  • 121 PNG card icons under web/public/cards/clash-royale/
  • Card art shown on solved cells, autocomplete suggestions, and end-game solutions list
  • Per-card icon_url in the entity payload (schema extension)
  • entityId echoed back in PlayResponse so the client doesn't have to slugify user input

Domains 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 + entityId

  • contracts/entity-schema.json: optional icon_url (uri) on Entity. Backward compatible.
  • core::entity::Entity: matching Option<String>. All 20+ predicate test literals updated (icon_url: None,).
  • contracts/openapi.yaml + routes/games.rs::play: PlayResponse returns the resolved canonical entityId on 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 pass

  • scripts/ingest/build_clash_royale_dataset.py: persists iconUrls.medium on each entity, new download_icons() pass writes PNGs to web/public/cards/clash-royale/<id>.png. Idempotent, throttled, failures non-fatal.
  • CLI flags --icons-dir, --no-icons for selective re-runs.
  • domains/clash-royale/entities.json: 121 entries now carry icon_url.

9376f77 feat(web): 121 PNG card assets

The card art itself — 285×420 PNG RGBA from Supercell's api-assets.clashroyale.com CDN, ~18 MB total. Stored under web/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 integration

CardIcon.svelte — generic component. loading="lazy" by default; opt-in eager for already-visible contexts. onerror hides 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"]):

  • CellButton — card-art domains: full-bleed image with the entity name as a thin caption strip. Text-only domains unchanged.
  • AutocompleteModal — suggestion list flips from column to row when art is available, 36 px thumbnail to the left of the highlighted name.
  • EndGameModal — 28 px thumbnail next to each candidate in the expanded solutions accordion.

Plumbing: gameStore.CellAnswer gains entityId: string | null, persisted in localStorage so the cell-art survives a refresh. Grid.svelte forwards its domain prop to CellButton.

Checklist

  • Conventional commit titles, no AI / Claude mention
  • cargo fmt --check && clippy --all-targets -- -D warnings && test --workspace passes
  • pnpm --dir web lint && typecheck && test && build passes (21 pages, 25 tests)
  • No new unsafe, no unwrap() in non-test code
  • No new secret committed (API key stays in ~/.config)
  • Contract changes are additive: `icon_url`, `entityId` are both optional

Test plan

  • Solve a cell on /paris-metro/play → text-only layout unchanged
  • Solve a cell on /clash-royale/play → card art fills the cell with the name as caption
  • Open autocomplete on a clash-royale cell, type "go" → suggestions list shows Goblins / Golem / Goblin Barrel with thumbnails
  • Finish a clash-royale game, expand "Voir les solutions" → each candidate has a small thumbnail next to its name
  • Inspect localStorage: `kalidoku.game.clash-royale.v1` answers carry an `entityId` string
  • 404 a card image (delete one file locally, refresh) → cell falls back to text gracefully

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.
@Calixteair Calixteair merged commit 095e090 into main May 14, 2026
7 checks passed
@Calixteair Calixteair deleted the feat/clash-royale-card-images branch May 14, 2026 01:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant