feat: add sfx

dunkirk.sh f681e6e8 e0161e21

verified
public/music/pixel-song-18.mp3

This is a binary file and will not be displayed.

public/music/pixel-song-19.mp3

This is a binary file and will not be displayed.

public/music/pixel-song-21.mp3

This is a binary file and will not be displayed.

public/music/pixel-song-3.mp3

This is a binary file and will not be displayed.

public/sfx/boom-1-bright-attack.mp3

This is a binary file and will not be displayed.

public/sfx/coin-1.wav

This is a binary file and will not be displayed.

public/sfx/coin-2.wav

This is a binary file and will not be displayed.

public/sfx/coin-3.wav

This is a binary file and will not be displayed.

public/sfx/coin-4.wav

This is a binary file and will not be displayed.

public/sfx/coin-5.wav

This is a binary file and will not be displayed.

public/sfx/large-underwater-explosion.mp3

This is a binary file and will not be displayed.

public/sfx/ouch.mp3

This is a binary file and will not be displayed.

public/sfx/pixel-explosion.mp3

This is a binary file and will not be displayed.

public/sfx/smb_powerup.wav

This is a binary file and will not be displayed.

public/sfx/windup.mp3

This is a binary file and will not be displayed.

+10
src/enemy.ts
···
health -= amount;
console.log(`Enemy damaged: ${amount}, health: ${health}`);
+
// Play hit sound
+
if (typeof (window as any).gameSound !== 'undefined') {
+
(window as any).gameSound.playSfx('hit', { volume: 0.3, detune: 200 });
+
}
+
// Flash red when hit
isHit = true;
this.color = k.rgb(255, 0, 0);
···
// Enemy death
die(this: GameObj) {
+
// Play death sound
+
if (typeof (window as any).gameSound !== 'undefined') {
+
(window as any).gameSound.playSfx('death', { volume: 0.4, detune: -100 });
+
}
+
// Add confetti effect only (no kaboom)
if (k.addConfetti) {
k.addConfetti(this.pos);
+14
src/main.ts
···
import player from "./player";
import { makeEnemy } from "./enemy";
import confettiPlugin from "./confetti";
+
import setupSoundSystem from "./sound";
const k = kaplay({ plugins: [crew] });
k.loadRoot("./"); // A good idea for Itch.io publishing later
···
const confetti = confettiPlugin(k);
k.addConfetti = confetti.addConfetti;
+
// Setup sound system
+
const sound = setupSoundSystem(k);
+
sound.preloadSounds();
+
+
// Make sound system globally available
+
(window as any).gameSound = sound;
+
// Game state
let gameActive = true;
let finalScore = 0;
···
k.scene("main", () => {
// Reset game state
gameActive = true;
+
+
// Start background music
+
sound.playRandomMusic();
k.setGravity(1600);
···
// Only trigger once when crossing the threshold
if (!k.get("level-up-text").length) {
difficultyLevel += 1;
+
+
// Play level up sound effect
+
sound.playSfx("levelUp");
// Update difficulty in tracker
const tracker = k.get("game-score-tracker")[0];
+67 -14
src/player.ts
···
import type { KAPLAYCtx, Comp, GameObj } from "kaplay";
import { Vec2 } from "kaplay";
+
// Make sound system available to the player component
+
declare global {
+
interface Window {
+
gameSound: any;
+
}
+
}
+
// Define player component type
interface PlayerComp extends Comp {
speed: number;
···
return k.vec2(center.x + dx * ratio, center.y + dy * ratio);
};
+
// Helper function to play sound if available
+
const playSound = (type: string, options = {}) => {
+
if (window.gameSound) {
+
window.gameSound.playSfx(type, options);
+
}
+
};
+
return {
id: "player",
require: ["body", "area", "pos"],
···
health -= amount;
+
// Play hit sound
+
playSound("hit", { volume: 0.4, detune: 300 });
+
// Flash red when hit
isHit = true;
this.color = k.rgb(255, 0, 0);
···
k.addKaboom(this.pos, { scale: 2 });
k.shake(20);
+
// Play death sound
+
playSound("explosion", { volume: 1, detune: -300 });
+
// Emit death event for game over handling
this.trigger("death");
}
···
heal(this: GameObj, amount: number) {
// Add health but don't exceed max health
health = Math.min(health + amount, maxHealth);
-
+
+
// Play heal sound
+
playSound("coin", { volume: 0.2, detune: 200 });
+
// Flash green when healed
this.color = k.rgb(0, 255, 0);
-
+
// Reset color after a short time
k.wait(0.1, () => {
this.color = k.rgb();
});
-
+
// Update health bar
if (healthBar) {
const healthPercent = Math.max(0, health / maxHealth);
···
this.onKeyPress(["space", "up", "w"], () => {
if (this.isGrounded()) {
this.jump(jumpForce);
+
playSound("coin", { volume: 0.3, detune: 400 });
}
});
// Attack with X key - now ultimate move
this.onKeyPress("x", () => {
+
// Play charging sound
+
playSound("windup", { volume: 0.5, detune: 500 });
+
// Create visual effects for charging up
const chargeEffect = k.add([
k.circle(50),
···
k.tween(
50,
200,
-
1.5,
+
1.4,
(v) => {
if (chargeEffect.exists()) {
chargeEffect.radius = v;
···
});
// After delay, trigger the ultimate explosion
-
k.wait(2, () => {
+
k.wait(1.5, () => {
if (chargeEffect.exists()) chargeEffect.destroy();
if (warningText.exists()) warningText.destroy();
···
);
}
+
// Play massive explosion sound
+
playSound("explosion", { volume: 1.0, detune: -600 });
+
// Visual effects
k.addKaboom(this.pos, {
scale: 5,
···
k.addKaboom(k.vec2(this.pos).add(offset), {
scale: 2 + Math.random() * 2,
});
+
+
// Play additional explosion sounds with slight delay
+
playSound("explosion", {
+
volume: 0.7,
+
detune: -300 + Math.random() * 200,
+
});
});
}
···
// Damage all enemies with high damage
const enemies = k.get("enemy");
let enemiesKilled = 0;
-
+
enemies.forEach((enemy) => {
const dist = k.vec2(enemy.pos).dist(this.pos);
if (dist < explosionRadius) {
// Count enemies killed
enemiesKilled++;
-
+
// Instant kill any enemy within the explosion radius
(enemy as any).damage(1000); // Extremely high damage to ensure death
···
k.addKaboom(enemy.pos, {
scale: 1 + Math.random(),
});
+
+
// Play enemy death sound
+
playSound("explosion", {
+
volume: 0.5,
+
detune: Math.random() * 400 - 200,
+
});
});
}
});
-
+
// Calculate bonus score based on health and enemies killed
// Higher health = higher score multiplier
const healthPercent = health / maxHealth;
-
const scoreBonus = Math.round(500 * healthPercent * (1 + enemiesKilled * 0.5));
-
+
const scoreBonus = Math.round(
+
500 * healthPercent * (1 + enemiesKilled * 0.5),
+
);
+
// Add score bonus
if (scoreBonus > 0) {
// Get score object
···
const newScore = currentScore + scoreBonus;
// Update score display
scoreObj.text = `Score: ${newScore}`;
-
+
// Update the actual score variable in the game scene
// This is needed for the game over screen to show the correct score
const gameScores = k.get("game-score-tracker");
if (gameScores.length > 0) {
gameScores[0].updateScore(newScore);
}
-
+
+
// Play bonus sound
+
playSound("coin", { volume: 0.8 });
+
// Show bonus text
const bonusText = k.add([
k.text(`+${scoreBonus} ULTIMATE BONUS!`, { size: 32 }),
···
k.z(100),
k.opacity(1),
]);
-
+
// Fade out and destroy the text
k.tween(
1,
···
},
k.easings.easeInQuad,
);
-
+
k.wait(1.5, () => {
if (bonusText.exists()) bonusText.destroy();
});
···
);
console.log("Creating explosion at", clampedPos.x, clampedPos.y);
+
+
// Play explosion sound
+
playSound("explosion", { volume: 0.6 });
// Create visual explosion effect
k.addKaboom(clampedPos);
···
if (isAttacking) return;
isAttacking = true;
+
+
// Play sword swing sound
+
playSound("explosion", { volume: 1, detune: 800 });
if (sword) {
// Set sword to attacking state for collision detection
+179
src/sound.ts
···
+
import type { KAPLAYCtx } from "kaplay";
+
+
// Sound effects and music system
+
export function setupSoundSystem(k: KAPLAYCtx) {
+
// Available music tracks
+
const musicTracks = [
+
"pixel-song-3.mp3",
+
"pixel-song-18.mp3",
+
"pixel-song-19.mp3",
+
"pixel-song-21.mp3",
+
];
+
+
// Sound effects
+
const soundEffects = {
+
coin: [
+
"coin-1.wav",
+
"coin-2.wav",
+
"coin-3.wav",
+
"coin-4.wav",
+
"coin-5.wav",
+
],
+
explosion: ["pixel-explosion.mp3"],
+
jump: ["coin-1.wav"],
+
hit: ["ouch.mp3"],
+
heal: ["coin-3.wav"],
+
death: ["large-underwater-explosion.mp3"],
+
levelUp: ["smb_powerup.wav"],
+
windup: ["windup.mp3"],
+
};
+
+
// Keep track of last played music to avoid repeats
+
let lastPlayedMusic = "";
+
let currentMusic: any = null;
+
let musicVolume = 0.8; // Increased from 0.5 to 0.8
+
let sfxVolume = 0.7;
+
let musicEnabled = true;
+
let sfxEnabled = true;
+
+
// Preload all sounds
+
function preloadSounds() {
+
// Preload music
+
musicTracks.forEach((track) => {
+
k.loadSound(track, `music/${track}`);
+
});
+
+
// Preload sound effects
+
Object.values(soundEffects)
+
.flat()
+
.forEach((sfx) => {
+
k.loadSound(sfx, `sfx/${sfx}`);
+
});
+
}
+
+
// Play a random music track (avoiding the last played one)
+
function playRandomMusic() {
+
if (!musicEnabled) return;
+
+
// Stop current music if playing
+
if (currentMusic) {
+
currentMusic.stop();
+
}
+
+
// Filter out the last played track to avoid repeats
+
const availableTracks = musicTracks.filter(
+
(track) => track !== lastPlayedMusic,
+
);
+
+
// Select a random track from available tracks
+
const randomIndex = Math.floor(Math.random() * availableTracks.length);
+
const selectedTrack = availableTracks[randomIndex];
+
+
// Update last played track
+
lastPlayedMusic = selectedTrack;
+
+
// Play the selected track
+
currentMusic = k.play(selectedTrack, {
+
volume: musicVolume,
+
loop: true,
+
});
+
+
return currentMusic;
+
}
+
+
// Play a sound effect
+
function playSfx(type: keyof typeof soundEffects, options: any = {}) {
+
if (!sfxEnabled) return;
+
+
const sounds = soundEffects[type];
+
if (!sounds || sounds.length === 0) return;
+
+
// Select a random sound from the category
+
const randomIndex = Math.floor(Math.random() * sounds.length);
+
const selectedSound = sounds[randomIndex];
+
// Play the sound with options
+
return k.play(selectedSound, {
+
volume: options.volume ?? sfxVolume,
+
...options,
+
});
+
}
+
+
// Play a specific sound file directly
+
function playSound(soundName: string, options: any = {}) {
+
if (!sfxEnabled) return;
+
+
return k.play(soundName, {
+
volume: options.volume ?? sfxVolume,
+
...options,
+
});
+
}
+
+
// Toggle music on/off
+
function toggleMusic() {
+
musicEnabled = !musicEnabled;
+
+
if (musicEnabled) {
+
playRandomMusic();
+
} else if (currentMusic) {
+
currentMusic.stop();
+
currentMusic = null;
+
}
+
+
return musicEnabled;
+
}
+
+
// Toggle sound effects on/off
+
function toggleSfx() {
+
sfxEnabled = !sfxEnabled;
+
return sfxEnabled;
+
}
+
+
// Set music volume
+
function setMusicVolume(volume: number) {
+
musicVolume = Math.max(0, Math.min(1, volume));
+
if (currentMusic) {
+
currentMusic.volume(musicVolume);
+
}
+
return musicVolume;
+
}
+
+
// Set sound effects volume
+
function setSfxVolume(volume: number) {
+
sfxVolume = Math.max(0, Math.min(1, volume));
+
return sfxVolume;
+
}
+
+
// Check if a sound is currently playing
+
function isMusicPlaying() {
+
return currentMusic !== null;
+
}
+
+
// Stop current music
+
function stopMusic() {
+
if (currentMusic) {
+
currentMusic.stop();
+
currentMusic = null;
+
}
+
}
+
+
// Get current music track name
+
function getCurrentMusic() {
+
return lastPlayedMusic;
+
}
+
+
return {
+
preloadSounds,
+
playRandomMusic,
+
playSfx,
+
playSound,
+
toggleMusic,
+
toggleSfx,
+
setMusicVolume,
+
setSfxVolume,
+
isMusicPlaying,
+
stopMusic,
+
getCurrentMusic,
+
};
+
}
+
+
export default setupSoundSystem;