Refonte des récompenses de connexion quotidienne + codes à échanger — gagne des fragments chaque jour et utilise les codes de la communauté
Deux changements pensés pour la communauté arrivent ensemble. (1) La connexion quotidienne ne distribue plus de boosters gratuits sur un planning plat — elle accorde maintenant des fragments de booster chaque jour selon un cycle [3,3,3,5,5,5,8], et compléter une semaine consécutive donne un bonus de boosters croissant (1 → 2 → 3 → 4, plafonné à 4). Manque un jour et la série retombe à zéro — termine la semaine pour empocher la récompense d'ancrage. (2) Nouvelle section dans l'onglet Profil pour utiliser des codes (par ex. WELCOME-2026, codes de partenaires, tirages au sort de créateurs). Les codes peuvent donner n'importe quelle combinaison de boosters, fragments et fragments-de-rareté ; l'admin les rédige depuis le nouvel onglet Codes du panel d'administration, avec plafonds d'usage et dates d'expiration en option.
Récompenses quotidiennes
- Cycle quotidien de fragments : [3, 3, 3, 5, 5, 5, 8] selon ta position dans la série. Les fragments s'accumulent pour la forge et la conversion en boosters.
- Bonus de boosters à la fin de semaine, au jour 7 / 14 / 21 / 28 / … : +1 booster la première semaine, +2 la deuxième, +3 la troisième, +4 à partir de la semaine 4 (plafond).
- Manque un jour → la série retombe à 0 ; le prochain bonus 'semaine complétée' redémarre à +1. Les anciens paliers +3 / +5 / +10 aux jours 7 / 14 / 28 sont retirés.
- Interface du calendrier mise à jour : bande de 7 jours montrant la semaine en cours, avec un badge 'Sem N' pour le total de semaines complétées.
- Les séries existantes ont été remises à zéro au déploiement pour que tout le monde démarre le nouveau planning à égalité — aucun crédit de demi-semaine n'est reporté.
Codes à échanger
- Nouvelle section 'Échanger' dans l'onglet Profil — entre un code, vois la récompense, consulte tes 20 derniers échanges.
- Chaque utilisateur ne peut échanger un même code qu'une seule fois (garanti par un index unique par (code, utilisateur) — pas de double échange).
- Les codes peuvent contenir des boosters, des fragments et des fragments-de-rareté en n'importe quelle combinaison ; l'admin définit la récompense à la création.
- Plafond total d'utilisations facultatif (par ex. 'les 100 premiers échanges seulement') et date d'expiration par code (par ex. 'valide jusqu'à vendredi').
- Limité à 5 tentatives par minute et par utilisateur pour écarter le brute-force par typo. Toute création / édition / suppression de code par l'admin atterrit dans le log d'audit.
Vague plus large de traductions en jeu
- Les étiquettes de carte (ATK / DEF / PV / PM / Passive), les badges (NOUVEAU !, ✦ Nouveau, Bientôt en rotation, Legacy) et les phases (Bébé / Phase 1-3 / MAX) s'affichent dans ta langue dans la collection, la révélation gacha et les écrans de combat.
- Cloche de notifications + toasts de guerre de guilde : chaque ligne, suffixe 'il y a X', badge victoire/défaite/égalité et résumé de récompenses passe maintenant par la locale, avec les pluriels polonais/russes corrects.
- Le décor de la salle de combat (texte du lobby, overlay 'tourne ton appareil', étiquette de la main, préfixe d'adversaire, indice de cible, lien Annuler) et l'overlay de la pub récompensée (chargement, motifs d'erreur, bouton de retrait, indication de garder visible) sont maintenant entièrement localisés.
- Flux de confirmation de la galerie de copies (progression de niveau, confirmations de fonte, alertes de deck verrouillé et d'échange en attente, compte à rebours du rate limit, prévisualisation des fragments / pièces) — chaque chaîne qui n'existait qu'en anglais est désormais par langue.
- Le fallback ErrorBoundary (bouton Recharger + texte sûr) et les info-bulles de la WalletBar (boosters / fragments / pity / bonus quotidien dispo) s'affichent désormais dans la langue active sur les 12 locales supportées.
- Écrans pré-connexion traduits de bout en bout : connexion / inscription / 2FA / mot de passe oublié / réinitialisation (titres, sous-titres, placeholders, boutons, messages d'erreur et de succès, texte du gate âge + CGU, avertissement email non vérifié avec compte à rebours).
- Overlay d'onboarding au premier lancement (3 étapes actives + indicateurs à points, boutons passer et suivant, toutes les descriptions de types de carte / rareté / holo / phase / capacité) maintenant dans ta langue pour que le flux du premier booster atterrisse en natif.
- Phrases d'ambiance pendant l'ouverture d'un booster : 58 phrases uniques × 12 locales (~700 écritures créatives) pour que le texte qui défile pendant l'ouverture s'affiche dans ta langue, sur les boosters standard, or et diamant.
Polissage des langues sur tout le site
- Le bandeau cookies change maintenant correctement de langue à chaque changement de locale — avant, il restait coincé dans la première langue visitée dès le deuxième changement (par ex. /es → /en basculait, mais /en → /it gardait le bandeau en anglais au-dessus de la page italienne).
- Cartes en Vedette (la page de showcase) traduite de bout en bout dans les 12 langues supportées — chaque nom de carte et chaque texte marketing de 100-150 mots est désormais natif au lieu de retomber sur l'espagnol pour les visiteurs non hispanophones et non anglophones.
- Entrées de la page Nouveautés : cette entrée et les deux précédentes (2026-04-28 — traduction du texte des cartes + clarification du plafond de Fonte de masse) sont également entièrement natives dans les 12 langues. Les entrées plus anciennes retombent élégamment sur l'anglais en attendant qu'on les complète dans les prochaines mises à jour.
- Fenêtre détails de carte : l'étiquette 'X copies possédées' et le plafond de Fonte de masse reflètent maintenant la carte que tu regardes vraiment — avant, ouvrir une carte puis naviguer vers une autre dans la même session du modal affichait le compte de la PREMIÈRE carte pour toutes les cartes suivantes, donnant des nombres faux comme '4 copies' sur une pile dont tu possédais en réalité 19.
Le texte des cartes maintenant dans ta langue — noms, flavor et descriptions de capacités traduits pour les 113 cartes
Jusqu'à aujourd'hui, l'interface était localisée mais le contenu des cartes restait en anglais : noms, lignes de flavor, noms et descriptions de capacités s'affichaient en anglais quelle que soit la langue choisie. Nous avons traduit les 113 cartes en espagnol (Espagne + fallback LATAM), français, italien, allemand, portugais (BR + fallback PT), polonais, néerlandais, russe et turc — c'est-à-dire le nom canonique plus le flavor, plus les noms et descriptions des capacités actives et passives quand elles existent. La charge serveur en anglais reste la référence pour la base ; ce que tu vois à l'écran est superposé côté client selon ta langue, donc l'identité d'une carte reste la même à travers les changements de langue et les échanges. Si une traduction manque pour un champ d'une carte, le texte anglais prend le relais — jamais un blanc.
Ce qui a changé
- Les 113 noms de cartes traduits dans 11 langues cibles (es, es-419, fr, it, de, pt-BR, pt-PT, pl, nl, ru, tr) — pt-PT et es-419 sont désormais entièrement remplis en variantes natives, pas en fallback.
- PT-PT (portugais européen) : orthographes régionales (Fênix→Fénix, Tectônico→Tectónico, Gêiser→Géiser, Fumaça→Fumo), vocabulaire distinct (mordida→dentada, filhote→cria, pulinho→saltinho, quica→salta, terremoto→terramoto, demais→restantes, libera→liberta, bravo→zangado), placement enclitique (se alimenta→alimenta-se, a se tornar→a tornar-se), et impératifs en tu (Você não passará→Tu não passarás).
- es-419 (espagnol latino-américain) : tournures naturelles LATAM (Picado en Bomba→Bombardeo en Picada), évitement de 'Concha' (vulgaire dans le Cône Sud) remplacé par 'Caparazón' par prudence, et note claire que le voseo (AR/UY) n'est volontairement pas appliqué pour que le catalogue reste universel en LATAM.
- Lignes de flavor traduites pour chaque carte — soit 113 chaînes uniques × 9 langues.
- Noms et descriptions des capacités actives traduits pour les 60 cartes-créatures qui en disposent.
- Noms et descriptions des capacités passives traduits pour les 50 cartes-créatures qui en disposent.
- Câblé partout où une carte est affichée : grille de collection, modal de carte, comparateur, deck builder, modal de forge, onglet Forge, révélation gacha, salle de draft, main et zones en combat, info-bulles au survol, prévisualisations en drag, listes déroulantes d'offres d'échange et aperçu d'échange.
Notes
- Les lignes de stats d'objets comme '+5 ATK.' ou 'Feu +3 ATK.' restent telles quelles — c'est du texte mécanique où la traduction n'apporterait rien.
- L'identité de la carte (card_id, instance_id, seed holo, ATK/DEF/PV/PM) est préservée à travers la surcouche locale — seules les chaînes affichées changent. Échanges, decks et rejoues de combat sont 100 % compatibles entre langues.
Le plafond de Fonte de masse s'explique tout seul — tu vois pourquoi ton max est ce qu'il est
Retour des joueurs après le lancement de Fonte de masse : le changelog disait "jusqu'à 50 par lot" mais un joueur avec seulement 21 copies d'une carte tombait sur un plafond de 20 et supposait que c'était cassé. Rien n'était cassé — le serveur impose "garder au moins 1 copie de chaque carte" donc une pile de 21 copies plafonne à 20 d'un coup. Le plafond a toujours été min(plafond serveur 50, copies éligibles, copies totales − 1) ; il était simplement invisible. Maintenant le libellé du bouton d'entrée dit exactement ton plafond de lot sous la forme "max N sur M éligibles", et l'info-bulle au survol explique la contrainte qui plafonne (plafond serveur, éligibilité, ou garde-1). L'en-tête du panneau de sélection répète la contrainte pour que tu n'aies plus à deviner.
Ce qui a changé
- Le bouton d'entrée de Fonte de masse affiche maintenant "💠💠 Fonte de masse... (max N sur M éligibles)" au lieu de juste "(M éligibles)" — N est le vrai plafond de sélection respectant plafond serveur + garde-1.
- L'info-bulle au survol du bouton montre la raison qui plafonne : "plafond serveur 50", "M éligibles" ou "garde 1 sur T au total".
- L'en-tête du panneau de sélection ajoute désormais la contrainte qui plafonne après le chiffre, donc la raison est visible sans survol.
- Comportement inchangé : la règle garde-1 a toujours été imposée côté serveur. Ce changement est purement de la clarté UX — mêmes protections, étiquettes plus transparentes.
Fonte de masse — élimine jusqu'à 50 copies d'une carte en une seule action
Accumulateurs, celle-ci est pour vous. Faire fondre les copies communes une par une grillait la fenêtre de rate-limit par utilisateur — un joueur avec 80 copies communes d'une seule carte ne pouvait pas raisonnablement les nettoyer en une session. La vue détail de carte affiche désormais un bouton "Fonte de masse..." sous la Fonte par copie déjà existante, qui apparaît dès que tu as au moins 2 copies éligibles de la carte que tu regardes. Clique, choisis quels numéros d'impression détruire depuis une grille de tuiles (les copies verrouillées, dans un deck ou dans un échange actif sont grisées et désactivées), et tout le lot passe en une seule confirmation + un seul créneau de rate-limit. Le plafond serveur est 50 par appel, donc la réponse reste rapide. L'aperçu de récompense te montre les fragments exacts + les fragments-de-rareté que tu obtiendras avant d'appuyer sur Tout détruire.
Ce qui a changé
- Vue détail de carte → nouveau bouton "💠💠 Fonte de masse... (N éligibles)" sous la Fonte par copie. N'apparaît que quand tu as ≥2 copies éligibles (éligibles = non verrouillées + pas dans un deck + pas dans un échange actif).
- La grille de tuiles affiche chaque copie avec son numéro d'impression et les marqueurs holo / verrouillée / dans-deck / en-échange. Les tuiles non éligibles sont grisées et désactivées. La sélection est plafonnée à min(50, éligibles, copies − 1) pour que tu ne puisses jamais vider entièrement la pile.
- Contrôles "Tout sélectionner" + "Effacer" + un aperçu en direct "+X fragments / +Y fragments-R" qui reflète l'UI de récompense par copie. Le modal de confirmation demande une fois avant tout appel réseau : "Faire fondre N copies de \"Nom de la carte\" ? +X frags +Y fragments-R. [Annuler] [Tout détruire]".
- Côté serveur : un seul créneau de rate-limit est consommé quelle que soit la taille du lot — c'est tout l'intérêt de la fonctionnalité. La transaction complète est atomique (les 50 copies fondent toutes ensemble ou aucune ne fond).
- Sécurité sur échanges en attente : si une copie sélectionnée se trouve dans un échange actif, le serveur prévient et propose un dialogue "Annuler & Fondre" qui renvoie l'appel avec confirmation explicite. Pas d'écrasement silencieux d'un échange en cours.
- Sécurité dernière copie : par carte, le serveur garantit que tu gardes au moins une copie. Un lot qui mettrait une pile à zéro est rejeté avec une erreur claire et aucune ligne n'est supprimée.
Collection : filtre "Bientôt en rotation" + classements publics des tournois de guildes
Deux petites améliorations de confort bien visibles. Dans l'onglet Collection, le menu de disponibilité gagne une option « Bientôt en rotation » qui filtre la grille sur les cartes qui expirent dans les 30 prochains jours — même seuil que le badge qui s'affichait déjà sur ces cartes, donc filtre et badge restent toujours d'accord. Dans l'onglet Tournoi, les tournois de guildes affichent désormais leur panneau de classement à n'importe quel visiteur — plus seulement aux participants inscrits. Avant, on ouvrait un tournoi de guildes et on voyait les participants mais aucun tableau à moins que sa guilde soit inscrite. Maintenant le panneau public se rend pour tout le monde, en lisant les mêmes données que le serveur renvoyait déjà, alors on peut voir qui mène sans devoir s'inscrire d'abord.
Ce qui a changé
- Collection → menu de disponibilité → nouvelle option « Bientôt en rotation » filtre les cartes dont `available_until` tombe dans les 30 prochains jours. La rangée de puces actives affiche une puce « Bientôt en rotation » retirable tant que le filtre est actif.
- La fenêtre de 30 jours est désormais une constante partagée unique, donc le filtre et le badge « Bientôt en rotation » existant ne peuvent pas dériver.
- Onglet Tournoi → tournois de guildes → le panneau de classement se rend aussi pour les visiteurs non inscrits. Mêmes données que la vue participant ; le panneau te montre qui mène avant même que ta guilde ne s'inscrive.
- Les tournois hors guildes restent inchangés — le panneau public ne se rend que lorsque `tournament_type === "guild"`.
Durcissement build + CI — gates de qualité déplacés en CI, dérogations d'audit de dépendances, rafraîchissement quotidien de l'IAB GVL — sweep de revue hostile de 14 zones terminé
La dernière zone du sweep de revue hostile de 14 zones — pipelines de build et gates de CI. La plupart des changements sont invisibles pour les joueurs (ils vivent dans `.github/workflows/` et `scripts/`) mais ils font passer la qualité de « local uniquement avec bypass --no-verify » à « bloquant le merge sur chaque PR ». La suite vitest complète (~2700 tests), le type-check TypeScript, le drift gate de Drizzle, la parité des clés i18n, le pin BullMQ, la validation battle-config et le baseline bare-db-execute tournent désormais tous en CI sur chaque PR. Le scan CVE de production a gagné un gate conscient des dérogations qui expose les vulnérabilités modérées (le seuil précédent en taisait six). L'IAB Global Vendor List utilisée par le bandeau de consentement se rafraîchit désormais quotidiennement via un workflow planifié qui ouvre une PR quand la liste change. Deux tests skipped de la suite battle portent des références suivies (T-201, T-202) et un nouveau gate invariant fait échouer le build sur tout futur skip non tagué pour qu'aucun code mort ne s'accumule.
Ce qui a changé pour les joueurs
- Rien visuellement. Tout le lot est de l'outillage interne — workflows CI, gates d'audit de dépendances, hygiène de build. La note de clôture compte parce que la revue de 14 zones a fait remonter ~600 trouvailles à travers tout le code (auth, moteur de bataille, schéma, gacha, API admin, API publique, Game.tsx + onglets, UI cartes/decks/bataille, internals profil/guilde/tournoi, anticheat, i18n, légal/consentement, observabilité, infra build/test) — chaque correctif visible pour le joueur des treize zones précédentes a déjà été livré dans des entrées de changelog antérieures. Cette entrée ne fait que clore le sweep.
- Côté opérateur : l'IAB Global Vendor List que le bandeau de consentement utilise pour la divulgation par fournisseur se rafraîchit désormais quotidiennement via une GitHub Action planifiée qui ouvre une PR quand la liste change. Une GVL obsolète était un risque de dérive de portée de fournisseur (pire cas : Limited Ads d'AdSense dans l'UE). Un humain relit chaque PR de rafraîchissement avant le merge.
- Deux dépendances de production avec vulnérabilités connues (next-intl + postcss-via-next) sont maintenant formellement documentées dans `context/refs/cve-waivers.md` avec des dates de revisite explicites (2026-07-25). Le gate CVE précédent les masquait en relevant le seuil de gravité ; le nouveau gate fait remonter toutes les modérées-et-au-dessus et fait échouer la CI à moins qu'une ligne de dérogation ne les couvre. Visibilité honnête plutôt que risque silencieux.
Stabilité du serveur + confidentialité des logs — handlers d'erreurs du processus, rétention d'audit appliquée, noms de joueur redacted dans les logs
Une passe sur la pipeline d'observabilité et d'audit a comblé l'écart entre ce que la politique de confidentialité dit et ce que le serveur fait vraiment. Rien n'a changé visuellement pour les joueurs, mais plusieurs choses se sont améliorées en-dessous : le processus Node.js journalise et compte désormais les erreurs non capturées au lieu de planter en silence et laisser la plateforme le relancer sans explication ; la table audit_log est désormais purgée quotidiennement par un cron qui applique la fenêtre de rétention de 90 jours promise par la politique ; les noms de joueurs sont désormais oblitérés des lignes de log serveur (le considérant 30 du RGPD traite les noms d'affichage comme données personnelles) ; et la pipeline d'audit des événements de sécurité a gagné des checks de durabilité + une provenance par hash IP par action, ce qui permet à une inspection réglementaire de reconstruire n'importe quelle action de compte avec horodatage, acteur et origine.
Ce qui a changé
- Le processus Node.js journalise désormais une erreur non capturée avant que la plateforme ne le relance. Avant, le serveur mourait silencieusement, le conteneur redémarrait tout seul, et le débogage post-incident demandait de deviner depuis les horodatages de mort du conteneur. Désormais, chaque erreur fatale atterrit dans le log structuré avec la stack trace complète + un compteur pour qu'on puisse représenter les taux de crash dans le temps.
- La politique de confidentialité dit que les logs d'audit sont conservés 90 jours. Avant, rien ne l'appliquait vraiment — les anciennes lignes s'accumulaient pour toujours. Un nouveau cron quotidien (`/api/cron/audit-retention`) élague tout ce qui a plus de 90 jours par lots, ainsi la table reste bornée et la promesse de la politique est honnête.
- Les noms d'affichage des joueurs sont désormais oblitérés des lignes de log du serveur. Le considérant 30 du RGPD traite les noms d'affichage comme données personnelles ; la forme antérieure des logs incluait les champs winnerName / challengerName / opponentName mot pour mot sur chaque ligne de fin de bataille. Pino redact les masque par [Redacted] avant même qu'ils n'atteignent l'agrégateur de logs.
- Côté opérateur : les endpoints /api/metrics + /api/health supportent désormais un gate par token Bearer en production pour que les données d'observabilité ne soient pas exposées sur l'internet ouvert (poser METRICS_BEARER_TOKEN / HEALTH_TOKEN ; le dev tolère l'absence). L'endpoint de rapport de violation CSP (/api/csp-report) limite désormais le débit par IP pour qu'un spammeur ne puisse pas noyer les vraies alertes CSP.
Renforcement juridique et consentement — vraie chaîne TCF v2.3, audit de consentement durable, âge + CGU vérifiés côté serveur, liste réelle des sous-traitants, suppression + export de données étendus
Une passe sur la surface juridique et consentement a refermé une longue liste de trous de conformité. Têtes d'affiche : la chaîne de consentement IAB TCF v2.3 est désormais le format bit-packed officiel produit par les bibliothèques @iabtechlabtcf/* au lieu d'un shim base64(JSON) — le parser de Google AdSense rejetait notre shim depuis la date limite du 2026-03-01, ce qui rétrogradait silencieusement les visiteurs de l'UE en Limited Ads. Chaque choix sur le bandeau de consentement (Tout accepter / Tout refuser / Personnaliser / retirer / invite de version bump / réouverture) écrit maintenant une ligne durable dans audit_log pour qu'on puisse démontrer le consentement face à une enquête au titre de l'Art.7(1) RGPD. La confirmation d'âge 16+ et l'acceptation des CGU à l'inscription sont désormais appliquées côté serveur via les additional fields de Better Auth — l'ancienne case côté client uniquement pouvait être contournée par un simple curl. La politique de confidentialité nomme désormais les vrais sous-traitants (Arsys pour l'hébergement + Google AdSense pour la publicité) au lieu d'un texte placeholder — Postgres et Redis tournent sur une infrastructure Arsys gérée par l'opérateur et ne sont pas des sous-traitants distincts. La suppression de compte efface désormais un ensemble bien plus large de tables de données personnelles (amitiés, appartenance à une guilde, inscriptions aux tournois, tickets, historique de visionnage de pubs, graines provably-fair, jetons de vérification et plusieurs autres), utilise un suffixe pseudonyme cryptographiquement aléatoire qui casse l'ancienne traçabilité dans audit_log, refuse de recréer une demande de suppression pour un compte déjà entièrement supprimé, et borne les retries du cron pour qu'une panne transitoire ne boucle pas indéfiniment. L'artefact d'export de données a gagné six nouvelles sections (appartenance à une guilde, tournois, amitiés, tickets de support, historique de visionnage, historique de graines provably-fair) et des plafonds de lignes par section pour qu'un utilisateur power ne puisse pas faire OOM la génération. Le bandeau de consentement est désormais une vraie modale avec focus trap et aria-modal, le sélecteur de langue est visible à chaque breakpoint (caché sur mobile auparavant), et les bannissements admin délivrent maintenant une Déclaration de Motifs au titre de l'Art.17 DSA via notification in-app avec la règle enfreinte et un parcours de recours. En plus : une IAB Global Vendor List fraîche embarquée et un script de rafraîchissement quotidien pour que les changements de portée fournisseur ne dérivent pas, les cookies first-party AdSense vraiment supprimés au retrait du consentement (auparavant seul le tag de script était démonté), et un nouveau gate CSRF avec jeton sur les endpoints de suppression de compte et d'export de données pour qu'une page attaquante avec un formulaire falsifié ne puisse pas lancer le décompte de suppression pour un visiteur connecté. Version de consentement passée de 3.1 à 3.2 — chaque visiteur reverra le bandeau une fois à sa prochaine visite pour reconfirmer sous la liste mise à jour de sous-traitants.
Vraie chaîne IAB TCF v2.3 + audit de consentement durable
- La chaîne de consentement TCF v2.3 est désormais produite par l'encoder officiel @iabtechlabtcf/core et l'API de gestion du consentement est la bibliothèque officielle @iabtechlabtcf/cmpapi — remplaçant un shim base64(JSON) que le parser de Google AdSense rejetait depuis le 2026-03-01, ce qui rétrogradait les visiteurs de l'UE en Limited Ads. La CMP s'installe en `__tcfapi` sur la page exactement selon la spécification IAB CMP v2.
- Chaque résolution du bandeau de consentement (Tout accepter / Tout refuser / Personnaliser / retirer / invite de version bump / réouverture) écrit une ligne durable dans audit_log taguée scope='legal' avec le payload complet de consentement par finalité, les opt-ins de fonctionnalités spéciales, la chaîne addtl_consent, la version précédente et un flag indiquant si la curation Google ATP manquait au moment du consentement. On peut maintenant répondre à « prouvez que cet utilisateur a consenti à ce moment sous cette version » uniquement depuis audit_log — avant le correctif, la trace n'existait pas.
- La IAB Global Vendor List embarquée est rafraîchie et un script `npm run refresh:gvl` permet à un cron quotidien de déposer un snapshot frais. Une GVL périmée représentait un risque de dérive de portée fournisseur — chaque rafraîchissement effectue une vérification de schéma avant un renommage atomique sur disque, pour qu'un fetch interrompu ne laisse jamais un fichier à demi écrit.
Âge + CGU + CSRF + liste des sous-traitants vérifiés côté serveur
- L'inscription persiste maintenant `age_confirmed_at` et `tos_version_accepted` sur la ligne utilisateur via les additional fields de Better Auth, et un hook côté serveur refuse en dur toute inscription à laquelle manque l'une ou l'autre valeur. Avant, c'étaient deux cases côté client uniquement qu'une simple requête curl pouvait sauter.
- La liste des sous-traitants de la politique de confidentialité nomme maintenant les vrais tiers : Arsys (Espagne) pour l'hébergement + Google AdSense / AdMob pour la publicité. Postgres et Redis tournent sur une infrastructure Arsys gérée par l'opérateur et ne sont pas des sous-traitants distincts. La ligne hypothétique « prestataire de paiement » disparaît jusqu'à ce que la boutique cosmétique sorte vraiment sous un futur flag.
- Les endpoints de suppression de compte et d'export de données exigent désormais un token CSRF (pattern double-submit cookie), pour qu'une page attaquante avec un formulaire falsifié ne puisse pas lancer le décompte de suppression à 30 jours ni voler l'artefact d'export d'une victime connectée. Le cookie de session Better Auth est sameSite=Lax, ce qui aurait sinon autorisé des POSTs cross-origin depuis une navigation top-level.
Suppression de compte et export de données étendus
- La suppression de compte efface ou anonymise désormais un ensemble bien plus large de tables de données personnelles : amitiés et demandes d'amitié, appartenance et chat de guilde, demandes d'adhésion et wishlist de guilde, participants et rounds de tournoi, tickets de support et réponses, jetons et historique de visionnage de pubs, historique de graines provably-fair, jetons de vérification d'e-mail et de réinitialisation de mot de passe — et applique l'anonymisation sentinelle à match-telemetry, aux dons au pot de guilde, et à plusieurs autres tables conservées pour audit. Avant le correctif, plus de dix tables conservaient silencieusement les données de l'utilisateur même après que la suppression soit « terminée ».
- Le pseudonyme utilisé pour écraser le nom et l'e-mail d'un compte supprimé utilise maintenant un suffixe cryptographiquement aléatoire au lieu d'une tranche déterministe de 8 caractères de l'identifiant utilisateur d'origine. L'ancienne tranche emportait 32 bits d'entropie de l'identifiant d'origine, donc un régulateur pouvait corréler le nom sentinelle vers l'utilisateur supprimé via audit_log.target_id. Le nouveau suffixe casse complètement le lien.
- Deux demandes de suppression simultanées du même utilisateur se rabattent maintenant sur une seule ligne au niveau de la base de données (index unique partiel sur `(user_id) WHERE status IN ('pending','processing')`) ; le POST de la demande renvoie `already_pending` au lieu de crasher en 500. Un utilisateur dont la suppression est déjà terminée ne peut pas créer une nouvelle demande — la nouvelle vérification `wasUserEverDeleted` refuse net, prévenant qu'un futur un-ban ne ressuscite un compte à demi anonymisé.
- Si une transaction de suppression échoue en cours de route (violation de clé étrangère, lock timeout, micro-coupure réseau), le worker cron incrémente maintenant un compteur de retry et sauvegarde le dernier message d'erreur au lieu de boucler indéfiniment en `pending`. Après cinq tentatives échouées, la ligne bascule en `failed` pour attention admin.
- L'export de données couvre désormais six nouvelles sections : appartenance à une guilde, inscriptions aux tournois, amitiés, tickets de support et réponses, historique de visionnage de pubs, et historique de graines provably-fair. L'auto-documentation `gaps[]` antérieure les listait comme « on a la donnée mais on ne la livre pas » — ce qui violait la portabilité de l'Art.20 RGPD. Chaque section porte aussi un plafond de 10 000 lignes pour qu'un utilisateur power ne puisse pas faire OOM la génération de l'export, et un champ `truncations[]` rapporte honnêtement les exports partiels.
Accessibilité du bandeau + hygiène des cookies AdSense + délivrance Art.17 DSA
- Le bandeau cookies est désormais une vraie modale : role='dialog', aria-modal='true', aria-labelledby pointant vers un titre réservé aux lecteurs d'écran, et un focus trap qui capture le focus à l'ouverture et le restitue à la fermeture. Le Tab ne peut pas s'échapper sur la page en dessous. Esc, intentionnellement, NE FERME PAS, car ce serait un refus implicite — un dark pattern.
- Le sélecteur de langue (ES | EN) est désormais visible à tous les breakpoints, y compris mobile — avant le correctif il était caché sous le breakpoint sm:, ce qui se mariait mal avec le bug middleware F-A11-009 qui mettait déjà silencieusement les visiteurs hors-locale-par-défaut dans la mauvaise langue.
- Le retrait du consentement publicitaire supprime maintenant vraiment les cookies first-party d'AdSense (`__gads`, `__gpi`, `__eoi`, `FCNEC`, `FCCDCF`) du navigateur, et pas seulement la balise de script injectée. Avant le correctif, un retrait laissait ces cookies en place, ce qui violait ePrivacy + Art.7(3) RGPD (le retrait doit équivaloir à la suppression des cookies).
- Quand un admin bannit un utilisateur, l'utilisateur reçoit maintenant une notification in-app contenant la Déclaration de Motifs au titre de l'Art.17 DSA avec la règle enfreinte, la raison et un parcours de recours — et la ligne audit_log porte le même payload sous scope='legal'. Avant le correctif, les bannissements étaient silencieux : l'utilisateur ne pouvait pas se connecter, ne savait pas pourquoi, et n'avait aucun chemin de recours documenté. L'endpoint refuse désormais tout bannissement qui ne fournit pas la règle et la raison.
i18n hardening — Spanish chrome everywhere, locale-aware dates, mobile language switcher, unified contact email
A pass through the bilingual surface closed every visible English-only chrome remnant on Spanish pages and tightened a few middleware + SEO gaps. Headline items: the marketing nav (About / How to Play / Cards / FAQ / Contact / Updates / Log in / Play for Free), every legal-page header link, the global cookie-consent banner, the offline banner, and the language-switcher screen-reader label all read from the active-locale message bundle now — Spanish visitors no longer see English wrappers around Spanish copy. The language switcher is also visible on phones (was hidden under the sm: breakpoint), so a visitor landing on the wrong locale on mobile can finally jump in-page. Changelog dates are stored as ISO YYYY-MM-DD and rendered through Intl.DateTimeFormat per locale — Spanish visitors see "26 de abril de 2026" / English visitors see "April 26, 2026". Email addresses across the legal + contact surface have been unified to a single canonical contact@drawntcg.com so visitors don't have to guess between info / privacy / support inboxes. A new /robots.txt now points crawlers at the per-locale sitemap, and link-preview crawlers (Slack, Discord, iMessage, Twitter) get an Open Graph card with the right locale tag. A subtle middleware bug that made the <html lang="…"> attribute always say "es" on /en/* pages — silently breaking screen-reader locale routing and Google's hreflang verification — has been fixed.
Spanish chrome on every public surface
- MarketingHeader (every public page) reads its nav labels and CTAs from the marketing_header.* namespace — Spanish visitors see Sobre nosotros / Cómo jugar / Cartas / FAQ / Contacto / Novedades / Iniciar sesión / Juega Gratis instead of the English equivalents.
- Legal page chrome (the back button, the four short links in the header — Terms / Privacy / Cookies / Legal Notice — and the long versions in the footer plus the all-rights-reserved line) all translate. Pre-fix /es/legal/* pages had a Spanish body with English chrome wrapping it.
- Global cookie-consent banner (the EU-mandated TCF v2.3 surface) translates intro copy, the Accept All / Reject All / Customize / Save Preferences buttons, every section heading, and every screen-reader aria-label so Spanish visitors don't see an English banner when they first land.
- The "You appear to be offline" banner (mounted on every page) and the language-switcher's accessibility label both read from the active locale.
Locale-aware dates + visible language switcher on mobile
- Changelog entry dates are now stored as ISO YYYY-MM-DD and rendered with Intl.DateTimeFormat — "26 de abril de 2026" in Spanish, "April 26, 2026" in English, with a proper <time dateTime="…"> wrapper for SEO.
- The Spanish changelog page heading is now "Registro de cambios" instead of the English word "Changelog".
- The language switcher (ES | EN) is now visible at every breakpoint, including phones. Pre-fix it was hidden under the sm: breakpoint, so a phone visitor landing on the wrong locale had to manually edit the URL to switch.
Single canonical contact email + softer Discord copy
- Every contact channel across the marketing, legal, and privacy surface points at a single canonical address: contact@drawntcg.com. Pre-fix the same visitor could see info@ on the contact page, privacy@ in the privacy policy, and support@ in the terms — confusing for anyone trying to figure out where to write.
- The Discord callout was softened — instead of "the invite link is being set up — when it lands it will show up here" it now says the invite is coming soon and points readers at the changelog page for updates in the meantime. No false promise of imminence.
Crawler + middleware fixes (mostly invisible but real)
- A new /robots.txt enumerates the public surface, blocks the authenticated game shell + admin + API + auth-only paths from crawl, and points search engines at /sitemap.xml so the per-locale sitemap is no longer discovered organically.
- Link-preview crawlers (Slack, Discord, iMessage, Twitter / X) now receive an Open Graph card with the active locale tag (es_ES or en_US) plus the alternate-locale list and a Twitter summary_large_image hint — bilingual link shares no longer surface the wrong-locale variant by accident.
- A subtle middleware bug that made <html lang="…"> always say "es" on /en/* pages is fixed. Pre-fix the request-header forwarding ran before next-intl's locale-injection, so the root layout never saw the active locale and fell back to the default — silently breaking screen-reader locale routing and Google's hreflang verification.
- Set-Cookie headers are now appended (not set) when merging next-intl's response — multiple cookies on a single response are no longer collapsed into one comma-joined value that browsers misparse.
Anticheat + provably-fair hardening — atomic seed rotation, replay scrubbing, spectator strategy strip
A pass through the anticheat surface (provably-fair seeds, replay sanitization, spectator state, collusion-detection telemetry) closed a small list of subtle but real defects. Headline items: pack-opening seed rotation is now atomic against in-flight pack opens, so a verifier replay can never reproduce N−1 of N openings while the Nth orphan dangles unverifiable; replay history scrubs raw player ids out of turn-log strings before persistence so the publishable replay file truly cannot be cross-correlated by id; spectator state strips two alpha-strategy hints (adrenaline counters and per-turn summon counts) that pre-fix flowed through unmasked; provably-fair seed rotations and client-seed changes are now durably audited so a player dispute has a recoverable trail; the history endpoint no longer 500s on garbage page params and no longer leaks raw Postgres errors to clients.
Provably-fair integrity
- Pack-opening seed rotation is now atomic against any in-flight pack open. Pre-fix a rotate landing between a pack-open's seed read and nonce update could publish a stale total_nonces value, leaving the in-flight opening unverifiable by the verifier. The race is closed via a transactional row lock plus a database-level unique-active-seed constraint.
- Both seed rotations and player-supplied client_seed changes now write a durable audit row, so a player who later disputes "I rotated at 12:34 and my opening is broken" has a recoverable trail in audit_log alone instead of relying on log-line forensics.
- PATCH client_seed now validates the format (8-64 hex chars) so unicode, control characters, or `:` separators that would ambiguate the verifier's HMAC message string can no longer be saved.
- Provably-fair history page now degrades gracefully on garbage page parameters (?page=NaN, ?page=abc) and no longer leaks raw Postgres error messages to authenticated clients.
Spectator + replay privacy
- Spectator state no longer surfaces adrenaline counters or per-turn summon counts — these are alpha-strategy hints that competitive metagame analysis tooling could be built off of. Combatants still see them in their own action-planning UI.
- Persisted replays now scrub raw player ids out of turn-log strings before storage. Pre-fix the JSON-keyed identifier fields were already pseudonymized to player_1 / player_2 but the free-text log lines could still embed raw ids — a non-admin reader of the replay blob could correlate replays by matching id substrings.
Profile, guild, tournament, and battle UI hardening — public profile clarity, achievement toasts, action timeouts
A pass through tournaments, guilds, the profile sections, and the in-battle internals fixed a long list of small bugs and added the visible affordances that were missing. Headline items: viewing a stranger's profile now tells you why bio / display case / achievements are hidden ("add as friend to see more") instead of rendering blank sections; achievement toasts auto-dismiss after 6 seconds (and cap at 5 visible at once) instead of stacking forever; every in-battle action — summon, attack, ability, mulligan, end turn, forfeit — now has a 5-second "no response from server" safety net so a flaky socket can't leave the UI stuck pending; the forfeit button uses an in-app modal instead of the browser's native confirm() popup that iOS PWAs sometimes silently suppress; the tournament bracket marks forfeit / disconnect / bye matches with a small badge so admins and spectators can tell them apart from real wins; deck-builder save validates the 3-copies-per-card limit client-side before letting you press Save; and a list of guild-management quality-of-life polish items (kick / promote confirmation, donation amount cap, war-end refresh filtering, identical-message dedupe).
Public profile + achievement toasts
- Viewing another player's profile now shows an inline notice ("Add as friend to see this player's bio, display case, achievements, and recent activity") when those sections are gated by the friendship check. Pre-fix the sections just rendered blank and players thought their account was broken.
- Public profile now shows the player's division badge (Bronze / Silver / Gold / etc.) alongside ELO — was missing on other-player profiles even though it was on your own.
- Sharing a profile link now shows "Link copied!" / "Could not share" feedback instead of being silent.
- Achievement toasts now auto-dismiss after 6 seconds and cap at 5 visible at once. Pre-fix they stacked forever and players who unlocked 5 achievements in a battle had to manually click each ×.
In-battle robustness
- Every player action — summon, evolve, attack, ability, equip, set trap, place field, use object, mulligan, end turn, go to battle, forfeit — now races against a 5-second server-ack timeout. If the server doesn't respond (network drop, server hiccup) you get a "No response from server" toast instead of the action sitting visually pending forever.
- Forfeit button uses an in-app confirmation modal showing the consequences (loss for record + rating). Replaces the browser's native confirm() popup that iOS PWA installs sometimes silently suppress, leading to "I clicked forfeit and nothing happened" reports.
- Mulligan decision can no longer multi-fire: a rapid double-tap on Keep/Mulligan now sends only one decision to the server.
- Spectator orientation hardening: the in-battle action handlers now no-op for spectators as a defense-in-depth layer over the existing UI gating.
- Battle tooltips no longer flicker shut on Android because of the touch+click race; status / zone tooltips now stay open on tap until you tap somewhere else.
- Mulligan timer screen and the in-app confirm modals now have proper aria-modal labelling for screen-reader users; previously they were unannounced.
Tournaments + guilds
- Tournament bracket now shows a small FF / DC / BYE badge on matches that ended by forfeit, disconnect, or auto-advance — pre-fix those rendered identical to real wins.
- Guild kick / promote / demote actions now require a second click to confirm (button changes to "Click again" with a 4-second auto-cancel). Pre-fix a single accidental click on Kick removed the member instantly.
- Guild chat: Enter spam can no longer post identical messages within 5 seconds; emoji and RTL names render their first character correctly in avatar bubbles.
- Guild pool donation amount is now bounded (1 to 99,999 fragments) and rejects garbage values like NaN that the form could previously generate.
- Guild war end-of-war notification now only refetches our own guild's wars instead of every guild_war_ended event in the world.
Profile sections
- Display Case selection: ordering is now user-controllable (←→ buttons on selected cards); cards traded away or dissolved no longer linger as zombie selections; the per-page collection cap was raised to 1000 cards with a clear notice if you have more than that.
- Profile name / avatar / bio now save independently — pre-fix clicking Save next to your avatar (after also typing a new name) submitted both, accidentally burning the 30-day name change cooldown.
- Avatar URL field now shows an inline preview as you type, with a fallback when the URL is invalid.
- Password change client validation now mirrors the server policy (min 8 chars + at least one digit + at least one letter + must differ from current). Pre-fix the only check was 8-char minimum.
- 2FA setup secrets (TOTP URI, backup codes, password fields) now wipe from React state on unmount so browser extensions can't scrape them from a stale snapshot.
- Provably-fair Rotate Seed: failures now surface a clear error (was silent), and the revealed seed gets a "Copy seed" button + a warning that the next rotation will reveal a new one and this one cannot be re-displayed.
- Account deletion request: surfaces "Deletion service is temporarily unavailable" upfront if the backend isn't configured, and double-clicks are rejected synchronously so two deletion rows can't be scheduled.
Battle UI fixes — spectator view, mulligan timer, deck builder hardening
A pass through the in-battle screen, the deck builder, and the card / trade modals fixed a long list of small but visible bugs. The headline items: spectators were seeing the battle mirrored to the wrong side (their view labelled the opponent as "you"), the mulligan timer reset to 30 every time anything happened on the board, the queue size showed blank when joining casual matchmaking, and trade history's wishlist 🎯 markers showed the wrong player's wishlist. The card modal now cancels in-flight fetches when you flip through a gallery, the deck builder caps how many pages it loads (so a server hiccup can't lock the screen), and copying a deck export now actually tells you whether it worked.
In-battle correctness
- Spectators now see the battle with a fixed orientation — challenger always at the bottom, opponent always at the top — instead of having the view randomly mirror to the wrong side and label the wrong player as you.
- Mulligan timer no longer resets to 30 every time the opponent acts. It now starts at 30 once when the mulligan phase begins and counts down honestly from there.
- Card tooltips no longer get wiped mid-read every time the opponent's HP ticks. They only close on real turn / phase changes now.
- If you tab out of a battle and come back, the in-battle screen requests a fresh state from the server instead of rendering whatever stale snapshot it had.
- Leaving a battle from the lobby (before the opponent joins) now actually frees your slot on the server — pre-fix you could get stuck in "already in a battle" trying to requeue.
- Casual queue size now displays correctly on join (was showing blank / NaN due to a payload mismatch).
- Battle history (the resolved list under your decks) now updates live when a battle ends — no more refresh required to see your latest match.
- Rematch banner now only shows after a battle that resolved naturally (win / loss). It used to show after forfeits and opponent disconnects too, which was confusing.
- Tournament report-result now uses an in-app modal that shows the opponent's name. Native browser confirm() popups are gone (one less spot the design guidelines didn't apply).
Deck builder + card modal
- Deck builder now validates the 3-copies-per-card limit before letting you press Save (was checked at add time only — corrupt or imported decks could slip through with 4+ copies and silently fail server-side).
- Loading a giant collection in the deck builder is now bounded — there's a hard cap on how many pages it'll fetch (50, well above any realistic collection) so a server pagination bug can't lock the screen.
- Card modal cancels in-flight network requests when you flip to another card. Pre-fix the previous card's wishlist / instances / evolutions could resolve last and overwrite the new card's data.
- Locking / unlocking a card copy now surfaces an error if the server rejects it. Used to silently swallow the failure.
- Trade history shows a Retry button if the load fails, instead of a fake "No completed trades yet" forever.
- Copying a deck export to clipboard now shows a Copied! / Copy failed toast. Used to be silent.
- Live battles list (the spectate panel) no longer shows a Watch button on your own battles, and pauses its 15-second poll when the tab is in the background.
Battle action feedback — no more frozen UI when the server hiccups
When you summon a creature, attack, or end your turn, the client now waits up to 5 seconds for the server to confirm. If the confirmation never arrives — server hiccup, network drop, dropped packet — you get a clear "No response from server — check your connection" message instead of a button that just stops responding. If the server rejects the action (out of phase, illegal target, rate-limited, etc.) the rejection reason now reaches you immediately even on flaky connections, instead of riding the next state push and arriving seconds late. The other (less critical) actions are unchanged. Spanish and English locales also got `not-found` and `loading` pages so a stale link or a slow load lands on a localised page instead of the framework default.
Battle action feedback
- Summon, attack, and end-turn now show an error toast if the server doesn't acknowledge within 5 seconds, instead of leaving the action visually pending.
- Server-side rejections (out of phase, illegal target, rate-limited) now reach you immediately even when state push is delayed.
- Localised `not-found` and `loading` pages — stale links and slow loads now land on a Spanish or English page instead of the framework default.
Privacy + safety tightening
- Profile bio, display case, recent activity, and achievements are now visible only to friends and to yourself. Strangers see your name, rating, and division but not your collection wall — closes a long-standing complaint that anyone with your user URL could browse your private profile content.
- Blocking a player is now a hard hide — your profile returns 404 to anyone you've blocked (and theirs to you), instead of just hiding tabs while still leaking the basics.
- Friend activity feed no longer surfaces fields that the original event didn't intend to share. Each event type has a strict allowlist (rare pull, achievement, battle result, trade, guild join, tournament win) so nothing extra leaks even if a future event payload changes shape.
- Leaderboard now requires sign-in. Anonymous bots can no longer scrape the rating table.
Notifications + import
- You can now mark individual notifications as read instead of "all or nothing" — the inbox now passes the specific ids you tap.
- Deck import now caps the pasted text at 4 KB so a giant accidental paste returns a clear error instead of stalling. The cap is large enough for any legitimate deck list.
Real-time push fixes
- Friend requests, accepts, and rejects now actually update the badge counters and friend list without a manual refresh. The wire vocabulary between client and server had drifted apart and every notification was being silently dropped at the server allowlist; the canonical verbs are now shared and the relay forwards them end-to-end.
- Trade offers, accepts, declines, and cancellations also push to the other player live now — the badge ticks up the moment the offer arrives, instead of waiting for the next page load.
- Trade UI: the 🎯 markers now correctly show cards the OTHER player wants (used to silently show your own wishlist on both columns).
Tab UX + battery savings
- Hidden tabs no longer drain bandwidth in the background — the leaderboard and challenges tabs now pause their live subscriptions when you're on another tab and resume when you come back.
- Leaderboard now shows a clear error + retry button if the load fails, instead of the perpetual "Loading leaderboard…" spinner.
- Challenge / Accept buttons in Battles no longer lock forever when the socket hiccups. After 15 seconds without a server ack you get a "Network timeout — try again" toast and the button releases.
- Tournament "I Won" confirmation is now a styled in-app dialog showing the opponent's name, instead of the browser's native confirm() popup. Same for visual polish + accessibility (aria-modal).
- Support ticket replies are now a multi-line textbox (Shift+Enter for newline, Enter to send) instead of a single-line input that silently truncated paragraph replies.
SEO sweep — sitemap, hreflang, canonical URLs across all locales — Phase 1 i18n fully closed
The bilingual public surface is now wired up for search engines. Every Spanish and English page emits a `<link rel="canonical">` pointing at its own locale URL plus an `<link rel="alternate" hreflang>` map covering both locales and an `x-default` fallback, so Google can pair the /es and /en variants of the same page instead of treating them as duplicates. A new `/sitemap.xml` enumerates every Phase 1 public page in every locale with the same hreflang alternates per entry. With this release Phase 1 of the i18n project is fully closed: 4 legal pages bilingual, 7 marketing pages bilingual, controlling-version clauses live in both languages, locale switcher in both headers, hreflang plumbing in metadata and sitemap, and a written workflow template for any future page.
SEO + future-page template
- New `/sitemap.xml` enumerates every Phase 1 public page (landing, about, contact, FAQ, how-to-play, showcase, changelog, legal/cookies, legal/notice, legal/privacy, legal/terms) in both Spanish and English, with per-entry hreflang alternates so search engines pair the locale variants properly.
- Every translated page now emits a per-page canonical URL pointing at its own /es or /en path and an hreflang `languages` map covering both locales plus `x-default` (defaulting to Spanish), so duplicate-content penalties are off the table.
- Future-page workflow template added to `context/refs/marketing-hero-draft.md` — every new public page added after Phase 1 must ship in BOTH Spanish and English from the first commit. The template covers the dual-locale draft workflow, TCG vocabulary, register rules, and the required SEO wiring.
- Phase 1 i18n closes here: 11 public pages × 2 locales fully bilingual end-to-end, plus the SEO infrastructure, controlling-version clauses, locale switcher, and the written contract for future bilingual pages.
Marketing pages, mastery, tournaments, and a sweep of UX gaps
A big batch of player-facing work landed across the spring. The headline items: a public marketing surface for new visitors, a card mastery system that rewards loyalty to specific cards, a tournament bracket viewer that finally lets you see how a tournament is unfolding, and a long list of small UX fixes that close gaps players have been pointing out for months. Below is the rough shape of what changed, organised by area. Patch notes for individual cards and balance tweaks live in the in-game patch feed; this changelog covers the structural and surface-level stuff.
Marketing pages (public)
- New landing page that actually explains the game, instead of dropping you straight onto a login form. Returning players get a one-click "Log in" in the sticky header.
- About, How to Play, FAQ, Contact, Featured Cards, and this Changelog — all reachable from the same header on every public page.
- Cookie consent banner upgraded to the IAB TCF v2.3 standard so EU players get the proper opt-in flow with all the per-purpose toggles.
Crafting and collection
- New Forge tab dedicated to crafting cards from the fragments you earn by dissolving duplicates.
- Mastery system: as you play with the same card across battles, it levels up. Once a card hits level 5, you get a 10% crafting discount on copies of it. At level 10 it gets a "Mastered" badge.
- Crafting cost now shows the discounted price inline at the craft button, so you can see the saving without diving into card detail.
- Wallet bar now shows your fragment stash split per rarity, instead of one aggregate number. Easier to see at a glance whether you can afford a craft.
- Card sets browser: see every set, what cards belong to it, and your progress through it. Helps you target which sets to chase.
- Persistent "Verify past pulls" link in the Gacha tab — tap it any time to audit any pack you have ever opened against the provably-fair RNG record.
Battles and competitive
- Tournament bracket viewer: when you click into a single-elimination tournament, you now see the full bracket tree with every match, who is playing whom, and who has won so far.
- Public guild tournament standings: non-participants browsing a guild tournament can see the standings panel without being enrolled.
- Battle replays now have their own shareable URL. Open the replay in a new tab, send the link to a friend, point at the moment you want to talk about. Works for any battle either of you played in.
- Element field-spell weather effects now show up properly in damage calculations during battle — fire fields boost fire creatures, water fields boost water, etc.
- Daily and weekly challenges have a streak system: completing them on consecutive days builds a streak with milestone rewards at 7, 14, and 28 days.
- Battle stats panel shows per-element breakdowns + period filtering, so you can see how your fire decks have been doing this week vs last month.
- AI battle spam fix: a race condition that allowed double-starting AI battles is closed. No more orphan match windows.
Account and trust
- Two-factor authentication available from your profile security section. Standard authenticator apps (Authy, Google Authenticator, 1Password, Bitwarden) all work.
- Self-serve account deletion lives in the profile section — no support ticket, no won't-let-you-leave dance.
- Self-serve data export: download a copy of everything we hold about your account.
- Session security tightened: changing your password or enabling 2FA now invalidates all your other active sessions automatically. If someone was logged in as you on a different device, they get kicked.
- Email verification flow improvements: clearer messaging when verification is pending, retry-with-cooldown on the resend button.
Performance and reliability
- Battle reconnection improvements: if you drop mid-match for under a minute, you reconnect to the same game state with no penalty.
- Server-side deck-out handling rewrites — the slow control decks that win by exhausting your opponent's library now work cleanly across reconnects.
- Tournament forfeit timeouts moved to a durable job queue, so a server restart no longer eats a forfeit-pending tournament.
- Offline detection banner: if your connection drops, you see a clear banner explaining what happened, instead of the UI silently failing.
Notes for next time
- We are still adding cards, sets, and modes. The world has plenty of room to grow.
- Discord invite link is being set up — when it lands the contact page will surface it.
- If you spot something here that does not match what you see in-game, tell us — most likely we shipped it differently than the note suggests.
All marketing pages now available in Spanish — Phase 1 complete
Six more marketing pages are now bilingual, closing out Phase 1 of the marketing translation effort. /es/about renders the team / vision / game-world write-up in Spanish. /es/contact translates every channel. /es/faq covers all 12 Q&A items. /es/how-to-play translates the 10-minute rundown. /es/showcase renders the curated 7-card showcase with bilingual card names + flavor blurbs. /es/changelog (this page) is now bilingual too — every ChangelogEntry carries both Spanish and English copy together in the source so future updates land in both locales atomically. The four legal pages shipped bilingual earlier this month, so the public surface is now fully bilingual end-to-end.
Marketing pages translated
- About page (Sobre Nosotros) translated to Spanish — hero 'Hecho entre amigos, por gusto', team / vision / game-world sections all mirror the English source 1:1.
- Contact page translated — section headings 'Correo / Discord / Tickets de soporte / Una nota sobre los tiempos de respuesta', back / FAQ navigation buttons.
- FAQ translated — all 12 Q&A items across three sections (Sobre el juego / Cómo jugar / Cuenta), including questions about rewarded ads, account deletion, and where data is stored.
- How to Play translated — three sections (Mecánicas, Tipos de cartas y elementos, Flujo de batalla — turno a turno). TCG glossary mazo / sobre / carta / PV / zona de monstruo / mulligan kept as-is.
- Featured Cards (showcase) translated — every card name + 50-150 word marketing blurb. Element + monster-class labels also translated (Fuego/Agua/Viento/Tierra/Metal/Rayo/Neutral; Dragón/Bestia/Constructo/Leviatán/Alado; slime preserved as TCG term).
- Changelog page itself bilingual — every ChangelogEntry now carries both Spanish and English copy together in the source. MAINTAINER NOTE in the page source updated: from this release on, every new ChangelogEntry MUST land in BOTH Spanish AND English in the same commit. Single-locale entries are forbidden.
- All Spanish versions use the indie/playful voice from the landing page — informal tú throughout, TCG vocabulary preserved (sobres, mazos, cartas, tablero, dorsos, gremios).
- Locale switcher in the marketing header lets you flip between /es and /en versions of any page without losing your place. Phase 1 marketing translations now complete.
Landing page now available in Spanish — first marketing page translated
The homepage hero is now bilingual. Spanish-speaking visitors landing on /es see the full marketing pitch in Spanish — anti-pay-to-win positioning, daily-free-packs promise, multi-pace modes (casual, ranked, AI, just collecting), element chips, and the 'built among friends' footer — all in informal tú voice. English visitors keep the original wording at /en. This is the first marketing page to land in two languages; the rest are queued up next.
Marketing pages
- Landing page (homepage hero) translated to Spanish — every paragraph mirrors the English source 1:1: hero badge ('Gratis · Sin pay-to-win'), headline, subhead, three 'why play' tiles, 'Cómo funciona' section with element chips (Fuego, Agua, Viento, Tierra, Metal, Rayo, Neutral), and the 'Hecho entre amigos por gusto' footer.
- Spanish version uses the same playful indie register as the English source — informal tú throughout, TCG vocabulary players already know (sobres, mazos, cartas), no marketing-speak.
- Marketing translations rolling out next: about, how-to-play, FAQ, contact, showcase, changelog. Each will follow the same per-page review gate.
- Locale switcher in the marketing header lets you flip between /es and /en versions of the landing page without losing your place — same flow already in use on the legal pages.
Privacy Policy now available in Spanish — Phase 1 legal coverage complete
The Privacy Policy is now bilingual, closing out Phase 1 of the legal-page localization effort. Spanish-speaking visitors land on a fully translated /es/legal/privacy, English visitors keep the original wording at /en/legal/privacy. With this release, all four legal pages — Terms of Service, Cookie Policy, Legal Notice, and Privacy Policy — ship in both English and Spanish, each carrying the controlling-version notice that names the English text as the legally authoritative version. Phase 1 legal coverage is now fully complete.
Legal pages
- Privacy Policy translated to Spanish — every section, every data table, every right enumeration mirrors the English source 1:1 (data controller, categories of data collected, purposes and legal basis, data sharing with processors, international transfers, retention periods, GDPR data-subject rights including access/rectification/erasure/portability/objection/restriction/withdrawal of consent, GDPR Article 21 objection box, AEPD complaint route, GDPR Article 33/34 breach notification, CCPA/CPRA rights for California users, cookie disclosure, children-under-16 protections, automated-decision-making disclosure).
- Spanish version uses canonical RGPD terminology throughout: 'responsable del tratamiento', 'interesado', 'encargado del tratamiento', 'base legal', 'consentimiento', 'interés legítimo', 'Delegado de Protección de Datos (DPD)', plus the full Spanish names for each GDPR right ('derecho de acceso', 'derecho de supresión', 'derecho de oposición', 'derecho a la portabilidad', 'derecho a la limitación').
- Phase 1 legal coverage now fully complete: Terms, Cookies, Legal Notice, and Privacy all available in both Spanish and English with controlling-version notices in place.
- Locale switcher in the legal header lets you flip between /es/legal/privacy and /en/legal/privacy without losing your place — same flow already in use on the other three legal pages.
Terms of Service now available in Spanish — three legal pages bilingual
The Terms of Service page is now bilingual. Spanish-speaking visitors land on a fully translated /es/legal/terms, English visitors keep the original wording at /en/legal/terms. Both versions carry the same controlling-version notice at the top, making clear that the English text remains the legally authoritative version and the Spanish translation is provided for convenience. Three of four legal pages — Terms, Cookies, and Legal Notice — now ship in both languages; Privacy is the next page in line.
Legal pages
- Terms of Service translated to Spanish — every clause, every list item, every legal definition mirrors the English source 1:1 (eligibility, gacha mechanics, ad disclosures, IP ownership, DSA Article 17 appeal flow, Belgian gacha note, EU consumer right of withdrawal).
- Phase 1 legal-page coverage is now three-quarters complete: Terms, Cookies, and Legal Notice all available in Spanish and English. Privacy Policy translation is the remaining page.
- Controlling-version banner present at the top of every translated legal page in both locales: the English text remains legally binding and translations are explicitly marked as non-authoritative for legal interpretation.
- Locale switcher in the legal header lets you flip between /es and /en versions of any legal page without losing your place.
Cookie Policy and Legal Notice now available in Spanish
The Cookie Policy and Legal Notice pages are now bilingual. Spanish-speaking visitors land on fully translated /es/legal/cookies and /es/legal/notice, English visitors keep the original wording at /en/legal/cookies and /en/legal/notice. All four pages carry a controlling-version notice at the top making clear that the English text is the legally authoritative version and the Spanish translation is provided for convenience. Terms and privacy will follow the same pattern in upcoming releases.
Legal pages
- Cookie Policy translated to Spanish — every section, every cookie table, every browser-settings note is mirrored 1:1 with the English source.
- Legal Notice (Aviso Legal) translated to Spanish — operator identity, NIF/CIF cell, Spain-based address, applicable LSSI-CE/RGPD/LOPDGDD/DSA legislation list, intellectual property, liability and dispute-resolution sections all mirrored 1:1.
- Controlling-version banner added at the top of both legal pages in both locales: the English text remains the legally binding version and translations are explicitly marked as non-authoritative for legal interpretation.
- Locale switcher in the legal header lets you flip between /es/legal/* and /en/legal/* without losing your place.
Tu veux suggérer la prochaine chose ? La page contact a email et Discord. Tu veux suivre les mises à jour à mesure qu'elles sortent ? Mets cette page en favori — nous la mettons à jour à chaque changement visible au joueur.