Skip to content

Repository files navigation


Un survivor-like (bullet heaven) jouable directement dans le navigateur. Vous ne tirez jamais : vos armes se déclenchent seules. Toute la profondeur vient du choix des améliorations entre deux vagues, et de la construction d’un build qui transforme un chasseur fragile en machine à effacer l’écran.

Une partie dure 30 minutes. Le temps est le seul véritable adversaire.

Aucune dépendance runtime. Aucun fichier binaire. Sprites, animations, décor, biomes, illustration des menus, ornements d’interface, logo, curseur et sons sont générés par le code au démarrage. Le jeu entier tient en 79 ko compressés et fonctionne hors ligne.

Une partie à la quinzième minute


Aperçu

Écran titre Sélection de personnage
Écran titre – scène nocturne en six couches animées par parallaxe, logo pixel dont le sang coule, compteurs répartis dans les coins Six personnages, chacun avec son arme de départ, son bonus et son défaut. La progression vers chaque déblocage est chiffrée
Montée de niveau Relique
Un choix à chaque niveau – trois cartes tirées par catégorie, la horde figée derrière 24 reliques, dont quatre maudites : puissantes, mais elles se paient
Codex Bestiaire
Codex de 77 entrées à sprites animés, silhouettées tant qu’elles ne sont pas découvertes Bestiaire – dix-huit créatures, leurs statistiques et leur comportement
Ruines Village abandonné
Des ruines qui arrêtent – six bâtiments de 84 à 216 px, avec collision. Toute ruine a au moins deux ouvertures Villages abandonnés – des maisons le long d’une rue dallée, la pression majorée et un coffre au centre
Sanctuaire Fin de partie
Sanctuaire – l’or survit à la mort et achète des améliorations permanentes Bilan de fin, graine comprise pour rejouer exactement le même run

Contenu

18 armes + 15 évolutions, débloquées uniquement par les coffres
12 passifs 6 emplacements d’armes, 6 de passifs – le plafond force les vrais choix
24 reliques Objets uniques, hors emplacements, dont 4 maudites
13 ennemis 9 comportements d’IA distincts
5 boss Dont la Faucheuse, invulnérable, qui met fin à la partie
6 personnages 4 à débloquer, avec progression affichée
5 biomes Composition d’ennemis et effets passifs propres
7 structures Autel, bûcher, obélisque, puits, ossuaire, chapelle, cairn
11 améliorations Méta-progression permanente au Sanctuaire
Codex 77 entrées à sprites animés – armes, évolutions, bestiaire, reliques
Progression Les 15 recettes d’évolution et les 4 déblocages, avec leur avancement chiffré
Reprise de partie Un onglet fermé ne coûte plus la partie en cours
Boutique 23 cosmétiques – teintes, traînées, thèmes d’interface, curseurs
Collection 42 pièces disséminées, trouvables à la résonance, et leur archive
Accessibilité Échelle d’interface, contraste, animations, vitesse de jeu, repère joueur
Tactile Téléphone et tablette – joystick, pause, zones sûres, écran maintenu allumé

Jouer

Action Clavier Manette Tactile
Déplacement ZQSD / WASD / flèches Stick gauche Glisser (joystick virtuel)
Pause Échap / P Start Bouton en haut de l’écran
Valider Entrée / Espace / clic A Tap
Minimap M
Plein écran F

La minimap sert aussi de détecteur : elle pulse en direction de ce qui est enfoui à proximité.

Sur téléphone et tablette, le jeu propose de passer en paysage – mais seulement quand ça vaut la peine : le déclencheur est la part d’écran réellement perdue en bandes noires, pas le type d’appareil. Un iPhone en portrait en perd 61 %, un iPad 32 %, une tablette 4∶3 seulement 17 % et n’est donc pas sollicitée. La proposition reste une proposition.

Il n’y a pas de touche d’attaque. C’est le genre qui veut ça.

AZERTY et QWERTY fonctionnent simultanément : les touches sont lues par code physique.

Outils de développement
Touche Effet
~ Compteurs de debug : FPS, entités, temps par système, et le rapport d’échelle physique (net / rééchantillonné)
F1 +10 niveaux · F2 +1 minute · F3 soins complets
F4 Tue tout à l’écran · F5 +10 000 or
F6 Fait tomber une relique · F7 fait tomber un coffre
F8 Bascule entre le héros détaillé et l’ancien, pour comparer

Planche des sprites : pnpm planche écrit dist-planche/planche.html, qui affiche les 116 sprites du jeu générés à l’instant par ses propres fonctions – avec leur taille et leur nombre de teintes réelles. Elle est aussi servie en ligne à /planche.html.

La console expose window.sanguine : .snapshot(), .world, .startRun(id), .exportSprites().


Développement

pnpm install
pnpm dev        # serveur de développement → http://localhost:5180
pnpm build      # production → dist/
pnpm preview    # sert le build

dist/index.html fonctionne aussi en file:// et hors ligne : rien n’est chargé depuis le réseau.

Mesure d’audience

Facultative, et absente par défaut. Sans clé de site à la compilation, src/core/analytics.ts est un no-op : aucune requête, aucun stockage, rien à refuser.

cp .env.example .env       # puis renseigner VITE_SARUTOBI_KEY
pnpm build                 # ou : docker compose up -d --build

