Bonjour je fais actuellement un jeu rogue like en html css javascript et phaser. Mon message va comporter deux parties. La première partie c'est les modifications que je veux que tu ajoutes a mon code. Le deuxième partie c'est le code original dans lequel tu dois faire les modifications. Tu devras me donner un code entier avec les modifications et fonctionnel. Première partie ci après. Voici les étapes et les sections du code que tu devras modifier ou ajouter :

Chargement des Assets (IntroScene)
    Tu dois charger les images du Knight (K1 à K8) dans la fonction preload de la scène IntroScene, car c'est là que tous les assets du jeu sont chargés.

Création des Animations (GameScene)
    Dans la fonction createAnimations de la scène GameScene, tu dois définir les animations pour le déplacement du Knight vers la droite et vers la gauche en utilisant les images chargées.

Déclaration et Initialisation du Groupe de Knights (GameScene) * Dans le constructor de GameScene, déclare une variable pour le groupe de Knights. * Dans la fonction startGame de GameScene, initialise ce groupe de physique.

Définition des Collisions/Overlaps (GameScene)
    Dans la fonction startGame, ajoute les overlaps nécessaires :
        Entre le joueur (this.player) et le groupe de Knights (this.knights).
        Entre les projectiles du joueur (this.balls) et le groupe de Knights (this.knights).

Implémentation des Fonctions de Collision (GameScene)
    Crée deux nouvelles méthodes dans GameScene :
        playerHitKnight(player, knight) : Gère ce qui se passe quand le joueur touche un Knight (similaire à playerHitBat).
        ballHitKnight(ball, knight) : Gère ce qui se passe quand une balle du joueur touche un Knight (similaire à ballHitBat).

Logique de Spawn des Knights (GameScene) * Déclare des variables pour gérer le timer et les paramètres de spawn des Knights dans le constructor. * Dans startGame, démarre un timer pour le spawn des Knights. * Crée une méthode spawnKnightWave() qui sera appelée par le timer. Cette méthode déterminera combien de Knights spawner et appellera spawnSingleKnight(). Elle devra aussi s'arrêter quand le boss apparaît. * Crée une méthode spawnSingleKnight() qui choisira une position de spawn aléatoire hors écran et créera un sprite de Knight.

Mise à Jour de la Logique des Knights (GameScene)
    Dans la fonction update de GameScene, ajoute une boucle pour parcourir tous les Knights actifs dans leur groupe.
    Pour chaque Knight, calcule la direction vers le joueur et ajuste sa vélocité et son animation (droite/gauche) en fonction de cette direction, comme tu le fais pour les Bats.

Nettoyage (GameScene)
    Dans la fonction gameOver, assure-toi de nettoyer le groupe de Knights et d'arrêter le timer de spawn des Knights.

Voici les modifications spécifiques à ajouter ou à trouver dans ton code :

    Dans IntroScene.preload():

Ajoute le chargement des images du Knight :

// IntroScene class preload() { console.log("IntroScene preloading assets..."); // ... (chargement des assets existants) ...

// Knight assets (NEW)
this.load.image('knight_K1', 'assets/K1.png'); // Right 1
this.load.image('knight_K2', 'assets/K2.png'); // Right 2
this.load.image('knight_K3', 'assets/K3.png'); // Right 3
this.load.image('knight_K4', 'assets/K4.png'); // Right 4
this.load.image('knight_K5', 'assets/K5.png'); // Left 1
this.load.image('knight_K6', 'assets/K6.png'); // Left 2
this.load.image('knight_K7', 'assets/K7.png'); // Left 3
this.load.image('knight_K8', 'assets/K8.png'); // Left 4

// ... (chargement des autres assets) ...
console.log("IntroScene preloading finished.");

}

    Dans GameScene.constructor():

Déclare les variables pour le groupe de Knights et leur spawn :

// GameScene class constructor() { super({ key: 'GameScene' });

// ... (variables existantes) ...

this.bats = null;
this.knights = null; // NEW: Group for Knights
this.coins = null;
this.bossBombs = null;

// ... (autres variables) ...

this.batSpawnTimer = null;
this.batSpawnDelay = 1500;
this.batsPerSpawn = 1;

this.knightSpawnTimer = null; // NEW: Timer for Knight spawn
this.knightSpawnDelay = 3000; // NEW: Initial delay for Knights (e.g., slower spawn than bats)
this.knightsPerSpawn = 1; // NEW: Initial number of Knights per spawn
this.knightSpeed = 80; // NEW: Speed for Knights (e.g., slightly slower than bats)

// ... (autres variables) ...

}

    Dans GameScene.createAnimations():

Ajoute les animations du Knight :

// GameScene class createAnimations() { // ... (animations existantes pour dragon, bat, wizard) ...

// Knight animations (NEW)
this.anims.create({
    key: 'knight_walk_right', // Right animation (Knight)
    frames: [
        { key: 'knight_K1' },
        { key: 'knight_K2' },
        { key: 'knight_K3' },
        { key: 'knight_K4' }
    ],
    frameRate:  8, // Adjust frame rate as needed
    repeat: -1
});

this.anims.create({
    key: 'knight_walk_left', // Left animation (Knight)
    frames: [
        { key: 'knight_K5' },
        { key: 'knight_K6' },
        { key: 'knight_K7' },
        { key: 'knight_K8' }
    ],
    frameRate: 8, // Adjust frame rate as needed
    repeat: -1
});

}

    Dans GameScene.startGame():

Initialise le groupe de Knights, ajoute les overlaps et démarre le timer de spawn des Knights :

// GameScene class startGame() { console.log("Starting actual game initialization...");

// ... (nettoyage des sprites d'intro) ...

// --- INITIALIZE Game Elements ---

// ... (création du player) ...

// Create physics groups
this.balls = this.physics.add.group(); // Player projectiles
this.bats = this.physics.add.group();
this.knights = this.physics.add.group(); // NEW: Initialize Knight group
this.coins = this.physics.add.group();
this.bossBombs = this.physics.add.group(); // Boss projectiles

// Define collisions/overlaps - Ensure groups/player exist before adding
if (this.player && this.bats) this.physics.add.overlap(this.player, this.bats, this.playerHitBat, null, this);
if (this.player && this.knights) this.physics.add.overlap(this.player, this.knights, this.playerHitKnight, null, this); // NEW: Player hits Knight
if (this.balls && this.bats) this.physics.add.overlap(this.balls, this.bats, this.ballHitBat, null, this); // Ball hits Bat
if (this.balls && this.knights) this.physics.add.overlap(this.balls, this.knights, this.ballHitKnight, null, this); // NEW: Ball hits Knight
if (this.player && this.coins) this.physics.add.overlap(this.player, this.coins, this.playerHitCoin, null, this);
if (this.player && this.bossBombs) this.physics.add.overlap(this.player, this.bossBombs, this.playerHitBossBomb, null, this); // Player hits Boss Bomb
// Boss overlaps (playerHitBoss, ballHitBoss) are added in spawnBoss()

// ... (start camera follow, add UI) ...

// Start game timers
// ... (gameTimerEvent, playerAttackTimer) ...

// Bat spawn timer
if (this.batSpawnTimer) { this.batSpawnTimer.remove(); }
this.batSpawnTimer = this.time.addEvent({
    delay: this.batSpawnDelay,
    callback: this.spawnBatWave,
    callbackScope: this,
    loop: true
});

// Knight spawn timer (NEW)
if (this.knightSpawnTimer) { this.knightSpawnTimer.remove(); }
this.knightSpawnTimer = this.time.addEvent({
    delay: this.knightSpawnDelay,
    callback: this.spawnKnightWave, // Call the new knight spawn method
    callbackScope: this,
    loop: true
});

// ... (bossAttackTimer, start background music) ...

// Game is now running!
this.isGameRunning = true;

// ... (ensure initial player state) ...

console.log("Game initialized and running!");

}

    Dans GameScene.update():

Ajoute la logique de mouvement et d'animation pour les Knights :

// GameScene class update(time, delta) { // --- Only run game logic if game is running --- if (!this.isGameRunning) { return; // Stop update loop if game is not running } // ... (vérification player actif) ...

// ... (logique de mouvement du player) ...

// ... (rotation des balles) ...

// ... (mise à jour UI) ...

// Bat logic (move towards player) - Ensure bats group and player exist
if(this.bats && this.player) {
    this.bats.getChildren().forEach(bat => {
        // ... (logique de mouvement et animation des bats) ...
    });
}

