# GOÛTERMAGEDDON Jeu de baston 2D en temps réel, **exclusivement LAN**, de 3 à 8 joueurs en chacun pour soi. Le décor (un goûter géant) est **entièrement destructible**. Le but : tenir la **cerise** le plus longtemps possible pendant que la **crème anglaise** monte. Ce fichier sert de référence pour le game design et l'architecture. Lis-le avant toute modification. Si une décision le contredit, mets-le à jour dans le même commit. --- ## 1. Principes non négociables 1. **Parties courtes.** Une manche dure 3 minutes, une partie 4 manches (environ 12 minutes). La relance se fait en un clic. 2. **On attaque le sol, pas les joueurs.** La destruction du décor est la mécanique centrale. Une feature qui ne passe pas par le terrain doit le justifier. 3. **LAN uniquement.** Pas de compte, pas de matchmaking en ligne, pas de backend distant. On découvre les parties par broadcast UDP. 4. **Lisibilité et humour.** DA enfantine et drôle. Chaque action a un retour visuel et sonore exagéré. 5. **Tout est piloté par les données.** Cartes, matériaux, personnages et objets sont décrits dans des fichiers de données. Ajouter une carte ne demande aucun code. --- ## 2. Stack technique | Rôle | Choix | |---|---| | Langage | **TypeScript** (strict, ESM) | | Runtime / desktop | **Node.js + Electron** | | Build / dev | **electron-vite** (HMR sur le renderer) | | Rendu 2D | **PixiJS v8** | | Physique | **planck.js** (port de Box2D, déterministe et sans dépendance) | | Réseau | **ws** (WebSocket sur TCP local) + **dgram** (UDP broadcast pour la découverte) | | Sérialisation | **msgpackr** (binaire, dès le départ) | | Tests | **vitest** (logique de `shared/` et `server/`) | | Packaging | **electron-builder** (cible Windows en priorité) | | UI des menus | DOM + CSS simple, **pas de framework** | Pas de nouvelle dépendance sans justification dans le message de commit. ### Commandes ```bash npm install npm run dev # Electron en dev (HMR renderer) npm run server # serveur headless en Node pur (tests, bots, debug) # ex. : npm run server -- --name="Test" --round=30 --rounds=2 --pause=5 npm test # vitest npm run bench # benchmarks vitest (budget terrain §5) npm run gen:test-map # régénère les PNG placeholder de gateau-etages npm run lint # eslint + tsc --noEmit npm run build # build de prod npm run dist # installeur Windows (electron-builder) ``` --- ## 3. Architecture ``` goutermageddon/ ├── CLAUDE.md ├── package.json ├── electron.vite.config.ts ├── src/ │ ├── main/ # Process principal Electron : fenêtres, découverte LAN, lancement du serveur │ ├── preload/ # Pont sécurisé (contextIsolation) : API minimale exposée au renderer │ ├── renderer/ # Client : rendu PixiJS, inputs, UI, interpolation, sons │ ├── server/ # Simulation autoritaire : Node pur, AUCUNE dépendance Electron/DOM │ └── shared/ # Code commun : terrain, matériaux, protocole, constantes, RNG, types ├── assets/ │ ├── maps/ # Une carte par dossier (voir §6) │ ├── characters/ # Sprites et données des personnages │ ├── materials/ # Textures des matériaux │ ├── items/ │ └── audio/ └── tests/ ``` ### Règles de dépendances (strictes) - `shared/` ne dépend de rien d'autre que de lui-même : pas de Pixi, pas de DOM, pas d'Electron, pas de `fs`. - `server/` dépend de `shared/` et de Node. - `renderer/` dépend de `shared/` et de Pixi. Il ne contient **aucune logique de jeu autoritaire**. - `main/` orchestre : il crée les fenêtres, lance le serveur dans un `utilityProcess` quand le joueur héberge, et gère la découverte UDP. ### Modèle réseau - **L'hôte fait autorité.** Le joueur qui héberge lance `server/` dans un `utilityProcess`. Tous les clients, y compris le renderer de l'hôte, se connectent en WebSocket à `IP_hôte:47100`. - **Découverte :** le serveur émet un broadcast UDP sur le port `47101` toutes les secondes (nom de la partie, carte, nombre de joueurs, version). Les clients écoutent et listent les parties. - **Simulation :** pas fixe à **60 Hz** côté serveur. Snapshots envoyés à **30 Hz**. - **Clients :** ils envoient leurs **inputs** à chaque tick (bitmask des touches + direction de visée). Pas de prédiction au départ, car la latence LAN est inférieure à 5 ms. Les autres entités sont interpolées avec environ 50 ms de retard. - **Le terrain n'est JAMAIS envoyé en entier**, sauf au chargement de la carte (par son id et sa seed, chaque client charge les fichiers localement). Ensuite, on n'envoie que des **événements de destruction** (§5). - Tous les messages sont typés dans `shared/protocol.ts` et sérialisés avec msgpackr. Le protocole est versionné : un client d'une autre version est refusé avec un message clair. ### Aléatoire Toute la logique aléatoire (objets, Magicien Raté, mutateurs) passe par un **RNG à seed** (`shared/rng.ts`, mulberry32 ou équivalent) côté serveur. `Math.random()` est interdit dans `shared/` et `server/`. --- ## 4. Game design ### Déroulement d'une manche - La cerise apparaît au sommet de la carte (position définie par la carte). - Tant qu'un joueur la tient, il marque **1 point par seconde**. S'il est touché, il la lâche et elle roule (objet physique). - **La crème anglaise monte** en continu. La vitesse est définie par la carte et accélère dans les 30 dernières secondes. - Un joueur qui tombe dans la crème réapparaît 5 secondes plus tard sur une **cuillère-catapulte** qui le renvoie sur le terrain. - Des **objets tombent du ciel** à intervalles aléatoires : - **Bougie** : explosive, à lancer. - **Smarties** : boost de vitesse pendant 5 secondes. - **Fouet électrique** : secoue tout le terrain, ce qui fait trembler la gelée et détache les blocs fragilisés. - Il n'y a pas de barre de vie. Un coup fait **reculer** et peut **assommer** brièvement. On meurt uniquement en tombant dans la crème ou sous un bloc qui s'écrase. ### Partie - 4 manches. Le score total décide du vainqueur. - **Rattrapage :** avant chaque manche, le dernier du classement **choisit la carte** (parmi la rotation, §6) et **impose un personnage** au premier. - Écran de fin avec trophées : Le Glouton, Le Kamikaze, Le Promoteur Immobilier, Pas Sa Faute, etc. Ils sont calculés à partir des stats de la partie (`server/stats.ts`). ### Matériaux Ils sont définis dans `assets/materials/materials.json`. L'id `0` correspond au vide. | Id | Matériau | Résistance | Particularité | |---|---|---|---| | 1 | Génoise | faible | Matériau de base | | 2 | Chocolat | forte | Résiste aux explosions | | 3 | Chantilly | très faible | Rebondissante, amortit les chutes | | 4 | Gelée | moyenne | Glissante, transmet les secousses | | 5 | Biscuit | moyenne | Cassant : se détache en gros blocs | | 6 | Caramel | forte | Collant : ralentit les déplacements | | 7 | Laine | faible | Créée par Mamie Tricot, inflammable | | 8 | Génoise détrempée | très faible | Créée par Bébé Pompier, s'affaisse lentement | | 255 | Assiette | indestructible | **Point d'ancrage** (§5) | Chaque matériau a les champs suivants : `hardness`, `friction`, `restitution`, `density`, `flammable`, `color`, `texture`, `particles` (type de miettes), `sfx`. ### Personnages Ils sont définis dans `src/shared/characters/` : une classe par personnage qui étend `Character`, avec les stats dans `assets/characters//character.json`. | Personnage | Attaque | Spécial | Défaut | |---|---|---|---| | **Castor Glouton** | Coup de dents | Mange le décor en avançant (creuse) | Grossit en mangeant, jusqu'à passer à travers le sol | | **Mamie Tricot** | Aiguilles | Lance des pelotes qui créent ponts et filets (matériau Laine) | Lente, et ses constructions brûlent | | **Pyromanchot** | Pétards | Glissade sur le ventre avec explosion à l'arrivée | 1 pétard sur 3 lui explose dessus | | **Gégé le Ver** | Coup de tête | Nage dans le terrain et surgit sous les pieds | Lent et vulnérable à découvert | | **Bébé Pompier** | Jet d'eau (repousse) | Détrempe le terrain (Génoise → Génoise détrempée) | Le recul le propulse en arrière | | **Captain Prout** | Coup de cape | Jetpack par petites poussées, laisse un nuage toxique (vision floue + toux) | Chaque poussée s'entend | | **Le Chat** | Coup de patte | Pousse blocs, objets et joueurs assommés du bord | Se lèche la patte au hasard (1 s) | | **Magicien Raté** | Baguette | Sort aléatoire : terrain en gelée, échange de position, lapin géant, « pfft » | Tirage pondéré : plus il est en tête, plus ses sorts ratent | API commune : `onAttack()`, `onSpecial()`, `onTick(dt)`, `onHit()`, et les stats (`speed`, `jump`, `mass`, `size`, `cooldowns`). --- ## 5. Terrain destructible (cœur technique) ### Représentation - Le terrain est un **bitmap de matériaux** : `Uint8Array` de taille `width × height` (une carte typique fait 2048 × 1024 px). - Il est découpé en **morceaux de 64 × 64 px**, chacun avec un drapeau `dirty`. - **Côté serveur :** chaque morceau modifié reconstruit ses **colliders planck** (marching squares, puis simplification Douglas-Peucker, puis chain shapes statiques). - **Côté client :** chaque morceau modifié reconstruit sa **texture Pixi**. La texture du matériau est échantillonnée en coordonnées monde, et une passe de contour donne l'effet feutre. ### Opérations Toutes les modifications passent par des **opérations déterministes en arithmétique entière** définies dans `shared/terrain/ops.ts` : ```ts type TerrainOp = | { kind: 'carve'; x: number; y: number; r: number; power: number } // explosion, morsure | { kind: 'fill'; x: number; y: number; r: number; material: number } // laine de Mamie | { kind: 'convert'; x: number; y: number; r: number; from: number; to: number } // détrempage ``` - `power` est comparé à la `hardness` de chaque matériau. Le chocolat résiste à un petit pétard. - Le serveur applique l'op, puis la diffuse **telle quelle** avec un numéro de séquence. Chaque client applique les ops dans l'ordre et obtient un bitmap **identique au bit près**. - Ne jamais introduire de flottant dans l'application d'une op. ### Blocs qui se détachent - **Règle :** tout pixel solide qui n'est plus relié (4-connexité) à un pixel **Assiette** (ancre) tombe. - Après chaque op, le serveur lance un flood-fill à partir des pixels voisins de la zone modifiée pour trouver les composantes non ancrées. - Chaque composante détachée est retirée du bitmap et devient un **corps dynamique planck** (`FallingChunk`) avec son masque de pixels. Le serveur diffuse un événement `detach { id, x, y, w, h, mask }` (masque compressé en RLE). Les composantes minuscules (`MIN_CHUNK_PIXELS`) s'émiettent (`debris`) sans corps physique. - Les clients retirent les mêmes pixels et affichent le bloc comme un sprite. La position et la rotation du bloc arrivent dans les snapshots. - Quand le bloc s'immobilise (vitesse quasi nulle pendant 0,5 s), le serveur le **rastérise** dans le bitmap et diffuse `merge { id, x, y, w, h, patch }`. Le patch est le résultat pixel par pixel, pour qu'aucun client n'ait à recalculer la rotation. Il comble jusqu'à `CHUNK_GLUE_PX` pixels sous le bloc pour le raccrocher au sol. Si le résultat ne serait pas ancré (bloc posé sur un joueur ou sur un autre bloc), la réintégration est retentée plus tard. - Les ops, `detach` et `merge` forment **un seul flux d'événements séquencés** (`shared/terrain/events.ts`) : c'est ce flux, rejoué dans l'ordre, qui garantit un bitmap identique au bit près sur tous les clients (vérifié par les tests). - Côté physique, un bloc est approché par des boîtes alignées sur une grille (Box2D ne fait pas collisionner deux chaînes entre elles). - Un bloc qui tombe et percute un joueur avec assez d'énergie l'écrase (élimination). Un bloc qui tombe dans la crème disparaît. ### Budget de performance - Une op avec flood-fill et régénération des colliders doit tenir en **2 ms** côté serveur. Si ce n'est pas le cas, on optimise : flood-fill borné, bitsets, cache des composantes. - 60 FPS sur un PC milieu de gamme avec 8 joueurs et 20 blocs en chute. - Écrire un **benchmark vitest** pour les ops et le flood-fill dès la première étape. --- ## 6. Cartes Une carte = un dossier dans `assets/maps//`. Ajouter une carte ne demande **aucun code**. ``` assets/maps/gateau-etages/ ├── map.json # métadonnées et règles ├── terrain.png # masque des matériaux (une couleur exacte = un matériau) ├── background.png # décor de fond (non destructible, parallaxe possible) └── thumb.png # vignette pour le menu ``` ### `map.json` ```jsonc { "id": "gateau-etages", "name": "Le Gâteau à Étages", "size": [2048, 1024], "materialPalette": { // couleur du PNG -> id de matériau "#F2C46D": 1, "#5A3A22": 2, "#FFFFFF": 3, "#D93A5B": 4, "#C8964B": 5, "#000000": 0, "#8899AA": 255 }, "spawns": [[300, 200], [1700, 200], [600, 400], [1400, 400]], "cherrySpawn": [1024, 80], "custard": { "startY": 1000, "speed": 2.5, "finalRushSpeed": 8 }, "items": { "interval": [6, 12], "weights": { "candle": 3, "smarties": 2, "whisk": 1 } }, "music": "birthday-flute", "modifiers": [] // règles spéciales de la carte (voir ci-dessous) } ``` ### Validation `shared/maps/loader.ts` valide chaque carte au chargement : - chaque couleur du PNG appartient à la palette ; - au moins une zone d'Assiette existe ; - les points d'apparition sont dans le vide et au-dessus d'un sol. Une carte invalide est **rejetée avec un message explicite**. On ne la charge jamais à moitié. ### Cartes prévues | Id | Nom | Particularité | |---|---|---| | `gateau-etages` | Le Gâteau à Étages | Carte de référence : étages de génoise et chocolat, cerise au sommet | | `mille-feuille` | Le Mille-Feuille | Couches horizontales alternées dures et molles : on creuse vers le bas facilement, sur le côté difficilement | | `roule` | Le Roulé | Spirale de génoise et de confiture (gelée) : on tourne en rond, ça s'effondre en anneaux | | `piece-montee` | La Pièce Montée | Pyramide de choux reliés par du caramel collant. Retirer un chou fait tomber tous ceux du dessus | | `tarte-fruits` | La Tarte aux Fruits | Fond en biscuit cassant et fruits en gelée. Modificateur : le fond se fissure au hasard | | `crepes` | La Pile de Crêpes | Couches très fines superposées, et une pile qui penche. Modificateur : vent latéral | - `modifiers` est une liste d'ids de règles spéciales implémentées dans `server/modifiers/` (par exemple `random-cracks`, `wind`). Une carte peut en combiner plusieurs. - **Rotation :** la liste des cartes jouables est configurée par l'hôte dans le lobby (toutes, sélection, ou aléatoire). Le dernier du classement choisit parmi cette rotation. - **Plus tard :** un éditeur de cartes simple dans l'app (peinture des matériaux au pinceau, placement des spawns), qui exporte au format ci-dessus. --- ## 7. Direction artistique et son - **Style dessin d'enfant amélioré :** gros contours au feutre (noir, 3 à 4 px), couleurs saturées, textures de crayon de couleur sur les personnages. - **Un terrain appétissant :** chaque matériau a sa texture en motif répété et ses propres particules (miettes, copeaux, éclaboussures). - **Personnages :** ronds, grosse tête, pattes courtes, yeux très expressifs. Squash and stretch sur chaque saut, impact et atterrissage. - **Effets :** secousse d'écran proportionnelle à l'énergie, freeze-frame de 50 ms sur les gros impacts, nombreuses particules. - **Son :** mastication, « splotch », « crrrac » de biscuit, prouts variés. Musique : chanson d'anniversaire à la flûte à bec qui accélère avec la montée de la crème. - Placeholders autorisés pour le développement : formes géométriques colorées. La logique ne dépend jamais des assets finaux. --- ## 8. Conventions de code - Identifiants, fichiers et commits **en anglais**. Textes affichés **en français**, centralisés dans `renderer/i18n/fr.ts`. - TypeScript `strict: true`, pas de `any` sans commentaire justificatif. - Fonctions pures dans `shared/` dès que possible : c'est ce qui se teste. - Toute logique de `shared/terrain/` et `shared/maps/` est **couverte par des tests vitest**. - Pas de valeurs magiques : elles vont dans `shared/constants.ts` ou dans les données (JSON). - Commits petits et ciblés. Format : `feat(terrain): ...`, `fix(net): ...`, `chore: ...`. - Electron : `contextIsolation: true`, `nodeIntegration: false`. Le preload expose une API minimale et typée (`window.lan.discover()`, `window.lan.host()`, etc.). --- ## 9. Feuille de route Chaque étape doit être **jouable** avant de passer à la suivante. ### M1 : Terrain destructible en solo - Structure du projet (electron-vite, TS, Pixi, planck, vitest). - Bitmap des matériaux, morceaux de 64 px, rendu Pixi, colliders par morceau. - Chargement de la carte `gateau-etages` (avec un PNG de test). - Un personnage générique qui court, saute et creuse au clic (`carve`). - Tests : ops déterministes, marching squares, benchmark. - **Terminé quand :** on peut creuser le gâteau à 60 FPS et c'est agréable. - **Fait en avance (choix de l'équipe) :** le serveur tourne déjà dans un `utilityProcess` avec WebSocket, msgpackr et interpolation (le renderer ne doit pas porter la simulation, §3). Le **Castor Glouton** (M5) est jouable, avec un écran titre minimal (pseudo et choix du personnage), une première passe de DA procédurale (textures de matériaux, contour feutre, intérieur des trous, persos animés, onomatopées) et un décor de cuisine. ### M2 : Blocs qui tombent - Flood-fill vers l'ancre Assiette, `FallingChunk`, réintégration du bloc immobile, écrasement. - **Terminé quand :** creuser sous un étage le fait tomber et l'étage se pose proprement. - **État :** implémenté et testé (détachement, chute, réintégration, écrasement, cohérence serveur/clients). Reste au-dessus du budget §5 : le décrochage d'un étage entier (~90 000 px) prend ~9 ms, car le flood-fill doit parcourir tout le bloc. C'est rare, mais à optimiser (balayage par lignes, bitsets). ### M3 : Multijoueur LAN - Serveur dans un `utilityProcess`, mode headless, WebSocket, protocole msgpackr. - Découverte UDP, lobby (liste des parties, choix du pseudo, prêt). - Inputs vers le serveur, snapshots et ops vers les clients, interpolation. - Test de cohérence : hash du bitmap comparé entre serveur et clients toutes les 5 secondes, avec un log d'erreur en cas d'écart. - **Terminé quand :** 4 PC en LAN creusent le même gâteau sans aucune différence. - **État :** implémenté. Le serveur s'annonce sur chaque sous-réseau et en local. L'écran « Les goûters du coin » liste les parties, permet d'en créer une ou d'en rejoindre une par adresse. Pendant l'échauffement, chacun se déclare prêt ; quand tout le monde l'est, un compte à rebours de 3 s lance la partie sur une carte neuve. Le hash est vérifié toutes les 5 s et un écart est loggé côté serveur. Testé avec plusieurs clients en tests automatiques et avec 2 instances sur le même PC. **Reste à valider sur 4 vrais PC** (pare-feu Windows à autoriser au premier lancement). ### M4 : Règles de jeu - Cerise, score, crème qui monte, cuillère de réapparition, objets, manches, partie, écran de score. - **Terminé quand :** une partie complète se joue du lobby aux trophées. - **État :** implémenté. - **Échauffement :** la carte sert de salle d'attente. Pas de chrono, de cerise ni d'objets, et la crème ne monte pas, mais on peut y tomber. - **Mort :** tomber dans la crème ou être écrasé par un bloc. On revient 5 s plus tard par la cuillère, lancée depuis une colonne de vide vers le point d'apparition le plus proche. - **Objets :** Smarties et fouet agissent au ramassage. La bougie se garde et se lance avec **F** ; son explosion touche tout le monde, le lanceur compris. - **Trophées** (`server/stats.ts`) : Le Glouton, Le Kamikaze, Le Promoteur Immobilier, Pas Sa Faute, L'Artificier, Le Cueilleur. - **Reste en M7 :** le rattrapage (le dernier choisit la carte et impose un personnage). ### M5 : Trois personnages - Castor Glouton (déjà jouable depuis M1), Mamie Tricot, Pyromanchot (le trio creuser / construire / exploser). - Écran de sélection des personnages (une version minimale existe dans l'écran titre). ### M6 : Plusieurs cartes - Loader validé, rotation configurable dans le lobby, choix par le dernier, `modifiers`. - Cartes `mille-feuille` et `piece-montee` en plus de `gateau-etages`. ### M7 et suite - Les 5 autres personnages, les 3 autres cartes, le rattrapage complet, les trophées, la DA finale, l'audio, l'éditeur de cartes, l'installeur Windows. --- ## 10. Pour Claude : comment travailler sur ce projet - Commence toujours par identifier l'étape en cours (§9) et ne travaille pas sur une étape future sans qu'on te le demande. - Avant de toucher au terrain ou au réseau, relis le §5 et le §3 : le **déterminisme des ops** et l'**autorité du serveur** sont les deux invariants qui ne doivent jamais casser. - Après une modification de `shared/` ou `server/`, lance `npm test` et `npm run lint`. - Si une demande contredit ce document, signale-le et propose de mettre ce fichier à jour plutôt que de diverger silencieusement. - En cas de doute sur une question de game feel (vitesse, gravité, puissance), mets la valeur dans les constantes ou les données et indique-la pour qu'on la règle en jouant.