Rend le boost du Vol d'Icare aléatoire et incontrôlable
Build and deploy / deploy (push) Successful in 35s

Le nombre de charges (colonnes cassables) est désormais tiré entre 3 et 8
à chaque cueillette, au lieu d'un nombre fixe. Pendant que le bouclier est
actif, Icare est immobile (gravité coupée, taps/barre d'espace sans
effet) et le défilement accéléré (×1,7) : impossible d'agir sur la
trajectoire jusqu'à épuisement des charges, où le contrôle reprend.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Valentin ROBIN
2026-08-23 23:38:42 +02:00
parent b6a025c23f
commit bdeeb68845
3 changed files with 40 additions and 12 deletions
+33 -10
View File
@@ -2,12 +2,14 @@
import { useCallback, useEffect, useRef, useState } from "react";
import {
BOOST_CHARGES,
BOOST_DASH_DISTANCE,
BOOST_DASH_MS,
BOOST_MAX_CHARGES,
BOOST_MAX_COLUMN_INTERVAL,
BOOST_MIN_CHARGES,
BOOST_MIN_COLUMN_INTERVAL,
BOOST_RADIUS,
BOOST_SPEED_MULTIPLIER,
COLUMN_GAP,
COLUMN_GAP_MARGIN,
COLUMN_SPACING,
@@ -68,6 +70,12 @@ function randomBoostInterval(): number {
);
}
// Nombre de colonnes cassables par ce boost, tiré à chaque cueillette
// (tirage entier inclusif des deux bornes).
function randomBoostCharges(): number {
return BOOST_MIN_CHARGES + Math.floor(Math.random() * (BOOST_MAX_CHARGES - BOOST_MIN_CHARGES + 1));
}
// Une aile, dessinée pointant vers -x ; side=-1 la reflète pour l'aile
// opposée. scale(side,1) inverse déjà le sens de rotation pour le côté
// reflété — passer le même wingAngle (sans le renverser à la main) aux deux
@@ -342,17 +350,28 @@ export function IcarusGame({ onFinish }: { onFinish: (score: number) => void })
flapRequestedRef.current = false;
while (accumulator >= FIXED_DT_MS) {
if (shouldFlap) {
icarusRef.current = { ...icarusRef.current, velocityY: FLAP_IMPULSE };
lastFlapAtRef.current = now;
// Tant que le bouclier est actif, Icare est immobile (pas de
// gravité, les taps n'ont aucun effet) et le décor défile vite à
// sa place — impossible de toucher au mouvement pendant cette
// période.
if (boostChargesRef.current > 0) {
shouldFlap = false;
} else {
if (shouldFlap) {
icarusRef.current = { ...icarusRef.current, velocityY: FLAP_IMPULSE };
lastFlapAtRef.current = now;
shouldFlap = false;
}
icarusRef.current = stepIcarus(icarusRef.current, FIXED_DT_MS);
}
icarusRef.current = stepIcarus(icarusRef.current, FIXED_DT_MS);
// Difficulté progressive : vitesse en hausse et écart resserré au
// fil du score, jusqu'à un palier (voir difficultyProgress).
const progress = difficultyProgress(scoreRef.current);
const currentSpeed = FORWARD_SPEED * (1 + (SPEED_MAX_MULTIPLIER - 1) * progress);
const baseSpeed = FORWARD_SPEED * (1 + (SPEED_MAX_MULTIPLIER - 1) * progress);
// Défilement accéléré tant que le bouclier du boost est actif
// (sans risque : une colonne touchée casse au lieu de tuer).
const currentSpeed = boostChargesRef.current > 0 ? baseSpeed * BOOST_SPEED_MULTIPLIER : baseSpeed;
const currentGap = COLUMN_GAP * (1 - (1 - GAP_MIN_MULTIPLIER) * progress);
for (const column of columnsRef.current) {
@@ -399,12 +418,16 @@ export function IcarusGame({ onFinish }: { onFinish: (score: number) => void })
const collectedIndex = boostsRef.current.findIndex((b) => collidesWithBoost(icarusRef.current.y, b));
if (collectedIndex !== -1) {
boostsRef.current.splice(collectedIndex, 1);
// Active un bouclier de BOOST_CHARGES charges (voir plus bas) et
// déclenche l'animation de bond en avant — la casse des
// Active un bouclier d'un nombre aléatoire de charges (voir plus
// bas) et déclenche l'animation de bond en avant — la casse des
// colonnes ne se fait plus d'un coup à la cueillette, elle se
// fait au contact, une par une, tant qu'il reste des charges.
boostChargesRef.current = BOOST_CHARGES;
// Icare se fige (vitesse verticale annulée, plus de gravité ni
// de prise en compte des taps tant que dure le bouclier, voir
// plus haut).
boostChargesRef.current = randomBoostCharges();
boostDashAtRef.current = now;
icarusRef.current = { ...icarusRef.current, velocityY: 0 };
}
// Colonnes réellement touchées (mortelles en temps normal). Tant
@@ -437,7 +460,7 @@ export function IcarusGame({ onFinish }: { onFinish: (score: number) => void })
}
}
if (hasHitGround(icarusRef.current)) {
if (boostChargesRef.current === 0 && hasHitGround(icarusRef.current)) {
statusRef.current = "dead";
setStatus("dead");
onFinishRef.current(scoreRef.current);
+6 -1
View File
@@ -62,7 +62,12 @@ export const BOOST_COLLECT_RADIUS = 34;
// que 1 à 2, mais une partie plus longue peut légitimement en voir plus.
export const BOOST_MIN_COLUMN_INTERVAL = 15;
export const BOOST_MAX_COLUMN_INTERVAL = 25;
export const BOOST_CHARGES = 5;
export const BOOST_MIN_CHARGES = 3;
export const BOOST_MAX_CHARGES = 8;
// Vitesse de défilement multipliée tant qu'il reste des charges (sans
// risque : une colonne touchée casse au lieu de tuer pendant cette
// période) — renforce la sensation de vitesse du bouclier actif.
export const BOOST_SPEED_MULTIPLIER = 1.7;
// Animation de cueillette : Icare fait un bond en avant (aller-retour,
// pic à mi-parcours) plutôt que le halo statique d'avant.
export const BOOST_DASH_MS = 350;