// Knight logic (move towards player) - Ensure knights group and player exist (NEW)
if(this.knights && this.player) {
    this.knights.getChildren().forEach(knight => {
         // Only process active knights
         if (!knight.active) return;

        let directionX = this.player.x - knight.x;
        let directionY = this.player.y - knight.y;

        let length = Math.sqrt(directionX * directionX + directionY * directionY);
        if (length > 0) {
             // Speed increases slightly up to boss spawn time, then stops increasing
             // Added checks for gameDuration and bossSpawnTime being valid numbers > 0
             let timeFactor = 0;
             if (this.gameDuration > 0) {
                 timeFactor = Math.min(this.timeElapsed, this.bossSpawnTime) / this.gameDuration;
             }
             // Use knightSpeed variable
             let speed = this.knightSpeed + timeFactor * 50; // Knights might scale speed differently
            knight.setVelocity(directionX / length * speed, directionY / length * speed);

             // Manage sprite animation based on horizontal direction
             if (directionX < 0) {
                knight.anims.play('knight_walk_left', true); // Knight left animation
            } else if (directionX > 0) {
                knight.anims.play('knight_walk_right', true); // Knight right animation
            } else {
                // Vertical movement only, stop animation and set idle frame
                if (knight.anims.isPlaying) {
                     knight.anims.stop();
                     // Keep idle orientation based on last played anim
                     if (knight.anims.currentAnim && knight.anims.currentAnim.key === 'knight_walk_left') {
                         knight.setFrame('knight_K5'); // Left idle frame
                     } else {
                         knight.setFrame('knight_K1'); // Right idle frame
                     }
                } else {
                     // If no animation was playing, choose a default frame
                     knight.setFrame('knight_K1');
                }
            }
        } else {
            // Knight is exactly on the player, stop movement and animation
             knight.setVelocity(0);
             if (knight.anims.isPlaying) {
                 knight.anims.stop();
                 if (knight.anims.currentAnim && knight.anims.currentAnim.key === 'knight_walk_left') {
                     knight.setFrame('knight_K5');
                 } else {
                     knight.setFrame('knight_K1');
                 }
             }
        }
    });
}


// ... (Wizard Boss Logic) ...

// Check game end conditions
// ... (vérification player health et timer) ...

} // End update()

    Ajoute les nouvelles méthodes de collision (playerHitKnight, ballHitKnight) dans GameScene:

Ces méthodes sont des copies adaptées de playerHitBat et ballHitBat.

// GameScene class // ... (méthodes existantes comme gameOver, playerHitBat, playerHitBoss, etc.) ...

playerHitKnight(player, knight) { // NEW: Player hits Knight // Ensure objects exist and are active, and game is running, and player is not invincible if (this.isGameRunning && player && player.active && knight && knight.active && !this.isPlayerInvincible) { this.playerHealth--; // Knights deal damage this.updateHealthDisplay(); // Update heart icons

if (this.playerHealth <= 0) {
        this.gameOver(false); // End game
    } else {
        // Check if knight exists before destroying
        if (knight && knight.active) knight.destroy();
        this.activateInvincibility(); // Grant invincibility
    }
} else if (knight && knight.active) {
     // If game not running or player invincible/inactive, destroy knight anyway to clean up
     knight.destroy();
}

}

ballHitKnight(ball, knight) { // NEW: Ball hits Knight // Ensure objects exist and are active if (!ball || !knight || !ball.active || !knight.active) return;

const knightX = knight.x;
const knightY = knight.y;

// Ensure objects exist before destroying
if (ball && ball.active) ball.destroy();
if (knight && knight.active) knight.destroy();


// Play sound - Check if instance exists
if (this.explosionSound) {
    this.explosionSound.play();
} else {
    console.warn("Explosion sound not initialized.");
}

// Only spawn coin if coins group exists
if(this.coins) {
     let coin = this.coins.create(knightX, knightY, 'coin');
     coin.setScale(0.1);
     coin.setCollideWorldBounds(false);
     // Destroy coin after a delay to prevent infinite coins
     this.time.delayedCall(10000, coin.destroy, [], coin); // Destroy after 10 seconds
} else {
     console.warn("Coins group not initialized, cannot spawn coin.");
}

// Only count kill if game is running
if(this.isGameRunning) {
     this.killedCount++;
}

}

// ... (autres méthodes existantes) ...

    Ajoute les nouvelles méthodes de spawn (spawnKnightWave, spawnSingleKnight) dans GameScene:

Ces méthodes sont des copies adaptées de spawnBatWave et spawnSingleBat.

// GameScene class // ... (méthodes existantes comme spawnBatWave, spawnSingleBat, spawnBoss, etc.) ...

spawnKnightWave() { // NEW: Spawn wave of Knights // Don't spawn knights if game is not running, or boss has appeared, or knights group doesn't exist // Also ensure knightSpawnTimer exists if this is called by the timer if (!this.isGameRunning || this.timeElapsed ≥ this.bossSpawnTime || !this.knights || !this.knightSpawnTimer) return;

let elapsedMinutes = this.timeElapsed / 60;
// Adjust spawn delay and number of knights based on time, similar to bats
let currentSpawnDelay = Math.max(1000, this.knightSpawnDelay - elapsedMinutes * 150); // Knights might spawn slightly less often or scale differently
let currentKnightsPerSpawn = Math.floor(this.knightsPerSpawn + elapsedMinutes * 0.5); // Knights might increase slower than bats
currentKnightsPerSpawn = Math.max(1, currentKnightsPerSpawn);

// Update the existing timer delay. Timer is created in startGame().
this.knightSpawnTimer.delay = currentSpawnDelay;

for (let i = 0; i < currentKnightsPerSpawn; i++) {
    this.spawnSingleKnight(); // Call the method to spawn one knight
}

}

spawnSingleKnight() { // NEW: Spawn a single Knight // Don't spawn if game is not running or boss has appeared or knights group doesn't exist if (!this.isGameRunning || this.timeElapsed ≥ this.bossSpawnTime || !this.knights) return;

let x, y;
let camera = this.cameras.main;
let spawnPadding =  150; // Spawn outside visible screen

// Ensure camera worldView is available
if (!camera || !camera.worldView) {
     console.warn("Camera or worldview not available, cannot spawn knight.");
      return;
}

let worldLeft = camera.worldView.left;
let worldRight = camera.worldView.right;
let worldTop = camera.worldView.top;
let worldBottom = camera.worldView.bottom;

 // Ensure world bounds are valid
 if (worldLeft >= worldRight || worldTop >= worldBottom) {
      console.warn("Invalid camera worldview dimensions, cannot spawn knight.");
     return;
 }


let side = Phaser.Math.Between(0, 3); // 0:Top, 1:Bottom, 2:Left, 3:Right

switch (side) {
    case 0: // Top (above camera view)
        x = Phaser.Math.Between(worldLeft, worldRight);
        y = worldTop - spawnPadding;
        break;
    case 1: // Bottom (below camera view)
        x = Phaser.Math.Between(worldLeft, worldRight);
        y = worldBottom + spawnPadding;
        break;
    case 2: // Left (left of camera view)
        x = worldLeft - spawnPadding;
        y = Phaser.Math.Between(worldTop, worldBottom);
        break;
    case 3: // Right (right of camera view)
        x = worldRight + spawnPadding;
        y = Phaser.Math.Between(worldTop, worldBottom);
        break;
    default: // Should not happen
        x = worldLeft - spawnPadding; y = worldTop - spawnPadding;
         console.warn("Invalid spawn side calculated.");
        break;
}

// Create the knight sprite using the group
let knight = this.knights.create(x, y, 'knight_K1'); // Use the first frame (right idle) initially
// Ensure knight was created before configuring
if (!knight) {
     console.warn("Failed to create knight sprite.");
     return;
}

knight.setScale(0.15); // Adjust scale as needed for your knight sprites
knight.setCollideWorldBounds(false);
 // Animation is played in update() based on movement direction

 // Destroy knight after a very long time to prevent runaway memory use
 this.time.delayedCall(60000, knight.destroy, [], knight); // Destroy after 60 seconds

}

// ... (autres méthodes existantes) ...

    Dans GameScene.gameOver():

Ajoute le nettoyage du groupe de Knights et de leur timer :

// GameScene class gameOver(survived) { console.log("Game Over. Survived:", survived);

