1010 lines
No EOL
33 KiB
JavaScript
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);
|
|
}); |