24/12
32
index.html
@@ -7,6 +7,11 @@
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-fullscreen">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<!-- Force landscape orientation for PWA -->
|
||||
<meta name="screen-orientation" content="landscape">
|
||||
<meta name="x5-orientation" content="landscape">
|
||||
<meta name="full-screen" content="yes">
|
||||
<meta name="x5-fullscreen" content="true">
|
||||
|
||||
<title>Mario Runner - Jeu Mobile</title>
|
||||
<link rel="shortcut icon" href="/icons/favicon.ico">
|
||||
@@ -47,33 +52,6 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Forcer l'orientation paysage */
|
||||
@media screen and (orientation: portrait) {
|
||||
#game-container::before {
|
||||
content: '↻';
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 80px;
|
||||
color: white;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
#game-container::after {
|
||||
content: 'Veuillez tourner votre téléphone en mode paysage';
|
||||
position: fixed;
|
||||
top: 60%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
padding: 0 20px;
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
|
||||
BIN
public/assets/audio/chien.mp3
Normal file
BIN
public/assets/images/user1.jpg
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
public/assets/images/user1.png
Executable file
|
After Width: | Height: | Size: 173 KiB |
BIN
public/assets/images/user2.jpg
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
public/assets/images/user2.png
Executable file
|
After Width: | Height: | Size: 101 KiB |
BIN
public/assets/sprites/decor/b_130.png
Executable file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/assets/sprites/decor/b_140.png
Executable file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/assets/sprites/decor/b_179.png
Executable file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
public/assets/sprites/decor/b_40.png
Executable file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/assets/sprites/decor/b_73.png
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/assets/sprites/decor/gift_40.png
Executable file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
public/assets/sprites/decor/sol_150.png
Executable file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/assets/sprites/decor/star_40.png
Executable file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/assets/sprites/dog/dog.xcf
Executable file
BIN
public/assets/sprites/dog/idle_01.png
Executable file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
public/assets/sprites/dog/idle_02.png
Executable file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/assets/sprites/dog/idle_03.png
Executable file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
public/assets/sprites/dog/idle_04.png
Executable file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
public/assets/sprites/dog/turn_01.png
Executable file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
public/assets/sprites/dog/turn_02.png
Executable file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
public/assets/sprites/dog/turn_03.png
Executable file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
public/assets/sprites/dog/walkback_01.png
Executable file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
public/assets/sprites/dog/walkback_02.png
Executable file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
public/assets/sprites/dog/walkback_03.png
Executable file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
public/assets/sprites/dog/walkback_04.png
Executable file
|
After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 14 KiB |
BIN
public/assets/sprites/user1/image finale1.jpeg
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
public/assets/sprites/user1/jump_1.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/assets/sprites/user1/jump_2.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/assets/sprites/user1/jump_3.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/assets/sprites/user1/jump_4.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/assets/sprites/user1/jump_5.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/assets/sprites/user1/player_spritesheet.png
Executable file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/assets/sprites/user1/user1.xcf
Executable file
BIN
public/assets/sprites/user1/walk_1.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
public/assets/sprites/user1/walk_2.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/assets/sprites/user1/walk_3.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/assets/sprites/user1/walk_4.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/assets/sprites/user1_backup/jump_1.png
Executable file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/assets/sprites/user1_backup/jump_2.png
Executable file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/assets/sprites/user1_backup/jump_3.png
Executable file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/assets/sprites/user1_backup/jump_4.png
Executable file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/assets/sprites/user1_backup/jump_5.png
Executable file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/assets/sprites/user1_backup/walk_1.png
Executable file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/assets/sprites/user1_backup/walk_2.png
Executable file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/assets/sprites/user1_backup/walk_3.png
Executable file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/assets/sprites/user1_backup/walk_4.png
Executable file
|
After Width: | Height: | Size: 23 KiB |
BIN
public/assets/sprites/user2/image finale2.jpeg
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
public/assets/sprites/user2/jump_1.png
Executable file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/assets/sprites/user2/jump_2.png
Executable file
|
After Width: | Height: | Size: 23 KiB |
BIN
public/assets/sprites/user2/jump_3.png
Executable file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/assets/sprites/user2/jump_4.png
Executable file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/assets/sprites/user2/jump_5.png
Executable file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/assets/sprites/user2/player_spritesheet.png
Executable file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/assets/sprites/user2/user2.xcf
Executable file
BIN
public/assets/sprites/user2/walk_1.png
Executable file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/assets/sprites/user2/walk_2.png
Executable file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/assets/sprites/user2/walk_3.png
Executable file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/assets/sprites/user2/walk_4.png
Executable file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/assets/sprites/user2_backup/jump_1.png
Executable file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/assets/sprites/user2_backup/jump_2.png
Executable file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/assets/sprites/user2_backup/jump_3.png
Executable file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/assets/sprites/user2_backup/jump_4.png
Executable file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/assets/sprites/user2_backup/jump_5.png
Executable file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/assets/sprites/user2_backup/player_spritesheet.png
Executable file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
public/assets/sprites/user2_backup/walk_1.png
Executable file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/assets/sprites/user2_backup/walk_2.png
Executable file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/assets/sprites/user2_backup/walk_3.png
Executable file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/assets/sprites/user2_backup/walk_4.png
Executable file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB |
BIN
public/assets/video/zoe cadeaux1.mp4
Executable file
@@ -4,7 +4,7 @@
|
||||
"description": "Jeu de plateforme mobile avec contrôle gyroscope",
|
||||
"start_url": "/",
|
||||
"display": "fullscreen",
|
||||
"orientation": "landscape",
|
||||
"orientation": "landscape-primary",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#4CAF50",
|
||||
"icons": [
|
||||
|
||||
88
resize-user1-sprites.cjs
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script pour redimensionner les sprites user1 à 93x224px (même taille que user2)
|
||||
* Utilise le package sharp pour le traitement d'images
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Vérifier si sharp est disponible
|
||||
let sharp;
|
||||
try {
|
||||
sharp = require('sharp');
|
||||
} catch (err) {
|
||||
console.log('❌ Le package "sharp" n\'est pas installé.');
|
||||
console.log('📦 Installation en cours...');
|
||||
const { execSync } = require('child_process');
|
||||
try {
|
||||
execSync('npm install --no-save sharp', { stdio: 'inherit' });
|
||||
sharp = require('sharp');
|
||||
console.log('✅ Sharp installé avec succès!\n');
|
||||
} catch (installErr) {
|
||||
console.error('❌ Impossible d\'installer sharp. Veuillez l\'installer manuellement:');
|
||||
console.error(' npm install sharp');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const USER1_DIR = path.join(__dirname, 'public/assets/sprites/user1');
|
||||
const BACKUP_DIR = path.join(__dirname, 'public/assets/sprites/user1_backup');
|
||||
const TARGET_WIDTH = 93;
|
||||
const TARGET_HEIGHT = 224;
|
||||
|
||||
async function resizeImages() {
|
||||
try {
|
||||
// Créer un backup
|
||||
if (!fs.existsSync(BACKUP_DIR)) {
|
||||
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
||||
console.log('📁 Dossier de backup créé:', BACKUP_DIR);
|
||||
}
|
||||
|
||||
// Lire tous les fichiers PNG sauf le spritesheet
|
||||
const files = fs.readdirSync(USER1_DIR)
|
||||
.filter(f => f.endsWith('.png') && f !== 'player_spritesheet.png');
|
||||
|
||||
console.log(`\n🖼️ Redimensionnement de ${files.length} sprites vers ${TARGET_WIDTH}x${TARGET_HEIGHT}px...\n`);
|
||||
|
||||
for (const file of files) {
|
||||
const inputPath = path.join(USER1_DIR, file);
|
||||
const backupPath = path.join(BACKUP_DIR, file);
|
||||
|
||||
// Backup de l'original
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
fs.copyFileSync(inputPath, backupPath);
|
||||
}
|
||||
|
||||
// Lire les métadonnées de l'image
|
||||
const metadata = await sharp(inputPath).metadata();
|
||||
|
||||
console.log(`📏 ${file}`);
|
||||
console.log(` ${metadata.width}x${metadata.height} → ${TARGET_WIDTH}x${TARGET_HEIGHT}`);
|
||||
|
||||
// Redimensionner avec fit: 'contain' pour préserver le ratio et ajouter fond transparent
|
||||
await sharp(inputPath)
|
||||
.resize(TARGET_WIDTH, TARGET_HEIGHT, {
|
||||
kernel: 'lanczos3',
|
||||
fit: 'contain',
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||
})
|
||||
.toFile(inputPath + '.tmp');
|
||||
|
||||
// Remplacer l'original
|
||||
fs.renameSync(inputPath + '.tmp', inputPath);
|
||||
console.log(` ✅ Redimensionné\n`);
|
||||
}
|
||||
|
||||
console.log('✨ Toutes les images ont été redimensionnées avec succès!');
|
||||
console.log(`💾 Les originaux sont sauvegardés dans: ${BACKUP_DIR}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Exécuter
|
||||
resizeImages();
|
||||
88
resize-user2-sprites.cjs
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script pour redimensionner les sprites user2 de 25%
|
||||
* Utilise le package sharp pour le traitement d'images
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Vérifier si sharp est disponible
|
||||
let sharp;
|
||||
try {
|
||||
sharp = require('sharp');
|
||||
} catch (err) {
|
||||
console.log('❌ Le package "sharp" n\'est pas installé.');
|
||||
console.log('📦 Installation en cours...');
|
||||
const { execSync } = require('child_process');
|
||||
try {
|
||||
execSync('npm install --no-save sharp', { stdio: 'inherit' });
|
||||
sharp = require('sharp');
|
||||
console.log('✅ Sharp installé avec succès!\n');
|
||||
} catch (installErr) {
|
||||
console.error('❌ Impossible d\'installer sharp. Veuillez l\'installer manuellement:');
|
||||
console.error(' npm install sharp');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const USER2_DIR = path.join(__dirname, 'public/assets/sprites/user2');
|
||||
const BACKUP_DIR = path.join(__dirname, 'public/assets/sprites/user2_backup');
|
||||
const TARGET_WIDTH = 93;
|
||||
const TARGET_HEIGHT = 224;
|
||||
|
||||
async function resizeImages() {
|
||||
try {
|
||||
// Créer un backup
|
||||
if (!fs.existsSync(BACKUP_DIR)) {
|
||||
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
||||
console.log('📁 Dossier de backup créé:', BACKUP_DIR);
|
||||
}
|
||||
|
||||
// Lire tous les fichiers PNG sauf le spritesheet
|
||||
const files = fs.readdirSync(USER2_DIR)
|
||||
.filter(f => f.endsWith('.png') && f !== 'player_spritesheet.png');
|
||||
|
||||
console.log(`\n🖼️ Redimensionnement de ${files.length} sprites vers ${TARGET_WIDTH}x${TARGET_HEIGHT}px...\n`);
|
||||
|
||||
for (const file of files) {
|
||||
const inputPath = path.join(USER2_DIR, file);
|
||||
const backupPath = path.join(BACKUP_DIR, file);
|
||||
|
||||
// Backup de l'original
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
fs.copyFileSync(inputPath, backupPath);
|
||||
}
|
||||
|
||||
// Lire les métadonnées de l'image
|
||||
const metadata = await sharp(inputPath).metadata();
|
||||
|
||||
console.log(`📏 ${file}`);
|
||||
console.log(` ${metadata.width}x${metadata.height} → ${TARGET_WIDTH}x${TARGET_HEIGHT}`);
|
||||
|
||||
// Redimensionner avec fit: 'contain' pour préserver le ratio et ajouter fond transparent
|
||||
await sharp(inputPath)
|
||||
.resize(TARGET_WIDTH, TARGET_HEIGHT, {
|
||||
kernel: 'lanczos3',
|
||||
fit: 'contain',
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||
})
|
||||
.toFile(inputPath + '.tmp');
|
||||
|
||||
// Remplacer l'original
|
||||
fs.renameSync(inputPath + '.tmp', inputPath);
|
||||
console.log(` ✅ Redimensionné\n`);
|
||||
}
|
||||
|
||||
console.log('✨ Toutes les images ont été redimensionnées avec succès!');
|
||||
console.log(`💾 Les originaux sont sauvegardés dans: ${BACKUP_DIR}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Exécuter
|
||||
resizeImages();
|
||||
@@ -1,14 +1,97 @@
|
||||
import { GYRO_DEADZONE, GYRO_MAX_TILT, GYRO_SENSITIVITY } from '../utils/constants';
|
||||
import {
|
||||
GYRO_DEADZONE_ANDROID,
|
||||
GYRO_MAX_TILT_ANDROID,
|
||||
GYRO_SENSITIVITY_ANDROID,
|
||||
GYRO_CALIBRATION_SAMPLES_ANDROID,
|
||||
GYRO_INVERT_BETA_ANDROID,
|
||||
GYRO_USE_GAMMA_ANDROID,
|
||||
GYRO_USE_ALPHA_ANDROID,
|
||||
GYRO_DEADZONE_IOS,
|
||||
GYRO_MAX_TILT_IOS,
|
||||
GYRO_SENSITIVITY_IOS,
|
||||
GYRO_CALIBRATION_SAMPLES_IOS,
|
||||
GYRO_INVERT_BETA_IOS,
|
||||
GYRO_USE_GAMMA_IOS,
|
||||
GYRO_USE_ALPHA_IOS,
|
||||
} from '../utils/constants';
|
||||
|
||||
/**
|
||||
* Gestion du gyroscope pour iOS et Android
|
||||
* Retourne une valeur normalisée entre -1 et 1
|
||||
*
|
||||
* Configuration centralisée dans constants.ts pour faciliter le débogage:
|
||||
* - iOS: GYRO_*_IOS (sensibilité réduite, pas d'inversion beta)
|
||||
* - Android: GYRO_*_ANDROID (sensibilité élevée, inversion beta)
|
||||
*
|
||||
* Pour ajuster les contrôles, modifier les constantes dans utils/constants.ts
|
||||
*/
|
||||
export class GyroControl {
|
||||
private tiltValue: number = 0;
|
||||
private isActive: boolean = false;
|
||||
private lastEventLogTime: number = 0;
|
||||
private lastTiltLogTime: number = 0;
|
||||
private lastAlpha: number = 0; // Rotation boussole (0-360°)
|
||||
private lastBeta: number = 0; // Inclinaison avant/arrière
|
||||
private lastGamma: number = 0; // Inclinaison gauche/droite
|
||||
private lastAngle: number = 0;
|
||||
private lastAbs: boolean | null = null;
|
||||
private lastEventAt: number = 0;
|
||||
private baselineTilt: number = 0; // Baseline pour calibration
|
||||
private isCalibrated: boolean = false;
|
||||
private calibrationSamples: number[] = [];
|
||||
private readonly calibrationSamplesCount: number; // Nombre d'échantillons pour moyenne (varie selon plateforme)
|
||||
|
||||
// Configuration spécifique selon la plateforme
|
||||
private readonly isIOS: boolean;
|
||||
private readonly deadzone: number;
|
||||
private readonly maxTilt: number;
|
||||
private readonly sensitivity: number;
|
||||
private readonly invertBeta: boolean; // Inverser la lecture du beta?
|
||||
private readonly useGamma: boolean; // Utiliser gamma au lieu de beta en paysage?
|
||||
private readonly useAlpha: boolean; // Utiliser alpha (rotation boussole)?
|
||||
|
||||
constructor() {
|
||||
// Détecter iOS
|
||||
this.isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||
|
||||
// Configurer selon la plateforme (toutes les valeurs viennent de constants.ts)
|
||||
if (this.isIOS) {
|
||||
this.deadzone = GYRO_DEADZONE_IOS;
|
||||
this.maxTilt = GYRO_MAX_TILT_IOS;
|
||||
this.sensitivity = GYRO_SENSITIVITY_IOS;
|
||||
this.calibrationSamplesCount = GYRO_CALIBRATION_SAMPLES_IOS;
|
||||
this.invertBeta = GYRO_INVERT_BETA_IOS;
|
||||
this.useGamma = GYRO_USE_GAMMA_IOS;
|
||||
this.useAlpha = GYRO_USE_ALPHA_IOS;
|
||||
console.log('[GyroControl] ✅ Configuration iOS chargée depuis constants.ts', {
|
||||
deadzone: this.deadzone,
|
||||
maxTilt: this.maxTilt,
|
||||
sensitivity: this.sensitivity,
|
||||
calibrationSamples: this.calibrationSamplesCount,
|
||||
invertBeta: this.invertBeta,
|
||||
useGamma: this.useGamma,
|
||||
useAlpha: this.useAlpha
|
||||
});
|
||||
} else {
|
||||
this.deadzone = GYRO_DEADZONE_ANDROID;
|
||||
this.maxTilt = GYRO_MAX_TILT_ANDROID;
|
||||
this.sensitivity = GYRO_SENSITIVITY_ANDROID;
|
||||
this.calibrationSamplesCount = GYRO_CALIBRATION_SAMPLES_ANDROID;
|
||||
this.invertBeta = GYRO_INVERT_BETA_ANDROID;
|
||||
this.useGamma = GYRO_USE_GAMMA_ANDROID;
|
||||
this.useAlpha = GYRO_USE_ALPHA_ANDROID;
|
||||
console.log('[GyroControl] ✅ Configuration Android chargée depuis constants.ts', {
|
||||
deadzone: this.deadzone,
|
||||
maxTilt: this.maxTilt,
|
||||
sensitivity: this.sensitivity,
|
||||
calibrationSamples: this.calibrationSamplesCount,
|
||||
invertBeta: this.invertBeta,
|
||||
useGamma: this.useGamma,
|
||||
useAlpha: this.useAlpha
|
||||
});
|
||||
}
|
||||
|
||||
this.setupGyroscope();
|
||||
}
|
||||
|
||||
@@ -46,33 +129,130 @@ export class GyroControl {
|
||||
|
||||
// Déterminer l'axe horizontal en fonction de l'orientation écran
|
||||
const angle = (window.screen.orientation?.angle ?? (window as any).orientation ?? 0) as number;
|
||||
const alpha = event.alpha ?? 0; // rotation boussole (0-360°)
|
||||
const beta = event.beta ?? 0; // inclinaison avant/arrière
|
||||
const gamma = event.gamma ?? 0; // inclinaison gauche/droite
|
||||
|
||||
// En paysage, la gauche/droite correspond à beta; en portrait, à gamma
|
||||
let horizontalTiltDeg = gamma;
|
||||
if (angle === 90) {
|
||||
horizontalTiltDeg = -beta;
|
||||
} else if (angle === -90 || angle === 270) {
|
||||
horizontalTiltDeg = beta;
|
||||
const now = Date.now();
|
||||
this.lastAlpha = alpha;
|
||||
this.lastBeta = beta;
|
||||
this.lastGamma = gamma;
|
||||
this.lastAngle = angle;
|
||||
this.lastAbs = event.absolute ?? null;
|
||||
this.lastEventAt = now;
|
||||
if (now - this.lastEventLogTime > 1000) {
|
||||
console.log('[Gyro] event', {
|
||||
angle,
|
||||
alpha,
|
||||
beta,
|
||||
gamma,
|
||||
abs: event.absolute ?? null,
|
||||
});
|
||||
this.lastEventLogTime = now;
|
||||
}
|
||||
|
||||
const relativeTilt = horizontalTiltDeg;
|
||||
// IMPORTANT : Déterminer l'axe à utiliser selon l'orientation réelle
|
||||
// iOS: beta (avant/arrière), Android: peut utiliser alpha (rotation boussole), gamma, ou beta
|
||||
let horizontalTiltDeg: number;
|
||||
|
||||
// Appliquer la deadzone
|
||||
if (Math.abs(relativeTilt) < GYRO_DEADZONE) {
|
||||
// Si on utilise alpha (rotation boussole - Android uniquement)
|
||||
if (this.useAlpha) {
|
||||
// Alpha va de 0 à 360°
|
||||
// Convertir en -180 à 180 pour avoir une baseline centrée
|
||||
let normalizedAlpha = alpha;
|
||||
if (normalizedAlpha > 180) {
|
||||
normalizedAlpha -= 360;
|
||||
}
|
||||
horizontalTiltDeg = normalizedAlpha;
|
||||
}
|
||||
// ANDROID : Forcer l'utilisation de BETA quelle que soit l'orientation
|
||||
else if (!this.isIOS && !this.useGamma && !this.useAlpha) {
|
||||
// Android avec configuration BETA : utiliser beta directement
|
||||
horizontalTiltDeg = this.invertBeta ? -beta : beta;
|
||||
|
||||
// Ajuster selon l'orientation du téléphone
|
||||
// Si angle = 90° (paysage avec home button à droite) ou -90° (home button à gauche)
|
||||
if (Math.abs(angle - 90) < 45) {
|
||||
// Paysage normal (90°) : pas d'inversion supplémentaire
|
||||
// horizontalTiltDeg reste tel quel
|
||||
} else if (Math.abs(angle + 90) < 45 || Math.abs(angle - 270) < 45) {
|
||||
// Paysage inversé (-90° ou 270°) : inverser
|
||||
horizontalTiltDeg = -horizontalTiltDeg;
|
||||
}
|
||||
}
|
||||
// Sinon, si angle est proche de 0° ou 180° (mode paysage classique)
|
||||
else if (Math.abs(angle) < 45 || Math.abs(angle - 180) < 45 || Math.abs(angle + 180) < 45) {
|
||||
// Mode paysage
|
||||
if (this.useGamma) {
|
||||
// Android : utiliser gamma pour l'inclinaison gauche/droite
|
||||
horizontalTiltDeg = gamma;
|
||||
|
||||
// Inverser si paysage inversé (angle proche de 180)
|
||||
if (Math.abs(angle - 180) < 45 || Math.abs(angle + 180) < 45) {
|
||||
horizontalTiltDeg = -horizontalTiltDeg;
|
||||
}
|
||||
} else {
|
||||
// iOS : utiliser beta pour l'inclinaison avant/arrière
|
||||
// L'inversion du beta est configurée dans constants.ts selon la plateforme
|
||||
horizontalTiltDeg = this.invertBeta ? -beta : beta;
|
||||
|
||||
// Inverser si paysage inversé (angle proche de 180)
|
||||
if (Math.abs(angle - 180) < 45 || Math.abs(angle + 180) < 45) {
|
||||
horizontalTiltDeg = -horizontalTiltDeg;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Si angle proche de 90° ou -90°/270° (mode portrait)
|
||||
else {
|
||||
// Mode portrait : utiliser gamma pour l'inclinaison gauche/droite
|
||||
horizontalTiltDeg = gamma;
|
||||
|
||||
// Ajuster le signe selon l'orientation
|
||||
if (angle < 0 || angle > 180) {
|
||||
horizontalTiltDeg = -gamma;
|
||||
}
|
||||
}
|
||||
|
||||
// Calibration automatique : collecter les premiers échantillons
|
||||
if (!this.isCalibrated) {
|
||||
this.calibrationSamples.push(horizontalTiltDeg);
|
||||
if (this.calibrationSamples.length >= this.calibrationSamplesCount) {
|
||||
// Calculer la moyenne pour établir la baseline
|
||||
this.baselineTilt = this.calibrationSamples.reduce((sum, val) => sum + val, 0) / this.calibrationSamples.length;
|
||||
this.isCalibrated = true;
|
||||
console.log(`[Gyro ${this.isIOS ? 'iOS' : 'Android'}] ✅ Calibration terminée - Baseline: ${this.baselineTilt.toFixed(2)}° (${this.calibrationSamplesCount} échantillons)`);
|
||||
} else {
|
||||
console.log(`[Gyro ${this.isIOS ? 'iOS' : 'Android'}] Calibration en cours... (${this.calibrationSamples.length}/${this.calibrationSamplesCount})`);
|
||||
this.tiltValue = 0; // Pas de mouvement pendant la calibration
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Soustraire la baseline pour obtenir le tilt relatif à la position neutre
|
||||
const relativeTilt = horizontalTiltDeg - this.baselineTilt;
|
||||
|
||||
// Appliquer la deadzone (spécifique à la plateforme)
|
||||
if (Math.abs(relativeTilt) < this.deadzone) {
|
||||
this.tiltValue = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Normaliser entre -1 et 1
|
||||
let normalizedTilt = relativeTilt / GYRO_MAX_TILT;
|
||||
// Normaliser entre -1 et 1 (avec maxTilt spécifique à la plateforme)
|
||||
let normalizedTilt = relativeTilt / this.maxTilt;
|
||||
|
||||
// Clamper entre -1 et 1
|
||||
normalizedTilt = Math.max(-1, Math.min(1, normalizedTilt));
|
||||
|
||||
// Inversion gauche/droite (plus naturel selon retour)
|
||||
this.tiltValue = -normalizedTilt;
|
||||
// Pas d'inversion finale - elle est déjà faite au niveau de la lecture du beta/gamma
|
||||
// iOS: beta direct → pencher en arrière = beta négatif → mouvement droite
|
||||
// Android: gamma direct → pencher à droite = gamma positif → mouvement droite
|
||||
this.tiltValue = normalizedTilt;
|
||||
|
||||
if (now - this.lastTiltLogTime > 1000) {
|
||||
const axisUsed = this.useAlpha ? 'alpha' : (this.useGamma ? 'gamma' : 'beta');
|
||||
console.log(`[Gyro ${this.isIOS ? 'iOS' : 'Android'}] tilt normalized`, this.tiltValue,
|
||||
`(axe: ${axisUsed}, raw: ${horizontalTiltDeg.toFixed(1)}°, baseline: ${this.baselineTilt.toFixed(1)}°, relative: ${relativeTilt.toFixed(1)}°, deadzone: ${this.deadzone}°, maxTilt: ${this.maxTilt}°)`);
|
||||
this.lastTiltLogTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,18 +263,22 @@ export class GyroControl {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la vitesse calculée depuis le tilt
|
||||
* Retourne la vitesse calculée depuis le tilt (avec sensibilité spécifique à la plateforme)
|
||||
*/
|
||||
public getVelocity(): number {
|
||||
return this.tiltValue * GYRO_SENSITIVITY;
|
||||
return this.tiltValue * this.sensitivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calibre le gyroscope (définit l'orientation actuelle comme neutre)
|
||||
*/
|
||||
public calibrate(): void {
|
||||
// Calibration simplifiée : pas de base dynamique, on recentre juste à 0
|
||||
// Reset de la calibration pour recalculer la baseline
|
||||
this.isCalibrated = false;
|
||||
this.calibrationSamples = [];
|
||||
this.baselineTilt = 0;
|
||||
this.tiltValue = 0;
|
||||
console.log('[Gyro] Recalibration demandée...');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,4 +305,47 @@ export class GyroControl {
|
||||
this.isActive = false;
|
||||
this.tiltValue = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug info for on-screen diagnostics.
|
||||
*/
|
||||
public getDebugInfo(): {
|
||||
active: boolean;
|
||||
tilt: number;
|
||||
alpha: number;
|
||||
beta: number;
|
||||
gamma: number;
|
||||
angle: number;
|
||||
abs: boolean | null;
|
||||
lastEventAt: number;
|
||||
calibrated: boolean;
|
||||
baseline: number;
|
||||
platform: string;
|
||||
deadzone: number;
|
||||
maxTilt: number;
|
||||
sensitivity: number;
|
||||
invertBeta: boolean;
|
||||
useGamma: boolean;
|
||||
useAlpha: boolean;
|
||||
} {
|
||||
return {
|
||||
active: this.isActive,
|
||||
tilt: this.tiltValue,
|
||||
alpha: this.lastAlpha,
|
||||
beta: this.lastBeta,
|
||||
gamma: this.lastGamma,
|
||||
angle: this.lastAngle,
|
||||
abs: this.lastAbs,
|
||||
lastEventAt: this.lastEventAt,
|
||||
calibrated: this.isCalibrated,
|
||||
baseline: this.baselineTilt,
|
||||
platform: this.isIOS ? 'iOS' : 'Android',
|
||||
deadzone: this.deadzone,
|
||||
maxTilt: this.maxTilt,
|
||||
sensitivity: this.sensitivity,
|
||||
invertBeta: this.invertBeta,
|
||||
useGamma: this.useGamma,
|
||||
useAlpha: this.useAlpha,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
125
src/entities/Cage.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import Phaser from 'phaser';
|
||||
import { Dog } from './Dog';
|
||||
|
||||
/**
|
||||
* Cage contenant le chien
|
||||
* Peut être ouverte avec une clé
|
||||
*/
|
||||
export class Cage extends Phaser.GameObjects.Container {
|
||||
public scene: Phaser.Scene;
|
||||
private dog: Dog;
|
||||
private door?: Phaser.GameObjects.Rectangle;
|
||||
private bars: Phaser.GameObjects.Rectangle[] = [];
|
||||
private isOpen: boolean = false;
|
||||
private lockIcon?: Phaser.GameObjects.Text;
|
||||
|
||||
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||
super(scene, x, y);
|
||||
this.scene = scene;
|
||||
|
||||
scene.add.existing(this);
|
||||
this.setDepth(49);
|
||||
|
||||
// Créer la cage visuellement
|
||||
this.createCage();
|
||||
|
||||
// Créer le chien à l'intérieur
|
||||
this.dog = new Dog(scene, x, y - 10, 150);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée la cage (barreaux, sol, porte)
|
||||
*/
|
||||
private createCage(): void {
|
||||
const cageWidth = 180;
|
||||
const cageHeight = 150;
|
||||
|
||||
// Sol de la cage
|
||||
const floor = this.scene.add.rectangle(0, 10, cageWidth, 10, 0x8B4513);
|
||||
this.add(floor);
|
||||
|
||||
// Toit
|
||||
const roof = this.scene.add.rectangle(0, -cageHeight + 10, cageWidth, 8, 0x654321);
|
||||
this.add(roof);
|
||||
|
||||
// Barreaux verticaux (fond)
|
||||
const barCount = 8;
|
||||
const barSpacing = cageWidth / barCount;
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const barX = -cageWidth / 2 + i * barSpacing + barSpacing / 2;
|
||||
const bar = this.scene.add.rectangle(
|
||||
barX,
|
||||
-cageHeight / 2 + 10,
|
||||
6,
|
||||
cageHeight - 10,
|
||||
0x555555
|
||||
);
|
||||
this.add(bar);
|
||||
this.bars.push(bar);
|
||||
}
|
||||
|
||||
// Porte (côté droit de la cage)
|
||||
this.door = this.scene.add.rectangle(
|
||||
cageWidth / 2 - 3,
|
||||
-cageHeight / 2 + 10,
|
||||
8,
|
||||
cageHeight - 10,
|
||||
0x333333
|
||||
);
|
||||
this.add(this.door);
|
||||
|
||||
// Icône de cadenas
|
||||
this.lockIcon = this.scene.add.text(cageWidth / 2 - 20, -cageHeight / 2, '🔒', {
|
||||
fontSize: '32px',
|
||||
});
|
||||
this.add(this.lockIcon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ouvre la cage
|
||||
*/
|
||||
public open(): void {
|
||||
if (this.isOpen) return;
|
||||
this.isOpen = true;
|
||||
|
||||
// Retirer le cadenas
|
||||
this.lockIcon?.destroy();
|
||||
|
||||
// Animation d'ouverture de la porte
|
||||
this.scene.tweens.add({
|
||||
targets: this.door,
|
||||
x: this.door!.x + 80,
|
||||
angle: -90,
|
||||
duration: 1000,
|
||||
ease: 'Back.easeOut',
|
||||
onComplete: () => {
|
||||
// Libérer le chien
|
||||
this.dog.free();
|
||||
}
|
||||
});
|
||||
|
||||
// Son d'ouverture (optionnel)
|
||||
// this.scene.sound.play('cage-open');
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la cage est ouverte
|
||||
*/
|
||||
public isOpened(): boolean {
|
||||
return this.isOpen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le chien
|
||||
*/
|
||||
public getDog(): Dog {
|
||||
return this.dog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update pour le chien
|
||||
*/
|
||||
public update(): void {
|
||||
this.dog.update();
|
||||
}
|
||||
}
|
||||
194
src/entities/Dog.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import Phaser from 'phaser';
|
||||
|
||||
/**
|
||||
* Entité Dog - Petit chien en cage
|
||||
* Fait des va-et-vient dans la cage avec alternance idle/marche
|
||||
*/
|
||||
export class Dog extends Phaser.GameObjects.Sprite {
|
||||
public scene: Phaser.Scene;
|
||||
private direction: number = 1; // 1 = droite, -1 = gauche
|
||||
private moveTimer?: Phaser.Time.TimerEvent;
|
||||
private idleTimer?: Phaser.Time.TimerEvent;
|
||||
private cageLeft: number;
|
||||
private cageRight: number;
|
||||
private isMoving: boolean = false;
|
||||
private isFree: boolean = false;
|
||||
|
||||
constructor(
|
||||
scene: Phaser.Scene,
|
||||
x: number,
|
||||
y: number,
|
||||
cageWidth: number = 150
|
||||
) {
|
||||
super(scene, x, y, 'dog-idle', 0);
|
||||
this.scene = scene;
|
||||
|
||||
// Définir les limites de la cage
|
||||
this.cageLeft = x - cageWidth / 2 + 20;
|
||||
this.cageRight = x + cageWidth / 2 - 20;
|
||||
|
||||
// Ajouter au scene
|
||||
scene.add.existing(this);
|
||||
|
||||
// Échelle du chien
|
||||
this.setScale(0.8);
|
||||
this.setOrigin(0.5, 1); // Origine aux pieds
|
||||
|
||||
// Depth
|
||||
this.setDepth(50);
|
||||
|
||||
// Démarrer le comportement
|
||||
this.startBehavior();
|
||||
}
|
||||
|
||||
/**
|
||||
* Démarre le comportement de va-et-vient
|
||||
*/
|
||||
private startBehavior(): void {
|
||||
// Commencer par idle
|
||||
this.playIdleAnimation();
|
||||
|
||||
// Après un temps aléatoire (2-4 sec), commencer à marcher
|
||||
const idleDuration = Phaser.Math.Between(2000, 4000);
|
||||
this.idleTimer = this.scene.time.delayedCall(idleDuration, () => {
|
||||
this.startWalking();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Joue l'animation idle
|
||||
*/
|
||||
private playIdleAnimation(): void {
|
||||
this.isMoving = false;
|
||||
this.play('dog-idle', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Commence à marcher
|
||||
*/
|
||||
private startWalking(): void {
|
||||
this.isMoving = true;
|
||||
|
||||
// Choisir aléatoirement la direction (sauf si on est contre un mur)
|
||||
if (this.x <= this.cageLeft) {
|
||||
this.direction = 1; // Forcer vers la droite
|
||||
} else if (this.x >= this.cageRight) {
|
||||
this.direction = -1; // Forcer vers la gauche
|
||||
} else {
|
||||
this.direction = Phaser.Math.Between(0, 1) === 0 ? -1 : 1;
|
||||
}
|
||||
|
||||
// Si on change de direction, jouer l'animation de tour
|
||||
if (this.direction === 1 && this.flipX) {
|
||||
this.playTurnAnimation(() => {
|
||||
this.flipX = false;
|
||||
this.walk();
|
||||
});
|
||||
} else if (this.direction === -1 && !this.flipX) {
|
||||
this.playTurnAnimation(() => {
|
||||
this.flipX = true;
|
||||
this.walk();
|
||||
});
|
||||
} else {
|
||||
this.walk();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marche dans la direction actuelle
|
||||
*/
|
||||
private walk(): void {
|
||||
// Jouer l'animation de marche (walkback = marche vers la gauche en visuel)
|
||||
this.play('dog-walkback', true);
|
||||
|
||||
// Marcher pendant 1-2 secondes
|
||||
const walkDuration = Phaser.Math.Between(1000, 2000);
|
||||
this.moveTimer = this.scene.time.delayedCall(walkDuration, () => {
|
||||
this.stopWalking();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Arrête de marcher et retourne en idle
|
||||
*/
|
||||
private stopWalking(): void {
|
||||
this.playIdleAnimation();
|
||||
|
||||
// Après un temps d'idle, recommencer à marcher
|
||||
const idleDuration = Phaser.Math.Between(2000, 4000);
|
||||
this.idleTimer = this.scene.time.delayedCall(idleDuration, () => {
|
||||
if (!this.isFree) {
|
||||
this.startWalking();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Joue l'animation de tour (demi-tour)
|
||||
*/
|
||||
private playTurnAnimation(onComplete: () => void): void {
|
||||
this.play('dog-turn');
|
||||
this.once('animationcomplete', () => {
|
||||
onComplete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Libère le chien (sort de la cage)
|
||||
*/
|
||||
public free(): void {
|
||||
this.isFree = true;
|
||||
this.isMoving = false;
|
||||
|
||||
// Annuler les timers
|
||||
this.moveTimer?.destroy();
|
||||
this.idleTimer?.destroy();
|
||||
|
||||
// Jouer une animation de joie (idle pour l'instant)
|
||||
this.playIdleAnimation();
|
||||
|
||||
// Animation de sortie de cage (optionnel - peut courir vers la droite)
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
x: this.x + 200,
|
||||
duration: 2000,
|
||||
ease: 'Power2',
|
||||
onStart: () => {
|
||||
this.flipX = false;
|
||||
this.play('dog-walkback', true);
|
||||
},
|
||||
onComplete: () => {
|
||||
this.playIdleAnimation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode update pour le mouvement
|
||||
*/
|
||||
public update(): void {
|
||||
if (!this.isMoving || this.isFree) return;
|
||||
|
||||
// Déplacer le chien
|
||||
const speed = 20;
|
||||
this.x += this.direction * speed * (1 / 60); // 60 FPS
|
||||
|
||||
// Vérifier les limites de la cage
|
||||
if (this.x <= this.cageLeft) {
|
||||
this.x = this.cageLeft;
|
||||
this.stopWalking();
|
||||
} else if (this.x >= this.cageRight) {
|
||||
this.x = this.cageRight;
|
||||
this.stopWalking();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoyage
|
||||
*/
|
||||
public destroy(fromScene?: boolean): void {
|
||||
this.moveTimer?.destroy();
|
||||
this.idleTimer?.destroy();
|
||||
super.destroy(fromScene);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
PLAYER_ACCELERATION,
|
||||
PLAYER_MAX_JUMPS,
|
||||
RESPAWN_INVINCIBILITY_TIME,
|
||||
USER1_HITBOX,
|
||||
USER2_HITBOX,
|
||||
} from '../utils/constants';
|
||||
|
||||
/**
|
||||
@@ -19,11 +21,19 @@ export class Player extends Phaser.Physics.Arcade.Sprite {
|
||||
private invincibilityTimer?: Phaser.Time.TimerEvent;
|
||||
private animationsCreated: boolean = false;
|
||||
private wasAir: boolean = false;
|
||||
private playerPrefix: string = 'player_user1'; // Préfixe pour les sprites (user1 ou user2)
|
||||
|
||||
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||
// Pour l'instant, utiliser un sprite simple
|
||||
// TODO: Remplacer par le spritesheet du neveu
|
||||
super(scene, x, y, 'player');
|
||||
// Récupérer le joueur sélectionné depuis le registry
|
||||
const selectedPlayer = scene.registry.get('selectedPlayer') as string | undefined;
|
||||
const playerPrefix = selectedPlayer === 'user2' ? 'player_user2' : 'player_user1';
|
||||
|
||||
// Utiliser le spritesheet comme texture de base
|
||||
const spritesheetKey = selectedPlayer === 'user2' ? 'player_user2' : 'player_user1';
|
||||
super(scene, x, y, spritesheetKey, 0); // Frame 0 du spritesheet
|
||||
|
||||
// Stocker le préfixe pour utilisation ultérieure
|
||||
this.playerPrefix = playerPrefix;
|
||||
|
||||
// Ajouter à la scène
|
||||
scene.add.existing(this);
|
||||
@@ -34,35 +44,41 @@ export class Player extends Phaser.Physics.Arcade.Sprite {
|
||||
body.setGravityY(PLAYER_GRAVITY);
|
||||
body.setCollideWorldBounds(true); // Collision avec les limites du monde
|
||||
body.onWorldBounds = true; // Active les événements de collision
|
||||
body.setSize(40, 70); // Hitbox
|
||||
body.setMaxVelocity(PLAYER_MAX_SPEED, 1000);
|
||||
// Aligner la hitbox en bas du sprite (80x169 par défaut)
|
||||
body.setOffset((this.width - 40) / 2, (this.height || 169) - 70);
|
||||
|
||||
// Temporaire : créer un rectangle coloré si pas de texture
|
||||
if (!scene.textures.exists('player')) {
|
||||
this.createPlaceholderTexture(scene);
|
||||
}
|
||||
|
||||
this.setOrigin(0.5, 1); // Origine en bas au centre
|
||||
body.setOffset((this.width - 40) / 2, (this.height || 169) - 70); // aligner la hitbox en bas
|
||||
this.setScale(1); // Échelle 1:1 pour les deux joueurs
|
||||
|
||||
// Utiliser la configuration de hitbox spécifique au joueur
|
||||
const hitboxConfig = selectedPlayer === 'user2' ? USER2_HITBOX : USER1_HITBOX;
|
||||
|
||||
// Attendre que le sprite soit complètement initialisé pour configurer la hitbox
|
||||
this.scene.time.delayedCall(0, () => {
|
||||
const spriteWidth = this.width;
|
||||
const spriteHeight = this.height;
|
||||
|
||||
// Centrer la hitbox horizontalement
|
||||
const offsetX = (spriteWidth - hitboxConfig.width) / 2;
|
||||
// Placer la hitbox en bas du sprite
|
||||
const offsetY = spriteHeight - hitboxConfig.height;
|
||||
|
||||
body.setSize(hitboxConfig.width, hitboxConfig.height);
|
||||
body.setOffset(offsetX, offsetY);
|
||||
|
||||
console.log(`🎯 Hitbox FINALE configurée pour ${this.playerPrefix}:`, {
|
||||
selectedPlayer,
|
||||
spriteActualSize: `${spriteWidth}x${spriteHeight}px`,
|
||||
hitboxSize: `${hitboxConfig.width}x${hitboxConfig.height}px`,
|
||||
offset: `x=${offsetX.toFixed(1)}, y=${offsetY.toFixed(1)}px`
|
||||
});
|
||||
});
|
||||
|
||||
this.ensureAnimations();
|
||||
this.setTexture('player_walk_1'); // frame par défaut
|
||||
|
||||
// Définir la frame par défaut (idle = première frame du spritesheet)
|
||||
this.setFrame(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une texture temporaire pour le joueur
|
||||
*/
|
||||
private createPlaceholderTexture(scene: Phaser.Scene): void {
|
||||
const graphics = scene.add.graphics();
|
||||
graphics.fillStyle(0xFF0000, 1);
|
||||
graphics.fillRect(0, 0, 50, 80);
|
||||
graphics.generateTexture('player', 50, 80);
|
||||
graphics.destroy();
|
||||
|
||||
this.setTexture('player');
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le mouvement du joueur
|
||||
@@ -117,15 +133,21 @@ export class Player extends Phaser.Physics.Arcade.Sprite {
|
||||
this.jumpCount++;
|
||||
|
||||
// Déclencher l'anim de saut immédiatement si disponible
|
||||
if (this.animationsCreated && this.anims.get('player-jump')) {
|
||||
this.anims.play('player-jump', true);
|
||||
const jumpAnimKey = `${this.playerPrefix}-jump`;
|
||||
const animExists = this.scene.anims.exists(jumpAnimKey);
|
||||
console.log(`🦘 Tentative de saut #${this.jumpCount}: anim "${jumpAnimKey}" existe = ${animExists}, animationsCreated = ${this.animationsCreated}`);
|
||||
|
||||
if (this.animationsCreated && animExists) {
|
||||
this.anims.play(jumpAnimKey, true);
|
||||
console.log(`✅ Animation de saut jouée: ${jumpAnimKey}`);
|
||||
} else {
|
||||
console.warn(`⚠️ Animation de saut non disponible: ${jumpAnimKey}`);
|
||||
}
|
||||
|
||||
// Effet sonore de saut (volume global SFX depuis le registry)
|
||||
const sfxVolume = (this.scene.registry.get('sfxVolume') as number | undefined) ?? 1;
|
||||
this.scene.sound.play('sfx_jump', { volume: 0.5 * sfxVolume });
|
||||
|
||||
// TODO: Jouer son de saut (différent pour double saut)
|
||||
console.log(`Saut ${this.jumpCount}/${PLAYER_MAX_JUMPS}`);
|
||||
}
|
||||
}
|
||||
@@ -141,32 +163,46 @@ export class Player extends Phaser.Physics.Arcade.Sprite {
|
||||
this.jumpCount = 0;
|
||||
}
|
||||
|
||||
// TODO: Jouer les animations appropriées
|
||||
if (!this.animationsCreated) return;
|
||||
|
||||
const isMoving = Math.abs(this.velocityX) > 10;
|
||||
const bodyState = this.body as Phaser.Physics.Arcade.Body;
|
||||
const isAir = !bodyState.touching.down;
|
||||
|
||||
// Utiliser les clés d'animation spécifiques au joueur
|
||||
const walkAnimKey = `${this.playerPrefix}-walk`;
|
||||
const idleAnimKey = `${this.playerPrefix}-idle`;
|
||||
const jumpAnimKey = `${this.playerPrefix}-jump`;
|
||||
|
||||
// Transition sol -> air : jouer l'anim jump une fois
|
||||
if (isAir && !this.wasAir) {
|
||||
const jumpAnim = this.anims.get('player-jump');
|
||||
if (jumpAnim) {
|
||||
this.anims.play('player-jump', true);
|
||||
if (this.scene.anims.exists(jumpAnimKey)) {
|
||||
this.anims.play(jumpAnimKey, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Sol : choisir walk ou idle
|
||||
if (!isAir) {
|
||||
if (isMoving) {
|
||||
this.anims.play('player-walk', true);
|
||||
if (this.scene.anims.exists(walkAnimKey)) {
|
||||
this.anims.play(walkAnimKey, true);
|
||||
}
|
||||
} else {
|
||||
this.anims.play('player-idle', true);
|
||||
// Jouer idle uniquement si on n'est pas déjà en train de le jouer
|
||||
if (this.anims.currentAnim?.key !== idleAnimKey) {
|
||||
if (this.scene.anims.exists(idleAnimKey)) {
|
||||
this.anims.play(idleAnimKey, true);
|
||||
} else {
|
||||
// Fallback : afficher directement la frame 0 du spritesheet
|
||||
this.anims.stop();
|
||||
this.setFrame(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// En l'air : si pas d'anim jump, forcer dernière frame jump
|
||||
if (this.anims.currentAnim?.key !== 'player-jump' && this.anims.get('player-jump')) {
|
||||
this.anims.play('player-jump', true);
|
||||
if (this.anims.currentAnim?.key !== jumpAnimKey && this.scene.anims.exists(jumpAnimKey)) {
|
||||
this.anims.play(jumpAnimKey, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,45 +268,97 @@ export class Player extends Phaser.Physics.Arcade.Sprite {
|
||||
}
|
||||
|
||||
/**
|
||||
* Création des animations (walk + idle)
|
||||
* Création des animations (walk + idle + jump) selon le joueur sélectionné
|
||||
*/
|
||||
private ensureAnimations(): void {
|
||||
if (this.animationsCreated) return;
|
||||
|
||||
const scene = this.scene;
|
||||
const walkFrames = ['player_walk_1', 'player_walk_2', 'player_walk_3', 'player_walk_4']
|
||||
const selectedPlayer = scene.registry.get('selectedPlayer') as string | undefined;
|
||||
|
||||
// Utiliser le préfixe du joueur (player_user1 ou player_user2)
|
||||
const walkFrameKeys = [
|
||||
`${this.playerPrefix}_walk_1`,
|
||||
`${this.playerPrefix}_walk_2`,
|
||||
`${this.playerPrefix}_walk_3`,
|
||||
`${this.playerPrefix}_walk_4`
|
||||
];
|
||||
|
||||
const jumpFrameKeys = [
|
||||
`${this.playerPrefix}_jump_1`,
|
||||
`${this.playerPrefix}_jump_2`,
|
||||
`${this.playerPrefix}_jump_3`,
|
||||
`${this.playerPrefix}_jump_4`,
|
||||
`${this.playerPrefix}_jump_5`
|
||||
];
|
||||
|
||||
// Debug: vérifier quelles textures existent
|
||||
console.log(`🔍 Vérification des textures pour ${this.playerPrefix}:`);
|
||||
walkFrameKeys.forEach(key => {
|
||||
const exists = scene.textures.exists(key);
|
||||
console.log(` Walk frame "${key}": ${exists ? '✅' : '❌'}`);
|
||||
});
|
||||
jumpFrameKeys.forEach(key => {
|
||||
const exists = scene.textures.exists(key);
|
||||
console.log(` Jump frame "${key}": ${exists ? '✅' : '❌'}`);
|
||||
});
|
||||
|
||||
const walkFrames = walkFrameKeys
|
||||
.filter((key) => scene.textures.exists(key))
|
||||
.map((key) => ({ key }));
|
||||
const jumpFrames = ['player_jump_1', 'player_jump_2', 'player_jump_3', 'player_jump_4', 'player_jump_5']
|
||||
|
||||
const jumpFrames = jumpFrameKeys
|
||||
.filter((key) => scene.textures.exists(key))
|
||||
.map((key) => ({ key }));
|
||||
|
||||
// Créer des clés d'animation uniques par joueur
|
||||
const walkAnimKey = `${this.playerPrefix}-walk`;
|
||||
const idleAnimKey = `${this.playerPrefix}-idle`;
|
||||
const jumpAnimKey = `${this.playerPrefix}-jump`;
|
||||
|
||||
if (walkFrames.length >= 2) {
|
||||
scene.anims.create({
|
||||
key: 'player-walk',
|
||||
frames: walkFrames,
|
||||
frameRate: 10,
|
||||
repeat: -1,
|
||||
});
|
||||
// Vérifier si l'animation existe déjà avant de la créer
|
||||
if (!scene.anims.exists(walkAnimKey)) {
|
||||
scene.anims.create({
|
||||
key: walkAnimKey,
|
||||
frames: walkFrames,
|
||||
frameRate: 10,
|
||||
repeat: -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Animation idle : utilise la frame 0 du spritesheet (pose debout)
|
||||
const spritesheetKey = selectedPlayer === 'user2' ? 'player_user2' : 'player_user1';
|
||||
if (!scene.anims.exists(idleAnimKey)) {
|
||||
scene.anims.create({
|
||||
key: 'player-idle',
|
||||
frames: [{ key: walkFrames[0].key }],
|
||||
key: idleAnimKey,
|
||||
frames: [{ key: spritesheetKey, frame: 0 }],
|
||||
frameRate: 1,
|
||||
repeat: -1,
|
||||
});
|
||||
}
|
||||
|
||||
if (jumpFrames.length >= 2) {
|
||||
scene.anims.create({
|
||||
key: 'player-jump',
|
||||
frames: jumpFrames,
|
||||
frameRate: 12,
|
||||
repeat: 0,
|
||||
});
|
||||
if (!scene.anims.exists(jumpAnimKey)) {
|
||||
scene.anims.create({
|
||||
key: jumpAnimKey,
|
||||
frames: jumpFrames,
|
||||
frameRate: 12,
|
||||
repeat: 0,
|
||||
});
|
||||
console.log(`✅ Animation de saut créée: ${jumpAnimKey} avec ${jumpFrames.length} frames`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`⚠️ Pas assez de frames pour l'animation de saut (${jumpFrames.length}/2 requis)`);
|
||||
}
|
||||
|
||||
this.animationsCreated = walkFrames.length >= 2 || jumpFrames.length >= 2;
|
||||
console.log(`🎬 Animations ${this.playerPrefix}:`, {
|
||||
walk: walkFrames.length,
|
||||
jump: jumpFrames.length,
|
||||
created: this.animationsCreated
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,18 +9,20 @@ export class SuperTreasure extends Phaser.Physics.Arcade.Sprite {
|
||||
private pulseTimer: Phaser.Time.TimerEvent;
|
||||
|
||||
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||
super(scene, x, y, 'supertreasure');
|
||||
super(scene, x, y, 'star_sprite');
|
||||
|
||||
scene.add.existing(this);
|
||||
scene.physics.add.existing(this);
|
||||
|
||||
// Créer texture temporaire si elle n'existe pas
|
||||
if (!scene.textures.exists('supertreasure')) {
|
||||
// Utiliser le sprite star_40.png (40x40px) au lieu de créer une texture
|
||||
// Si le sprite n'existe pas, créer une texture temporaire en fallback
|
||||
if (scene.textures.exists('star_sprite')) {
|
||||
this.setTexture('star_sprite');
|
||||
} else if (!scene.textures.exists('supertreasure')) {
|
||||
this.createPlaceholderTexture(scene);
|
||||
this.setTexture('supertreasure');
|
||||
}
|
||||
|
||||
this.setTexture('supertreasure');
|
||||
|
||||
// Taille plus grande que les cadeaux normaux
|
||||
this.setScale(1.5);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import Phaser from 'phaser';
|
||||
export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
|
||||
private isOpen: boolean = false;
|
||||
private requiredGifts: number;
|
||||
private requirementText?: Phaser.GameObjects.Text;
|
||||
|
||||
constructor(scene: Phaser.Scene, x: number, y: number, requiredGifts: number = 15) {
|
||||
super(scene, x, y, 'chest');
|
||||
@@ -108,10 +109,10 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée le texte qui indique le nombre de cadeaux requis
|
||||
* Crée le texte qui indique le nombre de cadeaux requis (caché par défaut)
|
||||
*/
|
||||
private createRequirementText(scene: Phaser.Scene, x: number, y: number): void {
|
||||
const text = scene.add.text(
|
||||
this.requirementText = scene.add.text(
|
||||
x,
|
||||
y - 80,
|
||||
`🎁 ${this.requiredGifts} cadeaux requis`,
|
||||
@@ -123,12 +124,13 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
|
||||
fontStyle: 'bold',
|
||||
}
|
||||
);
|
||||
text.setOrigin(0.5);
|
||||
text.setDepth(this.depth + 1);
|
||||
this.requirementText.setOrigin(0.5);
|
||||
this.requirementText.setDepth(this.depth + 1);
|
||||
this.requirementText.setVisible(false); // Caché par défaut
|
||||
|
||||
// Animation pulse
|
||||
scene.tweens.add({
|
||||
targets: text,
|
||||
targets: this.requirementText,
|
||||
scaleX: 1.1,
|
||||
scaleY: 1.1,
|
||||
duration: 800,
|
||||
@@ -137,6 +139,20 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche le texte des cadeaux requis
|
||||
*/
|
||||
public showRequirementText(): void {
|
||||
this.requirementText?.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache le texte des cadeaux requis
|
||||
*/
|
||||
public hideRequirementText(): void {
|
||||
this.requirementText?.setVisible(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si le joueur peut ouvrir le coffre
|
||||
*/
|
||||
@@ -145,10 +161,10 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ouvre le coffre et donne le mega bonus
|
||||
* Ouvre le coffre et donne la clé
|
||||
*/
|
||||
public open(scene: Phaser.Scene): number {
|
||||
if (this.isOpen) return 0;
|
||||
public open(scene: Phaser.Scene): { bonus: number; hasKey: boolean } {
|
||||
if (this.isOpen) return { bonus: 0, hasKey: false };
|
||||
|
||||
this.isOpen = true;
|
||||
this.setTexture('chest-open');
|
||||
@@ -159,13 +175,13 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
|
||||
// Particules dorées qui explosent
|
||||
this.createExplosionParticles(scene);
|
||||
|
||||
// Message épique
|
||||
// Message épique (sans la clé - elle sera affichée séparément)
|
||||
const megaBonusText = scene.add.text(
|
||||
scene.cameras.main.scrollX + scene.cameras.main.width / 2,
|
||||
scene.cameras.main.height / 2 - 100,
|
||||
'🏆 COFFRE OUVERT ! 🏆\n★★ MEGA BONUS +1000 ★★',
|
||||
'🏆 COFFRE OUVERT ! 🏆\n★★ BONUS +1000 ★★',
|
||||
{
|
||||
fontSize: '56px',
|
||||
fontSize: '48px',
|
||||
color: '#FFD700',
|
||||
stroke: '#FF4500',
|
||||
strokeThickness: 8,
|
||||
@@ -190,9 +206,9 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
|
||||
},
|
||||
});
|
||||
|
||||
console.log('🏆 COFFRE AU TRÉSOR OUVERT ! MEGA BONUS +1000 !');
|
||||
console.log('🏆 COFFRE AU TRÉSOR OUVERT ! BONUS +1000 + CLÉ !');
|
||||
|
||||
return 1000; // Mega bonus de points
|
||||
return { bonus: 1000, hasKey: true }; // Mega bonus de points + clé
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
12
src/game.ts
@@ -1,9 +1,11 @@
|
||||
import Phaser from 'phaser';
|
||||
import { GAME_WIDTH, GAME_HEIGHT } from './utils/constants';
|
||||
import { BootScene } from './scenes/BootScene';
|
||||
import { PlayerSelectScene } from './scenes/PlayerSelectScene';
|
||||
import { MenuScene } from './scenes/MenuScene';
|
||||
import { GameScene } from './scenes/GameScene';
|
||||
import { IntroScene } from './scenes/IntroScene';
|
||||
import { IntroVideoScene } from './scenes/IntroVideoScene';
|
||||
import { EndScene } from './scenes/EndScene';
|
||||
|
||||
// Configuration Phaser
|
||||
@@ -23,7 +25,15 @@ const config: Phaser.Types.Core.GameConfig = {
|
||||
debug: false, // Mettre à true pour voir les hitboxes
|
||||
},
|
||||
},
|
||||
scene: [BootScene, IntroScene, MenuScene, GameScene, EndScene],
|
||||
scene: [
|
||||
BootScene,
|
||||
PlayerSelectScene,
|
||||
IntroScene,
|
||||
IntroVideoScene,
|
||||
MenuScene,
|
||||
GameScene,
|
||||
EndScene,
|
||||
],
|
||||
backgroundColor: '#87CEEB',
|
||||
render: {
|
||||
pixelArt: false,
|
||||
|
||||
13
src/main.ts
@@ -6,12 +6,13 @@ const game = new Phaser.Game(config);
|
||||
|
||||
// Gestion du fullscreen au clic (optionnel)
|
||||
window.addEventListener('load', () => {
|
||||
// Enregistrer le service worker pour PWA
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/service-worker.js').catch((error) => {
|
||||
console.log('Service Worker registration failed:', error);
|
||||
});
|
||||
}
|
||||
// TODO: Enregistrer le service worker pour PWA (désactivé temporairement)
|
||||
// Il faut d'abord créer le fichier public/service-worker.js
|
||||
// if ('serviceWorker' in navigator) {
|
||||
// navigator.serviceWorker.register('/service-worker.js').catch((error) => {
|
||||
// console.log('Service Worker registration failed:', error);
|
||||
// });
|
||||
// }
|
||||
|
||||
// Bloquer le zoom pinch sur mobile
|
||||
document.addEventListener('gesturestart', (e) => e.preventDefault());
|
||||
|
||||
@@ -42,21 +42,36 @@ export class BootScene extends Phaser.Scene {
|
||||
loadingText.destroy();
|
||||
});
|
||||
|
||||
// Sprites du joueur (80x169, 1 frame pour l'instant)
|
||||
this.load.spritesheet('player', 'assets/sprites/player_spritesheet.png', {
|
||||
frameWidth: 80,
|
||||
frameHeight: 169,
|
||||
// Charger les sprites des deux joueurs
|
||||
// User1 (Baptiste) - dimensions réelles du spritesheet 93x224px
|
||||
this.load.spritesheet('player_user1', 'assets/sprites/user1/player_spritesheet.png', {
|
||||
frameWidth: 93,
|
||||
frameHeight: 224,
|
||||
});
|
||||
// Frames de marche (sprite individuel)
|
||||
this.load.image('player_walk_1', 'assets/sprites/walk_1.png');
|
||||
this.load.image('player_walk_2', 'assets/sprites/walk_2.png');
|
||||
this.load.image('player_walk_3', 'assets/sprites/walk_3.png');
|
||||
this.load.image('player_walk_4', 'assets/sprites/walk_4.png');
|
||||
this.load.image('player_jump_1', 'assets/sprites/jump_1.png');
|
||||
this.load.image('player_jump_2', 'assets/sprites/jump_2.png');
|
||||
this.load.image('player_jump_3', 'assets/sprites/jump_3.png');
|
||||
this.load.image('player_jump_4', 'assets/sprites/jump_4.png');
|
||||
this.load.image('player_jump_5', 'assets/sprites/jump_5.png');
|
||||
this.load.image('player_user1_walk_1', 'assets/sprites/user1/walk_1.png');
|
||||
this.load.image('player_user1_walk_2', 'assets/sprites/user1/walk_2.png');
|
||||
this.load.image('player_user1_walk_3', 'assets/sprites/user1/walk_3.png');
|
||||
this.load.image('player_user1_walk_4', 'assets/sprites/user1/walk_4.png');
|
||||
this.load.image('player_user1_jump_1', 'assets/sprites/user1/jump_1.png');
|
||||
this.load.image('player_user1_jump_2', 'assets/sprites/user1/jump_2.png');
|
||||
this.load.image('player_user1_jump_3', 'assets/sprites/user1/jump_3.png');
|
||||
this.load.image('player_user1_jump_4', 'assets/sprites/user1/jump_4.png');
|
||||
this.load.image('player_user1_jump_5', 'assets/sprites/user1/jump_5.png');
|
||||
|
||||
// User2 (Julien) - dimensions réelles du spritesheet
|
||||
this.load.spritesheet('player_user2', 'assets/sprites/user2/player_spritesheet.png', {
|
||||
frameWidth: 93,
|
||||
frameHeight: 224,
|
||||
});
|
||||
this.load.image('player_user2_walk_1', 'assets/sprites/user2/walk_1.png');
|
||||
this.load.image('player_user2_walk_2', 'assets/sprites/user2/walk_2.png');
|
||||
this.load.image('player_user2_walk_3', 'assets/sprites/user2/walk_3.png');
|
||||
this.load.image('player_user2_walk_4', 'assets/sprites/user2/walk_4.png');
|
||||
this.load.image('player_user2_jump_1', 'assets/sprites/user2/jump_1.png');
|
||||
this.load.image('player_user2_jump_2', 'assets/sprites/user2/jump_2.png');
|
||||
this.load.image('player_user2_jump_3', 'assets/sprites/user2/jump_3.png');
|
||||
this.load.image('player_user2_jump_4', 'assets/sprites/user2/jump_4.png');
|
||||
this.load.image('player_user2_jump_5', 'assets/sprites/user2/jump_5.png');
|
||||
|
||||
// Musique de fond
|
||||
this.load.audio('bgm', 'assets/audio/01. Ground Theme.mp3');
|
||||
@@ -72,24 +87,107 @@ export class BootScene extends Phaser.Scene {
|
||||
this.load.audio('sfx_hit', ['assets/audio/champignon.mp3', 'assets/audio/champignon.aiff']);
|
||||
this.load.audio('sfx_super', 'assets/audio/super_tresor.mp3');
|
||||
this.load.audio('sfx_saute_champi', 'assets/audio/saute_champi.mp3');
|
||||
this.load.audio('sfx_chien', 'assets/audio/chien.mp3');
|
||||
|
||||
// Sprites obstacles
|
||||
this.load.image('obstacle_mushroom', 'assets/sprites/champignon.png');
|
||||
|
||||
// Vidéo d'intro (mp4 uniquement)
|
||||
// Sprites plateformes (blocs de différentes tailles)
|
||||
this.load.image('platform_40', 'assets/sprites/decor/b_40.png');
|
||||
this.load.image('platform_73', 'assets/sprites/decor/b_73.png');
|
||||
this.load.image('platform_130', 'assets/sprites/decor/b_130.png');
|
||||
this.load.image('platform_140', 'assets/sprites/decor/b_140.png');
|
||||
this.load.image('platform_179', 'assets/sprites/decor/b_179.png');
|
||||
|
||||
// Sol (vert)
|
||||
this.load.image('ground_tile', 'assets/sprites/decor/sol_150.png');
|
||||
|
||||
// Sprites objets de jeu
|
||||
this.load.image('gift_sprite', 'assets/sprites/decor/gift_40.png');
|
||||
this.load.image('star_sprite', 'assets/sprites/decor/star_40.png');
|
||||
|
||||
// Selection joueur (portrait)
|
||||
this.load.image('user1', 'assets/images/user1.jpg');
|
||||
this.load.image('user2', 'assets/images/user2.jpg');
|
||||
|
||||
// Images finales (affichées après la vidéo de fin)
|
||||
this.load.image('finale_user1', 'assets/sprites/user1/image finale1.jpeg');
|
||||
this.load.image('finale_user2', 'assets/sprites/user2/image finale2.jpeg');
|
||||
|
||||
// Vidéos d'intro (mp4 uniquement) - une par joueur
|
||||
// Le 3e paramètre 'noAudio' est à false pour garder l'audio si présent
|
||||
// Ajout d'un timestamp pour forcer le rechargement (éviter le cache)
|
||||
const timestamp = Date.now();
|
||||
this.load.video('intro', `assets/video/intro.mp4?v=${timestamp}`, false);
|
||||
this.load.video('intro_user1', `assets/video/intro_user1.mp4?v=${timestamp}`, false);
|
||||
this.load.video('intro_user2', `assets/video/intro_user2.mp4?v=${timestamp}`, false);
|
||||
|
||||
// Vidéo de fin (quand le joueur gagne)
|
||||
this.load.video('end', `assets/video/end_J.mp4?v=${timestamp}`, false);
|
||||
this.load.video('end', `assets/video/zoe cadeaux1.mp4?v=${timestamp}`, false);
|
||||
|
||||
// Sprites du chien
|
||||
this.load.image('dog-idle-1', 'assets/sprites/dog/idle_01.png');
|
||||
this.load.image('dog-idle-2', 'assets/sprites/dog/idle_02.png');
|
||||
this.load.image('dog-idle-3', 'assets/sprites/dog/idle_03.png');
|
||||
this.load.image('dog-idle-4', 'assets/sprites/dog/idle_04.png');
|
||||
this.load.image('dog-walkback-1', 'assets/sprites/dog/walkback_01.png');
|
||||
this.load.image('dog-walkback-2', 'assets/sprites/dog/walkback_02.png');
|
||||
this.load.image('dog-walkback-3', 'assets/sprites/dog/walkback_03.png');
|
||||
this.load.image('dog-walkback-4', 'assets/sprites/dog/walkback_04.png');
|
||||
this.load.image('dog-turn-1', 'assets/sprites/dog/turn_01.png');
|
||||
this.load.image('dog-turn-2', 'assets/sprites/dog/turn_02.png');
|
||||
this.load.image('dog-turn-3', 'assets/sprites/dog/turn_03.png');
|
||||
|
||||
// TODO: Charger d'autres sprites, backgrounds, sons, etc.
|
||||
}
|
||||
|
||||
create(): void {
|
||||
// Passer par l'intro vidéo puis le menu
|
||||
this.scene.start('IntroScene');
|
||||
// Créer les animations du chien
|
||||
this.createDogAnimations();
|
||||
|
||||
// Demarrer par la selection joueur (portrait sur mobile)
|
||||
this.scene.start('PlayerSelectScene');
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée les animations du chien
|
||||
*/
|
||||
private createDogAnimations(): void {
|
||||
// Animation IDLE (queue qui remue)
|
||||
this.anims.create({
|
||||
key: 'dog-idle',
|
||||
frames: [
|
||||
{ key: 'dog-idle-1' },
|
||||
{ key: 'dog-idle-2' },
|
||||
{ key: 'dog-idle-3' },
|
||||
{ key: 'dog-idle-4' }
|
||||
],
|
||||
frameRate: 6,
|
||||
repeat: -1
|
||||
});
|
||||
|
||||
// Animation WALK BACK (marche vers la gauche)
|
||||
this.anims.create({
|
||||
key: 'dog-walkback',
|
||||
frames: [
|
||||
{ key: 'dog-walkback-1' },
|
||||
{ key: 'dog-walkback-2' },
|
||||
{ key: 'dog-walkback-3' },
|
||||
{ key: 'dog-walkback-4' }
|
||||
],
|
||||
frameRate: 8,
|
||||
repeat: -1
|
||||
});
|
||||
|
||||
// Animation TURN (demi-tour)
|
||||
this.anims.create({
|
||||
key: 'dog-turn',
|
||||
frames: [
|
||||
{ key: 'dog-turn-1' },
|
||||
{ key: 'dog-turn-2' },
|
||||
{ key: 'dog-turn-3' }
|
||||
],
|
||||
frameRate: 10,
|
||||
repeat: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,51 @@ export class EndScene extends Phaser.Scene {
|
||||
console.log('[EndScene] Création de la scène de fin');
|
||||
console.log('[EndScene] Dimensions:', width, 'x', height);
|
||||
|
||||
// Lancer directement la vidéo de fin
|
||||
this.playEndVideo();
|
||||
// Afficher le bouton de lecture au lieu de lancer automatiquement
|
||||
this.showPlayButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche le bouton de lecture
|
||||
*/
|
||||
private showPlayButton(): void {
|
||||
const { width, height } = this.cameras.main;
|
||||
|
||||
// Message "Appuyez sur l'écran pour visualiser la vidéo"
|
||||
const message = this.add.text(
|
||||
width / 2,
|
||||
height / 2 - 80,
|
||||
'Appuyez sur l\'écran\npour visualiser la vidéo',
|
||||
{
|
||||
fontSize: '32px',
|
||||
color: '#ffffff',
|
||||
align: 'center',
|
||||
fontFamily: 'Arial',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 4,
|
||||
}
|
||||
);
|
||||
message.setOrigin(0.5);
|
||||
|
||||
// Bouton play (cercle avec triangle)
|
||||
const playButton = this.add.container(width / 2, height / 2 + 60);
|
||||
|
||||
const circle = this.add.circle(0, 0, 60, 0x4CAF50, 1);
|
||||
circle.setStrokeStyle(4, 0xffffff);
|
||||
const triangle = this.add.triangle(5, 0, -15, -20, -15, 20, 20, 0, 0xffffff);
|
||||
|
||||
playButton.add([circle, triangle]);
|
||||
circle.setInteractive({ useHandCursor: true });
|
||||
|
||||
// Clic sur le bouton ou n'importe où sur l'écran
|
||||
const startVideo = () => {
|
||||
message.destroy();
|
||||
playButton.destroy();
|
||||
this.playEndVideo();
|
||||
};
|
||||
|
||||
circle.on('pointerdown', startVideo);
|
||||
this.input.once('pointerdown', startVideo);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,8 +101,16 @@ export class EndScene extends Phaser.Scene {
|
||||
this.video.setMute(false);
|
||||
this.video.setLoop(false);
|
||||
|
||||
// IMPORTANT: Forcer explicitement loop=false sur l'élément HTML5 natif
|
||||
// pour éviter que la vidéo se lise en boucle
|
||||
if (this.video.video) {
|
||||
this.video.video.loop = false;
|
||||
console.log('[EndScene] Attribut loop forcé à false sur l\'élément vidéo natif');
|
||||
}
|
||||
|
||||
console.log('[EndScene] Démarrage de la lecture');
|
||||
const started = this.video.play(true); // Autoplay
|
||||
// IMPORTANT: Utiliser play(true) pour l'autoplay après interaction utilisateur (requis pour iOS)
|
||||
const started = this.video.play(true);
|
||||
console.log('[EndScene] Lecture démarrée?', started);
|
||||
|
||||
if (!started) {
|
||||
@@ -85,22 +136,40 @@ export class EndScene extends Phaser.Scene {
|
||||
this.gotoMenu();
|
||||
});
|
||||
|
||||
// Sécurité : timer basé sur la durée de la vidéo + 2 secondes
|
||||
// IMPORTANT: Écouter aussi l'événement natif 'ended' de l'élément HTML5
|
||||
// C'est la méthode la plus fiable pour détecter la fin d'une vidéo
|
||||
if (this.video.video) {
|
||||
this.video.video.addEventListener('ended', () => {
|
||||
console.log('[EndScene] Vidéo terminée (événement HTML5 ended) → passage au menu');
|
||||
this.gotoMenu();
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
// Sécurité : timer basé sur la durée de la vidéo + 1 seconde
|
||||
// Si la vidéo n'est pas finie après sa durée, forcer le passage au menu
|
||||
this.video.on('metadata', () => {
|
||||
if (!this.video || !this.video.video) return;
|
||||
const duration = this.video.getDuration();
|
||||
console.log('[EndScene] Durée de la vidéo:', duration, 'secondes');
|
||||
|
||||
// Timer de sécurité : durée vidéo + 2 secondes
|
||||
this.time.delayedCall((duration + 2) * 1000, () => {
|
||||
// Timer de sécurité : durée vidéo + 1 seconde
|
||||
this.time.delayedCall((duration + 1) * 1000, () => {
|
||||
if (!this.hasFinished) {
|
||||
console.warn('[EndScene] Timer de sécurité déclenché → passage au menu');
|
||||
console.warn('[EndScene] Timer de sécurité (metadata) déclenché → passage au menu');
|
||||
this.gotoMenu();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Timer de sécurité fixe basé sur la durée connue (36 secondes exactement)
|
||||
// Au cas où les métadonnées ne se chargent pas correctement
|
||||
this.time.delayedCall(36000, () => {
|
||||
if (!this.hasFinished) {
|
||||
console.warn('[EndScene] Timer de sécurité fixe (36s) déclenché → passage au menu');
|
||||
this.gotoMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Ajuster si resize
|
||||
this.scale.on('resize', (gameSize: Phaser.Structs.Size) => {
|
||||
if (this.video && this.video.isPlaying()) {
|
||||
@@ -150,13 +219,155 @@ export class EndScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
/**
|
||||
* Passe au menu
|
||||
* Affiche l'image finale après la vidéo
|
||||
*/
|
||||
private gotoMenu(): void {
|
||||
if (this.hasFinished) return;
|
||||
this.hasFinished = true;
|
||||
|
||||
// Détruire la vidéo
|
||||
this.video?.stop();
|
||||
this.video?.destroy();
|
||||
this.scene.start('MenuScene');
|
||||
|
||||
// Afficher l'image finale
|
||||
this.showFinalImage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche l'image finale du joueur puis le message de réussite
|
||||
*/
|
||||
private showFinalImage(): void {
|
||||
// Récupérer le joueur sélectionné
|
||||
const selectedPlayer = this.registry.get('selectedPlayer') as string | undefined;
|
||||
const imageKey = selectedPlayer === 'user2' ? 'finale_user2' : 'finale_user1';
|
||||
|
||||
console.log('[EndScene] Affichage image finale:', imageKey);
|
||||
|
||||
// Afficher l'image en grand (centrée et mise à l'échelle)
|
||||
const finalImage = this.add.image(
|
||||
this.cameras.main.width / 2,
|
||||
this.cameras.main.height / 2,
|
||||
imageKey
|
||||
);
|
||||
|
||||
// Mettre à l'échelle pour s'adapter à l'écran sans dépasser (mode "contain")
|
||||
// Limiter à 85% de la hauteur/largeur pour laisser de l'espace pour le message
|
||||
const maxWidth = this.cameras.main.width * 0.85;
|
||||
const maxHeight = this.cameras.main.height * 0.85;
|
||||
const scaleX = maxWidth / finalImage.width;
|
||||
const scaleY = maxHeight / finalImage.height;
|
||||
const scale = Math.min(scaleX, scaleY); // Min pour contenir sans déborder
|
||||
finalImage.setScale(scale);
|
||||
finalImage.setDepth(0);
|
||||
|
||||
// Après 2 secondes, afficher le message et le bouton
|
||||
this.time.delayedCall(2000, () => {
|
||||
this.showSuccessMessage();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche le message "Niveau 1 réussi!" et le bouton
|
||||
*/
|
||||
private showSuccessMessage(): void {
|
||||
// Message "Niveau 1 réussi!"
|
||||
const successText = this.add.text(
|
||||
this.cameras.main.width / 2,
|
||||
this.cameras.main.height / 2 - 100,
|
||||
'Niveau 1 réussi!',
|
||||
{
|
||||
fontSize: '64px',
|
||||
color: '#FFD700',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 8,
|
||||
fontStyle: 'bold',
|
||||
}
|
||||
);
|
||||
successText.setOrigin(0.5);
|
||||
successText.setDepth(1000);
|
||||
successText.setAlpha(0);
|
||||
|
||||
// Animation d'apparition du texte
|
||||
this.tweens.add({
|
||||
targets: successText,
|
||||
alpha: 1,
|
||||
scale: { from: 0.5, to: 1.2 },
|
||||
duration: 800,
|
||||
ease: 'Back.easeOut',
|
||||
});
|
||||
|
||||
// Bouton "Passer au niveau 2"
|
||||
const buttonY = this.cameras.main.height / 2 + 100;
|
||||
|
||||
// Fond du bouton
|
||||
const buttonBg = this.add.rectangle(
|
||||
this.cameras.main.width / 2,
|
||||
buttonY,
|
||||
400,
|
||||
80,
|
||||
0x4CAF50
|
||||
);
|
||||
buttonBg.setOrigin(0.5);
|
||||
buttonBg.setDepth(999);
|
||||
buttonBg.setInteractive({ useHandCursor: true });
|
||||
buttonBg.setAlpha(0);
|
||||
|
||||
// Texte du bouton
|
||||
const buttonText = this.add.text(
|
||||
this.cameras.main.width / 2,
|
||||
buttonY,
|
||||
'Passer au niveau 2',
|
||||
{
|
||||
fontSize: '32px',
|
||||
color: '#FFFFFF',
|
||||
fontStyle: 'bold',
|
||||
}
|
||||
);
|
||||
buttonText.setOrigin(0.5);
|
||||
buttonText.setDepth(1000);
|
||||
buttonText.setAlpha(0);
|
||||
|
||||
// Animation d'apparition du bouton (un peu après le texte)
|
||||
this.tweens.add({
|
||||
targets: [buttonBg, buttonText],
|
||||
alpha: 1,
|
||||
duration: 600,
|
||||
delay: 400,
|
||||
ease: 'Power2',
|
||||
});
|
||||
|
||||
// Effet hover sur le bouton
|
||||
buttonBg.on('pointerover', () => {
|
||||
buttonBg.setFillStyle(0x66BB6A);
|
||||
this.tweens.add({
|
||||
targets: buttonBg,
|
||||
scaleX: 1.05,
|
||||
scaleY: 1.05,
|
||||
duration: 200,
|
||||
});
|
||||
});
|
||||
|
||||
buttonBg.on('pointerout', () => {
|
||||
buttonBg.setFillStyle(0x4CAF50);
|
||||
this.tweens.add({
|
||||
targets: buttonBg,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
duration: 200,
|
||||
});
|
||||
});
|
||||
|
||||
// Clic sur le bouton → Fin du jeu (retour au menu)
|
||||
buttonBg.on('pointerdown', () => {
|
||||
console.log('[EndScene] Bouton "Passer au niveau 2" cliqué → retour au menu');
|
||||
|
||||
// Flash blanc
|
||||
this.cameras.main.flash(300, 255, 255, 255, true);
|
||||
|
||||
// Retour au menu
|
||||
this.time.delayedCall(300, () => {
|
||||
this.scene.start('MenuScene');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Phaser from 'phaser';
|
||||
import { LEVEL_DURATION, PLAYER_STARTING_LIVES, CHEST_REQUIRED_GIFTS } from '../utils/constants';
|
||||
import { LEVEL_DURATION, PLAYER_STARTING_LIVES, CHEST_REQUIRED_GIFTS, MIN_MUSHROOMS } from '../utils/constants';
|
||||
import { Player } from '../entities/Player';
|
||||
import { GyroControl } from '../controls/GyroControl';
|
||||
import { JumpButton } from '../controls/JumpButton';
|
||||
import { DirectionalButtons } from '../controls/DirectionalButtons';
|
||||
import { SuperTreasure } from '../entities/SuperTreasure';
|
||||
import { TreasureChest } from '../entities/TreasureChest';
|
||||
import { Cage } from '../entities/Cage';
|
||||
|
||||
/**
|
||||
* Scène principale du jeu
|
||||
@@ -24,6 +25,8 @@ export class GameScene extends Phaser.Scene {
|
||||
private gifts?: Phaser.Physics.Arcade.Group;
|
||||
private superTreasures?: Phaser.Physics.Arcade.Group;
|
||||
private treasureChest?: TreasureChest;
|
||||
private cage?: Cage;
|
||||
private hasKey: boolean = false;
|
||||
private bgMusic?: Phaser.Sound.BaseSound;
|
||||
private platformRects: { x: number; y: number; w: number; h: number }[] = [];
|
||||
|
||||
@@ -38,6 +41,11 @@ export class GameScene extends Phaser.Scene {
|
||||
private giftsCollectedText?: Phaser.GameObjects.Text;
|
||||
private volumeText?: Phaser.GameObjects.Text;
|
||||
private sfxVolumeText?: Phaser.GameObjects.Text;
|
||||
private debugText?: Phaser.GameObjects.Text;
|
||||
private lastDebugUpdate: number = 0;
|
||||
private gyroDebugButton?: Phaser.GameObjects.Text;
|
||||
private gyroDomButton?: HTMLButtonElement;
|
||||
private recalibrateDomButton?: HTMLButtonElement;
|
||||
|
||||
// Game state
|
||||
private score: number = 0;
|
||||
@@ -73,8 +81,8 @@ export class GameScene extends Phaser.Scene {
|
||||
this.isMobile = this.sys.game.device.os.android || this.sys.game.device.os.iOS;
|
||||
|
||||
// Configurer les limites du monde physique (IMPORTANT pour permettre mouvement infini)
|
||||
// Étendre un peu plus pour inclure la plateforme finale et le coffre
|
||||
const levelWidth = Math.max(width * 7, 8000);
|
||||
// Étendre encore plus pour inclure la cage du chien après le coffre
|
||||
const levelWidth = Math.max(width * 7, 10500); // Augmenté de 2500px
|
||||
this.physics.world.setBounds(0, 0, levelWidth, height);
|
||||
|
||||
// Créer le background qui défile
|
||||
@@ -97,6 +105,9 @@ export class GameScene extends Phaser.Scene {
|
||||
|
||||
if (!playerBody || !platformBody) return true;
|
||||
|
||||
const isGround = (platform as any).getData?.('isGround') === true;
|
||||
if (isGround) return true;
|
||||
|
||||
// Autoriser la collision uniquement si le joueur vient du dessus
|
||||
// (sa vitesse verticale est positive = tombe, et son bas est au-dessus du haut de la plateforme)
|
||||
return playerBody.velocity.y >= 0 && playerBody.bottom <= platformBody.top + 10;
|
||||
@@ -111,6 +122,8 @@ export class GameScene extends Phaser.Scene {
|
||||
|
||||
// Contrôles PC (clavier)
|
||||
this.cursors = this.input.keyboard?.createCursorKeys();
|
||||
console.log('[GameScene] 🎮 Cursors initialized:', this.cursors ? '✅' : '❌',
|
||||
'Left:', !!this.cursors?.left, 'Right:', !!this.cursors?.right, 'Space:', !!this.cursors?.space);
|
||||
|
||||
// Contrôles Mobile (gyroscope + bouton tactile)
|
||||
if (this.isMobile) {
|
||||
@@ -119,6 +132,9 @@ export class GameScene extends Phaser.Scene {
|
||||
|
||||
// UI
|
||||
this.createUI();
|
||||
this.createDebugOverlay();
|
||||
this.createGyroDomButton();
|
||||
this.createRecalibrateDomButton();
|
||||
|
||||
// Effet neige
|
||||
this.createSnow();
|
||||
@@ -173,13 +189,16 @@ export class GameScene extends Phaser.Scene {
|
||||
private createPlatforms(): void {
|
||||
this.platforms = this.physics.add.staticGroup();
|
||||
|
||||
const width = this.cameras.main.width;
|
||||
const height = this.cameras.main.height;
|
||||
|
||||
// Sol principal (très large pour le niveau 6x)
|
||||
const groundWidth = width * 6;
|
||||
const ground = this.add.rectangle(groundWidth / 2, height - 25, groundWidth, 50, 0x8B4513);
|
||||
// Sol principal (très large pour tout le niveau jusqu'à la cage + 400px)
|
||||
// Utiliser TileSprite pour répéter le motif sur toute la longueur
|
||||
const groundWidth = 10500; // Jusqu'à x=10500 (cage à 10100 + 400px)
|
||||
|
||||
// Sol avec sprite répété (sol_150.png)
|
||||
const ground = this.add.tileSprite(groundWidth / 2, height - 25, groundWidth, 50, 'ground_tile');
|
||||
this.physics.add.existing(ground, true);
|
||||
(ground as any).setData?.('isGround', true);
|
||||
this.platforms.add(ground);
|
||||
this.platformRects.push({ x: groundWidth / 2, y: height - 25, w: groundWidth, h: 50 });
|
||||
|
||||
@@ -219,13 +238,28 @@ export class GameScene extends Phaser.Scene {
|
||||
{ x: 6800, y: height - 450, w: 130, h: 30 },
|
||||
{ x: 7100, y: height - 350, w: 180, h: 30 },
|
||||
|
||||
// Zone 6 (finale)
|
||||
// Zone 6 (finale avant coffre)
|
||||
{ x: 7400, y: height - 250, w: 200, h: 30 },
|
||||
{ x: 7700, y: height - 180, w: 300, h: 30 }, // Grande plateforme finale
|
||||
{ x: 7700, y: height - 180, w: 300, h: 30 }, // Grande plateforme pour coffre
|
||||
|
||||
// Zone 7 (après le coffre - vers la cage du chien) +2500px
|
||||
{ x: 8000, y: height - 200, w: 180, h: 30 },
|
||||
{ x: 8300, y: height - 320, w: 160, h: 30 },
|
||||
{ x: 8600, y: height - 240, w: 200, h: 30 },
|
||||
{ x: 8900, y: height - 380, w: 140, h: 30 },
|
||||
{ x: 9200, y: height - 280, w: 180, h: 30 },
|
||||
{ x: 9500, y: height - 400, w: 150, h: 30 },
|
||||
{ x: 9800, y: height - 200, w: 220, h: 30 },
|
||||
// Grande plateforme finale pour la cage - prolongée jusqu'à la fin (2600px de long)
|
||||
{ x: 9900, y: height - 150, w: 2600, h: 30 }
|
||||
];
|
||||
|
||||
platformPositions.forEach((pos) => {
|
||||
const platform = this.add.rectangle(pos.x, pos.y, pos.w, pos.h, 0x6B8E23);
|
||||
// Choisir le meilleur sprite selon la largeur de la plateforme
|
||||
const spriteKey = this.choosePlatformSprite(pos.w);
|
||||
|
||||
// Créer un TileSprite qui répète automatiquement le motif
|
||||
const platform = this.add.tileSprite(pos.x, pos.y, pos.w, pos.h, spriteKey);
|
||||
this.physics.add.existing(platform, true);
|
||||
this.platforms!.add(platform);
|
||||
this.platformRects.push({ x: pos.x, y: pos.y, w: pos.w, h: pos.h });
|
||||
@@ -234,6 +268,47 @@ export class GameScene extends Phaser.Scene {
|
||||
console.log(`${platformPositions.length} plateformes créées sur ${groundWidth}px`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Choisit le meilleur sprite de plateforme selon la largeur
|
||||
* Stratégie : utiliser le sprite le plus proche de la largeur demandée
|
||||
*/
|
||||
private choosePlatformSprite(width: number): string {
|
||||
// Largeurs des sprites disponibles
|
||||
const sprites = [
|
||||
{ key: 'platform_40', width: 40 },
|
||||
{ key: 'platform_73', width: 73 },
|
||||
{ key: 'platform_130', width: 130 },
|
||||
{ key: 'platform_140', width: 140 },
|
||||
{ key: 'platform_179', width: 179 }
|
||||
];
|
||||
|
||||
// Pour les très grandes plateformes (>200px), toujours utiliser le plus grand
|
||||
if (width >= 200) {
|
||||
return 'platform_179';
|
||||
}
|
||||
|
||||
// Pour les petites/moyennes, choisir le sprite le plus proche
|
||||
// qui divise bien la largeur totale (pour éviter les coupures bizarres)
|
||||
let bestSprite = sprites[sprites.length - 1]; // Par défaut, le plus grand
|
||||
let bestScore = Infinity;
|
||||
|
||||
for (const sprite of sprites) {
|
||||
// Calculer combien de fois le sprite rentre dans la largeur
|
||||
const repetitions = width / sprite.width;
|
||||
const remainder = width % sprite.width;
|
||||
|
||||
// Score = reste (on préfère un reste petit) + pénalité si trop de répétitions
|
||||
const score = remainder + Math.abs(repetitions - Math.round(repetitions)) * 10;
|
||||
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
bestSprite = sprite;
|
||||
}
|
||||
}
|
||||
|
||||
return bestSprite.key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée les groupes d'obstacles, cadeaux et super trésors
|
||||
*/
|
||||
@@ -253,6 +328,37 @@ export class GameScene extends Phaser.Scene {
|
||||
*/
|
||||
private spawnTestObjects(): void {
|
||||
const height = this.cameras.main.height;
|
||||
const minObjectSpacing = 120;
|
||||
const reservedPositions: Array<{ x: number; y: number; radius: number }> = [];
|
||||
const reserve = (x: number, y: number, radius: number): void => {
|
||||
reservedPositions.push({ x, y, radius });
|
||||
};
|
||||
const isTooClose = (x: number, y: number): boolean =>
|
||||
reservedPositions.some((pos) => {
|
||||
const distance = Phaser.Math.Distance.Between(x, y, pos.x, pos.y);
|
||||
return distance < minObjectSpacing + pos.radius;
|
||||
});
|
||||
let obstacleCount = 0;
|
||||
const targetObstacleCount = MIN_MUSHROOMS;
|
||||
const obstacleCandidates: Array<{ x: number; y: number }> = [];
|
||||
const addCandidate = (x: number, y: number): void => {
|
||||
obstacleCandidates.push({ x, y });
|
||||
};
|
||||
|
||||
// SUPER TRESORS (rares et precieux - 1 par zone)
|
||||
const superTreasurePositions = [
|
||||
{ x: 1000, y: height - 350 }, // Zone 1 - en hauteur
|
||||
{ x: 2500, y: height - 420 }, // Zone 2 - tres haut
|
||||
{ x: 3900, y: height - 450 }, // Zone 3 - tres haut
|
||||
{ x: 5400, y: height - 470 }, // Zone 4 - ultra haut
|
||||
{ x: 6800, y: height - 500 }, // Zone 5 - ultra haut
|
||||
{ x: 7300, y: height - 250 }, // Zone 6 - sur plateforme finale
|
||||
];
|
||||
// Coffre posé sur la plateforme à height - 180 (hauteur plateforme = 30px)
|
||||
const chestPosition = { x: 7700, y: height - 180 - 50 }; // -50 pour être bien au-dessus de la plateforme
|
||||
|
||||
superTreasurePositions.forEach((pos) => reserve(pos.x, pos.y, 40));
|
||||
reserve(chestPosition.x, chestPosition.y, 60);
|
||||
|
||||
// BEAUCOUP de cadeaux répartis partout (environ tous les 300-500px)
|
||||
const giftPositions = [
|
||||
@@ -268,6 +374,8 @@ export class GameScene extends Phaser.Scene {
|
||||
5400, 5700, 6000, 6300,
|
||||
// Zone 6
|
||||
6600, 6900, 7200, 7500,
|
||||
// Zone 7 (après le coffre - vers la cage)
|
||||
8000, 8300, 8600, 8900, 9200, 9500, 9800, 10100,
|
||||
];
|
||||
|
||||
giftPositions.forEach((x) => {
|
||||
@@ -275,9 +383,12 @@ export class GameScene extends Phaser.Scene {
|
||||
const isHigh = Math.random() > 0.5;
|
||||
const y = isHigh ? height - 200 - Math.random() * 150 : height - 100;
|
||||
|
||||
const gift = this.add.circle(x, y, 20, 0xFFEB3B);
|
||||
// Utiliser le sprite de cadeau (gift_40.png = 40x40px)
|
||||
const gift = this.add.image(x, y, 'gift_sprite');
|
||||
gift.setScale(1); // Garder la taille native (40x40)
|
||||
this.physics.add.existing(gift);
|
||||
this.gifts!.add(gift);
|
||||
reserve(x, y, 25);
|
||||
});
|
||||
|
||||
// BEAUCOUP d'obstacles répartis partout
|
||||
@@ -294,6 +405,8 @@ export class GameScene extends Phaser.Scene {
|
||||
5600, 5900, 6200, 6500,
|
||||
// Zone 6
|
||||
6800, 7100, 7400,
|
||||
// Zone 7 (après le coffre - vers la cage) - PAS de champignons près de la cage
|
||||
8100, 8400, 8700, 9000,
|
||||
];
|
||||
|
||||
// Obstacles sur plateformes (x, y)
|
||||
@@ -314,48 +427,28 @@ export class GameScene extends Phaser.Scene {
|
||||
const platformPlaced: Array<{ x: number; y: number }> = [];
|
||||
this.platformRects
|
||||
.filter((rect) => rect.y < height - 80 && rect.w >= 120) // ignorer le sol et les très petites plateformes
|
||||
.forEach((rect, idx) => {
|
||||
// Placer un champignon sur ~1 plateforme sur 2 pour ne pas surcharger
|
||||
if (idx % 2 !== 0) return;
|
||||
.forEach((rect) => {
|
||||
const x = rect.x + Phaser.Math.Between(Math.round(-rect.w / 4), Math.round(rect.w / 4));
|
||||
const y = rect.y - rect.h / 2;
|
||||
platformPlaced.push({ x, y });
|
||||
});
|
||||
|
||||
obstaclePositions.filter((_, idx) => idx % 2 === 0).forEach((x) => {
|
||||
const obstacle = this.physics.add.sprite(x, height - 50, 'obstacle_mushroom');
|
||||
obstacle.setOrigin(0.5, 1); // ancré sur les pieds
|
||||
obstacle.setImmovable(true);
|
||||
obstacle.setPushable(false);
|
||||
obstacle.setScale(0.9);
|
||||
obstaclePositions.forEach((x) => addCandidate(x, height - 50));
|
||||
|
||||
const body = obstacle.body as Phaser.Physics.Arcade.Body;
|
||||
body.setAllowGravity(false);
|
||||
body.setSize(45, 81); // hitbox 10% plus petite avec le scale
|
||||
body.setOffset((obstacle.displayWidth - 45) / 2, obstacle.displayHeight - 81);
|
||||
|
||||
this.obstacles!.add(obstacle);
|
||||
});
|
||||
|
||||
obstaclePlatforms.filter((_, idx) => idx % 2 === 0).forEach((pos) => {
|
||||
// Trouver la plateforme la plus proche à cet x
|
||||
obstaclePlatforms.forEach((pos) => {
|
||||
const target = this.platformRects.find((rect) => Math.abs(pos.x - rect.x) <= rect.w / 2);
|
||||
const topY = target ? target.y - target.h / 2 : pos.y;
|
||||
const obstacle = this.physics.add.sprite(pos.x, topY, 'obstacle_mushroom');
|
||||
obstacle.setOrigin(0.5, 1);
|
||||
obstacle.setImmovable(true);
|
||||
obstacle.setPushable(false);
|
||||
obstacle.setScale(0.9);
|
||||
|
||||
const body = obstacle.body as Phaser.Physics.Arcade.Body;
|
||||
body.setAllowGravity(false);
|
||||
body.setSize(45, 81);
|
||||
body.setOffset((obstacle.displayWidth - 45) / 2, obstacle.displayHeight - 81);
|
||||
|
||||
this.obstacles!.add(obstacle);
|
||||
addCandidate(pos.x, topY);
|
||||
});
|
||||
|
||||
platformPlaced.filter((_, idx) => idx % 2 === 0).forEach((pos) => {
|
||||
platformPlaced.forEach((pos) => addCandidate(pos.x, pos.y));
|
||||
|
||||
Phaser.Utils.Array.Shuffle(obstacleCandidates);
|
||||
|
||||
obstacleCandidates.forEach((pos) => {
|
||||
if (obstacleCount >= targetObstacleCount) return;
|
||||
if (isTooClose(pos.x, pos.y)) return;
|
||||
|
||||
const obstacle = this.physics.add.sprite(pos.x, pos.y, 'obstacle_mushroom');
|
||||
obstacle.setOrigin(0.5, 1);
|
||||
obstacle.setImmovable(true);
|
||||
@@ -368,17 +461,16 @@ export class GameScene extends Phaser.Scene {
|
||||
body.setOffset((obstacle.displayWidth - 45) / 2, obstacle.displayHeight - 81);
|
||||
|
||||
this.obstacles!.add(obstacle);
|
||||
obstacleCount += 1;
|
||||
reserve(pos.x, pos.y, 50);
|
||||
});
|
||||
|
||||
// SUPER TRÉSORS (rares et précieux - 1 par zone)
|
||||
const superTreasurePositions = [
|
||||
{ x: 1000, y: height - 350 }, // Zone 1 - en hauteur
|
||||
{ x: 2500, y: height - 420 }, // Zone 2 - très haut
|
||||
{ x: 3900, y: height - 450 }, // Zone 3 - très haut
|
||||
{ x: 5400, y: height - 470 }, // Zone 4 - ultra haut
|
||||
{ x: 6800, y: height - 500 }, // Zone 5 - ultra haut
|
||||
{ x: 7300, y: height - 250 }, // Zone 6 - sur plateforme finale
|
||||
];
|
||||
if (obstacleCount < targetObstacleCount) {
|
||||
console.warn(
|
||||
`⚠️ Champignons insuffisants: ${obstacleCount}/${targetObstacleCount}. ` +
|
||||
'Augmentez les positions candidates ou reduisez le minObjectSpacing.'
|
||||
);
|
||||
}
|
||||
|
||||
superTreasurePositions.forEach((pos) => {
|
||||
const superTreasure = new SuperTreasure(this, pos.x, pos.y);
|
||||
@@ -386,10 +478,32 @@ export class GameScene extends Phaser.Scene {
|
||||
});
|
||||
|
||||
// COFFRE FINAL au bout du niveau
|
||||
this.treasureChest = new TreasureChest(this, 7700, height - 300, CHEST_REQUIRED_GIFTS);
|
||||
this.treasureChest = new TreasureChest(this, chestPosition.x, chestPosition.y, CHEST_REQUIRED_GIFTS);
|
||||
this.physics.add.overlap(this.player!, this.treasureChest, this.openChest, undefined, this);
|
||||
|
||||
console.log(`${giftPositions.length} cadeaux, ${obstaclePositions.length} obstacles, ${superTreasurePositions.length} SUPER TRÉSORS et 1 COFFRE FINAL créés`);
|
||||
// CAGE avec le chien à la toute fin
|
||||
this.cage = new Cage(this, 10100, height - 150);
|
||||
|
||||
console.log(`${giftPositions.length} cadeaux, ${obstaclePositions.length} obstacles, ${superTreasurePositions.length} SUPER TRÉSORS, 1 COFFRE FINAL et 1 CAGE créés`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Débloque l'audio sur iOS (requis après interaction utilisateur)
|
||||
*/
|
||||
private unlockAudio(): void {
|
||||
// Sur iOS, l'audio doit être activé après une interaction utilisateur
|
||||
const soundManager = this.sound as Phaser.Sound.WebAudioSoundManager;
|
||||
|
||||
if (soundManager.context) {
|
||||
// Si le contexte est suspendu, le reprendre
|
||||
if (soundManager.context.state === 'suspended') {
|
||||
soundManager.context.resume().then(() => {
|
||||
console.log('[GameScene Audio] ✅ Context audio débloqué pour iOS');
|
||||
}).catch((error: any) => {
|
||||
console.error('[GameScene Audio] ❌ Erreur déblocage audio:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,6 +515,8 @@ export class GameScene extends Phaser.Scene {
|
||||
|
||||
// Bouton de saut
|
||||
this.jumpButton = new JumpButton(this, () => {
|
||||
// IMPORTANT: Débloquer l'audio sur iOS lors de l'interaction
|
||||
this.unlockAudio();
|
||||
this.player?.jump();
|
||||
});
|
||||
|
||||
@@ -485,6 +601,8 @@ export class GameScene extends Phaser.Scene {
|
||||
backButton.setDepth(100);
|
||||
backButton.setInteractive({ useHandCursor: true });
|
||||
backButton.on('pointerdown', () => {
|
||||
// IMPORTANT: Débloquer l'audio sur iOS
|
||||
this.unlockAudio();
|
||||
this.cleanup();
|
||||
this.scene.start('MenuScene');
|
||||
});
|
||||
@@ -501,6 +619,8 @@ export class GameScene extends Phaser.Scene {
|
||||
this.volumeText.setDepth(100);
|
||||
this.volumeText.setInteractive({ useHandCursor: true });
|
||||
this.volumeText.on('pointerdown', () => {
|
||||
// IMPORTANT: Débloquer l'audio sur iOS
|
||||
this.unlockAudio();
|
||||
this.cycleMusicVolume();
|
||||
});
|
||||
|
||||
@@ -516,10 +636,247 @@ export class GameScene extends Phaser.Scene {
|
||||
this.sfxVolumeText.setDepth(100);
|
||||
this.sfxVolumeText.setInteractive({ useHandCursor: true });
|
||||
this.sfxVolumeText.on('pointerdown', () => {
|
||||
// IMPORTANT: Débloquer l'audio sur iOS
|
||||
this.unlockAudio();
|
||||
this.cycleSfxVolume();
|
||||
});
|
||||
}
|
||||
|
||||
private createDebugOverlay(): void {
|
||||
if (!this.isMobile) return;
|
||||
|
||||
this.debugText = this.add.text(10, 130, '', {
|
||||
fontSize: '12px',
|
||||
color: '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 6, y: 6 },
|
||||
});
|
||||
this.debugText.setOrigin(0, 0);
|
||||
this.debugText.setScrollFactor(0);
|
||||
this.debugText.setDepth(200);
|
||||
|
||||
this.gyroDebugButton = this.add.text(10, 70, 'Autoriser gyroscope', {
|
||||
fontSize: '12px',
|
||||
color: '#00ff00',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 6, y: 4 },
|
||||
});
|
||||
this.gyroDebugButton.setOrigin(0, 0);
|
||||
this.gyroDebugButton.setScrollFactor(0);
|
||||
this.gyroDebugButton.setDepth(200);
|
||||
this.gyroDebugButton.setInteractive({ useHandCursor: true });
|
||||
this.gyroDebugButton.on('pointerdown', () => {
|
||||
// IMPORTANT: Débloquer l'audio sur iOS
|
||||
this.unlockAudio();
|
||||
this.requestGyroPermission();
|
||||
});
|
||||
}
|
||||
|
||||
private createGyroDomButton(): void {
|
||||
if (!this.isMobile || this.gyroDomButton) return;
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'Autoriser gyroscope';
|
||||
button.style.position = 'fixed';
|
||||
button.style.right = '8px';
|
||||
button.style.top = '8px';
|
||||
button.style.zIndex = '9999';
|
||||
button.style.padding = '6px 10px';
|
||||
button.style.fontSize = '12px';
|
||||
button.style.background = '#000000';
|
||||
button.style.color = '#00ff00';
|
||||
button.style.border = '1px solid #00ff00';
|
||||
button.style.borderRadius = '4px';
|
||||
button.style.cursor = 'pointer';
|
||||
button.style.opacity = '0.9';
|
||||
button.addEventListener('click', () => {
|
||||
this.requestGyroPermission();
|
||||
});
|
||||
|
||||
document.body.appendChild(button);
|
||||
this.gyroDomButton = button;
|
||||
|
||||
this.events.once('shutdown', () => {
|
||||
this.removeGyroDomButton();
|
||||
});
|
||||
this.events.once('destroy', () => {
|
||||
this.removeGyroDomButton();
|
||||
});
|
||||
}
|
||||
|
||||
private removeGyroDomButton(): void {
|
||||
if (!this.gyroDomButton) return;
|
||||
this.gyroDomButton.remove();
|
||||
this.gyroDomButton = undefined;
|
||||
}
|
||||
|
||||
private createRecalibrateDomButton(): void {
|
||||
if (!this.isMobile || this.recalibrateDomButton) return;
|
||||
|
||||
// Détecter iOS
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||
|
||||
// Bouton de recalibration uniquement sur iOS
|
||||
if (!isIOS) return;
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.textContent = '🔄 Recalibrer';
|
||||
button.style.position = 'fixed';
|
||||
button.style.right = '8px';
|
||||
button.style.top = '48px'; // En dessous du bouton gyroscope
|
||||
button.style.zIndex = '9999';
|
||||
button.style.padding = '8px 12px';
|
||||
button.style.fontSize = '14px';
|
||||
button.style.background = '#FF9500';
|
||||
button.style.color = '#ffffff';
|
||||
button.style.border = '2px solid #ffffff';
|
||||
button.style.borderRadius = '6px';
|
||||
button.style.cursor = 'pointer';
|
||||
button.style.opacity = '0.95';
|
||||
button.style.fontWeight = 'bold';
|
||||
button.addEventListener('click', () => {
|
||||
this.recalibrateGyroscope();
|
||||
});
|
||||
|
||||
document.body.appendChild(button);
|
||||
this.recalibrateDomButton = button;
|
||||
|
||||
this.events.once('shutdown', () => {
|
||||
this.removeRecalibrateDomButton();
|
||||
});
|
||||
this.events.once('destroy', () => {
|
||||
this.removeRecalibrateDomButton();
|
||||
});
|
||||
}
|
||||
|
||||
private removeRecalibrateDomButton(): void {
|
||||
if (!this.recalibrateDomButton) return;
|
||||
this.recalibrateDomButton.remove();
|
||||
this.recalibrateDomButton = undefined;
|
||||
}
|
||||
|
||||
private recalibrateGyroscope(): void {
|
||||
if (!this.gyroControl) return;
|
||||
|
||||
// Afficher un message temporaire
|
||||
const message = document.createElement('div');
|
||||
message.textContent = '📱 Maintenez le téléphone en position neutre...';
|
||||
message.style.position = 'fixed';
|
||||
message.style.top = '50%';
|
||||
message.style.left = '50%';
|
||||
message.style.transform = 'translate(-50%, -50%)';
|
||||
message.style.zIndex = '10000';
|
||||
message.style.padding = '20px 30px';
|
||||
message.style.fontSize = '18px';
|
||||
message.style.background = 'rgba(0, 0, 0, 0.9)';
|
||||
message.style.color = '#ffffff';
|
||||
message.style.border = '3px solid #FF9500';
|
||||
message.style.borderRadius = '12px';
|
||||
message.style.fontWeight = 'bold';
|
||||
message.style.textAlign = 'center';
|
||||
document.body.appendChild(message);
|
||||
|
||||
// Recalibrer après un court délai (laisser le temps à l'utilisateur de voir le message)
|
||||
setTimeout(() => {
|
||||
this.gyroControl?.calibrate();
|
||||
message.textContent = '✅ Calibration en cours...';
|
||||
|
||||
// Retirer le message après 3 secondes (temps de calibration: 20 échantillons)
|
||||
setTimeout(() => {
|
||||
message.remove();
|
||||
}, 3000);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private showInitialCalibrationMessage(): void {
|
||||
const message = document.createElement('div');
|
||||
message.innerHTML = '📱 <strong>Calibration automatique</strong><br><br>Maintenez le téléphone en position neutre pendant 2 secondes...<br><br><small>Utilisez le bouton "🔄 Recalibrer" pour réinitialiser si besoin</small>';
|
||||
message.style.position = 'fixed';
|
||||
message.style.top = '50%';
|
||||
message.style.left = '50%';
|
||||
message.style.transform = 'translate(-50%, -50%)';
|
||||
message.style.zIndex = '10000';
|
||||
message.style.padding = '25px 35px';
|
||||
message.style.fontSize = '16px';
|
||||
message.style.background = 'rgba(0, 0, 0, 0.95)';
|
||||
message.style.color = '#ffffff';
|
||||
message.style.border = '3px solid #4CAF50';
|
||||
message.style.borderRadius = '12px';
|
||||
message.style.fontWeight = 'normal';
|
||||
message.style.textAlign = 'center';
|
||||
message.style.lineHeight = '1.5';
|
||||
document.body.appendChild(message);
|
||||
|
||||
// Retirer le message après 4 secondes
|
||||
setTimeout(() => {
|
||||
message.remove();
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
private async requestGyroPermission(): Promise<void> {
|
||||
const hasDeviceOrientation =
|
||||
typeof window.DeviceOrientationEvent !== 'undefined';
|
||||
const requestOrientationPermission =
|
||||
(window.DeviceOrientationEvent as any)?.requestPermission;
|
||||
const requestMotionPermission =
|
||||
(window.DeviceMotionEvent as any)?.requestPermission;
|
||||
const isIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
|
||||
const userActivation = (navigator as any).userActivation;
|
||||
|
||||
if (isIOS && !window.isSecureContext) {
|
||||
this.registry.set('gyroPermission', 'blocked');
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasDeviceOrientation && typeof requestOrientationPermission === 'function') {
|
||||
try {
|
||||
const orientationStatus = await requestOrientationPermission.call(
|
||||
window.DeviceOrientationEvent
|
||||
);
|
||||
let motionStatus = 'granted';
|
||||
if (typeof requestMotionPermission === 'function') {
|
||||
motionStatus = await requestMotionPermission.call(
|
||||
window.DeviceMotionEvent
|
||||
);
|
||||
}
|
||||
|
||||
if (orientationStatus === 'granted' && motionStatus === 'granted') {
|
||||
this.registry.set('gyroPermission', 'granted');
|
||||
this.removeGyroDomButton();
|
||||
|
||||
// Afficher un message de calibration pour iOS
|
||||
if (isIOS) {
|
||||
this.showInitialCalibrationMessage();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const permState =
|
||||
orientationStatus === 'granted' || motionStatus === 'granted'
|
||||
? 'partial'
|
||||
: 'denied';
|
||||
this.registry.set('gyroPermission', permState);
|
||||
this.registry.set(
|
||||
'gyroPermissionError',
|
||||
`activation ${userActivation?.isActive}/${userActivation?.hasBeenActive}`
|
||||
);
|
||||
return;
|
||||
} catch (error) {
|
||||
const errorText =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
this.registry.set('gyroPermission', 'error');
|
||||
this.registry.set(
|
||||
'gyroPermissionError',
|
||||
`${errorText} (activation ${userActivation?.isActive}/${userActivation?.hasBeenActive})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.registry.set('gyroPermission', 'not-required');
|
||||
}
|
||||
|
||||
update(time: number): void {
|
||||
if (!this.player) return;
|
||||
|
||||
@@ -529,36 +886,85 @@ export class GameScene extends Phaser.Scene {
|
||||
// Gestion des contrôles
|
||||
let direction = 0;
|
||||
|
||||
// PC : Clavier
|
||||
if (this.cursors) {
|
||||
// PC/Laptop : Clavier (priorité absolue)
|
||||
if (!this.isMobile && this.cursors) {
|
||||
if (this.cursors.left.isDown) {
|
||||
direction = -1;
|
||||
if (time % 1000 < 16) console.log('⬅️ Left pressed');
|
||||
} else if (this.cursors.right.isDown) {
|
||||
direction = 1;
|
||||
if (time % 1000 < 16) console.log('➡️ Right pressed');
|
||||
}
|
||||
|
||||
// Saut avec Espace
|
||||
if (Phaser.Input.Keyboard.JustDown(this.cursors.space!)) {
|
||||
console.log('🚀 Space pressed - Jump!');
|
||||
this.player.jump();
|
||||
}
|
||||
|
||||
// Saut avec flèche haut (alternative)
|
||||
if (Phaser.Input.Keyboard.JustDown(this.cursors.up!)) {
|
||||
console.log('⬆️ Up pressed - Jump!');
|
||||
this.player.jump();
|
||||
}
|
||||
} else if (!this.isMobile) {
|
||||
// Debug: pourquoi le clavier ne fonctionne pas?
|
||||
if (time % 2000 < 16) {
|
||||
console.log('[GameScene] ⚠️ Keyboard disabled: isMobile=', this.isMobile, 'cursors=', !!this.cursors);
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile : boutons directionnels priment, sinon gyroscope
|
||||
// Mobile/Tablette : boutons directionnels priment, sinon gyroscope
|
||||
if (this.isMobile) {
|
||||
// Vérifier d'abord les boutons directionnels (priorité)
|
||||
const dirButtons = this.directionalButtons?.getDirection() ?? 0;
|
||||
if (dirButtons !== 0) {
|
||||
direction = dirButtons;
|
||||
} else if (this.gyroControl) {
|
||||
// Sinon utiliser le gyroscope
|
||||
const tiltValue = this.gyroControl.getTiltValue();
|
||||
direction = tiltValue; // -1 à 1
|
||||
}
|
||||
}
|
||||
|
||||
// Debug overlay (throttled)
|
||||
if (this.debugText && this.gyroControl && time - this.lastDebugUpdate > 500) {
|
||||
const info = this.gyroControl.getDebugInfo();
|
||||
const now = Date.now();
|
||||
const lastEventMs =
|
||||
info.lastEventAt > 0 ? Math.round(now - info.lastEventAt) : -1;
|
||||
const permState = this.registry.get('gyroPermission') || 'unknown';
|
||||
const permError = this.registry.get('gyroPermissionError') || '';
|
||||
|
||||
// Déterminer quel axe est utilisé actuellement
|
||||
const axisUsed = info.useAlpha ? 'ALPHA' : (info.useGamma ? 'GAMMA' : 'BETA');
|
||||
|
||||
this.debugText.setText(
|
||||
[
|
||||
`perm: ${permState}`,
|
||||
permError ? `err: ${permError}` : '',
|
||||
`platform: ${info.platform}`,
|
||||
`gyro active: ${info.active}`,
|
||||
`--- AXES (raw values) ---`,
|
||||
`ALPHA: ${info.alpha.toFixed(1)}° (boussole/Z)`,
|
||||
`BETA: ${info.beta.toFixed(1)}° (avant/arrière/X)`,
|
||||
`GAMMA: ${info.gamma.toFixed(1)}° (gauche/droite/Y)`,
|
||||
`--- CONTROL ---`,
|
||||
`Axe utilisé: ${axisUsed}`,
|
||||
`tilt normalized: ${info.tilt.toFixed(2)}`,
|
||||
`orientation: ${info.angle}°`,
|
||||
`last event: ${lastEventMs}ms`,
|
||||
]
|
||||
.filter((line) => line !== '')
|
||||
.join('\n')
|
||||
);
|
||||
this.lastDebugUpdate = time;
|
||||
}
|
||||
|
||||
if (this.gyroDomButton && this.registry.get('gyroPermission') === 'granted') {
|
||||
this.removeGyroDomButton();
|
||||
}
|
||||
|
||||
// Déplacer le joueur
|
||||
this.player.move(direction);
|
||||
this.player.update();
|
||||
@@ -577,6 +983,43 @@ export class GameScene extends Phaser.Scene {
|
||||
if (this.background) {
|
||||
this.background.tilePositionX = this.cameras.main.scrollX * 0.3;
|
||||
}
|
||||
|
||||
// Vérifier la proximité avec le coffre pour afficher le message si pas assez de cadeaux
|
||||
if (this.treasureChest && !this.treasureChest.getIsOpen()) {
|
||||
const distanceToChest = Phaser.Math.Distance.Between(
|
||||
this.player.x,
|
||||
this.player.y,
|
||||
7700, // Position X du coffre
|
||||
this.cameras.main.height - 180 // Position Y du coffre
|
||||
);
|
||||
|
||||
// Si proche du coffre (moins de 150px) et pas assez de cadeaux, afficher le message
|
||||
if (distanceToChest < 150 && this.giftsCollected < CHEST_REQUIRED_GIFTS) {
|
||||
this.treasureChest.showRequirementText();
|
||||
} else {
|
||||
this.treasureChest.hideRequirementText();
|
||||
}
|
||||
}
|
||||
|
||||
// Mise à jour du chien dans la cage
|
||||
if (this.cage) {
|
||||
this.cage.update();
|
||||
|
||||
// Vérifier si le joueur est près de la cage avec la clé
|
||||
if (this.hasKey && !this.cage.isOpened()) {
|
||||
const distance = Phaser.Math.Distance.Between(
|
||||
this.player.x,
|
||||
this.player.y,
|
||||
10100, // Position X de la cage
|
||||
this.cameras.main.height - 150 // Position Y de la cage
|
||||
);
|
||||
|
||||
// Si le joueur est assez proche (moins de 100px)
|
||||
if (distance < 100) {
|
||||
this.openCageAndCompleteLevel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -699,17 +1142,22 @@ export class GameScene extends Phaser.Scene {
|
||||
this.addScore(500);
|
||||
this.playSfx('sfx_super', 0.6);
|
||||
|
||||
// BONUS DE TEMPS : +5 secondes
|
||||
this.timeRemaining += 5;
|
||||
console.log('⏱️ Super Trésor : +5 secondes ! Temps restant:', this.timeRemaining);
|
||||
|
||||
// Message spécial
|
||||
const bonusText = this.add.text(
|
||||
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||
this.cameras.main.height / 2,
|
||||
'★ SUPER TRÉSOR +500 ★',
|
||||
'★ SUPER TRÉSOR +500 ★\n⏱️ +5 SECONDES ⏱️',
|
||||
{
|
||||
fontSize: '48px',
|
||||
color: '#FFD700',
|
||||
stroke: '#FF8C00',
|
||||
strokeThickness: 6,
|
||||
fontStyle: 'bold',
|
||||
align: 'center',
|
||||
}
|
||||
);
|
||||
bonusText.setOrigin(0.5);
|
||||
@@ -733,17 +1181,23 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ouvre le coffre au trésor final
|
||||
* Ouvre le coffre au trésor final et donne la clé
|
||||
*/
|
||||
private openChest(_player: any, chest: any): void {
|
||||
if (chest.canOpen(this.giftsCollected)) {
|
||||
const bonus = chest.open(this);
|
||||
this.addScore(bonus);
|
||||
const result = chest.open(this);
|
||||
this.addScore(result.bonus);
|
||||
|
||||
// VICTOIRE ! Lancer l'animation de fin
|
||||
this.time.delayedCall(2000, () => {
|
||||
this.levelComplete();
|
||||
});
|
||||
// Donner la clé au joueur avec animation
|
||||
if (result.hasKey) {
|
||||
this.hasKey = true;
|
||||
console.log('🗝️ Clé obtenue ! Direction la cage du chien !');
|
||||
|
||||
// Créer une clé animée qui vole vers le joueur
|
||||
this.createKeyPickupAnimation();
|
||||
}
|
||||
|
||||
// Ne PAS terminer le niveau ici - continuer vers la cage
|
||||
} else if (!chest.getIsOpen()) {
|
||||
// Pas assez de cadeaux
|
||||
const remaining = chest.getRequiredGifts() - this.giftsCollected;
|
||||
@@ -772,6 +1226,139 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation de récupération de la clé
|
||||
*/
|
||||
private createKeyPickupAnimation(): void {
|
||||
// Créer une grosse icône de clé au centre de l'écran
|
||||
const keyIcon = this.add.text(
|
||||
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||
this.cameras.main.height / 2,
|
||||
'🗝️',
|
||||
{
|
||||
fontSize: '120px',
|
||||
}
|
||||
);
|
||||
keyIcon.setOrigin(0.5);
|
||||
keyIcon.setScrollFactor(0);
|
||||
keyIcon.setDepth(10000); // Augmenté pour être au-dessus de tout (neige = 2000)
|
||||
keyIcon.setAlpha(0);
|
||||
keyIcon.setScale(0.1);
|
||||
|
||||
// Message "Clé obtenue"
|
||||
const keyText = this.add.text(
|
||||
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||
this.cameras.main.height / 2 + 100,
|
||||
'🗝️ CLÉ OBTENUE ! 🗝️\nDirection : Cage du chien →',
|
||||
{
|
||||
fontSize: '40px', // Augmenté pour être plus visible
|
||||
color: '#FFD700',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 8, // Augmenté pour meilleure visibilité
|
||||
fontStyle: 'bold',
|
||||
align: 'center',
|
||||
}
|
||||
);
|
||||
keyText.setOrigin(0.5);
|
||||
keyText.setScrollFactor(0);
|
||||
keyText.setDepth(10000); // Augmenté pour être au-dessus de tout (neige = 2000)
|
||||
keyText.setAlpha(0);
|
||||
|
||||
// Animation de la clé : apparition avec rotation
|
||||
this.tweens.add({
|
||||
targets: keyIcon,
|
||||
scale: 1.5,
|
||||
alpha: 1,
|
||||
angle: 360,
|
||||
duration: 800,
|
||||
ease: 'Back.easeOut',
|
||||
});
|
||||
|
||||
// Animation du texte : apparition
|
||||
this.tweens.add({
|
||||
targets: keyText,
|
||||
alpha: 1,
|
||||
duration: 600,
|
||||
delay: 400,
|
||||
ease: 'Power2',
|
||||
});
|
||||
|
||||
// Après 2 secondes, faire disparaître tout
|
||||
this.time.delayedCall(2500, () => {
|
||||
this.tweens.add({
|
||||
targets: [keyIcon, keyText],
|
||||
alpha: 0,
|
||||
scale: 0.5,
|
||||
duration: 500,
|
||||
ease: 'Power2',
|
||||
onComplete: () => {
|
||||
keyIcon.destroy();
|
||||
keyText.destroy();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Flash doré pour attirer l'attention
|
||||
this.cameras.main.flash(300, 255, 215, 0, true);
|
||||
|
||||
// Son (si disponible)
|
||||
this.playSfx('sfx_powerup', 0.7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ouvre la cage avec la clé et termine le niveau
|
||||
*/
|
||||
private openCageAndCompleteLevel(): void {
|
||||
if (!this.cage || this.cage.isOpened()) return;
|
||||
|
||||
console.log('🗝️ Ouverture de la cage avec la clé !');
|
||||
|
||||
// Ouvrir la cage
|
||||
this.cage.open();
|
||||
|
||||
// Jouer la musique du chien libéré
|
||||
this.playSfx('sfx_chien', 0.7);
|
||||
|
||||
// Message de libération
|
||||
const liberationText = this.add.text(
|
||||
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||
this.cameras.main.height / 2 - 100,
|
||||
'🐕 CHIEN LIBÉRÉ ! 🐕\n🎉 NIVEAU TERMINÉ ! 🎉',
|
||||
{
|
||||
fontSize: '48px',
|
||||
color: '#FFD700',
|
||||
stroke: '#FF4500',
|
||||
strokeThickness: 8,
|
||||
fontStyle: 'bold',
|
||||
align: 'center',
|
||||
}
|
||||
);
|
||||
liberationText.setOrigin(0.5);
|
||||
liberationText.setScrollFactor(0);
|
||||
liberationText.setDepth(2000);
|
||||
|
||||
// Flash de victoire
|
||||
this.cameras.main.flash(500, 255, 215, 0, true);
|
||||
|
||||
// Animation du texte
|
||||
this.tweens.add({
|
||||
targets: liberationText,
|
||||
scaleX: 1.3,
|
||||
scaleY: 1.3,
|
||||
alpha: 0,
|
||||
duration: 3000,
|
||||
ease: 'Power2',
|
||||
onComplete: () => {
|
||||
liberationText.destroy();
|
||||
},
|
||||
});
|
||||
|
||||
// Après 3 secondes, terminer le niveau
|
||||
this.time.delayedCall(3000, () => {
|
||||
this.levelComplete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation de victoire - Niveau terminé !
|
||||
*/
|
||||
|
||||
171
src/scenes/IntroVideoScene.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import Phaser from 'phaser';
|
||||
|
||||
/**
|
||||
* Scene de lecture de la vidéo d'intro en mode paysage (User1 uniquement).
|
||||
*/
|
||||
export class IntroVideoScene extends Phaser.Scene {
|
||||
private video?: Phaser.GameObjects.Video;
|
||||
private playButton?: Phaser.GameObjects.Container;
|
||||
private hasFinished: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super({ key: 'IntroVideoScene' });
|
||||
}
|
||||
|
||||
create(): void {
|
||||
this.cameras.main.setBackgroundColor('#000000');
|
||||
this.showPlayButton();
|
||||
}
|
||||
|
||||
private showPlayButton(): void {
|
||||
const { width, height } = this.cameras.main;
|
||||
this.playButton = this.add.container(width / 2, height / 2);
|
||||
|
||||
const circle = this.add.circle(0, 0, 60, 0x4CAF50, 1);
|
||||
circle.setStrokeStyle(4, 0xffffff);
|
||||
const triangle = this.add.triangle(5, 0, -15, -20, -15, 20, 20, 0, 0xffffff);
|
||||
const text = this.add.text(0, 100, 'Appuyez pour demarrer la video', {
|
||||
fontSize: '20px',
|
||||
color: '#ffffff',
|
||||
align: 'center',
|
||||
fontFamily: 'Arial',
|
||||
});
|
||||
text.setOrigin(0.5);
|
||||
|
||||
this.playButton.add([circle, triangle, text]);
|
||||
circle.setInteractive({ useHandCursor: true });
|
||||
circle.on('pointerdown', () => {
|
||||
this.playButton?.destroy();
|
||||
this.playButton = undefined;
|
||||
this.playIntroVideo();
|
||||
});
|
||||
}
|
||||
|
||||
private playIntroVideo(): void {
|
||||
const { width, height } = this.cameras.main;
|
||||
|
||||
// Déterminer quelle vidéo jouer selon le joueur sélectionné
|
||||
const selectedPlayer = this.registry.get('selectedPlayer') as string | undefined;
|
||||
const videoKey = selectedPlayer === 'user2' ? 'intro_user2' : 'intro_user1';
|
||||
|
||||
console.log('[IntroVideoScene] Lecture de la vidéo:', videoKey);
|
||||
console.log('[IntroVideoScene] Vidéo dans cache?', this.cache.video.exists(videoKey));
|
||||
|
||||
if (!this.cache.video.exists(videoKey)) {
|
||||
console.warn(`[IntroVideoScene] Vidéo ${videoKey} non trouvée, passage au menu`);
|
||||
this.gotoMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[IntroVideoScene] Création de l\'objet vidéo');
|
||||
this.video = this.add.video(width / 2, height / 2, videoKey);
|
||||
this.video.setOrigin(0.5);
|
||||
this.video.setDepth(1000);
|
||||
|
||||
// Attendre que les métadonnées soient chargées
|
||||
this.video.on('metadata', () => {
|
||||
if (!this.video) return;
|
||||
|
||||
const videoWidth = this.video.video?.videoWidth || 1280;
|
||||
const videoHeight = this.video.video?.videoHeight || 720;
|
||||
|
||||
console.log('[IntroVideoScene] Métadonnées vidéo chargées:', videoWidth, 'x', videoHeight);
|
||||
|
||||
this.video.setSize(videoWidth, videoHeight);
|
||||
this.updateVideoSize();
|
||||
});
|
||||
|
||||
// Activer l'audio
|
||||
this.video.setMute(false);
|
||||
this.video.setLoop(false);
|
||||
|
||||
console.log('[IntroVideoScene] Démarrage de la lecture');
|
||||
// IMPORTANT: Utiliser play(true) pour l'autoplay après interaction utilisateur (requis pour iOS)
|
||||
const started = this.video.play(true);
|
||||
console.log('[IntroVideoScene] Lecture démarrée?', started);
|
||||
|
||||
if (!started) {
|
||||
console.warn('[IntroVideoScene] Lecture vidéo bloquée → passage au menu');
|
||||
this.gotoMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
// Événement de fin de vidéo
|
||||
this.video.once('complete', () => {
|
||||
console.log('[IntroVideoScene] Vidéo terminée (événement complete) → passage au menu');
|
||||
this.gotoMenu();
|
||||
});
|
||||
|
||||
// Événement alternatif 'stop' au cas où 'complete' ne se déclenche pas
|
||||
this.video.once('stop', () => {
|
||||
console.log('[IntroVideoScene] Vidéo arrêtée (événement stop) → passage au menu');
|
||||
this.gotoMenu();
|
||||
});
|
||||
|
||||
this.video.once('error', (err: any) => {
|
||||
console.error('[IntroVideoScene] Erreur lecture vidéo:', err);
|
||||
this.gotoMenu();
|
||||
});
|
||||
|
||||
// Timer de sécurité basé sur la durée de la vidéo
|
||||
// IMPORTANT pour iOS où 'complete' ne se déclenche pas toujours
|
||||
this.video.on('metadata', () => {
|
||||
if (!this.video || !this.video.video) return;
|
||||
const duration = this.video.getDuration();
|
||||
console.log('[IntroVideoScene] Durée de la vidéo:', duration, 'secondes');
|
||||
|
||||
// Timer de sécurité : durée vidéo + 0.5 seconde
|
||||
this.time.delayedCall((duration + 0.5) * 1000, () => {
|
||||
if (!this.hasFinished) {
|
||||
console.warn('[IntroVideoScene] Timer de sécurité (metadata) déclenché → passage au menu');
|
||||
this.gotoMenu();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Timer de sécurité fixe au cas où les métadonnées ne se chargent pas
|
||||
// Vidéo d'intro = 8 secondes + 1 seconde de marge
|
||||
this.time.delayedCall(9000, () => {
|
||||
if (!this.hasFinished) {
|
||||
console.warn('[IntroVideoScene] Timer de sécurité fixe (9s) déclenché → passage au menu');
|
||||
this.gotoMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Ajuster si resize
|
||||
this.scale.on('resize', (gameSize: Phaser.Structs.Size) => {
|
||||
if (this.video && this.video.isPlaying()) {
|
||||
this.updateVideoSize(gameSize.width, gameSize.height);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateVideoSize(targetW?: number, targetH?: number): void {
|
||||
if (!this.video) return;
|
||||
const w = targetW ?? this.cameras.main.width;
|
||||
const h = targetH ?? this.cameras.main.height;
|
||||
|
||||
const nativeW = this.video.video?.videoWidth || 1280;
|
||||
const nativeH = this.video.video?.videoHeight || 720;
|
||||
const videoRatio = nativeW / nativeH;
|
||||
const screenRatio = w / h;
|
||||
|
||||
let scale: number;
|
||||
if (screenRatio > videoRatio) {
|
||||
scale = h / nativeH;
|
||||
} else {
|
||||
scale = w / nativeW;
|
||||
}
|
||||
|
||||
this.video.setScale(scale);
|
||||
this.video.setPosition(w / 2, h / 2);
|
||||
}
|
||||
|
||||
private gotoMenu(): void {
|
||||
if (this.hasFinished) return;
|
||||
this.hasFinished = true;
|
||||
this.video?.stop();
|
||||
this.video?.destroy();
|
||||
this.scene.start('MenuScene');
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@ export class MenuScene extends Phaser.Scene {
|
||||
private startButton?: Phaser.GameObjects.Text;
|
||||
private gyroStatus?: Phaser.GameObjects.Text;
|
||||
private hasStarted: boolean = false;
|
||||
private debugText?: Phaser.GameObjects.Text;
|
||||
private debugLines: string[] = [];
|
||||
private hasGyroDebugListener: boolean = false;
|
||||
private isRequestingPermission: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super({ key: 'MenuScene' });
|
||||
@@ -18,6 +22,8 @@ export class MenuScene extends Phaser.Scene {
|
||||
|
||||
// Réinitialiser le flag à chaque création du menu
|
||||
this.hasStarted = false;
|
||||
this.registry.set('gyroPermission', 'unknown');
|
||||
this.registry.set('gyroPermissionError', '');
|
||||
|
||||
// Titre
|
||||
const title = this.add.text(width / 2, height / 3, 'MARIO RUNNER', {
|
||||
@@ -64,8 +70,28 @@ export class MenuScene extends Phaser.Scene {
|
||||
});
|
||||
this.gyroStatus.setOrigin(0.5);
|
||||
|
||||
// Click sur le bouton
|
||||
// Debug gyroscope (logs a l'ecran)
|
||||
this.debugText = this.add.text(10, height - 110, '', {
|
||||
fontSize: '12px',
|
||||
color: '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 6, y: 6 },
|
||||
wordWrap: { width: width - 20 },
|
||||
});
|
||||
this.debugText.setOrigin(0, 0);
|
||||
this.debugText.setScrollFactor(0);
|
||||
|
||||
// Demande iOS dès le premier tap (exigé par la permission).
|
||||
this.input.once('pointerdown', () => {
|
||||
// IMPORTANT: Débloquer l'audio sur iOS
|
||||
this.unlockAudio();
|
||||
this.requestGyroPermission();
|
||||
});
|
||||
|
||||
// Click sur le bouton (fallback si le tap global a déjà eu lieu).
|
||||
this.startButton.on('pointerdown', () => {
|
||||
// IMPORTANT: Débloquer l'audio sur iOS
|
||||
this.unlockAudio();
|
||||
this.requestGyroPermission();
|
||||
});
|
||||
|
||||
@@ -79,13 +105,168 @@ export class MenuScene extends Phaser.Scene {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Débloque l'audio sur iOS (requis après interaction utilisateur)
|
||||
*/
|
||||
private unlockAudio(): void {
|
||||
// Sur iOS, l'audio doit être activé après une interaction utilisateur
|
||||
const soundManager = this.sound as Phaser.Sound.WebAudioSoundManager;
|
||||
|
||||
if (soundManager.context) {
|
||||
console.log('[Audio] Context state:', soundManager.context.state);
|
||||
|
||||
// Si le contexte est suspendu, le reprendre
|
||||
if (soundManager.context.state === 'suspended') {
|
||||
soundManager.context.resume().then(() => {
|
||||
console.log('[Audio] ✅ Context audio débloqué pour iOS');
|
||||
}).catch((error: any) => {
|
||||
console.error('[Audio] ❌ Erreur déblocage audio:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Jouer un son silencieux pour initialiser l'audio sur mobile
|
||||
// Cela garantit que les sons futurs fonctionneront
|
||||
try {
|
||||
const silentSound = this.sound.add('__silent__', { volume: 0 });
|
||||
silentSound.play();
|
||||
silentSound.destroy();
|
||||
} catch (error: any) {
|
||||
console.log('[Audio] Pas de son silencieux (normal)');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Demande la permission gyroscope (iOS) et lance le jeu
|
||||
*/
|
||||
private requestGyroPermission(): void {
|
||||
// Simplifié : on part immédiatement en jeu, sans attendre la permission.
|
||||
this.gyroStatus?.setText('Lancement du jeu...');
|
||||
private async requestGyroPermission(): Promise<void> {
|
||||
if (this.isRequestingPermission) {
|
||||
this.logDebug('permission request in progress');
|
||||
return;
|
||||
}
|
||||
this.isRequestingPermission = true;
|
||||
|
||||
try {
|
||||
console.log('[Gyro] request permission');
|
||||
console.log('[Gyro] userAgent:', navigator.userAgent);
|
||||
console.log('[Gyro] isSecureContext:', window.isSecureContext);
|
||||
this.gyroStatus?.setText("Demande d'autorisation du gyroscope...");
|
||||
|
||||
const hasDeviceOrientation =
|
||||
typeof window.DeviceOrientationEvent !== 'undefined';
|
||||
const requestOrientationPermission =
|
||||
(window.DeviceOrientationEvent as any)?.requestPermission;
|
||||
const requestMotionPermission =
|
||||
(window.DeviceMotionEvent as any)?.requestPermission;
|
||||
const isIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
|
||||
|
||||
const userActivation = (navigator as any).userActivation;
|
||||
this.logDebug(`ua: ${navigator.userAgent}`);
|
||||
this.logDebug(`secure: ${window.isSecureContext}`);
|
||||
this.logDebug(`iOS: ${isIOS}`);
|
||||
this.logDebug(`DeviceOrientationEvent: ${hasDeviceOrientation}`);
|
||||
this.logDebug(
|
||||
`requestPermission: ${typeof requestOrientationPermission === 'function'}`
|
||||
);
|
||||
this.logDebug(
|
||||
`activation: ${userActivation?.isActive}/${userActivation?.hasBeenActive}`
|
||||
);
|
||||
this.logDebug(`top-level: ${window.top === window.self}`);
|
||||
this.logDebug(`visible: ${document.visibilityState}`);
|
||||
this.setupGyroDebugListener();
|
||||
|
||||
if (isIOS && !window.isSecureContext) {
|
||||
this.gyroStatus?.setText(
|
||||
'Le gyroscope iOS requiert HTTPS. Ouvrez le jeu en https.'
|
||||
);
|
||||
this.logDebug('blocked: not https');
|
||||
this.registry.set('gyroPermission', 'blocked');
|
||||
return;
|
||||
}
|
||||
|
||||
// iOS 13+ requires explicit permission from a user gesture.
|
||||
if (hasDeviceOrientation && typeof requestOrientationPermission === 'function') {
|
||||
try {
|
||||
const orientationStatus = await requestOrientationPermission.call(
|
||||
window.DeviceOrientationEvent
|
||||
);
|
||||
let motionStatus = 'granted';
|
||||
if (typeof requestMotionPermission === 'function') {
|
||||
motionStatus = await requestMotionPermission.call(
|
||||
window.DeviceMotionEvent
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[Gyro] permission orientation:', orientationStatus);
|
||||
console.log('[Gyro] permission motion:', motionStatus);
|
||||
this.logDebug(`perm orientation: ${orientationStatus}`);
|
||||
this.logDebug(`perm motion: ${motionStatus}`);
|
||||
|
||||
if (orientationStatus === 'granted' && motionStatus === 'granted') {
|
||||
this.registry.set('gyroPermission', 'granted');
|
||||
this.gyroStatus?.setText('Gyroscope activé. Lancement...');
|
||||
this.startGame();
|
||||
return;
|
||||
}
|
||||
|
||||
this.gyroStatus?.setText(
|
||||
'Acces au gyroscope refuse. Activez-le dans les reglages iOS.'
|
||||
);
|
||||
const permState =
|
||||
orientationStatus === 'granted' || motionStatus === 'granted'
|
||||
? 'partial'
|
||||
: 'denied';
|
||||
this.registry.set('gyroPermission', permState);
|
||||
this.logDebug('permission refused');
|
||||
this.logDebug('starting without gyro');
|
||||
this.startGame();
|
||||
return;
|
||||
} catch (error) {
|
||||
console.warn('[MenuScene] Permission gyroscope refusée', error);
|
||||
this.gyroStatus?.setText(
|
||||
"Impossible d'activer le gyroscope. Reessayez."
|
||||
);
|
||||
const errorText =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
this.logDebug(`permission error: ${errorText}`);
|
||||
this.registry.set('gyroPermission', 'error');
|
||||
this.registry.set('gyroPermissionError', errorText);
|
||||
this.logDebug('starting without gyro');
|
||||
this.startGame();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Android/desktop or older iOS: no permission API.
|
||||
this.registry.set('gyroPermission', 'not-required');
|
||||
this.gyroStatus?.setText('Gyroscope prêt. Lancement...');
|
||||
this.startGame();
|
||||
} finally {
|
||||
this.isRequestingPermission = false;
|
||||
}
|
||||
}
|
||||
|
||||
private logDebug(line: string): void {
|
||||
this.debugLines.push(line);
|
||||
if (this.debugLines.length > 6) {
|
||||
this.debugLines.shift();
|
||||
}
|
||||
this.debugText?.setText(this.debugLines.join('\n'));
|
||||
}
|
||||
|
||||
private setupGyroDebugListener(): void {
|
||||
if (this.hasGyroDebugListener) return;
|
||||
this.hasGyroDebugListener = true;
|
||||
|
||||
let lastLogTime = 0;
|
||||
window.addEventListener('deviceorientation', (event) => {
|
||||
const now = Date.now();
|
||||
if (now - lastLogTime < 1000) return;
|
||||
lastLogTime = now;
|
||||
const beta = event.beta ?? 0;
|
||||
const gamma = event.gamma ?? 0;
|
||||
this.logDebug(`event beta:${beta.toFixed(1)} gamma:${gamma.toFixed(1)}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||