Ajoute une difficulté progressive au Vol d'Icare
Build and deploy / deploy (push) Successful in 1m36s

À partir du score 10, la vitesse de défilement augmente et l'écart
entre les colonnes se resserre progressivement, jusqu'à un palier à
score 50 (vitesse ×1.6, écart réduit à 75%) pour ne pas devenir
injouable sur une longue partie. Le gap est maintenant stocké par
colonne (plutôt qu'une constante globale) pour que les colonnes déjà
en place ne changent pas de taille rétroactivement.
This commit is contained in:
Valentin ROBIN
2026-08-01 22:40:54 +02:00
parent 7c5ce9dcb9
commit 4e22001ae0
4 changed files with 43 additions and 13 deletions
+25 -7
View File
@@ -6,13 +6,17 @@ import {
COLUMN_GAP_MARGIN,
COLUMN_SPACING,
COLUMN_WIDTH,
DIFFICULTY_RAMP_SPAN,
DIFFICULTY_START_SCORE,
FIXED_DT_MS,
FLAP_IMPULSE,
FORWARD_SPEED,
GAP_MIN_MULTIPLIER,
GROUND_Y,
ICARUS_RADIUS,
ICARUS_X,
MAX_FRAME_DT_MS,
SPEED_MAX_MULTIPLIER,
TILT_MAX_RADIANS,
VIEWPORT_HEIGHT,
VIEWPORT_WIDTH,
@@ -29,9 +33,16 @@ type Status = "idle" | "flying" | "dead";
const CONFETTI_COLORS = ["#E7C560", "#C9A227", "#F4ECD8", "#A5342A", "#5E6B3B", "#2E6E7E", "#B08D57"];
function randomGapCenter(): number {
const minCenter = COLUMN_GAP / 2 + COLUMN_GAP_MARGIN;
const maxCenter = GROUND_Y - COLUMN_GAP / 2 - COLUMN_GAP_MARGIN;
// 0 avant DIFFICULTY_START_SCORE, monte jusqu'à 1 sur DIFFICULTY_RAMP_SPAN
// points, puis reste à 1 (palier, pour ne pas devenir injouable).
function difficultyProgress(score: number): number {
if (score <= DIFFICULTY_START_SCORE) return 0;
return Math.min(1, (score - DIFFICULTY_START_SCORE) / DIFFICULTY_RAMP_SPAN);
}
function randomGapCenter(gap: number): number {
const minCenter = gap / 2 + COLUMN_GAP_MARGIN;
const maxCenter = GROUND_Y - gap / 2 - COLUMN_GAP_MARGIN;
return minCenter + Math.random() * Math.max(0, maxCenter - minCenter);
}
@@ -112,8 +123,8 @@ function drawColumnShaft(
}
function drawColumn(ctx: CanvasRenderingContext2D, column: Column) {
const gapTop = column.gapCenterY - COLUMN_GAP / 2;
const gapBottom = column.gapCenterY + COLUMN_GAP / 2;
const gapTop = column.gapCenterY - column.gap / 2;
const gapBottom = column.gapCenterY + column.gap / 2;
drawColumnShaft(ctx, column.x, 0, gapTop, true);
drawColumnShaft(ctx, column.x, gapBottom, GROUND_Y - gapBottom, false);
}
@@ -203,8 +214,14 @@ export function IcarusGame({ onFinish }: { onFinish: (score: number) => void })
}
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 currentGap = COLUMN_GAP * (1 - (1 - GAP_MIN_MULTIPLIER) * progress);
for (const column of columnsRef.current) {
column.x -= FORWARD_SPEED * (FIXED_DT_MS / 1000);
column.x -= currentSpeed * (FIXED_DT_MS / 1000);
if (!column.scored && column.x + COLUMN_WIDTH < ICARUS_X - ICARUS_RADIUS) {
column.scored = true;
scoreRef.current += 1;
@@ -217,7 +234,8 @@ export function IcarusGame({ onFinish }: { onFinish: (score: number) => void })
if (!last || last.x < VIEWPORT_WIDTH - COLUMN_SPACING) {
columnsRef.current.push({
x: last ? last.x + COLUMN_SPACING : VIEWPORT_WIDTH,
gapCenterY: randomGapCenter(),
gapCenterY: randomGapCenter(currentGap),
gap: currentGap,
scored: false,
});
}
+12 -2
View File
@@ -27,12 +27,22 @@ export const WING_IDLE_PERIOD_MS = 480;
export const WING_FLAP_MAX_RADIANS = (75 * Math.PI) / 180;
export const WING_FLAP_ANIM_MS = 260;
export const FORWARD_SPEED = 130; // unités/seconde, vitesse de défilement des colonnes
export const FORWARD_SPEED = 130; // unités/seconde, vitesse de défilement des colonnes de base
export const COLUMN_WIDTH = 46;
export const COLUMN_GAP = 128;
export const COLUMN_GAP = 128; // écart de base entre les colonnes d'une paire
export const COLUMN_SPACING = 210;
export const COLUMN_GAP_MARGIN = 50; // distance mini entre le centre du trou et le sol/plafond
// Difficulté progressive : au-delà de DIFFICULTY_START_SCORE, la vitesse
// augmente et l'écart entre les colonnes se resserre progressivement,
// jusqu'à un palier atteint à DIFFICULTY_START_SCORE + DIFFICULTY_RAMP_SPAN
// (au-delà, la difficulté n'augmente plus — évite un jeu qui devient
// littéralement impossible sur une très longue partie).
export const DIFFICULTY_START_SCORE = 10;
export const DIFFICULTY_RAMP_SPAN = 40;
export const SPEED_MAX_MULTIPLIER = 1.6;
export const GAP_MIN_MULTIPLIER = 0.75;
export const FIXED_DT_MS = 1000 / 60;
export const MAX_FRAME_DT_MS = 250; // évite la "spirale de la mort" après un onglet en arrière-plan
+2 -3
View File
@@ -1,5 +1,4 @@
import {
COLUMN_GAP,
COLUMN_WIDTH,
GRAVITY,
GROUND_Y,
@@ -31,7 +30,7 @@ export function collidesWithColumn(icarusY: number, column: Column): boolean {
const overlapsX = columnRight > ICARUS_X - ICARUS_RADIUS && columnLeft < ICARUS_X + ICARUS_RADIUS;
if (!overlapsX) return false;
const gapTop = column.gapCenterY - COLUMN_GAP / 2;
const gapBottom = column.gapCenterY + COLUMN_GAP / 2;
const gapTop = column.gapCenterY - column.gap / 2;
const gapBottom = column.gapCenterY + column.gap / 2;
return icarusY - ICARUS_RADIUS < gapTop || icarusY + ICARUS_RADIUS > gapBottom;
}
+4 -1
View File
@@ -1,3 +1,6 @@
export type Column = { x: number; gapCenterY: number; scored: boolean };
// gap est stocké par colonne (pas une constante globale) : la difficulté
// progressive resserre l'écart des colonnes créées après un certain score,
// sans changer la forme des colonnes déjà en place.
export type Column = { x: number; gapCenterY: number; gap: number; scored: boolean };
export type IcarusState = { y: number; velocityY: number };