feat: Implement combat and movement sound effects with looping footsteps

This commit is contained in:
2026-01-07 20:01:58 +01:00
parent 180cf3ab94
commit 5c5cc13903
3 changed files with 52 additions and 4 deletions

View File

@@ -254,6 +254,8 @@ export class GameEngine {
const result = CombatMechanics.resolveMeleeAttack(hero, monster, this); const result = CombatMechanics.resolveMeleeAttack(hero, monster, this);
hero.hasAttacked = true; hero.hasAttacked = true;
if (window.SOUND_MANAGER) window.SOUND_MANAGER.playSound('sword');
if (this.onCombatResult) this.onCombatResult(result); if (this.onCombatResult) this.onCombatResult(result);
return { success: true, result }; return { success: true, result };
@@ -309,6 +311,8 @@ export class GameEngine {
const result = CombatMechanics.resolveRangedAttack(hero, monster, this); const result = CombatMechanics.resolveRangedAttack(hero, monster, this);
hero.hasAttacked = true; hero.hasAttacked = true;
if (window.SOUND_MANAGER) window.SOUND_MANAGER.playSound('arrow');
if (this.onCombatResult) this.onCombatResult(result); if (this.onCombatResult) this.onCombatResult(result);
return { success: true, result }; return { success: true, result };

View File

@@ -349,6 +349,8 @@ export class GameRenderer {
} }
updateAnimations(time) { updateAnimations(time) {
let isAnyMoving = false;
this.entities.forEach((mesh, id) => { this.entities.forEach((mesh, id) => {
const data = mesh.userData; const data = mesh.userData;
@@ -362,6 +364,11 @@ export class GameRenderer {
data.targetPos = new THREE.Vector3(nextStep.x, mesh.position.y, -nextStep.y); 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) { if (data.isMoving) {
const duration = 300; // Hero movement speed (300ms per tile) const duration = 300; // Hero movement speed (300ms per tile)
const elapsed = time - data.startTime; const elapsed = time - data.startTime;
@@ -457,14 +464,24 @@ export class GameRenderer {
// IF Finished Sequence (Queue empty) // IF Finished Sequence (Queue empty)
if (data.pathQueue.length === 0) { if (data.pathQueue.length === 0 && !data.isMoving) { // Ensure strict finished state
// Check if it's the player (id 'p1') // Check if it's the player (id 'p1') -- NOTE: ID might be hero_barbarian etc.
if (id === 'p1' && this.onHeroFinishedMove) { // Using generic callback
if (id === 'p1' && this.onHeroFinishedMove) { // Legacy check?
// Grid Coords from World Coords (X, -Z) // Grid Coords from World Coords (X, -Z)
this.onHeroFinishedMove(mesh.position.x, -mesh.position.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) { renderExits(exits) {
// Cancel any pending render // Cancel any pending render

View File

@@ -12,11 +12,15 @@ export class SoundManager {
'exploration': '/assets/music/ingame/Abandoned_Ruins.mp3' 'exploration': '/assets/music/ingame/Abandoned_Ruins.mp3'
}, },
sfx: { 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.initialized = false;
this.activeLoops = new Map();
} }
/** /**
@@ -112,4 +116,27 @@ export class SoundManager {
// or log if needed // 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);
}
}
} }