La clé n’est pas versionnée : .gitignore refuse .env, et docker-compose.yml ne fait que la relayer au build via un ARG. L’image finale reste statique — la clé est figée dans le bundle, il n’y a aucune variable d’environnement à l’exécution.

La mesure parle directement le contrat d’ingestion Sarutobi plutôt que d’embarquer son SDK ou son <script> distant : la promesse « aucun asset externe » vaut aussi pour ce qui mesure le jeu. Coût mesuré : +1,05 ko gzip.

Variable Rôle
VITE_SARUTOBI_KEY Clé publique du site. Absente ⇒ mesure désactivée
VITE_SARUTOBI_HOST Instance de collecte, si ce n’est pas sarutobi.ascencia.re

Ce que cela coûte : connect-src valait 'none' dans docker/security-headers.conf, ce qui faisait de l’absence de requête sortante une garantie imposée par le navigateur. Elle est désormais énumérée à un hôte unique. Le jeu ne charge toujours rien de l’extérieur, mais il n’est plus vrai qu’il ne joint personne — et une seule destination reste joignable, en écriture.

Ce qui part : pageview, run_started, run_ended, upgrade_picked, upgrade_skipped, chest_opened, pageleave, et les erreurs JavaScript non rattrapées. Les propriétés sont des identifiants de contenu du jeu — ysolde, faux, victory — et les compteurs sont arrondis. Aucune identité persistante n’est créée : la session vit en mémoire et meurt avec l’onglet. Le localStorage ne contient que la sauvegarde du jeu et la préférence de langue FR/EN.

Docker

docker compose up -d --build     # → http://127.0.0.1:4020

Mise en production complète dans DEPLOY.md. Publication du manuel : ./tools/publish-docs.sh.


Architecture

src/
├── core/     boucle à pas fixe, RNG déterministe, grille spatiale, entrées, sauvegarde, mesure
├── gfx/      palette, primitives pixel, générateurs de sprites, caméra, particules, rendu
├── audio/    synthétiseur Web Audio + musique adaptative en couches
├── data/     tables de contenu pures (aucune dépendance)
├── game/     joueur, ennemis, armes, terrain, butin, director, améliorations
└── ui/       HUD, écrans, minimap, logo animé, illustration des menus, ornements
Choix Raison
Canvas 2D plutôt que WebGL Suffisant jusqu’à ~2000 sprites, sans la complexité des shaders
Pools pré-alloués plutôt qu’un ECS Peu de types d’entités, énormément d’instances
Grille de hachage reconstruite chaque frame Plus rapide qu’une mise à jour incrémentale, impossible à désynchroniser
HUD en DOM Texte net à toute résolution, accessible, gratuit dans la boucle de rendu
Monde déterministe par position Monde infini à coût mémoire nul, runs rejouables à la graine
Facteur d’échelle entier en pixels physiques Un pixel de jeu vaut toujours un nombre entier de pixels écran, à n’importe quel devicePixelRatio

La résolution logique s’adapte à la fenêtre en conservant le facteur entier : pas de bandes noires, et un pixel art net y compris sous une mise à l’échelle Windows à 125 % ou 150 %.

Mesuré à la densité de la 28ᵉ minute, Déferlante active : 60 fps constants avec 460 ennemis et 2 200 objets au sol (update ~1 ms, render 5 à 12 ms), tas mémoire stable sur la durée.


Documentation

La documentation de conception est en français et sert de source de vérité : les tables de src/data/ doivent la refléter.

Document Contenu
docs/00-vision.md Pitch, piliers, contraintes, critères de réussite
docs/01-game-design.md Boucles, statistiques, formules, sauvegarde, accessibilité
docs/02-content-bible.md Armes, passifs, reliques, ennemis, biomes, structures
docs/03-technical-architecture.md Stack, modules, budget de performance
docs/04-art-direction.md Palette, pipeline procédural, game feel, erreurs corrigées
docs/05-audio-design.md Synthèse, catalogue d’effets, musique adaptative
docs/06-roadmap.md Jalons, hors périmètre, dette technique assumée
docs/07-monde-et-decor.md Végétation, ruines, collision, routes, sols à motifs, ornements

Équilibrage mesuré, pas deviné

Les courbes ne sont pas réglées à l’intuition mais mesurées par un bot qui joue réellement : il fuit la pression locale des ennemis, dérive vers les gemmes et choisit ses cartes.

Deux réglages de densité ont été mesurés puis rejetés. Le second est le plus instructif :

Réglage Résultat mesuré
0.9 + min × 0.55 ~20 ennemis à l’écran, niveau 8 seulement à 3 min 30 : trop vide
1.6 + min × 1.05 474 ennemis à 6 min, une seule arme au niveau 5, mort à 6 min 48
1.1 + min × 0.70 Niveau 14 à 4 min 30, 4 armes, 745 morts, 60 fps – retenu

Au-delà d’un certain seuil, augmenter la densité réduit la progression : le joueur ne tue plus assez vite, ne ramasse plus de gemmes, et la courbe d’XP s’effondre pendant que l’écran se remplit.


Licence

MIT – faites-en ce que vous voulez.


© 2026 Ascencia

About

Survivor-like jouable dans le navigateur. Zéro dépendance runtime, tous les assets (sprites, décor, sons, interface) générés par le code.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages