24/12
32
index.html
@@ -7,6 +7,11 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-fullscreen">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-fullscreen">
|
||||||
<meta name="theme-color" content="#000000">
|
<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>
|
<title>Mario Runner - Jeu Mobile</title>
|
||||||
<link rel="shortcut icon" href="/icons/favicon.ico">
|
<link rel="shortcut icon" href="/icons/favicon.ico">
|
||||||
@@ -47,33 +52,6 @@
|
|||||||
align-items: center;
|
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 {
|
canvas {
|
||||||
display: block;
|
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",
|
"description": "Jeu de plateforme mobile avec contrôle gyroscope",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "fullscreen",
|
"display": "fullscreen",
|
||||||
"orientation": "landscape",
|
"orientation": "landscape-primary",
|
||||||
"background_color": "#000000",
|
"background_color": "#000000",
|
||||||
"theme_color": "#4CAF50",
|
"theme_color": "#4CAF50",
|
||||||
"icons": [
|
"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
|
* Gestion du gyroscope pour iOS et Android
|
||||||
* Retourne une valeur normalisée entre -1 et 1
|
* 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 {
|
export class GyroControl {
|
||||||
private tiltValue: number = 0;
|
private tiltValue: number = 0;
|
||||||
private isActive: boolean = false;
|
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() {
|
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();
|
this.setupGyroscope();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,33 +129,130 @@ export class GyroControl {
|
|||||||
|
|
||||||
// Déterminer l'axe horizontal en fonction de l'orientation écran
|
// 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 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 beta = event.beta ?? 0; // inclinaison avant/arrière
|
||||||
const gamma = event.gamma ?? 0; // inclinaison gauche/droite
|
const gamma = event.gamma ?? 0; // inclinaison gauche/droite
|
||||||
|
const now = Date.now();
|
||||||
// En paysage, la gauche/droite correspond à beta; en portrait, à gamma
|
this.lastAlpha = alpha;
|
||||||
let horizontalTiltDeg = gamma;
|
this.lastBeta = beta;
|
||||||
if (angle === 90) {
|
this.lastGamma = gamma;
|
||||||
horizontalTiltDeg = -beta;
|
this.lastAngle = angle;
|
||||||
} else if (angle === -90 || angle === 270) {
|
this.lastAbs = event.absolute ?? null;
|
||||||
horizontalTiltDeg = beta;
|
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
|
// Si on utilise alpha (rotation boussole - Android uniquement)
|
||||||
if (Math.abs(relativeTilt) < GYRO_DEADZONE) {
|
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;
|
this.tiltValue = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normaliser entre -1 et 1
|
// Normaliser entre -1 et 1 (avec maxTilt spécifique à la plateforme)
|
||||||
let normalizedTilt = relativeTilt / GYRO_MAX_TILT;
|
let normalizedTilt = relativeTilt / this.maxTilt;
|
||||||
|
|
||||||
// Clamper entre -1 et 1
|
// Clamper entre -1 et 1
|
||||||
normalizedTilt = Math.max(-1, Math.min(1, normalizedTilt));
|
normalizedTilt = Math.max(-1, Math.min(1, normalizedTilt));
|
||||||
|
|
||||||
// Inversion gauche/droite (plus naturel selon retour)
|
// Pas d'inversion finale - elle est déjà faite au niveau de la lecture du beta/gamma
|
||||||
this.tiltValue = -normalizedTilt;
|
// 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 {
|
public getVelocity(): number {
|
||||||
return this.tiltValue * GYRO_SENSITIVITY;
|
return this.tiltValue * this.sensitivity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calibre le gyroscope (définit l'orientation actuelle comme neutre)
|
* Calibre le gyroscope (définit l'orientation actuelle comme neutre)
|
||||||
*/
|
*/
|
||||||
public calibrate(): void {
|
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;
|
this.tiltValue = 0;
|
||||||
|
console.log('[Gyro] Recalibration demandée...');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -121,4 +305,47 @@ export class GyroControl {
|
|||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
this.tiltValue = 0;
|
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_ACCELERATION,
|
||||||
PLAYER_MAX_JUMPS,
|
PLAYER_MAX_JUMPS,
|
||||||
RESPAWN_INVINCIBILITY_TIME,
|
RESPAWN_INVINCIBILITY_TIME,
|
||||||
|
USER1_HITBOX,
|
||||||
|
USER2_HITBOX,
|
||||||
} from '../utils/constants';
|
} from '../utils/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,11 +21,19 @@ export class Player extends Phaser.Physics.Arcade.Sprite {
|
|||||||
private invincibilityTimer?: Phaser.Time.TimerEvent;
|
private invincibilityTimer?: Phaser.Time.TimerEvent;
|
||||||
private animationsCreated: boolean = false;
|
private animationsCreated: boolean = false;
|
||||||
private wasAir: 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) {
|
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||||
// Pour l'instant, utiliser un sprite simple
|
// Récupérer le joueur sélectionné depuis le registry
|
||||||
// TODO: Remplacer par le spritesheet du neveu
|
const selectedPlayer = scene.registry.get('selectedPlayer') as string | undefined;
|
||||||
super(scene, x, y, 'player');
|
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
|
// Ajouter à la scène
|
||||||
scene.add.existing(this);
|
scene.add.existing(this);
|
||||||
@@ -34,35 +44,41 @@ export class Player extends Phaser.Physics.Arcade.Sprite {
|
|||||||
body.setGravityY(PLAYER_GRAVITY);
|
body.setGravityY(PLAYER_GRAVITY);
|
||||||
body.setCollideWorldBounds(true); // Collision avec les limites du monde
|
body.setCollideWorldBounds(true); // Collision avec les limites du monde
|
||||||
body.onWorldBounds = true; // Active les événements de collision
|
body.onWorldBounds = true; // Active les événements de collision
|
||||||
body.setSize(40, 70); // Hitbox
|
|
||||||
body.setMaxVelocity(PLAYER_MAX_SPEED, 1000);
|
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
|
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.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
|
* Met à jour le mouvement du joueur
|
||||||
@@ -117,15 +133,21 @@ export class Player extends Phaser.Physics.Arcade.Sprite {
|
|||||||
this.jumpCount++;
|
this.jumpCount++;
|
||||||
|
|
||||||
// Déclencher l'anim de saut immédiatement si disponible
|
// Déclencher l'anim de saut immédiatement si disponible
|
||||||
if (this.animationsCreated && this.anims.get('player-jump')) {
|
const jumpAnimKey = `${this.playerPrefix}-jump`;
|
||||||
this.anims.play('player-jump', true);
|
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)
|
// Effet sonore de saut (volume global SFX depuis le registry)
|
||||||
const sfxVolume = (this.scene.registry.get('sfxVolume') as number | undefined) ?? 1;
|
const sfxVolume = (this.scene.registry.get('sfxVolume') as number | undefined) ?? 1;
|
||||||
this.scene.sound.play('sfx_jump', { volume: 0.5 * sfxVolume });
|
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}`);
|
console.log(`Saut ${this.jumpCount}/${PLAYER_MAX_JUMPS}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,32 +163,46 @@ export class Player extends Phaser.Physics.Arcade.Sprite {
|
|||||||
this.jumpCount = 0;
|
this.jumpCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Jouer les animations appropriées
|
|
||||||
if (!this.animationsCreated) return;
|
if (!this.animationsCreated) return;
|
||||||
|
|
||||||
const isMoving = Math.abs(this.velocityX) > 10;
|
const isMoving = Math.abs(this.velocityX) > 10;
|
||||||
const bodyState = this.body as Phaser.Physics.Arcade.Body;
|
const bodyState = this.body as Phaser.Physics.Arcade.Body;
|
||||||
const isAir = !bodyState.touching.down;
|
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
|
// Transition sol -> air : jouer l'anim jump une fois
|
||||||
if (isAir && !this.wasAir) {
|
if (isAir && !this.wasAir) {
|
||||||
const jumpAnim = this.anims.get('player-jump');
|
if (this.scene.anims.exists(jumpAnimKey)) {
|
||||||
if (jumpAnim) {
|
this.anims.play(jumpAnimKey, true);
|
||||||
this.anims.play('player-jump', true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sol : choisir walk ou idle
|
// Sol : choisir walk ou idle
|
||||||
if (!isAir) {
|
if (!isAir) {
|
||||||
if (isMoving) {
|
if (isMoving) {
|
||||||
this.anims.play('player-walk', true);
|
if (this.scene.anims.exists(walkAnimKey)) {
|
||||||
|
this.anims.play(walkAnimKey, true);
|
||||||
|
}
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
// En l'air : si pas d'anim jump, forcer dernière frame jump
|
// 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')) {
|
if (this.anims.currentAnim?.key !== jumpAnimKey && this.scene.anims.exists(jumpAnimKey)) {
|
||||||
this.anims.play('player-jump', true);
|
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 {
|
private ensureAnimations(): void {
|
||||||
if (this.animationsCreated) return;
|
if (this.animationsCreated) return;
|
||||||
|
|
||||||
const scene = this.scene;
|
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))
|
.filter((key) => scene.textures.exists(key))
|
||||||
.map((key) => ({ 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))
|
.filter((key) => scene.textures.exists(key))
|
||||||
.map((key) => ({ 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) {
|
if (walkFrames.length >= 2) {
|
||||||
scene.anims.create({
|
// Vérifier si l'animation existe déjà avant de la créer
|
||||||
key: 'player-walk',
|
if (!scene.anims.exists(walkAnimKey)) {
|
||||||
frames: walkFrames,
|
scene.anims.create({
|
||||||
frameRate: 10,
|
key: walkAnimKey,
|
||||||
repeat: -1,
|
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({
|
scene.anims.create({
|
||||||
key: 'player-idle',
|
key: idleAnimKey,
|
||||||
frames: [{ key: walkFrames[0].key }],
|
frames: [{ key: spritesheetKey, frame: 0 }],
|
||||||
frameRate: 1,
|
frameRate: 1,
|
||||||
repeat: -1,
|
repeat: -1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jumpFrames.length >= 2) {
|
if (jumpFrames.length >= 2) {
|
||||||
scene.anims.create({
|
if (!scene.anims.exists(jumpAnimKey)) {
|
||||||
key: 'player-jump',
|
scene.anims.create({
|
||||||
frames: jumpFrames,
|
key: jumpAnimKey,
|
||||||
frameRate: 12,
|
frames: jumpFrames,
|
||||||
repeat: 0,
|
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;
|
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;
|
private pulseTimer: Phaser.Time.TimerEvent;
|
||||||
|
|
||||||
constructor(scene: Phaser.Scene, x: number, y: number) {
|
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||||
super(scene, x, y, 'supertreasure');
|
super(scene, x, y, 'star_sprite');
|
||||||
|
|
||||||
scene.add.existing(this);
|
scene.add.existing(this);
|
||||||
scene.physics.add.existing(this);
|
scene.physics.add.existing(this);
|
||||||
|
|
||||||
// Créer texture temporaire si elle n'existe pas
|
// Utiliser le sprite star_40.png (40x40px) au lieu de créer une texture
|
||||||
if (!scene.textures.exists('supertreasure')) {
|
// 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.createPlaceholderTexture(scene);
|
||||||
|
this.setTexture('supertreasure');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setTexture('supertreasure');
|
|
||||||
|
|
||||||
// Taille plus grande que les cadeaux normaux
|
// Taille plus grande que les cadeaux normaux
|
||||||
this.setScale(1.5);
|
this.setScale(1.5);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Phaser from 'phaser';
|
|||||||
export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
|
export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
|
||||||
private isOpen: boolean = false;
|
private isOpen: boolean = false;
|
||||||
private requiredGifts: number;
|
private requiredGifts: number;
|
||||||
|
private requirementText?: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
constructor(scene: Phaser.Scene, x: number, y: number, requiredGifts: number = 15) {
|
constructor(scene: Phaser.Scene, x: number, y: number, requiredGifts: number = 15) {
|
||||||
super(scene, x, y, 'chest');
|
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 {
|
private createRequirementText(scene: Phaser.Scene, x: number, y: number): void {
|
||||||
const text = scene.add.text(
|
this.requirementText = scene.add.text(
|
||||||
x,
|
x,
|
||||||
y - 80,
|
y - 80,
|
||||||
`🎁 ${this.requiredGifts} cadeaux requis`,
|
`🎁 ${this.requiredGifts} cadeaux requis`,
|
||||||
@@ -123,12 +124,13 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
|
|||||||
fontStyle: 'bold',
|
fontStyle: 'bold',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
text.setOrigin(0.5);
|
this.requirementText.setOrigin(0.5);
|
||||||
text.setDepth(this.depth + 1);
|
this.requirementText.setDepth(this.depth + 1);
|
||||||
|
this.requirementText.setVisible(false); // Caché par défaut
|
||||||
|
|
||||||
// Animation pulse
|
// Animation pulse
|
||||||
scene.tweens.add({
|
scene.tweens.add({
|
||||||
targets: text,
|
targets: this.requirementText,
|
||||||
scaleX: 1.1,
|
scaleX: 1.1,
|
||||||
scaleY: 1.1,
|
scaleY: 1.1,
|
||||||
duration: 800,
|
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
|
* 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 {
|
public open(scene: Phaser.Scene): { bonus: number; hasKey: boolean } {
|
||||||
if (this.isOpen) return 0;
|
if (this.isOpen) return { bonus: 0, hasKey: false };
|
||||||
|
|
||||||
this.isOpen = true;
|
this.isOpen = true;
|
||||||
this.setTexture('chest-open');
|
this.setTexture('chest-open');
|
||||||
@@ -159,13 +175,13 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
|
|||||||
// Particules dorées qui explosent
|
// Particules dorées qui explosent
|
||||||
this.createExplosionParticles(scene);
|
this.createExplosionParticles(scene);
|
||||||
|
|
||||||
// Message épique
|
// Message épique (sans la clé - elle sera affichée séparément)
|
||||||
const megaBonusText = scene.add.text(
|
const megaBonusText = scene.add.text(
|
||||||
scene.cameras.main.scrollX + scene.cameras.main.width / 2,
|
scene.cameras.main.scrollX + scene.cameras.main.width / 2,
|
||||||
scene.cameras.main.height / 2 - 100,
|
scene.cameras.main.height / 2 - 100,
|
||||||
'🏆 COFFRE OUVERT ! 🏆\n★★ MEGA BONUS +1000 ★★',
|
'🏆 COFFRE OUVERT ! 🏆\n★★ BONUS +1000 ★★',
|
||||||
{
|
{
|
||||||
fontSize: '56px',
|
fontSize: '48px',
|
||||||
color: '#FFD700',
|
color: '#FFD700',
|
||||||
stroke: '#FF4500',
|
stroke: '#FF4500',
|
||||||
strokeThickness: 8,
|
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 Phaser from 'phaser';
|
||||||
import { GAME_WIDTH, GAME_HEIGHT } from './utils/constants';
|
import { GAME_WIDTH, GAME_HEIGHT } from './utils/constants';
|
||||||
import { BootScene } from './scenes/BootScene';
|
import { BootScene } from './scenes/BootScene';
|
||||||
|
import { PlayerSelectScene } from './scenes/PlayerSelectScene';
|
||||||
import { MenuScene } from './scenes/MenuScene';
|
import { MenuScene } from './scenes/MenuScene';
|
||||||
import { GameScene } from './scenes/GameScene';
|
import { GameScene } from './scenes/GameScene';
|
||||||
import { IntroScene } from './scenes/IntroScene';
|
import { IntroScene } from './scenes/IntroScene';
|
||||||
|
import { IntroVideoScene } from './scenes/IntroVideoScene';
|
||||||
import { EndScene } from './scenes/EndScene';
|
import { EndScene } from './scenes/EndScene';
|
||||||
|
|
||||||
// Configuration Phaser
|
// Configuration Phaser
|
||||||
@@ -23,7 +25,15 @@ const config: Phaser.Types.Core.GameConfig = {
|
|||||||
debug: false, // Mettre à true pour voir les hitboxes
|
debug: false, // Mettre à true pour voir les hitboxes
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scene: [BootScene, IntroScene, MenuScene, GameScene, EndScene],
|
scene: [
|
||||||
|
BootScene,
|
||||||
|
PlayerSelectScene,
|
||||||
|
IntroScene,
|
||||||
|
IntroVideoScene,
|
||||||
|
MenuScene,
|
||||||
|
GameScene,
|
||||||
|
EndScene,
|
||||||
|
],
|
||||||
backgroundColor: '#87CEEB',
|
backgroundColor: '#87CEEB',
|
||||||
render: {
|
render: {
|
||||||
pixelArt: false,
|
pixelArt: false,
|
||||||
|
|||||||
13
src/main.ts
@@ -6,12 +6,13 @@ const game = new Phaser.Game(config);
|
|||||||
|
|
||||||
// Gestion du fullscreen au clic (optionnel)
|
// Gestion du fullscreen au clic (optionnel)
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
// Enregistrer le service worker pour PWA
|
// TODO: Enregistrer le service worker pour PWA (désactivé temporairement)
|
||||||
if ('serviceWorker' in navigator) {
|
// Il faut d'abord créer le fichier public/service-worker.js
|
||||||
navigator.serviceWorker.register('/service-worker.js').catch((error) => {
|
// if ('serviceWorker' in navigator) {
|
||||||
console.log('Service Worker registration failed:', error);
|
// navigator.serviceWorker.register('/service-worker.js').catch((error) => {
|
||||||
});
|
// console.log('Service Worker registration failed:', error);
|
||||||
}
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
// Bloquer le zoom pinch sur mobile
|
// Bloquer le zoom pinch sur mobile
|
||||||
document.addEventListener('gesturestart', (e) => e.preventDefault());
|
document.addEventListener('gesturestart', (e) => e.preventDefault());
|
||||||
|
|||||||
@@ -42,21 +42,36 @@ export class BootScene extends Phaser.Scene {
|
|||||||
loadingText.destroy();
|
loadingText.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sprites du joueur (80x169, 1 frame pour l'instant)
|
// Charger les sprites des deux joueurs
|
||||||
this.load.spritesheet('player', 'assets/sprites/player_spritesheet.png', {
|
// User1 (Baptiste) - dimensions réelles du spritesheet 93x224px
|
||||||
frameWidth: 80,
|
this.load.spritesheet('player_user1', 'assets/sprites/user1/player_spritesheet.png', {
|
||||||
frameHeight: 169,
|
frameWidth: 93,
|
||||||
|
frameHeight: 224,
|
||||||
});
|
});
|
||||||
// Frames de marche (sprite individuel)
|
this.load.image('player_user1_walk_1', 'assets/sprites/user1/walk_1.png');
|
||||||
this.load.image('player_walk_1', 'assets/sprites/walk_1.png');
|
this.load.image('player_user1_walk_2', 'assets/sprites/user1/walk_2.png');
|
||||||
this.load.image('player_walk_2', 'assets/sprites/walk_2.png');
|
this.load.image('player_user1_walk_3', 'assets/sprites/user1/walk_3.png');
|
||||||
this.load.image('player_walk_3', 'assets/sprites/walk_3.png');
|
this.load.image('player_user1_walk_4', 'assets/sprites/user1/walk_4.png');
|
||||||
this.load.image('player_walk_4', 'assets/sprites/walk_4.png');
|
this.load.image('player_user1_jump_1', 'assets/sprites/user1/jump_1.png');
|
||||||
this.load.image('player_jump_1', 'assets/sprites/jump_1.png');
|
this.load.image('player_user1_jump_2', 'assets/sprites/user1/jump_2.png');
|
||||||
this.load.image('player_jump_2', 'assets/sprites/jump_2.png');
|
this.load.image('player_user1_jump_3', 'assets/sprites/user1/jump_3.png');
|
||||||
this.load.image('player_jump_3', 'assets/sprites/jump_3.png');
|
this.load.image('player_user1_jump_4', 'assets/sprites/user1/jump_4.png');
|
||||||
this.load.image('player_jump_4', 'assets/sprites/jump_4.png');
|
this.load.image('player_user1_jump_5', 'assets/sprites/user1/jump_5.png');
|
||||||
this.load.image('player_jump_5', 'assets/sprites/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
|
// Musique de fond
|
||||||
this.load.audio('bgm', 'assets/audio/01. Ground Theme.mp3');
|
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_hit', ['assets/audio/champignon.mp3', 'assets/audio/champignon.aiff']);
|
||||||
this.load.audio('sfx_super', 'assets/audio/super_tresor.mp3');
|
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_saute_champi', 'assets/audio/saute_champi.mp3');
|
||||||
|
this.load.audio('sfx_chien', 'assets/audio/chien.mp3');
|
||||||
|
|
||||||
// Sprites obstacles
|
// Sprites obstacles
|
||||||
this.load.image('obstacle_mushroom', 'assets/sprites/champignon.png');
|
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
|
// 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)
|
// Ajout d'un timestamp pour forcer le rechargement (éviter le cache)
|
||||||
const timestamp = Date.now();
|
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)
|
// 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.
|
// TODO: Charger d'autres sprites, backgrounds, sons, etc.
|
||||||
}
|
}
|
||||||
|
|
||||||
create(): void {
|
create(): void {
|
||||||
// Passer par l'intro vidéo puis le menu
|
// Créer les animations du chien
|
||||||
this.scene.start('IntroScene');
|
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] Création de la scène de fin');
|
||||||
console.log('[EndScene] Dimensions:', width, 'x', height);
|
console.log('[EndScene] Dimensions:', width, 'x', height);
|
||||||
|
|
||||||
// Lancer directement la vidéo de fin
|
// Afficher le bouton de lecture au lieu de lancer automatiquement
|
||||||
this.playEndVideo();
|
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.setMute(false);
|
||||||
this.video.setLoop(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');
|
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);
|
console.log('[EndScene] Lecture démarrée?', started);
|
||||||
|
|
||||||
if (!started) {
|
if (!started) {
|
||||||
@@ -85,22 +136,40 @@ export class EndScene extends Phaser.Scene {
|
|||||||
this.gotoMenu();
|
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
|
// Si la vidéo n'est pas finie après sa durée, forcer le passage au menu
|
||||||
this.video.on('metadata', () => {
|
this.video.on('metadata', () => {
|
||||||
if (!this.video || !this.video.video) return;
|
if (!this.video || !this.video.video) return;
|
||||||
const duration = this.video.getDuration();
|
const duration = this.video.getDuration();
|
||||||
console.log('[EndScene] Durée de la vidéo:', duration, 'secondes');
|
console.log('[EndScene] Durée de la vidéo:', duration, 'secondes');
|
||||||
|
|
||||||
// Timer de sécurité : durée vidéo + 2 secondes
|
// Timer de sécurité : durée vidéo + 1 seconde
|
||||||
this.time.delayedCall((duration + 2) * 1000, () => {
|
this.time.delayedCall((duration + 1) * 1000, () => {
|
||||||
if (!this.hasFinished) {
|
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();
|
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
|
// Ajuster si resize
|
||||||
this.scale.on('resize', (gameSize: Phaser.Structs.Size) => {
|
this.scale.on('resize', (gameSize: Phaser.Structs.Size) => {
|
||||||
if (this.video && this.video.isPlaying()) {
|
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 {
|
private gotoMenu(): void {
|
||||||
if (this.hasFinished) return;
|
if (this.hasFinished) return;
|
||||||
this.hasFinished = true;
|
this.hasFinished = true;
|
||||||
|
|
||||||
|
// Détruire la vidéo
|
||||||
this.video?.stop();
|
this.video?.stop();
|
||||||
this.video?.destroy();
|
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 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 { Player } from '../entities/Player';
|
||||||
import { GyroControl } from '../controls/GyroControl';
|
import { GyroControl } from '../controls/GyroControl';
|
||||||
import { JumpButton } from '../controls/JumpButton';
|
import { JumpButton } from '../controls/JumpButton';
|
||||||
import { DirectionalButtons } from '../controls/DirectionalButtons';
|
import { DirectionalButtons } from '../controls/DirectionalButtons';
|
||||||
import { SuperTreasure } from '../entities/SuperTreasure';
|
import { SuperTreasure } from '../entities/SuperTreasure';
|
||||||
import { TreasureChest } from '../entities/TreasureChest';
|
import { TreasureChest } from '../entities/TreasureChest';
|
||||||
|
import { Cage } from '../entities/Cage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scène principale du jeu
|
* Scène principale du jeu
|
||||||
@@ -24,6 +25,8 @@ export class GameScene extends Phaser.Scene {
|
|||||||
private gifts?: Phaser.Physics.Arcade.Group;
|
private gifts?: Phaser.Physics.Arcade.Group;
|
||||||
private superTreasures?: Phaser.Physics.Arcade.Group;
|
private superTreasures?: Phaser.Physics.Arcade.Group;
|
||||||
private treasureChest?: TreasureChest;
|
private treasureChest?: TreasureChest;
|
||||||
|
private cage?: Cage;
|
||||||
|
private hasKey: boolean = false;
|
||||||
private bgMusic?: Phaser.Sound.BaseSound;
|
private bgMusic?: Phaser.Sound.BaseSound;
|
||||||
private platformRects: { x: number; y: number; w: number; h: number }[] = [];
|
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 giftsCollectedText?: Phaser.GameObjects.Text;
|
||||||
private volumeText?: Phaser.GameObjects.Text;
|
private volumeText?: Phaser.GameObjects.Text;
|
||||||
private sfxVolumeText?: 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
|
// Game state
|
||||||
private score: number = 0;
|
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;
|
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)
|
// Configurer les limites du monde physique (IMPORTANT pour permettre mouvement infini)
|
||||||
// Étendre un peu plus pour inclure la plateforme finale et le coffre
|
// Étendre encore plus pour inclure la cage du chien après le coffre
|
||||||
const levelWidth = Math.max(width * 7, 8000);
|
const levelWidth = Math.max(width * 7, 10500); // Augmenté de 2500px
|
||||||
this.physics.world.setBounds(0, 0, levelWidth, height);
|
this.physics.world.setBounds(0, 0, levelWidth, height);
|
||||||
|
|
||||||
// Créer le background qui défile
|
// Créer le background qui défile
|
||||||
@@ -97,6 +105,9 @@ export class GameScene extends Phaser.Scene {
|
|||||||
|
|
||||||
if (!playerBody || !platformBody) return true;
|
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
|
// 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)
|
// (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;
|
return playerBody.velocity.y >= 0 && playerBody.bottom <= platformBody.top + 10;
|
||||||
@@ -111,6 +122,8 @@ export class GameScene extends Phaser.Scene {
|
|||||||
|
|
||||||
// Contrôles PC (clavier)
|
// Contrôles PC (clavier)
|
||||||
this.cursors = this.input.keyboard?.createCursorKeys();
|
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)
|
// Contrôles Mobile (gyroscope + bouton tactile)
|
||||||
if (this.isMobile) {
|
if (this.isMobile) {
|
||||||
@@ -119,6 +132,9 @@ export class GameScene extends Phaser.Scene {
|
|||||||
|
|
||||||
// UI
|
// UI
|
||||||
this.createUI();
|
this.createUI();
|
||||||
|
this.createDebugOverlay();
|
||||||
|
this.createGyroDomButton();
|
||||||
|
this.createRecalibrateDomButton();
|
||||||
|
|
||||||
// Effet neige
|
// Effet neige
|
||||||
this.createSnow();
|
this.createSnow();
|
||||||
@@ -173,13 +189,16 @@ export class GameScene extends Phaser.Scene {
|
|||||||
private createPlatforms(): void {
|
private createPlatforms(): void {
|
||||||
this.platforms = this.physics.add.staticGroup();
|
this.platforms = this.physics.add.staticGroup();
|
||||||
|
|
||||||
const width = this.cameras.main.width;
|
|
||||||
const height = this.cameras.main.height;
|
const height = this.cameras.main.height;
|
||||||
|
|
||||||
// Sol principal (très large pour le niveau 6x)
|
// Sol principal (très large pour tout le niveau jusqu'à la cage + 400px)
|
||||||
const groundWidth = width * 6;
|
// Utiliser TileSprite pour répéter le motif sur toute la longueur
|
||||||
const ground = this.add.rectangle(groundWidth / 2, height - 25, groundWidth, 50, 0x8B4513);
|
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);
|
this.physics.add.existing(ground, true);
|
||||||
|
(ground as any).setData?.('isGround', true);
|
||||||
this.platforms.add(ground);
|
this.platforms.add(ground);
|
||||||
this.platformRects.push({ x: groundWidth / 2, y: height - 25, w: groundWidth, h: 50 });
|
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: 6800, y: height - 450, w: 130, h: 30 },
|
||||||
{ x: 7100, y: height - 350, w: 180, 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: 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) => {
|
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.physics.add.existing(platform, true);
|
||||||
this.platforms!.add(platform);
|
this.platforms!.add(platform);
|
||||||
this.platformRects.push({ x: pos.x, y: pos.y, w: pos.w, h: pos.h });
|
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`);
|
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
|
* Crée les groupes d'obstacles, cadeaux et super trésors
|
||||||
*/
|
*/
|
||||||
@@ -253,6 +328,37 @@ export class GameScene extends Phaser.Scene {
|
|||||||
*/
|
*/
|
||||||
private spawnTestObjects(): void {
|
private spawnTestObjects(): void {
|
||||||
const height = this.cameras.main.height;
|
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)
|
// BEAUCOUP de cadeaux répartis partout (environ tous les 300-500px)
|
||||||
const giftPositions = [
|
const giftPositions = [
|
||||||
@@ -268,6 +374,8 @@ export class GameScene extends Phaser.Scene {
|
|||||||
5400, 5700, 6000, 6300,
|
5400, 5700, 6000, 6300,
|
||||||
// Zone 6
|
// Zone 6
|
||||||
6600, 6900, 7200, 7500,
|
6600, 6900, 7200, 7500,
|
||||||
|
// Zone 7 (après le coffre - vers la cage)
|
||||||
|
8000, 8300, 8600, 8900, 9200, 9500, 9800, 10100,
|
||||||
];
|
];
|
||||||
|
|
||||||
giftPositions.forEach((x) => {
|
giftPositions.forEach((x) => {
|
||||||
@@ -275,9 +383,12 @@ export class GameScene extends Phaser.Scene {
|
|||||||
const isHigh = Math.random() > 0.5;
|
const isHigh = Math.random() > 0.5;
|
||||||
const y = isHigh ? height - 200 - Math.random() * 150 : height - 100;
|
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.physics.add.existing(gift);
|
||||||
this.gifts!.add(gift);
|
this.gifts!.add(gift);
|
||||||
|
reserve(x, y, 25);
|
||||||
});
|
});
|
||||||
|
|
||||||
// BEAUCOUP d'obstacles répartis partout
|
// BEAUCOUP d'obstacles répartis partout
|
||||||
@@ -294,6 +405,8 @@ export class GameScene extends Phaser.Scene {
|
|||||||
5600, 5900, 6200, 6500,
|
5600, 5900, 6200, 6500,
|
||||||
// Zone 6
|
// Zone 6
|
||||||
6800, 7100, 7400,
|
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)
|
// Obstacles sur plateformes (x, y)
|
||||||
@@ -314,48 +427,28 @@ export class GameScene extends Phaser.Scene {
|
|||||||
const platformPlaced: Array<{ x: number; y: number }> = [];
|
const platformPlaced: Array<{ x: number; y: number }> = [];
|
||||||
this.platformRects
|
this.platformRects
|
||||||
.filter((rect) => rect.y < height - 80 && rect.w >= 120) // ignorer le sol et les très petites plateformes
|
.filter((rect) => rect.y < height - 80 && rect.w >= 120) // ignorer le sol et les très petites plateformes
|
||||||
.forEach((rect, idx) => {
|
.forEach((rect) => {
|
||||||
// Placer un champignon sur ~1 plateforme sur 2 pour ne pas surcharger
|
|
||||||
if (idx % 2 !== 0) return;
|
|
||||||
const x = rect.x + Phaser.Math.Between(Math.round(-rect.w / 4), Math.round(rect.w / 4));
|
const x = rect.x + Phaser.Math.Between(Math.round(-rect.w / 4), Math.round(rect.w / 4));
|
||||||
const y = rect.y - rect.h / 2;
|
const y = rect.y - rect.h / 2;
|
||||||
platformPlaced.push({ x, y });
|
platformPlaced.push({ x, y });
|
||||||
});
|
});
|
||||||
|
|
||||||
obstaclePositions.filter((_, idx) => idx % 2 === 0).forEach((x) => {
|
obstaclePositions.forEach((x) => addCandidate(x, height - 50));
|
||||||
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);
|
|
||||||
|
|
||||||
const body = obstacle.body as Phaser.Physics.Arcade.Body;
|
obstaclePlatforms.forEach((pos) => {
|
||||||
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
|
|
||||||
const target = this.platformRects.find((rect) => Math.abs(pos.x - rect.x) <= rect.w / 2);
|
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 topY = target ? target.y - target.h / 2 : pos.y;
|
||||||
const obstacle = this.physics.add.sprite(pos.x, topY, 'obstacle_mushroom');
|
addCandidate(pos.x, topY);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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');
|
const obstacle = this.physics.add.sprite(pos.x, pos.y, 'obstacle_mushroom');
|
||||||
obstacle.setOrigin(0.5, 1);
|
obstacle.setOrigin(0.5, 1);
|
||||||
obstacle.setImmovable(true);
|
obstacle.setImmovable(true);
|
||||||
@@ -368,17 +461,16 @@ export class GameScene extends Phaser.Scene {
|
|||||||
body.setOffset((obstacle.displayWidth - 45) / 2, obstacle.displayHeight - 81);
|
body.setOffset((obstacle.displayWidth - 45) / 2, obstacle.displayHeight - 81);
|
||||||
|
|
||||||
this.obstacles!.add(obstacle);
|
this.obstacles!.add(obstacle);
|
||||||
|
obstacleCount += 1;
|
||||||
|
reserve(pos.x, pos.y, 50);
|
||||||
});
|
});
|
||||||
|
|
||||||
// SUPER TRÉSORS (rares et précieux - 1 par zone)
|
if (obstacleCount < targetObstacleCount) {
|
||||||
const superTreasurePositions = [
|
console.warn(
|
||||||
{ x: 1000, y: height - 350 }, // Zone 1 - en hauteur
|
`⚠️ Champignons insuffisants: ${obstacleCount}/${targetObstacleCount}. ` +
|
||||||
{ x: 2500, y: height - 420 }, // Zone 2 - très haut
|
'Augmentez les positions candidates ou reduisez le minObjectSpacing.'
|
||||||
{ 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
|
|
||||||
];
|
|
||||||
|
|
||||||
superTreasurePositions.forEach((pos) => {
|
superTreasurePositions.forEach((pos) => {
|
||||||
const superTreasure = new SuperTreasure(this, pos.x, pos.y);
|
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
|
// 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);
|
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
|
// Bouton de saut
|
||||||
this.jumpButton = new JumpButton(this, () => {
|
this.jumpButton = new JumpButton(this, () => {
|
||||||
|
// IMPORTANT: Débloquer l'audio sur iOS lors de l'interaction
|
||||||
|
this.unlockAudio();
|
||||||
this.player?.jump();
|
this.player?.jump();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -485,6 +601,8 @@ export class GameScene extends Phaser.Scene {
|
|||||||
backButton.setDepth(100);
|
backButton.setDepth(100);
|
||||||
backButton.setInteractive({ useHandCursor: true });
|
backButton.setInteractive({ useHandCursor: true });
|
||||||
backButton.on('pointerdown', () => {
|
backButton.on('pointerdown', () => {
|
||||||
|
// IMPORTANT: Débloquer l'audio sur iOS
|
||||||
|
this.unlockAudio();
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
this.scene.start('MenuScene');
|
this.scene.start('MenuScene');
|
||||||
});
|
});
|
||||||
@@ -501,6 +619,8 @@ export class GameScene extends Phaser.Scene {
|
|||||||
this.volumeText.setDepth(100);
|
this.volumeText.setDepth(100);
|
||||||
this.volumeText.setInteractive({ useHandCursor: true });
|
this.volumeText.setInteractive({ useHandCursor: true });
|
||||||
this.volumeText.on('pointerdown', () => {
|
this.volumeText.on('pointerdown', () => {
|
||||||
|
// IMPORTANT: Débloquer l'audio sur iOS
|
||||||
|
this.unlockAudio();
|
||||||
this.cycleMusicVolume();
|
this.cycleMusicVolume();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -516,10 +636,247 @@ export class GameScene extends Phaser.Scene {
|
|||||||
this.sfxVolumeText.setDepth(100);
|
this.sfxVolumeText.setDepth(100);
|
||||||
this.sfxVolumeText.setInteractive({ useHandCursor: true });
|
this.sfxVolumeText.setInteractive({ useHandCursor: true });
|
||||||
this.sfxVolumeText.on('pointerdown', () => {
|
this.sfxVolumeText.on('pointerdown', () => {
|
||||||
|
// IMPORTANT: Débloquer l'audio sur iOS
|
||||||
|
this.unlockAudio();
|
||||||
this.cycleSfxVolume();
|
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 {
|
update(time: number): void {
|
||||||
if (!this.player) return;
|
if (!this.player) return;
|
||||||
|
|
||||||
@@ -529,36 +886,85 @@ export class GameScene extends Phaser.Scene {
|
|||||||
// Gestion des contrôles
|
// Gestion des contrôles
|
||||||
let direction = 0;
|
let direction = 0;
|
||||||
|
|
||||||
// PC : Clavier
|
// PC/Laptop : Clavier (priorité absolue)
|
||||||
if (this.cursors) {
|
if (!this.isMobile && this.cursors) {
|
||||||
if (this.cursors.left.isDown) {
|
if (this.cursors.left.isDown) {
|
||||||
direction = -1;
|
direction = -1;
|
||||||
|
if (time % 1000 < 16) console.log('⬅️ Left pressed');
|
||||||
} else if (this.cursors.right.isDown) {
|
} else if (this.cursors.right.isDown) {
|
||||||
direction = 1;
|
direction = 1;
|
||||||
|
if (time % 1000 < 16) console.log('➡️ Right pressed');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saut avec Espace
|
// Saut avec Espace
|
||||||
if (Phaser.Input.Keyboard.JustDown(this.cursors.space!)) {
|
if (Phaser.Input.Keyboard.JustDown(this.cursors.space!)) {
|
||||||
|
console.log('🚀 Space pressed - Jump!');
|
||||||
this.player.jump();
|
this.player.jump();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saut avec flèche haut (alternative)
|
// Saut avec flèche haut (alternative)
|
||||||
if (Phaser.Input.Keyboard.JustDown(this.cursors.up!)) {
|
if (Phaser.Input.Keyboard.JustDown(this.cursors.up!)) {
|
||||||
|
console.log('⬆️ Up pressed - Jump!');
|
||||||
this.player.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) {
|
if (this.isMobile) {
|
||||||
|
// Vérifier d'abord les boutons directionnels (priorité)
|
||||||
const dirButtons = this.directionalButtons?.getDirection() ?? 0;
|
const dirButtons = this.directionalButtons?.getDirection() ?? 0;
|
||||||
if (dirButtons !== 0) {
|
if (dirButtons !== 0) {
|
||||||
direction = dirButtons;
|
direction = dirButtons;
|
||||||
} else if (this.gyroControl) {
|
} else if (this.gyroControl) {
|
||||||
|
// Sinon utiliser le gyroscope
|
||||||
const tiltValue = this.gyroControl.getTiltValue();
|
const tiltValue = this.gyroControl.getTiltValue();
|
||||||
direction = tiltValue; // -1 à 1
|
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
|
// Déplacer le joueur
|
||||||
this.player.move(direction);
|
this.player.move(direction);
|
||||||
this.player.update();
|
this.player.update();
|
||||||
@@ -577,6 +983,43 @@ export class GameScene extends Phaser.Scene {
|
|||||||
if (this.background) {
|
if (this.background) {
|
||||||
this.background.tilePositionX = this.cameras.main.scrollX * 0.3;
|
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.addScore(500);
|
||||||
this.playSfx('sfx_super', 0.6);
|
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
|
// Message spécial
|
||||||
const bonusText = this.add.text(
|
const bonusText = this.add.text(
|
||||||
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
this.cameras.main.height / 2,
|
this.cameras.main.height / 2,
|
||||||
'★ SUPER TRÉSOR +500 ★',
|
'★ SUPER TRÉSOR +500 ★\n⏱️ +5 SECONDES ⏱️',
|
||||||
{
|
{
|
||||||
fontSize: '48px',
|
fontSize: '48px',
|
||||||
color: '#FFD700',
|
color: '#FFD700',
|
||||||
stroke: '#FF8C00',
|
stroke: '#FF8C00',
|
||||||
strokeThickness: 6,
|
strokeThickness: 6,
|
||||||
fontStyle: 'bold',
|
fontStyle: 'bold',
|
||||||
|
align: 'center',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
bonusText.setOrigin(0.5);
|
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 {
|
private openChest(_player: any, chest: any): void {
|
||||||
if (chest.canOpen(this.giftsCollected)) {
|
if (chest.canOpen(this.giftsCollected)) {
|
||||||
const bonus = chest.open(this);
|
const result = chest.open(this);
|
||||||
this.addScore(bonus);
|
this.addScore(result.bonus);
|
||||||
|
|
||||||
// VICTOIRE ! Lancer l'animation de fin
|
// Donner la clé au joueur avec animation
|
||||||
this.time.delayedCall(2000, () => {
|
if (result.hasKey) {
|
||||||
this.levelComplete();
|
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()) {
|
} else if (!chest.getIsOpen()) {
|
||||||
// Pas assez de cadeaux
|
// Pas assez de cadeaux
|
||||||
const remaining = chest.getRequiredGifts() - this.giftsCollected;
|
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é !
|
* 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 startButton?: Phaser.GameObjects.Text;
|
||||||
private gyroStatus?: Phaser.GameObjects.Text;
|
private gyroStatus?: Phaser.GameObjects.Text;
|
||||||
private hasStarted: boolean = false;
|
private hasStarted: boolean = false;
|
||||||
|
private debugText?: Phaser.GameObjects.Text;
|
||||||
|
private debugLines: string[] = [];
|
||||||
|
private hasGyroDebugListener: boolean = false;
|
||||||
|
private isRequestingPermission: boolean = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ key: 'MenuScene' });
|
super({ key: 'MenuScene' });
|
||||||
@@ -18,6 +22,8 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
|
|
||||||
// Réinitialiser le flag à chaque création du menu
|
// Réinitialiser le flag à chaque création du menu
|
||||||
this.hasStarted = false;
|
this.hasStarted = false;
|
||||||
|
this.registry.set('gyroPermission', 'unknown');
|
||||||
|
this.registry.set('gyroPermissionError', '');
|
||||||
|
|
||||||
// Titre
|
// Titre
|
||||||
const title = this.add.text(width / 2, height / 3, 'MARIO RUNNER', {
|
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);
|
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', () => {
|
this.startButton.on('pointerdown', () => {
|
||||||
|
// IMPORTANT: Débloquer l'audio sur iOS
|
||||||
|
this.unlockAudio();
|
||||||
this.requestGyroPermission();
|
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
|
* Demande la permission gyroscope (iOS) et lance le jeu
|
||||||
*/
|
*/
|
||||||
private requestGyroPermission(): void {
|
private async requestGyroPermission(): Promise<void> {
|
||||||
// Simplifié : on part immédiatement en jeu, sans attendre la permission.
|
if (this.isRequestingPermission) {
|
||||||
this.gyroStatus?.setText('Lancement du jeu...');
|
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();
|
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)}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||