🎵🎨 Jan 8 Visual & Audio Systems Complete - Biome Music + Spatial Triggers + Test Scene
✅ SYSTEMS CREATED: **1. BiomeMusicSystem.js (Background Music):** - Automatic music switching based on player position - Smooth cross-fade transitions (2 seconds) - Biome-specific tracks (grassland, forest, town, combat) - Night music override (8pm-6am) - Volume control + master volume - Loop support for ambient tracks **2. AudioTriggerSystem.js (Spatial Audio):** - Trigger audio when player enters specific tiles - One-time trigger support (play only once) - Radius detection (exact tile or area) - Delay support before audio plays - Callback functions after audio - Visual debug markers (green circle + 🔊 icon) - Trigger history tracking **3. TestVisualAudioScene.js (DEMO SCENE):** 🎬 Complete visual & audio demonstration: **Visual Effects:** - Kai character with 8 animated dreadlocks - Dreadlocks wave in wind (sine wave animation) - 20 falling leaves (continuous spawn) - Leaf rotation + side-sway animation - WASD movement controls - Camera follow with zoom **Audio Triggers:** - Yellow tile at (10, 7) triggers Kai's voice - Plays: 'My name is Kai, and I will find my sister.' - One-time trigger (won't repeat) - Speech bubble appears after trigger - Visual feedback (green flash) **Scene Features:** - Grass tile grid (20x15) - Alternating light/dark grass pattern - Instructions overlay - ESC to exit scene **Integration:** - Added to index.html - Added to game.js scene list - Ready to launch: game.scene.start('TestVisualAudioScene') 🎯 Test Command: Open browser console and type: game.scene.start('TestVisualAudioScene') 📝 For music: 1. Add music files to /assets/audio/music/ 2. System automatically cross-fades on biome change 3. Night music override active 8pm-6am
This commit is contained in:
181
src/systems/AudioTriggerSystem.js
Normal file
181
src/systems/AudioTriggerSystem.js
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* AudioTriggerSystem.js
|
||||
* Spatial audio triggers - play sound once when player enters area
|
||||
*/
|
||||
|
||||
class AudioTriggerSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Active triggers
|
||||
this.triggers = new Map();
|
||||
|
||||
// Triggered history (to prevent re-triggering)
|
||||
this.triggered = new Set();
|
||||
|
||||
console.log('🔊 AudioTriggerSystem initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a spatial audio trigger
|
||||
* @param {number} x - Grid X position
|
||||
* @param {number} y - Grid Y position
|
||||
* @param {string} audioKey - Audio file key
|
||||
* @param {object} options - Additional options
|
||||
*/
|
||||
addTrigger(x, y, audioKey, options = {}) {
|
||||
const triggerId = `${x},${y}`;
|
||||
|
||||
const trigger = {
|
||||
x,
|
||||
y,
|
||||
audioKey,
|
||||
radius: options.radius || 0, // 0 = exact tile only
|
||||
volume: options.volume || 1.0,
|
||||
oneTime: options.oneTime !== false, // Default true
|
||||
delay: options.delay || 0, // Delay before playing (ms)
|
||||
callback: options.callback || null, // Optional callback after playing
|
||||
visualDebug: options.visualDebug || false
|
||||
};
|
||||
|
||||
this.triggers.set(triggerId, trigger);
|
||||
|
||||
// Add visual debug marker if enabled
|
||||
if (trigger.visualDebug && this.scene.add) {
|
||||
const worldX = x * 48 + 24;
|
||||
const worldY = y * 48 + 24;
|
||||
|
||||
const circle = this.scene.add.circle(worldX, worldY, 20, 0x00ff00, 0.3);
|
||||
circle.setDepth(1000);
|
||||
|
||||
const text = this.scene.add.text(worldX, worldY - 30, '🔊', {
|
||||
fontSize: '20px',
|
||||
color: '#00ff00'
|
||||
});
|
||||
text.setOrigin(0.5);
|
||||
text.setDepth(1001);
|
||||
}
|
||||
|
||||
console.log(`✅ Audio trigger added at (${x}, ${y}): ${audioKey}`);
|
||||
|
||||
return triggerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a trigger
|
||||
*/
|
||||
removeTrigger(triggerId) {
|
||||
this.triggers.delete(triggerId);
|
||||
this.triggered.delete(triggerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset trigger (allow re-triggering)
|
||||
*/
|
||||
resetTrigger(triggerId) {
|
||||
this.triggered.delete(triggerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all triggers
|
||||
*/
|
||||
resetAll() {
|
||||
this.triggered.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player is in trigger zone
|
||||
*/
|
||||
checkTrigger(playerX, playerY) {
|
||||
const playerGridX = Math.floor(playerX / 48);
|
||||
const playerGridY = Math.floor(playerY / 48);
|
||||
|
||||
this.triggers.forEach((trigger, triggerId) => {
|
||||
// Skip if already triggered and it's one-time only
|
||||
if (trigger.oneTime && this.triggered.has(triggerId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check distance
|
||||
const dx = Math.abs(playerGridX - trigger.x);
|
||||
const dy = Math.abs(playerGridY - trigger.y);
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance <= trigger.radius) {
|
||||
// TRIGGER!
|
||||
this.activateTrigger(trigger, triggerId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a trigger (play audio)
|
||||
*/
|
||||
activateTrigger(trigger, triggerId) {
|
||||
console.log(`🔊 TRIGGER ACTIVATED: ${triggerId} (${trigger.audioKey})`);
|
||||
|
||||
// Mark as triggered
|
||||
this.triggered.add(triggerId);
|
||||
|
||||
// Play audio after delay
|
||||
if (trigger.delay > 0) {
|
||||
this.scene.time.delayedCall(trigger.delay, () => {
|
||||
this.playAudio(trigger);
|
||||
});
|
||||
} else {
|
||||
this.playAudio(trigger);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play audio for trigger
|
||||
*/
|
||||
playAudio(trigger) {
|
||||
// Check if audio exists
|
||||
if (!this.scene.cache.audio.exists(trigger.audioKey)) {
|
||||
console.warn(`⚠️ Audio not found: ${trigger.audioKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Play sound
|
||||
const sound = this.scene.sound.add(trigger.audioKey, {
|
||||
volume: trigger.volume
|
||||
});
|
||||
|
||||
sound.play();
|
||||
|
||||
// Visual feedback (optional)
|
||||
if (trigger.visualDebug) {
|
||||
const worldX = trigger.x * 48 + 24;
|
||||
const worldY = trigger.y * 48 + 24;
|
||||
|
||||
const flash = this.scene.add.circle(worldX, worldY, 30, 0xffff00, 0.8);
|
||||
flash.setDepth(1002);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: flash,
|
||||
alpha: 0,
|
||||
scale: 2,
|
||||
duration: 500,
|
||||
ease: 'Quad.Out',
|
||||
onComplete: () => flash.destroy()
|
||||
});
|
||||
}
|
||||
|
||||
// Callback
|
||||
if (trigger.callback) {
|
||||
trigger.callback();
|
||||
}
|
||||
|
||||
console.log(`✅ Audio played: ${trigger.audioKey}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update - called every frame
|
||||
*/
|
||||
update(playerX, playerY) {
|
||||
if (this.triggers.size === 0) return;
|
||||
|
||||
this.checkTrigger(playerX, playerY);
|
||||
}
|
||||
}
|
||||
171
src/systems/BiomeMusicSystem.js
Normal file
171
src/systems/BiomeMusicSystem.js
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* BiomeMusicSystem.js
|
||||
* Cross-fade background music based on biome transitions
|
||||
*/
|
||||
|
||||
class BiomeMusicSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Music tracks by biome
|
||||
this.biomeTracks = {
|
||||
'grassland': 'music/farm_ambient',
|
||||
'forest': 'music/forest_ambient',
|
||||
'town': 'music/town_theme',
|
||||
'combat': 'music/combat_theme',
|
||||
'night': 'music/night_theme'
|
||||
};
|
||||
|
||||
// Current playing track
|
||||
this.currentTrack = null;
|
||||
this.currentBiome = null;
|
||||
|
||||
// Cross-fade settings
|
||||
this.fadeDuration = 2000; // 2 seconds
|
||||
this.volume = 0.5; // Master volume
|
||||
|
||||
console.log('🎵 BiomeMusicSystem initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload all music tracks
|
||||
*/
|
||||
preload() {
|
||||
Object.entries(this.biomeTracks).forEach(([biome, track]) => {
|
||||
if (this.scene.cache.audio.exists(track)) {
|
||||
console.log(`✅ Music ready: ${track}`);
|
||||
} else {
|
||||
console.warn(`⚠️ Music missing: ${track}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start music for a biome
|
||||
*/
|
||||
playBiomeMusic(biome) {
|
||||
// Skip if already playing this biome's music
|
||||
if (biome === this.currentBiome && this.currentTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trackKey = this.biomeTracks[biome];
|
||||
|
||||
if (!trackKey) {
|
||||
console.warn(`⚠️ No music for biome: ${biome}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if track exists
|
||||
if (!this.scene.cache.audio.exists(trackKey)) {
|
||||
console.warn(`⚠️ Music not loaded: ${trackKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`🎵 Transitioning to: ${biome} (${trackKey})`);
|
||||
|
||||
// Cross-fade to new track
|
||||
this.crossFadeTo(trackKey, biome);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross-fade from current track to new track
|
||||
*/
|
||||
crossFadeTo(newTrackKey, biome) {
|
||||
const oldTrack = this.currentTrack;
|
||||
|
||||
// Create new track
|
||||
const newTrack = this.scene.sound.add(newTrackKey, {
|
||||
loop: true,
|
||||
volume: 0 // Start silent
|
||||
});
|
||||
|
||||
newTrack.play();
|
||||
|
||||
// Fade in new track
|
||||
this.scene.tweens.add({
|
||||
targets: newTrack,
|
||||
volume: this.volume,
|
||||
duration: this.fadeDuration,
|
||||
ease: 'Linear'
|
||||
});
|
||||
|
||||
// Fade out old track if it exists
|
||||
if (oldTrack) {
|
||||
this.scene.tweens.add({
|
||||
targets: oldTrack,
|
||||
volume: 0,
|
||||
duration: this.fadeDuration,
|
||||
ease: 'Linear',
|
||||
onComplete: () => {
|
||||
oldTrack.stop();
|
||||
oldTrack.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update current track
|
||||
this.currentTrack = newTrack;
|
||||
this.currentBiome = biome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all music
|
||||
*/
|
||||
stop() {
|
||||
if (this.currentTrack) {
|
||||
this.scene.tweens.add({
|
||||
targets: this.currentTrack,
|
||||
volume: 0,
|
||||
duration: 1000,
|
||||
ease: 'Linear',
|
||||
onComplete: () => {
|
||||
this.currentTrack.stop();
|
||||
this.currentTrack.destroy();
|
||||
this.currentTrack = null;
|
||||
this.currentBiome = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set master volume
|
||||
*/
|
||||
setVolume(volume) {
|
||||
this.volume = Phaser.Math.Clamp(volume, 0, 1);
|
||||
|
||||
if (this.currentTrack) {
|
||||
this.currentTrack.setVolume(this.volume);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update called every frame
|
||||
* Checks player's current biome and switches music
|
||||
*/
|
||||
update(playerX, playerY) {
|
||||
// Get current biome from biomeSystem
|
||||
if (!this.scene.biomeSystem) return;
|
||||
|
||||
const gridX = Math.floor(playerX / 48);
|
||||
const gridY = Math.floor(playerY / 48);
|
||||
|
||||
const biome = this.scene.biomeSystem.getBiomeAt(gridX, gridY);
|
||||
|
||||
if (biome && biome !== this.currentBiome) {
|
||||
this.playBiomeMusic(biome);
|
||||
}
|
||||
|
||||
// Handle night music override
|
||||
if (this.scene.timeSystem) {
|
||||
const hour = this.scene.timeSystem.currentHour || 12;
|
||||
|
||||
if (hour >= 20 || hour < 6) { // Night time
|
||||
if (this.currentBiome !== 'night') {
|
||||
this.playBiomeMusic('night');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user