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
··· 37 37 health -= amount; 38 38 console.log(`Enemy damaged: ${amount}, health: ${health}`); 39 39 40 + // Play hit sound 41 + if (typeof (window as any).gameSound !== 'undefined') { 42 + (window as any).gameSound.playSfx('hit', { volume: 0.3, detune: 200 }); 43 + } 44 + 40 45 // Flash red when hit 41 46 isHit = true; 42 47 this.color = k.rgb(255, 0, 0); ··· 62 67 63 68 // Enemy death 64 69 die(this: GameObj) { 70 + // Play death sound 71 + if (typeof (window as any).gameSound !== 'undefined') { 72 + (window as any).gameSound.playSfx('death', { volume: 0.4, detune: -100 }); 73 + } 74 + 65 75 // Add confetti effect only (no kaboom) 66 76 if (k.addConfetti) { 67 77 k.addConfetti(this.pos);
+14
src/main.ts
··· 4 4 import player from "./player"; 5 5 import { makeEnemy } from "./enemy"; 6 6 import confettiPlugin from "./confetti"; 7 + import setupSoundSystem from "./sound"; 7 8 8 9 const k = kaplay({ plugins: [crew] }); 9 10 k.loadRoot("./"); // A good idea for Itch.io publishing later ··· 15 16 const confetti = confettiPlugin(k); 16 17 k.addConfetti = confetti.addConfetti; 17 18 19 + // Setup sound system 20 + const sound = setupSoundSystem(k); 21 + sound.preloadSounds(); 22 + 23 + // Make sound system globally available 24 + (window as any).gameSound = sound; 25 + 18 26 // Game state 19 27 let gameActive = true; 20 28 let finalScore = 0; ··· 23 31 k.scene("main", () => { 24 32 // Reset game state 25 33 gameActive = true; 34 + 35 + // Start background music 36 + sound.playRandomMusic(); 26 37 27 38 k.setGravity(1600); 28 39 ··· 123 134 // Only trigger once when crossing the threshold 124 135 if (!k.get("level-up-text").length) { 125 136 difficultyLevel += 1; 137 + 138 + // Play level up sound effect 139 + sound.playSfx("levelUp"); 126 140 127 141 // Update difficulty in tracker 128 142 const tracker = k.get("game-score-tracker")[0];
+67 -14
src/player.ts
··· 1 1 import type { KAPLAYCtx, Comp, GameObj } from "kaplay"; 2 2 import { Vec2 } from "kaplay"; 3 3 4 + // Make sound system available to the player component 5 + declare global { 6 + interface Window { 7 + gameSound: any; 8 + } 9 + } 10 + 4 11 // Define player component type 5 12 interface PlayerComp extends Comp { 6 13 speed: number; ··· 67 74 return k.vec2(center.x + dx * ratio, center.y + dy * ratio); 68 75 }; 69 76 77 + // Helper function to play sound if available 78 + const playSound = (type: string, options = {}) => { 79 + if (window.gameSound) { 80 + window.gameSound.playSfx(type, options); 81 + } 82 + }; 83 + 70 84 return { 71 85 id: "player", 72 86 require: ["body", "area", "pos"], ··· 82 96 83 97 health -= amount; 84 98 99 + // Play hit sound 100 + playSound("hit", { volume: 0.4, detune: 300 }); 101 + 85 102 // Flash red when hit 86 103 isHit = true; 87 104 this.color = k.rgb(255, 0, 0); ··· 107 124 k.addKaboom(this.pos, { scale: 2 }); 108 125 k.shake(20); 109 126 127 + // Play death sound 128 + playSound("explosion", { volume: 1, detune: -300 }); 129 + 110 130 // Emit death event for game over handling 111 131 this.trigger("death"); 112 132 } ··· 116 136 heal(this: GameObj, amount: number) { 117 137 // Add health but don't exceed max health 118 138 health = Math.min(health + amount, maxHealth); 119 - 139 + 140 + // Play heal sound 141 + playSound("coin", { volume: 0.2, detune: 200 }); 142 + 120 143 // Flash green when healed 121 144 this.color = k.rgb(0, 255, 0); 122 - 145 + 123 146 // Reset color after a short time 124 147 k.wait(0.1, () => { 125 148 this.color = k.rgb(); 126 149 }); 127 - 150 + 128 151 // Update health bar 129 152 if (healthBar) { 130 153 const healthPercent = Math.max(0, health / maxHealth); ··· 201 224 this.onKeyPress(["space", "up", "w"], () => { 202 225 if (this.isGrounded()) { 203 226 this.jump(jumpForce); 227 + playSound("coin", { volume: 0.3, detune: 400 }); 204 228 } 205 229 }); 206 230 207 231 // Attack with X key - now ultimate move 208 232 this.onKeyPress("x", () => { 233 + // Play charging sound 234 + playSound("windup", { volume: 0.5, detune: 500 }); 235 + 209 236 // Create visual effects for charging up 210 237 const chargeEffect = k.add([ 211 238 k.circle(50), ··· 220 247 k.tween( 221 248 50, 222 249 200, 223 - 1.5, 250 + 1.4, 224 251 (v) => { 225 252 if (chargeEffect.exists()) { 226 253 chargeEffect.radius = v; ··· 250 277 }); 251 278 252 279 // After delay, trigger the ultimate explosion 253 - k.wait(2, () => { 280 + k.wait(1.5, () => { 254 281 if (chargeEffect.exists()) chargeEffect.destroy(); 255 282 if (warningText.exists()) warningText.destroy(); 256 283 ··· 277 304 ); 278 305 } 279 306 307 + // Play massive explosion sound 308 + playSound("explosion", { volume: 1.0, detune: -600 }); 309 + 280 310 // Visual effects 281 311 k.addKaboom(this.pos, { 282 312 scale: 5, ··· 291 321 k.addKaboom(k.vec2(this.pos).add(offset), { 292 322 scale: 2 + Math.random() * 2, 293 323 }); 324 + 325 + // Play additional explosion sounds with slight delay 326 + playSound("explosion", { 327 + volume: 0.7, 328 + detune: -300 + Math.random() * 200, 329 + }); 294 330 }); 295 331 } 296 332 ··· 329 365 // Damage all enemies with high damage 330 366 const enemies = k.get("enemy"); 331 367 let enemiesKilled = 0; 332 - 368 + 333 369 enemies.forEach((enemy) => { 334 370 const dist = k.vec2(enemy.pos).dist(this.pos); 335 371 if (dist < explosionRadius) { 336 372 // Count enemies killed 337 373 enemiesKilled++; 338 - 374 + 339 375 // Instant kill any enemy within the explosion radius 340 376 (enemy as any).damage(1000); // Extremely high damage to ensure death 341 377 ··· 344 380 k.addKaboom(enemy.pos, { 345 381 scale: 1 + Math.random(), 346 382 }); 383 + 384 + // Play enemy death sound 385 + playSound("explosion", { 386 + volume: 0.5, 387 + detune: Math.random() * 400 - 200, 388 + }); 347 389 }); 348 390 } 349 391 }); 350 - 392 + 351 393 // Calculate bonus score based on health and enemies killed 352 394 // Higher health = higher score multiplier 353 395 const healthPercent = health / maxHealth; 354 - const scoreBonus = Math.round(500 * healthPercent * (1 + enemiesKilled * 0.5)); 355 - 396 + const scoreBonus = Math.round( 397 + 500 * healthPercent * (1 + enemiesKilled * 0.5), 398 + ); 399 + 356 400 // Add score bonus 357 401 if (scoreBonus > 0) { 358 402 // Get score object ··· 364 408 const newScore = currentScore + scoreBonus; 365 409 // Update score display 366 410 scoreObj.text = `Score: ${newScore}`; 367 - 411 + 368 412 // Update the actual score variable in the game scene 369 413 // This is needed for the game over screen to show the correct score 370 414 const gameScores = k.get("game-score-tracker"); 371 415 if (gameScores.length > 0) { 372 416 gameScores[0].updateScore(newScore); 373 417 } 374 - 418 + 419 + // Play bonus sound 420 + playSound("coin", { volume: 0.8 }); 421 + 375 422 // Show bonus text 376 423 const bonusText = k.add([ 377 424 k.text(`+${scoreBonus} ULTIMATE BONUS!`, { size: 32 }), ··· 382 429 k.z(100), 383 430 k.opacity(1), 384 431 ]); 385 - 432 + 386 433 // Fade out and destroy the text 387 434 k.tween( 388 435 1, ··· 396 443 }, 397 444 k.easings.easeInQuad, 398 445 ); 399 - 446 + 400 447 k.wait(1.5, () => { 401 448 if (bonusText.exists()) bonusText.destroy(); 402 449 }); ··· 422 469 ); 423 470 424 471 console.log("Creating explosion at", clampedPos.x, clampedPos.y); 472 + 473 + // Play explosion sound 474 + playSound("explosion", { volume: 0.6 }); 425 475 426 476 // Create visual explosion effect 427 477 k.addKaboom(clampedPos); ··· 478 528 if (isAttacking) return; 479 529 480 530 isAttacking = true; 531 + 532 + // Play sword swing sound 533 + playSound("explosion", { volume: 1, detune: 800 }); 481 534 482 535 if (sword) { 483 536 // Set sword to attacking state for collision detection
+179
src/sound.ts
··· 1 + import type { KAPLAYCtx } from "kaplay"; 2 + 3 + // Sound effects and music system 4 + export function setupSoundSystem(k: KAPLAYCtx) { 5 + // Available music tracks 6 + const musicTracks = [ 7 + "pixel-song-3.mp3", 8 + "pixel-song-18.mp3", 9 + "pixel-song-19.mp3", 10 + "pixel-song-21.mp3", 11 + ]; 12 + 13 + // Sound effects 14 + const soundEffects = { 15 + coin: [ 16 + "coin-1.wav", 17 + "coin-2.wav", 18 + "coin-3.wav", 19 + "coin-4.wav", 20 + "coin-5.wav", 21 + ], 22 + explosion: ["pixel-explosion.mp3"], 23 + jump: ["coin-1.wav"], 24 + hit: ["ouch.mp3"], 25 + heal: ["coin-3.wav"], 26 + death: ["large-underwater-explosion.mp3"], 27 + levelUp: ["smb_powerup.wav"], 28 + windup: ["windup.mp3"], 29 + }; 30 + 31 + // Keep track of last played music to avoid repeats 32 + let lastPlayedMusic = ""; 33 + let currentMusic: any = null; 34 + let musicVolume = 0.8; // Increased from 0.5 to 0.8 35 + let sfxVolume = 0.7; 36 + let musicEnabled = true; 37 + let sfxEnabled = true; 38 + 39 + // Preload all sounds 40 + function preloadSounds() { 41 + // Preload music 42 + musicTracks.forEach((track) => { 43 + k.loadSound(track, `music/${track}`); 44 + }); 45 + 46 + // Preload sound effects 47 + Object.values(soundEffects) 48 + .flat() 49 + .forEach((sfx) => { 50 + k.loadSound(sfx, `sfx/${sfx}`); 51 + }); 52 + } 53 + 54 + // Play a random music track (avoiding the last played one) 55 + function playRandomMusic() { 56 + if (!musicEnabled) return; 57 + 58 + // Stop current music if playing 59 + if (currentMusic) { 60 + currentMusic.stop(); 61 + } 62 + 63 + // Filter out the last played track to avoid repeats 64 + const availableTracks = musicTracks.filter( 65 + (track) => track !== lastPlayedMusic, 66 + ); 67 + 68 + // Select a random track from available tracks 69 + const randomIndex = Math.floor(Math.random() * availableTracks.length); 70 + const selectedTrack = availableTracks[randomIndex]; 71 + 72 + // Update last played track 73 + lastPlayedMusic = selectedTrack; 74 + 75 + // Play the selected track 76 + currentMusic = k.play(selectedTrack, { 77 + volume: musicVolume, 78 + loop: true, 79 + }); 80 + 81 + return currentMusic; 82 + } 83 + 84 + // Play a sound effect 85 + function playSfx(type: keyof typeof soundEffects, options: any = {}) { 86 + if (!sfxEnabled) return; 87 + 88 + const sounds = soundEffects[type]; 89 + if (!sounds || sounds.length === 0) return; 90 + 91 + // Select a random sound from the category 92 + const randomIndex = Math.floor(Math.random() * sounds.length); 93 + const selectedSound = sounds[randomIndex]; 94 + // Play the sound with options 95 + return k.play(selectedSound, { 96 + volume: options.volume ?? sfxVolume, 97 + ...options, 98 + }); 99 + } 100 + 101 + // Play a specific sound file directly 102 + function playSound(soundName: string, options: any = {}) { 103 + if (!sfxEnabled) return; 104 + 105 + return k.play(soundName, { 106 + volume: options.volume ?? sfxVolume, 107 + ...options, 108 + }); 109 + } 110 + 111 + // Toggle music on/off 112 + function toggleMusic() { 113 + musicEnabled = !musicEnabled; 114 + 115 + if (musicEnabled) { 116 + playRandomMusic(); 117 + } else if (currentMusic) { 118 + currentMusic.stop(); 119 + currentMusic = null; 120 + } 121 + 122 + return musicEnabled; 123 + } 124 + 125 + // Toggle sound effects on/off 126 + function toggleSfx() { 127 + sfxEnabled = !sfxEnabled; 128 + return sfxEnabled; 129 + } 130 + 131 + // Set music volume 132 + function setMusicVolume(volume: number) { 133 + musicVolume = Math.max(0, Math.min(1, volume)); 134 + if (currentMusic) { 135 + currentMusic.volume(musicVolume); 136 + } 137 + return musicVolume; 138 + } 139 + 140 + // Set sound effects volume 141 + function setSfxVolume(volume: number) { 142 + sfxVolume = Math.max(0, Math.min(1, volume)); 143 + return sfxVolume; 144 + } 145 + 146 + // Check if a sound is currently playing 147 + function isMusicPlaying() { 148 + return currentMusic !== null; 149 + } 150 + 151 + // Stop current music 152 + function stopMusic() { 153 + if (currentMusic) { 154 + currentMusic.stop(); 155 + currentMusic = null; 156 + } 157 + } 158 + 159 + // Get current music track name 160 + function getCurrentMusic() { 161 + return lastPlayedMusic; 162 + } 163 + 164 + return { 165 + preloadSounds, 166 + playRandomMusic, 167 + playSfx, 168 + playSound, 169 + toggleMusic, 170 + toggleSfx, 171 + setMusicVolume, 172 + setSfxVolume, 173 + isMusicPlaying, 174 + stopMusic, 175 + getCurrentMusic, 176 + }; 177 + } 178 + 179 + export default setupSoundSystem;