Bez popisu

Idleman 2be396f3f5 feat(dist): self-update from the git repository's release tags před 5 dny
assets a11d7249aa feat: playtest fixes - fair stuns, smart spoon, sound and options před 6 dny
scripts 2be396f3f5 feat(dist): self-update from the git repository's release tags před 5 dny
src 2be396f3f5 feat(dist): self-update from the git repository's release tags před 5 dny
tests 2be396f3f5 feat(dist): self-update from the git repository's release tags před 5 dny
.gitignore 083d43faff chore: scaffold M1 (destructible terrain, authoritative server, tooling) před 1 týdnem
CLAUDE.md 083d43faff chore: scaffold M1 (destructible terrain, authoritative server, tooling) před 1 týdnem
electron-builder.yml d1e70b0e18 chore(dist): portable exe, zip and installer for Windows před 6 dny
electron.vite.config.ts 2be396f3f5 feat(dist): self-update from the git repository's release tags před 5 dny
eslint.config.mjs 083d43faff chore: scaffold M1 (destructible terrain, authoritative server, tooling) před 1 týdnem
package-lock.json 083d43faff chore: scaffold M1 (destructible terrain, authoritative server, tooling) před 1 týdnem
package.json 2be396f3f5 feat(dist): self-update from the git repository's release tags před 5 dny
readme.md 2be396f3f5 feat(dist): self-update from the git repository's release tags před 5 dny
tsconfig.base.json 083d43faff chore: scaffold M1 (destructible terrain, authoritative server, tooling) před 1 týdnem
tsconfig.json 083d43faff chore: scaffold M1 (destructible terrain, authoritative server, tooling) před 1 týdnem
tsconfig.node.json 083d43faff chore: scaffold M1 (destructible terrain, authoritative server, tooling) před 1 týdnem
tsconfig.web.json 083d43faff chore: scaffold M1 (destructible terrain, authoritative server, tooling) před 1 týdnem
vitest.config.ts 083d43faff chore: scaffold M1 (destructible terrain, authoritative server, tooling) před 1 týdnem

readme.md

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

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.

