Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
翻译准备中
正在加载完整 README。 以下为已保存的摘录。
Donjon et Dragons — Codex des personnages Bibliothèque de fiches de personnages D&D 5e en codex médiévaux (parchemin, or, obsidienne). Construit en Nuxt 4 + Tailwind CSS v4 + TypeScript strict.查看完整 README ↗
Bibliothèque de fiches de personnages D&D 5e en codex médiévaux (parchemin, or, obsidienne). Construit en Nuxt 4 + Tailwind CSS v4 + TypeScript strict.
🌐 Production : https://donjon-et-dragons.vercel.app/
| Slug | Personnage | Joueur | Race · Classe |
|---|---|---|---|
dareth-brumeval | Dareth Brumeval | Steve | Demi-elfe · Rôdeur niveau 6 |
skamos-aurum | Skamos Aurum | Normand | Tieffelin · Ensorceleur niveau 4 |
zanna | Zanna | Myriam | Gnome des forêts · Occultiste niveau 3 |
maera-vifbois | Maera Vifbois | Sandra | Humaine · Paladin (Conquête) niveau 4 |
thunon | Thunon | Juan | Haut-elfe · Magicien (Évocation) niveau 4 |
kael-draven | Kael Draven | Jérôme | Humain · Roublard (Voleur) niveau 4 |
@theme inline dans app/assets/css/main.cssany)public/fonts/ (souveraineté CEJEF, aucun CDN externe)@vite-pwa/nuxt, manifeste « Codex D&D », service worker Workbox (mise à jour proposée par un bandeau), utilisable hors ligne, aide à l'installation (bouton sur Android, instructions sur iPhone)idb), rendu 100 % client (ssr: false, page d'accueil pré-rendue). Les 6 fiches de app/data/characters/ sont importées au premier lancementuseCharacterState singleton (HP, inspiration, repos, jets de mort, slots multi-niveaux, sorts daily) via useState Nuxt, enregistré dans IndexedDB à chaque changementuseInventory (équipement, argent, notes), modifiable et enregistré dans IndexedDB/personnages/nouveau, /personnages/<id>/modifier), validation structurelle, confirmation avant de quitter sans enregistrervercel.jsondonjon-et-dragons/
├── app/
│ ├── app.vue # layout racine + skip link
│ ├── assets/css/main.css # @theme Tailwind + @font-face + print A4 paysage
│ ├── composables/
│ │ ├── useCharacterState.ts # état mutable + repos D&D 5e, enregistré dans IndexedDB
│ │ ├── useInventory.ts # sac : équipement, argent, notes, enregistré dans IndexedDB
│ │ ├── persistState.ts # synchronise un état partagé avec IndexedDB (chargement unique, écriture à chaque changement)
│ │ ├── useCharacters.ts # useCharacterList(), useCharacter(id) depuis IndexedDB
│ │ ├── useObjectUrl.ts # URL blob: d'un portrait, révoquée automatiquement
│ │ ├── useMobileTab.ts # onglet actif mobile (Profil, Combat, Sorts, Sac, Stats)
│ │ ├── useT.ts # t() / tCount() : textes de l'interface
│ │ ├── useTransfer.ts # transfert par code : envoi chiffré, réception, erreurs traduites
│ │ └── useIsDesktop.ts # détecte le viewport >= lg
│ ├── db/
│ │ ├── schema.ts # base IndexedDB `codex` : stores characters, states, inventories, meta
│ │ ├── characterRepository.ts # lecture, écriture, suppression atomique des fiches
│ │ ├── migrations.ts # migration des documents selon `schemaVersion`
│ │ ├── seed.ts # import des fiches de départ (et de leur sac) au premier lancement
│ │ └── legacy.ts # reprise des données des versions précédentes (localStorage, format v1)
│ ├── plugins/
│ │ └── db.client.ts # seed + demande de stockage persistant avant le premier rendu
│ ├── i18n/
│ │ └── fr.ts # catalogue des textes de l'interface (clés plates)
│ ├── utils/
│ │ ├── image.ts # portrait importé : réduit à 1024 px, JPEG 0,85, orientation EXIF, 25 Mo max
│ │ ├── characterFile.ts # format d'export « codex-dnd/character » : sérialisation, lecture stricte, nom de fichier
│ │ ├── characterShape.ts # contrôle complet des types d'une fiche importée
│ │ ├── base64.ts # portrait ↔ base64 (export)
│ │ ├── share.ts # partage natif (Web Share) ou téléchargement
│ │ ├── toPlain.ts # copie sans proxy réactif (avant écriture IndexedDB)
│ │ ├── characterFactory.ts # fiche vierge (niveau 1) et fiche prête à enregistrer depuis un brouillon
│ │ ├── validateCharacter.ts # contrôles structurels de l'éditeur (aucune règle D&D)
│ │ ├── vitals.ts # libellés CA / initiative / vitesse lus par l'en-tête
│ │ ├── deepEqual.ts # détection des modifications du brouillon
│ │ ├── newId.ts # UUID v4, aussi en http (réseau local)
│ │ ├── transferCode.ts # code de transfert base32 Crockford, saisie tolérante
│ │ ├── transferCrypto.ts # chiffrement AES-GCM, clé PBKDF2 dérivée du code
│ │ ├── platform.ts # iPhone / app installée (écran d'accueil)
│ │ └── swipe.ts # logique pure du swipe mobile (verrouillage d'axe)
│ ├── components/
│ │ ├── PrintButton.vue
│ │ ├── ConfirmDialog.vue # boîte de confirmation du codex (useConfirm), remplace confirm()
│ │ ├── pwa/ # PwaUpdateToast (nouvelle version), PwaInstallHint (installation)
│ │ ├── transfer/ # TransferDialog : code de transfert, copier, partager le lien
│ │ ├── edit/ # éditeur : EditForm, sections (identité, stats, caractéristiques, compétences, aptitudes, attaques, sorts, rites, langues, personnalité), champs (EditText, EditNumber, EditList repliable…)
│ │ ├── mobile/
│ │ │ ├── MobileMiniHeader.vue
│ │ │ ├── MobileSwipeContainer.vue # swipe horizontal entre onglets
│ │ │ └── MobileTabBar.vue
│ │ └── codex/
│ │ ├── CodexCharacterCard.vue # carte accueil avec brouillard au scroll
│ │ ├── CodexCounter.vue # compteur +/− générique
│ │ ├── CodexHero.vue # portrait + nom + vitals + brouillard animé
│ │ ├── CodexStatusBar.vue # HP tracker + inspiration + repos + jets de mort
│ │ ├── CodexAbilityScores.vue
│ │ ├── CodexSkillList.vue
│ │ ├── CodexFeatureList.vue # capacités + liste d'avantages
│ │ ├── CodexInventory.vue # argent, équipement, notes éditables
│ │ ├── CodexPersonality.vue
│ │ ├── CodexAttacks.vue
│ │ ├── CodexSpells.vue # slots interactifs aria-pressed, consomme useCharacterState
│ │ ├── CodexSpellInfo.vue # détails d'un sort : incantation, portée, durée, concentration
│ │ ├── CodexLanguages.vue
│ │ ├── CodexRituals.vue # rites de combat, accordéon sur mobile
│ │ └── CodexSection.vue
│ ├── data/characters/
│ │ ├── index.ts # fiches de départ : seeds + getSeed(slug)
│ │ ├── dareth-brumeval.ts
│ │ ├── skamos-aurum.ts
│ │ ├── zanna.ts
│ │ ├── maera-vifbois.ts
│ │ ├── thunon.ts
│ │ └── kael-draven.ts
│ └── pages/
│ ├── index.vue # liste des codex
│ ├── recevoir.vue # recevoir une fiche par code ou lien
│ └── personnages/
│ ├── nouveau.vue # création d'une fiche
│ └── [id]/
│ ├── index.vue # fiche dynamique (lue dans IndexedDB)
│ └── modifier.vue # édition d'une fiche
├── shared/types/character.ts # interface Character (abilities, skills, attacks, spellcasting, rituals, personality)
├── prompts/
│ └── character-portrait.md # template prompt pour la génération des portraits (Gemini)
├── public/
│ ├── fonts/ # Cinzel, EB Garamond (.ttf)
│ └── img/ # portraits JPEG (6) + fog textures (fog1/fog2.png) + favicons
├── tests/
│ ├── unit/ # Vitest
│ └── e2e/ # Playwright
├── docs/UX-AUDIT-REPORT.md
├── nuxt.config.ts
├── vitest.config.ts
├── playwright.config.ts
└── vercel.json
pnpm install # installe les dépendances
pnpm dev # serveur dev http://localhost:3000
pnpm build # build Nitro pour Vercel
pnpm generate # SSG
pnpm preview # prévisualise le build
pnpm typecheck # vérification TS stricte
pnpm test # Vitest (unit) — 310 tests (requêtes SQL sur PGlite, stockage IndexedDB, composables d'état et de sac, dataset, composants, swipe)
pnpm test:watch # Vitest en mode watch
pnpm test:e2e # Playwright e2e — 164 tests (chromium desktop + mobile safari)
# dont mobile-standards.spec.ts : normes smartphone (débordement, 44 px, tailles, contraste 7:1) sur tous les écrans
pnpm test:e2e:pwa # Playwright sur build de production : manifeste, hors ligne, /api hors service worker (port 3211)
Le serveur e2e tourne sur le port 3210 pour éviter les collisions avec un dev server existant. Le setup Vitest (tests/setup.ts) stubbe le hook useState de Nuxt et fournit une base IndexedDB neuve à chaque test (fake-indexeddb). tests/helpers/characters.ts construit des fiches stockées à partir des seeds.
Le composable app/composables/useCharacterState.ts expose l'état mutable de chaque personnage (HP courant/temp, inspiration, dés de vie utilisés, jets de mort, emplacements de sort consommés) avec :
states, clé = identifiant de la fiche. Écriture à chaque changement : une écriture différée serait perdue si l'app se ferme juste aprèsuseState Nuxt pour que CodexStatusBar et CodexSpells partagent le même statedamage / heal (HP temp d'abord, reset jets de mort si > 0), castSpell (plus petit emplacement libre de niveau ≥ au sort : lancement à niveau supérieur, magie de pacte), shortRest (slots occultiste), longRest (tout reset, moitié des dés de vie récupérés)computePassivePerception(character) calcule 10 + mod sagesse + bonus maîtrise si Perception est maîtriséeLe composable app/composables/useInventory.ts gère l'équipement, les cinq pièces et les notes :
inventory des fiches de départ (equipment, coins, notes), copié dans le sac au premier lancement. Une fiche créée sur l'appareil démarre avec un sac vide.inventories, clé = identifiant de la fiche. Propre à chaque appareil, non synchronisé.Menu « ⋯ » d'une fiche : Transférer (code à 8 caractères, voir Relais de transfert), Partager (menu natif du téléphone : Mail, Messages, WhatsApp, Fichiers… ; masqué si le navigateur ne sait pas partager de fichiers), Exporter (fichier) (téléchargement), Restaurer l'original (fiches de départ) et Supprimer. Sur l'accueil : Importer une fiche, Recevoir une fiche (page /recevoir) et restauration des fiches de départ supprimées.
Format *.codex.json (export). Partager envoie le même contenu nommé *.codex.txt (text/plain), car Chrome Android refuse de partager un .json ; s'il refuse quand même, la fiche est téléchargée en .codex.json. L'import accepte les deux extensions :
{
"format": "codex-dnd/character",
"schemaVersion": 2, // version du modèle (migrée à l'import si plus ancienne)
"exportedAt": "2026-10-11T12:00:00.000Z",
"character": { /* fiche complète */ "portrait": { "alt": "…", "mime": "image/jpeg", "base64": "…" } },
"state": { /* PV, inspiration, dés de vie, emplacements… (facultatif) */ },
"inventory": { /* sac (facultatif) */ }
}
L'import refuse un fichier qui n'est pas une fiche du codex, d'une version plus récente de l'app, de plus de 15 Mo, ou dont un champ a un type inattendu ; un état de jeu ou un sac abîmés sont ignorés. Si la fiche existe déjà sur l'appareil : Remplacer, Importer en copie (nouvel identifiant, nom suivi de « (copie) ») ou Annuler, avec l'indication de la version la plus récente.
Transfert d'une fiche (photo comprise) avec un code de 8 caractères, valable 24 h. Le téléphone chiffre la fiche avant l'envoi : le serveur ne reçoit que l'empreinte du code et un contenu illisible.
Parcours : sur sa fiche, « ⋯ » → Transférer affiche le code (K7P4-QX9M), avec Copier le code et Partager le lien (/recevoir#K7P4QX9M : le code reste après le #, jamais envoyé au serveur). Le destinataire touche Recevoir une fiche sur l'accueil, saisit le code ou ouvre le lien, puis Recevoir : la fiche passe par la même lecture stricte que l'import de fichier, avec le même choix en cas de doublon (Remplacer / Importer en copie / Annuler). Rien n'est importé sans appui sur Recevoir.
iPhone : un lien s'ouvre dans Safari, dont les fiches sont séparées de celles de l'app installée. Dans Safari, la page /recevoir rappelle d'ouvrir l'app installée et d'y saisir le code (bouton Copier le code), et propose Recevoir dans Safari sinon.
| Élément | Rôle |
|---|---|
app/utils/transferCode.ts | code base32 de Crockford (K7P4-QX9M), saisie tolérante |
app/utils/transferCrypto.ts | identifiant = SHA-256 du code ; AES-GCM 256, clé PBKDF2-SHA256 (600 000 itérations) |
app/composables/useTransfer.ts | sendCharacter (chiffrement + dépôt, nouveau code si 409), receiveCharacter (lecture + déchiffrement + parseCharacterFile), erreurs traduites |
app/components/transfer/TransferDialog.vue | boîte « Transférer » : code, échéance, copier, partager le lien, réessayer |
app/pages/recevoir.vue | saisie du code (prérempli depuis le lien), rappel iPhone dans Safari |
server/api/transfers/index.post.ts | dépôt { id, data } → { expiresAt } (400, 409, 413, 429, 503) |
server/api/transfers/[id].get.ts | lecture { data, expiresAt } (404 si inconnu ou expiré) |
server/api/cron/transfers-cleanup.get.ts | nettoyage quotidien (Cron Vercel, 3 h UTC) |
server/utils/transferStore.ts | stockage Neon Postgres (DATABASE_URL) ou mémoire (local, e2e) |
Limites : 2 Mo, 20 dépôts et 60 lectures par heure et par adresse IP (stockée hachée). Fonctions Vercel en région fra1, base Neon à Francfort.
Mise en service : Vercel → Storage → Create → Neon (région Frankfurt), connecter le projet en Production et Preview (injecte DATABASE_URL). Le schéma est créé et mis à jour au premier appel (migrations de server/utils/database.ts). Optionnel : CRON_SECRET pour réserver le nettoyage à la tâche Cron.
Connexion par identifiant et mot de passe (issue #53). Le master crée les comptes des joueurs (aucune inscription publique) et réinitialise les mots de passe oubliés (aucune adresse email enregistrée). La synchronisation des fiches arrivera dans une prochaine version : pour l'instant, se connecter ne change rien aux fiches de l'appareil.
Parcours :
/installation crée le compte master. En ligne, le code d'installation est demandé (variable SETUP_CODE à définir dans Vercel) ; la page ne fonctionne plus dès qu'un compte existe./comptes, master) : créer un compte joueur affiche un mot de passe provisoire (K7P4-QX9M-3HDE), une seule fois. Sur chaque compte : Renommer (le sien compris), Passer master / Passer joueur (le groupe garde toujours au moins un master), Réinitialiser (nouveau mot de passe provisoire, déconnexion de tous ses appareils) et Supprimer. Tous les masters ont les mêmes droits ; un master ne peut ni se supprimer ni réinitialiser son propre mot de passe./connexion, lien en bas de l'accueil). Avec un mot de passe provisoire, l'app impose d'abord d'en choisir un nouveau (/mot-de-passe).Sécurité :
node:crypto, sel aléatoire), 8 caractères minimum ;httpOnly, Secure en ligne, SameSite=Lax, valable 90 jours et prolongé à l'usage ; le serveur n'en garde que l'empreinte ;SETUP_CODE, l'installation n'est possible qu'en développement local (pnpm dev) ;server/middleware/csrf.ts).| Élément | Rôle |
|---|---|
server/utils/database.ts | client Neon partagé, migrations numérotées (schema_migrations), compteurs de limitation |
server/utils/accountStore.ts | comptes et sessions, Neon ou mémoire (local, e2e) |
server/utils/accountRules.ts, password.ts, session.ts | règles (identifiant, mot de passe, durées), scrypt, cookie de session et contrôles d'accès |
server/api/setup.*, server/api/auth/*, server/api/users/* | installation, connexion, déconnexion, compte courant, mot de passe, gestion des comptes (création, renommage, rôle, réinitialisation, suppression) |
app/composables/useAuth.ts | compte connecté (useState), appels à l'API, erreurs traduites |
app/pages/connexion.vue, mot-de-passe.vue, installation.vue, comptes.vue | pages des comptes ; app/middleware/auth.global.ts protège /comptes et impose le changement d'un mot de passe provisoire |
Les fiches sont stockées sur le serveur, une ligne par fiche. Une fiche a trois parties datées : la fiche elle-même, l'état de jeu (PV, emplacements…) et le sac. La dernière modification gagne, partie par partie : des PV joués à la table n'écrasent pas une fiche modifiée sur l'ordinateur. Le portrait est stocké à part ; un appareil ne le télécharge que si son empreinte a changé. L'app ne s'en sert pas encore : ce sera la PR 3 (#53).
| Route | Rôle |
|---|---|
GET /api/sync?since=N | fiches modifiées depuis le curseur N (0 : toutes), par pages de 20, avec propriétaire et dates |
POST /api/sync | modifications { id, part: sheet | state | inventory | delete, data, changedAt }, au plus 50 ; résultat par modification : applied, stale (plus ancienne que le serveur), forbidden, invalid, notFound |
GET /api/portraits/:id | portrait (ETag = empreinte ; nosniff et CSP sandbox) |
PATCH /api/characters/:id/owner | attribuer une fiche à un compte (master) ; null : fiche du groupe |
GET /api/members | membres du groupe (identifiant, rôle), pour afficher le propriétaire d'une fiche |
Règles :
parseCharacterFile) ;Fichiers : server/utils/syncRules.ts (règles pures), server/utils/syncStore.ts (Neon ou mémoire), shared/types/sync.ts.
Déploiement automatique sur Vercel via Git (branche main).
Manuel :
pnpm dlx vercel deploy --prod
Le projet est lié via .vercel/project.json (org steves-projects-7a849401, projet donjon-et-dragons). Nitro preset vercel est configuré dans nuxt.config.ts.
Fiche de départ livrée avec l'app (importée au premier lancement sur chaque appareil) :
app/data/characters/<slug>.ts exportant un CharacterSeed typé depuis ~~/shared/types/character (ne pas oublier le champ player)app/data/characters/index.ts et l'ajouter au tableau seedsprompts/character-portrait.md, le convertir en JPEG (sips -s format jpeg -s formatOptions 85 <slug>.png --out public/img/<slug>.jpg)/personnages/<slug>pnpm test && pnpm test:e2e⚠️ Un appareil déjà initialisé n'importe pas les nouvelles fiches de départ (l'import ne tourne qu'une fois) ni les modifications des fichiers existants.
lang="fr", skip link, focus-visible custom, prefers-reduced-motionaria-labelledby), caption sr-only sur la table des armes, scope="col"role="group" + aria-pressed) avec compteur live<abbr title="Maîtrise">M</abbr> distinct du bullet décoratif ◆parchment-mute à #b09d72 (AAA)@page A4 landscape avec marges 5-6 mm. Page 1 : fiche complète en 3 colonnes (caractéristiques / compétences+capacités+personnalité / attaques+sorts+langues). Page 2 : rites de combat en 3 colonnes. Footer masqué, image grayscale, bouton Imprimer fixé en bas-droite (.no-print).
feat/, fix/, docs/, chore/feat/ → minor, fix/ → patch, chore/ → patch, docs/ → pas de releaseapp/i18n/fr.ts et l'afficher avec t('cle', { param }) ; pluriels via les paires cle.one / cle.other et tCount('cle', n)docs/UX-AUDIT-REPORT.md — Audit UX/a11y NN/g + WCAG 2.2 sur la version HTML initiale