Skip to content

Cookie Clicker - Apprendre Svelte par la pratique

Objectif : Maîtriser les fondamentaux de Svelte 5 en construisant un cookie clicker interactif

Pourquoi un cookie clicker ?

  • Concept simple mais extensible
  • Démontre les principes clés : réactivité, événements, état, boucles
  • Permet de progresser du basique au complexe
  • Chaque niveau apporte de nouveaux concepts

Format : Atelier progressif avec deux niveaux de complexité


Si vous n’avez pas encore de projet Svelte, créez-en un :

Terminal window
npm create vite@latest mon-cookie-clicker -- --template svelte
cd mon-cookie-clicker
npm install
npm run dev

Explication :

  • npm create vite@latest : crée un nouveau projet avec Vite (outil de build ultra-rapide)
  • mon-cookie-clicker : nom de votre dossier projet
  • --template svelte : configure le projet pour Svelte
  • cd mon-cookie-clicker : entre dans le dossier
  • npm install : installe les dépendances
  • npm run dev : démarre le serveur de développement (accès via http://localhost:5173)

Le terminal affichera l’URL locale. Votre navigateur recharge automatiquement quand vous modifiez le code.


Si vous avez déjà un projet Svelte :

Vérifiez que le serveur tourne :

  • Terminal ouvert
  • Serveur Vite actif (npm run dev)
  • Éditeur de code ouvert (VSCode/PhpStorm)

Si besoin de créer un nouveau composant :

macOS/Linux :

Terminal window
mkdir -p src/lib
touch src/lib/CookieClicker.svelte

Windows PowerShell :

Terminal window
mkdir src\lib -Force
New-Item src\lib\CookieClicker.svelte

Alternative : Créer le fichier directement dans l’éditeur.


Créer src/lib/CookieClicker.svelte :

{cookieClickerNiveau1}

Utiliser le composant dans src/App.svelte :

<script>
import CookieClicker from './lib/CookieClicker.svelte';
</script>
<main>
<CookieClicker />
</main>
<style>
main {
padding: 2rem;
}
</style>

1. Réactivité automatique avec $state()

let cookies = $state(0); // Variable réactive avec la rune $state()
function cliquerCookie() {
cookies += 1; // Simple assignation → interface mise à jour automatiquement
}

La rune $state() rend la variable réactive. Pas besoin de setState, this.$set, ou autre. Une simple assignation suffit.


2. Événements

<button on:click={cliquerCookie}>

Syntax : on:nomEvenement={fonction}

Autres événements utiles :

  • on:click : clic
  • on:dblclick : double-clic
  • on:mouseover : survol
  • on:input : saisie de texte
  • on:keydown : touche clavier

3. Conditions avec {#if} (inchangé)

{#if cookies < 10}
<p>Message pour débutant</p>
{:else if cookies < 50}
<p>Message intermédiaire</p>
{:else}
<p>Message expert</p>
{/if}

Syntax :

  • {#if condition} : ouvre le bloc conditionnel
  • {:else if condition} : condition alternative
  • {:else} : sinon
  • {/if} : ferme le bloc


Créer src/lib/Upgrade.svelte :

<script>
// Props : données passées par le composant parent (Svelte 5 rune)
let {
nom, // Nom de l'upgrade
prix, // Prix en cookies
gain, // Cookies par seconde gagnés
quantite, // Nombre possédé
cookiesActuels, // Pour savoir si on peut acheter
onacheter // Callback au lieu d'événement custom
} = $props();
// Réactivité dérivée avec $derived() (Svelte 5)
let peutAcheter = $derived(cookiesActuels >= prix);
</script>
<div class="upgrade" class:disabled={!peutAcheter}>
<div class="info">
<h3>{nom}</h3>
<p class="description">+{gain} 🍪/sec</p>
<p class="quantite">Possédé : {quantite}</p>
</div>
<button
class="acheter"
on:click={onacheter}
disabled={!peutAcheter}
>
{prix} 🍪
</button>
</div>
<style>
.upgrade {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
margin: 0.5rem 0;
background: #f8f9fa;
border-radius: 8px;
border: 2px solid #dee2e6;
transition: all 0.2s ease;
}
.upgrade:hover {
background: #e9ecef;
}
.upgrade.disabled {
opacity: 0.5;
}
.info {
text-align: left;
}
h3 {
margin: 0 0 0.25rem 0;
color: #333;
font-size: 1.1rem;
}
.description {
margin: 0.25rem 0;
color: #666;
font-size: 0.9rem;
}
.quantite {
margin: 0.25rem 0;
color: #999;
font-size: 0.85rem;
}
.acheter {
padding: 0.5rem 1rem;
background: #e67e22;
color: white;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
font-size: 1rem;
}
.acheter:hover:not(:disabled) {
background: #d35400;
}
.acheter:disabled {
cursor: not-allowed;
opacity: 0.5;
}
</style>

Modifier src/lib/CookieClicker.svelte :

<script>
import { onMount } from 'svelte';
import Upgrade from './Upgrade.svelte';
// État du jeu (Svelte 5 runes)
let cookies = $state(0);
let cookiesParSeconde = $state(0);
// Liste des upgrades disponibles
let upgrades = $state([
{ id: 1, nom: 'Curseur', prix: 10, gain: 1, quantite: 0 },
{ id: 2, nom: 'Grand-mère', prix: 50, gain: 5, quantite: 0 },
{ id: 3, nom: 'Ferme à cookies', prix: 200, gain: 20, quantite: 0 },
{ id: 4, nom: 'Usine', prix: 500, gain: 50, quantite: 0 },
]);
// Fonction pour cliquer sur le cookie
function cliquerCookie() {
cookies += 1;
}
// Fonction pour acheter un upgrade
function acheterUpgrade(id) {
// Trouver l'upgrade dans la liste
const upgrade = upgrades.find(u => u.id === id);
// Vérifier si on a assez de cookies
if (cookies >= upgrade.prix) {
cookies -= upgrade.prix; // Déduire le prix
upgrade.quantite += 1; // Incrémenter la quantité
upgrade.prix = Math.floor(upgrade.prix * 1.5); // Augmenter le prix pour le prochain achat
// Recalculer les cookies par seconde
calculerCookiesParSeconde();
}
}
// Calculer le total de cookies par seconde
function calculerCookiesParSeconde() {
cookiesParSeconde = upgrades.reduce((total, upgrade) => {
return total + (upgrade.gain * upgrade.quantite);
}, 0);
}
// Au montage du composant : démarrer le timer
// En Svelte 5, onMount peut retourner une fonction de cleanup
onMount(() => {
const interval = setInterval(() => {
cookies += cookiesParSeconde;
}, 1000); // Chaque seconde
// Fonction de cleanup automatique
return () => clearInterval(interval);
});
</script>
<div class="container">
<div class="game">
<!-- Colonne gauche : le cookie -->
<div class="cookie-zone">
<h1>Cookie Clicker</h1>
<p class="score">🍪 {Math.floor(cookies)} cookies</p>
<p class="rate">+{cookiesParSeconde} cookies/sec</p>
<button class="cookie" on:click={cliquerCookie}>
🍪
</button>
{#if cookies < 10}
<p class="message">Cliquez sur le cookie !</p>
{:else if cookiesParSeconde === 0}
<p class="message">Achetez votre premier upgrade →</p>
{:else}
<p class="message">🎉 Votre empire grandit !</p>
{/if}
</div>
<!-- Colonne droite : les upgrades -->
<div class="upgrades-zone">
<h2>Upgrades</h2>
{#each upgrades as upgrade (upgrade.id)}
<Upgrade
nom={upgrade.nom}
prix={upgrade.prix}
gain={upgrade.gain}
quantite={upgrade.quantite}
cookiesActuels={cookies}
onacheter={() => acheterUpgrade(upgrade.id)}
/>
{/each}
</div>
</div>
</div>
<style>
.container {
max-width: 1000px;
margin: 0 auto;
padding: 2rem;
}
.game {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.cookie-zone {
text-align: center;
}
h1 {
color: #333;
margin-bottom: 1rem;
}
.score {
font-size: 2rem;
font-weight: bold;
color: #e67e22;
margin: 1rem 0 0.5rem 0;
}
.rate {
font-size: 1.2rem;
color: #27ae60;
margin: 0 0 1rem 0;
}
.cookie {
font-size: 8rem;
background: none;
border: none;
cursor: pointer;
transition: transform 0.1s ease;
padding: 0;
margin: 2rem 0;
}
.cookie:hover {
transform: scale(1.1);
}
.cookie:active {
transform: scale(0.95);
}
.message {
font-size: 1.2rem;
color: #666;
font-style: italic;
}
.upgrades-zone {
background: #fff;
padding: 1rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h2 {
margin-top: 0;
color: #333;
}
@media (max-width: 768px) {
.game {
grid-template-columns: 1fr;
}
}
</style>

Étape 3 : Explication des nouveaux concepts

Section titled “Étape 3 : Explication des nouveaux concepts”

1. Props : passer des données à un composant enfant (Svelte 5)

Dans le composant enfant (Upgrade.svelte) :

let { nom, prix } = $props();

La rune $props() récupère les props passées par le parent. Plus besoin de export let.

Dans le composant parent (CookieClicker.svelte) :

<Upgrade
nom={upgrade.nom}
prix={upgrade.prix}
/>

Le passage des props reste identique.


2. Listes avec {#each}

{#each upgrades as upgrade (upgrade.id)}
<Upgrade ... />
{/each}

Syntax :

  • {#each liste as element (clé unique)} : boucle
  • (upgrade.id) : clé unique pour optimiser le rendu
  • {/each} : ferme la boucle

3. Événements custom → Callbacks (Svelte 5)

En Svelte 5, les événements custom sont remplacés par des props callback.

Dans le composant enfant (Upgrade.svelte) :

let { onacheter } = $props();
// Appeler directement la fonction
<button on:click={onacheter}>

Dans le composant parent (CookieClicker.svelte) :

<Upgrade onacheter={() => acheterUpgrade(upgrade.id)} />

Notez le changement : on:acheter devient onacheter (une prop fonction).


4. Réactivité dérivée avec $derived() (Svelte 5)

let peutAcheter = $derived(cookiesActuels >= prix);

La rune $derived() recalcule automatiquement la valeur quand ses dépendances changent. C’est le remplacement de $: pour les valeurs dérivées.


5. Lifecycle avec onMount (Svelte 5 simplifié)

import { onMount } from 'svelte';
onMount(() => {
// Code exécuté quand le composant est créé
const interval = setInterval(() => {
cookies += cookiesParSeconde;
}, 1000);
// En Svelte 5, retourner une fonction la rend automatiquement fonction de cleanup
return () => clearInterval(interval);
});

Svelte 5 simplifie : plus besoin de onDestroy, juste retourner la fonction de nettoyage depuis onMount.


Concepts Svelte 5 maîtrisés :

  • ✅ Réactivité avec $state()
  • ✅ Événements DOM (on:click)
  • ✅ Callbacks (remplacement des événements custom)
  • ✅ Conditions ({#if})
  • ✅ Listes ({#each})
  • ✅ Props avec $props() (communication parent → enfant)
  • ✅ Callbacks (communication enfant → parent)
  • ✅ Lifecycle (onMount avec cleanup)
  • ✅ Réactivité dérivée avec $derived()

Ce qu’on peut construire avec ça :

  • Interfaces interactives
  • Jeux simples
  • Expériences narratives
  • Outils créatifs

→ Voir la page complète des territoires

  1. Expériences muséales/spatiales
  2. Jeux de société hybrides
  3. Expériences de lecture augmentées
  4. Interfaces expérimentales/ludiques
  5. Outils créatifs et générateurs

Trouvez 3 à 5 œuvres qui vous inspirent dans votre territoire préféré.

Pour chaque œuvre, notez dans votre carnet :

  • Nom et lien
  • Description de l’expérience
  • Ce qui fonctionne / ne fonctionne pas
  • Pourquoi ça vous inspire

Où chercher :

Section titled “2. Enrichir votre cookie clicker (optionnel mais recommandé)”

Idées d’améliorations :

Facile :

  • Ajouter plus d’upgrades
  • Changer les emojis et le thème visuel
  • Ajouter des sons au clic
  • Sauvegarder le score dans LocalStorage

Moyen :

  • Ajouter des achievements (badges)
  • Créer des combos de clics
  • Animations de particules au clic
  • Thème jour/nuit

Avancé :

  • Mini-jeux bonus
  • Système de prestige (reset avec bonus)
  • Graphique de progression
  • Mode multiplayer (partage de score)

3. Commencer à réfléchir à votre projet

Section titled “3. Commencer à réfléchir à votre projet”

Répondez à ces 3 questions :

Intention : Que voulez-vous faire ressentir/découvrir ?
Geste : Quelle action fait l’utilisateur ?
Synergie : Comment le geste et l’intention se renforcent ?

Exemple :

  • Intention : Faire ressentir la solitude d’un personnage
  • Geste : Cliquer pour interagir, mais le personnage s’éloigne
  • Synergie : Plus on essaie de se rapprocher, plus il fuit → frustration = émotion

Documentation Svelte :

Aide technique :


  • ✅ Cookie clicker fonctionnel avec réactivité Svelte
  • ✅ Compréhension des props et événements
  • ✅ Gestion de listes avec {#each}
  • ✅ État complexe avec cookies par seconde
  • ✅ Premières idées de territoire de projet