```git commit message

Implement wave-based enemy system with shooting enemies, improved visuals, and sound effects

- Added enemy bullet system with dedicated group and canvas texture
- Implemented wave progression system (3 waves) with increasing difficulty
- Enhanced enemy spawning with wave-specific properties and shooting mechanics
- Added player explosion animation on death with sound effect
- Included wave start notifications with scaling text animation
- Improved background scrolling and enemy off-screen cleanup
- Integrated new sound assets for enemy shooting, player death, and wave transitions
- Updated game scene with proper texture handling for different enemy types across waves

This commit transforms the basic shooter into a structured wave-based gameplay experience with escalating challenge levels.
```
This commit is contained in:
Brian Fertig 2025-08-03 20:00:31 -06:00
parent 8a7b321872
commit bd00c62205
9 changed files with 190 additions and 18 deletions

BIN
assets/enemy-sprite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

BIN
assets/sounds/next-wave.mp3 Normal file

Binary file not shown.

Binary file not shown.

View File

@ -17,6 +17,8 @@
} }
#game-container { #game-container {
position: relative; position: relative;
}
#game-container canvas {
cursor: none; cursor: none;
} }
</style> </style>

BIN
raw/enemies.psd Normal file

Binary file not shown.

Binary file not shown.

View File

@ -9,12 +9,19 @@ export class GameScene extends Phaser.Scene {
this.enemies = null; this.enemies = null;
this.bullets = null; this.bullets = null;
// Enemy bullets group
this.enemyBullets = null;
// Game state // Game state
this.score = 0; this.score = 0;
this.gameOver = false; this.gameOver = false;
// Mouse reticle // Mouse reticle
this.reticle = null; this.reticle = null;
// Wave system
this.waveTimer = 0;
this.currentWave = 0;
} }
preload() { preload() {
@ -34,11 +41,19 @@ export class GameScene extends Phaser.Scene {
frameWidth: 48, frameWidth: 48,
frameHeight: 48 frameHeight: 48
}); });
this.load.spritesheet('enemy-sprite', 'assets/enemy-sprite.png', {
frameWidth: 48,
frameHeight: 48
});
// Load background music and sounds // Load background music and sounds
this.load.audio('bgm', 'assets/music/NeonPulse.mp3'); this.load.audio('bgm', 'assets/music/NeonPulse.mp3');
this.load.audio('main-gun', 'assets/sounds/main-gun.mp3'); this.load.audio('main-gun', 'assets/sounds/main-gun.mp3');
this.load.audio('enemy-kill', 'assets/sounds/enemy-kill.mp3'); this.load.audio('enemy-kill', 'assets/sounds/enemy-kill.mp3');
this.load.audio('enemy-shoot', 'assets/sounds/enemy-shoot.mp3');
this.load.audio('player-death', 'assets/sounds/player-death.mp3');
this.load.audio('enemy-shoot', 'assets/sounds/enemy-shoot.mp3');
this.load.audio('next-wave', 'assets/sounds/next-wave.mp3');
// Create simple placeholder graphics for now // Create simple placeholder graphics for now
this.createPlaceholderGraphics(); this.createPlaceholderGraphics();
@ -57,11 +72,13 @@ export class GameScene extends Phaser.Scene {
// Create groups for bullets and enemies // Create groups for bullets and enemies
this.bullets = this.physics.add.group(); this.bullets = this.physics.add.group();
this.enemyBullets = this.physics.add.group();
this.enemies = this.physics.add.group(); this.enemies = this.physics.add.group();
// Setup collision detection // Setup collision detection
this.physics.add.overlap(this.bullets, this.enemies, this.hitEnemy, null, this); this.physics.add.overlap(this.bullets, this.enemies, this.hitEnemy, null, this);
this.physics.add.overlap(this.player.sprite, this.enemies, this.hitPlayer, null, this); this.physics.add.overlap(this.player.sprite, this.enemies, this.hitPlayer, null, this);
this.physics.add.overlap(this.player.sprite, this.enemyBullets, this.hitPlayer, null, this);
// Start enemy spawning // Start enemy spawning
this.time.addEvent({ this.time.addEvent({
@ -94,18 +111,31 @@ export class GameScene extends Phaser.Scene {
//this.bgSpeed3 = 0.8; //this.bgSpeed3 = 0.8;
} }
update() { update(time, delta) {
if (this.gameOver) return; if (this.gameOver) return;
// Update background positions for parallax effect // Update background positions for parallax effect
this.updateBackgrounds(); this.updateBackgrounds();
// Update wave timer
this.waveTimer += delta;
// Check for wave changes every 30 seconds (30,000 milliseconds)
if (this.waveTimer >= 5000 && this.currentWave === 0) {
this.startWave(1);
} else if (this.waveTimer >= 10000 && this.currentWave === 1) {
this.startWave(2);
}
// Get A/D keys for player movement // Get A/D keys for player movement
const aKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A); const aKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A);
const dKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D); const dKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D);
// Update player with A/D controls // Update player with A/D controls
this.player.update(aKey, dKey); this.player.update(aKey, dKey);
// Check if enemies are off-screen and destroy them
this.checkEnemiesOffScreen();
} }
updateBackgrounds() { updateBackgrounds() {
@ -115,7 +145,6 @@ export class GameScene extends Phaser.Scene {
//this.bgLayer3.x -= this.bgSpeed3; //this.bgLayer3.x -= this.bgSpeed3;
// Reset positions to create continuous scrolling effect // Reset positions to create continuous scrolling effect
console.log(this.bgLayer1.y);
if (this.bgLayer1.y > 736) { if (this.bgLayer1.y > 736) {
this.bgLayer1.y = 30; // Width of game + layer width this.bgLayer1.y = 30; // Width of game + layer width
} }
@ -157,18 +186,6 @@ export class GameScene extends Phaser.Scene {
canvas.width = 32; canvas.width = 32;
canvas.height = 32; canvas.height = 32;
// // Player ship (triangle)
// const ctx = canvas.getContext('2d');
// ctx.fillStyle = '#00ffcc';
// ctx.beginPath();
// ctx.moveTo(16, 5);
// ctx.lineTo(5, 30);
// ctx.lineTo(27, 30);
// ctx.closePath();
// ctx.fill();
// this.textures.addCanvas('player', canvas);
// Enemy (square) // Enemy (square)
const enemyCanvas = document.createElement('canvas'); const enemyCanvas = document.createElement('canvas');
enemyCanvas.width = 24; enemyCanvas.width = 24;
@ -192,6 +209,19 @@ export class GameScene extends Phaser.Scene {
bCtx.fill(); bCtx.fill();
this.textures.addCanvas('bullet', bulletCanvas); this.textures.addCanvas('bullet', bulletCanvas);
// Enemy Bullet (small circle)
const enemyBulletCanvas = document.createElement('canvas');
enemyBulletCanvas.width = 8;
enemyBulletCanvas.height = 8;
const ebCtx = enemyBulletCanvas.getContext('2d');
ebCtx.fillStyle = '#00ffcc';
ebCtx.beginPath();
ebCtx.arc(4, 4, 3, 0, Math.PI * 2);
ebCtx.fill();
this.textures.addCanvas('enemyBullet', enemyBulletCanvas);
} }
createUI() { createUI() {
@ -238,15 +268,53 @@ export class GameScene extends Phaser.Scene {
spawnEnemy() { spawnEnemy() {
// Spawn enemies at random x position within game width (800px) // Spawn enemies at random x position within game width (800px)
const x = Phaser.Math.Between(20, 780); const x = Phaser.Math.Between(20, 580);
const enemy = this.enemies.create(x, 0, 'enemy');
// Calculate texture range with minimum of 0
const minTexture = Math.max(0, this.currentWave - 2);
const maxTexture = this.currentWave;
// Ensure we don't get negative values
const texture = Phaser.Math.Between(minTexture, maxTexture);
const enemy = this.enemies.create(x, 0, 'enemy-sprite', texture);
// Randomize enemy speed // Randomize enemy speed
const speed = Phaser.Math.Between(50, 150); const speed = Phaser.Math.Between(50, 150);
enemy.setVelocityY(speed); enemy.setVelocityY(speed);
// Add some rotation for visual effect // Add some rotation for visual effect
enemy.setRotation(Phaser.Math.FloatBetween(-0.2, 0.2)); //enemy.setRotation(Phaser.Math.FloatBetween(-0.2, 0.2));
// Apply wave-specific properties
this.applyWaveProperties(enemy, texture);
}
applyWaveProperties(enemy, texture) {
// Set the current wave number on the enemy
enemy.waveNumber = this.currentWave;
// Wave 1: Basic enemies (no shooting)
if (this.currentWave === 0) {
enemy.canShoot = false;
if (enemy.shootTimer) {
enemy.shootTimer.remove();
enemy.shootTimer = null;
}
}
// Wave 2: Enemies that can shoot
else if (this.currentWave >= 1 && texture > 0) {
enemy.canShoot = true;
// Only set up shooting timer if it doesn't exist yet
if (!enemy.shootTimer) {
enemy.shootTimer = this.time.addEvent({
delay: Phaser.Math.Between(1000, 3000),
callback: () => this.enemyShoot(enemy, texture),
callbackScope: this,
loop: true
});
}
}
} }
hitEnemy(bullet, enemy) { hitEnemy(bullet, enemy) {
@ -259,8 +327,37 @@ export class GameScene extends Phaser.Scene {
} }
hitPlayer(player, enemy) { hitPlayer(player, enemy) {
// Create explosion at player's position
const explosion = this.add.sprite(this.player.sprite.x, this.player.sprite.y, 'player-sprite', 3);
const explosion2 = this.add.sprite(this.player.sprite.x, this.player.sprite.y, 'player-sprite', 3);
// Add rotation and scaling animation
this.tweens.add({
targets: explosion,
scale: 3, // Scale up to 3x
rotation: Math.PI * 2, // Full rotation
alpha: 0.5, // Fade out slightly
duration: 1000,
ease: 'Power2',
onComplete: () => {
explosion.destroy();
}
});
this.tweens.add({
targets: explosion2,
scale: 3, // Scale up to 3x
rotation: -Math.PI * 2, // Full rotation
alpha: 0.5, // Fade out slightly
duration: 1000,
ease: 'Power2',
onComplete: () => {
explosion2.destroy();
}
});
player.destroy(); player.destroy();
enemy.destroy(); enemy.destroy();
this.sound.play('player-death');
this.gameOver = true; this.gameOver = true;
@ -273,8 +370,81 @@ export class GameScene extends Phaser.Scene {
gameOverText.setOrigin(0.5); gameOverText.setOrigin(0.5);
// Restart game after delay // Restart game after delay
// this.time.delayedCall(2000, () => {
// this.scene.restart();
// });
}
startWave(waveNumber) {
this.currentWave = waveNumber;
this.sound.play('next-wave');
// Add wave indicator text
const waveText = this.add.text(300, 150, `WAVE ${waveNumber + 1}`, {
fontSize: '48px',
fill: '#ffff00',
fontFamily: 'Arial'
});
waveText.setOrigin(0.5);
// Add scaling animation
this.tweens.add({
targets: waveText,
scale: 2, // Grow to double size
duration: 1000,
ease: 'Elastic.out', // Elastic easing for bouncy effect
yoyo: true, // Scale back and forth
repeat: 1 // Do the animation twice (total of 3 states)
});
// Remove text after delay
this.time.delayedCall(2000, () => { this.time.delayedCall(2000, () => {
this.scene.restart(); waveText.destroy();
});
// Apply wave properties to all existing enemies
this.enemies.children.iterate((enemy) => {
this.applyWaveProperties(enemy);
});
}
enemyShoot(enemy, texture) {
// Only shoot if enemy can shoot and isn't destroyed
if (!enemy.canShoot || !enemy.active) return;
console.log(texture);
// Check if this is wave 3 and enemy has texture 2 (special shooting)
if (this.currentWave >= 2 && enemy.waveNumber >= 2 && texture >= 2) {
// Shoot toward player
const angle = Phaser.Math.Angle.Between(
enemy.x,
enemy.y,
this.player.sprite.x,
this.player.sprite.y
);
const bullet = this.enemyBullets.create(enemy.x, enemy.y, 'enemyBullet');
const speed = 200;
bullet.setVelocityX(Math.cos(angle) * speed);
bullet.setVelocityY(Math.sin(angle) * speed);
bullet.setScale(1.5);
} else {
// Regular straight-down shooting
const bullet = this.enemyBullets.create(enemy.x, enemy.y, 'enemyBullet');
bullet.setVelocityY(300); // Move straight down at 200px/s
bullet.setScale(1.5);
}
// Sound effect for enemy shooting
this.sound.play('enemy-shoot');
}
checkEnemiesOffScreen() {
// Destroy enemies that have gone below the screen
this.enemies.children.iterate((enemy) => {
if (enemy && enemy.y > 800) { // Adjust 600 to your game height as needed
enemy.destroy();
}
}); });
} }
} }