Tu reprends le projet Demiurge pour Ple (GitHub dodz2). Parle-lui en français. Tu n’agis jamais à sa place sans son accord (surtout merge, deploy, fan-out). Lis ce fichier en entier avant de toucher au code.
Demiurge est un sandbox spatial 3D pixel art (N-body maison) : le joueur crée, déplace, modifie et détruit des corps, puis observe les conséquences. Inspiration Universe Sandbox, DA pixel art volumétrique, pas un filtre.
Source de vérité : ROADMAP.md. Toute évolution de périmètre passe par ce fichier avant le code. Journal des décisions = §11, inliné dans la même PR (un sidecar seul est un BLOCKER Reviewer).
| Dépôt | https://github.com/dodz2/demiurge — privé |
| Compte | dodz2 |
| Branche de vérité | main |
| HEAD (29 août 2026, ~21:08 Europe/Paris) | e56fa60b787cb47af68a23f94c5577fb09042e91 |
| Preview jouable | https://dodz2.github.io/demiurge/ (public). Le source reste privé ; seule cette page est publique, dans dodz2/dodz2.github.io sous demiurge/ (build Vite avec base /demiurge/). |
- P4
9161284— scènes JSON + calibration (21 août 2026) - P5
4906759— PR #1, pipeline pixel art - P6
f7475cb— PR #2, caméra multi-échelles - P7
e56fa60— PR #3, sélection / temps / panneau
Branches feature encore sur le remote (ne plus les prendre comme source de vérité) :
feat/phase-5-pixel-art-renderfeat/phase-6-multi-scale-camerafeat/phase-7-selection-time-ui— ne pas rebaser dessus : la tip a parfois un ROADMAP tronqué.mainest intact.
- Pas de Cloud Agent Cursor : SuperGrok n’est pas Cursor Pro. Tout passe par GitHub MCP (
user-Github) + API Contents. - Ne pas git clone sauf demande explicite de Ple.
- MCP create_or_update_file rate souvent sur les gros fichiers (ROADMAP ~49 Ko, package-lock ~95 Ko). Workaround : PUT Contents API. Un PUT trop petit a tronqué ROADMAP au blockquote (NO-GO Ops P7) : vérifier taille et §11 après chaque PUT.
- CLI
ghpas forcément loggée. Connecteur GitHub de Ple installé. - Self-approve GitHub impossible (auteur = dodz2). Reviewer = COMMENT interne, pas APPROVE.
- Ne merge que si Ple a dit GO (ou « une fois que tout est bon »). Méthode : squash.
- Langue : français. Commits conventionnels
feat(phase-N):. Timezone : Europe/Paris.
Ple a demandé d’appeler les autres agents à chaque phase.
- Ops — checklists GO/NO-GO, ordre, critères de merge
- Coder — brief fichiers / API / tests / pièges
- Reviewer — grille BLOCKER / SHOULD / HORS SCOPE ; attendre son verdict avant squash (P6 a été mergé trop tôt)
Ne lance pas de Cloud Agent depuis Coder (STOP déjà nécessaire en P7).
TypeScript strict, Vite 8, Vitest 4, Three.js 0.185.x, ESLint + Prettier, CI GitHub Actions (typecheck, lint, test, build, Node 24).
Layout :
src/sim— simulation pure. Zéro import three / DOM / UI. Frontière lint + purity.test.tssrc/render— snapshots vers meshes. RT 480×270 nearest. SCALE uniquesrc/camera— caméra orbitale custom (pas d’OrbitControls). Sans threesrc/input— pointer : orbite vs pick. Picking headlesssrc/ui— overlay DOM natif (pas de framework) : panneau, barre temps, tooltip, sélectionsrc/scenes— JSON versionné + loader/saversrc/main.ts— câblage uniquement
Unités sim : distance Mm, masse kg, temps s. G et softening par scène. dt jeu = 1/120 s. Intégrateur velocity Verlet. Collisions sphères balayées, résolution par fusion inélastique (plus petit id survit).
Flux :
- UI / Input envoie des commandes à la Simulation (seule proprio de l’état)
- Simulation envoie des snapshots lecture seule vers Render / UI
- Simulation envoie des événements (collisions) vers Render. Les FX = P10 ; aujourd’hui on drain sans FX
Une PR qui importe du rendu/DOM dans src/sim est rejetée. En P5–P7 on a exigé src/sim 0 diff (SHAs blob identiques vs main).
Rendu pixel art (ne pas casser) :
- Render target 480×270, NearestFilter, generateMipmaps false, setPixelRatio(1)
- Upscale = blit nearest letterbox 16:9, pas de shader pixelate, pas d’upscale CSS
- SCALE = 1 uniquement dans
src/render/scale.ts(1 unité scène = 1 Mm). Meshes via toScenePosition / toSceneRadius. Jamais mesh.position pour follow ou picking - Une seule PerspectiveCamera. aspect toujours RT_ASPECT, jamais innerWidth/innerHeight
- Fond spatial généré à l’intérieur du RT
Scène par défaut : src/scenes/data/systeme-simple.json — étoile Heliandre id 1 + planètes (Braise id 2, etc.), G calibré, orbites 45 / 75 / 105 Mm.
Phases 0 à 7 sont mergées sur main. Prochaine phase produit : P8 — outil de création de corps. Ne pas commencer P8 (ni P9–P11, ni les SHOULD) sans GO explicite de Ple.
Socle npm / TypeScript / Vite / CI. Simulation pure, gravité N-body, collisions/fusions + file d’événements, scènes JSON, round-trip, calibration gameplay. Demos headless phase 1 à 4.
Three.js confiné à src/render. PixelArtRenderer consomme getSnapshot(). Palette par catégorie (star #FFD166, planet #5B8CDE, asteroid #A8A29E) + visualColor. Caméra encore statique +Z, lookAt origine. L’étoile paraissait hors-centre (les planètes tirent l’étoile). Corrigé en P6 par le follow id 1.
Module src/camera sans Three.js. Interdit OrbitControls / TrackballControls.
- Sphérique yaw / pitch / distance / target / panOffset
- Amortissement exponentiel λ = 12 s⁻¹, dt frame, jamais dt sim
- Zoom exponentiel k = 0.001
- Pan et déplacement monde proportionnels à la distance
- followTarget ancre lookAt sur toScenePosition(snapshot.position) + panOffset. Jamais mesh.position. Id absent/fusionné → unfollow propre
- Défaut : étoile id 1, distance 260, dès la frame 0
- near = max(1e-4, distance × 0.02), far = distance × 80. Pas de floating origin, pas de logarithmicDepthBuffer
- Renderer : applyView(pose) seulement, aspect = RT_ASPECT
- Pointer/wheel : même mapPointerToLetterbox que le blit ; barres noires ignorées
- Test headless follow 300 s (orbite synthétique + Braise / systeme-simple), dérive < 1e-9
- Constantes : DEFAULT_DISTANCE 260, DEFAULT_FOLLOW_ID 1, MAX_DISTANCE 2000, ORBIT_RADIANS_PER_PIXEL 0.005, PITCH_LIMIT ≈ 85°, CAMERA_FOV 50
Fichiers : src/camera (constants, math, orbital-camera, input, index, tests), src/render/letterbox.ts, pixel-renderer.ts, main.ts, README, docs/journal-phase-6.md.
Livrable : « je sélectionne → je lis → j’accélère → j’observe ».
- P7-1 Picking headless
src/input/picking.ts: rayon depuis CameraPose, NDC x = nx×2−1, y = 1−ny×2, sphère toScenePosition + toSceneRadius(visualRadius). Pas de Raycaster / mesh. Highlight PixelArtRenderer.setSelection(id) emissive Lambert 0x222222 → 0x886622, sans recréer le mesh. - Clic vs drag Seuil 5 px letterbox (CLICK_MOVE_THRESHOLD_PX). Coordinateur src/input/pointer.ts. Clic gauche < 5 px = pick (ou clear si miss). ≥ 5 px = orbite P6. Droit / milieu / Maj = pan. Molette = zoom. Letterbox miss = ignore.
- P7-2 src/ui/info-panel.ts : nom, catégorie, masse, rayons, |v|, distance = |position| depuis l’origine en Mm. Snapshot only.
- P7-3 src/ui/time-controls.ts : Pause / ×1 / ×10 / ×100. loop.update(elapsed × timeScale, world.step). Même dt (1/120). Pause = 0 sous-pas. camera.update(snapshot, elapsed) = dt frame réel. Espace = pause, 1/2/3 = warps. maxStepsPerUpdate 4096 (préexistant).
- P7-4 Tooltip nom, pointer-events none. Hover ≠ sélection.
- P7-5 src/ui/selection.ts : id disparu/fusionné → null, panneau vide, pas de resélection auto du survivant, pas de throw.
- Follow inchangé au pick (étoile P6). Clic vide clear sélection/highlight, pas le follow.
Tests au merge : 112 / 17 fichiers (85 P0–P6 + 27 P7). src/sim 0 diff vs f7475cb. CI verte.
HUD : DEMIURGE - phase 7 · {fps} fps · ×{scale}. Barre temps / panneau : pointer-events auto. index.html : #info-panel, #time-bar, #tooltip.
- Clic gauche sans glisser : sélectionner / désélectionner
- Glisser gauche : orbite amortie
- Molette : zoom exponentiel
- Droit / milieu / Maj+gauche : pan proportionnel à la distance
- Barres noires 16:9 ignorées
- Barre Pause / ×1 / ×10 / ×100
- Survol : nom
Local : npm install puis npm run dev (Vite, souvent localhost:5173). Ou la preview Pages ci-dessus.
Issu des reviews P6/P7 (Ops / Coder / Reviewer) :
- Micro-orbite — orbit() part dès le premier pointermove, même si maxMovement < 5 px. Un clic tremblé = orbite et pick. Fix : bufferer l’orbite jusqu’au seuil. Coder + Reviewer d’accord, pas un BLOCKER.
- applyView force CAMERA_FOV / RT_ASPECT alors que le picking lit pose.fov / pose.aspect. Alignés aujourd’hui ; demain ça diverge = pick ≠ pixels.
- Sidecars redondants avec §11 : docs/journal-phase-6-roadmap-rows.md, docs/journal-phase-7-roadmap-rows.md. Supprimables après GO.
- Vec3 tripliqué : src/sim/vec3.ts, src/camera/math.ts, src/input/picking.ts. NIT, ne pas fusionner les modules à la légère (frontière sim).
- Hitch à ×100 : ~192 pas/frame à 60 fps OK ; un freeze ~0,34 s tape maxStepsPerUpdate 4096 et jette du temps simulé.
- Pas de test picking aux zooms extrêmes (P7-1 « tous les zooms »). Les tests couvrent NDC / nearest / miss / visualRadius / t<0 / letterbox.
- Capture manuelle P6 jamais faite : zoom planète → système → planète (critère visuel ROADMAP).
- Ligne journal P6 « Pas de picking (P7) » périmée.
- isPointerClick surtout utilisé dans les tests.
- CAMERA_NEAR / CAMERA_FAR restent des défauts constructeur ; applyView les écrase chaque frame.
Graphe : P8 → P9 → P10 → P11. Rien hors phase courante. Idée séduisante → section Post-MVP du ROADMAP, pas du code.
Geste fondateur : placer + drag de vitesse. Livrable : un novice crée en 2 min une planète qui orbite.
- P8-1 Barre 3 presets (étoile / planète / astéroïde), masses/rayons documentés
- P8-2 Clic = position sur un plan orbital
- P8-3 Drag = direction + amplitude, flèche vectorielle pixelisée
- P8-4 Release = commande atomique spawn, vitesse = drag à epsilon près
- P8-5 Échap + aperçu visuel ; rien de corrompu après cancel
L’aperçu pendant le drag est purement visuel. Le corps spawné devient sélectionnable. Ne pas muter la sim depuis l’UI autrement que par commande. Hors scope : presets exotiques, température, placement relatif.
Piège P7 : le clic gauche sert déjà au pick et à l’orbite. P8 devra un mode outil (barre) pour que clic+drag = spawn, sans casser pick/orbite. Trancher avec Ops/Coder dans le ROADMAP §11 avant de coder.
Slider log masse, rayon physique (+ visuel cohérent), facteur de vitesse, suppression du sélectionné. Chaque edit = commande atomique. Éditer masse/rayon ne change jamais la vitesse tout seul. Guards > 0. Suppression propre pendant une collision. Pas d’undo, pas de multi-corps, pas de température.
Consommer enfin drainEvents() : impact pixelisé + secousse caméra proportionnelle à l’énergie, traînées (côté rendu), vecteurs vitesse toggle, skins pixel 2–3 par catégorie, lumière depuis l’étoile. Aucun FX ne mute la sim. Pas de sons, débris physiques, anneaux, atmosphères.
Scène d’ouverture calibrée, build prod, plafond de corps, aide 3–5 lignes, playtest scénario ROADMAP §2.1, run 30 min ×100. Cocher toute la section 7 du ROADMAP.
Post-MVP (ne pas négocier maintenant) : floating origin, Barnes-Hut, prédiction d’orbites, fragmentation, audio, éditeur, save UI, etc.
- Ple dit GO Phase N.
- Brief Ops (checklist, ordre, DoD merge), Coder (fichiers, API, tests, pièges), Reviewer (grille BLOCKER). Fan-out OK sur ce projet.
- Branche feat/phase-N-… depuis main actuel. Peu de commits (P6/P7 ont dérivé à 13–17 commits bruyants ; le squash sauve l’historique).
- Implémenter uniquement PN-x. Préserver P5–P7. src/sim 0 diff sauf besoin réel P8+ (alors commandes atomiques, pas d’écriture Body depuis l’UI).
- Tests headless sans WebGL. Garder les 112 existants verts. typecheck + lint + test + build.
- README (statut + contrôles) + ROADMAP §11 inliné (vérifier que le fichier n’est pas tronqué : plusieurs centaines de lignes, table P0…PN). Journal docs/journal-phase-N.md en plus, pas à la place.
- PR vers main, body FR, « Ne pas merger » le temps des reviews.
- CI verte + GO Ops + GO Reviewer (0 BLOCKER) → squash merge. Ple a validé ce rythme.
- Ping Ops / Coder / Reviewer avec le lien PR puis avec le SHA squash. Pas de phase suivante tant que Ple n’a pas dit GO.
- Preview Pages : rebuild Vite avec base /demiurge/ et pousser dist/ vers dodz2/dodz2.github.io chemin demiurge/ si Ple veut un lien cliquable.
BLOCKERS types Reviewer : casser l’orbite P6, UI qui mute la sim, ×100 qui change les trajectoires, sélection fantôme après fusion, RT/SCALE/sim cassés, pick dans le letterbox, Raycaster Three, panneau ≠ snapshot, tests rouges, §11 absent ou ROADMAP tronqué, hors-scope (P8 dans une PR P7, etc.).
À lire en premier sur main @ e56fa60 :
- ROADMAP.md §6 Phase 8 + §11 (fin de table)
- src/main.ts — boucle : timeScale → step → snapshot → selection → sync → highlight → camera.update(frame dt) → applyView → render → drainEvents
- src/input/pointer.ts, picking.ts, src/camera/input.ts — conflit clic
- src/ui/* — overlay, pointer-events
- src/sim/world.ts — addBody existe déjà ; l’UI ne l’appelle pas encore
- src/render/pixel-renderer.ts — setSelection, applyView
- src/scenes/data/systeme-simple.json
Commandes : npm run dev, test, lint, typecheck, build.
Les phases 0–7 sont sur main (e56fa60). Il peut jouer ici : https://dodz2.github.io/demiurge/
La suite ROADMAP est P8 création de corps. Tu n’y touches pas tant qu’il n’a pas dit GO. Les SHOULD (buffer 5 px, Vec3, captures, sidecars) sont parkés.
Première action : attendre le GO P8, relire ROADMAP.md Phase 8, brief Ops/Coder/Reviewer, puis une PR propre avec §11 inliné et src/sim touché seulement via addBody (commande), jamais en contournant le monde.