Add audio assets and integrate sound effects with enhanced visual feedback

This commit is contained in:
Brian Fertig 2026-03-08 11:19:16 -06:00
parent b17cb86c55
commit 4bcbfbd9c9
28 changed files with 178 additions and 26 deletions

Binary file not shown.

BIN
assets/fx/ArrowPing.mp3 Normal file

Binary file not shown.

BIN
assets/fx/Death.mp3 Normal file

Binary file not shown.

BIN
assets/fx/EdgeRemove.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/fx/EnemyWave.mp3 Normal file

Binary file not shown.

BIN
assets/fx/NewLife.mp3 Normal file

Binary file not shown.

BIN
assets/fx/Shoot.mp3 Normal file

Binary file not shown.

BIN
assets/fx/ShooterShoot.mp3 Normal file

Binary file not shown.

BIN
assets/fx/SprayerShoot.mp3 Normal file

Binary file not shown.

BIN
assets/fx/TakeDamage.mp3 Normal file

Binary file not shown.

Binary file not shown.

View File

@ -5,6 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Overrun</title>
<style>
@font-face {
font-family: 'FutureImperfect';
src: url('assets/fonts/FutureImperfect-2gmo.otf') format('opentype');
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #000;

View File

@ -25,6 +25,7 @@ export class Player {
this.hp = this.stats.maxHp;
this.lives = 3;
this.invincible = false;
this._dead = false;
this._fireCooldown = 0;
this._baseFireInterval = 250; // ms between shots
this._isMoving = false;
@ -40,10 +41,10 @@ export class Player {
const anims = this.scene.anims;
if (anims.exists('player-idle')) return; // already created
anims.create({ key: 'player-idle', frames: [{ key: 'player', frame: 0 }], frameRate: 1, repeat: -1 });
anims.create({ key: 'player-walk', frames: anims.generateFrameNumbers('player', { frames: [1, 2] }), frameRate: 8, repeat: -1 });
anims.create({ key: 'player-idle-gun', frames: [{ key: 'player', frame: 3 }], frameRate: 1, repeat: -1 });
anims.create({ key: 'player-walk-gun', frames: anims.generateFrameNumbers('player', { frames: [4, 5] }), frameRate: 8, repeat: -1 });
anims.create({ key: 'player-idle', frames: [{ key: 'player', frame: 0 }], frameRate: 1, repeat: -1 });
anims.create({ key: 'player-walk', frames: anims.generateFrameNumbers('player', { frames: [1, 2] }), frameRate: 8, repeat: -1 });
anims.create({ key: 'player-idle-gun', frames: [{ key: 'player', frame: 3 }], frameRate: 1, repeat: -1 });
anims.create({ key: 'player-walk-gun', frames: anims.generateFrameNumbers('player', { frames: [4, 5] }), frameRate: 8, repeat: -1 });
}
_buildSprite(x, y) {
@ -69,6 +70,7 @@ export class Player {
get active() { return this.sprite?.active ?? false; }
update(delta) {
if (this._dead) return;
this._move();
this._rotateFacing(delta);
this._handleFire(delta);
@ -78,10 +80,10 @@ export class Player {
_move() {
let dx = 0, dy = 0;
if (this.keys.left.isDown) dx -= 1;
if (this.keys.left.isDown) dx -= 1;
if (this.keys.right.isDown) dx += 1;
if (this.keys.up.isDown) dy -= 1;
if (this.keys.down.isDown) dy += 1;
if (this.keys.up.isDown) dy -= 1;
if (this.keys.down.isDown) dy += 1;
this._isMoving = dx !== 0 || dy !== 0;
if (dx !== 0 && dy !== 0) { dx *= 0.707; dy *= 0.707; }
@ -109,10 +111,10 @@ export class Player {
_updateAnimation() {
let key;
if (this._isMoving && this._isShooting) key = 'player-walk-gun';
else if (this._isMoving) key = 'player-walk';
else if (this._isShooting) key = 'player-idle-gun';
else key = 'player-idle';
if (this._isMoving && this._isShooting) key = 'player-walk-gun';
else if (this._isMoving) key = 'player-walk';
else if (this._isShooting) key = 'player-idle-gun';
else key = 'player-idle';
if (this.sprite.anims.currentAnim?.key !== key) {
this.sprite.play(key);
@ -120,6 +122,7 @@ export class Player {
}
_spawnBullet() {
this.scene.sound.play('sfx-shoot', { volume: 0.4 });
const bx = this.x + Math.cos(this.facing) * GUN_TIP_DIST;
const by = this.y + Math.sin(this.facing) * GUN_TIP_DIST;
const bullet = this.scene.add.circle(bx, by, BULLET_SIZE, 0xffff00);
@ -149,32 +152,140 @@ export class Player {
if (this.hp <= 0) {
this.hp = 0;
this._hitFlash(true);
this._loseLife();
} else {
this._hitFlash(false);
}
}
_hitFlash(isLifeLost) {
this.scene.sound.play('sfx-take-damage', { volume: 0.5 });
const scene = this.scene;
const W = scene.scale.width;
const H = scene.scale.height;
scene.cameras.main.shake(isLifeLost ? 300 : 80, isLifeLost ? 0.016 : 0.005);
const alpha = isLifeLost ? 0.55 : 0.28;
const duration = isLifeLost ? 400 : 220;
const flash = scene.add.rectangle(W / 2, H / 2, W, H, 0xff0000, alpha).setDepth(49);
scene.tweens.add({ targets: flash, alpha: 0, duration, onComplete: () => flash.destroy() });
if (!isLifeLost) {
this.sprite.setTint(0xff4444);
scene.time.delayedCall(100, () => { if (this.sprite?.active) this.sprite.clearTint(); });
}
}
_loseLife() {
const livesBeforeDeath = this.lives;
this.lives--;
if (this.lives <= 0) {
this.scene.events.emit('game-over');
return;
}
this.scene.sound.play('sfx-death', { volume: 0.6 });
this.hp = this.stats.maxHp;
this._startInvincibility();
this.invincible = true;
this._dead = true;
this.sprite.body.setVelocity(0, 0);
this.sprite.anims.stop();
// Spin and fade out
this.scene.tweens.add({
targets: this.sprite,
angle: '+=720',
alpha: 0,
duration: 600,
ease: 'Power2',
onComplete: () => {
const W = this.scene.scale.width;
const H = this.scene.scale.height;
this.sprite.setPosition(W / 2, H / 2);
this.sprite.setAngle(0);
this.sprite.setAlpha(1);
this._dead = false;
this.scene.sound.play('sfx-new-life', { volume: 0.6 });
this._startInvincibility();
},
});
this._showLivesOverlay(livesBeforeDeath, this.lives);
}
_startInvincibility() {
this.invincible = true;
// Blink for ~5 seconds: 17 cycles × 300ms = 5.1s
this.scene.tweens.add({
targets: this.sprite,
alpha: 0,
duration: 150,
yoyo: true,
repeat: 4,
repeat: 16,
onComplete: () => {
this.sprite.setAlpha(1);
this.invincible = false;
}
},
});
}
_showLivesOverlay(oldLives, newLives) {
const scene = this.scene;
const W = scene.scale.width;
const H = scene.scale.height;
const cx = W / 2;
const cy = H * 0.3;
const baseStyle = {
fontFamily: 'FutureImperfect',
fontSize: '52px',
stroke: '#000000', strokeThickness: 6,
};
const label = scene.add.text(cx, cy, 'Lives: ', { ...baseStyle, fill: '#ffffff' })
.setOrigin(1, 0.5).setDepth(60);
const oldText = scene.add.text(cx, cy, String(oldLives), { ...baseStyle, fill: '#ff4444' })
.setOrigin(0, 0.5).setDepth(60);
// Shake old count, then fade it out, then show new count
scene.tweens.add({
targets: oldText,
x: oldText.x + 12,
duration: 50,
yoyo: true,
repeat: 7,
delay: 400,
onComplete: () => {
scene.tweens.add({
targets: oldText,
alpha: 0,
duration: 180,
onComplete: () => {
oldText.destroy();
const newText = scene.add.text(cx, cy, String(newLives), { ...baseStyle, fill: '#44ff88' })
.setOrigin(0, 0.5).setDepth(60).setAlpha(0);
scene.tweens.add({
targets: newText,
alpha: 1,
duration: 200,
onComplete: () => {
scene.time.delayedCall(900, () => {
scene.tweens.add({
targets: [label, newText],
alpha: 0,
duration: 1200,
onComplete: () => { label.destroy(); newText.destroy(); },
});
});
},
});
},
});
},
});
}

View File

@ -79,6 +79,7 @@ export class BaseEnemy {
_die() {
this._dying = true;
if (this._deathSound) this.scene.sound.play(this._deathSound, { volume: 0.5 });
this.scene.events.emit('enemy-killed', { xp: this.xp, x: this.x, y: this.y });
// Stop movement and hide HP bar immediately

View File

@ -23,6 +23,7 @@ export class BomberEnemy extends BaseEnemy {
this._offsetAngle = Math.random() * Math.PI * 2;
this._wobble = 0.3 + Math.random() * 0.4;
this._exploded = false;
this._deathSound = 'sfx-death-bomber';
}
update(delta) {

View File

@ -11,6 +11,7 @@ export class ChaseEnemy extends BaseEnemy {
contactDamage: 12,
});
this._deathPulseColor = 0x00ff44;
this._deathSound = 'sfx-death-chaser';
}
update(delta) {

View File

@ -19,6 +19,7 @@ export class ShooterEnemy extends BaseEnemy {
this._shootTimer = SHOOT_INTERVAL * Math.random(); // stagger first shot
this.projectiles = scene.add.group();
this._deathPulseColor = 0x9900ff;
this._deathSound = 'sfx-death-shooter';
}
_die() {
@ -64,6 +65,7 @@ export class ShooterEnemy extends BaseEnemy {
}
_fireProjectile() {
this.scene.sound.play('sfx-shooter-shoot', { volume: 0.35 });
const angle = Phaser.Math.Angle.Between(this.x, this.y, this.player.x, this.player.y);
const proj = this.scene.add.circle(this.x, this.y, 6, PROJECTILE_COLOR);
this.scene.physics.add.existing(proj);

View File

@ -20,6 +20,7 @@ export class SprayerEnemy extends BaseEnemy {
this._shootTimer = SHOOT_INTERVAL * Math.random();
this.projectiles = scene.add.group();
this._deathPulseColor = 0x00ffcc;
this._deathSound = 'sfx-death-sprayer';
}
_die() {
@ -63,6 +64,7 @@ export class SprayerEnemy extends BaseEnemy {
}
_fireSpray() {
this.scene.sound.play('sfx-sprayer-shoot', { volume: 0.4 });
const baseAngle = Phaser.Math.Angle.Between(this.x, this.y, this.player.x, this.player.y);
for (let i = 0; i < SPRAY_COUNT; i++) {
// Spread bullets evenly across the arc: indices 0..4 map to -1..+1

View File

@ -14,6 +14,7 @@ export class SwarmerEnemy extends BaseEnemy {
this._offsetAngle = Math.random() * Math.PI * 2;
this._wobble = Math.random() * 0.5;
this._deathPulseColor = 0xff2222;
this._deathSound = 'sfx-death-swarmer';
}
_die() {

View File

@ -10,9 +10,9 @@ export class GameOverScene extends Phaser.Scene {
this.add.rectangle(W / 2, H / 2, W, H, 0x000000, 0.75);
this.add.text(W / 2, H / 2 - 60, 'GAME OVER', {
fontFamily: 'FutureImperfect',
fontSize: '64px',
fill: '#ff3333',
fontStyle: 'bold',
stroke: '#660000',
strokeThickness: 6,
}).setOrigin(0.5);

View File

@ -15,8 +15,23 @@ export class GameScene extends Phaser.Scene {
preload() {
this.load.json('zones', './js/data/zones.json');
this.load.json('skillTree', './js/data/skillTree.json');
this.load.spritesheet('player', './assets/sprites/player.png', { frameWidth: 48, frameHeight: 48 });
this.load.spritesheet('player', './assets/sprites/player.png', { frameWidth: 48, frameHeight: 48 });
this.load.spritesheet('enemies', './assets/sprites/enemies.png', { frameWidth: 48, frameHeight: 48 });
this.load.audio('music-game', './assets/music/gameBackground.mp3');
this.load.audio('sfx-edge-remove', './assets/fx/EdgeRemove.mp3');
this.load.audio('sfx-arrow-ping', './assets/fx/ArrowPing.mp3');
this.load.audio('sfx-sprayer-shoot', './assets/fx/SprayerShoot.mp3');
this.load.audio('sfx-death', './assets/fx/Death.mp3');
this.load.audio('sfx-new-life', './assets/fx/NewLife.mp3');
this.load.audio('sfx-shoot', './assets/fx/Shoot.mp3');
this.load.audio('sfx-take-damage', './assets/fx/TakeDamage.mp3');
this.load.audio('sfx-shooter-shoot', './assets/fx/ShooterShoot.mp3');
this.load.audio('sfx-enemy-wave', './assets/fx/EnemyWave.mp3');
this.load.audio('sfx-death-chaser', './assets/fx/EnemyChaseDeath.mp3');
this.load.audio('sfx-death-swarmer', './assets/fx/EnemySwarmerDeath.mp3');
this.load.audio('sfx-death-shooter', './assets/fx/EnemyShooterDeath.mp3');
this.load.audio('sfx-death-sprayer', './assets/fx/EnemySprayerDeath.mp3');
this.load.audio('sfx-death-bomber', './assets/fx/EnemyBomberDeath.mp3');
}
create() {
@ -55,12 +70,16 @@ export class GameScene extends Phaser.Scene {
this._waitingForExit = false;
this._pulseEffects = [];
this.events.on('wave-start', () => this.sound.play('sfx-enemy-wave', { volume: 0.5 }));
this.events.on('zone-waves-complete', () => this._startZoneExit());
this.reticle = new Reticle(this);
this._setupBarriers();
this._bgMusic = this.sound.add('music-game', { loop: true, volume: 0.8 });
this._bgMusic.play();
this.waveManager.start();
}
@ -191,6 +210,7 @@ export class GameScene extends Phaser.Scene {
// ── Zone exit (Smash TV style) ─────────────────────────────────────────────
_startZoneExit() {
this.sound.play('sfx-edge-remove', { volume: 0.6 });
// Explode edge barriers first, then allow exit
this.barrierManager.explodeEdgeBarriers(() => {
this._waitingForExit = true;
@ -209,11 +229,11 @@ export class GameScene extends Phaser.Scene {
const ARROW_W = 30; // triangle base half-width
const ARROW_D = 28; // triangle depth (pointing direction)
const MARGIN = 18; // gap from screen edge to arrow tip
const MARGIN = 18; // gap from screen edge to arrow tip
const drawUp = (cx, tipY) => g.fillTriangle(cx - ARROW_W, tipY + ARROW_D, cx + ARROW_W, tipY + ARROW_D, cx, tipY);
const drawDown = (cx, tipY) => g.fillTriangle(cx - ARROW_W, tipY - ARROW_D, cx + ARROW_W, tipY - ARROW_D, cx, tipY);
const drawLeft = (tipX, cy) => g.fillTriangle(tipX + ARROW_D, cy - ARROW_W, tipX + ARROW_D, cy + ARROW_W, tipX, cy);
const drawUp = (cx, tipY) => g.fillTriangle(cx - ARROW_W, tipY + ARROW_D, cx + ARROW_W, tipY + ARROW_D, cx, tipY);
const drawDown = (cx, tipY) => g.fillTriangle(cx - ARROW_W, tipY - ARROW_D, cx + ARROW_W, tipY - ARROW_D, cx, tipY);
const drawLeft = (tipX, cy) => g.fillTriangle(tipX + ARROW_D, cy - ARROW_W, tipX + ARROW_D, cy + ARROW_W, tipX, cy);
const drawRight = (tipX, cy) => g.fillTriangle(tipX - ARROW_D, cy - ARROW_W, tipX - ARROW_D, cy + ARROW_W, tipX, cy);
// Three arrows per edge
@ -230,6 +250,9 @@ export class GameScene extends Phaser.Scene {
drawRight(W - MARGIN, cy);
});
this._arrowPing = this.sound.add('sfx-arrow-ping', { loop: true, volume: 0.4 });
this._arrowPing.play();
// Blink the arrows
this._exitArrowsTween = this.tweens.add({
targets: g,
@ -262,6 +285,8 @@ export class GameScene extends Phaser.Scene {
this._exitArrowsTween?.stop();
this._exitArrowsGfx?.destroy();
this._exitText?.destroy();
this._arrowPing?.stop();
this._arrowPing = null;
this._exitArrowsGfx = null;
this._exitText = null;
}
@ -272,9 +297,9 @@ export class GameScene extends Phaser.Scene {
const px = this.player.x;
const py = this.player.y;
if (px < -24) this._doZoneTransition('left');
if (px < -24) this._doZoneTransition('left');
else if (px > W + 24) this._doZoneTransition('right');
else if (py < -24) this._doZoneTransition('top');
else if (py < -24) this._doZoneTransition('top');
else if (py > H + 24) this._doZoneTransition('bottom');
}
@ -289,7 +314,7 @@ export class GameScene extends Phaser.Scene {
const px = Phaser.Math.Clamp(this.player.x, 48, W - 48);
const py = Phaser.Math.Clamp(this.player.y, 48, H - 48);
const spawnX = direction === 'left' ? W - 48 : direction === 'right' ? 48 : px;
const spawnY = direction === 'top' ? H - 48 : direction === 'bottom' ? 48 : py;
const spawnY = direction === 'top' ? H - 48 : direction === 'bottom' ? 48 : py;
// White flash
const flash = this.add.rectangle(W / 2, H / 2, W, H, 0xffffff).setDepth(50);
@ -341,7 +366,8 @@ export class GameScene extends Phaser.Scene {
this.add.rectangle(W / 2, H / 2, W, H, 0x000000, 0.7).setDepth(30);
this.add.text(W / 2, H / 2 - 40, 'VICTORY!', {
fontSize: '72px', fill: '#ffdd00', fontStyle: 'bold'
fontFamily: 'FutureImperfect',
fontSize: '72px', fill: '#ffdd00',
}).setOrigin(0.5).setDepth(31);
const prompt = this.add.text(W / 2, H / 2 + 60, 'Press R to return to menu', {
@ -364,6 +390,8 @@ export class GameScene extends Phaser.Scene {
this.player?.destroy();
this.waveManager?.reset();
this.hud?.destroy();
this._bgMusic?.stop();
this._bgMusic = null;
this._enemyProjectiles = [];
this._pulseEffects = [];
}

View File

@ -10,9 +10,9 @@ export class IntroScene extends Phaser.Scene {
this.add.rectangle(W / 2, H / 2, W, H, 0x000000);
this.add.text(W / 2, H / 2 - 100, 'OVERRUN', {
fontFamily: 'FutureImperfect',
fontSize: '72px',
fill: '#00ccff',
fontStyle: 'bold',
stroke: '#003366',
strokeThickness: 6,
}).setOrigin(0.5);

View File

@ -25,7 +25,8 @@ export class SkillTreeUI {
// Title
const title = this.scene.add.text(W / 2, H / 2 - 160, `LEVEL UP!`, {
fontSize: '36px', fill: '#ffdd44', fontStyle: 'bold'
fontFamily: 'FutureImperfect',
fontSize: '36px', fill: '#ffdd44',
}).setOrigin(0.5).setDepth(21);
this._elements.push(title);