// Stop timers - Check if they exist before removing
if (this.gameTimerEvent) { this.gameTimerEvent.remove(); this.gameTimerEvent = null; }
if (this.playerAttackTimer) { this.playerAttackTimer.remove(); this.playerAttackTimer = null; }
if (this.batSpawnTimer) { this.batSpawnTimer.remove(); this.batSpawnTimer = null; }
if (this.knightSpawnTimer) { this.knightSpawnTimer.remove(); this.knightSpawnTimer = null; } // NEW: Stop Knight spawn timer
if (this.invincibilityTimer) { this.invincibilityTimer.remove(); this.invincibilityTimer = null; }
 if (this.bossAttackTimer) { this.bossAttackTimer.remove(); this.bossAttackTimer = null; }

// ... (stop/destroy player) ...

// ... (hide/clear UI) ...

// Clear groups - Check if groups exist before clearing
if (this.bats) { this.bats.clear(true, true); this.bats = null; }
if (this.knights) { this.knights.clear(true, true); this.knights = null; } // NEW: Clear Knight group
if (this.balls) { this.balls.clear(true, true); this.balls = null; }
if (this.coins) { this.coins.clear(true, true); this.coins = null; }
if (this.bossBombs) { this.bossBombs.clear(true, true); this.bossBombs = null; }

// ... (handle boss if exists) ...

// ... (stop intro sequence elements) ...

// ... (stop background music and other sounds) ...

// Reset isGameRunning state for potential future restarts
 this.isGameRunning = false;

// Transition to IntroScene with results
this.scene.start('IntroScene', {
    survived: survived,
    killedCount: this.killedCount,
    coinCount: this.coinCount
});

}

Résumé des modifications :

IntroScene.preload: Ajout des load.image pour K1-K8.
GameScene.constructor: Déclaration de this.knights, this.knightSpawnTimer, this.knightSpawnDelay, this.knightsPerSpawn, this.knightSpeed.
GameScene.createAnimations: Ajout des animations knight_walk_right et knight_walk_left.
GameScene.startGame: Initialisation de this.knights, ajout des overlaps playerHitKnight et ballHitKnight, démarrage du knightSpawnTimer.
GameScene.update: Ajout de la boucle forEach pour this.knights afin de gérer leur mouvement et leur animation.
GameScene: Ajout des méthodes playerHitKnight et ballHitKnight.
GameScene: Ajout des méthodes spawnKnightWave et spawnSingleKnight.
GameScene.gameOver: Ajout du nettoyage pour this.knights et this.knightSpawnTimer.

Ces modifications devraient intégrer les Knights comme un nouvel ennemi se comportant de manière similaire aux Bats, avec leurs propres assets et animations. N'oublie pas d'ajuster les valeurs comme knightSpawnDelay, knightsPerSpawn, knightSpeed, et setScale pour le Knight dans spawnSingleKnight pour qu'ils correspondent à l'équilibre que tu souhaites pour ton jeu. Deuxième partie ci après (le code à modifier). <!DOCTYPE html>
<html lang="fr"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Dragon's Revenge</title> <style> body { margin: 0; padding: 0; background-color: #000; display: flex; justify-content: center; align-items: center; min-height: 100vh; overflow: hidden; } canvas { display: block; } </style> </head> <body> <script src="//cdn.jsdelivr.net/npm/phaser@3.55.2/dist/phaser.min.js"></script> <script> const config = { type: Phaser.AUTO, width: 1600, height: 900, physics: { default: 'arcade', arcade: { gravity: { y: 0 }, // debug: true // Uncomment to see physics bodies } }, scene: [] // Scenes will be added here }; // --- SCÈNE 1: Intro/Menu Scene --- class IntroScene extends Phaser.Scene { constructor() { super({ key: 'IntroScene' }); this.startButton = null; this.resultText = null; // Text for stats } preload() { console.log("IntroScene preloading assets..."); // Load menu/intro specific assets this.load.image('start-button', 'assets/start-button.png'); // Load ALL game assets here for GameScene (including the new sequence assets) this.load.image('background', 'assets/background.png'); // Dragon assets this.load.image('dragon_D1', 'assets/D1.png'); // Right 1 this.load.image('dragon_D2', 'assets/D2.png'); // Right 2 this.load.image('dragon_D3', 'assets/D3.png'); // Right 3 this.load.image('dragon_D4', 'assets/D4.png'); // Right 4 this.load.image('dragon_D5', 'assets/D5.png'); // Left 1 this.load.image('dragon_D6', 'assets/D6.png'); // Left 2 this.load.image('dragon_D7', 'assets/D7.png'); // Left 3 this.load.image('dragon_D8', 'assets/D8.png'); // Left 4 // Wizard assets (for the boss) this.load.image('wizard_W1', 'assets/W1.png'); this.load.image('wizard_W2', 'assets/W2.png'); this.load.image('wizard_W3', 'assets/W3.png'); this.load.image('wizard_W4', 'assets/W4.png'); this.load.image('wizard_W5', 'assets/W5.png'); this.load.image('wizard_W6', 'assets/W6.png'); this.load.image('wizard_W7', 'assets/W7.png'); this.load.image('wizard_W8', 'assets/W8.png'); // Projectile assets this.load.image('bomb', 'assets/bomb.png'); // Boss projectile this.load.image('ball', 'assets/ball.png'); // Player projectile this.load.image('bat_BD', 'assets/BD.png'); this.load.image('bat_BG', 'assets/BG.png'); this.load.image('coin', 'assets/coin.png'); // Heart assets this.load.image('heart', 'assets/heart.png'); this.load.image('emptyheart', 'assets/emptyheart.png'); // --- Assets for the Intro Sequence in GameScene --- this.load.image('egg', 'assets/egg.png'); this.load.image('part1', 'assets/part1.png'); this.load.image('part2', 'assets/part2.png'); // --- END NEW --- // Load audio this.load.audio('music', 'assets/music.mp3'); this.load.audio('cash', 'assets/cash.mp3'); this.load.audio('explosion', 'assets/explosion.mp3'); // --- Audio for the Intro Sequence --- this.load.audio('broken', 'assets/broken.mp3'); // --- END NEW --- console.log("IntroScene preloading finished."); } create(data) { console.log("IntroScene create started."); // Display black background this.add.graphics().fillStyle(0x000000, 1).fillRect(0, 0, config.width, config.height); // Check if coming from Game Over or Win if (data && typeof data.survived !== 'undefined') { // Display end message and stats let message = data.survived ? 'Vous Avez Survécu !' : 'Game Over'; let messageColor = data.survived ? '#0f0' : '#f00'; // Main message (Game Over / Survived) this.add.text(config.width / 2, config.height / 2 - 120, message, { fontSize: '40px', fill: messageColor, align: 'center' }).setOrigin(0.5); // Position stats text below main message this.resultText = this.add.text(config.width / 2, config.height / 2 - 50, `Tués: ${data.killedCount}\nCoins Collectés: ${data.coinCount}`, { fontSize: '24px', fill: '#fff', align: 'center' }).setOrigin(0.5); } else { // First load: display game title this.add.text(config.width / 2, config.height / 2 - 120, "Dragon's Revenge", { fontSize: '40px', fill: '#fff', align: 'center' }).setOrigin(0.5); } // Add Start button this.startButton = this.add.image(config.width / 2, config.height / 2 + 100, 'start-button') .setInteractive(); // Make button interactive // Listen for button click this.startButton.on('pointerdown', this.startCinematic, this); console.log("IntroScene create finished."); } startCinematic() { console.log("Start button clicked. Starting Cinematic Scene..."); this.scene.start('CinematicScene'); } } // --- SCÈNE 2: Cinematic Scene --- class CinematicScene extends Phaser.Scene { constructor() { super({ key: 'CinematicScene' }); this.video = null; } preload() { // Video is loaded here this.load.video('introVid', 'assets/intro.mp4', false, true); console.log("CinematicScene preloaded video."); } create() { console.log("CinematicScene create started."); this.add.graphics().fillStyle(0x000000, 1).fillRect(0, 0, config.width, config.height); this.video = this.add.video(config.width / 2, config.height / 2, 'introVid'); this.video.setOrigin(0.5); // Scale video to fit game dimensions this.video.displayWidth = config.width; this.video.displayHeight = config.height; this.video.setMute(false); console.log("CinematicScene created video, attempting to play..."); // Added check if video exists before playing if (this.video) { this.video.play(); this.video.on('complete', this.onVideoComplete, this); } else { console.warn("Video object not created, skipping video playback."); // Automatically skip if video fails to load/create this.time.delayedCall(100, this.onVideoComplete, [], this); } let skipText = this.add.text(config.width - 20, 20, 'Passer', { fontSize: '24px', fill: '#fff' }).setOrigin(1, 0).setInteractive(); skipText.on('pointerdown', this.onVideoComplete, this); console.log("CinematicScene create finished."); } onVideoComplete() { console.log("Cinematic complete. Starting GameScene..."); if (this.video) { this.video.stop(); this.video.destroy(); this.video = null; // Clear reference } this.scene.start('GameScene'); // Transition to GameScene } } // --- SCÈNE 3: Game Scene --- class GameScene extends Phaser.Scene { constructor() { super({ key: 'GameScene' }); // --- Game State & Elements (Initialized in startGame) --- this.player = null; this.speed = 200; this.playerDirection = 'right'; // 'left' or 'right' this.maxPlayerHealth = 10; this.playerHealth = this.maxPlayerHealth; this.heartGroup = null; this.isPlayerInvincible = false; this.invincibilityDuration = 1000; this.invincibilityTimer = null; this.balls = null; // Player projectiles this.bats = null; this.coins = null; this.bossBombs = null; // Boss projectiles this.cursors = null; // Initialized here but used when isGameRunning this.wasd = null; // Initialized here but used when isGameRunning this.playerAttackCooldown = 500; this.playerAttackTimer = null; this.gameDuration = 120; // 2 minutes this.gameTimerEvent = null; this.timeElapsed = 0; this.batSpawnTimer = null; this.batSpawnDelay = 1500; this.batsPerSpawn = 1; this.ballRotationSpeed = 10; this.ballSpeed = 250; this.wizardBoss = null; this.bossSpeed = 200; this.bossSpawnTime = this.gameDuration - 20; this.wizardBossHits = 0; this.wizardBossRawHits = 0; this.wizardBossMaxHits = 5000; this.bossAttackCooldown = 1500; this.bossAttackTimer = null; // UI Elements (Initialized in startGame) this.timerText = null; this.killedText = null; this.killedCount = 0; this.coinText = null; this.coinCount = 0; // --- Intro Sequence Elements --- this.eggSprite = null; this.part1Sprite = null; this.part2Sprite = null; this.brokenSound = null; // --- END NEW --- this.isGameRunning = false; // Game starts NOT running // Sounds (Loaded in IntroScene, referenced here) // Initialized as null, will be created/assigned in startGame() this.backgroundMusic = null; this.cashSound = null; this.explosionSound = null; } preload() { // All game assets (including egg, parts, broken sound) are loaded in IntroScene console.log("GameScene preload completed (assets loaded in IntroScene)."); } create() { console.log("GameScene create started."); let worldWidth = 3200; let worldHeight = 2400; // --- Initial Setup (Background, Camera, World Bounds) --- let bg = this.add.image(worldWidth / 2, worldHeight / 2, 'background'); bg.setDisplaySize(worldWidth, worldHeight); this.physics.world.setBounds(0, 0, worldWidth, worldHeight); this.cameras.main.setBounds(0, 0, worldWidth, worldHeight); // Camera does NOT follow the player yet, center on world for sequence if needed this.cameras.main.centerOn(worldWidth / 2, worldHeight / 2); // --- Input Setup (Needed early for any potential key presses, though not used until game starts) --- this.cursors = this.input.keyboard.createCursorKeys(); this.wasd = this.input.keyboard.addKeys('W,A,S,D'); // --- Create Animations (Moved from prototype modification) --- this.createAnimations(); // --- END Create Animations --- // --- Intro Sequence Elements (Initially Hidden), setScrollFactor(0) to fix to camera --- this.eggSprite = this.add.image(config.width / 2, config.height / 2, 'egg').setOrigin(0.5).setVisible(false).setScrollFactor(0); this.part1Sprite = this.add.image(config.width / 2, config.height / 2, 'part1').setOrigin(0.5).setVisible(false).setScrollFactor(0); this.part2Sprite = this.add.image(config.width / 2, config.height / 2, 'part2').setOrigin(0.5).setVisible(false).setScrollFactor(0); // The 'broken' sound instance is created in startIntroSequence // --- END NEW --- // --- Game State (Initially Not Running) --- this.isGameRunning = false; // --- Game Elements are NOT created here, they are created in startGame() --- // --- Start the introductory sequence after a small delay --- this.time.delayedCall(200, this.startIntroSequence, [], this); // Delay to ensure everything is rendered console.log("GameScene create finished, waiting for sequence to start."); } // --- Method to create all animations --- createAnimations() { // Define dragon animations this.anims.create({ key: 'fly_right', // Right animation (Dragon) frames: [ { key: 'dragon_D1' }, { key: 'dragon_D2' }, { key: 'dragon_D3' }, { key: 'dragon_D4' } ], frameRate: 10, repeat: -1 }); this.anims.create({ key: 'fly_left', // Left animation (Dragon) frames: [ { key: 'dragon_D5' }, { key: 'dragon_D6' }, { key: 'dragon_D7' }, { key: 'dragon_D8' } ], frameRate: 10, repeat: -1 }); // Bat animation this.anims.create({ key: 'fly_bat', frames: [ { key: 'bat_BG' }, // Bat facing left (no flipX) { key: 'bat_BD' } // Bat facing right (flipX=true) ], frameRate: 8, repeat: -1 }); // Wizard Boss animations this.anims.create({ key: 'wizard_walk_right', // Wizard walking right animation frames: [ { key: 'wizard_W1' }, { key: 'wizard_W2' }, { key: 'wizard_W3' }, { key: 'wizard_W4' } ], frameRate: 10, repeat: -1 }); this.anims.create({ key: 'wizard_walk_left', // Wizard walking left animation frames: [ { key: 'wizard_W5' }, { key: 'wizard_W6' }, { key: 'wizard_W7' }, { key: 'wizard_W8' } ], frameRate: 10, repeat: -1 }); } // --- END createAnimations method --- // --- Method to start the visual/audio intro sequence --- startIntroSequence() { console.log("Starting Intro Sequence..."); // Create the 'broken' sound instance here (it's loaded in IntroScene preload) this.brokenSound = this.sound.add('broken'); // Shake the screen (duration roughly matching sound, intensity 0.01 is a medium shake) const shakeDuration = 1500; // Adjust based on your broken.mp3 length this.cameras.main.shake(shakeDuration, 0.01); // Show the egg sprite if (this.eggSprite) this.eggSprite.setVisible(true); // Play the broken sound if (this.brokenSound) { this.brokenSound.play(); } // Set up the callback for when the sound finishes // Use a fixed duration fallback if sound duration is zero or sound is missing const soundDurationFallback = 1500; // Matches shake duration if (this.brokenSound && this.brokenSound.duration > 0) { this.brokenSound.once('complete', this.onBrokenSoundComplete, this); } else { console.warn("Broken sound not loaded or has zero duration, falling back to delayed call for sequence."); this.time.delayedCall(soundDurationFallback, this.onBrokenSoundComplete, [], this); // Use shake duration as fallback time } } // --- END startIntroSequence method --- // --- Callback when the broken sound finishes --- onBrokenSoundComplete() { console.log("Broken sound complete. Showing broken egg parts and blinking..."); // Hide the egg sprite and show the broken parts if (this.eggSprite) this.eggSprite.setVisible(false); if (this.part1Sprite) this.part1Sprite.setVisible(true); if (this.part2Sprite) this.part2Sprite.setVisible(true); // Destroy the broken sound instance after it has played if (this.brokenSound) { this.brokenSound.destroy(); this.brokenSound = null; // Clear reference } // Make parts blink const blinkDuration = 200; // Duration of one fade (in or out) in ms const blinkRepeat = 4; // Number of yoyo repeats (total 5 fade cycles: 1->0.5, 0.5->1, ..., ends at 1) const totalBlinkTime = (blinkRepeat + 1) * blinkDuration * 2; // Example: (4+1)*200*2 = 2000ms = 2 seconds // Ensure parts sprites exist before tweening if (this.part1Sprite && this.part2Sprite) { this.tweens.add({ targets: [this.part1Sprite, this.part2Sprite], alpha: { start: 1, to: 0.5 }, // Fade to 50% alpha duration: blinkDuration, yoyo: true, // Fade back to 1 repeat: blinkRepeat, // Repeat the yoyo cycle onComplete: () => { console.log("Blinking complete. Starting game..."); // Add a short pause before starting the game elements this.time.delayedCall(500, this.startGame, [], this); // 500ms pause after blinking }, onCompleteScope: this // Ensure onComplete scope is correct }); } else { // Fallback if parts sprites weren't created/found console.warn("Broken parts sprites not found, skipping blink sequence."); this.time.delayedCall(500, this.startGame, [], this); // Proceed to start game after a minimal pause } } // --- END onBrokenSoundComplete method --- // --- Method to initialize and start the actual gameplay --- startGame() { console.log("Starting actual game initialization..."); // --- Clean up Intro Sequence Sprites --- if (this.eggSprite) this.eggSprite.destroy(); if (this.part1Sprite) this.part1Sprite.destroy(); if (this.part2Sprite) this.part2Sprite.destroy(); this.eggSprite = null; this.part1Sprite = null; this.part2Sprite = null; // --- INITIALIZE Game Elements --- // Create player sprite (Dragon) // Position it at the world center initially this.player = this.physics.add.sprite( this.physics.world.bounds.width / 2, this.physics.world.bounds.height / 2, 'dragon_D1' // Uses first dragon frame (right oriented) ); this.player.setScale(0.125); // Keep original scaled size this.player.setCollideWorldBounds(true); // Player is now visible and active // Create physics groups this.balls = this.physics.add.group(); // Player projectiles this.bats = this.physics.add.group(); this.coins = this.physics.add.group(); this.bossBombs = this.physics.add.group(); // Boss projectiles // Define collisions/overlaps - Ensure groups/player exist before adding if (this.player && this.bats) this.physics.add.overlap(this.player, this.bats, this.playerHitBat, null, this); if (this.balls && this.bats) this.physics.add.overlap(this.balls, this.bats, this.ballHitBat, null, this); // Ball hits Bat if (this.player && this.coins) this.physics.add.overlap(this.player, this.coins, this.playerHitCoin, null, this); if (this.player && this.bossBombs) this.physics.add.overlap(this.player, this.bossBombs, this.playerHitBossBomb, null, this); // Player hits Boss Bomb // Boss overlaps (playerHitBoss, ballHitBoss) are added in spawnBoss() // Start camera follow AFTER player is created if(this.player) { this.cameras.main.startFollow(this.player); } else { console.warn("Player not created, camera will not follow."); } // Add UI (Hearts, Timer, Kills, Coins) this.heartGroup = this.add.group(); const heartOffsetX = 10; const heartOffsetY = 10; const heartSpacing = 5; const heartScale = 0.04; let currentX = heartOffsetX; for (let i = 0; i < this.maxPlayerHealth; i++) { let heart = this.heartGroup.create(currentX, heartOffsetY, 'heart'); heart.setScale(heartScale); heart.setScrollFactor(0); // Keep fixed on camera heart.setOrigin(0, 0); currentX += heart.displayWidth + heartSpacing; } // Position text UI elements relative to the camera/screen and fix them this.timerText = this.add.text(this.cameras.main.width - 10, 10, 'Temps: 02:00', { fontSize: '24px', fill: '#fff' }).setOrigin(1, 0).setScrollFactor(0); this.killedText = this.add.text(10, 40, 'Tués: 0', { fontSize: '24px', fill: '#fff' }).setScrollFactor(0); this.coinText = this.add.text(10, 70, 'Coins: 0', { fontSize: '24px', fill: '#ff0' }).setScrollFactor(0); // Reset stats this.playerHealth = this.maxPlayerHealth; // Reset player health to max this.killedCount = 0; this.coinCount = 0; this.wizardBossHits = 0; this.wizardBossRawHits = 0; // Update UI with initial stats (all hearts full, timer 2:00, etc.) this.updateHealthDisplay(); this.timerText.setText('Temps: 02:00'); // Initial display this.killedText.setText('Tués: ' + this.killedCount); this.coinText.setText('Coins: ' + this.coinCount); // --- FIX: Initialize sound instances using this.sound.add() --- // (They were loaded in IntroScene preload) console.log("Initializing sound instances in startGame..."); // Use add to create the sound instances in this scene this.backgroundMusic = this.sound.add('music', { loop: true, volume: 0.5 }); // Add options from Code 2 this.cashSound = this.sound.add('cash'); this.explosionSound = this.sound.add('explosion'); // --- END FIX --- // Start game timers this.timeElapsed = 0; // Reset this.gameTimerEvent = this.time.addEvent({ delay: 1000, callback: this.incrementTimer, callbackScope: this, loop: true }); this.playerAttackCooldown = 500; // Remove any existing timer first (shouldn't exist but safety) if (this.playerAttackTimer) { this.playerAttackTimer.remove(); } this.playerAttackTimer = this.time.addEvent({ delay: this.playerAttackCooldown, callback: this.fireBall, // Player fires Balls callbackScope: this, loop: true }); this.batSpawnDelay = 1500; this.batsPerSpawn = 1; // Remove any existing timer first if (this.batSpawnTimer) { this.batSpawnTimer.remove(); } // Bat spawn timer starts immediately but will stop when boss appears this.batSpawnTimer = this.time.addEvent({ delay: this.batSpawnDelay, callback: this.spawnBatWave, callbackScope: this, loop: true }); // Boss attack timer is null initially, starts in spawnBoss() this.bossAttackTimer = null; // Start background music now that the instance is correctly added if (this.backgroundMusic && !this.backgroundMusic.isPlaying) { console.log("Playing background music..."); this.backgroundMusic.play(); } else { console.warn("Background music not ready or already playing."); } // Game is now running! this.isGameRunning = true; // Ensure initial player state (tint, invincibility) - Check if player exists if(this.player) { this.player.setTint(0xffffff); // Reset tint this.isPlayerInvincible = false; if (this.invincibilityTimer) { this.invincibilityTimer.remove(); this.invinvincibilityTimer = null; } // Initialize player orientation/animation this.playerDirection = 'right'; this.player.setFrame('dragon_D1'); // Set default right idle frame (stops animation) } else { console.warn("Player does not exist, cannot initialize state."); // If player creation failed, maybe trigger game over immediately? // For now, just log warning and continue. this.time.delayedCall(100, () => this.gameOver(false), [], this); // Trigger game over if player is null return; // Stop startGame execution } console.log("Game initialized and running!"); } // --- END NEW startGame method --- update(time, delta) { // --- Only run game logic if game is running --- if (!this.isGameRunning) { return; // Stop update loop if game is not running } // --- END NEW --- // Ensure player exists before processing input/movement if (!this.player || !this.player.active) { // If player is gone but game is still marked as running, something is wrong. // This check helps prevent errors in the update loop if player is destroyed. console.warn("Player missing or inactive in update loop while game is running."); this.isGameRunning = false; // Mark game as not running to stop loop this.gameOver(false); // Trigger game over return; } // ************************************************************* // PLAYER MOVEMENT LOGIC (DRAGON) // ************************************************************* const cursorLeft = this.cursors.left.isDown || this.wasd.A.isDown; const cursorRight = this.cursors.right.isDown || this.wasd.D.isDown; const cursorUp = this.cursors.up.isDown || this.wasd.W.isDown; const cursorDown = this.cursors.down.isDown || this.wasd.S.isDown; let vx = 0; let vy = 0; let moving = false; // To check if player is pressing a movement key if (cursorLeft) { vx = -1; this.playerDirection = 'left'; // Update direction moving = true; } else if (cursorRight) { vx = 1; this.playerDirection = 'right'; // Update direction moving = true; } if (cursorUp) { vy = -1; moving = true; } else if (cursorDown) { vy = 1; moving = true; } const movementVector = new Phaser.Math.Vector2(vx, vy); if (moving) { // Use 'moving' to determine if a key is pressed movementVector.normalize().scale(this.speed); this.player.setVelocity(movementVector.x, movementVector.y); // Play animation based on stored direction if (this.playerDirection === 'left') { this.player.anims.play('fly_left', true); // Dragon left animation } else { // this.playerDirection === 'right' this.player.anims.play('fly_right', true); // Dragon right animation } } else { this.player.setVelocity(0); // Stop animation and set idle frame based on last direction if (this.player.anims.isPlaying) { this.player.anims.stop(); } if (this.playerDirection === 'left') { this.player.setFrame('dragon_D5'); // Dragon left idle frame } else { // this.playerDirection === 'right' this.player.setFrame('dragon_D1'); // Dragon right idle frame } } // ************************************************************* // END PLAYER MOVEMENT LOGIC // ************************************************************* // Rotate the player's balls - Ensure balls group exists if(this.balls) { this.balls.getChildren().forEach(ball => { ball.angle += this.ballRotationSpeed; // Increase angle for rotation }); } // Update UI - Ensure UI elements exist if (this.timerText) this.updateTimerDisplay(); if (this.killedText) this.killedText.setText('Tués: ' + this.killedCount); if (this.coinText) this.coinText.setText('Coins: ' + this.coinCount); // Bat logic (move towards player) - Ensure bats group and player exist if(this.bats && this.player) { this.bats.getChildren().forEach(bat => { // Only process active bats if (!bat.active) return; let directionX = this.player.x - bat.x; let directionY = this.player.y - bat.y; let length = Math.sqrt(directionX * directionX + directionY * directionY); if (length > 0) { // Speed increases slightly up to boss spawn time, then stops increasing // Added checks for gameDuration and bossSpawnTime being valid numbers > 0 let timeFactor = 0; if (this.gameDuration > 0) { timeFactor = Math.min(this.timeElapsed, this.bossSpawnTime) / this.gameDuration; } let speed = 100 + timeFactor * 100; bat.setVelocity(directionX / length * speed, directionY / length * speed); // Manage sprite flip based on horizontal direction if (directionX < 0) { bat.setFlipX(false); // BG is the non-flipped frame (oriented left) } else if (directionX > 0) { bat.setFlipX(true); // Flip BD to face right } } // Play fly animation if not already playing or stopped if (!bat.anims.isPlaying || bat.anims.currentAnim.key !== 'fly_bat') { bat.anims.play('fly_bat', true); } }); } // Wizard Boss Logic // Check if it's time to spawn the boss - Ensure game is running and bossSpawnTime is valid if (!this.wizardBoss && this.isGameRunning && this.bossSpawnTime > 0 && this.timeElapsed >= this.bossSpawnTime) { this.spawnBoss(); // This method handles the isGameRunning check internally } // If boss exists, make it move towards the player - Ensure player exists if (this.wizardBoss && this.wizardBoss.active && this.player && this.player.active) { let bossDirectionX = this.player.x - this.wizardBoss.x; let bossDirectionY = this.player.y - this.wizardBoss.y; let bossLength = Math.sqrt(bossDirectionX * bossDirectionX + bossDirectionY * bossDirectionY); if (bossLength > 0) { // Constant boss speed this.wizardBoss.setVelocity(bossDirectionX / bossLength * this.bossSpeed, bossDirectionY / bossLength * this.bossSpeed); // Manage boss animation based on horizontal direction if (bossDirectionX < 0) { this.wizardBoss.anims.play('wizard_walk_left', true); } else if (bossDirectionX > 0) { this.wizardBoss.anims.play('wizard_walk_right', true); } else { // Vertical movement only, keep horizontal orientation if (this.wizardBoss.anims.currentAnim) { if (this.wizardBoss.anims.currentAnim.key === 'wizard_walk_left') { this.wizardBoss.setFrame('wizard_W5'); // Left idle frame } else { // assume wizard_walk_right or was idle right this.wizardBoss.setFrame('wizard_W1'); // Right idle frame } this.wizardBoss.anims.stop(); } else { // If no animation was playing, choose a default frame this.wizardBoss.setFrame('wizard_W1'); } } } else { // Boss is exactly on the player, stop movement this.wizardBoss.setVelocity(0); if (this.wizardBoss.anims.isPlaying) { this.wizardBoss.anims.stop(); // Keep idle orientation based on last played anim if (this.wizardBoss.anims.currentAnim && this.wizardBoss.anims.currentAnim.key === 'wizard_walk_left') { this.wizardBoss.setFrame('wizard_W5'); } else { this.wizardBoss.setFrame('wizard_W1'); } } } } // Check game end conditions // Check player health first if (this.playerHealth <= 0) { // game over logic is in gameOver, this just triggers it if(this.isGameRunning) { // Prevent calling multiple times console.log("Player health <= 0. Triggering game over."); this.isGameRunning = false; // Mark as finished immediately this.gameOver(false); // Player died } } // Then check timer else if (this.gameDuration > 0 && this.timeElapsed >= this.gameDuration) { // Ensure gameDuration is positive // game over logic is in gameOver, this just triggers it if(this.isGameRunning) { // Prevent calling multiple times console.log("Time elapsed >= game duration. Triggering game over."); this.isGameRunning = false; // Mark as finished immediately // Check if boss was defeated for survival condition let survived = (this.wizardBoss === null || !this.wizardBoss.active); this.gameOver(survived); // Survival successful if boss is gone } } // Note: Boss defeat calls destroyBoss(), which sets wizardBoss to null. // The timer check will then eventually pass and call gameOver(true). } // End update() // --- Game Methods --- gameOver(survived) { console.log("Game Over. Survived:", survived); // Stop timers - Check if they exist before removing if (this.gameTimerEvent) { this.gameTimerEvent.remove(); this.gameTimerEvent = null; } if (this.playerAttackTimer) { this.playerAttackTimer.remove(); this.playerAttackTimer = null; } if (this.batSpawnTimer) { this.batSpawnTimer.remove(); this.batSpawnTimer = null; } if (this.invincibilityTimer) { this.invincibilityTimer.remove(); this.invincibilityTimer = null; } if (this.bossAttackTimer) { this.bossAttackTimer.remove(); this.bossAttackTimer = null; } // Stop player - Check if player exists if (this.player) { this.player.setVelocity(0, 0); this.player.anims.stop(); // Optional: Hide player this.player.setVisible(false); this.player.setActive(false); // Destroy the player sprite this.player.destroy(); this.player = null; // Clear reference } // Hide/Clear UI - Check if groups/elements exist if(this.heartGroup) { this.heartGroup.setVisible(false); // Destroy all heart sprites this.heartGroup.clear(true, true); this.heartGroup = null; // Clear reference } // Hide and destroy text UI if(this.timerText) { this.timerText.destroy(); this.timerText = null; } if(this.killedText) { this.killedText.destroy(); this.killedText = null; } if(this.coinText) { this.coinText.destroy(); this.coinText = null; } // Clear groups - Check if groups exist before clearing if (this.bats) { this.bats.clear(true, true); this.bats = null; } if (this.balls) { this.balls.clear(true, true); this.balls = null; } if (this.coins) { this.coins.clear(true, true); this.coins = null; } if (this.bossBombs) { this.bossBombs.clear(true, true); this.bossBombs = null; } // Handle boss if exists if (this.wizardBoss) { // Stop/Hide/Destroy even if not active if (this.wizardBoss.active) { this.wizardBoss.setVelocity(0, 0); this.wizardBoss.anims.stop(); this.wizardBoss.setVisible(false); this.wizardBoss.setActive(false); } this.wizardBoss.destroy(); this.wizardBoss = null; // Set null explicitly } // Stop intro sequence elements if they somehow still exist (shouldn't happen if startGame was called) if (this.eggSprite) { this.eggSprite.destroy(); this.eggSprite = null; } if (this.part1Sprite) { this.part1Sprite.destroy(); this.part1Sprite = null; } if (this.part2Sprite) { this.part2Sprite.destroy(); this.part2Sprite = null; } if (this.brokenSound) { this.brokenSound.destroy(); this.brokenSound = null; } // Destroy broken sound if exists // Stop background music - Check if instance exists and is playing if (this.backgroundMusic && this.backgroundMusic.isPlaying) { console.log("Stopping background music."); this.backgroundMusic.stop(); // Optional: Destroy sound instances on game over? Can be good for memory. this.backgroundMusic.destroy(); this.backgroundMusic = null; } // Destroy other sound instances too if (this.cashSound) { this.cashSound.destroy(); this.cashSound = null; } if (this.explosionSound) { this.explosionSound.destroy(); this.explosionSound = null; } // Reset isGameRunning state for potential future restarts this.isGameRunning = false; // Transition to IntroScene with results this.scene.start('IntroScene', { survived: survived, killedCount: this.killedCount, coinCount: this.coinCount }); } playerHitBat(player, bat) { // Ensure objects exist and are active, and game is running, and player is not invincible if (this.isGameRunning && player && player.active && bat && bat.active && !this.isPlayerInvincible) { this.playerHealth--; this.updateHealthDisplay(); // Update heart icons if (this.playerHealth <= 0) { this.gameOver(false); // End game } else { // Check if bat exists before destroying if (bat && bat.active) bat.destroy(); this.activateInvincibility(); // Grant invincibility } } else if (bat && bat.active) { // If game not running or player invincible/inactive, destroy bat anyway to clean up bat.destroy(); } } playerHitBoss(player, boss) { // Note: The boss doesn't take damage from this, only the player. // Ensure objects exist and are active, and game is running, and player is not invincible if (this.isGameRunning && player && player.active && boss && boss.active && !this.isPlayerInvincible) { // Boss deals damage to player this.playerHealth -= 2; // Example: lose 2 health points per boss hit this.updateHealthDisplay(); // Update heart icons if (this.playerHealth <= 0) { this.gameOver(false); // End game } else { // Boss is NOT destroyed, it continues attacking this.activateInvincibility(); // Apply invincibility to player } } } playerHitBossBomb(player, bomb) { // Method for collision between player (Dragon) and Boss Bomb (Wizard projectile) // Ensure objects exist and are active, and game is running, and player is not invincible if (this.isGameRunning && player && player.active && bomb && bomb.active && !this.isPlayerInvincible) { this.playerHealth -= 1; // Boss bombs deal 1 damage this.updateHealthDisplay(); // Update heart icons // Ensure bomb exists and is active before destroying it if(bomb && bomb.active) { bomb.destroy(); } if (this.playerHealth <= 0) { this.gameOver(false); // End game } else { this.activateInvincibility(); // Apply invincibility to player } } else if (bomb && bomb.active) { // Destroy bomb even if player is invincible/inactive or game ended bomb.destroy(); } } activateInvincibility() { // Ensure player exists and is active if (!this.player || !this.player.active) return; this.isPlayerInvincible = true; this.player.setTint(0xff0000); // Red tint // Ensure previous tweens are stopped if (this.player.existingTween) { this.player.existingTween.stop(); } const flashTween = this.tweens.add({ targets: this.player, alpha: { start: 1, to: 0.5 }, duration: 100, yoyo: true, repeat: Math.floor(this.invincibilityDuration / 100) - 1, // Repeat enough times for the duration, use floor onComplete: () => { // Ensure player still exists before setting alpha/clearing tint if(this.player && this.player.active) { this.player.setAlpha(1); // Ensure full alpha at the end // Tint is reset by invincibilityTimer } this.player.existingTween = null; // Clear reference }, onCompleteScope: this }); this.player.existingTween = flashTween; // Store tween reference if (this.invincibilityTimer) { this.invincibilityTimer.remove(); } this.invincibilityTimer = this.time.addEvent({ delay: this.invincibilityDuration, callback: () => { // Only remove invincibility if game is still running and player is active if(this.isGameRunning && this.player && this.player.active) { this.isPlayerInvincible = false; this.player.setTint(0xffffff); // Reset tint } else if (this.player && this.player.active){ // If game ended but player still active, reset tint this.player.setTint(0xffffff); } this.invincibilityTimer = null; }, callbackScope: this }); } ballHitBat(ball, bat) { // Ensure objects exist and are active if (!ball || !bat || !ball.active || !bat.active) return; const batX = bat.x; const batY = bat.y; // Ensure objects exist before destroying if (ball && ball.active) ball.destroy(); if (bat && bat.active) bat.destroy(); // Play sound - Check if instance exists if (this.explosionSound) { this.explosionSound.play(); } else { console.warn("Explosion sound not initialized."); } // Only spawn coin if coins group exists if(this.coins) { let coin = this.coins.create(batX, batY, 'coin'); coin.setScale(0.1); coin.setCollideWorldBounds(false); // Destroy coin after a delay to prevent infinite coins this.time.delayedCall(10000, coin.destroy, [], coin); // Destroy after 10 seconds } else { console.warn("Coins group not initialized, cannot spawn coin."); } // Only count kill if game is running if(this.isGameRunning) { this.killedCount++; } } // Method for collision between player Ball and Boss ballHitBoss(ball, boss) { // Ensure objects exist and are active, and game is running if (!this.isGameRunning || !ball || !boss || !ball.active || !boss.active) return; // Détruit la balle du joueur (elle est toujours consommée) if (ball && ball.active) { ball.destroy(); } else { return; // Stop if ball is already gone } // Incrémente le compteur de coups bruts reçus this.wizardBossRawHits++; // console.log(`Boss received a hit. Total received: ${this.wizardBossRawHits}`); // Keep commented for cleaner console // Vérifie si ce coup est le 2ème, 4ème, 6ème, etc. (un coup sur deux) if (this.wizardBossRawHits % 2 === 0) { // Ce coup cause des dégâts au boss this.wizardBossHits++; // Incrémente les coups qui causent des dégâts // console.log(`Boss took damage! Damage hits: ${this.wizardBossHits}`); // Keep commented // Optional: Add visual feedback for taking damage (like tinting) - Check if boss exists if (boss && boss.active) { boss.setTint(0xff0000); this.time.delayedCall(100, () => { if(boss && boss.active) boss.setTint(0xffffff); }, [], this); } // Vérifie si le boss est vaincu *seulement* après un coup qui cause des dégâts if (this.wizardBossHits >= this.wizardBossMaxHits) { this.destroyBoss(); // This method checks if boss is active } } else { // Ce coup ne cause PAS de dégâts (la balle est absorbée) // console.log("Boss resisted the hit!"); // Keep commented // Optional: Add visual feedback for resisting damage (e.g., white flash) - Check if boss exists if (boss && boss.active) { boss.setTint(0xffffff); // White tint this.time.delayedCall(100, () => { if(boss && boss.active) boss.setTint(0xffffff); }, [], this); // Reset tint quickly } } } playerHitCoin(player, coin) { // Ensure objects exist and are active if (!player || !coin || !player.active || !coin.active) return; // Basic check // Ensure coin exists before destroying if (coin && coin.active) coin.destroy(); // Play sound - Check if instance exists if (this.cashSound) { this.cashSound.play(); } else { console.warn("Cash sound not initialized."); } // Only count coin if game is running if(this.isGameRunning) { this.coinCount++; } } // Method for the Player (Dragon) to fire a Ball fireBall() { // Only fire if game is running and player/balls group are active/exist if (!this.isGameRunning || !this.player || !this.player.active || !this.balls) return; let ball = this.balls.create(this.player.x, this.player.y, 'ball'); // Ensure ball was created before configuring/using it if (!ball) { console.warn("Failed to create player ball."); return; } // Divide ball size by three (0.25 / 3 ≈ 0.083) ball.setScale(0.083); // Reduced scale ball.setCollideWorldBounds(false); // Balls can go off-screen let angle = Phaser.Math.Between(0, 360); // Reduced ball speed let vec = this.physics.velocityFromAngle(angle, this.ballSpeed); // Reduced speed ball.setVelocity(vec.x, vec.y); // Destroy the ball after a certain time (prevents infinite balls) this.time.delayedCall(3000, ball.destroy, [], ball); } // Method for the Boss (Wizard) to fire a Bomb fireBossBomb() { // Only fire if game is running, boss/player are active, and bossBombs group exists if (!this.isGameRunning || !this.wizardBoss || !this.wizardBoss.active || !this.player || !this.player.active || !this.bossBombs) return; let bomb = this.bossBombs.create(this.wizardBoss.x, this.wizardBoss.y, 'bomb'); // Ensure bomb was created before configuring/using it if (!bomb) { console.warn("Failed to create boss bomb."); return; } bomb.setScale(0.15); // Adjust scale for bomb projectile bomb.setCollideWorldBounds(false); // Bombs can go off-screen // Calculate direction towards the player let directionX = this.player.x - bomb.x; let directionY = this.player.y - bomb.y; let length = Math.sqrt(directionX * directionX + directionY * directionY); if (length > 0) { let speed = 300; // Speed for boss bombs bomb.setVelocity(directionX / length * speed, directionY / length * speed); } else { // If exactly on player, fire straight up or in a default direction bomb.setVelocity(0, -300); // Example: fire upwards at speed 300 } // Destroy the bomb after a certain time (prevents infinite bombs) this.time.delayedCall(4000, bomb.destroy, [], bomb); // Adjust duration as needed } spawnBatWave() { // Don't spawn bats if game is not running, or boss has appeared, or bats group doesn't exist // Also ensure batSpawnTimer exists if this is called by the timer if (!this.isGameRunning || this.timeElapsed >= this.bossSpawnTime || !this.bats || !this.batSpawnTimer) return; let elapsedMinutes = this.timeElapsed / 60; let currentSpawnDelay = Math.max(500, this.batSpawnDelay - elapsedMinutes * 100); let currentBatsPerSpawn = Math.floor(this.batsPerSpawn + elapsedMinutes * 0.75); currentBatsPerSpawn = Math.max(1, currentBatsPerSpawn); // Update the existing timer delay. Timer is created in startGame(). this.batSpawnTimer.delay = currentSpawnDelay; for (let i = 0; i < currentBatsPerSpawn; i++) { this.spawnSingleBat(); // This method checks isGameRunning and bats group } } spawnSingleBat() { // Don't spawn if game is not running or boss has appeared or bats group doesn't exist if (!this.isGameRunning || this.timeElapsed >= this.bossSpawnTime || !this.bats) return; let x, y; let camera = this.cameras.main; let spawnPadding = 150; // Spawn outside visible screen // Ensure camera worldView is available (it is after camera bounds are set) if (!camera || !camera.worldView) { console.warn("Camera or worldview not available, cannot spawn bat."); return; } let worldLeft = camera.worldView.left; let worldRight = camera.worldView.right; let worldTop = camera.worldView.top; let worldBottom = camera.worldView.bottom; // Ensure world bounds are valid if (worldLeft >= worldRight || worldTop >= worldBottom) { console.warn("Invalid camera worldview dimensions, cannot spawn bat."); return; } let side = Phaser.Math.Between(0, 3); // 0:Top, 1:Bottom, 2:Left, 3:Right switch (side) { case 0: // Top (above camera view) x = Phaser.Math.Between(worldLeft, worldRight); y = worldTop - spawnPadding; break; case 1: // Bottom (below camera view) x = Phaser.Math.Between(worldLeft, worldRight); y = worldBottom + spawnPadding; break; case 2: // Left (left of camera view) x = worldLeft - spawnPadding; y = Phaser.Math.Between(worldTop, worldBottom); break; case 3: // Right (right of camera view) x = worldRight + spawnPadding; y = Phaser.Math.Between(worldTop, worldBottom); break; default: // Should not happen x = worldLeft - spawnPadding; y = worldTop - spawnPadding; console.warn("Invalid spawn side calculated."); break; } let bat = this.bats.create(x, y, 'bat_BG'); // Ensure bat was created before configuring if (!bat) { console.warn("Failed to create bat sprite."); return; } bat.setScale(0.25); bat.setCollideWorldBounds(false); // 'fly_bat' animation is played in update() for each bat // Destroy bat after a very long time to prevent runaway memory use this.time.delayedCall(60000, bat.destroy, [], bat); // Destroy after 60 seconds } // Method to spawn the boss spawnBoss() { // Only spawn if game is running and boss doesn't exist yet and player exists if (!this.isGameRunning || this.wizardBoss || !this.player) return; console.log("Boss Wizard appears!"); // Spawn randomly outside the camera view let x, y; let camera = this.cameras.main; let spawnPadding = 200; // Larger padding for the boss // Ensure camera worldView is available if (!camera || !camera.worldView) { console.warn("Camera or worldview not available, cannot spawn boss."); return; } let worldLeft = camera.worldView.left; let worldRight = camera.worldView.right; let worldTop = camera.worldView.top; let worldBottom = camera.worldView.bottom; // Ensure world bounds are valid if (worldLeft >= worldRight || worldTop >= worldBottom) { console.warn("Invalid camera worldview dimensions, cannot spawn boss."); return; } let side = Phaser.Math.Between(0, 3); // 0:Top, 1:Bottom, 2:Left, 3:Right switch (side) { case 0: x = Phaser.Math.Between(worldLeft, worldRight); y = worldTop - spawnPadding; break; case 1: x = Phaser.Math.Between(worldLeft, worldRight); y = worldBottom + spawnPadding; break; case 2: x = worldLeft - spawnPadding; y = Phaser.Math.Between(worldTop, worldBottom); break; case 3: x = worldRight + spawnPadding; y = Phaser.Math.Between(worldTop, worldBottom); break; default: // Should not happen x = worldLeft - spawnPadding; y = worldTop - spawnPadding; console.warn("Invalid boss spawn side calculated."); break; } this.wizardBoss = this.physics.add.sprite(x, y, 'wizard_W1'); // Uses right idle frame // Ensure boss sprite was created if (!this.wizardBoss) { console.warn("Failed to create boss sprite."); return; } // Halve the boss size (original 0.4 / 2 = 0.2) this.wizardBoss.setScale(0.2); this.wizardBoss.setCollideWorldBounds(true); // Boss bounces off world bounds // Add overlaps related to the boss // Ensure player and balls group exist before adding overlaps if (this.player) { this.physics.add.overlap(this.player, this.wizardBoss, this.playerHitBoss, null, this); // Player hit by boss body } if (this.balls) { this.physics.add.overlap(this.balls, this.wizardBoss, this.ballHitBoss, null, this); // Player ball hits boss } // Overlap between boss and player projectiles (bombs) will be handled by playerHitBossBomb check // Reset boss hits this.wizardBossHits = 0; // Reset damage hits this.wizardBossRawHits = 0; // Reset raw hits count // Start boss attack timer - Check if timer exists before removing/creating if (this.bossAttackTimer) { this.bossAttackTimer.remove(); } this.bossAttackTimer = this.time.addEvent({ delay: this.bossAttackCooldown, callback: this.fireBossBomb, // Boss fires Bombs callbackScope: this, loop: true }); // Optional: Display appearance message - Ensure camera is available if (this.cameras && this.cameras.main) { let bossWarningText = this.add.text(this.cameras.main.width / 2, this.cameras.main.height / 2, "Le Magicien Enragé Apparaît !", { fontSize: '48px', fill: '#f00', align: 'center' }).setOrigin(0.5).setScrollFactor(0); this.tweens.add({ targets: bossWarningText, alpha: { start: 1, to: 0 }, duration: 3000, ease: 'Quad.easeOut', onComplete: () => { if (bossWarningText) bossWarningText.destroy(); } }); } // Stop bat spawn timer when boss appears (check if timer exists) if (this.batSpawnTimer) { this.batSpawnTimer.remove(); this.batSpawnTimer = null; } // Existing bats remain, but no new ones spawn. } // Method to handle boss defeat destroyBoss() { // Ensure boss exists and is active if (!this.wizardBoss || !this.wizardBoss.active) return; console.log("Boss Wizard defeated!"); // Stop boss movement and animation this.wizardBoss.setVelocity(0, 0); this.wizardBoss.anims.stop(); // Play a death animation or effect this.tweens.add({ targets: this.wizardBoss, alpha: { start: 1, to: 0 }, scale: { start: this.wizardBoss.scale, to: this.wizardBoss.scale * 1.5 }, // Grow slightly as it fades angle: { start: this.wizardBoss.angle, to: this.wizardBoss.angle + 360 }, // Spin as it fades duration: 1000, ease: 'Quad.easeOut', onComplete: () => { if(this.wizardBoss) { // Check again before destroying this.wizardBoss.destroy(); // Finally destroy the boss sprite this.wizardBoss = null; // Set reference to null } }, onCompleteScope: this }); // Stop boss attack timer and clear bombs (check if timer/group exist) if (this.bossAttackTimer) { this.bossAttackTimer.remove(); this.bossAttackTimer = null; } if (this.bossBombs) { this.bossBombs.clear(true, true); // Clear any active boss bombs } // The game doesn't end *immediately* on boss death, but surviving the timer after boss death is the win condition. // The update loop handles the timer check and calls gameOver(true) if time runs out and boss is gone (wizardBoss is null). } incrementTimer() { // Only increment if game is running and timer event exists if(this.isGameRunning && this.gameTimerEvent) { this.timeElapsed++; // console.log("Time elapsed:", this.timeElapsed); // Debug timer } } updateTimerDisplay() { // Ensure timerText exists if (!this.timerText) return; let remainingTime = Math.max(0, this.gameDuration - this.timeElapsed); let minutes = Math.floor(remainingTime / 60); let seconds = remainingTime % 60; let secondsFormatted = seconds < 10 ? '0' + seconds : seconds; this.timerText.setText(`Temps: ${minutes}:${secondsFormatted}`); } updateHealthDisplay() { // Ensure heartGroup exists if (!this.heartGroup) return; const hearts = this.heartGroup.getChildren(); for (let i = 0; i < this.maxPlayerHealth; i++) { const heart = hearts[i]; // Add check if heart sprite exists at this index if (!heart) { console.warn(`Heart sprite at index ${i} is missing.`); continue; } if (i < this.playerHealth) { heart.setTexture('heart'); } else { heart.setTexture('emptyheart'); } } } } // --- Game Configuration --- // Add scenes to config config.scene = [IntroScene, CinematicScene, GameScene]; const game = new Phaser.Game(config); </script> </body> </html>