Mises à jour automatiques

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.
  • Au lancement, le jeu lit les références du dépôt (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 ».
  • Le point d'entrée (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.
  • Seul le code du jeu est mis à jour, pas le runtime Electron. Si un build utilise une autre version majeure d'Electron, le jeu affiche qu'il faut réinstaller.
  • Ça marche avec l'installeur, le zip et l'exe portable (la version téléchargée est rangée dans le profil Windows du joueur, pas à côté de l'exe).
  • Le nouveau joueur reçoit une seule fois 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.


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. La cuillère vise une corniche du gâteau qu'elle peut atteindre (trajectoire vérifiée contre le terrain), même s'il ne reste qu'un petit îlot.
  • Un joueur coincé peut abandonner (touche K) : il plonge dans la crème et revient par la cuillère.
  • La manche s'arrête plus tôt s'il ne reste presque plus de gâteau au-dessus de la crème (MIN_CAKE_PIXELS).
  • 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.
  • 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).

    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/<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

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 :

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). Les blocs mous (laine, chantilly : densité < CRUSH_MIN_DENSITY) n'écrasent pas. 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.

  • 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) :

    • un coup ordinaire (op, recherche de détachement, colliders, pas physique) : environ 0,1 ms en moyenne, ~1 ms au p99 ;
    • un coup unique qui détache un étage entier (~90 000 px) : ~5 ms pour tout le tick, dont ~2 ms de recherche, ~1 ms de détachement et ~1,5 ms de colliders.
    • la chute de cet étage jusqu'à sa réintégration : ~5 ms par tick en moyenne, ~45 ms sur le seul tick de l'impact. Deux choses comptent : la tolérance de simplification des colliders (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.

Ce cas extrême reste au-dessus de 2 ms. Il faudrait étaler le travail sur plusieurs ticks, ou ajouter un cache de connectivité entre morceaux.

6. Cartes

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)
}

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.
  • Netteté (rendu client) : le renderer suit la densité de l'écran (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.
  • Ambiance onirique (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.
  • Personnages : une boîte à outils commune (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. ---

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). 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.

    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.

  • État : implémenté.

    • Mamie Tricot : ses aiguilles assomment longtemps. Sa pelote reste reliée par un fil au sol sous ses pieds (le fil est dessiné en vol). Là où elle touche le décor, le fil est posé tendu en Laine, avec une boule collée à l'impact : un pont tenu par ses deux bouts. Une pelote qui ne touche rien se défait. Elle traverse les joueurs, et aucune laine n'est jamais posée dans un joueur. Toute explosion brûle la laine dans WOOL_BURN_RADIUS_FACTOR fois son rayon.
    • Pyromanchot : ses pétards explosent au contact, sauf un sur trois qui lui explose dessus. Sa glissade sur le ventre se termine par une explosion qui l'épargne.
    • Objets lancés : bougie, pétards et pelotes partagent un même système de projectiles côté serveur.
    • Écran de sélection : une fiche par personnage (attaque, spécial, défaut) avec des barres Vitesse / Saut / Poids lues dans character.json.

      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.

  • État : implémenté.

    • Validation : au démarrage, le serveur valide chaque dossier de assets/maps/, modificateurs inconnus compris. Une carte invalide est écartée avec un message explicite dans le journal.
    • Rotation : l'hôte (👑, le premier arrivé) la règle dans le panneau d'échauffement : toutes dans l'ordre, au hasard, ou une sélection. L'échauffement se fait sur la première carte de la rotation.
    • Choix par le dernier : pendant l'écran de fin de manche, le dernier choisit la carte suivante parmi la rotation (logique dans server/rotation.ts). Sans choix, la rotation décide.
    • Modificateurs (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.

      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.

  • Fait :

    • Rattrapage complet : en fin de manche, le dernier choisit la carte et impose un personnage au premier, pour une manche.
    • Trophées : depuis M4.
    • Les 3 cartes : le Roulé (spirale de confiture ; le roulé ne tient que par son socle en biscuit), la Tarte aux Fruits (fond en biscuit cassant, random-cracks) et la Pile de Crêpes (fines couches, pile qui penche, wind).
    • Les 5 personnages :
    • Gégé le Ver : spécial maintenu, il nage vers la visée en creusant une galerie étroite (le chocolat l'arrête). En sortant du gâteau, ou en lâchant le bouton sous terre, il surgit et éjecte qui se tient au-dessus. Il encaisse moins de recul enterré, plus à découvert.
    • Bébé Pompier : le jet pousse tout ce qu'il touche (joueurs compris) et le recul le renvoie en arrière. Le détrempage change la génoise en génoise détrempée, qui s'affaisse par petits bouts pendant SOAK_SAG_TIME_S.
    • Captain Prout : chaque poussée le soulève, s'affiche pour tout le monde et laisse un nuage toxique. Les autres joueurs dedans ont la vue floue, ralentissent et toussent (la course trébuche). Il est immunisé contre ses propres nuages.
    • Le Chat : sa poussée déplace blocs, objets et joueurs assommés à pleine force (les autres un peu). Il s'arrête au hasard pour se lécher la patte et ignore alors les commandes.
    • Magicien Raté : gelée (génoise, biscuit et chantilly deviennent gelée), échange de place avec un joueur au hasard, lapin géant en génoise qui tombe du ciel, ou « pfft ». Le poids du « pfft » augmente avec son classement dans la manche.
    • Les signatures (prout, jet, sortie de terre, sorts, léchouille) passent par un événement cue commun, prêt pour l'audio.
    • DA, première passe finale : terrain et fonds nets à toutes les résolutions, personnages en volume et plus expressifs, ambiance onirique (§7).
    • Versions Windows autonomes : 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.
    • Mises à jour automatiques : npm run release publie une version dans le dépôt Git, et le jeu installé la télécharge au lancement (§2).
    • Retours de la première vraie partie :
    • Équilibrage : plus d'assommage en boucle (§4). Pyromanchot : pétards qui vont plus loin, explosion ratée aléatoire et moins forte. Captain Prout : 3 poussées avant de reposer le pied. Gégé : saute plus haut et creuse le chocolat. Bébé Pompier : son détrempage piège ceux qui marchent dessus et ralentit ceux qu'il arrose. Le Chat : bond griffu, coup de patte plus fort, léchouilles plus rares.
    • Cuillère : elle vise une corniche atteignable (§4).
    • Fin de manche anticipée quand il ne reste presque plus de gâteau, et abandon avec K.
    • Hôte : il peut arrêter la partie depuis les options pour revenir à l'échauffement. Pendant l'échauffement, chacun peut changer de perso.
    • Choix du dernier : 20 s au lieu de 7, raccourcies à 3 s dès que les choix sont faits.
    • Perso au hasard : une carte « Au hasard » tire un perso à chaque manche.
    • Couleurs : les joueurs qui ont le même perso sont recolorés.
    • Cartes : la Pile de Crêpes est refaite (crêpes épaisses, garnitures molles qu'on peut toutes creuser, une seconde petite pile). Des gaufrettes servent de corniches autour du Roulé, et les choux de la Pièce Montée ont le dessus plat.
    • Captain Prout : le flou du nuage passe par le filtre onirique. Il ne rétrécit plus l'écran.
    • Audio : tous les sons sont synthétisés en WebAudio (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.
    • Écran d'options (Échap ou ⚙) : volumes, son coupé, secousses de l'écran. Les réglages sont gardés sur le PC (renderer/settings.ts).
  • Reste : la suite de la DA (sprites dessinés, particules par matériau), l'éditeur de cartes, et l'icône et la signature des exe.

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.