end
This commit is contained in:
BIN
public/assets/video/end_J.mp4
Normal file
BIN
public/assets/video/end_J.mp4
Normal file
Binary file not shown.
@@ -4,6 +4,7 @@ import { BootScene } from './scenes/BootScene';
|
|||||||
import { MenuScene } from './scenes/MenuScene';
|
import { MenuScene } from './scenes/MenuScene';
|
||||||
import { GameScene } from './scenes/GameScene';
|
import { GameScene } from './scenes/GameScene';
|
||||||
import { IntroScene } from './scenes/IntroScene';
|
import { IntroScene } from './scenes/IntroScene';
|
||||||
|
import { EndScene } from './scenes/EndScene';
|
||||||
|
|
||||||
// Configuration Phaser
|
// Configuration Phaser
|
||||||
const config: Phaser.Types.Core.GameConfig = {
|
const config: Phaser.Types.Core.GameConfig = {
|
||||||
@@ -22,7 +23,7 @@ const config: Phaser.Types.Core.GameConfig = {
|
|||||||
debug: false, // Mettre à true pour voir les hitboxes
|
debug: false, // Mettre à true pour voir les hitboxes
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scene: [BootScene, IntroScene, MenuScene, GameScene],
|
scene: [BootScene, IntroScene, MenuScene, GameScene, EndScene],
|
||||||
backgroundColor: '#87CEEB',
|
backgroundColor: '#87CEEB',
|
||||||
render: {
|
render: {
|
||||||
pixelArt: false,
|
pixelArt: false,
|
||||||
|
|||||||
@@ -78,7 +78,12 @@ export class BootScene extends Phaser.Scene {
|
|||||||
|
|
||||||
// Vidéo d'intro (mp4 uniquement)
|
// Vidéo d'intro (mp4 uniquement)
|
||||||
// Le 3e paramètre 'noAudio' est à false pour garder l'audio si présent
|
// Le 3e paramètre 'noAudio' est à false pour garder l'audio si présent
|
||||||
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.
|
// TODO: Charger d'autres sprites, backgrounds, sons, etc.
|
||||||
}
|
}
|
||||||
|
|||||||
162
src/scenes/EndScene.ts
Normal file
162
src/scenes/EndScene.ts
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,8 +86,21 @@ export class GameScene extends Phaser.Scene {
|
|||||||
// Créer le joueur
|
// Créer le joueur
|
||||||
this.player = new Player(this, 200, height - 150);
|
this.player = new Player(this, 200, height - 150);
|
||||||
|
|
||||||
// Collision joueur / plateformes
|
// Collision joueur / plateformes (one-way platforms)
|
||||||
this.physics.add.collider(this.player, this.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
|
// Créer les groupes d'objets
|
||||||
this.createObjectGroups();
|
this.createObjectGroups();
|
||||||
@@ -833,11 +846,11 @@ export class GameScene extends Phaser.Scene {
|
|||||||
stats.setScrollFactor(0);
|
stats.setScrollFactor(0);
|
||||||
stats.setDepth(2000);
|
stats.setDepth(2000);
|
||||||
|
|
||||||
// Message retour menu
|
// Message vidéo de fin
|
||||||
const returnText = this.add.text(
|
const returnText = this.add.text(
|
||||||
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
this.cameras.main.height - 80,
|
this.cameras.main.height - 80,
|
||||||
'Retour au menu dans 7 secondes...',
|
'Vidéo de fin dans 5 secondes...',
|
||||||
{
|
{
|
||||||
fontSize: '24px',
|
fontSize: '24px',
|
||||||
color: '#CCCCCC',
|
color: '#CCCCCC',
|
||||||
@@ -852,10 +865,10 @@ export class GameScene extends Phaser.Scene {
|
|||||||
// Particules de célébration
|
// Particules de célébration
|
||||||
this.createVictoryParticles();
|
this.createVictoryParticles();
|
||||||
|
|
||||||
// Retour au menu après 7 secondes
|
// Lancer la vidéo de fin après 5 secondes
|
||||||
this.time.delayedCall(7000, () => {
|
this.time.delayedCall(5000, () => {
|
||||||
this.cleanup();
|
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(
|
const respawnText = this.add.text(
|
||||||
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
this.cameras.main.height / 2,
|
this.cameras.main.height / 2,
|
||||||
`💫 RESPAWN! ${this.lives} ❤️ restantes`,
|
`💫 RÉAPPARITION! ${this.lives} ❤️ restantes`,
|
||||||
{
|
{
|
||||||
fontSize: '36px',
|
fontSize: '36px',
|
||||||
color: '#00FF00',
|
color: '#00FF00',
|
||||||
|
|||||||
@@ -216,8 +216,9 @@ export class IntroScene extends Phaser.Scene {
|
|||||||
this.updateVideoSize();
|
this.updateVideoSize();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Forcer mute pour éviter les blocages autoplay
|
// NE PAS muter pour entendre l'audio de la vidéo
|
||||||
this.video.setMute(true);
|
// (pas de problème d'autoplay car l'utilisateur a cliqué sur Play)
|
||||||
|
this.video.setMute(false);
|
||||||
this.video.setLoop(false);
|
this.video.setLoop(false);
|
||||||
|
|
||||||
console.log('[IntroScene] Démarrage de la lecture');
|
console.log('[IntroScene] Démarrage de la lecture');
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
const width = this.cameras.main.width;
|
const width = this.cameras.main.width;
|
||||||
const height = this.cameras.main.height;
|
const height = this.cameras.main.height;
|
||||||
|
|
||||||
|
// Réinitialiser le flag à chaque création du menu
|
||||||
|
this.hasStarted = false;
|
||||||
|
|
||||||
// Titre
|
// Titre
|
||||||
const title = this.add.text(width / 2, height / 3, 'MARIO RUNNER', {
|
const title = this.add.text(width / 2, height / 3, 'MARIO RUNNER', {
|
||||||
fontSize: '64px',
|
fontSize: '64px',
|
||||||
@@ -89,8 +92,17 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
* Lance la scène de jeu
|
* Lance la scène de jeu
|
||||||
*/
|
*/
|
||||||
private startGame(): void {
|
private startGame(): void {
|
||||||
if (this.hasStarted || this.scene.isActive('GameScene')) return;
|
if (this.hasStarted) return;
|
||||||
this.hasStarted = true;
|
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');
|
this.scene.start('GameScene');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user