🎵🎨 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:
@@ -244,6 +244,11 @@
|
|||||||
<!-- 🧪 TEST SCENE -->
|
<!-- 🧪 TEST SCENE -->
|
||||||
<script src="src/scenes/SystemsTestScene.js"></script>
|
<script src="src/scenes/SystemsTestScene.js"></script>
|
||||||
|
|
||||||
|
<!-- 🎵 NEW AUDIO SYSTEMS - JAN 8 2026 -->
|
||||||
|
<script src="src/systems/BiomeMusicSystem.js"></script> <!-- Background music cross-fade -->
|
||||||
|
<script src="src/systems/AudioTriggerSystem.js"></script> <!-- Spatial audio triggers -->
|
||||||
|
<script src="src/scenes/TestVisualAudioScene.js"></script> <!-- Visual & Audio Test Scene -->
|
||||||
|
|
||||||
<script src="src/scenes/GameScene.js"></script>
|
<script src="src/scenes/GameScene.js"></script>
|
||||||
|
|
||||||
<script src="src/game.js"></script>
|
<script src="src/game.js"></script>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const config = {
|
|||||||
debug: false
|
debug: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scene: [BootScene, PreloadScene, SystemsTestScene, DemoScene, DemoSceneEnhanced, TiledTestScene, StoryScene, /* PrologueScene - DISABLED */, GameScene, UIScene, TownSquareScene],
|
scene: [BootScene, PreloadScene, SystemsTestScene, TestVisualAudioScene, DemoScene, DemoSceneEnhanced, TiledTestScene, StoryScene, /* PrologueScene - DISABLED */, GameScene, UIScene, TownSquareScene],
|
||||||
scale: {
|
scale: {
|
||||||
mode: Phaser.Scale.FIT,
|
mode: Phaser.Scale.FIT,
|
||||||
autoCenter: Phaser.Scale.CENTER_BOTH
|
autoCenter: Phaser.Scale.CENTER_BOTH
|
||||||
|
|||||||
321
src/scenes/TestVisualAudioScene.js
Normal file
321
src/scenes/TestVisualAudioScene.js
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
/**
|
||||||
|
* TestVisualAudioScene.js
|
||||||
|
* Demo scene: Kai speaks, dreadlocks wave, leaves fall
|
||||||
|
*/
|
||||||
|
|
||||||
|
class TestVisualAudioScene extends Phaser.Scene {
|
||||||
|
constructor() {
|
||||||
|
super({ key: 'TestVisualAudioScene' });
|
||||||
|
}
|
||||||
|
|
||||||
|
preload() {
|
||||||
|
console.log('🎬 Loading Test Visual & Audio Scene...');
|
||||||
|
|
||||||
|
// Load Kai voice
|
||||||
|
this.load.audio('kai_test_voice', 'assets/audio/voices/kai/kai_test_01.mp3');
|
||||||
|
|
||||||
|
// Load music (if exists)
|
||||||
|
if (this.cache.audio.exists('music/forest_ambient')) {
|
||||||
|
console.log('✅ Music ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Kai sprite (placeholder for now)
|
||||||
|
// this.load.image('kai_idle', 'assets/references/main_characters/kai/animations/idle/kai_idle_frame1.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
create() {
|
||||||
|
console.log('🎨 Creating Test Scene...');
|
||||||
|
|
||||||
|
const width = this.cameras.main.width;
|
||||||
|
const height = this.cameras.main.height;
|
||||||
|
|
||||||
|
// Background
|
||||||
|
this.cameras.main.setBackgroundColor('#7cfc00'); // Grassland green
|
||||||
|
|
||||||
|
// Title
|
||||||
|
const title = this.add.text(width / 2, 50, '🎨 VISUAL & AUDIO TEST', {
|
||||||
|
fontSize: '32px',
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
color: '#ffffff',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 4
|
||||||
|
});
|
||||||
|
title.setOrigin(0.5);
|
||||||
|
title.setDepth(1000);
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
const instructions = this.add.text(width / 2, 100,
|
||||||
|
'Use WASD to move Kai\nStep on YELLOW TILE to trigger voice\nWatch dreadlocks wave in wind\nWatch leaves fall', {
|
||||||
|
fontSize: '16px',
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
color: '#ffffff',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 3,
|
||||||
|
align: 'center'
|
||||||
|
});
|
||||||
|
instructions.setOrigin(0.5);
|
||||||
|
instructions.setDepth(1000);
|
||||||
|
|
||||||
|
// Create ground tiles
|
||||||
|
this.createGround();
|
||||||
|
|
||||||
|
// Create Kai (simple placeholder)
|
||||||
|
this.createKai();
|
||||||
|
|
||||||
|
// Initialize systems
|
||||||
|
this.initSystems();
|
||||||
|
|
||||||
|
// Create falling leaves
|
||||||
|
this.createFallingLeaves();
|
||||||
|
|
||||||
|
// Create wind effect on Kai's dreadlocks
|
||||||
|
this.createWindEffect();
|
||||||
|
|
||||||
|
// Camera follow
|
||||||
|
this.cameras.main.startFollow(this.kai, true, 0.1, 0.1);
|
||||||
|
this.cameras.main.setZoom(1.2);
|
||||||
|
|
||||||
|
// Controls
|
||||||
|
this.cursors = this.input.keyboard.createCursorKeys();
|
||||||
|
this.wasd = {
|
||||||
|
up: this.input.keyboard.addKey('W'),
|
||||||
|
down: this.input.keyboard.addKey('S'),
|
||||||
|
left: this.input.keyboard.addKey('A'),
|
||||||
|
right: this.input.keyboard.addKey('D')
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('✅ Test Scene Ready!');
|
||||||
|
}
|
||||||
|
|
||||||
|
createGround() {
|
||||||
|
// Simple grass tiles
|
||||||
|
const tileSize = 48;
|
||||||
|
const gridWidth = 20;
|
||||||
|
const gridHeight = 15;
|
||||||
|
|
||||||
|
for (let y = 0; y < gridHeight; y++) {
|
||||||
|
for (let x = 0; x < gridWidth; x++) {
|
||||||
|
const worldX = x * tileSize;
|
||||||
|
const worldY = y * tileSize;
|
||||||
|
|
||||||
|
// Grass tile (light/dark alternating)
|
||||||
|
const isLight = (x + y) % 2 === 0;
|
||||||
|
const color = isLight ? 0x7cfc00 : 0x6ab04c;
|
||||||
|
|
||||||
|
const tile = this.add.rectangle(worldX, worldY, tileSize, tileSize, color);
|
||||||
|
tile.setOrigin(0);
|
||||||
|
tile.setAlpha(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio trigger tile (YELLOW)
|
||||||
|
const triggerX = 10;
|
||||||
|
const triggerY = 7;
|
||||||
|
const triggerTile = this.add.rectangle(triggerX * tileSize, triggerY * tileSize, tileSize, tileSize, 0xffff00);
|
||||||
|
triggerTile.setOrigin(0);
|
||||||
|
triggerTile.setAlpha(0.7);
|
||||||
|
|
||||||
|
// Label
|
||||||
|
const label = this.add.text(triggerX * tileSize + 24, triggerY * tileSize + 24, '🎙️', {
|
||||||
|
fontSize: '28px'
|
||||||
|
});
|
||||||
|
label.setOrigin(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
createKai() {
|
||||||
|
// Simplified Kai representation
|
||||||
|
const kaiX = 240;
|
||||||
|
const kaiY = 240;
|
||||||
|
|
||||||
|
// Body
|
||||||
|
this.kai = this.add.circle(kaiX, kaiY, 20, 0xffc0cb); // Pink body
|
||||||
|
this.kai.setDepth(10);
|
||||||
|
|
||||||
|
// Add physics
|
||||||
|
this.physics.world.enable(this.kai);
|
||||||
|
this.kai.body.setCollideWorldBounds(true);
|
||||||
|
this.kai.speed = 150;
|
||||||
|
|
||||||
|
// Dreadlocks (will animate)
|
||||||
|
this.dreadlocks = [];
|
||||||
|
const dreadCount = 8;
|
||||||
|
const radius = 25;
|
||||||
|
|
||||||
|
for (let i = 0; i < dreadCount; i++) {
|
||||||
|
const angle = (i / dreadCount) * Math.PI * 2;
|
||||||
|
const x = kaiX + Math.cos(angle) * radius;
|
||||||
|
const y = kaiY + Math.sin(angle) * radius;
|
||||||
|
|
||||||
|
const dread = this.add.rectangle(x, y, 4, 30, 0xff1493); // Hot pink
|
||||||
|
dread.setOrigin(0.5, 0); // Pivot at top
|
||||||
|
dread.angle = (angle * 180 / Math.PI) + 90;
|
||||||
|
dread.setDepth(9);
|
||||||
|
dread.baseAngle = dread.angle;
|
||||||
|
dread.swayOffset = i * 0.5; // Stagger animation
|
||||||
|
|
||||||
|
this.dreadlocks.push(dread);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name label
|
||||||
|
this.kaiLabel = this.add.text(kaiX, kaiY - 40, 'KAI', {
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
color: '#ffffff',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 3
|
||||||
|
});
|
||||||
|
this.kaiLabel.setOrigin(0.5);
|
||||||
|
this.kaiLabel.setDepth(11);
|
||||||
|
}
|
||||||
|
|
||||||
|
initSystems() {
|
||||||
|
// Audio Trigger System
|
||||||
|
this.audioTriggers = new AudioTriggerSystem(this);
|
||||||
|
|
||||||
|
// Add trigger at yellow tile (10, 7)
|
||||||
|
this.audioTriggers.addTrigger(10, 7, 'kai_test_voice', {
|
||||||
|
radius: 0, // Exact tile only
|
||||||
|
volume: 1.0,
|
||||||
|
oneTime: true,
|
||||||
|
visualDebug: true,
|
||||||
|
callback: () => {
|
||||||
|
console.log('✅ Kai spoke her line!');
|
||||||
|
this.showSpeechBubble();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Biome Music System (if music exists)
|
||||||
|
// this.biomeMusicSystem = new BiomeMusicSystem(this);
|
||||||
|
// this.biomeMusicSystem.playBiomeMusic('grassland');
|
||||||
|
}
|
||||||
|
|
||||||
|
showSpeechBubble() {
|
||||||
|
// Show speech bubble above Kai
|
||||||
|
const bubble = this.add.text(this.kai.x, this.kai.y - 60,
|
||||||
|
'"My name is Kai..."', {
|
||||||
|
fontSize: '12px',
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
color: '#000000',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
padding: { x: 8, y: 4 }
|
||||||
|
});
|
||||||
|
bubble.setOrigin(0.5);
|
||||||
|
bubble.setDepth(100);
|
||||||
|
|
||||||
|
// Fade out after 3 seconds
|
||||||
|
this.tweens.add({
|
||||||
|
targets: bubble,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 1000,
|
||||||
|
delay: 2000,
|
||||||
|
onComplete: () => bubble.destroy()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createFallingLeaves() {
|
||||||
|
// Particle emitter for falling leaves
|
||||||
|
this.leaves = [];
|
||||||
|
|
||||||
|
// Create 20 leaves
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
this.time.delayedCall(i * 300, () => {
|
||||||
|
this.spawnLeaf();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnLeaf() {
|
||||||
|
const x = Phaser.Math.Between(0, this.cameras.main.width);
|
||||||
|
const y = -20;
|
||||||
|
|
||||||
|
const leaf = this.add.ellipse(x, y, 8, 12, 0x90ee90);
|
||||||
|
leaf.setDepth(5);
|
||||||
|
leaf.setAlpha(0.7);
|
||||||
|
|
||||||
|
// Fall animation
|
||||||
|
this.tweens.add({
|
||||||
|
targets: leaf,
|
||||||
|
y: this.cameras.main.height + 50,
|
||||||
|
duration: Phaser.Math.Between(4000, 7000),
|
||||||
|
ease: 'Sine.InOut',
|
||||||
|
onComplete: () => {
|
||||||
|
leaf.destroy();
|
||||||
|
// Spawn new leaf
|
||||||
|
this.spawnLeaf();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sway side-to-side
|
||||||
|
this.tweens.add({
|
||||||
|
targets: leaf,
|
||||||
|
x: x + Phaser.Math.Between(-50, 50),
|
||||||
|
duration: 2000,
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1,
|
||||||
|
ease: 'Sine.InOut'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rotate
|
||||||
|
this.tweens.add({
|
||||||
|
targets: leaf,
|
||||||
|
angle: 360,
|
||||||
|
duration: 3000,
|
||||||
|
repeat: -1,
|
||||||
|
ease: 'Linear'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createWindEffect() {
|
||||||
|
// Dreadlocks sway in wind
|
||||||
|
this.time.addEvent({
|
||||||
|
delay: 50,
|
||||||
|
loop: true,
|
||||||
|
callback: () => {
|
||||||
|
const time = this.time.now / 1000;
|
||||||
|
|
||||||
|
this.dreadlocks.forEach((dread, i) => {
|
||||||
|
const sway = Math.sin(time * 2 + dread.swayOffset) * 15;
|
||||||
|
dread.angle = dread.baseAngle + sway;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (!this.kai) return;
|
||||||
|
|
||||||
|
// Movement
|
||||||
|
this.kai.body.setVelocity(0);
|
||||||
|
|
||||||
|
if (this.cursors.left.isDown || this.wasd.left.isDown) {
|
||||||
|
this.kai.body.setVelocityX(-this.kai.speed);
|
||||||
|
} else if (this.cursors.right.isDown || this.wasd.right.isDown) {
|
||||||
|
this.kai.body.setVelocityX(this.kai.speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cursors.up.isDown || this.wasd.up.isDown) {
|
||||||
|
this.kai.body.setVelocityY(-this.kai.speed);
|
||||||
|
} else if (this.cursors.down.isDown || this.wasd.down.isDown) {
|
||||||
|
this.kai.body.setVelocityY(this.kai.speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update dreadlocks position
|
||||||
|
this.dreadlocks.forEach((dread, i) => {
|
||||||
|
const angle = (i / this.dreadlocks.length) * Math.PI * 2;
|
||||||
|
const radius = 25;
|
||||||
|
dread.x = this.kai.x + Math.cos(angle) * radius;
|
||||||
|
dread.y = this.kai.y + Math.sin(angle) * radius;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update label position
|
||||||
|
this.kaiLabel.setPosition(this.kai.x, this.kai.y - 40);
|
||||||
|
|
||||||
|
// Update audio triggers
|
||||||
|
this.audioTriggers.update(this.kai.x, this.kai.y);
|
||||||
|
|
||||||
|
// ESC to return to menu
|
||||||
|
if (this.input.keyboard.addKey('ESC').isDown) {
|
||||||
|
this.scene.start('GameScene');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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