BeamNG.Earth
BeamNG.drive용 1:1 비율 실시간 지구, 실제 고도·위성 이미지·부동 원점·보조 서버에서 LOD 스트리밍
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