|
|
vor 4 Tagen | |
|---|---|---|
| assets | vor 4 Tagen | |
| scripts | vor 5 Tagen | |
| src | vor 4 Tagen | |
| tests | vor 4 Tagen | |
| .gitignore | vor 1 Woche | |
| CLAUDE.md | vor 1 Woche | |
| electron-builder.yml | vor 6 Tagen | |
| electron.vite.config.ts | vor 5 Tagen | |
| eslint.config.mjs | vor 1 Woche | |
| package-lock.json | vor 1 Woche | |
| package.json | vor 5 Tagen | |
| readme.md | vor 4 Tagen | |
| tsconfig.base.json | vor 1 Woche | |
| tsconfig.json | vor 1 Woche | |
| tsconfig.node.json | vor 1 Woche | |
| tsconfig.web.json | vor 1 Woche | |
| vitest.config.ts | vor 1 Woche |
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.
| 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.
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 bench:node # même budget mesuré sous Node, sans le biais du lanceur de tests
npm run gen:maps # régénère les PNG placeholder des cartes intégrées
npm run lint # eslint + tsc --noEmit
npm run build # build de prod
npm run dist # versions Windows autonomes dans dist/ (electron-builder)
npm run release -- 0.2.0 # publie la version 0.2.0 : tag, build, mise à jour automatique (voir plus bas)
npm run dist produit trois fichiers dans dist/. Aucun d'eux n'a besoin de Node.js sur le PC qui joue :
Goutermageddon-<version>-portable.exe : un seul fichier à envoyer, on double-clique et on joue. Il se décompresse à chaque lancement, donc démarre en quelques secondes.Goutermageddon-<version>-x64.zip : le jeu décompressé. On extrait le dossier et on lance Goûtermageddon.exe. Le démarrage est plus rapide, et l'autorisation du pare-feu reste valable d'une fois sur l'autre.Goutermageddon-<version>-setup.exe : un installeur classique, avec un raccourci dans le menu Démarrer.Le jeu installé se met à jour tout seul à partir du dépôt Git (UPDATE_REPO_URL), sans serveur en plus :
npm run release -- <x.y.z> vérifie que main est propre, lance les tests et le lint, change la version, construit les exe, puis crée le tag v<x.y.z> et un commit sans parent à la référence refs/builds/v<x.y.z>. Ce commit contient les fichiers du jeu (app.asar, ses modules natifs, assets/, environ 16 Mo) et un manifest.json (taille et sha256 de chaque fichier). Le script pousse ensuite main, le tag et le build (--no-push pour ne rien pousser). Un clone ne récupère jamais refs/builds/*, donc l'historique du code reste léger.info/refs, public) et cherche un build plus récent que lui. Il télécharge les fichiers modifiés par l'URL brute du serveur Git, vérifie chaque sha256, et range la version dans %APPDATA%\Goûtermageddon\game\<version>\. Un bandeau en bas à gauche affiche la progression, puis un bouton « Relancer ».main/boot.ts) lance la version téléchargée la plus récente, sinon celle de l'installeur. Si la version téléchargée plante au démarrage, il revient à celle de l'installeur.Goutermageddon-<version>-setup.exe (ou le portable) ; ensuite tout passe par les releases.GOUTER_NO_UPDATE=1 désactive la mise à jour, GOUTER_UPDATE_REPO=<url> la fait pointer sur un autre dépôt (tests).Les exe ne sont pas signés. Windows SmartScreen affiche donc « Windows a protégé votre ordinateur » : il faut cliquer sur « Informations complémentaires », puis « Exécuter quand même ». Au premier hébergement, le pare-feu demande l'accès : il faut cocher Réseaux privés, sinon les autres PC ne voient pas la partie.
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/
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.
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.
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/.
CHERRY_HOLDER_JUMP_MULT), et Captain Prout n'a qu'une poussée.CHERRY_FLEE_*).CHERRY_HOT_S secondes par le même joueur, elle chauffe, tremble, fait tic-tac, puis lui explose dans les mains.CHERRY_STEAL_WINDOW_S secondes rapporte CHERRY_STEAL_BONUS points.server/surprises/) : deux fois par manche, à un moment tiré au hasard, un événement est annoncé en grand :
Les poids et les durées sont dans SURPRISE_WEIGHTS et SURPRISE_DURATIONS.
WALL_*). Aucun personnage ne reste coincé sous un étage.MIN_CAKE_PIXELS).Pas d'assommage en boucle : un coup reçu pendant qu'on est assommé ne prolonge pas l'assommage. Après un assommage, on est protégé pendant STUN_GUARD_S (les coups font alors moins reculer).
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).
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.
Ils sont définis dans src/shared/characters/ : une classe par personnage qui étend Character, avec les stats dans assets/characters/<id>/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 | De temps en temps (backfireChance), un pétard 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, qui cède sous les pieds) et trempe les joueurs arrosés (plus lents) | Le recul le propulse en arrière |
| Captain Prout | Coup de cape | Jetpack par petites poussées (3 avant de reposer le pied), laisse un nuage toxique (vision floue + toux) | Chaque poussée s'entend |
| Le Chat | Coup de patte qui griffe | Bond griffes en avant : creuse, éjecte qui il percute, puis pousse blocs, objets et joueurs | Se lèche la patte au hasard |
| 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 |
| Tatie Douille | Rouleau à pâtisserie | Pose une tourelle sur le gâteau (socle en biscuit) qui tire des dragées sur le joueur le plus proche, jamais sur elle (2 au plus) | Reste plantée le temps de la poser, et la tourelle tombe avec le gâteau dessous |
| Léon le Caméléon | Grande langue : creuse un sillon et ramène ce qu'elle attrape (joueurs, objets, blocs) | Invisible pour les autres pendant 5 s (il se voit en fantôme) | Attaquer ou être touché le révèle, et la cerise qu'il tient reste visible |
API commune : onAttack(), onSpecial(), onTick(dt), onHit(), et les stats (speed, jump, mass, size, cooldowns).
Uint8Array de taille width × height (une carte typique fait 2048 × 1024 px).dirty.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.
Toutes les modifications passent par des opérations déterministes en arithmétique entière définies dans shared/terrain/ops.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.Ne jamais introduire de flottant dans l'application d'une op.
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. La vitesse prise en compte est celle d'avant le choc. Un gros bloc élimine le joueur, mais un bloc de moins de CRUSH_KILL_PIXELS le transforme seulement en crêpe : il reste aplati et immobile pendant FLAT_S. Les blocs mous (laine, chantilly : densité < CRUSH_MIN_DENSITY) n'écrasent pas. Un bloc qui tombe dans la crème disparaît.
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.
Mesurer sous Node (npm run bench:node). Les benchmarks vitest sont pessimistes sur les boucles chaudes : le lanceur de tests transforme chaque lecture d'une constante importée en accès de propriété. Dans les modules de terrain, les boucles chaudes lisent donc des copies locales des constantes (const EMPTY = MATERIAL_EMPTY).
État mesuré (Node, JIT chaud) :
COLLIDER_SIMPLIFY_EPSILON, 1,2 px ; à 0,8 px un sol creusé fait des centaines de petits segments et la collision continue de planck coûtait jusqu'à 185 ms par tick), et la mise en sommeil d'un bloc posé (CHUNK_SLEEP_TIME) en attendant sa réintégration. Une modification du terrain près d'un bloc endormi le réveille.Une carte = un dossier dans assets/maps/<id>/. 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{
"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)
}
shared/maps/loader.ts valide chaque carte au chargement :
| 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.devicePixelRatio, plafonnée à MAX_RESOLUTION). Le terrain est dessiné en sur-échantillonnage (TERRAIN_SUPERSAMPLE texels par pixel) à partir d'un champ de distance signée calculé autour de chaque morceau : les bords sont des courbes lissées et anticrénelées, jamais l'escalier du bitmap. Le bitmap et les ops restent au pixel près (seul l'affichage change). Les fonds sont générés en 2× et anticrénelés par npm run gen:maps ; le client déduit leur résolution de leur taille.renderer/fx/Dream.ts, réglages DREAM_* dans renderer/config.ts) : le monde est rendu en 2× dans un filtre puis réduit (super-échantillonnage : tous les bords sont lissés, personnages compris). Le filtre ajoute un halo lumineux sur les zones claires, un léger voile flou, des couleurs pastel légèrement désaturées et des ombres teintées de lilas. Le fond de cuisine est flou (profondeur de champ, BACKGROUND_BLUR), et de la poussière de sucre flotte devant et derrière le gâteau. Les transitions entre matériaux et les dégradés de lumière du terrain sont interpolés, et les coulures de crème sont des gouttes arrondies.renderer/characters/painters.ts) leur donne du volume (ombre propre, reflet, éclat, hachures au crayon), des yeux expressifs (sourcils selon l'action, clignements, spirales quand assommé), des joues roses, une ombre de contact au sol et une inclinaison dans la course.
---renderer/i18n/fr.ts.strict: true, pas de any sans commentaire justificatif.shared/ dès que possible : c'est ce qui se teste.shared/terrain/ et shared/maps/ est couverte par des tests vitest.shared/constants.ts ou dans les données (JSON).feat(terrain): ..., fix(net): ..., chore: ....contextIsolation: true, nodeIntegration: false. Le preload expose une API minimale et typée (window.lan.discover(), window.lan.host(), etc.).
---Chaque étape doit être jouable avant de passer à la suivante.
gateau-etages (avec un PNG de test).carve).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.
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). Recherche en trois phases (largeur, profondeur vers le bas, puis remplissage par lignes), une seule recherche par tick, mémoire de travail partagée : voir §5 pour les mesures. Le décrochage d'un étage entier reste à ~5 ms par tick, au-dessus des 2 ms.
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).
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é.
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).
Castor Glouton (déjà jouable depuis M1), Mamie Tricot, Pyromanchot (le trio creuser / construire / exploser).
Écran de sélection des personnages.
État : implémenté.
WOOL_BURN_RADIUS_FACTOR fois son rayon.Écran de sélection : une fiche par personnage (attaque, spécial, défaut) avec des barres Vitesse / Saut / Poids lues dans character.json.
Loader validé, rotation configurable dans le lobby, choix par le dernier, modifiers.
Cartes mille-feuille et piece-montee en plus de gateau-etages.
État : implémenté.
assets/maps/, modificateurs inconnus compris. Une carte invalide est écartée avec un message explicite dans le journal.server/rotation.ts). Sans choix, la rotation décide.server/modifiers/) : wind (rafales latérales, indicateur dans le bandeau) et random-cracks (fissures en zigzag dans le biscuit). Ils ne jouent que pendant les manches. Depuis M7, random-cracks est sur la Tarte aux Fruits et wind sur la Pile de Crêpes, comme prévu au §6.Cartes : générées par npm run gen:maps. Sur la Pièce Montée, chaque chou n'est collé qu'à celui du dessous, donc en retirer un fait tomber toute la colonne au-dessus.
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.
Fait :
random-cracks) et la Pile de Crêpes (fines couches, pile qui penche, wind).SOAK_SAG_TIME_S.cue commun, prêt pour l'audio.npm run dist produit un exe portable, un zip et un installeur (§2). Ils ne sont pas signés et n'ont pas encore d'icône.npm run release publie une version dans le dépôt Git, et le jeu installé la télécharge au lancement (§2).renderer/audio/), sans fichier. Chaque matériau joue le son nommé dans materials.json (sfx). Les sons sont spatialisés autour de la caméra et varient à chaque fois. La musique, « Joyeux anniversaire » à la flûte à bec, accélère au fil de la manche et pendant la montée finale.fill), et quand ce socle disparaît, la tourelle disparaît aussi. La tourelle vise le joueur le plus proche qu'elle voit (ligne de vue vérifiée contre le terrain) et tire en compensant la gravité. Les dragées sont des projectiles comme les pétards, trop faibles pour casser le biscuit. Réglages : turret* et dragee* dans character.json.renderer/settings.ts).shared/ ou server/, lance npm test et npm run lint.