diff --git a/index.html b/index.html
index dc94d22..41568f7 100644
--- a/index.html
+++ b/index.html
@@ -7,6 +7,11 @@
+
+
+
+
+
Mario Runner - Jeu Mobile
@@ -47,33 +52,6 @@
align-items: center;
}
- /* Forcer l'orientation paysage */
- @media screen and (orientation: portrait) {
- #game-container::before {
- content: '↻';
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- font-size: 80px;
- color: white;
- z-index: 9999;
- }
-
- #game-container::after {
- content: 'Veuillez tourner votre téléphone en mode paysage';
- position: fixed;
- top: 60%;
- left: 50%;
- transform: translate(-50%, -50%);
- color: white;
- font-family: Arial, sans-serif;
- font-size: 18px;
- text-align: center;
- padding: 0 20px;
- z-index: 9999;
- }
- }
canvas {
display: block;
diff --git a/public/assets/audio/chien.mp3 b/public/assets/audio/chien.mp3
new file mode 100644
index 0000000..7451c3e
Binary files /dev/null and b/public/assets/audio/chien.mp3 differ
diff --git a/public/assets/images/user1.jpg b/public/assets/images/user1.jpg
new file mode 100644
index 0000000..2ff70dd
Binary files /dev/null and b/public/assets/images/user1.jpg differ
diff --git a/public/assets/images/user1.png b/public/assets/images/user1.png
new file mode 100755
index 0000000..bec7765
Binary files /dev/null and b/public/assets/images/user1.png differ
diff --git a/public/assets/images/user2.jpg b/public/assets/images/user2.jpg
new file mode 100644
index 0000000..d908820
Binary files /dev/null and b/public/assets/images/user2.jpg differ
diff --git a/public/assets/images/user2.png b/public/assets/images/user2.png
new file mode 100755
index 0000000..b57f78f
Binary files /dev/null and b/public/assets/images/user2.png differ
diff --git a/public/assets/sprites/decor/b_130.png b/public/assets/sprites/decor/b_130.png
new file mode 100755
index 0000000..f39357b
Binary files /dev/null and b/public/assets/sprites/decor/b_130.png differ
diff --git a/public/assets/sprites/decor/b_140.png b/public/assets/sprites/decor/b_140.png
new file mode 100755
index 0000000..1eb2739
Binary files /dev/null and b/public/assets/sprites/decor/b_140.png differ
diff --git a/public/assets/sprites/decor/b_179.png b/public/assets/sprites/decor/b_179.png
new file mode 100755
index 0000000..44abe0a
Binary files /dev/null and b/public/assets/sprites/decor/b_179.png differ
diff --git a/public/assets/sprites/decor/b_40.png b/public/assets/sprites/decor/b_40.png
new file mode 100755
index 0000000..7858834
Binary files /dev/null and b/public/assets/sprites/decor/b_40.png differ
diff --git a/public/assets/sprites/decor/b_73.png b/public/assets/sprites/decor/b_73.png
new file mode 100755
index 0000000..56d8541
Binary files /dev/null and b/public/assets/sprites/decor/b_73.png differ
diff --git a/public/assets/sprites/decor/gift_40.png b/public/assets/sprites/decor/gift_40.png
new file mode 100755
index 0000000..5daea31
Binary files /dev/null and b/public/assets/sprites/decor/gift_40.png differ
diff --git a/public/assets/sprites/decor/sol_150.png b/public/assets/sprites/decor/sol_150.png
new file mode 100755
index 0000000..4b448c1
Binary files /dev/null and b/public/assets/sprites/decor/sol_150.png differ
diff --git a/public/assets/sprites/decor/star_40.png b/public/assets/sprites/decor/star_40.png
new file mode 100755
index 0000000..cf71d60
Binary files /dev/null and b/public/assets/sprites/decor/star_40.png differ
diff --git a/public/assets/sprites/dog/dog.xcf b/public/assets/sprites/dog/dog.xcf
new file mode 100755
index 0000000..72e75ea
Binary files /dev/null and b/public/assets/sprites/dog/dog.xcf differ
diff --git a/public/assets/sprites/dog/idle_01.png b/public/assets/sprites/dog/idle_01.png
new file mode 100755
index 0000000..fce3013
Binary files /dev/null and b/public/assets/sprites/dog/idle_01.png differ
diff --git a/public/assets/sprites/dog/idle_02.png b/public/assets/sprites/dog/idle_02.png
new file mode 100755
index 0000000..500beb7
Binary files /dev/null and b/public/assets/sprites/dog/idle_02.png differ
diff --git a/public/assets/sprites/dog/idle_03.png b/public/assets/sprites/dog/idle_03.png
new file mode 100755
index 0000000..832f622
Binary files /dev/null and b/public/assets/sprites/dog/idle_03.png differ
diff --git a/public/assets/sprites/dog/idle_04.png b/public/assets/sprites/dog/idle_04.png
new file mode 100755
index 0000000..e0f182a
Binary files /dev/null and b/public/assets/sprites/dog/idle_04.png differ
diff --git a/public/assets/sprites/dog/turn_01.png b/public/assets/sprites/dog/turn_01.png
new file mode 100755
index 0000000..3300971
Binary files /dev/null and b/public/assets/sprites/dog/turn_01.png differ
diff --git a/public/assets/sprites/dog/turn_02.png b/public/assets/sprites/dog/turn_02.png
new file mode 100755
index 0000000..416ad1f
Binary files /dev/null and b/public/assets/sprites/dog/turn_02.png differ
diff --git a/public/assets/sprites/dog/turn_03.png b/public/assets/sprites/dog/turn_03.png
new file mode 100755
index 0000000..a929f77
Binary files /dev/null and b/public/assets/sprites/dog/turn_03.png differ
diff --git a/public/assets/sprites/dog/walkback_01.png b/public/assets/sprites/dog/walkback_01.png
new file mode 100755
index 0000000..c47fd9c
Binary files /dev/null and b/public/assets/sprites/dog/walkback_01.png differ
diff --git a/public/assets/sprites/dog/walkback_02.png b/public/assets/sprites/dog/walkback_02.png
new file mode 100755
index 0000000..c256830
Binary files /dev/null and b/public/assets/sprites/dog/walkback_02.png differ
diff --git a/public/assets/sprites/dog/walkback_03.png b/public/assets/sprites/dog/walkback_03.png
new file mode 100755
index 0000000..d7e2e03
Binary files /dev/null and b/public/assets/sprites/dog/walkback_03.png differ
diff --git a/public/assets/sprites/dog/walkback_04.png b/public/assets/sprites/dog/walkback_04.png
new file mode 100755
index 0000000..cf278dd
Binary files /dev/null and b/public/assets/sprites/dog/walkback_04.png differ
diff --git a/public/assets/sprites/jump_1.png b/public/assets/sprites/jump_1.png
deleted file mode 100644
index 6b20bad..0000000
Binary files a/public/assets/sprites/jump_1.png and /dev/null differ
diff --git a/public/assets/sprites/jump_2.png b/public/assets/sprites/jump_2.png
deleted file mode 100644
index ab0f823..0000000
Binary files a/public/assets/sprites/jump_2.png and /dev/null differ
diff --git a/public/assets/sprites/jump_3.png b/public/assets/sprites/jump_3.png
deleted file mode 100644
index 2c7df30..0000000
Binary files a/public/assets/sprites/jump_3.png and /dev/null differ
diff --git a/public/assets/sprites/jump_4.png b/public/assets/sprites/jump_4.png
deleted file mode 100644
index a5b9324..0000000
Binary files a/public/assets/sprites/jump_4.png and /dev/null differ
diff --git a/public/assets/sprites/jump_5.png b/public/assets/sprites/jump_5.png
deleted file mode 100644
index 555d7b6..0000000
Binary files a/public/assets/sprites/jump_5.png and /dev/null differ
diff --git a/public/assets/sprites/player_spritesheet.png b/public/assets/sprites/player_spritesheet.png
deleted file mode 100644
index 5825731..0000000
Binary files a/public/assets/sprites/player_spritesheet.png and /dev/null differ
diff --git a/public/assets/sprites/user1/image finale1.jpeg b/public/assets/sprites/user1/image finale1.jpeg
new file mode 100644
index 0000000..c324499
Binary files /dev/null and b/public/assets/sprites/user1/image finale1.jpeg differ
diff --git a/public/assets/sprites/user1/jump_1.png b/public/assets/sprites/user1/jump_1.png
new file mode 100644
index 0000000..ddce2e7
Binary files /dev/null and b/public/assets/sprites/user1/jump_1.png differ
diff --git a/public/assets/sprites/user1/jump_2.png b/public/assets/sprites/user1/jump_2.png
new file mode 100644
index 0000000..b7976b3
Binary files /dev/null and b/public/assets/sprites/user1/jump_2.png differ
diff --git a/public/assets/sprites/user1/jump_3.png b/public/assets/sprites/user1/jump_3.png
new file mode 100644
index 0000000..a319481
Binary files /dev/null and b/public/assets/sprites/user1/jump_3.png differ
diff --git a/public/assets/sprites/user1/jump_4.png b/public/assets/sprites/user1/jump_4.png
new file mode 100644
index 0000000..09a6a6d
Binary files /dev/null and b/public/assets/sprites/user1/jump_4.png differ
diff --git a/public/assets/sprites/user1/jump_5.png b/public/assets/sprites/user1/jump_5.png
new file mode 100644
index 0000000..cfcb94d
Binary files /dev/null and b/public/assets/sprites/user1/jump_5.png differ
diff --git a/public/assets/sprites/user1/player_spritesheet.png b/public/assets/sprites/user1/player_spritesheet.png
new file mode 100755
index 0000000..d362f72
Binary files /dev/null and b/public/assets/sprites/user1/player_spritesheet.png differ
diff --git a/public/assets/sprites/user1/user1.xcf b/public/assets/sprites/user1/user1.xcf
new file mode 100755
index 0000000..2347138
Binary files /dev/null and b/public/assets/sprites/user1/user1.xcf differ
diff --git a/public/assets/sprites/user1/walk_1.png b/public/assets/sprites/user1/walk_1.png
new file mode 100644
index 0000000..2c662a4
Binary files /dev/null and b/public/assets/sprites/user1/walk_1.png differ
diff --git a/public/assets/sprites/user1/walk_2.png b/public/assets/sprites/user1/walk_2.png
new file mode 100644
index 0000000..efdd76e
Binary files /dev/null and b/public/assets/sprites/user1/walk_2.png differ
diff --git a/public/assets/sprites/user1/walk_3.png b/public/assets/sprites/user1/walk_3.png
new file mode 100644
index 0000000..092f999
Binary files /dev/null and b/public/assets/sprites/user1/walk_3.png differ
diff --git a/public/assets/sprites/user1/walk_4.png b/public/assets/sprites/user1/walk_4.png
new file mode 100644
index 0000000..8b08811
Binary files /dev/null and b/public/assets/sprites/user1/walk_4.png differ
diff --git a/public/assets/sprites/user1_backup/jump_1.png b/public/assets/sprites/user1_backup/jump_1.png
new file mode 100755
index 0000000..55e51fc
Binary files /dev/null and b/public/assets/sprites/user1_backup/jump_1.png differ
diff --git a/public/assets/sprites/user1_backup/jump_2.png b/public/assets/sprites/user1_backup/jump_2.png
new file mode 100755
index 0000000..82a6ade
Binary files /dev/null and b/public/assets/sprites/user1_backup/jump_2.png differ
diff --git a/public/assets/sprites/user1_backup/jump_3.png b/public/assets/sprites/user1_backup/jump_3.png
new file mode 100755
index 0000000..8b05af8
Binary files /dev/null and b/public/assets/sprites/user1_backup/jump_3.png differ
diff --git a/public/assets/sprites/user1_backup/jump_4.png b/public/assets/sprites/user1_backup/jump_4.png
new file mode 100755
index 0000000..7191930
Binary files /dev/null and b/public/assets/sprites/user1_backup/jump_4.png differ
diff --git a/public/assets/sprites/user1_backup/jump_5.png b/public/assets/sprites/user1_backup/jump_5.png
new file mode 100755
index 0000000..af5367e
Binary files /dev/null and b/public/assets/sprites/user1_backup/jump_5.png differ
diff --git a/public/assets/sprites/user1_backup/walk_1.png b/public/assets/sprites/user1_backup/walk_1.png
new file mode 100755
index 0000000..89f49ac
Binary files /dev/null and b/public/assets/sprites/user1_backup/walk_1.png differ
diff --git a/public/assets/sprites/user1_backup/walk_2.png b/public/assets/sprites/user1_backup/walk_2.png
new file mode 100755
index 0000000..be2d42f
Binary files /dev/null and b/public/assets/sprites/user1_backup/walk_2.png differ
diff --git a/public/assets/sprites/user1_backup/walk_3.png b/public/assets/sprites/user1_backup/walk_3.png
new file mode 100755
index 0000000..c24e957
Binary files /dev/null and b/public/assets/sprites/user1_backup/walk_3.png differ
diff --git a/public/assets/sprites/user1_backup/walk_4.png b/public/assets/sprites/user1_backup/walk_4.png
new file mode 100755
index 0000000..c660c7b
Binary files /dev/null and b/public/assets/sprites/user1_backup/walk_4.png differ
diff --git a/public/assets/sprites/user2/image finale2.jpeg b/public/assets/sprites/user2/image finale2.jpeg
new file mode 100644
index 0000000..6c1e021
Binary files /dev/null and b/public/assets/sprites/user2/image finale2.jpeg differ
diff --git a/public/assets/sprites/user2/jump_1.png b/public/assets/sprites/user2/jump_1.png
new file mode 100755
index 0000000..53aeefd
Binary files /dev/null and b/public/assets/sprites/user2/jump_1.png differ
diff --git a/public/assets/sprites/user2/jump_2.png b/public/assets/sprites/user2/jump_2.png
new file mode 100755
index 0000000..ea41a5f
Binary files /dev/null and b/public/assets/sprites/user2/jump_2.png differ
diff --git a/public/assets/sprites/user2/jump_3.png b/public/assets/sprites/user2/jump_3.png
new file mode 100755
index 0000000..01d1d68
Binary files /dev/null and b/public/assets/sprites/user2/jump_3.png differ
diff --git a/public/assets/sprites/user2/jump_4.png b/public/assets/sprites/user2/jump_4.png
new file mode 100755
index 0000000..d37390d
Binary files /dev/null and b/public/assets/sprites/user2/jump_4.png differ
diff --git a/public/assets/sprites/user2/jump_5.png b/public/assets/sprites/user2/jump_5.png
new file mode 100755
index 0000000..3a5d6ac
Binary files /dev/null and b/public/assets/sprites/user2/jump_5.png differ
diff --git a/public/assets/sprites/user2/player_spritesheet.png b/public/assets/sprites/user2/player_spritesheet.png
new file mode 100755
index 0000000..0e7acf4
Binary files /dev/null and b/public/assets/sprites/user2/player_spritesheet.png differ
diff --git a/public/assets/sprites/user2/user2.xcf b/public/assets/sprites/user2/user2.xcf
new file mode 100755
index 0000000..714a83c
Binary files /dev/null and b/public/assets/sprites/user2/user2.xcf differ
diff --git a/public/assets/sprites/user2/walk_1.png b/public/assets/sprites/user2/walk_1.png
new file mode 100755
index 0000000..3c196d9
Binary files /dev/null and b/public/assets/sprites/user2/walk_1.png differ
diff --git a/public/assets/sprites/user2/walk_2.png b/public/assets/sprites/user2/walk_2.png
new file mode 100755
index 0000000..e6aa9d2
Binary files /dev/null and b/public/assets/sprites/user2/walk_2.png differ
diff --git a/public/assets/sprites/user2/walk_3.png b/public/assets/sprites/user2/walk_3.png
new file mode 100755
index 0000000..b3544fa
Binary files /dev/null and b/public/assets/sprites/user2/walk_3.png differ
diff --git a/public/assets/sprites/user2/walk_4.png b/public/assets/sprites/user2/walk_4.png
new file mode 100755
index 0000000..bb96b3f
Binary files /dev/null and b/public/assets/sprites/user2/walk_4.png differ
diff --git a/public/assets/sprites/user2_backup/jump_1.png b/public/assets/sprites/user2_backup/jump_1.png
new file mode 100755
index 0000000..14f33db
Binary files /dev/null and b/public/assets/sprites/user2_backup/jump_1.png differ
diff --git a/public/assets/sprites/user2_backup/jump_2.png b/public/assets/sprites/user2_backup/jump_2.png
new file mode 100755
index 0000000..6ad93ad
Binary files /dev/null and b/public/assets/sprites/user2_backup/jump_2.png differ
diff --git a/public/assets/sprites/user2_backup/jump_3.png b/public/assets/sprites/user2_backup/jump_3.png
new file mode 100755
index 0000000..24cd273
Binary files /dev/null and b/public/assets/sprites/user2_backup/jump_3.png differ
diff --git a/public/assets/sprites/user2_backup/jump_4.png b/public/assets/sprites/user2_backup/jump_4.png
new file mode 100755
index 0000000..85f3f57
Binary files /dev/null and b/public/assets/sprites/user2_backup/jump_4.png differ
diff --git a/public/assets/sprites/user2_backup/jump_5.png b/public/assets/sprites/user2_backup/jump_5.png
new file mode 100755
index 0000000..31073b5
Binary files /dev/null and b/public/assets/sprites/user2_backup/jump_5.png differ
diff --git a/public/assets/sprites/user2_backup/player_spritesheet.png b/public/assets/sprites/user2_backup/player_spritesheet.png
new file mode 100755
index 0000000..a0a7cd5
Binary files /dev/null and b/public/assets/sprites/user2_backup/player_spritesheet.png differ
diff --git a/public/assets/sprites/user2_backup/walk_1.png b/public/assets/sprites/user2_backup/walk_1.png
new file mode 100755
index 0000000..3560371
Binary files /dev/null and b/public/assets/sprites/user2_backup/walk_1.png differ
diff --git a/public/assets/sprites/user2_backup/walk_2.png b/public/assets/sprites/user2_backup/walk_2.png
new file mode 100755
index 0000000..820e7a4
Binary files /dev/null and b/public/assets/sprites/user2_backup/walk_2.png differ
diff --git a/public/assets/sprites/user2_backup/walk_3.png b/public/assets/sprites/user2_backup/walk_3.png
new file mode 100755
index 0000000..4a67c89
Binary files /dev/null and b/public/assets/sprites/user2_backup/walk_3.png differ
diff --git a/public/assets/sprites/user2_backup/walk_4.png b/public/assets/sprites/user2_backup/walk_4.png
new file mode 100755
index 0000000..3e37350
Binary files /dev/null and b/public/assets/sprites/user2_backup/walk_4.png differ
diff --git a/public/assets/sprites/walk_1.png b/public/assets/sprites/walk_1.png
deleted file mode 100644
index 3d3e423..0000000
Binary files a/public/assets/sprites/walk_1.png and /dev/null differ
diff --git a/public/assets/sprites/walk_2.png b/public/assets/sprites/walk_2.png
deleted file mode 100644
index 917a3bf..0000000
Binary files a/public/assets/sprites/walk_2.png and /dev/null differ
diff --git a/public/assets/sprites/walk_3.png b/public/assets/sprites/walk_3.png
deleted file mode 100644
index f1b9fd8..0000000
Binary files a/public/assets/sprites/walk_3.png and /dev/null differ
diff --git a/public/assets/sprites/walk_4.png b/public/assets/sprites/walk_4.png
deleted file mode 100644
index f976cf3..0000000
Binary files a/public/assets/sprites/walk_4.png and /dev/null differ
diff --git a/public/assets/video/intro (Copie 2).mp4 b/public/assets/video/intro (Copie 2).mp4
deleted file mode 100644
index 8a6633c..0000000
Binary files a/public/assets/video/intro (Copie 2).mp4 and /dev/null differ
diff --git a/public/assets/video/intro (Copie).mp4 b/public/assets/video/intro (Copie).mp4
deleted file mode 100644
index caf8d21..0000000
Binary files a/public/assets/video/intro (Copie).mp4 and /dev/null differ
diff --git a/public/assets/video/intro.mp4 b/public/assets/video/intro_user1.mp4
similarity index 100%
rename from public/assets/video/intro.mp4
rename to public/assets/video/intro_user1.mp4
diff --git a/public/assets/video/end_J.mp4 b/public/assets/video/intro_user2.mp4
similarity index 100%
rename from public/assets/video/end_J.mp4
rename to public/assets/video/intro_user2.mp4
diff --git a/public/assets/video/zoe cadeaux1.mp4 b/public/assets/video/zoe cadeaux1.mp4
new file mode 100755
index 0000000..44e6810
Binary files /dev/null and b/public/assets/video/zoe cadeaux1.mp4 differ
diff --git a/public/manifest.json b/public/manifest.json
index 66ea5e9..7b0b4aa 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -4,7 +4,7 @@
"description": "Jeu de plateforme mobile avec contrôle gyroscope",
"start_url": "/",
"display": "fullscreen",
- "orientation": "landscape",
+ "orientation": "landscape-primary",
"background_color": "#000000",
"theme_color": "#4CAF50",
"icons": [
diff --git a/resize-user1-sprites.cjs b/resize-user1-sprites.cjs
new file mode 100644
index 0000000..f6c5084
--- /dev/null
+++ b/resize-user1-sprites.cjs
@@ -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();
diff --git a/resize-user2-sprites.cjs b/resize-user2-sprites.cjs
new file mode 100755
index 0000000..1ad2cb0
--- /dev/null
+++ b/resize-user2-sprites.cjs
@@ -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();
diff --git a/src/controls/GyroControl.ts b/src/controls/GyroControl.ts
index 277c779..ea06cb0 100644
--- a/src/controls/GyroControl.ts
+++ b/src/controls/GyroControl.ts
@@ -1,14 +1,97 @@
-import { GYRO_DEADZONE, GYRO_MAX_TILT, GYRO_SENSITIVITY } from '../utils/constants';
+import {
+ GYRO_DEADZONE_ANDROID,
+ GYRO_MAX_TILT_ANDROID,
+ GYRO_SENSITIVITY_ANDROID,
+ GYRO_CALIBRATION_SAMPLES_ANDROID,
+ GYRO_INVERT_BETA_ANDROID,
+ GYRO_USE_GAMMA_ANDROID,
+ GYRO_USE_ALPHA_ANDROID,
+ GYRO_DEADZONE_IOS,
+ GYRO_MAX_TILT_IOS,
+ GYRO_SENSITIVITY_IOS,
+ GYRO_CALIBRATION_SAMPLES_IOS,
+ GYRO_INVERT_BETA_IOS,
+ GYRO_USE_GAMMA_IOS,
+ GYRO_USE_ALPHA_IOS,
+} from '../utils/constants';
/**
* Gestion du gyroscope pour iOS et Android
* Retourne une valeur normalisée entre -1 et 1
+ *
+ * Configuration centralisée dans constants.ts pour faciliter le débogage:
+ * - iOS: GYRO_*_IOS (sensibilité réduite, pas d'inversion beta)
+ * - Android: GYRO_*_ANDROID (sensibilité élevée, inversion beta)
+ *
+ * Pour ajuster les contrôles, modifier les constantes dans utils/constants.ts
*/
export class GyroControl {
private tiltValue: number = 0;
private isActive: boolean = false;
+ private lastEventLogTime: number = 0;
+ private lastTiltLogTime: number = 0;
+ private lastAlpha: number = 0; // Rotation boussole (0-360°)
+ private lastBeta: number = 0; // Inclinaison avant/arrière
+ private lastGamma: number = 0; // Inclinaison gauche/droite
+ private lastAngle: number = 0;
+ private lastAbs: boolean | null = null;
+ private lastEventAt: number = 0;
+ private baselineTilt: number = 0; // Baseline pour calibration
+ private isCalibrated: boolean = false;
+ private calibrationSamples: number[] = [];
+ private readonly calibrationSamplesCount: number; // Nombre d'échantillons pour moyenne (varie selon plateforme)
+
+ // Configuration spécifique selon la plateforme
+ private readonly isIOS: boolean;
+ private readonly deadzone: number;
+ private readonly maxTilt: number;
+ private readonly sensitivity: number;
+ private readonly invertBeta: boolean; // Inverser la lecture du beta?
+ private readonly useGamma: boolean; // Utiliser gamma au lieu de beta en paysage?
+ private readonly useAlpha: boolean; // Utiliser alpha (rotation boussole)?
constructor() {
+ // Détecter iOS
+ this.isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
+ (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
+
+ // Configurer selon la plateforme (toutes les valeurs viennent de constants.ts)
+ if (this.isIOS) {
+ this.deadzone = GYRO_DEADZONE_IOS;
+ this.maxTilt = GYRO_MAX_TILT_IOS;
+ this.sensitivity = GYRO_SENSITIVITY_IOS;
+ this.calibrationSamplesCount = GYRO_CALIBRATION_SAMPLES_IOS;
+ this.invertBeta = GYRO_INVERT_BETA_IOS;
+ this.useGamma = GYRO_USE_GAMMA_IOS;
+ this.useAlpha = GYRO_USE_ALPHA_IOS;
+ console.log('[GyroControl] ✅ Configuration iOS chargée depuis constants.ts', {
+ deadzone: this.deadzone,
+ maxTilt: this.maxTilt,
+ sensitivity: this.sensitivity,
+ calibrationSamples: this.calibrationSamplesCount,
+ invertBeta: this.invertBeta,
+ useGamma: this.useGamma,
+ useAlpha: this.useAlpha
+ });
+ } else {
+ this.deadzone = GYRO_DEADZONE_ANDROID;
+ this.maxTilt = GYRO_MAX_TILT_ANDROID;
+ this.sensitivity = GYRO_SENSITIVITY_ANDROID;
+ this.calibrationSamplesCount = GYRO_CALIBRATION_SAMPLES_ANDROID;
+ this.invertBeta = GYRO_INVERT_BETA_ANDROID;
+ this.useGamma = GYRO_USE_GAMMA_ANDROID;
+ this.useAlpha = GYRO_USE_ALPHA_ANDROID;
+ console.log('[GyroControl] ✅ Configuration Android chargée depuis constants.ts', {
+ deadzone: this.deadzone,
+ maxTilt: this.maxTilt,
+ sensitivity: this.sensitivity,
+ calibrationSamples: this.calibrationSamplesCount,
+ invertBeta: this.invertBeta,
+ useGamma: this.useGamma,
+ useAlpha: this.useAlpha
+ });
+ }
+
this.setupGyroscope();
}
@@ -46,33 +129,130 @@ export class GyroControl {
// Déterminer l'axe horizontal en fonction de l'orientation écran
const angle = (window.screen.orientation?.angle ?? (window as any).orientation ?? 0) as number;
+ const alpha = event.alpha ?? 0; // rotation boussole (0-360°)
const beta = event.beta ?? 0; // inclinaison avant/arrière
const gamma = event.gamma ?? 0; // inclinaison gauche/droite
-
- // En paysage, la gauche/droite correspond à beta; en portrait, à gamma
- let horizontalTiltDeg = gamma;
- if (angle === 90) {
- horizontalTiltDeg = -beta;
- } else if (angle === -90 || angle === 270) {
- horizontalTiltDeg = beta;
+ const now = Date.now();
+ this.lastAlpha = alpha;
+ this.lastBeta = beta;
+ this.lastGamma = gamma;
+ this.lastAngle = angle;
+ this.lastAbs = event.absolute ?? null;
+ this.lastEventAt = now;
+ if (now - this.lastEventLogTime > 1000) {
+ console.log('[Gyro] event', {
+ angle,
+ alpha,
+ beta,
+ gamma,
+ abs: event.absolute ?? null,
+ });
+ this.lastEventLogTime = now;
}
- const relativeTilt = horizontalTiltDeg;
+ // IMPORTANT : Déterminer l'axe à utiliser selon l'orientation réelle
+ // iOS: beta (avant/arrière), Android: peut utiliser alpha (rotation boussole), gamma, ou beta
+ let horizontalTiltDeg: number;
- // Appliquer la deadzone
- if (Math.abs(relativeTilt) < GYRO_DEADZONE) {
+ // Si on utilise alpha (rotation boussole - Android uniquement)
+ if (this.useAlpha) {
+ // Alpha va de 0 à 360°
+ // Convertir en -180 à 180 pour avoir une baseline centrée
+ let normalizedAlpha = alpha;
+ if (normalizedAlpha > 180) {
+ normalizedAlpha -= 360;
+ }
+ horizontalTiltDeg = normalizedAlpha;
+ }
+ // ANDROID : Forcer l'utilisation de BETA quelle que soit l'orientation
+ else if (!this.isIOS && !this.useGamma && !this.useAlpha) {
+ // Android avec configuration BETA : utiliser beta directement
+ horizontalTiltDeg = this.invertBeta ? -beta : beta;
+
+ // Ajuster selon l'orientation du téléphone
+ // Si angle = 90° (paysage avec home button à droite) ou -90° (home button à gauche)
+ if (Math.abs(angle - 90) < 45) {
+ // Paysage normal (90°) : pas d'inversion supplémentaire
+ // horizontalTiltDeg reste tel quel
+ } else if (Math.abs(angle + 90) < 45 || Math.abs(angle - 270) < 45) {
+ // Paysage inversé (-90° ou 270°) : inverser
+ horizontalTiltDeg = -horizontalTiltDeg;
+ }
+ }
+ // Sinon, si angle est proche de 0° ou 180° (mode paysage classique)
+ else if (Math.abs(angle) < 45 || Math.abs(angle - 180) < 45 || Math.abs(angle + 180) < 45) {
+ // Mode paysage
+ if (this.useGamma) {
+ // Android : utiliser gamma pour l'inclinaison gauche/droite
+ horizontalTiltDeg = gamma;
+
+ // Inverser si paysage inversé (angle proche de 180)
+ if (Math.abs(angle - 180) < 45 || Math.abs(angle + 180) < 45) {
+ horizontalTiltDeg = -horizontalTiltDeg;
+ }
+ } else {
+ // iOS : utiliser beta pour l'inclinaison avant/arrière
+ // L'inversion du beta est configurée dans constants.ts selon la plateforme
+ horizontalTiltDeg = this.invertBeta ? -beta : beta;
+
+ // Inverser si paysage inversé (angle proche de 180)
+ if (Math.abs(angle - 180) < 45 || Math.abs(angle + 180) < 45) {
+ horizontalTiltDeg = -horizontalTiltDeg;
+ }
+ }
+ }
+ // Si angle proche de 90° ou -90°/270° (mode portrait)
+ else {
+ // Mode portrait : utiliser gamma pour l'inclinaison gauche/droite
+ horizontalTiltDeg = gamma;
+
+ // Ajuster le signe selon l'orientation
+ if (angle < 0 || angle > 180) {
+ horizontalTiltDeg = -gamma;
+ }
+ }
+
+ // Calibration automatique : collecter les premiers échantillons
+ if (!this.isCalibrated) {
+ this.calibrationSamples.push(horizontalTiltDeg);
+ if (this.calibrationSamples.length >= this.calibrationSamplesCount) {
+ // Calculer la moyenne pour établir la baseline
+ this.baselineTilt = this.calibrationSamples.reduce((sum, val) => sum + val, 0) / this.calibrationSamples.length;
+ this.isCalibrated = true;
+ console.log(`[Gyro ${this.isIOS ? 'iOS' : 'Android'}] ✅ Calibration terminée - Baseline: ${this.baselineTilt.toFixed(2)}° (${this.calibrationSamplesCount} échantillons)`);
+ } else {
+ console.log(`[Gyro ${this.isIOS ? 'iOS' : 'Android'}] Calibration en cours... (${this.calibrationSamples.length}/${this.calibrationSamplesCount})`);
+ this.tiltValue = 0; // Pas de mouvement pendant la calibration
+ return;
+ }
+ }
+
+ // Soustraire la baseline pour obtenir le tilt relatif à la position neutre
+ const relativeTilt = horizontalTiltDeg - this.baselineTilt;
+
+ // Appliquer la deadzone (spécifique à la plateforme)
+ if (Math.abs(relativeTilt) < this.deadzone) {
this.tiltValue = 0;
return;
}
- // Normaliser entre -1 et 1
- let normalizedTilt = relativeTilt / GYRO_MAX_TILT;
+ // Normaliser entre -1 et 1 (avec maxTilt spécifique à la plateforme)
+ let normalizedTilt = relativeTilt / this.maxTilt;
// Clamper entre -1 et 1
normalizedTilt = Math.max(-1, Math.min(1, normalizedTilt));
- // Inversion gauche/droite (plus naturel selon retour)
- this.tiltValue = -normalizedTilt;
+ // Pas d'inversion finale - elle est déjà faite au niveau de la lecture du beta/gamma
+ // iOS: beta direct → pencher en arrière = beta négatif → mouvement droite
+ // Android: gamma direct → pencher à droite = gamma positif → mouvement droite
+ this.tiltValue = normalizedTilt;
+
+ if (now - this.lastTiltLogTime > 1000) {
+ const axisUsed = this.useAlpha ? 'alpha' : (this.useGamma ? 'gamma' : 'beta');
+ console.log(`[Gyro ${this.isIOS ? 'iOS' : 'Android'}] tilt normalized`, this.tiltValue,
+ `(axe: ${axisUsed}, raw: ${horizontalTiltDeg.toFixed(1)}°, baseline: ${this.baselineTilt.toFixed(1)}°, relative: ${relativeTilt.toFixed(1)}°, deadzone: ${this.deadzone}°, maxTilt: ${this.maxTilt}°)`);
+ this.lastTiltLogTime = now;
+ }
}
/**
@@ -83,18 +263,22 @@ export class GyroControl {
}
/**
- * Retourne la vitesse calculée depuis le tilt
+ * Retourne la vitesse calculée depuis le tilt (avec sensibilité spécifique à la plateforme)
*/
public getVelocity(): number {
- return this.tiltValue * GYRO_SENSITIVITY;
+ return this.tiltValue * this.sensitivity;
}
/**
* Calibre le gyroscope (définit l'orientation actuelle comme neutre)
*/
public calibrate(): void {
- // Calibration simplifiée : pas de base dynamique, on recentre juste à 0
+ // Reset de la calibration pour recalculer la baseline
+ this.isCalibrated = false;
+ this.calibrationSamples = [];
+ this.baselineTilt = 0;
this.tiltValue = 0;
+ console.log('[Gyro] Recalibration demandée...');
}
/**
@@ -121,4 +305,47 @@ export class GyroControl {
this.isActive = false;
this.tiltValue = 0;
}
+
+ /**
+ * Debug info for on-screen diagnostics.
+ */
+ public getDebugInfo(): {
+ active: boolean;
+ tilt: number;
+ alpha: number;
+ beta: number;
+ gamma: number;
+ angle: number;
+ abs: boolean | null;
+ lastEventAt: number;
+ calibrated: boolean;
+ baseline: number;
+ platform: string;
+ deadzone: number;
+ maxTilt: number;
+ sensitivity: number;
+ invertBeta: boolean;
+ useGamma: boolean;
+ useAlpha: boolean;
+ } {
+ return {
+ active: this.isActive,
+ tilt: this.tiltValue,
+ alpha: this.lastAlpha,
+ beta: this.lastBeta,
+ gamma: this.lastGamma,
+ angle: this.lastAngle,
+ abs: this.lastAbs,
+ lastEventAt: this.lastEventAt,
+ calibrated: this.isCalibrated,
+ baseline: this.baselineTilt,
+ platform: this.isIOS ? 'iOS' : 'Android',
+ deadzone: this.deadzone,
+ maxTilt: this.maxTilt,
+ sensitivity: this.sensitivity,
+ invertBeta: this.invertBeta,
+ useGamma: this.useGamma,
+ useAlpha: this.useAlpha,
+ };
+ }
}
diff --git a/src/entities/Cage.ts b/src/entities/Cage.ts
new file mode 100644
index 0000000..68f55d8
--- /dev/null
+++ b/src/entities/Cage.ts
@@ -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();
+ }
+}
diff --git a/src/entities/Dog.ts b/src/entities/Dog.ts
new file mode 100644
index 0000000..9bb79b4
--- /dev/null
+++ b/src/entities/Dog.ts
@@ -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);
+ }
+}
diff --git a/src/entities/Player.ts b/src/entities/Player.ts
index 7cf542e..cfdd94a 100644
--- a/src/entities/Player.ts
+++ b/src/entities/Player.ts
@@ -6,6 +6,8 @@ import {
PLAYER_ACCELERATION,
PLAYER_MAX_JUMPS,
RESPAWN_INVINCIBILITY_TIME,
+ USER1_HITBOX,
+ USER2_HITBOX,
} from '../utils/constants';
/**
@@ -19,11 +21,19 @@ export class Player extends Phaser.Physics.Arcade.Sprite {
private invincibilityTimer?: Phaser.Time.TimerEvent;
private animationsCreated: boolean = false;
private wasAir: boolean = false;
+ private playerPrefix: string = 'player_user1'; // Préfixe pour les sprites (user1 ou user2)
constructor(scene: Phaser.Scene, x: number, y: number) {
- // Pour l'instant, utiliser un sprite simple
- // TODO: Remplacer par le spritesheet du neveu
- super(scene, x, y, 'player');
+ // Récupérer le joueur sélectionné depuis le registry
+ const selectedPlayer = scene.registry.get('selectedPlayer') as string | undefined;
+ const playerPrefix = selectedPlayer === 'user2' ? 'player_user2' : 'player_user1';
+
+ // Utiliser le spritesheet comme texture de base
+ const spritesheetKey = selectedPlayer === 'user2' ? 'player_user2' : 'player_user1';
+ super(scene, x, y, spritesheetKey, 0); // Frame 0 du spritesheet
+
+ // Stocker le préfixe pour utilisation ultérieure
+ this.playerPrefix = playerPrefix;
// Ajouter à la scène
scene.add.existing(this);
@@ -34,35 +44,41 @@ export class Player extends Phaser.Physics.Arcade.Sprite {
body.setGravityY(PLAYER_GRAVITY);
body.setCollideWorldBounds(true); // Collision avec les limites du monde
body.onWorldBounds = true; // Active les événements de collision
- body.setSize(40, 70); // Hitbox
body.setMaxVelocity(PLAYER_MAX_SPEED, 1000);
- // Aligner la hitbox en bas du sprite (80x169 par défaut)
- body.setOffset((this.width - 40) / 2, (this.height || 169) - 70);
-
- // Temporaire : créer un rectangle coloré si pas de texture
- if (!scene.textures.exists('player')) {
- this.createPlaceholderTexture(scene);
- }
this.setOrigin(0.5, 1); // Origine en bas au centre
- body.setOffset((this.width - 40) / 2, (this.height || 169) - 70); // aligner la hitbox en bas
+ this.setScale(1); // Échelle 1:1 pour les deux joueurs
+
+ // Utiliser la configuration de hitbox spécifique au joueur
+ const hitboxConfig = selectedPlayer === 'user2' ? USER2_HITBOX : USER1_HITBOX;
+
+ // Attendre que le sprite soit complètement initialisé pour configurer la hitbox
+ this.scene.time.delayedCall(0, () => {
+ const spriteWidth = this.width;
+ const spriteHeight = this.height;
+
+ // Centrer la hitbox horizontalement
+ const offsetX = (spriteWidth - hitboxConfig.width) / 2;
+ // Placer la hitbox en bas du sprite
+ const offsetY = spriteHeight - hitboxConfig.height;
+
+ body.setSize(hitboxConfig.width, hitboxConfig.height);
+ body.setOffset(offsetX, offsetY);
+
+ console.log(`🎯 Hitbox FINALE configurée pour ${this.playerPrefix}:`, {
+ selectedPlayer,
+ spriteActualSize: `${spriteWidth}x${spriteHeight}px`,
+ hitboxSize: `${hitboxConfig.width}x${hitboxConfig.height}px`,
+ offset: `x=${offsetX.toFixed(1)}, y=${offsetY.toFixed(1)}px`
+ });
+ });
this.ensureAnimations();
- this.setTexture('player_walk_1'); // frame par défaut
+
+ // Définir la frame par défaut (idle = première frame du spritesheet)
+ this.setFrame(0);
}
- /**
- * Crée une texture temporaire pour le joueur
- */
- private createPlaceholderTexture(scene: Phaser.Scene): void {
- const graphics = scene.add.graphics();
- graphics.fillStyle(0xFF0000, 1);
- graphics.fillRect(0, 0, 50, 80);
- graphics.generateTexture('player', 50, 80);
- graphics.destroy();
-
- this.setTexture('player');
- }
/**
* Met à jour le mouvement du joueur
@@ -117,15 +133,21 @@ export class Player extends Phaser.Physics.Arcade.Sprite {
this.jumpCount++;
// Déclencher l'anim de saut immédiatement si disponible
- if (this.animationsCreated && this.anims.get('player-jump')) {
- this.anims.play('player-jump', true);
+ const jumpAnimKey = `${this.playerPrefix}-jump`;
+ const animExists = this.scene.anims.exists(jumpAnimKey);
+ console.log(`🦘 Tentative de saut #${this.jumpCount}: anim "${jumpAnimKey}" existe = ${animExists}, animationsCreated = ${this.animationsCreated}`);
+
+ if (this.animationsCreated && animExists) {
+ this.anims.play(jumpAnimKey, true);
+ console.log(`✅ Animation de saut jouée: ${jumpAnimKey}`);
+ } else {
+ console.warn(`⚠️ Animation de saut non disponible: ${jumpAnimKey}`);
}
// Effet sonore de saut (volume global SFX depuis le registry)
const sfxVolume = (this.scene.registry.get('sfxVolume') as number | undefined) ?? 1;
this.scene.sound.play('sfx_jump', { volume: 0.5 * sfxVolume });
- // TODO: Jouer son de saut (différent pour double saut)
console.log(`Saut ${this.jumpCount}/${PLAYER_MAX_JUMPS}`);
}
}
@@ -141,32 +163,46 @@ export class Player extends Phaser.Physics.Arcade.Sprite {
this.jumpCount = 0;
}
- // TODO: Jouer les animations appropriées
if (!this.animationsCreated) return;
const isMoving = Math.abs(this.velocityX) > 10;
const bodyState = this.body as Phaser.Physics.Arcade.Body;
const isAir = !bodyState.touching.down;
+ // Utiliser les clés d'animation spécifiques au joueur
+ const walkAnimKey = `${this.playerPrefix}-walk`;
+ const idleAnimKey = `${this.playerPrefix}-idle`;
+ const jumpAnimKey = `${this.playerPrefix}-jump`;
+
// Transition sol -> air : jouer l'anim jump une fois
if (isAir && !this.wasAir) {
- const jumpAnim = this.anims.get('player-jump');
- if (jumpAnim) {
- this.anims.play('player-jump', true);
+ if (this.scene.anims.exists(jumpAnimKey)) {
+ this.anims.play(jumpAnimKey, true);
}
}
// Sol : choisir walk ou idle
if (!isAir) {
if (isMoving) {
- this.anims.play('player-walk', true);
+ if (this.scene.anims.exists(walkAnimKey)) {
+ this.anims.play(walkAnimKey, true);
+ }
} else {
- this.anims.play('player-idle', true);
+ // Jouer idle uniquement si on n'est pas déjà en train de le jouer
+ if (this.anims.currentAnim?.key !== idleAnimKey) {
+ if (this.scene.anims.exists(idleAnimKey)) {
+ this.anims.play(idleAnimKey, true);
+ } else {
+ // Fallback : afficher directement la frame 0 du spritesheet
+ this.anims.stop();
+ this.setFrame(0);
+ }
+ }
}
} else {
// En l'air : si pas d'anim jump, forcer dernière frame jump
- if (this.anims.currentAnim?.key !== 'player-jump' && this.anims.get('player-jump')) {
- this.anims.play('player-jump', true);
+ if (this.anims.currentAnim?.key !== jumpAnimKey && this.scene.anims.exists(jumpAnimKey)) {
+ this.anims.play(jumpAnimKey, true);
}
}
@@ -232,45 +268,97 @@ export class Player extends Phaser.Physics.Arcade.Sprite {
}
/**
- * Création des animations (walk + idle)
+ * Création des animations (walk + idle + jump) selon le joueur sélectionné
*/
private ensureAnimations(): void {
if (this.animationsCreated) return;
const scene = this.scene;
- const walkFrames = ['player_walk_1', 'player_walk_2', 'player_walk_3', 'player_walk_4']
+ const selectedPlayer = scene.registry.get('selectedPlayer') as string | undefined;
+
+ // Utiliser le préfixe du joueur (player_user1 ou player_user2)
+ const walkFrameKeys = [
+ `${this.playerPrefix}_walk_1`,
+ `${this.playerPrefix}_walk_2`,
+ `${this.playerPrefix}_walk_3`,
+ `${this.playerPrefix}_walk_4`
+ ];
+
+ const jumpFrameKeys = [
+ `${this.playerPrefix}_jump_1`,
+ `${this.playerPrefix}_jump_2`,
+ `${this.playerPrefix}_jump_3`,
+ `${this.playerPrefix}_jump_4`,
+ `${this.playerPrefix}_jump_5`
+ ];
+
+ // Debug: vérifier quelles textures existent
+ console.log(`🔍 Vérification des textures pour ${this.playerPrefix}:`);
+ walkFrameKeys.forEach(key => {
+ const exists = scene.textures.exists(key);
+ console.log(` Walk frame "${key}": ${exists ? '✅' : '❌'}`);
+ });
+ jumpFrameKeys.forEach(key => {
+ const exists = scene.textures.exists(key);
+ console.log(` Jump frame "${key}": ${exists ? '✅' : '❌'}`);
+ });
+
+ const walkFrames = walkFrameKeys
.filter((key) => scene.textures.exists(key))
.map((key) => ({ key }));
- const jumpFrames = ['player_jump_1', 'player_jump_2', 'player_jump_3', 'player_jump_4', 'player_jump_5']
+
+ const jumpFrames = jumpFrameKeys
.filter((key) => scene.textures.exists(key))
.map((key) => ({ key }));
+ // Créer des clés d'animation uniques par joueur
+ const walkAnimKey = `${this.playerPrefix}-walk`;
+ const idleAnimKey = `${this.playerPrefix}-idle`;
+ const jumpAnimKey = `${this.playerPrefix}-jump`;
+
if (walkFrames.length >= 2) {
- scene.anims.create({
- key: 'player-walk',
- frames: walkFrames,
- frameRate: 10,
- repeat: -1,
- });
+ // Vérifier si l'animation existe déjà avant de la créer
+ if (!scene.anims.exists(walkAnimKey)) {
+ scene.anims.create({
+ key: walkAnimKey,
+ frames: walkFrames,
+ frameRate: 10,
+ repeat: -1,
+ });
+ }
+ }
+ // Animation idle : utilise la frame 0 du spritesheet (pose debout)
+ const spritesheetKey = selectedPlayer === 'user2' ? 'player_user2' : 'player_user1';
+ if (!scene.anims.exists(idleAnimKey)) {
scene.anims.create({
- key: 'player-idle',
- frames: [{ key: walkFrames[0].key }],
+ key: idleAnimKey,
+ frames: [{ key: spritesheetKey, frame: 0 }],
frameRate: 1,
repeat: -1,
});
}
if (jumpFrames.length >= 2) {
- scene.anims.create({
- key: 'player-jump',
- frames: jumpFrames,
- frameRate: 12,
- repeat: 0,
- });
+ if (!scene.anims.exists(jumpAnimKey)) {
+ scene.anims.create({
+ key: jumpAnimKey,
+ frames: jumpFrames,
+ frameRate: 12,
+ repeat: 0,
+ });
+ console.log(`✅ Animation de saut créée: ${jumpAnimKey} avec ${jumpFrames.length} frames`);
+ }
+ } else {
+ console.warn(`⚠️ Pas assez de frames pour l'animation de saut (${jumpFrames.length}/2 requis)`);
}
this.animationsCreated = walkFrames.length >= 2 || jumpFrames.length >= 2;
+ console.log(`🎬 Animations ${this.playerPrefix}:`, {
+ walk: walkFrames.length,
+ jump: jumpFrames.length,
+ created: this.animationsCreated
+ });
}
/**
diff --git a/src/entities/SuperTreasure.ts b/src/entities/SuperTreasure.ts
index df8a0ce..e4ea9d6 100644
--- a/src/entities/SuperTreasure.ts
+++ b/src/entities/SuperTreasure.ts
@@ -9,18 +9,20 @@ export class SuperTreasure extends Phaser.Physics.Arcade.Sprite {
private pulseTimer: Phaser.Time.TimerEvent;
constructor(scene: Phaser.Scene, x: number, y: number) {
- super(scene, x, y, 'supertreasure');
+ super(scene, x, y, 'star_sprite');
scene.add.existing(this);
scene.physics.add.existing(this);
- // Créer texture temporaire si elle n'existe pas
- if (!scene.textures.exists('supertreasure')) {
+ // Utiliser le sprite star_40.png (40x40px) au lieu de créer une texture
+ // Si le sprite n'existe pas, créer une texture temporaire en fallback
+ if (scene.textures.exists('star_sprite')) {
+ this.setTexture('star_sprite');
+ } else if (!scene.textures.exists('supertreasure')) {
this.createPlaceholderTexture(scene);
+ this.setTexture('supertreasure');
}
- this.setTexture('supertreasure');
-
// Taille plus grande que les cadeaux normaux
this.setScale(1.5);
diff --git a/src/entities/TreasureChest.ts b/src/entities/TreasureChest.ts
index 7a80e22..68d7467 100644
--- a/src/entities/TreasureChest.ts
+++ b/src/entities/TreasureChest.ts
@@ -8,6 +8,7 @@ import Phaser from 'phaser';
export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
private isOpen: boolean = false;
private requiredGifts: number;
+ private requirementText?: Phaser.GameObjects.Text;
constructor(scene: Phaser.Scene, x: number, y: number, requiredGifts: number = 15) {
super(scene, x, y, 'chest');
@@ -108,10 +109,10 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
}
/**
- * Crée le texte qui indique le nombre de cadeaux requis
+ * Crée le texte qui indique le nombre de cadeaux requis (caché par défaut)
*/
private createRequirementText(scene: Phaser.Scene, x: number, y: number): void {
- const text = scene.add.text(
+ this.requirementText = scene.add.text(
x,
y - 80,
`🎁 ${this.requiredGifts} cadeaux requis`,
@@ -123,12 +124,13 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
fontStyle: 'bold',
}
);
- text.setOrigin(0.5);
- text.setDepth(this.depth + 1);
+ this.requirementText.setOrigin(0.5);
+ this.requirementText.setDepth(this.depth + 1);
+ this.requirementText.setVisible(false); // Caché par défaut
// Animation pulse
scene.tweens.add({
- targets: text,
+ targets: this.requirementText,
scaleX: 1.1,
scaleY: 1.1,
duration: 800,
@@ -137,6 +139,20 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
});
}
+ /**
+ * Affiche le texte des cadeaux requis
+ */
+ public showRequirementText(): void {
+ this.requirementText?.setVisible(true);
+ }
+
+ /**
+ * Cache le texte des cadeaux requis
+ */
+ public hideRequirementText(): void {
+ this.requirementText?.setVisible(false);
+ }
+
/**
* Vérifie si le joueur peut ouvrir le coffre
*/
@@ -145,10 +161,10 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
}
/**
- * Ouvre le coffre et donne le mega bonus
+ * Ouvre le coffre et donne la clé
*/
- public open(scene: Phaser.Scene): number {
- if (this.isOpen) return 0;
+ public open(scene: Phaser.Scene): { bonus: number; hasKey: boolean } {
+ if (this.isOpen) return { bonus: 0, hasKey: false };
this.isOpen = true;
this.setTexture('chest-open');
@@ -159,13 +175,13 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
// Particules dorées qui explosent
this.createExplosionParticles(scene);
- // Message épique
+ // Message épique (sans la clé - elle sera affichée séparément)
const megaBonusText = scene.add.text(
scene.cameras.main.scrollX + scene.cameras.main.width / 2,
scene.cameras.main.height / 2 - 100,
- '🏆 COFFRE OUVERT ! 🏆\n★★ MEGA BONUS +1000 ★★',
+ '🏆 COFFRE OUVERT ! 🏆\n★★ BONUS +1000 ★★',
{
- fontSize: '56px',
+ fontSize: '48px',
color: '#FFD700',
stroke: '#FF4500',
strokeThickness: 8,
@@ -190,9 +206,9 @@ export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
},
});
- console.log('🏆 COFFRE AU TRÉSOR OUVERT ! MEGA BONUS +1000 !');
+ console.log('🏆 COFFRE AU TRÉSOR OUVERT ! BONUS +1000 + CLÉ !');
- return 1000; // Mega bonus de points
+ return { bonus: 1000, hasKey: true }; // Mega bonus de points + clé
}
/**
diff --git a/src/game.ts b/src/game.ts
index 8eed933..9e926dc 100644
--- a/src/game.ts
+++ b/src/game.ts
@@ -1,9 +1,11 @@
import Phaser from 'phaser';
import { GAME_WIDTH, GAME_HEIGHT } from './utils/constants';
import { BootScene } from './scenes/BootScene';
+import { PlayerSelectScene } from './scenes/PlayerSelectScene';
import { MenuScene } from './scenes/MenuScene';
import { GameScene } from './scenes/GameScene';
import { IntroScene } from './scenes/IntroScene';
+import { IntroVideoScene } from './scenes/IntroVideoScene';
import { EndScene } from './scenes/EndScene';
// Configuration Phaser
@@ -23,7 +25,15 @@ const config: Phaser.Types.Core.GameConfig = {
debug: false, // Mettre à true pour voir les hitboxes
},
},
- scene: [BootScene, IntroScene, MenuScene, GameScene, EndScene],
+ scene: [
+ BootScene,
+ PlayerSelectScene,
+ IntroScene,
+ IntroVideoScene,
+ MenuScene,
+ GameScene,
+ EndScene,
+ ],
backgroundColor: '#87CEEB',
render: {
pixelArt: false,
diff --git a/src/main.ts b/src/main.ts
index 2e8b914..2b1e2b1 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -6,12 +6,13 @@ const game = new Phaser.Game(config);
// Gestion du fullscreen au clic (optionnel)
window.addEventListener('load', () => {
- // Enregistrer le service worker pour PWA
- if ('serviceWorker' in navigator) {
- navigator.serviceWorker.register('/service-worker.js').catch((error) => {
- console.log('Service Worker registration failed:', error);
- });
- }
+ // TODO: Enregistrer le service worker pour PWA (désactivé temporairement)
+ // Il faut d'abord créer le fichier public/service-worker.js
+ // if ('serviceWorker' in navigator) {
+ // navigator.serviceWorker.register('/service-worker.js').catch((error) => {
+ // console.log('Service Worker registration failed:', error);
+ // });
+ // }
// Bloquer le zoom pinch sur mobile
document.addEventListener('gesturestart', (e) => e.preventDefault());
diff --git a/src/scenes/BootScene.ts b/src/scenes/BootScene.ts
index 8517df9..af91deb 100644
--- a/src/scenes/BootScene.ts
+++ b/src/scenes/BootScene.ts
@@ -42,21 +42,36 @@ export class BootScene extends Phaser.Scene {
loadingText.destroy();
});
- // Sprites du joueur (80x169, 1 frame pour l'instant)
- this.load.spritesheet('player', 'assets/sprites/player_spritesheet.png', {
- frameWidth: 80,
- frameHeight: 169,
+ // Charger les sprites des deux joueurs
+ // User1 (Baptiste) - dimensions réelles du spritesheet 93x224px
+ this.load.spritesheet('player_user1', 'assets/sprites/user1/player_spritesheet.png', {
+ frameWidth: 93,
+ frameHeight: 224,
});
- // Frames de marche (sprite individuel)
- this.load.image('player_walk_1', 'assets/sprites/walk_1.png');
- this.load.image('player_walk_2', 'assets/sprites/walk_2.png');
- this.load.image('player_walk_3', 'assets/sprites/walk_3.png');
- this.load.image('player_walk_4', 'assets/sprites/walk_4.png');
- this.load.image('player_jump_1', 'assets/sprites/jump_1.png');
- this.load.image('player_jump_2', 'assets/sprites/jump_2.png');
- this.load.image('player_jump_3', 'assets/sprites/jump_3.png');
- this.load.image('player_jump_4', 'assets/sprites/jump_4.png');
- this.load.image('player_jump_5', 'assets/sprites/jump_5.png');
+ this.load.image('player_user1_walk_1', 'assets/sprites/user1/walk_1.png');
+ this.load.image('player_user1_walk_2', 'assets/sprites/user1/walk_2.png');
+ this.load.image('player_user1_walk_3', 'assets/sprites/user1/walk_3.png');
+ this.load.image('player_user1_walk_4', 'assets/sprites/user1/walk_4.png');
+ this.load.image('player_user1_jump_1', 'assets/sprites/user1/jump_1.png');
+ this.load.image('player_user1_jump_2', 'assets/sprites/user1/jump_2.png');
+ this.load.image('player_user1_jump_3', 'assets/sprites/user1/jump_3.png');
+ this.load.image('player_user1_jump_4', 'assets/sprites/user1/jump_4.png');
+ this.load.image('player_user1_jump_5', 'assets/sprites/user1/jump_5.png');
+
+ // User2 (Julien) - dimensions réelles du spritesheet
+ this.load.spritesheet('player_user2', 'assets/sprites/user2/player_spritesheet.png', {
+ frameWidth: 93,
+ frameHeight: 224,
+ });
+ this.load.image('player_user2_walk_1', 'assets/sprites/user2/walk_1.png');
+ this.load.image('player_user2_walk_2', 'assets/sprites/user2/walk_2.png');
+ this.load.image('player_user2_walk_3', 'assets/sprites/user2/walk_3.png');
+ this.load.image('player_user2_walk_4', 'assets/sprites/user2/walk_4.png');
+ this.load.image('player_user2_jump_1', 'assets/sprites/user2/jump_1.png');
+ this.load.image('player_user2_jump_2', 'assets/sprites/user2/jump_2.png');
+ this.load.image('player_user2_jump_3', 'assets/sprites/user2/jump_3.png');
+ this.load.image('player_user2_jump_4', 'assets/sprites/user2/jump_4.png');
+ this.load.image('player_user2_jump_5', 'assets/sprites/user2/jump_5.png');
// Musique de fond
this.load.audio('bgm', 'assets/audio/01. Ground Theme.mp3');
@@ -72,24 +87,107 @@ export class BootScene extends Phaser.Scene {
this.load.audio('sfx_hit', ['assets/audio/champignon.mp3', 'assets/audio/champignon.aiff']);
this.load.audio('sfx_super', 'assets/audio/super_tresor.mp3');
this.load.audio('sfx_saute_champi', 'assets/audio/saute_champi.mp3');
+ this.load.audio('sfx_chien', 'assets/audio/chien.mp3');
// Sprites obstacles
this.load.image('obstacle_mushroom', 'assets/sprites/champignon.png');
- // Vidéo d'intro (mp4 uniquement)
+ // Sprites plateformes (blocs de différentes tailles)
+ this.load.image('platform_40', 'assets/sprites/decor/b_40.png');
+ this.load.image('platform_73', 'assets/sprites/decor/b_73.png');
+ this.load.image('platform_130', 'assets/sprites/decor/b_130.png');
+ this.load.image('platform_140', 'assets/sprites/decor/b_140.png');
+ this.load.image('platform_179', 'assets/sprites/decor/b_179.png');
+
+ // Sol (vert)
+ this.load.image('ground_tile', 'assets/sprites/decor/sol_150.png');
+
+ // Sprites objets de jeu
+ this.load.image('gift_sprite', 'assets/sprites/decor/gift_40.png');
+ this.load.image('star_sprite', 'assets/sprites/decor/star_40.png');
+
+ // Selection joueur (portrait)
+ this.load.image('user1', 'assets/images/user1.jpg');
+ this.load.image('user2', 'assets/images/user2.jpg');
+
+ // Images finales (affichées après la vidéo de fin)
+ this.load.image('finale_user1', 'assets/sprites/user1/image finale1.jpeg');
+ this.load.image('finale_user2', 'assets/sprites/user2/image finale2.jpeg');
+
+ // Vidéos d'intro (mp4 uniquement) - une par joueur
// Le 3e paramètre 'noAudio' est à false pour garder l'audio si présent
// Ajout d'un timestamp pour forcer le rechargement (éviter le cache)
const timestamp = Date.now();
- this.load.video('intro', `assets/video/intro.mp4?v=${timestamp}`, false);
+ this.load.video('intro_user1', `assets/video/intro_user1.mp4?v=${timestamp}`, false);
+ this.load.video('intro_user2', `assets/video/intro_user2.mp4?v=${timestamp}`, false);
// Vidéo de fin (quand le joueur gagne)
- this.load.video('end', `assets/video/end_J.mp4?v=${timestamp}`, false);
+ this.load.video('end', `assets/video/zoe cadeaux1.mp4?v=${timestamp}`, false);
+
+ // Sprites du chien
+ this.load.image('dog-idle-1', 'assets/sprites/dog/idle_01.png');
+ this.load.image('dog-idle-2', 'assets/sprites/dog/idle_02.png');
+ this.load.image('dog-idle-3', 'assets/sprites/dog/idle_03.png');
+ this.load.image('dog-idle-4', 'assets/sprites/dog/idle_04.png');
+ this.load.image('dog-walkback-1', 'assets/sprites/dog/walkback_01.png');
+ this.load.image('dog-walkback-2', 'assets/sprites/dog/walkback_02.png');
+ this.load.image('dog-walkback-3', 'assets/sprites/dog/walkback_03.png');
+ this.load.image('dog-walkback-4', 'assets/sprites/dog/walkback_04.png');
+ this.load.image('dog-turn-1', 'assets/sprites/dog/turn_01.png');
+ this.load.image('dog-turn-2', 'assets/sprites/dog/turn_02.png');
+ this.load.image('dog-turn-3', 'assets/sprites/dog/turn_03.png');
// TODO: Charger d'autres sprites, backgrounds, sons, etc.
}
create(): void {
- // Passer par l'intro vidéo puis le menu
- this.scene.start('IntroScene');
+ // Créer les animations du chien
+ this.createDogAnimations();
+
+ // Demarrer par la selection joueur (portrait sur mobile)
+ this.scene.start('PlayerSelectScene');
+ }
+
+ /**
+ * Crée les animations du chien
+ */
+ private createDogAnimations(): void {
+ // Animation IDLE (queue qui remue)
+ this.anims.create({
+ key: 'dog-idle',
+ frames: [
+ { key: 'dog-idle-1' },
+ { key: 'dog-idle-2' },
+ { key: 'dog-idle-3' },
+ { key: 'dog-idle-4' }
+ ],
+ frameRate: 6,
+ repeat: -1
+ });
+
+ // Animation WALK BACK (marche vers la gauche)
+ this.anims.create({
+ key: 'dog-walkback',
+ frames: [
+ { key: 'dog-walkback-1' },
+ { key: 'dog-walkback-2' },
+ { key: 'dog-walkback-3' },
+ { key: 'dog-walkback-4' }
+ ],
+ frameRate: 8,
+ repeat: -1
+ });
+
+ // Animation TURN (demi-tour)
+ this.anims.create({
+ key: 'dog-turn',
+ frames: [
+ { key: 'dog-turn-1' },
+ { key: 'dog-turn-2' },
+ { key: 'dog-turn-3' }
+ ],
+ frameRate: 10,
+ repeat: 0
+ });
}
}
diff --git a/src/scenes/EndScene.ts b/src/scenes/EndScene.ts
index 7ccc503..5609a9d 100644
--- a/src/scenes/EndScene.ts
+++ b/src/scenes/EndScene.ts
@@ -18,8 +18,51 @@ export class EndScene extends Phaser.Scene {
console.log('[EndScene] Création de la scène de fin');
console.log('[EndScene] Dimensions:', width, 'x', height);
- // Lancer directement la vidéo de fin
- this.playEndVideo();
+ // Afficher le bouton de lecture au lieu de lancer automatiquement
+ this.showPlayButton();
+ }
+
+ /**
+ * Affiche le bouton de lecture
+ */
+ private showPlayButton(): void {
+ const { width, height } = this.cameras.main;
+
+ // Message "Appuyez sur l'écran pour visualiser la vidéo"
+ const message = this.add.text(
+ width / 2,
+ height / 2 - 80,
+ 'Appuyez sur l\'écran\npour visualiser la vidéo',
+ {
+ fontSize: '32px',
+ color: '#ffffff',
+ align: 'center',
+ fontFamily: 'Arial',
+ stroke: '#000000',
+ strokeThickness: 4,
+ }
+ );
+ message.setOrigin(0.5);
+
+ // Bouton play (cercle avec triangle)
+ const playButton = this.add.container(width / 2, height / 2 + 60);
+
+ const circle = this.add.circle(0, 0, 60, 0x4CAF50, 1);
+ circle.setStrokeStyle(4, 0xffffff);
+ const triangle = this.add.triangle(5, 0, -15, -20, -15, 20, 20, 0, 0xffffff);
+
+ playButton.add([circle, triangle]);
+ circle.setInteractive({ useHandCursor: true });
+
+ // Clic sur le bouton ou n'importe où sur l'écran
+ const startVideo = () => {
+ message.destroy();
+ playButton.destroy();
+ this.playEndVideo();
+ };
+
+ circle.on('pointerdown', startVideo);
+ this.input.once('pointerdown', startVideo);
}
/**
@@ -58,8 +101,16 @@ export class EndScene extends Phaser.Scene {
this.video.setMute(false);
this.video.setLoop(false);
+ // IMPORTANT: Forcer explicitement loop=false sur l'élément HTML5 natif
+ // pour éviter que la vidéo se lise en boucle
+ if (this.video.video) {
+ this.video.video.loop = false;
+ console.log('[EndScene] Attribut loop forcé à false sur l\'élément vidéo natif');
+ }
+
console.log('[EndScene] Démarrage de la lecture');
- const started = this.video.play(true); // Autoplay
+ // IMPORTANT: Utiliser play(true) pour l'autoplay après interaction utilisateur (requis pour iOS)
+ const started = this.video.play(true);
console.log('[EndScene] Lecture démarrée?', started);
if (!started) {
@@ -85,22 +136,40 @@ export class EndScene extends Phaser.Scene {
this.gotoMenu();
});
- // Sécurité : timer basé sur la durée de la vidéo + 2 secondes
+ // IMPORTANT: Écouter aussi l'événement natif 'ended' de l'élément HTML5
+ // C'est la méthode la plus fiable pour détecter la fin d'une vidéo
+ if (this.video.video) {
+ this.video.video.addEventListener('ended', () => {
+ console.log('[EndScene] Vidéo terminée (événement HTML5 ended) → passage au menu');
+ this.gotoMenu();
+ }, { once: true });
+ }
+
+ // Sécurité : timer basé sur la durée de la vidéo + 1 seconde
// Si la vidéo n'est pas finie après sa durée, forcer le passage au menu
this.video.on('metadata', () => {
if (!this.video || !this.video.video) return;
const duration = this.video.getDuration();
console.log('[EndScene] Durée de la vidéo:', duration, 'secondes');
- // Timer de sécurité : durée vidéo + 2 secondes
- this.time.delayedCall((duration + 2) * 1000, () => {
+ // Timer de sécurité : durée vidéo + 1 seconde
+ this.time.delayedCall((duration + 1) * 1000, () => {
if (!this.hasFinished) {
- console.warn('[EndScene] Timer de sécurité déclenché → passage au menu');
+ console.warn('[EndScene] Timer de sécurité (metadata) déclenché → passage au menu');
this.gotoMenu();
}
});
});
+ // Timer de sécurité fixe basé sur la durée connue (36 secondes exactement)
+ // Au cas où les métadonnées ne se chargent pas correctement
+ this.time.delayedCall(36000, () => {
+ if (!this.hasFinished) {
+ console.warn('[EndScene] Timer de sécurité fixe (36s) déclenché → passage au menu');
+ this.gotoMenu();
+ }
+ });
+
// Ajuster si resize
this.scale.on('resize', (gameSize: Phaser.Structs.Size) => {
if (this.video && this.video.isPlaying()) {
@@ -150,13 +219,155 @@ export class EndScene extends Phaser.Scene {
}
/**
- * Passe au menu
+ * Affiche l'image finale après la vidéo
*/
private gotoMenu(): void {
if (this.hasFinished) return;
this.hasFinished = true;
+
+ // Détruire la vidéo
this.video?.stop();
this.video?.destroy();
- this.scene.start('MenuScene');
+
+ // Afficher l'image finale
+ this.showFinalImage();
+ }
+
+ /**
+ * Affiche l'image finale du joueur puis le message de réussite
+ */
+ private showFinalImage(): void {
+ // Récupérer le joueur sélectionné
+ const selectedPlayer = this.registry.get('selectedPlayer') as string | undefined;
+ const imageKey = selectedPlayer === 'user2' ? 'finale_user2' : 'finale_user1';
+
+ console.log('[EndScene] Affichage image finale:', imageKey);
+
+ // Afficher l'image en grand (centrée et mise à l'échelle)
+ const finalImage = this.add.image(
+ this.cameras.main.width / 2,
+ this.cameras.main.height / 2,
+ imageKey
+ );
+
+ // Mettre à l'échelle pour s'adapter à l'écran sans dépasser (mode "contain")
+ // Limiter à 85% de la hauteur/largeur pour laisser de l'espace pour le message
+ const maxWidth = this.cameras.main.width * 0.85;
+ const maxHeight = this.cameras.main.height * 0.85;
+ const scaleX = maxWidth / finalImage.width;
+ const scaleY = maxHeight / finalImage.height;
+ const scale = Math.min(scaleX, scaleY); // Min pour contenir sans déborder
+ finalImage.setScale(scale);
+ finalImage.setDepth(0);
+
+ // Après 2 secondes, afficher le message et le bouton
+ this.time.delayedCall(2000, () => {
+ this.showSuccessMessage();
+ });
+ }
+
+ /**
+ * Affiche le message "Niveau 1 réussi!" et le bouton
+ */
+ private showSuccessMessage(): void {
+ // Message "Niveau 1 réussi!"
+ const successText = this.add.text(
+ this.cameras.main.width / 2,
+ this.cameras.main.height / 2 - 100,
+ 'Niveau 1 réussi!',
+ {
+ fontSize: '64px',
+ color: '#FFD700',
+ stroke: '#000000',
+ strokeThickness: 8,
+ fontStyle: 'bold',
+ }
+ );
+ successText.setOrigin(0.5);
+ successText.setDepth(1000);
+ successText.setAlpha(0);
+
+ // Animation d'apparition du texte
+ this.tweens.add({
+ targets: successText,
+ alpha: 1,
+ scale: { from: 0.5, to: 1.2 },
+ duration: 800,
+ ease: 'Back.easeOut',
+ });
+
+ // Bouton "Passer au niveau 2"
+ const buttonY = this.cameras.main.height / 2 + 100;
+
+ // Fond du bouton
+ const buttonBg = this.add.rectangle(
+ this.cameras.main.width / 2,
+ buttonY,
+ 400,
+ 80,
+ 0x4CAF50
+ );
+ buttonBg.setOrigin(0.5);
+ buttonBg.setDepth(999);
+ buttonBg.setInteractive({ useHandCursor: true });
+ buttonBg.setAlpha(0);
+
+ // Texte du bouton
+ const buttonText = this.add.text(
+ this.cameras.main.width / 2,
+ buttonY,
+ 'Passer au niveau 2',
+ {
+ fontSize: '32px',
+ color: '#FFFFFF',
+ fontStyle: 'bold',
+ }
+ );
+ buttonText.setOrigin(0.5);
+ buttonText.setDepth(1000);
+ buttonText.setAlpha(0);
+
+ // Animation d'apparition du bouton (un peu après le texte)
+ this.tweens.add({
+ targets: [buttonBg, buttonText],
+ alpha: 1,
+ duration: 600,
+ delay: 400,
+ ease: 'Power2',
+ });
+
+ // Effet hover sur le bouton
+ buttonBg.on('pointerover', () => {
+ buttonBg.setFillStyle(0x66BB6A);
+ this.tweens.add({
+ targets: buttonBg,
+ scaleX: 1.05,
+ scaleY: 1.05,
+ duration: 200,
+ });
+ });
+
+ buttonBg.on('pointerout', () => {
+ buttonBg.setFillStyle(0x4CAF50);
+ this.tweens.add({
+ targets: buttonBg,
+ scaleX: 1,
+ scaleY: 1,
+ duration: 200,
+ });
+ });
+
+ // Clic sur le bouton → Fin du jeu (retour au menu)
+ buttonBg.on('pointerdown', () => {
+ console.log('[EndScene] Bouton "Passer au niveau 2" cliqué → retour au menu');
+
+ // Flash blanc
+ this.cameras.main.flash(300, 255, 255, 255, true);
+
+ // Retour au menu
+ this.time.delayedCall(300, () => {
+ this.scene.start('MenuScene');
+ });
+ });
}
}
diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts
index 4b9b11c..17b0867 100644
--- a/src/scenes/GameScene.ts
+++ b/src/scenes/GameScene.ts
@@ -1,11 +1,12 @@
import Phaser from 'phaser';
-import { LEVEL_DURATION, PLAYER_STARTING_LIVES, CHEST_REQUIRED_GIFTS } from '../utils/constants';
+import { LEVEL_DURATION, PLAYER_STARTING_LIVES, CHEST_REQUIRED_GIFTS, MIN_MUSHROOMS } from '../utils/constants';
import { Player } from '../entities/Player';
import { GyroControl } from '../controls/GyroControl';
import { JumpButton } from '../controls/JumpButton';
import { DirectionalButtons } from '../controls/DirectionalButtons';
import { SuperTreasure } from '../entities/SuperTreasure';
import { TreasureChest } from '../entities/TreasureChest';
+import { Cage } from '../entities/Cage';
/**
* Scène principale du jeu
@@ -24,6 +25,8 @@ export class GameScene extends Phaser.Scene {
private gifts?: Phaser.Physics.Arcade.Group;
private superTreasures?: Phaser.Physics.Arcade.Group;
private treasureChest?: TreasureChest;
+ private cage?: Cage;
+ private hasKey: boolean = false;
private bgMusic?: Phaser.Sound.BaseSound;
private platformRects: { x: number; y: number; w: number; h: number }[] = [];
@@ -38,6 +41,11 @@ export class GameScene extends Phaser.Scene {
private giftsCollectedText?: Phaser.GameObjects.Text;
private volumeText?: Phaser.GameObjects.Text;
private sfxVolumeText?: Phaser.GameObjects.Text;
+ private debugText?: Phaser.GameObjects.Text;
+ private lastDebugUpdate: number = 0;
+ private gyroDebugButton?: Phaser.GameObjects.Text;
+ private gyroDomButton?: HTMLButtonElement;
+ private recalibrateDomButton?: HTMLButtonElement;
// Game state
private score: number = 0;
@@ -73,8 +81,8 @@ export class GameScene extends Phaser.Scene {
this.isMobile = this.sys.game.device.os.android || this.sys.game.device.os.iOS;
// Configurer les limites du monde physique (IMPORTANT pour permettre mouvement infini)
- // Étendre un peu plus pour inclure la plateforme finale et le coffre
- const levelWidth = Math.max(width * 7, 8000);
+ // Étendre encore plus pour inclure la cage du chien après le coffre
+ const levelWidth = Math.max(width * 7, 10500); // Augmenté de 2500px
this.physics.world.setBounds(0, 0, levelWidth, height);
// Créer le background qui défile
@@ -97,6 +105,9 @@ export class GameScene extends Phaser.Scene {
if (!playerBody || !platformBody) return true;
+ const isGround = (platform as any).getData?.('isGround') === true;
+ if (isGround) return true;
+
// Autoriser la collision uniquement si le joueur vient du dessus
// (sa vitesse verticale est positive = tombe, et son bas est au-dessus du haut de la plateforme)
return playerBody.velocity.y >= 0 && playerBody.bottom <= platformBody.top + 10;
@@ -111,6 +122,8 @@ export class GameScene extends Phaser.Scene {
// Contrôles PC (clavier)
this.cursors = this.input.keyboard?.createCursorKeys();
+ console.log('[GameScene] 🎮 Cursors initialized:', this.cursors ? '✅' : '❌',
+ 'Left:', !!this.cursors?.left, 'Right:', !!this.cursors?.right, 'Space:', !!this.cursors?.space);
// Contrôles Mobile (gyroscope + bouton tactile)
if (this.isMobile) {
@@ -119,6 +132,9 @@ export class GameScene extends Phaser.Scene {
// UI
this.createUI();
+ this.createDebugOverlay();
+ this.createGyroDomButton();
+ this.createRecalibrateDomButton();
// Effet neige
this.createSnow();
@@ -173,13 +189,16 @@ export class GameScene extends Phaser.Scene {
private createPlatforms(): void {
this.platforms = this.physics.add.staticGroup();
- const width = this.cameras.main.width;
const height = this.cameras.main.height;
- // Sol principal (très large pour le niveau 6x)
- const groundWidth = width * 6;
- const ground = this.add.rectangle(groundWidth / 2, height - 25, groundWidth, 50, 0x8B4513);
+ // Sol principal (très large pour tout le niveau jusqu'à la cage + 400px)
+ // Utiliser TileSprite pour répéter le motif sur toute la longueur
+ const groundWidth = 10500; // Jusqu'à x=10500 (cage à 10100 + 400px)
+
+ // Sol avec sprite répété (sol_150.png)
+ const ground = this.add.tileSprite(groundWidth / 2, height - 25, groundWidth, 50, 'ground_tile');
this.physics.add.existing(ground, true);
+ (ground as any).setData?.('isGround', true);
this.platforms.add(ground);
this.platformRects.push({ x: groundWidth / 2, y: height - 25, w: groundWidth, h: 50 });
@@ -219,13 +238,28 @@ export class GameScene extends Phaser.Scene {
{ x: 6800, y: height - 450, w: 130, h: 30 },
{ x: 7100, y: height - 350, w: 180, h: 30 },
- // Zone 6 (finale)
+ // Zone 6 (finale avant coffre)
{ x: 7400, y: height - 250, w: 200, h: 30 },
- { x: 7700, y: height - 180, w: 300, h: 30 }, // Grande plateforme finale
+ { x: 7700, y: height - 180, w: 300, h: 30 }, // Grande plateforme pour coffre
+
+ // Zone 7 (après le coffre - vers la cage du chien) +2500px
+ { x: 8000, y: height - 200, w: 180, h: 30 },
+ { x: 8300, y: height - 320, w: 160, h: 30 },
+ { x: 8600, y: height - 240, w: 200, h: 30 },
+ { x: 8900, y: height - 380, w: 140, h: 30 },
+ { x: 9200, y: height - 280, w: 180, h: 30 },
+ { x: 9500, y: height - 400, w: 150, h: 30 },
+ { x: 9800, y: height - 200, w: 220, h: 30 },
+ // Grande plateforme finale pour la cage - prolongée jusqu'à la fin (2600px de long)
+ { x: 9900, y: height - 150, w: 2600, h: 30 }
];
platformPositions.forEach((pos) => {
- const platform = this.add.rectangle(pos.x, pos.y, pos.w, pos.h, 0x6B8E23);
+ // Choisir le meilleur sprite selon la largeur de la plateforme
+ const spriteKey = this.choosePlatformSprite(pos.w);
+
+ // Créer un TileSprite qui répète automatiquement le motif
+ const platform = this.add.tileSprite(pos.x, pos.y, pos.w, pos.h, spriteKey);
this.physics.add.existing(platform, true);
this.platforms!.add(platform);
this.platformRects.push({ x: pos.x, y: pos.y, w: pos.w, h: pos.h });
@@ -234,6 +268,47 @@ export class GameScene extends Phaser.Scene {
console.log(`${platformPositions.length} plateformes créées sur ${groundWidth}px`);
}
+ /**
+ * Choisit le meilleur sprite de plateforme selon la largeur
+ * Stratégie : utiliser le sprite le plus proche de la largeur demandée
+ */
+ private choosePlatformSprite(width: number): string {
+ // Largeurs des sprites disponibles
+ const sprites = [
+ { key: 'platform_40', width: 40 },
+ { key: 'platform_73', width: 73 },
+ { key: 'platform_130', width: 130 },
+ { key: 'platform_140', width: 140 },
+ { key: 'platform_179', width: 179 }
+ ];
+
+ // Pour les très grandes plateformes (>200px), toujours utiliser le plus grand
+ if (width >= 200) {
+ return 'platform_179';
+ }
+
+ // Pour les petites/moyennes, choisir le sprite le plus proche
+ // qui divise bien la largeur totale (pour éviter les coupures bizarres)
+ let bestSprite = sprites[sprites.length - 1]; // Par défaut, le plus grand
+ let bestScore = Infinity;
+
+ for (const sprite of sprites) {
+ // Calculer combien de fois le sprite rentre dans la largeur
+ const repetitions = width / sprite.width;
+ const remainder = width % sprite.width;
+
+ // Score = reste (on préfère un reste petit) + pénalité si trop de répétitions
+ const score = remainder + Math.abs(repetitions - Math.round(repetitions)) * 10;
+
+ if (score < bestScore) {
+ bestScore = score;
+ bestSprite = sprite;
+ }
+ }
+
+ return bestSprite.key;
+ }
+
/**
* Crée les groupes d'obstacles, cadeaux et super trésors
*/
@@ -253,6 +328,37 @@ export class GameScene extends Phaser.Scene {
*/
private spawnTestObjects(): void {
const height = this.cameras.main.height;
+ const minObjectSpacing = 120;
+ const reservedPositions: Array<{ x: number; y: number; radius: number }> = [];
+ const reserve = (x: number, y: number, radius: number): void => {
+ reservedPositions.push({ x, y, radius });
+ };
+ const isTooClose = (x: number, y: number): boolean =>
+ reservedPositions.some((pos) => {
+ const distance = Phaser.Math.Distance.Between(x, y, pos.x, pos.y);
+ return distance < minObjectSpacing + pos.radius;
+ });
+ let obstacleCount = 0;
+ const targetObstacleCount = MIN_MUSHROOMS;
+ const obstacleCandidates: Array<{ x: number; y: number }> = [];
+ const addCandidate = (x: number, y: number): void => {
+ obstacleCandidates.push({ x, y });
+ };
+
+ // SUPER TRESORS (rares et precieux - 1 par zone)
+ const superTreasurePositions = [
+ { x: 1000, y: height - 350 }, // Zone 1 - en hauteur
+ { x: 2500, y: height - 420 }, // Zone 2 - tres haut
+ { x: 3900, y: height - 450 }, // Zone 3 - tres haut
+ { x: 5400, y: height - 470 }, // Zone 4 - ultra haut
+ { x: 6800, y: height - 500 }, // Zone 5 - ultra haut
+ { x: 7300, y: height - 250 }, // Zone 6 - sur plateforme finale
+ ];
+ // Coffre posé sur la plateforme à height - 180 (hauteur plateforme = 30px)
+ const chestPosition = { x: 7700, y: height - 180 - 50 }; // -50 pour être bien au-dessus de la plateforme
+
+ superTreasurePositions.forEach((pos) => reserve(pos.x, pos.y, 40));
+ reserve(chestPosition.x, chestPosition.y, 60);
// BEAUCOUP de cadeaux répartis partout (environ tous les 300-500px)
const giftPositions = [
@@ -268,6 +374,8 @@ export class GameScene extends Phaser.Scene {
5400, 5700, 6000, 6300,
// Zone 6
6600, 6900, 7200, 7500,
+ // Zone 7 (après le coffre - vers la cage)
+ 8000, 8300, 8600, 8900, 9200, 9500, 9800, 10100,
];
giftPositions.forEach((x) => {
@@ -275,9 +383,12 @@ export class GameScene extends Phaser.Scene {
const isHigh = Math.random() > 0.5;
const y = isHigh ? height - 200 - Math.random() * 150 : height - 100;
- const gift = this.add.circle(x, y, 20, 0xFFEB3B);
+ // Utiliser le sprite de cadeau (gift_40.png = 40x40px)
+ const gift = this.add.image(x, y, 'gift_sprite');
+ gift.setScale(1); // Garder la taille native (40x40)
this.physics.add.existing(gift);
this.gifts!.add(gift);
+ reserve(x, y, 25);
});
// BEAUCOUP d'obstacles répartis partout
@@ -294,6 +405,8 @@ export class GameScene extends Phaser.Scene {
5600, 5900, 6200, 6500,
// Zone 6
6800, 7100, 7400,
+ // Zone 7 (après le coffre - vers la cage) - PAS de champignons près de la cage
+ 8100, 8400, 8700, 9000,
];
// Obstacles sur plateformes (x, y)
@@ -314,48 +427,28 @@ export class GameScene extends Phaser.Scene {
const platformPlaced: Array<{ x: number; y: number }> = [];
this.platformRects
.filter((rect) => rect.y < height - 80 && rect.w >= 120) // ignorer le sol et les très petites plateformes
- .forEach((rect, idx) => {
- // Placer un champignon sur ~1 plateforme sur 2 pour ne pas surcharger
- if (idx % 2 !== 0) return;
+ .forEach((rect) => {
const x = rect.x + Phaser.Math.Between(Math.round(-rect.w / 4), Math.round(rect.w / 4));
const y = rect.y - rect.h / 2;
platformPlaced.push({ x, y });
});
- obstaclePositions.filter((_, idx) => idx % 2 === 0).forEach((x) => {
- const obstacle = this.physics.add.sprite(x, height - 50, 'obstacle_mushroom');
- obstacle.setOrigin(0.5, 1); // ancré sur les pieds
- obstacle.setImmovable(true);
- obstacle.setPushable(false);
- obstacle.setScale(0.9);
+ obstaclePositions.forEach((x) => addCandidate(x, height - 50));
- const body = obstacle.body as Phaser.Physics.Arcade.Body;
- body.setAllowGravity(false);
- body.setSize(45, 81); // hitbox 10% plus petite avec le scale
- body.setOffset((obstacle.displayWidth - 45) / 2, obstacle.displayHeight - 81);
-
- this.obstacles!.add(obstacle);
- });
-
- obstaclePlatforms.filter((_, idx) => idx % 2 === 0).forEach((pos) => {
- // Trouver la plateforme la plus proche à cet x
+ obstaclePlatforms.forEach((pos) => {
const target = this.platformRects.find((rect) => Math.abs(pos.x - rect.x) <= rect.w / 2);
const topY = target ? target.y - target.h / 2 : pos.y;
- const obstacle = this.physics.add.sprite(pos.x, topY, 'obstacle_mushroom');
- obstacle.setOrigin(0.5, 1);
- obstacle.setImmovable(true);
- obstacle.setPushable(false);
- obstacle.setScale(0.9);
-
- const body = obstacle.body as Phaser.Physics.Arcade.Body;
- body.setAllowGravity(false);
- body.setSize(45, 81);
- body.setOffset((obstacle.displayWidth - 45) / 2, obstacle.displayHeight - 81);
-
- this.obstacles!.add(obstacle);
+ addCandidate(pos.x, topY);
});
- platformPlaced.filter((_, idx) => idx % 2 === 0).forEach((pos) => {
+ platformPlaced.forEach((pos) => addCandidate(pos.x, pos.y));
+
+ Phaser.Utils.Array.Shuffle(obstacleCandidates);
+
+ obstacleCandidates.forEach((pos) => {
+ if (obstacleCount >= targetObstacleCount) return;
+ if (isTooClose(pos.x, pos.y)) return;
+
const obstacle = this.physics.add.sprite(pos.x, pos.y, 'obstacle_mushroom');
obstacle.setOrigin(0.5, 1);
obstacle.setImmovable(true);
@@ -368,17 +461,16 @@ export class GameScene extends Phaser.Scene {
body.setOffset((obstacle.displayWidth - 45) / 2, obstacle.displayHeight - 81);
this.obstacles!.add(obstacle);
+ obstacleCount += 1;
+ reserve(pos.x, pos.y, 50);
});
- // SUPER TRÉSORS (rares et précieux - 1 par zone)
- const superTreasurePositions = [
- { x: 1000, y: height - 350 }, // Zone 1 - en hauteur
- { x: 2500, y: height - 420 }, // Zone 2 - très haut
- { x: 3900, y: height - 450 }, // Zone 3 - très haut
- { x: 5400, y: height - 470 }, // Zone 4 - ultra haut
- { x: 6800, y: height - 500 }, // Zone 5 - ultra haut
- { x: 7300, y: height - 250 }, // Zone 6 - sur plateforme finale
- ];
+ if (obstacleCount < targetObstacleCount) {
+ console.warn(
+ `⚠️ Champignons insuffisants: ${obstacleCount}/${targetObstacleCount}. ` +
+ 'Augmentez les positions candidates ou reduisez le minObjectSpacing.'
+ );
+ }
superTreasurePositions.forEach((pos) => {
const superTreasure = new SuperTreasure(this, pos.x, pos.y);
@@ -386,10 +478,32 @@ export class GameScene extends Phaser.Scene {
});
// COFFRE FINAL au bout du niveau
- this.treasureChest = new TreasureChest(this, 7700, height - 300, CHEST_REQUIRED_GIFTS);
+ this.treasureChest = new TreasureChest(this, chestPosition.x, chestPosition.y, CHEST_REQUIRED_GIFTS);
this.physics.add.overlap(this.player!, this.treasureChest, this.openChest, undefined, this);
- console.log(`${giftPositions.length} cadeaux, ${obstaclePositions.length} obstacles, ${superTreasurePositions.length} SUPER TRÉSORS et 1 COFFRE FINAL créés`);
+ // CAGE avec le chien à la toute fin
+ this.cage = new Cage(this, 10100, height - 150);
+
+ console.log(`${giftPositions.length} cadeaux, ${obstaclePositions.length} obstacles, ${superTreasurePositions.length} SUPER TRÉSORS, 1 COFFRE FINAL et 1 CAGE créés`);
+ }
+
+ /**
+ * Débloque l'audio sur iOS (requis après interaction utilisateur)
+ */
+ private unlockAudio(): void {
+ // Sur iOS, l'audio doit être activé après une interaction utilisateur
+ const soundManager = this.sound as Phaser.Sound.WebAudioSoundManager;
+
+ if (soundManager.context) {
+ // Si le contexte est suspendu, le reprendre
+ if (soundManager.context.state === 'suspended') {
+ soundManager.context.resume().then(() => {
+ console.log('[GameScene Audio] ✅ Context audio débloqué pour iOS');
+ }).catch((error: any) => {
+ console.error('[GameScene Audio] ❌ Erreur déblocage audio:', error);
+ });
+ }
+ }
}
/**
@@ -401,6 +515,8 @@ export class GameScene extends Phaser.Scene {
// Bouton de saut
this.jumpButton = new JumpButton(this, () => {
+ // IMPORTANT: Débloquer l'audio sur iOS lors de l'interaction
+ this.unlockAudio();
this.player?.jump();
});
@@ -485,6 +601,8 @@ export class GameScene extends Phaser.Scene {
backButton.setDepth(100);
backButton.setInteractive({ useHandCursor: true });
backButton.on('pointerdown', () => {
+ // IMPORTANT: Débloquer l'audio sur iOS
+ this.unlockAudio();
this.cleanup();
this.scene.start('MenuScene');
});
@@ -501,6 +619,8 @@ export class GameScene extends Phaser.Scene {
this.volumeText.setDepth(100);
this.volumeText.setInteractive({ useHandCursor: true });
this.volumeText.on('pointerdown', () => {
+ // IMPORTANT: Débloquer l'audio sur iOS
+ this.unlockAudio();
this.cycleMusicVolume();
});
@@ -516,10 +636,247 @@ export class GameScene extends Phaser.Scene {
this.sfxVolumeText.setDepth(100);
this.sfxVolumeText.setInteractive({ useHandCursor: true });
this.sfxVolumeText.on('pointerdown', () => {
+ // IMPORTANT: Débloquer l'audio sur iOS
+ this.unlockAudio();
this.cycleSfxVolume();
});
}
+ private createDebugOverlay(): void {
+ if (!this.isMobile) return;
+
+ this.debugText = this.add.text(10, 130, '', {
+ fontSize: '12px',
+ color: '#ffffff',
+ backgroundColor: '#000000',
+ padding: { x: 6, y: 6 },
+ });
+ this.debugText.setOrigin(0, 0);
+ this.debugText.setScrollFactor(0);
+ this.debugText.setDepth(200);
+
+ this.gyroDebugButton = this.add.text(10, 70, 'Autoriser gyroscope', {
+ fontSize: '12px',
+ color: '#00ff00',
+ backgroundColor: '#000000',
+ padding: { x: 6, y: 4 },
+ });
+ this.gyroDebugButton.setOrigin(0, 0);
+ this.gyroDebugButton.setScrollFactor(0);
+ this.gyroDebugButton.setDepth(200);
+ this.gyroDebugButton.setInteractive({ useHandCursor: true });
+ this.gyroDebugButton.on('pointerdown', () => {
+ // IMPORTANT: Débloquer l'audio sur iOS
+ this.unlockAudio();
+ this.requestGyroPermission();
+ });
+ }
+
+ private createGyroDomButton(): void {
+ if (!this.isMobile || this.gyroDomButton) return;
+
+ const button = document.createElement('button');
+ button.textContent = 'Autoriser gyroscope';
+ button.style.position = 'fixed';
+ button.style.right = '8px';
+ button.style.top = '8px';
+ button.style.zIndex = '9999';
+ button.style.padding = '6px 10px';
+ button.style.fontSize = '12px';
+ button.style.background = '#000000';
+ button.style.color = '#00ff00';
+ button.style.border = '1px solid #00ff00';
+ button.style.borderRadius = '4px';
+ button.style.cursor = 'pointer';
+ button.style.opacity = '0.9';
+ button.addEventListener('click', () => {
+ this.requestGyroPermission();
+ });
+
+ document.body.appendChild(button);
+ this.gyroDomButton = button;
+
+ this.events.once('shutdown', () => {
+ this.removeGyroDomButton();
+ });
+ this.events.once('destroy', () => {
+ this.removeGyroDomButton();
+ });
+ }
+
+ private removeGyroDomButton(): void {
+ if (!this.gyroDomButton) return;
+ this.gyroDomButton.remove();
+ this.gyroDomButton = undefined;
+ }
+
+ private createRecalibrateDomButton(): void {
+ if (!this.isMobile || this.recalibrateDomButton) return;
+
+ // Détecter iOS
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
+ (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
+
+ // Bouton de recalibration uniquement sur iOS
+ if (!isIOS) return;
+
+ const button = document.createElement('button');
+ button.textContent = '🔄 Recalibrer';
+ button.style.position = 'fixed';
+ button.style.right = '8px';
+ button.style.top = '48px'; // En dessous du bouton gyroscope
+ button.style.zIndex = '9999';
+ button.style.padding = '8px 12px';
+ button.style.fontSize = '14px';
+ button.style.background = '#FF9500';
+ button.style.color = '#ffffff';
+ button.style.border = '2px solid #ffffff';
+ button.style.borderRadius = '6px';
+ button.style.cursor = 'pointer';
+ button.style.opacity = '0.95';
+ button.style.fontWeight = 'bold';
+ button.addEventListener('click', () => {
+ this.recalibrateGyroscope();
+ });
+
+ document.body.appendChild(button);
+ this.recalibrateDomButton = button;
+
+ this.events.once('shutdown', () => {
+ this.removeRecalibrateDomButton();
+ });
+ this.events.once('destroy', () => {
+ this.removeRecalibrateDomButton();
+ });
+ }
+
+ private removeRecalibrateDomButton(): void {
+ if (!this.recalibrateDomButton) return;
+ this.recalibrateDomButton.remove();
+ this.recalibrateDomButton = undefined;
+ }
+
+ private recalibrateGyroscope(): void {
+ if (!this.gyroControl) return;
+
+ // Afficher un message temporaire
+ const message = document.createElement('div');
+ message.textContent = '📱 Maintenez le téléphone en position neutre...';
+ message.style.position = 'fixed';
+ message.style.top = '50%';
+ message.style.left = '50%';
+ message.style.transform = 'translate(-50%, -50%)';
+ message.style.zIndex = '10000';
+ message.style.padding = '20px 30px';
+ message.style.fontSize = '18px';
+ message.style.background = 'rgba(0, 0, 0, 0.9)';
+ message.style.color = '#ffffff';
+ message.style.border = '3px solid #FF9500';
+ message.style.borderRadius = '12px';
+ message.style.fontWeight = 'bold';
+ message.style.textAlign = 'center';
+ document.body.appendChild(message);
+
+ // Recalibrer après un court délai (laisser le temps à l'utilisateur de voir le message)
+ setTimeout(() => {
+ this.gyroControl?.calibrate();
+ message.textContent = '✅ Calibration en cours...';
+
+ // Retirer le message après 3 secondes (temps de calibration: 20 échantillons)
+ setTimeout(() => {
+ message.remove();
+ }, 3000);
+ }, 1000);
+ }
+
+ private showInitialCalibrationMessage(): void {
+ const message = document.createElement('div');
+ message.innerHTML = '📱 Calibration automatique
Maintenez le téléphone en position neutre pendant 2 secondes...
Utilisez le bouton "🔄 Recalibrer" pour réinitialiser si besoin';
+ 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 {
+ const hasDeviceOrientation =
+ typeof window.DeviceOrientationEvent !== 'undefined';
+ const requestOrientationPermission =
+ (window.DeviceOrientationEvent as any)?.requestPermission;
+ const requestMotionPermission =
+ (window.DeviceMotionEvent as any)?.requestPermission;
+ const isIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
+ const userActivation = (navigator as any).userActivation;
+
+ if (isIOS && !window.isSecureContext) {
+ this.registry.set('gyroPermission', 'blocked');
+ return;
+ }
+
+ if (hasDeviceOrientation && typeof requestOrientationPermission === 'function') {
+ try {
+ const orientationStatus = await requestOrientationPermission.call(
+ window.DeviceOrientationEvent
+ );
+ let motionStatus = 'granted';
+ if (typeof requestMotionPermission === 'function') {
+ motionStatus = await requestMotionPermission.call(
+ window.DeviceMotionEvent
+ );
+ }
+
+ if (orientationStatus === 'granted' && motionStatus === 'granted') {
+ this.registry.set('gyroPermission', 'granted');
+ this.removeGyroDomButton();
+
+ // Afficher un message de calibration pour iOS
+ if (isIOS) {
+ this.showInitialCalibrationMessage();
+ }
+ return;
+ }
+
+ const permState =
+ orientationStatus === 'granted' || motionStatus === 'granted'
+ ? 'partial'
+ : 'denied';
+ this.registry.set('gyroPermission', permState);
+ this.registry.set(
+ 'gyroPermissionError',
+ `activation ${userActivation?.isActive}/${userActivation?.hasBeenActive}`
+ );
+ return;
+ } catch (error) {
+ const errorText =
+ error instanceof Error ? error.message : String(error);
+ this.registry.set('gyroPermission', 'error');
+ this.registry.set(
+ 'gyroPermissionError',
+ `${errorText} (activation ${userActivation?.isActive}/${userActivation?.hasBeenActive})`
+ );
+ return;
+ }
+ }
+
+ this.registry.set('gyroPermission', 'not-required');
+ }
+
update(time: number): void {
if (!this.player) return;
@@ -529,36 +886,85 @@ export class GameScene extends Phaser.Scene {
// Gestion des contrôles
let direction = 0;
- // PC : Clavier
- if (this.cursors) {
+ // PC/Laptop : Clavier (priorité absolue)
+ if (!this.isMobile && this.cursors) {
if (this.cursors.left.isDown) {
direction = -1;
+ if (time % 1000 < 16) console.log('⬅️ Left pressed');
} else if (this.cursors.right.isDown) {
direction = 1;
+ if (time % 1000 < 16) console.log('➡️ Right pressed');
}
// Saut avec Espace
if (Phaser.Input.Keyboard.JustDown(this.cursors.space!)) {
+ console.log('🚀 Space pressed - Jump!');
this.player.jump();
}
// Saut avec flèche haut (alternative)
if (Phaser.Input.Keyboard.JustDown(this.cursors.up!)) {
+ console.log('⬆️ Up pressed - Jump!');
this.player.jump();
}
+ } else if (!this.isMobile) {
+ // Debug: pourquoi le clavier ne fonctionne pas?
+ if (time % 2000 < 16) {
+ console.log('[GameScene] ⚠️ Keyboard disabled: isMobile=', this.isMobile, 'cursors=', !!this.cursors);
+ }
}
- // Mobile : boutons directionnels priment, sinon gyroscope
+ // Mobile/Tablette : boutons directionnels priment, sinon gyroscope
if (this.isMobile) {
+ // Vérifier d'abord les boutons directionnels (priorité)
const dirButtons = this.directionalButtons?.getDirection() ?? 0;
if (dirButtons !== 0) {
direction = dirButtons;
} else if (this.gyroControl) {
+ // Sinon utiliser le gyroscope
const tiltValue = this.gyroControl.getTiltValue();
direction = tiltValue; // -1 à 1
}
}
+ // Debug overlay (throttled)
+ if (this.debugText && this.gyroControl && time - this.lastDebugUpdate > 500) {
+ const info = this.gyroControl.getDebugInfo();
+ const now = Date.now();
+ const lastEventMs =
+ info.lastEventAt > 0 ? Math.round(now - info.lastEventAt) : -1;
+ const permState = this.registry.get('gyroPermission') || 'unknown';
+ const permError = this.registry.get('gyroPermissionError') || '';
+
+ // Déterminer quel axe est utilisé actuellement
+ const axisUsed = info.useAlpha ? 'ALPHA' : (info.useGamma ? 'GAMMA' : 'BETA');
+
+ this.debugText.setText(
+ [
+ `perm: ${permState}`,
+ permError ? `err: ${permError}` : '',
+ `platform: ${info.platform}`,
+ `gyro active: ${info.active}`,
+ `--- AXES (raw values) ---`,
+ `ALPHA: ${info.alpha.toFixed(1)}° (boussole/Z)`,
+ `BETA: ${info.beta.toFixed(1)}° (avant/arrière/X)`,
+ `GAMMA: ${info.gamma.toFixed(1)}° (gauche/droite/Y)`,
+ `--- CONTROL ---`,
+ `Axe utilisé: ${axisUsed}`,
+ `tilt normalized: ${info.tilt.toFixed(2)}`,
+ `orientation: ${info.angle}°`,
+ `last event: ${lastEventMs}ms`,
+ ]
+ .filter((line) => line !== '')
+ .join('\n')
+ );
+ this.lastDebugUpdate = time;
+ }
+
+ if (this.gyroDomButton && this.registry.get('gyroPermission') === 'granted') {
+ this.removeGyroDomButton();
+ }
+
// Déplacer le joueur
this.player.move(direction);
this.player.update();
@@ -577,6 +983,43 @@ export class GameScene extends Phaser.Scene {
if (this.background) {
this.background.tilePositionX = this.cameras.main.scrollX * 0.3;
}
+
+ // Vérifier la proximité avec le coffre pour afficher le message si pas assez de cadeaux
+ if (this.treasureChest && !this.treasureChest.getIsOpen()) {
+ const distanceToChest = Phaser.Math.Distance.Between(
+ this.player.x,
+ this.player.y,
+ 7700, // Position X du coffre
+ this.cameras.main.height - 180 // Position Y du coffre
+ );
+
+ // Si proche du coffre (moins de 150px) et pas assez de cadeaux, afficher le message
+ if (distanceToChest < 150 && this.giftsCollected < CHEST_REQUIRED_GIFTS) {
+ this.treasureChest.showRequirementText();
+ } else {
+ this.treasureChest.hideRequirementText();
+ }
+ }
+
+ // Mise à jour du chien dans la cage
+ if (this.cage) {
+ this.cage.update();
+
+ // Vérifier si le joueur est près de la cage avec la clé
+ if (this.hasKey && !this.cage.isOpened()) {
+ const distance = Phaser.Math.Distance.Between(
+ this.player.x,
+ this.player.y,
+ 10100, // Position X de la cage
+ this.cameras.main.height - 150 // Position Y de la cage
+ );
+
+ // Si le joueur est assez proche (moins de 100px)
+ if (distance < 100) {
+ this.openCageAndCompleteLevel();
+ }
+ }
+ }
}
/**
@@ -699,17 +1142,22 @@ export class GameScene extends Phaser.Scene {
this.addScore(500);
this.playSfx('sfx_super', 0.6);
+ // BONUS DE TEMPS : +5 secondes
+ this.timeRemaining += 5;
+ console.log('⏱️ Super Trésor : +5 secondes ! Temps restant:', this.timeRemaining);
+
// Message spécial
const bonusText = this.add.text(
this.cameras.main.scrollX + this.cameras.main.width / 2,
this.cameras.main.height / 2,
- '★ SUPER TRÉSOR +500 ★',
+ '★ SUPER TRÉSOR +500 ★\n⏱️ +5 SECONDES ⏱️',
{
fontSize: '48px',
color: '#FFD700',
stroke: '#FF8C00',
strokeThickness: 6,
fontStyle: 'bold',
+ align: 'center',
}
);
bonusText.setOrigin(0.5);
@@ -733,17 +1181,23 @@ export class GameScene extends Phaser.Scene {
}
/**
- * Ouvre le coffre au trésor final
+ * Ouvre le coffre au trésor final et donne la clé
*/
private openChest(_player: any, chest: any): void {
if (chest.canOpen(this.giftsCollected)) {
- const bonus = chest.open(this);
- this.addScore(bonus);
+ const result = chest.open(this);
+ this.addScore(result.bonus);
- // VICTOIRE ! Lancer l'animation de fin
- this.time.delayedCall(2000, () => {
- this.levelComplete();
- });
+ // Donner la clé au joueur avec animation
+ if (result.hasKey) {
+ this.hasKey = true;
+ console.log('🗝️ Clé obtenue ! Direction la cage du chien !');
+
+ // Créer une clé animée qui vole vers le joueur
+ this.createKeyPickupAnimation();
+ }
+
+ // Ne PAS terminer le niveau ici - continuer vers la cage
} else if (!chest.getIsOpen()) {
// Pas assez de cadeaux
const remaining = chest.getRequiredGifts() - this.giftsCollected;
@@ -772,6 +1226,139 @@ export class GameScene extends Phaser.Scene {
}
}
+ /**
+ * Animation de récupération de la clé
+ */
+ private createKeyPickupAnimation(): void {
+ // Créer une grosse icône de clé au centre de l'écran
+ const keyIcon = this.add.text(
+ this.cameras.main.scrollX + this.cameras.main.width / 2,
+ this.cameras.main.height / 2,
+ '🗝️',
+ {
+ fontSize: '120px',
+ }
+ );
+ keyIcon.setOrigin(0.5);
+ keyIcon.setScrollFactor(0);
+ keyIcon.setDepth(10000); // Augmenté pour être au-dessus de tout (neige = 2000)
+ keyIcon.setAlpha(0);
+ keyIcon.setScale(0.1);
+
+ // Message "Clé obtenue"
+ const keyText = this.add.text(
+ this.cameras.main.scrollX + this.cameras.main.width / 2,
+ this.cameras.main.height / 2 + 100,
+ '🗝️ CLÉ OBTENUE ! 🗝️\nDirection : Cage du chien →',
+ {
+ fontSize: '40px', // Augmenté pour être plus visible
+ color: '#FFD700',
+ stroke: '#000000',
+ strokeThickness: 8, // Augmenté pour meilleure visibilité
+ fontStyle: 'bold',
+ align: 'center',
+ }
+ );
+ keyText.setOrigin(0.5);
+ keyText.setScrollFactor(0);
+ keyText.setDepth(10000); // Augmenté pour être au-dessus de tout (neige = 2000)
+ keyText.setAlpha(0);
+
+ // Animation de la clé : apparition avec rotation
+ this.tweens.add({
+ targets: keyIcon,
+ scale: 1.5,
+ alpha: 1,
+ angle: 360,
+ duration: 800,
+ ease: 'Back.easeOut',
+ });
+
+ // Animation du texte : apparition
+ this.tweens.add({
+ targets: keyText,
+ alpha: 1,
+ duration: 600,
+ delay: 400,
+ ease: 'Power2',
+ });
+
+ // Après 2 secondes, faire disparaître tout
+ this.time.delayedCall(2500, () => {
+ this.tweens.add({
+ targets: [keyIcon, keyText],
+ alpha: 0,
+ scale: 0.5,
+ duration: 500,
+ ease: 'Power2',
+ onComplete: () => {
+ keyIcon.destroy();
+ keyText.destroy();
+ },
+ });
+ });
+
+ // Flash doré pour attirer l'attention
+ this.cameras.main.flash(300, 255, 215, 0, true);
+
+ // Son (si disponible)
+ this.playSfx('sfx_powerup', 0.7);
+ }
+
+ /**
+ * Ouvre la cage avec la clé et termine le niveau
+ */
+ private openCageAndCompleteLevel(): void {
+ if (!this.cage || this.cage.isOpened()) return;
+
+ console.log('🗝️ Ouverture de la cage avec la clé !');
+
+ // Ouvrir la cage
+ this.cage.open();
+
+ // Jouer la musique du chien libéré
+ this.playSfx('sfx_chien', 0.7);
+
+ // Message de libération
+ const liberationText = this.add.text(
+ this.cameras.main.scrollX + this.cameras.main.width / 2,
+ this.cameras.main.height / 2 - 100,
+ '🐕 CHIEN LIBÉRÉ ! 🐕\n🎉 NIVEAU TERMINÉ ! 🎉',
+ {
+ fontSize: '48px',
+ color: '#FFD700',
+ stroke: '#FF4500',
+ strokeThickness: 8,
+ fontStyle: 'bold',
+ align: 'center',
+ }
+ );
+ liberationText.setOrigin(0.5);
+ liberationText.setScrollFactor(0);
+ liberationText.setDepth(2000);
+
+ // Flash de victoire
+ this.cameras.main.flash(500, 255, 215, 0, true);
+
+ // Animation du texte
+ this.tweens.add({
+ targets: liberationText,
+ scaleX: 1.3,
+ scaleY: 1.3,
+ alpha: 0,
+ duration: 3000,
+ ease: 'Power2',
+ onComplete: () => {
+ liberationText.destroy();
+ },
+ });
+
+ // Après 3 secondes, terminer le niveau
+ this.time.delayedCall(3000, () => {
+ this.levelComplete();
+ });
+ }
+
/**
* Animation de victoire - Niveau terminé !
*/
diff --git a/src/scenes/IntroVideoScene.ts b/src/scenes/IntroVideoScene.ts
new file mode 100644
index 0000000..3b91669
--- /dev/null
+++ b/src/scenes/IntroVideoScene.ts
@@ -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');
+ }
+}
diff --git a/src/scenes/MenuScene.ts b/src/scenes/MenuScene.ts
index 3dc51ac..c1d47e0 100644
--- a/src/scenes/MenuScene.ts
+++ b/src/scenes/MenuScene.ts
@@ -7,6 +7,10 @@ export class MenuScene extends Phaser.Scene {
private startButton?: Phaser.GameObjects.Text;
private gyroStatus?: Phaser.GameObjects.Text;
private hasStarted: boolean = false;
+ private debugText?: Phaser.GameObjects.Text;
+ private debugLines: string[] = [];
+ private hasGyroDebugListener: boolean = false;
+ private isRequestingPermission: boolean = false;
constructor() {
super({ key: 'MenuScene' });
@@ -18,6 +22,8 @@ export class MenuScene extends Phaser.Scene {
// Réinitialiser le flag à chaque création du menu
this.hasStarted = false;
+ this.registry.set('gyroPermission', 'unknown');
+ this.registry.set('gyroPermissionError', '');
// Titre
const title = this.add.text(width / 2, height / 3, 'MARIO RUNNER', {
@@ -64,8 +70,28 @@ export class MenuScene extends Phaser.Scene {
});
this.gyroStatus.setOrigin(0.5);
- // Click sur le bouton
+ // Debug gyroscope (logs a l'ecran)
+ this.debugText = this.add.text(10, height - 110, '', {
+ fontSize: '12px',
+ color: '#ffffff',
+ backgroundColor: '#000000',
+ padding: { x: 6, y: 6 },
+ wordWrap: { width: width - 20 },
+ });
+ this.debugText.setOrigin(0, 0);
+ this.debugText.setScrollFactor(0);
+
+ // Demande iOS dès le premier tap (exigé par la permission).
+ this.input.once('pointerdown', () => {
+ // IMPORTANT: Débloquer l'audio sur iOS
+ this.unlockAudio();
+ this.requestGyroPermission();
+ });
+
+ // Click sur le bouton (fallback si le tap global a déjà eu lieu).
this.startButton.on('pointerdown', () => {
+ // IMPORTANT: Débloquer l'audio sur iOS
+ this.unlockAudio();
this.requestGyroPermission();
});
@@ -79,13 +105,168 @@ export class MenuScene extends Phaser.Scene {
});
}
+ /**
+ * Débloque l'audio sur iOS (requis après interaction utilisateur)
+ */
+ private unlockAudio(): void {
+ // Sur iOS, l'audio doit être activé après une interaction utilisateur
+ const soundManager = this.sound as Phaser.Sound.WebAudioSoundManager;
+
+ if (soundManager.context) {
+ console.log('[Audio] Context state:', soundManager.context.state);
+
+ // Si le contexte est suspendu, le reprendre
+ if (soundManager.context.state === 'suspended') {
+ soundManager.context.resume().then(() => {
+ console.log('[Audio] ✅ Context audio débloqué pour iOS');
+ }).catch((error: any) => {
+ console.error('[Audio] ❌ Erreur déblocage audio:', error);
+ });
+ }
+ }
+
+ // Jouer un son silencieux pour initialiser l'audio sur mobile
+ // Cela garantit que les sons futurs fonctionneront
+ try {
+ const silentSound = this.sound.add('__silent__', { volume: 0 });
+ silentSound.play();
+ silentSound.destroy();
+ } catch (error: any) {
+ console.log('[Audio] Pas de son silencieux (normal)');
+ }
+ }
+
/**
* Demande la permission gyroscope (iOS) et lance le jeu
*/
- private requestGyroPermission(): void {
- // Simplifié : on part immédiatement en jeu, sans attendre la permission.
- this.gyroStatus?.setText('Lancement du jeu...');
+ private async requestGyroPermission(): Promise {
+ if (this.isRequestingPermission) {
+ this.logDebug('permission request in progress');
+ return;
+ }
+ this.isRequestingPermission = true;
+
+ try {
+ console.log('[Gyro] request permission');
+ console.log('[Gyro] userAgent:', navigator.userAgent);
+ console.log('[Gyro] isSecureContext:', window.isSecureContext);
+ this.gyroStatus?.setText("Demande d'autorisation du gyroscope...");
+
+ const hasDeviceOrientation =
+ typeof window.DeviceOrientationEvent !== 'undefined';
+ const requestOrientationPermission =
+ (window.DeviceOrientationEvent as any)?.requestPermission;
+ const requestMotionPermission =
+ (window.DeviceMotionEvent as any)?.requestPermission;
+ const isIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
+
+ const userActivation = (navigator as any).userActivation;
+ this.logDebug(`ua: ${navigator.userAgent}`);
+ this.logDebug(`secure: ${window.isSecureContext}`);
+ this.logDebug(`iOS: ${isIOS}`);
+ this.logDebug(`DeviceOrientationEvent: ${hasDeviceOrientation}`);
+ this.logDebug(
+ `requestPermission: ${typeof requestOrientationPermission === 'function'}`
+ );
+ this.logDebug(
+ `activation: ${userActivation?.isActive}/${userActivation?.hasBeenActive}`
+ );
+ this.logDebug(`top-level: ${window.top === window.self}`);
+ this.logDebug(`visible: ${document.visibilityState}`);
+ this.setupGyroDebugListener();
+
+ if (isIOS && !window.isSecureContext) {
+ this.gyroStatus?.setText(
+ 'Le gyroscope iOS requiert HTTPS. Ouvrez le jeu en https.'
+ );
+ this.logDebug('blocked: not https');
+ this.registry.set('gyroPermission', 'blocked');
+ return;
+ }
+
+ // iOS 13+ requires explicit permission from a user gesture.
+ if (hasDeviceOrientation && typeof requestOrientationPermission === 'function') {
+ try {
+ const orientationStatus = await requestOrientationPermission.call(
+ window.DeviceOrientationEvent
+ );
+ let motionStatus = 'granted';
+ if (typeof requestMotionPermission === 'function') {
+ motionStatus = await requestMotionPermission.call(
+ window.DeviceMotionEvent
+ );
+ }
+
+ console.log('[Gyro] permission orientation:', orientationStatus);
+ console.log('[Gyro] permission motion:', motionStatus);
+ this.logDebug(`perm orientation: ${orientationStatus}`);
+ this.logDebug(`perm motion: ${motionStatus}`);
+
+ if (orientationStatus === 'granted' && motionStatus === 'granted') {
+ this.registry.set('gyroPermission', 'granted');
+ this.gyroStatus?.setText('Gyroscope activé. Lancement...');
+ this.startGame();
+ return;
+ }
+
+ this.gyroStatus?.setText(
+ 'Acces au gyroscope refuse. Activez-le dans les reglages iOS.'
+ );
+ const permState =
+ orientationStatus === 'granted' || motionStatus === 'granted'
+ ? 'partial'
+ : 'denied';
+ this.registry.set('gyroPermission', permState);
+ this.logDebug('permission refused');
+ this.logDebug('starting without gyro');
+ this.startGame();
+ return;
+ } catch (error) {
+ console.warn('[MenuScene] Permission gyroscope refusée', error);
+ this.gyroStatus?.setText(
+ "Impossible d'activer le gyroscope. Reessayez."
+ );
+ const errorText =
+ error instanceof Error ? error.message : String(error);
+ this.logDebug(`permission error: ${errorText}`);
+ this.registry.set('gyroPermission', 'error');
+ this.registry.set('gyroPermissionError', errorText);
+ this.logDebug('starting without gyro');
+ this.startGame();
+ return;
+ }
+ }
+
+ // Android/desktop or older iOS: no permission API.
+ this.registry.set('gyroPermission', 'not-required');
+ this.gyroStatus?.setText('Gyroscope prêt. Lancement...');
this.startGame();
+ } finally {
+ this.isRequestingPermission = false;
+ }
+ }
+
+ private logDebug(line: string): void {
+ this.debugLines.push(line);
+ if (this.debugLines.length > 6) {
+ this.debugLines.shift();
+ }
+ this.debugText?.setText(this.debugLines.join('\n'));
+ }
+
+ private setupGyroDebugListener(): void {
+ if (this.hasGyroDebugListener) return;
+ this.hasGyroDebugListener = true;
+
+ let lastLogTime = 0;
+ window.addEventListener('deviceorientation', (event) => {
+ const now = Date.now();
+ if (now - lastLogTime < 1000) return;
+ lastLogTime = now;
+ const beta = event.beta ?? 0;
+ const gamma = event.gamma ?? 0;
+ this.logDebug(`event beta:${beta.toFixed(1)} gamma:${gamma.toFixed(1)}`);
+ });
}
/**
diff --git a/src/scenes/PlayerSelectScene.ts b/src/scenes/PlayerSelectScene.ts
new file mode 100644
index 0000000..6442f10
--- /dev/null
+++ b/src/scenes/PlayerSelectScene.ts
@@ -0,0 +1,168 @@
+import Phaser from 'phaser';
+
+/**
+ * Scene de selection joueur (fonctionne en portrait et paysage).
+ */
+export class PlayerSelectScene extends Phaser.Scene {
+ private selectContainer?: Phaser.GameObjects.Container;
+ private titleText?: Phaser.GameObjects.Text;
+ private user1Image?: Phaser.GameObjects.Image;
+ private user2Image?: Phaser.GameObjects.Image;
+ private user1Label?: Phaser.GameObjects.Text;
+ private user2Label?: Phaser.GameObjects.Text;
+ private handleResizeBound?: () => void;
+ private isMobile: boolean = false;
+
+ constructor() {
+ super({ key: 'PlayerSelectScene' });
+ }
+
+ create(): void {
+ this.isMobile = this.sys.game.device.os.android || this.sys.game.device.os.iOS;
+
+ this.cameras.main.setBackgroundColor('#87CEEB');
+
+ this.createSelectContainer();
+ this.updateOrientation();
+
+ this.handleResizeBound = () => this.updateOrientation();
+ this.scale.on('resize', this.updateOrientation, this);
+ if (this.isMobile) {
+ window.addEventListener('orientationchange', this.handleResizeBound);
+ }
+
+ this.events.once('shutdown', () => {
+ this.scale.off('resize', this.updateOrientation, this);
+ if (this.handleResizeBound) {
+ window.removeEventListener('orientationchange', this.handleResizeBound);
+ }
+ });
+ }
+
+ private createSelectContainer(): void {
+ const title = this.add.text(0, 0, 'Selectionnez votre joueur', {
+ fontSize: '28px',
+ color: '#333333',
+ align: 'center',
+ fontFamily: 'Arial',
+ });
+ title.setOrigin(0.5);
+ this.titleText = title;
+
+ const user1 = this.add.image(0, 0, 'user1');
+ const user2 = this.add.image(0, 0, 'user2');
+ this.user1Image = user1;
+ this.user2Image = user2;
+
+ this.fitImage(user1, 260, 260);
+ this.fitImage(user2, 260, 260);
+
+ user1.setInteractive({ useHandCursor: true });
+ user2.setInteractive({ useHandCursor: true });
+
+ const label1 = this.add.text(0, 0, 'Baptiste', {
+ fontSize: '24px',
+ color: '#333333',
+ fontFamily: 'Arial',
+ fontStyle: 'bold',
+ });
+ label1.setOrigin(0.5);
+ const label2 = this.add.text(0, 0, 'Julien', {
+ fontSize: '24px',
+ color: '#333333',
+ fontFamily: 'Arial',
+ fontStyle: 'bold',
+ });
+ label2.setOrigin(0.5);
+ this.user1Label = label1;
+ this.user2Label = label2;
+
+ user1.on('pointerdown', () => this.selectPlayer('user1'));
+ user2.on('pointerdown', () => this.selectPlayer('user2'));
+
+ this.selectContainer = this.add.container(0, 0, [title, user1, user2, label1, label2]);
+ }
+
+ private fitImage(image: Phaser.GameObjects.Image, maxW: number, maxH: number): void {
+ const scale = Math.min(maxW / image.width, maxH / image.height);
+ image.setScale(scale);
+ }
+
+ private updateOrientation(): void {
+ const { width, height } = this.scale;
+ const isPortrait = this.getIsPortrait();
+
+ // Afficher la sélection en portrait ou paysage
+ if (this.selectContainer) {
+ this.selectContainer.setVisible(true);
+ if (isPortrait) {
+ this.layoutSelectContainerPortrait(width, height);
+ } else {
+ this.layoutSelectContainerLandscape(width, height);
+ }
+ }
+ }
+
+ private getIsPortrait(): boolean {
+ if (window.matchMedia) {
+ return window.matchMedia('(orientation: portrait)').matches;
+ }
+ return window.innerHeight >= window.innerWidth;
+ }
+
+ private layoutSelectContainerPortrait(width: number, height: number): void {
+ if (!this.titleText || !this.user1Image || !this.user2Image || !this.user1Label || !this.user2Label) {
+ return;
+ }
+
+ const titleY = Math.max(50, height * 0.08);
+ this.titleText.setPosition(width / 2, titleY);
+
+ // Agrandir les images au maximum: 90% de la largeur, 40% de la hauteur par image
+ const imageMaxW = Math.min(width * 0.90, 550);
+ const imageMaxH = Math.min(height * 0.40, 500);
+ this.fitImage(this.user1Image, imageMaxW, imageMaxH);
+ this.fitImage(this.user2Image, imageMaxW, imageMaxH);
+
+ const firstImageY = titleY + 60 + this.user1Image.displayHeight / 2;
+ const secondImageY =
+ firstImageY + this.user1Image.displayHeight / 2 + 40 + this.user2Image.displayHeight / 2;
+
+ this.user1Image.setPosition(width / 2, firstImageY);
+ this.user1Label.setPosition(width / 2, firstImageY + this.user1Image.displayHeight / 2 + 20);
+
+ this.user2Image.setPosition(width / 2, secondImageY);
+ this.user2Label.setPosition(width / 2, secondImageY + this.user2Image.displayHeight / 2 + 20);
+ }
+
+ private layoutSelectContainerLandscape(width: number, height: number): void {
+ if (!this.titleText || !this.user1Image || !this.user2Image || !this.user1Label || !this.user2Label) {
+ return;
+ }
+
+ // Titre en haut centré
+ const titleY = Math.max(40, height * 0.12);
+ this.titleText.setPosition(width / 2, titleY);
+
+ // Images côte à côte - agrandir encore plus (80% de la hauteur disponible)
+ const imageMaxW = Math.min(width * 0.45, 600);
+ const imageMaxH = Math.min(height * 0.80, 650);
+ this.fitImage(this.user1Image, imageMaxW, imageMaxH);
+ this.fitImage(this.user2Image, imageMaxW, imageMaxH);
+
+ const centerY = height / 2 + 20;
+ const spacing = width * 0.25;
+
+ this.user1Image.setPosition(width / 2 - spacing, centerY);
+ this.user1Label.setPosition(width / 2 - spacing, centerY + this.user1Image.displayHeight / 2 + 30);
+
+ this.user2Image.setPosition(width / 2 + spacing, centerY);
+ this.user2Label.setPosition(width / 2 + spacing, centerY + this.user2Image.displayHeight / 2 + 30);
+ }
+
+ private selectPlayer(playerId: 'user1' | 'user2'): void {
+ this.registry.set('selectedPlayer', playerId);
+ // Passer directement à la vidéo d'intro (le manifest.json gère l'orientation)
+ this.scene.start('IntroVideoScene');
+ }
+}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 63eab4a..73df8b5 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -11,10 +11,51 @@ export const PLAYER_MAX_SPEED = 300;
export const PLAYER_ACCELERATION = 50;
export const PLAYER_MAX_JUMPS = 2; // Nombre de sauts autorisés (double saut)
-// Gyroscope
-export const GYRO_DEADZONE = 10; // Degrés de zone morte (plus large pour éviter les faux mouvements)
-export const GYRO_MAX_TILT = 35; // Tilt max pris en compte (pense à incliner franchement)
-export const GYRO_SENSITIVITY = 10; // Multiplicateur de sensibilité
+// Hitbox spécifique par joueur
+// User1 et User2 ont les mêmes dimensions (93x224px) et la même hitbox
+export const USER1_HITBOX = {
+ width: 50,
+ height: 130,
+ spriteHeight: 224
+};
+
+export const USER2_HITBOX = {
+ width: 50,
+ height: 130,
+ spriteHeight: 224
+};
+
+// ========================================
+// GYROSCOPE - CONFIGURATION PAR PLATEFORME
+// ========================================
+// Les plateformes iOS et Android ont des comportements différents
+// pour le DeviceOrientationEvent. Ces configurations sont séparées
+// pour faciliter le réglage et le débogage sur chaque appareil.
+
+// Configuration ANDROID
+// ---------------------
+export const GYRO_DEADZONE_ANDROID = 3; // Zone morte (degrés) - en-dessous, pas de mouvement (réduit pour détecter plus tôt)
+export const GYRO_MAX_TILT_ANDROID = 30; // Tilt maximum pris en compte (degrés) - augmenté pour plus d'amplitude
+export const GYRO_SENSITIVITY_ANDROID = 8; // Multiplicateur de vitesse (réduit pour contrôle plus précis)
+export const GYRO_CALIBRATION_SAMPLES_ANDROID = 10; // Nombre d'échantillons pour calibration initiale
+export const GYRO_INVERT_BETA_ANDROID = false; // PAS d'inversion pour Android (false = pencher avant = avancer)
+export const GYRO_USE_GAMMA_ANDROID = false; // FALSE = beta (rotation autour de X = AVANT/ARRIÈRE), true = gamma (gauche/droite)
+export const GYRO_USE_ALPHA_ANDROID = false; // false = utiliser beta/gamma, true = alpha (rotation axe Z = boussole) - NON ADAPTÉ pour paysage
+
+// Configuration iOS
+// -----------------
+export const GYRO_DEADZONE_IOS = 5; // Zone morte (degrés) - augmentée pour plus de stabilité
+export const GYRO_MAX_TILT_IOS = 45; // Tilt maximum pris en compte (degrés) - augmenté pour nécessiter plus d'angle
+export const GYRO_SENSITIVITY_IOS = 8; // Multiplicateur de vitesse (réduit pour contrôle plus précis)
+export const GYRO_CALIBRATION_SAMPLES_IOS = 20; // Nombre d'échantillons pour calibration (plus stable sur iOS)
+export const GYRO_INVERT_BETA_IOS = false; // Inverser la lecture du beta (false = pas d'inversion)
+export const GYRO_USE_GAMMA_IOS = false; // Utiliser beta (avant/arrière) en paysage pour iOS
+export const GYRO_USE_ALPHA_IOS = false; // iOS n'utilise pas alpha
+
+// Compatibilité (valeurs par défaut si plateforme non détectée)
+export const GYRO_DEADZONE = GYRO_DEADZONE_ANDROID;
+export const GYRO_MAX_TILT = GYRO_MAX_TILT_ANDROID;
+export const GYRO_SENSITIVITY = GYRO_SENSITIVITY_ANDROID;
// Niveau
export const LEVEL_DURATION = 180; // 3 minutes en secondes
@@ -25,7 +66,10 @@ export const PLAYER_STARTING_LIVES = 3; // Nombre de vies au départ
export const RESPAWN_INVINCIBILITY_TIME = 2000; // Temps d'invincibilité après respawn (ms)
// Coffre final
-export const CHEST_REQUIRED_GIFTS = 15; // Nombre de cadeaux requis pour ouvrir le coffre
+export const CHEST_REQUIRED_GIFTS = 13; // Nombre de cadeaux requis pour ouvrir le coffre
+
+// Obstacles (champignons)
+export const MIN_MUSHROOMS = 25; // Nombre minimum de champignons places
// Couleurs
export const COLOR_PRIMARY = 0x4CAF50;
diff --git a/vite.config.ts b/vite.config.ts
index cc1a8d4..8436481 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -10,7 +10,27 @@ export default defineConfig({
'localhost',
'127.0.0.1',
],
+ // HMR désactivé temporairement - problème de proxy WebSocket avec Nginx Proxy Manager
+ // Pour l'instant, rafraîchir manuellement la page après modifications
+ hmr: false,
},
+ plugins: [
+ {
+ name: 'log-client-connections',
+ configureServer(server) {
+ server.middlewares.use((req, res, next) => {
+ const ip =
+ (req.headers['x-forwarded-for'] as string) ||
+ req.socket.remoteAddress ||
+ 'unknown';
+ const ua = req.headers['user-agent'] || 'unknown';
+ const url = req.url || '';
+ console.log(`[vite] ${ip} ${req.method} ${url} ua="${ua}"`);
+ next();
+ });
+ },
+ },
+ ],
build: {
outDir: 'dist',
assetsDir: 'assets',