diff --git a/index.html b/index.html index 8cae329e6..c01e6a85c 100644 --- a/index.html +++ b/index.html @@ -244,6 +244,11 @@ + + + + + diff --git a/src/game.js b/src/game.js index 42dbd4709..6a1ba3ce0 100644 --- a/src/game.js +++ b/src/game.js @@ -68,7 +68,7 @@ const config = { 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: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH diff --git a/src/scenes/TestVisualAudioScene.js b/src/scenes/TestVisualAudioScene.js new file mode 100644 index 000000000..f7a2624b0 --- /dev/null +++ b/src/scenes/TestVisualAudioScene.js @@ -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'); + } + } +} diff --git a/src/systems/AudioTriggerSystem.js b/src/systems/AudioTriggerSystem.js new file mode 100644 index 000000000..6f8252611 --- /dev/null +++ b/src/systems/AudioTriggerSystem.js @@ -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); + } +} diff --git a/src/systems/BiomeMusicSystem.js b/src/systems/BiomeMusicSystem.js new file mode 100644 index 000000000..dacbe0695 --- /dev/null +++ b/src/systems/BiomeMusicSystem.js @@ -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'); + } + } + } + } +}