From 5c5cc13903e0b0d815a8de8ec88965f371629f4f Mon Sep 17 00:00:00 2001 From: Marti Vich Date: Wed, 7 Jan 2026 20:01:58 +0100 Subject: [PATCH] feat: Implement combat and movement sound effects with looping footsteps --- src/engine/game/GameEngine.js | 4 ++++ src/view/GameRenderer.js | 23 ++++++++++++++++++++--- src/view/SoundManager.js | 29 ++++++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/engine/game/GameEngine.js b/src/engine/game/GameEngine.js index f33bd20..8f100af 100644 --- a/src/engine/game/GameEngine.js +++ b/src/engine/game/GameEngine.js @@ -254,6 +254,8 @@ export class GameEngine { const result = CombatMechanics.resolveMeleeAttack(hero, monster, this); hero.hasAttacked = true; + if (window.SOUND_MANAGER) window.SOUND_MANAGER.playSound('sword'); + if (this.onCombatResult) this.onCombatResult(result); return { success: true, result }; @@ -309,6 +311,8 @@ export class GameEngine { const result = CombatMechanics.resolveRangedAttack(hero, monster, this); hero.hasAttacked = true; + if (window.SOUND_MANAGER) window.SOUND_MANAGER.playSound('arrow'); + if (this.onCombatResult) this.onCombatResult(result); return { success: true, result }; diff --git a/src/view/GameRenderer.js b/src/view/GameRenderer.js index 6b28d69..15355dd 100644 --- a/src/view/GameRenderer.js +++ b/src/view/GameRenderer.js @@ -349,6 +349,8 @@ export class GameRenderer { } updateAnimations(time) { + let isAnyMoving = false; + this.entities.forEach((mesh, id) => { const data = mesh.userData; @@ -362,6 +364,11 @@ export class GameRenderer { data.targetPos = new THREE.Vector3(nextStep.x, mesh.position.y, -nextStep.y); } + // Check if this entity is contributing to movement sound + if (data.isMoving || data.pathQueue.length > 0) { + isAnyMoving = true; + } + if (data.isMoving) { const duration = 300; // Hero movement speed (300ms per tile) const elapsed = time - data.startTime; @@ -457,14 +464,24 @@ export class GameRenderer { // IF Finished Sequence (Queue empty) - if (data.pathQueue.length === 0) { - // Check if it's the player (id 'p1') - if (id === 'p1' && this.onHeroFinishedMove) { + if (data.pathQueue.length === 0 && !data.isMoving) { // Ensure strict finished state + // Check if it's the player (id 'p1') -- NOTE: ID might be hero_barbarian etc. + // Using generic callback + if (id === 'p1' && this.onHeroFinishedMove) { // Legacy check? // Grid Coords from World Coords (X, -Z) this.onHeroFinishedMove(mesh.position.x, -mesh.position.z); } } }); + + // Handle Footsteps Audio Globally + if (window.SOUND_MANAGER) { + if (isAnyMoving) { + window.SOUND_MANAGER.startLoop('footsteps'); + } else { + window.SOUND_MANAGER.stopLoop('footsteps'); + } + } } renderExits(exits) { // Cancel any pending render diff --git a/src/view/SoundManager.js b/src/view/SoundManager.js index 4775a50..616407a 100644 --- a/src/view/SoundManager.js +++ b/src/view/SoundManager.js @@ -12,11 +12,15 @@ export class SoundManager { 'exploration': '/assets/music/ingame/Abandoned_Ruins.mp3' }, sfx: { - 'door_open': '/assets/sfx/opendoor.mp3' + 'door_open': '/assets/sfx/opendoor.mp3', + 'footsteps': '/assets/sfx/footsteps.mp3', + 'sword': '/assets/sfx/sword1.mp3', + 'arrow': '/assets/sfx/arrow.mp3' } }; this.initialized = false; + this.activeLoops = new Map(); } /** @@ -112,4 +116,27 @@ export class SoundManager { // or log if needed }); } + + startLoop(key) { + if (this.isMuted) return; + if (this.activeLoops.has(key)) return; // Already playing + + const url = this.assets.sfx[key]; + if (!url) return; + + const audio = new Audio(url); + audio.loop = true; + audio.volume = this.sfxVolume; + audio.play().catch(() => { }); + this.activeLoops.set(key, audio); + } + + stopLoop(key) { + const audio = this.activeLoops.get(key); + if (audio) { + audio.pause(); + audio.currentTime = 0; + this.activeLoops.delete(key); + } + } }