diff --git a/public/assets/video/end_J.mp4 b/public/assets/video/end_J.mp4 new file mode 100644 index 0000000..a01dd0b Binary files /dev/null and b/public/assets/video/end_J.mp4 differ diff --git a/src/game.ts b/src/game.ts index dec357f..8eed933 100644 --- a/src/game.ts +++ b/src/game.ts @@ -4,6 +4,7 @@ import { BootScene } from './scenes/BootScene'; import { MenuScene } from './scenes/MenuScene'; import { GameScene } from './scenes/GameScene'; import { IntroScene } from './scenes/IntroScene'; +import { EndScene } from './scenes/EndScene'; // Configuration Phaser const config: Phaser.Types.Core.GameConfig = { @@ -22,7 +23,7 @@ const config: Phaser.Types.Core.GameConfig = { debug: false, // Mettre à true pour voir les hitboxes }, }, - scene: [BootScene, IntroScene, MenuScene, GameScene], + scene: [BootScene, IntroScene, MenuScene, GameScene, EndScene], backgroundColor: '#87CEEB', render: { pixelArt: false, diff --git a/src/scenes/BootScene.ts b/src/scenes/BootScene.ts index 5a809c0..8517df9 100644 --- a/src/scenes/BootScene.ts +++ b/src/scenes/BootScene.ts @@ -78,7 +78,12 @@ export class BootScene extends Phaser.Scene { // Vidéo d'intro (mp4 uniquement) // Le 3e paramètre 'noAudio' est à false pour garder l'audio si présent - this.load.video('intro', 'assets/video/intro.mp4', false); + // 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); + + // Vidéo de fin (quand le joueur gagne) + this.load.video('end', `assets/video/end_J.mp4?v=${timestamp}`, false); // TODO: Charger d'autres sprites, backgrounds, sons, etc. } diff --git a/src/scenes/EndScene.ts b/src/scenes/EndScene.ts new file mode 100644 index 0000000..7ccc503 --- /dev/null +++ b/src/scenes/EndScene.ts @@ -0,0 +1,162 @@ +import Phaser from 'phaser'; + +/** + * Scène de fin : lit une vidéo de victoire en mode paysage puis retourne au menu + */ +export class EndScene extends Phaser.Scene { + private video?: Phaser.GameObjects.Video; + private hasFinished: boolean = false; + + constructor() { + super({ key: 'EndScene' }); + } + + create(): void { + const { width, height } = this.cameras.main; + this.cameras.main.setBackgroundColor('#000000'); + + 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(); + } + + /** + * Lit la vidéo de fin + */ + private playEndVideo(): void { + const { width, height } = this.cameras.main; + + console.log('[EndScene] Vidéo dans cache?', this.cache.video.exists('end')); + + if (!this.cache.video.exists('end')) { + console.warn('[EndScene] Vidéo de fin non trouvée → passage au menu.'); + this.gotoMenu(); + return; + } + + console.log('[EndScene] Création de l\'objet vidéo'); + this.video = this.add.video(width / 2, height / 2, 'end'); + 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 || 324; + const videoHeight = this.video.video?.videoHeight || 720; + + console.log('[EndScene] 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('[EndScene] Démarrage de la lecture'); + const started = this.video.play(true); // Autoplay + console.log('[EndScene] Lecture démarrée?', started); + + if (!started) { + console.warn('[EndScene] Lecture vidéo bloquée → passage au menu.'); + this.gotoMenu(); + return; + } + + // Événement de fin de vidéo + this.video.once('complete', () => { + console.log('[EndScene] 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('[EndScene] Vidéo arrêtée (événement stop) → passage au menu'); + this.gotoMenu(); + }); + + this.video.once('error', (err: any) => { + console.error('[EndScene] Erreur lecture vidéo:', err); + this.gotoMenu(); + }); + + // Sécurité : timer basé sur la durée de la vidéo + 2 secondes + // 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, () => { + if (!this.hasFinished) { + console.warn('[EndScene] Timer de sécurité 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); + } + }); + } + + /** + * Ajuste la vidéo à l'écran en mode paysage en respectant le ratio + */ + 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; + + // Dimensions natives de la vidéo + const nativeW = this.video.video?.videoWidth || 324; + const nativeH = this.video.video?.videoHeight || 720; + + console.log('[EndScene] updateVideoSize - Écran:', w, 'x', h); + console.log('[EndScene] updateVideoSize - Vidéo native:', nativeW, 'x', nativeH); + + // Ratio vidéo et écran + const videoRatio = nativeW / nativeH; + const screenRatio = w / h; + + let scale: number; + + // Mode "contain" : adapter pour que toute la vidéo soit visible + if (screenRatio > videoRatio) { + // L'écran est plus large que la vidéo → adapter à la HAUTEUR + scale = h / nativeH; + } else { + // L'écran est plus étroit que la vidéo → adapter à la LARGEUR + scale = w / nativeW; + } + + const finalWidth = nativeW * scale; + const finalHeight = nativeH * scale; + + console.log('[EndScene] updateVideoSize - Scale:', scale.toFixed(2)); + console.log('[EndScene] updateVideoSize - Taille finale:', Math.round(finalWidth), 'x', Math.round(finalHeight)); + + this.video.setScale(scale); + this.video.setPosition(w / 2, h / 2); + } + + /** + * Passe au menu + */ + 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/GameScene.ts b/src/scenes/GameScene.ts index b0f76d2..4b9b11c 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -86,8 +86,21 @@ export class GameScene extends Phaser.Scene { // Créer le joueur this.player = new Player(this, 200, height - 150); - // Collision joueur / plateformes - this.physics.add.collider(this.player, this.platforms!); + // Collision joueur / plateformes (one-way platforms) + // Le joueur peut passer par en dessous mais atterrit par le dessus + this.physics.add.collider(this.player, this.platforms!, undefined, (playerObj, platformObj) => { + const player = playerObj as Phaser.GameObjects.GameObject; + const platform = platformObj as Phaser.GameObjects.GameObject; + + const playerBody = (player as any).body as Phaser.Physics.Arcade.Body; + const platformBody = (platform as any).body as Phaser.Physics.Arcade.Body; + + if (!playerBody || !platformBody) 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; + }); // Créer les groupes d'objets this.createObjectGroups(); @@ -833,11 +846,11 @@ export class GameScene extends Phaser.Scene { stats.setScrollFactor(0); stats.setDepth(2000); - // Message retour menu + // Message vidéo de fin const returnText = this.add.text( this.cameras.main.scrollX + this.cameras.main.width / 2, this.cameras.main.height - 80, - 'Retour au menu dans 7 secondes...', + 'Vidéo de fin dans 5 secondes...', { fontSize: '24px', color: '#CCCCCC', @@ -852,10 +865,10 @@ export class GameScene extends Phaser.Scene { // Particules de célébration this.createVictoryParticles(); - // Retour au menu après 7 secondes - this.time.delayedCall(7000, () => { + // Lancer la vidéo de fin après 5 secondes + this.time.delayedCall(5000, () => { this.cleanup(); - this.scene.start('MenuScene'); + this.scene.start('EndScene'); }); } @@ -932,7 +945,7 @@ export class GameScene extends Phaser.Scene { const respawnText = this.add.text( this.cameras.main.scrollX + this.cameras.main.width / 2, this.cameras.main.height / 2, - `💫 RESPAWN! ${this.lives} ❤️ restantes`, + `💫 RÉAPPARITION! ${this.lives} ❤️ restantes`, { fontSize: '36px', color: '#00FF00', diff --git a/src/scenes/IntroScene.ts b/src/scenes/IntroScene.ts index 1ffee68..bb80ec8 100644 --- a/src/scenes/IntroScene.ts +++ b/src/scenes/IntroScene.ts @@ -216,8 +216,9 @@ export class IntroScene extends Phaser.Scene { this.updateVideoSize(); }); - // Forcer mute pour éviter les blocages autoplay - this.video.setMute(true); + // NE PAS muter pour entendre l'audio de la vidéo + // (pas de problème d'autoplay car l'utilisateur a cliqué sur Play) + this.video.setMute(false); this.video.setLoop(false); console.log('[IntroScene] Démarrage de la lecture'); diff --git a/src/scenes/MenuScene.ts b/src/scenes/MenuScene.ts index 61d9f1d..3dc51ac 100644 --- a/src/scenes/MenuScene.ts +++ b/src/scenes/MenuScene.ts @@ -16,6 +16,9 @@ export class MenuScene extends Phaser.Scene { const width = this.cameras.main.width; const height = this.cameras.main.height; + // Réinitialiser le flag à chaque création du menu + this.hasStarted = false; + // Titre const title = this.add.text(width / 2, height / 3, 'MARIO RUNNER', { fontSize: '64px', @@ -89,8 +92,17 @@ export class MenuScene extends Phaser.Scene { * Lance la scène de jeu */ private startGame(): void { - if (this.hasStarted || this.scene.isActive('GameScene')) return; + if (this.hasStarted) return; this.hasStarted = true; + + console.log('[MenuScene] Démarrage de GameScene'); + + // Arrêter GameScene si elle existe déjà + if (this.scene.isActive('GameScene')) { + this.scene.stop('GameScene'); + } + + // Démarrer une nouvelle partie this.scene.start('GameScene'); } }