Annuaire › Code ouvert
Code publié par son auteur, en lecture seule. « Copier dans mon espace » crée un nouveau site à vous (non publié) que vous pouvez modifier librement.
js/jeu.js · 19.4 Ko
/* ============================================================
jeu.js - la classe Jeu (le chef d'orchestre)
============================================================
Cette classe :
- possède tous les objets du jeu (pirogue, gousses, obstacles,
particules, fond, sons) ;
- gère la MACHINE À ÉTATS : menu -> jeu -> fin -> jeu -> ... ;
- calcule la difficulté, les apparitions, les collisions, le score ;
- dessine l'écran (décor, objets, interface).
MACHINE À ÉTATS : le jeu est dans un état à la fois (this.etat).
Selon l'état, on fait des choses différentes à chaque image :
ETATS.MENU : le décor défile doucement, on attend un appui ;
ETATS.JEU : la partie se joue ;
ETATS.FIN : on affiche le score, on attend un appui pour rejouer.
TEMPS ET dt : chaque méthode mettreAJour(dt) reçoit `dt`, le temps
écoulé (en SECONDES) depuis la dernière image. On déplace chaque
chose de vitesse x dt : le jeu va à la même vitesse quel que soit
le nombre d'images par seconde de l'appareil.
============================================================ */
class Jeu {
/* constructor : crée tous les objets et charge le record. */
constructor() {
this.sons = new GestionnaireSons();
this.fond = new Fond();
this.pirogue = new Pirogue();
this.gousses = []; // gousses à l'écran
this.obstacles = []; // obstacles à l'écran
this.particules = []; // effets visuels
this.etat = ETATS.MENU;
this.tempsEtat = 0; // secondes écoulées depuis le dernier changement d'état
this.score = 0;
this.vies = VIES_DEPART;
this.temps = 0; // durée de la partie en cours (secondes)
this.vitesse = DIFFICULTE.vitesseMenu; // vitesse de défilement actuelle
this.chronoGousse = 0; // compte à rebours avant la prochaine gousse
this.chronoObstacle = 0; // idem pour les obstacles
this.secousse = 0; // durée restante de la secousse d'écran
// Record : lu dans le stockage (texte) puis converti en nombre entier.
this.record = parseInt(Stockage.lire(CLE_RECORD, '0'), 10) || 0;
this.nouveauRecord = false;
// Préférence de son mémorisée.
this.sons.muet = (Stockage.lire(CLE_MUET, '0') === '1');
}
/* ==========================================================
SECTION 1 : changements d'état et nouvelle partie
========================================================== */
/* changerEtat(nouvelEtat) : passe à un autre état et remet le chrono d'état à 0. */
changerEtat(nouvelEtat) {
this.etat = nouvelEtat;
this.tempsEtat = 0;
}
/* demarrerPartie() : réinitialise tout et lance une partie. */
demarrerPartie() {
this.gousses = [];
this.obstacles = [];
this.particules = [];
this.score = 0;
this.vies = VIES_DEPART;
this.temps = 0;
this.nouveauRecord = false;
this.chronoGousse = 0.6; // première gousse très vite
this.chronoObstacle = 1.6; // premier obstacle un peu plus tard : on laisse le temps de réagir
this.pirogue.reinitialiser();
this.sons.jouer('clic');
this.changerEtat(ETATS.JEU);
}
/* finirPartie() : passe à l'état FIN et enregistre le record si battu. */
finirPartie() {
if (this.score > this.record) {
this.record = this.score;
this.nouveauRecord = true;
Stockage.ecrire(CLE_RECORD, this.record);
this.sons.jouer('victoire');
}
this.changerEtat(ETATS.FIN);
}
/* ==========================================================
SECTION 2 : difficulté progressive
========================================================== */
/* difficulte() : renvoie un nombre de 0 (début) à 1 (difficulté maximale). */
difficulte() {
return constrain(this.temps / DIFFICULTE.dureeMax, 0, 1);
}
/* niveau() : numéro de niveau affiché (1, 2, 3...). */
niveau() {
return 1 + Math.floor(this.temps / DIFFICULTE.secondesParNiveau);
}
/* ==========================================================
SECTION 3 : mise à jour (appelée à chaque image)
========================================================== */
/* ----------------------------------------------------------
mettreAJour(dt, entrees)
Paramètres :
dt : secondes écoulées depuis la dernière image
entrees : {direction, cibleX} lus dans sketch.js
---------------------------------------------------------- */
mettreAJour(dt, entrees) {
this.tempsEtat += dt;
this.secousse = Math.max(0, this.secousse - dt);
if (this.etat === ETATS.JEU) {
this.mettreAJourPartie(dt, entrees);
} else {
// Menu et fin : le décor défile doucement, la pirogue flotte.
this.vitesse = DIFFICULTE.vitesseMenu;
this.fond.mettreAJour(dt, this.vitesse);
this.pirogue.mettreAJour(dt, 0, null);
}
// Particules : on met à jour, puis on GARDE seulement les vivantes (filter).
for (const p of this.particules) p.mettreAJour(dt);
this.particules = this.particules.filter(function (p) { return p.estVivante(); });
}
/* ----------------------------------------------------------
mettreAJourPartie(dt, entrees)
But : tout ce qui se passe pendant une partie.
---------------------------------------------------------- */
mettreAJourPartie(dt, entrees) {
this.temps += dt;
const d = this.difficulte();
// La vitesse de défilement augmente avec la difficulté :
// lerp(a, b, t) = a + (b - a) * t
this.vitesse = lerp(DIFFICULTE.vitesseMin, DIFFICULTE.vitesseMax, d);
this.fond.mettreAJour(dt, this.vitesse);
this.pirogue.mettreAJour(dt, entrees.direction, entrees.cibleX);
// --- Apparitions : un compte à rebours par type d'objet ---
this.chronoGousse -= dt;
if (this.chronoGousse <= 0) {
this.faireApparaitreGousses();
this.chronoGousse = lerp(DIFFICULTE.intervalleGousseMin, DIFFICULTE.intervalleGousseMax, d);
}
this.chronoObstacle -= dt;
if (this.chronoObstacle <= 0) {
this.faireApparaitreObstacle(d);
this.chronoObstacle = lerp(DIFFICULTE.intervalleObstacleMin, DIFFICULTE.intervalleObstacleMax, d);
}
// --- Mouvement de tous les objets ---
for (const g of this.gousses) g.mettreAJour(dt, this.vitesse);
for (const o of this.obstacles) o.mettreAJour(dt, this.vitesse);
this.gererCollisions();
// --- Nettoyage : on retire les objets ramassés, détruits ou sortis de l'écran ---
this.gousses = this.gousses.filter(function (g) { return !g.mort && !g.estSortie(); });
this.obstacles = this.obstacles.filter(function (o) { return !o.mort && !o.estSorti(); });
}
/* ----------------------------------------------------------
positionLibre(demiLargeur, ySpawn)
But : choisir une abscisse où l'on peut faire apparaître un objet
sans le superposer à un autre objet proche de la zone d'apparition.
Paramètres : demiLargeur = demi-largeur de l'objet ; ySpawn = son y d'apparition
Retour : un nombre (x) ou null si aucune place n'a été trouvée.
On tire jusqu'à 8 positions au hasard et on prend la première libre.
---------------------------------------------------------- */
positionLibre(demiLargeur, ySpawn) {
const xMin = RIVE + 10 + demiLargeur;
const xMax = LARGEUR_JEU - RIVE - 10 - demiLargeur;
const autres = this.gousses.concat(this.obstacles); // concat colle deux tableaux
for (let essai = 0; essai < 8; essai++) {
const x = random(xMin, xMax);
let libre = true;
for (const o of autres) {
const procheVerticalement = Math.abs(o.y - ySpawn) < 120;
const procheHorizontalement = Math.abs(o.x - x) < (o.rx + demiLargeur + 22);
if (procheVerticalement && procheHorizontalement) {
libre = false;
break; // inutile de tester les autres
}
}
if (libre) return x;
}
return null;
}
/* ----------------------------------------------------------
faireApparaitreGousses()
But : créer une file de 1 à 3 gousses alignées en haut de l'écran.
---------------------------------------------------------- */
faireApparaitreGousses() {
const x = this.positionLibre(16, -30);
if (x === null) {
this.chronoGousse = 0.2; // pas de place : on réessaie dans un instant
return;
}
const nombre = Math.floor(random(1, 4)); // 1, 2 ou 3
for (let i = 0; i < nombre; i++) {
const doree = random() < PROBA_DOREE; // random() : nombre entre 0 et 1
this.gousses.push(new Gousse(x, -30 - i * 55, doree));
}
}
/* ----------------------------------------------------------
faireApparaitreObstacle(d)
But : créer un rocher ou une vague (la part de vagues augmente avec d).
---------------------------------------------------------- */
faireApparaitreObstacle(d) {
const probaVague = lerp(DIFFICULTE.probaVagueMin, DIFFICULTE.probaVagueMax, d);
const type = (random() < probaVague) ? 'vague' : 'rocher';
const obstacle = new Obstacle(type, 0, -40);
const x = this.positionLibre(obstacle.rx, -40);
if (x === null) {
this.chronoObstacle = 0.2;
return;
}
obstacle.x = x;
this.obstacles.push(obstacle);
}
/* ----------------------------------------------------------
gererCollisions()
But : détecter les contacts pirogue/gousses et pirogue/obstacles.
---------------------------------------------------------- */
gererCollisions() {
const boitePirogue = this.pirogue.boite();
// Gousses : on les ramasse.
for (const g of this.gousses) {
if (!g.mort && collisionRect(boitePirogue, g.boite())) {
g.mort = true;
const points = g.doree ? POINTS_GOUSSE_DOREE : POINTS_GOUSSE;
this.score += points;
this.sons.jouer(g.doree ? 'succes' : 'piece');
Particule.gerbe(this.particules, g.x, g.y, g.doree ? 14 : 8,
g.doree ? [255, 215, 0] : [255, 255, 255], 160);
Particule.texteFlottant(this.particules, g.x, g.y - 12, '+' + points,
g.doree ? [255, 215, 0] : [255, 255, 255]);
}
}
// Obstacles : seulement si la pirogue n'est pas déjà invulnérable.
if (this.pirogue.invulnerable <= 0) {
for (const o of this.obstacles) {
if (!o.mort && collisionRect(boitePirogue, o.boite())) {
this.perdreVie(o);
break; // un seul choc par image
}
}
}
}
/* ----------------------------------------------------------
perdreVie(obstacle)
But : gérer un choc (vie en moins, effets, fin de partie éventuelle).
---------------------------------------------------------- */
perdreVie(obstacle) {
obstacle.mort = true;
this.vies -= 1;
this.pirogue.invulnerable = INVULNERABILITE;
this.secousse = 0.35;
this.sons.jouer('erreur');
Particule.gerbe(this.particules, this.pirogue.x, this.pirogue.y - 20, 18, [190, 235, 255], 240);
if (this.vies <= 0) {
Particule.gerbe(this.particules, this.pirogue.x, this.pirogue.y, 30, [190, 235, 255], 300);
this.finirPartie();
}
}
/* ==========================================================
SECTION 4 : entrées (appui souris/doigt, touche Entrée/Espace)
========================================================== */
/* ----------------------------------------------------------
appuyer(x, y)
But : réagir à un clic ou un toucher.
Paramètres : x, y = position dans le monde logique (360 x 640)
---------------------------------------------------------- */
appuyer(x, y) {
this.sons.activer(); // premier appui : on a le droit de préparer les sons
// Appui sur l'icône du son ?
if (dist(x, y, LARGEUR_JEU - 26, 66) < 24) {
this.sons.basculerMuet();
return;
}
this.actionPrincipale();
}
/* actionPrincipale() : lance ou relance la partie si l'état le permet. */
actionPrincipale() {
if (this.etat === ETATS.MENU) {
this.demarrerPartie();
} else if (this.etat === ETATS.FIN && this.tempsEtat > DELAI_REJOUER) {
this.demarrerPartie();
}
}
/* ==========================================================
SECTION 5 : dessin
========================================================== */
/* ----------------------------------------------------------
dessiner()
But : dessiner toute l'image, dans l'ordre (fond -> objets -> interface).
---------------------------------------------------------- */
dessiner() {
// Couleur de secours visible si la secousse décale l'image.
background(COULEURS.eauBas);
// Secousse d'écran : on décale TOUT le monde d'un petit montant aléatoire.
// push/pop limitent ce décalage au monde (l'interface reste stable).
push();
if (this.secousse > 0) {
const force = this.secousse * 14;
translate(random(-force, force), random(-force, force));
}
this.fond.dessiner();
for (const o of this.obstacles) o.dessiner();
for (const g of this.gousses) g.dessiner();
if (this.etat !== ETATS.FIN) this.pirogue.dessiner();
for (const p of this.particules) p.dessiner();
pop();
// Interface selon l'état
if (this.etat === ETATS.JEU) {
this.dessinerHUD();
} else if (this.etat === ETATS.MENU) {
this.dessinerMenu();
} else {
this.dessinerFin();
}
this.dessinerIconeSon();
}
/* ----------------------------------------------------------
dessinerHUD()
But : afficher score, vies, record et niveau pendant la partie.
HUD = "Head-Up Display" (l'affichage tête haute du jeu).
---------------------------------------------------------- */
dessinerHUD() {
noStroke();
fill(0, 0, 0, 90);
rect(0, 0, LARGEUR_JEU, 44); // bandeau sombre translucide
fill(COULEURS.texte);
textFont(POLICE);
textStyle(BOLD);
textSize(17);
textAlign(LEFT, CENTER);
text('Gousses : ' + this.score, 12, 22);
// Vies : un cœur plein par vie restante, un cœur vide sinon.
for (let i = 0; i < VIES_DEPART; i++) {
this.dessinerCoeur(168 + i * 26, 22, 20, i < this.vies);
}
textAlign(RIGHT, CENTER);
textSize(14);
textStyle(NORMAL);
fill(COULEURS.record);
text('Record : ' + this.record, LARGEUR_JEU - 12, 22);
// Niveau, sous le bandeau
fill(255, 255, 255, 200);
textAlign(LEFT, CENTER);
textSize(13);
text('Niveau ' + this.niveau(), 12, 60);
}
/* ----------------------------------------------------------
dessinerCoeur(x, y, taille, plein)
But : dessiner un cœur (2 cercles + 1 triangle).
Paramètres : x, y = centre ; taille ; plein = true (rouge) ou false (vide)
---------------------------------------------------------- */
dessinerCoeur(x, y, taille, plein) {
push();
translate(x, y);
noStroke();
fill(plein ? COULEURS.coeur : color(255, 255, 255, 70));
const s = taille;
circle(-s * 0.25, -s * 0.12, s * 0.55);
circle(s * 0.25, -s * 0.12, s * 0.55);
triangle(-s * 0.5, -s * 0.02, s * 0.5, -s * 0.02, 0, s * 0.5);
pop();
}
/* ----------------------------------------------------------
dessinerIconeSon()
But : petit haut-parleur (barré si le son est coupé) ; on peut
le toucher pour couper/remettre le son.
---------------------------------------------------------- */
dessinerIconeSon() {
push();
translate(LARGEUR_JEU - 26, 66);
noStroke();
fill(0, 0, 0, 90);
circle(0, 0, 36);
fill(255);
rect(-9, -4, 6, 8); // la base du haut-parleur
triangle(-3, -4, 5, -10, 5, 10); // le cône
triangle(-3, 4, -3, -4, 5, 10); // (complète la forme)
noFill();
stroke(255);
strokeWeight(2);
if (this.sons.muet) {
stroke(255, 120, 120);
line(-10, -10, 10, 10); // barre : son coupé
} else {
arc(5, 0, 10, 12, -HALF_PI, HALF_PI); // ondes sonores
arc(5, 0, 20, 22, -HALF_PI, HALF_PI);
}
pop();
}
/* ----------------------------------------------------------
dessinerBouton(cx, cy, l, h, libelle, actif)
But : un bouton rectangulaire arrondi (purement visuel : l'appui
est détecté sur tout l'écran).
Paramètres : cx, cy = centre ; l, h = taille ; libelle = texte ;
actif = false pour l'afficher grisé
---------------------------------------------------------- */
dessinerBouton(cx, cy, l, h, libelle, actif) {
// Léger effet de "respiration" pour attirer l'attention.
const pulse = actif ? 1 + 0.03 * Math.sin(millis() / 250) : 1;
push();
translate(cx, cy);
scale(pulse); // scale() agrandit / réduit le dessin
noStroke();
fill(actif ? COULEURS.bouton : color(160, 160, 160));
rect(-l / 2, -h / 2, l, h, 14);
fill(COULEURS.texteSombre);
textFont(POLICE);
textStyle(BOLD);
textSize(22);
textAlign(CENTER, CENTER);
text(libelle, 0, 1);
pop();
}
/* dessinerPanneau(x, y, l, h) : fond blanc translucide arrondi derrière les textes. */
dessinerPanneau(x, y, l, h) {
noStroke();
fill(255, 255, 255, 225);
rect(x, y, l, h, 18);
}
/* ----------------------------------------------------------
dessinerMenu()
But : écran d'accueil.
---------------------------------------------------------- */
dessinerMenu() {
this.dessinerPanneau(24, 90, LARGEUR_JEU - 48, 290);
fill(COULEURS.texteSombre);
textFont(POLICE);
textAlign(CENTER, CENTER);
textStyle(BOLD);
textSize(34);
text('Pirogue', LARGEUR_JEU / 2, 132);
textSize(26);
text("d'Antalaha", LARGEUR_JEU / 2, 168);
textStyle(NORMAL);
textSize(15);
text('Ramasse les gousses de vanille', LARGEUR_JEU / 2, 215);
text('et évite les rochers et les vagues.', LARGEUR_JEU / 2, 238);
textSize(14);
fill(60, 80, 100);
text('Flèches ou Q / D pour piloter', LARGEUR_JEU / 2, 280);
text('Sur mobile : glisse le doigt', LARGEUR_JEU / 2, 302);
fill(COULEURS.texteSombre);
textStyle(BOLD);
textSize(16);
text('Meilleur score : ' + this.record, LARGEUR_JEU / 2, 345);
this.dessinerBouton(LARGEUR_JEU / 2, 430, 190, 56, 'JOUER', true);
fill(255);
textStyle(NORMAL);
textSize(13);
textAlign(CENTER, CENTER);
text('Appuie, touche ou presse Entrée', LARGEUR_JEU / 2, 480);
}
/* ----------------------------------------------------------
dessinerFin()
But : écran de fin de partie.
---------------------------------------------------------- */
dessinerFin() {
fill(0, 0, 0, 80);
noStroke();
rect(0, 0, LARGEUR_JEU, HAUTEUR_JEU); // assombrit tout le décor
this.dessinerPanneau(24, 120, LARGEUR_JEU - 48, 280);
textFont(POLICE);
textAlign(CENTER, CENTER);
fill(COULEURS.texteSombre);
textStyle(BOLD);
textSize(30);
text('Fin de partie', LARGEUR_JEU / 2, 165);
textSize(20);
text(this.score + (this.score > 1 ? ' gousses ramassées' : ' gousse ramassée'), LARGEUR_JEU / 2, 220);
textSize(17);
if (this.nouveauRecord) {
fill('#b36b00');
text('Nouveau record !', LARGEUR_JEU / 2, 262);
} else {
fill(60, 80, 100);
textStyle(NORMAL);
text('Record : ' + this.record, LARGEUR_JEU / 2, 262);
}
const pret = this.tempsEtat > DELAI_REJOUER;
this.dessinerBouton(LARGEUR_JEU / 2, 340, 190, 56, 'REJOUER', pret);
}
}