Projet_Web_IceBreaker/assets/js/cruise.js
2025-12-21 13:10:23 +01:00

1010 lines
No EOL
33 KiB
JavaScript

/*
* Le jeu principal
*/
'use strict';
const CONFIG = {
// gameplay
targetDistance: 300,
containerRows: 5,
containerCols: 4,
colors: ['#4fc3f7', '#ffd54f', '#ff8a80'],
// physique (pour 60fps)
acceleration: 0.6,
friction: 0.94,
maxSpeed: 10,
targetFPS: 60,
frameTime: 1000 / 60,
// difficulté
stabilityThreshold: 0.8,
proximityRange: 100,
collisionRange: 25,
// perf
baseMaxIcebergs: 6,
collisionCheckInterval: 3,
};
// dimensions fenêtre (cache pour éviter les reflows)
let viewportWidth = window.innerWidth;
let viewportHeight = window.innerHeight;
window.addEventListener('resize', () => {
viewportWidth = window.innerWidth;
viewportHeight = window.innerHeight;
}, { passive: true });
class Boat {
constructor(elementId, sfxCallback) {
this.el = document.getElementById(elementId);
this.deck = document.getElementById('container-deck');
this.sfxCallback = sfxCallback;
// dimensions du bateau
this.width = 50;
this.height = 220;
this.cachedBounds = null;
this.boundsNeedUpdate = true;
// position et vitesse
this.x = window.innerWidth / 2 - 60;
this.y = 20;
this.vx = 0;
this.lastVx = 0;
// détection zigzag (pour pénaliser les changements brusques)
this.lastDirection = 0;
this.lastChangeTime = 0;
this.ZIGZAG_TIME_WINDOW = 800;
// Containers
this.containers = [];
this.activeContainerCount = CONFIG.containerRows * CONFIG.containerCols;
this.initDeck();
// touches
this.keys = { up: false, down: false, left: false, right: false };
this.bindControls();
// mesurer une fois au début
requestAnimationFrame(() => {
if (this.el) {
this.width = this.el.offsetWidth || 50;
this.height = this.el.offsetHeight || 220;
}
});
}
initDeck() {
this.deck.innerHTML = '';
const fragment = document.createDocumentFragment();
for (let r = 0; r < CONFIG.containerRows; r++) {
const row = document.createElement('div');
row.className = 'container-row';
for (let c = 0; c < CONFIG.containerCols; c++) {
const cont = document.createElement('div');
cont.className = 'container';
cont.style.backgroundColor = CONFIG.colors[Math.floor(Math.random() * CONFIG.colors.length)];
this.containers.push({ el: cont, active: true });
row.appendChild(cont);
}
fragment.appendChild(row);
}
this.deck.appendChild(fragment);
}
bindControls() {
const handleKey = (e, isPressed) => {
const k = e.key.toLowerCase();
if (['arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(k)) {
e.preventDefault();
}
if (k === 'z' || k === 'arrowup') this.keys.up = isPressed;
if (k === 's' || k === 'arrowdown') this.keys.down = isPressed;
if (k === 'q' || k === 'arrowleft') this.keys.left = isPressed;
if (k === 'd' || k === 'arrowright') this.keys.right = isPressed;
};
window.addEventListener('keydown', (e) => handleKey(e, true));
window.addEventListener('keyup', (e) => handleKey(e, false));
}
checkZigzagConflict() {
const now = performance.now(); // Plus précis que Date.now()
let currentDirection = 0;
if (this.keys.left && !this.keys.right) currentDirection = -1;
else if (this.keys.right && !this.keys.left) currentDirection = 1;
if (currentDirection !== 0 && currentDirection !== this.lastDirection && this.lastDirection !== 0) {
if (now - this.lastChangeTime < this.ZIGZAG_TIME_WINDOW) {
this.loseContainer('input_conflict');
this.vx *= 0.3;
this.lastDirection = 0;
this.lastChangeTime = 0;
return true;
}
}
if (currentDirection !== 0 && currentDirection !== this.lastDirection) {
this.lastDirection = currentDirection;
this.lastChangeTime = now;
}
return false;
}
updatePhysics(deltaTime = 1) {
if (this.checkZigzagConflict()) {
const shake = 5 * Math.sin(performance.now() / 50);
this.el.style.transform = `translateX(${this.x}px) rotate(${shake}deg)`;
this.el.style.bottom = `${this.y}px`;
this.boundsNeedUpdate = true;
return;
}
this.lastVx = this.vx;
// Accélération (normalisée par deltaTime)
const accel = CONFIG.acceleration * deltaTime;
if (this.keys.right) this.vx = Math.min(this.vx + accel, CONFIG.maxSpeed);
if (this.keys.left) this.vx = Math.max(this.vx - accel, -CONFIG.maxSpeed);
// Mouvement vertical (normalisé par deltaTime)
const vStep = 5 * deltaTime;
if (this.keys.up) this.y = Math.min(this.y + vStep, viewportHeight - 200);
if (this.keys.down) this.y = Math.max(this.y - vStep, 0);
// Friction (ajustée pour deltaTime)
const frictionFactor = Math.pow(CONFIG.friction, deltaTime);
this.vx *= frictionFactor;
// Position
this.x += this.vx * deltaTime;
// Limites
const maxX = viewportWidth - this.width;
if (this.x < 0) { this.x = 0; this.vx *= -0.5; }
if (this.x > maxX) { this.x = maxX; this.vx *= -0.5; }
// Rotation visuelle - utiliser transform3d pour GPU acceleration
const tilt = this.vx * 2;
this.el.style.transform = `translate3d(${this.x}px, 0, 0) rotate(${tilt}deg)`;
this.el.style.bottom = `${this.y}px`;
this.boundsNeedUpdate = true;
}
checkStability() {
const gForce = Math.abs(this.vx - this.lastVx);
if (gForce > CONFIG.stabilityThreshold) {
const dropChance = (gForce - CONFIG.stabilityThreshold) * 0.15;
if (Math.random() < dropChance) {
const direction = (this.vx - this.lastVx) > 0 ? 'left' : 'right';
this.loseContainer(direction);
}
}
}
loseContainer(reason) {
if (this.activeContainerCount === 0) return false;
// Trouver le dernier container actif sans filter()
let target = null;
for (let i = this.containers.length - 1; i >= 0; i--) {
if (this.containers[i].active) {
target = this.containers[i];
break;
}
}
if (!target) return false;
target.active = false;
this.activeContainerCount--;
let dropClass;
if (reason === 'input_conflict') {
dropClass = Math.random() > 0.5 ? 'dropped-left' : 'dropped-right';
} else {
dropClass = reason === 'left' ? 'dropped-left' : 'dropped-right';
}
target.el.classList.add(dropClass);
if (this.sfxCallback) {
this.sfxCallback('drop', 0.9);
}
return true;
}
getBounds() {
// Utiliser le cache si disponible et valide
if (!this.boundsNeedUpdate && this.cachedBounds) {
return this.cachedBounds;
}
// Calculer les bounds basés sur la position connue
// On utilise les dimensions ORIGINALES du bateau (sans rotation)
// pour que la hitbox reste cohérente pendant les virages
const top = viewportHeight - this.y - this.height;
this.cachedBounds = {
left: this.x,
top: top,
right: this.x + this.width,
bottom: top + this.height,
width: this.width,
height: this.height,
centerX: this.x + this.width / 2,
centerY: top + this.height / 2
};
this.boundsNeedUpdate = false;
return this.cachedBounds;
}
getContainerCount() {
return this.activeContainerCount;
}
}
// images d'icebergs
const ICEBERG_IMAGE_COUNT = 12;
// précharger pour éviter le lag
const ICEBERG_IMAGES = [];
let icebergImagesLoaded = false;
function preloadIcebergImages() {
let loaded = 0;
for (let i = 1; i <= ICEBERG_IMAGE_COUNT; i++) {
const img = new Image();
img.src = `./assets/img/icebergs/iceberg-${i}.png`;
img.onload = () => {
loaded++;
if (loaded === ICEBERG_IMAGE_COUNT) {
icebergImagesLoaded = true;
console.log('images icebergs ok');
}
};
ICEBERG_IMAGES.push(img);
}
}
// Lancer le préchargement
preloadIcebergImages();
class IcebergGenerator {
constructor(containerId, difficulty = 1) {
this.container = document.getElementById(containerId);
this.list = [];
this.activeCount = 0;
this.pooledElements = [];
// max icebergs à l'écran (6 à 15)
this.maxIcebergs = Math.min(Math.floor(CONFIG.baseMaxIcebergs + difficulty * 2), 15);
}
// Récupérer ou créer un élément image
getPooledElement(size, imageIndex) {
// Chercher un élément dans le pool
const pooled = this.pooledElements.pop();
if (pooled) {
pooled.style.width = `${size}px`;
pooled.style.height = `${size}px`;
pooled.style.display = 'block';
pooled.src = ICEBERG_IMAGES[imageIndex % ICEBERG_IMAGE_COUNT].src;
return pooled;
}
// Créer un nouvel élément image
const el = document.createElement('img');
el.className = 'iceberg';
el.src = ICEBERG_IMAGES[imageIndex % ICEBERG_IMAGE_COUNT].src;
el.alt = 'Iceberg';
el.draggable = false;
return el;
}
// Recycler un élément au lieu de le supprimer
recycleElement(el) {
el.style.display = 'none';
if (this.pooledElements.length < 20) { // Limite du pool
this.pooledElements.push(el);
} else {
el.remove();
}
}
spawn(difficulty) {
// pas trop d'icebergs en même temps
if (this.activeCount >= this.maxIcebergs) return;
// attendre que les images soient là
if (!icebergImagesLoaded) return;
// taille selon difficulté
const baseSize = 70 + difficulty * 10;
const sizeVariation = 100 + difficulty * 20;
const size = Math.min(baseSize + Math.random() * sizeVariation, 300);
// trouver une position libre
const posX = this.findSafePosition(size);
if (posX === null) return;
// image aléatoire
const imageIndex = Math.floor(Math.random() * ICEBERG_IMAGE_COUNT);
const el = this.getPooledElement(size, imageIndex);
el.style.width = `${size}px`;
el.style.height = `${size}px`;
el.style.left = `${posX}px`;
el.style.top = '-150px';
if (!el.parentNode) {
this.container.appendChild(el);
}
this.activeCount++;
this.list.push({
el,
x: posX,
y: -150,
size,
speed: (2 + Math.random() * 2) * difficulty,
active: true,
inProximity: false
});
}
findSafePosition(newSize) {
const margin = 30;
const maxAttempts = 5;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const posX = Math.random() * (viewportWidth - newSize);
let isSafe = true;
// check collision avec les autres icebergs en haut
for (const ice of this.list) {
if (!ice.active) continue;
// Ne vérifier que les icebergs qui sont encore en haut de l'écran
if (ice.y > 200) continue;
// check horizontal
const iceLeft = ice.x;
const iceRight = ice.x + ice.size;
const newLeft = posX;
const newRight = posX + newSize;
// overlap avec marge?
if (!(newRight + margin < iceLeft || newLeft - margin > iceRight)) {
isSafe = false;
break;
}
}
if (isSafe) return posX;
}
return null; // Aucune position sûre trouvée
}
update(deltaTime = 1) {
let needsCleanup = false;
const list = this.list;
const len = list.length;
// Une seule boucle optimisée
for (let i = 0; i < len; i++) {
const ice = list[i];
if (!ice.active) continue;
// Mouvement normalisé par deltaTime
ice.y += ice.speed * deltaTime;
if (ice.y > viewportHeight) {
ice.active = false;
ice.el.style.display = 'none';
this.activeCount--;
needsCleanup = true;
} else {
// Mise à jour visuelle directe
ice.el.style.transform = `translate3d(0, ${ice.y + 150}px, 0)`;
}
}
// nettoyage si besoin
if (needsCleanup) {
// Recyclage des éléments inactifs
for (let i = 0; i < len; i++) {
const ice = list[i];
if (!ice.active && ice.el.style.display === 'none') {
if (this.pooledElements.length < 15) {
this.pooledElements.push(ice.el);
}
}
}
this.list = list.filter(i => i.active);
}
}
checkIcebergOverlap(ice1, ice2, margin = 0) {
// Collision rectangulaire avec marge
const left1 = ice1.x - margin;
const right1 = ice1.x + ice1.size + margin;
const top1 = ice1.y - margin;
const bottom1 = ice1.y + ice1.size + margin;
const left2 = ice2.x;
const right2 = ice2.x + ice2.size;
const top2 = ice2.y;
const bottom2 = ice2.y + ice2.size;
return !(right1 < left2 || left1 > right2 || bottom1 < top2 || top1 > bottom2);
}
}
class Game {
constructor() {
const params = new URLSearchParams(window.location.search);
this.difficulty = parseFloat(params.get('difficulty')) || 1;
this.score = parseInt(params.get('score')) || 0;
this.distance = CONFIG.targetDistance;
this.isRunning = false;
this.debugMode = false; // touche H pour activer
// suivi cargaison sur tous les niveaux
this.totalDelivered = parseInt(params.get('totalDelivered')) || 0;
this.totalPossible = parseInt(params.get('totalPossible')) || 0;
this.checkpoints = parseInt(params.get('checkpoints')) || 0;
this.initialContainers = CONFIG.containerRows * CONFIG.containerCols; // 20
// toggle debug
this.bindDebugControls();
// sons
this.sfx = {
drop: document.getElementById('sfx-drop'),
alert: document.getElementById('sfx-alert'),
break: document.getElementById('sfx-break'),
bgm: document.getElementById('background-audio')
};
// Volume settings
this.masterVolume = 0.8;
this.loadVolumeSettings();
// le bateau
this.boat = new Boat('player-boat', this.playSound.bind(this));
this.icebergs = new IcebergGenerator('iceberg-layer', this.difficulty);
this.lastSpawn = 0;
this.spawnRate = 1000 / this.difficulty;
this.damageCooldown = 0;
// deltaTime
this.lastFrameTime = 0;
this.frameCount = 0;
this.accumulatedTime = 0;
// UI
this.ui = {
dist: document.getElementById('dist-display'),
score: document.getElementById('score-display'),
cont: document.getElementById('cont-display'),
captainName: document.getElementById('captain-name-display')
};
// limiter les updates UI
this.lastUIUpdate = 0;
this.uiUpdateInterval = 200;
this.distanceInterval = null;
this.loop = this.loop.bind(this);
// nom capitaine
this.displayCaptainName();
// audio en jeu
this.initGameAudioControls();
}
loadVolumeSettings() {
if (typeof IceBreakerStorage !== 'undefined') {
const settings = IceBreakerStorage.getSettings();
this.masterVolume = settings.masterVolume || 0.8;
}
}
displayCaptainName() {
if (this.ui.captainName && typeof IceBreakerStorage !== 'undefined') {
const name = IceBreakerStorage.getCaptainName();
if (name) {
this.ui.captainName.textContent = `Cap. ${name}`;
}
}
}
initGameAudioControls() {
const toggleBtn = document.getElementById('audio-toggle-game');
const volumeSlider = document.getElementById('volume-slider-game');
if (volumeSlider) {
volumeSlider.value = Math.round(this.masterVolume * 100);
volumeSlider.addEventListener('input', (e) => {
this.masterVolume = parseInt(e.target.value) / 100;
this.applyVolume();
if (typeof IceBreakerStorage !== 'undefined') {
IceBreakerStorage.saveSettings({ masterVolume: this.masterVolume });
}
});
}
if (toggleBtn) {
toggleBtn.addEventListener('click', () => {
if (this.sfx.bgm) {
if (this.sfx.bgm.paused) {
this.sfx.bgm.play();
toggleBtn.querySelector('.audio-icon').textContent = '🔊';
} else {
this.sfx.bgm.pause();
toggleBtn.querySelector('.audio-icon').textContent = '🔇';
}
}
});
}
}
applyVolume() {
if (this.sfx.bgm) {
this.sfx.bgm.volume = this.masterVolume * 0.9;
}
}
preloadSfx() {
['drop', 'alert', 'break'].forEach(name => {
const audio = this.sfx[name];
if (audio) {
audio.volume = 0;
audio.play().then(() => {
audio.pause();
audio.currentTime = 0;
}).catch(() => { });
}
});
}
playSound(sfxName, volume = 1.0) {
const audio = this.sfx[sfxName];
if (audio && this.isRunning) {
audio.currentTime = 0;
audio.volume = this.masterVolume * volume;
audio.play().catch(() => { });
}
}
startGame() {
if (this.isRunning) return;
this.isRunning = true;
this.preloadSfx();
this.applyVolume();
this.startGameLoop();
this.startDistanceInterval();
}
startGameLoop() {
requestAnimationFrame(this.loop);
}
startDistanceInterval() {
// toutes les 200ms
this.distanceInterval = setInterval(() => {
if (this.isRunning && this.distance > 0) {
this.distance--;
this.score += Math.ceil(this.difficulty);
// UI update géré dans la boucle principale
if (this.distance <= 0) this.win();
}
}, 200);
}
loop(timestamp) {
if (!this.isRunning) return;
// deltaTime (normalisé 60fps)
if (this.lastFrameTime === 0) {
this.lastFrameTime = timestamp;
requestAnimationFrame(this.loop);
return;
}
const rawDelta = timestamp - this.lastFrameTime;
this.lastFrameTime = timestamp;
// limiter pour éviter les sauts quand on change d'onglet
const clampedDelta = Math.min(rawDelta, 33);
const deltaTime = clampedDelta / CONFIG.frameTime;
this.frameCount++;
// physique
this.boat.updatePhysics(deltaTime);
// stabilité (pas à chaque frame)
if (this.frameCount % 4 === 0) {
this.boat.checkStability();
}
// spawn icebergs
if (timestamp - this.lastSpawn > this.spawnRate) {
this.icebergs.spawn(this.difficulty);
this.lastSpawn = timestamp;
this.spawnRate = (1000 + Math.random() * 500) / this.difficulty;
}
// update icebergs
this.icebergs.update(deltaTime);
// collisions (toutes les 3 frames)
if (this.frameCount % 3 === 0) {
this.checkCollisions();
}
// UI (pas trop souvent)
if (timestamp - this.lastUIUpdate > this.uiUpdateInterval) {
this.updateUI();
this.lastUIUpdate = timestamp;
}
// debug hitboxes (coûte cher)
if (this.debugMode && this.frameCount % 6 === 0) {
this.updateDebugVisuals();
}
requestAnimationFrame(this.loop);
}
// --- Debug (touche H) ---
bindDebugControls() {
window.addEventListener('keydown', (e) => {
if (e.key === 'h' || e.key === 'H') {
this.debugMode = !this.debugMode;
console.log('Debug mode:', this.debugMode ? 'ON' : 'OFF');
if (!this.debugMode) {
// Supprimer toutes les hitboxes
document.querySelectorAll('.debug-hitbox').forEach(el => el.remove());
}
}
});
}
updateDebugVisuals() {
// virer les anciens
document.querySelectorAll('.debug-hitbox').forEach(el => el.remove());
// hitbox bateau
this.drawBoatHitbox();
// hitbox icebergs
for (const ice of this.icebergs.list) {
if (ice.active) {
this.drawIcebergHitbox(ice);
}
}
}
drawBoatHitbox() {
const boatRect = this.boat.getBounds();
const turbulenceMargin = 30;
const tilt = this.boat.vx * 2; // Même rotation que le bateau visuel
// Zone de collision (rectangle = taille du bateau)
const collisionHitbox = document.createElement('div');
collisionHitbox.className = 'debug-hitbox';
collisionHitbox.style.cssText = `
position: fixed;
left: ${boatRect.left}px;
top: ${boatRect.top}px;
width: ${boatRect.width}px;
height: ${boatRect.height}px;
border: 3px solid #00ff00;
background: rgba(0, 255, 0, 0.15);
pointer-events: none;
z-index: 9999;
box-sizing: border-box;
transform: rotate(${tilt}deg);
transform-origin: center bottom;
`;
document.body.appendChild(collisionHitbox);
// Zone de turbulence (rectangle 30px plus large)
const turbulenceHitbox = document.createElement('div');
turbulenceHitbox.className = 'debug-hitbox';
turbulenceHitbox.style.cssText = `
position: fixed;
left: ${boatRect.left - turbulenceMargin}px;
top: ${boatRect.top - turbulenceMargin}px;
width: ${boatRect.width + turbulenceMargin * 2}px;
height: ${boatRect.height + turbulenceMargin * 2}px;
border: 2px dashed #ffff00;
background: rgba(255, 255, 0, 0.05);
pointer-events: none;
z-index: 9998;
box-sizing: border-box;
transform: rotate(${tilt}deg);
transform-origin: center bottom;
`;
document.body.appendChild(turbulenceHitbox);
// Label
const label = document.createElement('div');
label.className = 'debug-hitbox';
label.style.cssText = `
position: fixed;
left: ${boatRect.right + 10}px;
top: ${boatRect.top}px;
color: #00ff00;
font-size: 12px;
font-family: monospace;
background: rgba(0,0,0,0.7);
padding: 2px 6px;
pointer-events: none;
z-index: 10000;
`;
label.textContent = `BOAT ${Math.round(boatRect.width)}x${Math.round(boatRect.height)}`;
document.body.appendChild(label);
}
drawIcebergHitbox(ice) {
const centerX = ice.x + ice.size / 2;
const centerY = ice.y + ice.size / 2;
const radius = ice.size / 2;
// Hitbox de l'iceberg (cercle)
const hitbox = document.createElement('div');
hitbox.className = 'debug-hitbox';
hitbox.style.cssText = `
position: fixed;
left: ${centerX - radius}px;
top: ${centerY - radius}px;
width: ${radius * 2}px;
height: ${radius * 2}px;
border: 2px solid ${ice.inProximity ? '#ff0000' : '#ff6600'};
border-radius: 50%;
background: rgba(255, ${ice.inProximity ? '0' : '102'}, 0, 0.15);
pointer-events: none;
z-index: 9997;
box-sizing: border-box;
`;
document.body.appendChild(hitbox);
}
checkCollisions() {
const boatRect = this.boat.getBounds();
const turbulenceMargin = 30;
const now = performance.now();
const icebergList = this.icebergs.list;
const listLength = icebergList.length;
if (listLength === 0) return;
// valeurs bateau (une fois)
const boatLeft = boatRect.left;
const boatTop = boatRect.top;
const boatRight = boatRect.right;
const boatBottom = boatRect.bottom;
const boatCenterX = boatRect.centerX;
// zone turbulence
const turbLeft = boatLeft - turbulenceMargin;
const turbTop = boatTop - turbulenceMargin;
const turbRight = boatRight + turbulenceMargin;
const turbBottom = boatBottom + turbulenceMargin;
for (let i = 0; i < listLength; i++) {
const ice = icebergList[i];
if (!ice.active) continue;
// Early exit: ignorer les icebergs trop loin verticalement
const iceBottom = ice.y + ice.size;
if (iceBottom < turbTop - 50 || ice.y > turbBottom + 50) continue;
// Pré-calcul des valeurs iceberg
const halfSize = ice.size * 0.5;
const iceCenterX = ice.x + halfSize;
const iceCenterY = ice.y + halfSize;
// Early exit: ignorer les icebergs trop loin horizontalement
if (iceCenterX + halfSize < turbLeft - 20 || iceCenterX - halfSize > turbRight + 20) continue;
// Collision avec le bateau (test simplifié AABB d'abord)
if (iceCenterX + halfSize > boatLeft && iceCenterX - halfSize < boatRight &&
iceCenterY + halfSize > boatTop && iceCenterY - halfSize < boatBottom) {
this.playSound('break', 1.0);
this.gameOver();
return;
}
// Zone de turbulence (test simplifié AABB)
if (iceCenterX + halfSize > turbLeft && iceCenterX - halfSize < turbRight &&
iceCenterY + halfSize > turbTop && iceCenterY - halfSize < turbBottom) {
if (!ice.inProximity) {
this.playSound('alert', 0.4);
ice.inProximity = true;
}
if (now - this.damageCooldown > 500) {
const dropDirection = (iceCenterX < boatCenterX) ? 'right' : 'left';
if (this.boat.loseContainer(dropDirection)) {
this.damageCooldown = now;
}
}
} else {
ice.inProximity = false;
}
}
}
// Collision entre un rectangle et un cercle
checkRectCircleCollision(rectX, rectY, rectW, rectH, circleX, circleY, circleR) {
// Trouver le point le plus proche du cercle sur le rectangle
const closestX = Math.max(rectX, Math.min(circleX, rectX + rectW));
const closestY = Math.max(rectY, Math.min(circleY, rectY + rectH));
// Calculer la distance entre ce point et le centre du cercle
const distX = circleX - closestX;
const distY = circleY - closestY;
const distanceSquared = distX * distX + distY * distY;
return distanceSquared < (circleR * circleR);
}
updateUI() {
if (this.ui.dist) this.ui.dist.textContent = `${this.distance} km`;
if (this.ui.score) this.ui.score.textContent = this.score.toLocaleString();
if (this.ui.cont) this.ui.cont.textContent = this.boat.getContainerCount();
}
gameOver() {
this.isRunning = false;
if (this.distanceInterval) clearInterval(this.distanceInterval);
// Calculer les totaux pour ce game over (0 containers livrés ce niveau)
const containersThisLevel = 0;
const totalDelivered = this.totalDelivered + containersThisLevel;
const totalPossible = this.totalPossible + this.initialContainers;
const distanceTraveled = CONFIG.targetDistance - this.distance;
setTimeout(() => {
this.showGameOverModal(this.score, totalDelivered, totalPossible, distanceTraveled);
}, 300);
}
showGameOverModal(score, totalDelivered, totalPossible, distance) {
const modal = document.getElementById('gameover-modal');
const deliveryRate = totalPossible > 0 ? Math.round((totalDelivered / totalPossible) * 100) : 0;
if (!modal) {
// Fallback si la modal n'existe pas
alert("💥 GAME OVER!\n\nScore final: " + score.toLocaleString());
window.location.href = `index.html?finalScore=${score}&finalDifficulty=${this.difficulty}&totalDelivered=${totalDelivered}&totalPossible=${totalPossible}&checkpoints=${this.checkpoints}`;
return;
}
// Mettre à jour les statistiques
const scoreEl = document.getElementById('gameover-score');
const containersEl = document.getElementById('gameover-containers');
const distanceEl = document.getElementById('gameover-distance');
if (scoreEl) scoreEl.textContent = score.toLocaleString();
if (containersEl) containersEl.textContent = `${deliveryRate}% livré`;
if (distanceEl) distanceEl.textContent = `${distance} km`;
// Afficher la modal
modal.classList.remove('hidden');
// Gérer le bouton retour
const homeBtn = document.getElementById('gameover-home-btn');
if (homeBtn) {
homeBtn.onclick = () => {
window.location.href = `index.html?finalScore=${score}&finalDifficulty=${this.difficulty}&totalDelivered=${totalDelivered}&totalPossible=${totalPossible}&checkpoints=${this.checkpoints}`;
};
}
}
win() {
this.isRunning = false;
if (this.distanceInterval) clearInterval(this.distanceInterval);
const containers = this.boat.getContainerCount();
// Passer les données cumulatives au checkpoint
window.location.href = `checkpoint.html?score=${this.score}&difficulty=${this.difficulty}&containers=${containers}&totalDelivered=${this.totalDelivered}&totalPossible=${this.totalPossible}&checkpoints=${this.checkpoints}`;
}
}
// --- Démarrage ---
function setupAudioPrompt(gameInstance) {
const modal = document.getElementById('audio-prompt-modal');
const allowBtn = document.getElementById('allow-audio-button');
const skipBtn = document.getElementById('skip-audio-button');
if (!modal) {
gameInstance.startGame();
return;
}
// vérifier si déjà choisi
const settings = typeof IceBreakerStorage !== 'undefined'
? IceBreakerStorage.getSettings()
: null;
// si déjà choisi, on applique direct
if (settings && typeof settings.audioPromptAnswered !== 'undefined') {
modal.classList.add('hidden');
if (settings.musicEnabled) {
const bgm = gameInstance.sfx.bgm;
if (bgm) {
bgm.volume = gameInstance.masterVolume * 0.6;
bgm.play().catch(() => { });
}
}
gameInstance.startGame();
return;
}
const startWithAudio = () => {
// sauver le choix
if (typeof IceBreakerStorage !== 'undefined') {
IceBreakerStorage.saveSettings({
audioPromptAnswered: true,
musicEnabled: true
});
}
const bgm = gameInstance.sfx.bgm;
if (bgm) {
bgm.volume = gameInstance.masterVolume * 0.6;
bgm.play().catch(() => { });
}
modal.classList.add('hidden');
gameInstance.startGame();
};
const startWithoutAudio = () => {
// sauver le choix
if (typeof IceBreakerStorage !== 'undefined') {
IceBreakerStorage.saveSettings({
audioPromptAnswered: true,
musicEnabled: false
});
}
modal.classList.add('hidden');
gameInstance.startGame();
};
allowBtn?.addEventListener('click', startWithAudio);
skipBtn?.addEventListener('click', startWithoutAudio);
}
// Démarrage
window.addEventListener('DOMContentLoaded', () => {
const game = new Game();
setupAudioPrompt(game);
});