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',