Skip to content

feat: line badges, rarity tags, dynamic masthead#65

Merged
Calixteair merged 2 commits into
mainfrom
feat/visual-polish
May 13, 2026
Merged

feat: line badges, rarity tags, dynamic masthead#65
Calixteair merged 2 commits into
mainfrom
feat/visual-polish

Conversation

@Calixteair
Copy link
Copy Markdown
Owner

What

Three visual improvements to make the game more readable and a bit more rewarding to play.

  1. RATP line badges — predicate labels like "Sur la ligne 7" now inline a colored pastille for the line number instead of leaving it as plain text. M1..M14 render as discs, RER A..E as rounded squares.
  2. Live rarity tags — every time the player answers correctly, the server returns the entity's fame_score. The client derives a Common / Rare / Épique / Légendaire tier (80/50/20 thresholds), pops a transient toast above the grid, and leaves a persistent corner badge on the cell.
  3. Dynamic masthead — the header eyebrow + title + date now reflect the current mode (daily / solo / duel) and domain (Métro / RER) instead of hardcoding "Grille du jour / Métro de Paris" everywhere.

Why

The masthead bug was an explicit ask after the multi-domain refactor — /rer/play was still claiming "Métro de Paris" in its header, which is obviously wrong.

Line badges and rarity tags came together: both lift the UI from "list of grey predicates" to something that feels like the metro/rer it represents. With #57 baking real fame scores into the dataset, the rarity dimension can finally surface during play instead of waiting for the end-game modal.

How

40771a5 feat(web): inline SVG line badges in predicate chips

  • New LineBadge.svelte component. Inline SVG (no binary assets), colors from the official RATP palette via CSS variables. Discs for métro, rounded squares for RER. Scales with size prop (default 18 px).
  • PredicateChip.svelte tokenises the localised label with a regex (/\b(?:ligne|line|RER)\s+([A-E]|\d{1,2}(?:\s?bis)?)\b/). Tokens render text or badge. Other predicate families untouched.
  • Grid forwards its existing domain prop down so the chip picks the right palette.
  • Editorial use only — disc + line code, no RATP wordmark.

1efad9c feat: live rarity tags + dynamic masthead

Contracts + server:

  • PlayResponse carries an optional fameScore field. Server reads grid.payload.entities[id].fame_score (snapshotted at generation time, stable through re-ingest), null on wrong answers and un-scored entities.

Web — rarity:

  • src/lib/rarity.ts: rarityFor(fame) with 80/50/20 thresholds, null → 'common'.
  • CellAnswer.fameScore persisted in localStorage so badges survive a refresh.
  • CellButton.svelte: persistent corner badge on cells where rarity ≥ rare (common cells keep the understated check).
  • Grid.svelte: transient toast above the grid on each correct answer ≥ rare, 1.7 s fade, gold w/ glow for legendary. prefers-reduced-motion respected.

Web — masthead:

  • Grid header reads mode + domain props. Eyebrow flips between daily / solo / duel labels. Title flips too. Date renders only in daily mode (solo/duel grids aren't "of the day"). domainLabel switches between paris-metro and rer i18n keys.

i18n: 13 new fr+en keys (4 rarity tiers, grid_mode_, grid_title_).

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
  • Mobile-first respected (badges scale, rarity toast positions over the grid, masthead unchanged at 360 px)
  • No new unsafe, no unwrap() in non-test code
  • No new secret
  • Contract change in contracts/openapi.yaml (additive — fameScore is optional)

Test plan

  • Open /paris-metro/play, answer with Châtelet → toast doesn't show (common), no badge
  • Answer with a fame=15 station → 'Légendaire' toast pops, gold badge stays in the corner after toast fades
  • Refresh mid-game → badges still there
  • Predicate "Sur la ligne 7" → '7' renders as a pink disc
  • Open /rer/play → predicate "Sur la ligne A" → 'A' renders as a red rounded square
  • Masthead on /paris-metro/play shows "Grille solo / Métro de Paris" + title "Solo", no date
  • Masthead on /rer/ (daily) shows "Grille du jour / RER d'Île-de-France" + title "Grille du jour" + today's date

LineBadge.svelte: a small inline SVG pastille drawn from the official
RATP palette. M1..M14 render as colored discs, RER A..E as rounded
squares (matches the RATP signage convention). Customisable size and
optional aria-label override.

Rather than ship binary PNG/SVG assets, the badge is inlined so it can
re-color through CSS variables (works in dark mode), scales next to the
surrounding type, and adds zero HTTP requests.

PredicateChip.svelte: tokenises the localised predicate label with a
regex matching 'ligne X', 'line X', or 'RER X' (case-insensitive,
captures digits 1-14 with optional 'bis' suffix and letters A-E).
Tokens are split into text/badge segments and rendered with LineBadge
inline. Other predicate families pass through untouched.

The chip now receives a  prop so it picks the right palette
(metro vs rer); Grid.svelte forwards its existing domain prop down.

This is editorial/non-commercial use of the line colour scheme — just
the disc + line code, no RATP wordmark or logo.
Two improvements bundled because they share the same /play response
shape change.

contracts + server:
- PlayResponse now carries fameScore (nullable, 0..=100) when the
  answer was correct. Read from grid.payload.entities[id].fame_score
  on the server, snapshotted at generation time so the value is stable
  even after a domain re-ingest. Null on wrong answers and on entities
  without fame data.

web — rarity:
- src/lib/rarity.ts: rarityFor(fame) → 'common' | 'rare' | 'epic'
  | 'legendary' using the 80/50/20 thresholds we agreed on. Null fame
  → 'common' on purpose so un-scored stations don't silently get
  mis-labelled.
- CellAnswer.fameScore persisted in localStorage so the rarity badge
  survives a refresh.
- CellButton: persistent corner badge on solved cells (rare / epic /
  legendary). Common cells keep the understated check so the badge
  wall stays readable.
- Grid: a transient toast pops above the grid on each correct answer
  whose rarity is >= rare, fades after 1.7 s, gold for legendary
  with a soft glow. prefers-reduced-motion respected.

web — masthead:
- Grid header now reads (mode, domain) instead of hardcoding
  'Grille du jour / Métro de Paris'. The eyebrow switches between
  daily / solo / duel; the title between 'Grille du jour' / 'Solo'
  / 'Duel'; the date renders only in daily mode (solo and duel grids
  aren't 'of the day'). domainLabel picks paris-metro vs rer i18n
  keys.

i18n: 9 new fr+en keys covering rarity tiers + grid_mode_* and
grid_title_* variants.
@Calixteair Calixteair merged commit 74ac4dd into main May 13, 2026
7 checks passed
@Calixteair Calixteair deleted the feat/visual-polish branch May 13, 2026 22:52
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