/** * VISUAL SOUND CUE SYSTEM * Provides visual indicators for sounds (accessibility for deaf/hard-of-hearing players) */ class VisualSoundCueSystem { constructor(scene) { this.scene = scene; this.enabled = true; // Visual elements this.heartbeatIndicator = null; this.damageIndicators = []; this.screenFlash = null; this.subtitleBackground = null; this.subtitleText = null; this.subtitleSpeaker = null; this.subtitleArrows = { left: null, right: null }; this.heartbeatTween = null; this.fishingBobberIndicator = null; // Speaker color mapping this.speakerColors = { 'Player': '#00ff00', 'NPC': '#ffff00', 'Enemy': '#ff0000', 'System': '#00ffff', 'Narrator': '#ffffff' }; // Subtitle size presets this.subtitleSizes = { 'small': { main: 16, speaker: 12, arrow: 24 }, 'medium': { main: 20, speaker: 16, arrow: 32 }, 'large': { main: 28, speaker: 20, arrow: 40 }, 'very-large': { main: 36, speaker: 24, arrow: 48 } }; // Settings this.settings = { heartbeatEnabled: true, damageIndicatorEnabled: true, screenFlashEnabled: true, subtitlesEnabled: true, directionalArrowsEnabled: true, speakerNamesEnabled: true, subtitleOpacity: 0.8, fishingBobberEnabled: true, subtitleSize: 'medium' // 'small', 'medium', 'large', 'very-large' }; this.loadSettings(); this.init(); console.log('✅ Visual Sound Cue System initialized'); } init() { // Create heartbeat indicator (top-left corner) this.createHeartbeatIndicator(); // Create subtitle container (bottom center) this.createSubtitleContainer(); // Load settings from localStorage // this.loadSettings(); // Moved to constructor } createHeartbeatIndicator() { const x = 200; const y = 30; // Heart emoji as sprite this.heartbeatIndicator = this.scene.add.text(x, y, '❤️', { fontSize: '48px' }); this.heartbeatIndicator.setOrigin(0.5); this.heartbeatIndicator.setDepth(10000); this.heartbeatIndicator.setScrollFactor(0); this.heartbeatIndicator.setVisible(false); } createSubtitleContainer() { const width = this.scene.cameras.main.width; const height = this.scene.cameras.main.height; // Background this.subtitleBackground = this.scene.add.rectangle( width / 2, height - 100, width - 100, 100, 0x000000, this.settings.subtitleOpacity ); this.subtitleBackground.setOrigin(0.5); this.subtitleBackground.setDepth(9999); this.subtitleBackground.setScrollFactor(0); this.subtitleBackground.setVisible(false); // Speaker name text this.subtitleSpeaker = this.scene.add.text( width / 2, height - 130, '', { fontSize: '16px', fontFamily: 'Arial', fontStyle: 'bold', color: '#ffffff', align: 'center' } ); this.subtitleSpeaker.setOrigin(0.5); this.subtitleSpeaker.setDepth(10001); this.subtitleSpeaker.setScrollFactor(0); this.subtitleSpeaker.setVisible(false); // Main subtitle text this.subtitleText = this.scene.add.text( width / 2, height - 100, '', { fontSize: '20px', fontFamily: 'Arial', color: '#ffffff', align: 'center', wordWrap: { width: width - 160 } } ); this.subtitleText.setOrigin(0.5); this.subtitleText.setDepth(10000); this.subtitleText.setScrollFactor(0); this.subtitleText.setVisible(false); // Directional arrows this.subtitleArrows.left = this.scene.add.text( 50, height - 100, '◄', { fontSize: '32px', fontFamily: 'Arial', color: '#ffff00' } ); this.subtitleArrows.left.setOrigin(0.5); this.subtitleArrows.left.setDepth(10001); this.subtitleArrows.left.setScrollFactor(0); this.subtitleArrows.left.setVisible(false); this.subtitleArrows.right = this.scene.add.text( width - 50, height - 100, '►', { fontSize: '32px', fontFamily: 'Arial', color: '#ffff00' } ); this.subtitleArrows.right.setOrigin(0.5); this.subtitleArrows.right.setDepth(10001); this.subtitleArrows.right.setScrollFactor(0); this.subtitleArrows.right.setVisible(false); } // ========== VISUAL HEARTBEAT (LOW HEALTH) ========== updateHeartbeat(healthPercent) { if (!this.settings.heartbeatEnabled) return; // Show heartbeat when health < 30% if (healthPercent < 30) { if (!this.heartbeatActive) { this.startHeartbeat(healthPercent); } else { this.updateHeartbeatSpeed(healthPercent); } } else { this.stopHeartbeat(); } } startHeartbeat(healthPercent) { this.heartbeatActive = true; this.heartbeatIndicator.setVisible(true); // Calculate speed based on health (lower health = faster beat) const speed = Phaser.Math.Clamp(1000 - (healthPercent * 20), 300, 1000); this.heartbeatTween = this.scene.tweens.add({ targets: this.heartbeatIndicator, scale: 1.3, alpha: 0.6, duration: speed / 2, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); console.log('💓 Visual heartbeat started (health:', healthPercent + '%)'); } updateHeartbeatSpeed(healthPercent) { if (!this.heartbeatTween) return; const speed = Phaser.Math.Clamp(1000 - (healthPercent * 20), 300, 1000); this.heartbeatTween.updateTo('duration', speed / 2, true); } stopHeartbeat() { if (!this.heartbeatActive) return; this.heartbeatActive = false; this.heartbeatIndicator.setVisible(false); if (this.heartbeatTween) { this.heartbeatTween.stop(); this.heartbeatTween = null; } this.heartbeatIndicator.setScale(1); this.heartbeatIndicator.setAlpha(1); } // ========== DAMAGE DIRECTION INDICATOR ========== showDamageIndicator(direction, damage) { if (!this.settings.damageIndicatorEnabled) return; const width = this.scene.cameras.main.width; const height = this.scene.cameras.main.height; // Calculate position based on direction let x = width / 2; let y = height / 2; let arrow = '↓'; let rotation = 0; switch (direction) { case 'up': y = 100; arrow = '↑'; rotation = 0; break; case 'down': y = height - 100; arrow = '↓'; rotation = Math.PI; break; case 'left': x = 100; arrow = '←'; rotation = -Math.PI / 2; break; case 'right': x = width - 100; arrow = '→'; rotation = Math.PI / 2; break; } // Create damage indicator const indicator = this.scene.add.text(x, y, arrow, { fontSize: '64px', color: '#ff0000', fontStyle: 'bold' }); indicator.setOrigin(0.5); indicator.setDepth(10001); indicator.setScrollFactor(0); indicator.setRotation(rotation); // Animate and destroy this.scene.tweens.add({ targets: indicator, alpha: 0, scale: 1.5, duration: 800, ease: 'Power2', onComplete: () => { indicator.destroy(); } }); console.log('🎯 Damage indicator shown:', direction, damage); } // ========== SCREEN FLASH NOTIFICATIONS ========== showScreenFlash(type, message) { if (!this.settings.screenFlashEnabled) return; const width = this.scene.cameras.main.width; const height = this.scene.cameras.main.height; let color = 0xffffff; let icon = '⚠️'; switch (type) { case 'danger': color = 0xff0000; icon = '⚠️'; break; case 'warning': color = 0xffaa00; icon = '⚡'; break; case 'info': color = 0x00aaff; icon = 'ℹ️'; break; case 'success': color = 0x00ff00; icon = '✓'; break; } // Flash overlay const flash = this.scene.add.rectangle(0, 0, width, height, color, 0.3); flash.setOrigin(0); flash.setDepth(9998); flash.setScrollFactor(0); // Icon const iconText = this.scene.add.text(width / 2, height / 2, icon, { fontSize: '128px' }); iconText.setOrigin(0.5); iconText.setDepth(9999); iconText.setScrollFactor(0); // Message if (message) { this.showSubtitle(message, 2000); } // Fade out this.scene.tweens.add({ targets: [flash, iconText], alpha: 0, duration: 500, ease: 'Power2', onComplete: () => { flash.destroy(); iconText.destroy(); } }); console.log('⚡ Screen flash shown:', type, message); } // ========== SUBTITLES ========== showSubtitle(text, duration = 3000, speaker = null, direction = null) { if (!this.settings.subtitlesEnabled) return; // Set subtitle text this.subtitleText.setText(text); this.subtitleText.setVisible(true); this.subtitleBackground.setVisible(true); // Show speaker name with color if enabled if (speaker && this.settings.speakerNamesEnabled) { const color = this.speakerColors[speaker] || '#ffffff'; this.subtitleSpeaker.setText(speaker); this.subtitleSpeaker.setColor(color); this.subtitleSpeaker.setVisible(true); } else { this.subtitleSpeaker.setVisible(false); } // Show directional arrows if enabled and direction provided if (direction && this.settings.directionalArrowsEnabled) { this.showDirectionalArrows(direction); } else { this.hideDirectionalArrows(); } // Auto-hide after duration this.scene.time.delayedCall(duration, () => { this.hideSubtitle(); }); console.log('💬 Subtitle shown:', speaker ? `[${speaker}] ${text}` : text); } showDirectionalArrows(direction) { this.hideDirectionalArrows(); if (direction === 'left' || direction === 'west') { this.subtitleArrows.left.setVisible(true); // Pulse animation this.scene.tweens.add({ targets: this.subtitleArrows.left, alpha: { from: 1, to: 0.3 }, duration: 500, yoyo: true, repeat: -1 }); } else if (direction === 'right' || direction === 'east') { this.subtitleArrows.right.setVisible(true); // Pulse animation this.scene.tweens.add({ targets: this.subtitleArrows.right, alpha: { from: 1, to: 0.3 }, duration: 500, yoyo: true, repeat: -1 }); } else if (direction === 'both') { this.subtitleArrows.left.setVisible(true); this.subtitleArrows.right.setVisible(true); // Pulse animation for both this.scene.tweens.add({ targets: [this.subtitleArrows.left, this.subtitleArrows.right], alpha: { from: 1, to: 0.3 }, duration: 500, yoyo: true, repeat: -1 }); } } hideDirectionalArrows() { this.scene.tweens.killTweensOf(this.subtitleArrows.left); this.scene.tweens.killTweensOf(this.subtitleArrows.right); this.subtitleArrows.left.setVisible(false); this.subtitleArrows.right.setVisible(false); this.subtitleArrows.left.setAlpha(1); this.subtitleArrows.right.setAlpha(1); } hideSubtitle() { this.subtitleText.setVisible(false); this.subtitleBackground.setVisible(false); this.subtitleSpeaker.setVisible(false); this.hideDirectionalArrows(); } // ========== SOUND EVENT HANDLERS ========== onSoundPlayed(soundType, data = {}) { if (!this.enabled) return; switch (soundType) { case 'damage': this.showDamageIndicator(data.direction || 'down', data.amount || 10); this.showSubtitle('[DAMAGE TAKEN]', 1500, 'System', data.direction); break; case 'pickup': this.showSubtitle(`[PICKED UP: ${data.item || 'Item'}]`, 1500, 'System'); break; case 'harvest': this.showSubtitle('[CROP HARVESTED]', 1500, 'System'); break; case 'build': this.showSubtitle('[BUILDING PLACED]', 1500, 'System'); break; case 'dig': this.showSubtitle('[DIGGING SOUND]', 1000, 'System'); break; case 'plant': this.showSubtitle('[PLANTING SOUND]', 1000, 'System'); break; case 'footsteps': this.showSubtitle('[FOOTSTEPS]', 500, null, data.direction); break; case 'door': this.showSubtitle('[DOOR OPENS]', 1000, 'System'); break; case 'chest': this.showSubtitle('[CHEST OPENS]', 1000, 'System'); break; case 'water': this.showSubtitle('[WATER SPLASH]', 1000, 'System'); break; case 'fire': this.showSubtitle('[FIRE CRACKLING]', 2000, 'System'); break; case 'explosion': this.showSubtitle('[EXPLOSION!]', 1500, 'System'); this.showScreenFlash('danger', '[EXPLOSION!]'); break; case 'npc_talk': this.showSubtitle(data.text || '[NPC TALKING]', 3000, data.speaker || 'NPC', data.direction); break; case 'enemy_growl': this.showSubtitle('[ENEMY GROWL]', 1500, 'Enemy', data.direction); break; case 'fishing_cast': this.showSubtitle('[FISHING LINE CAST]', 1000, 'System'); break; case 'fishing_bite': this.showSubtitle('[FISH BITING!]', 1500, 'System'); this.showFishingBobberCue(); break; case 'danger': this.showScreenFlash('danger', '[DANGER!]'); this.showSubtitle('[DANGER NEARBY]', 2000, 'System'); break; case 'night': this.showScreenFlash('warning', '[NIGHT FALLING]'); this.showSubtitle('[NIGHT IS FALLING]', 2000, 'System'); break; case 'achievement': this.showScreenFlash('success', data.message || '[ACHIEVEMENT UNLOCKED]'); this.showSubtitle(data.message || '[ACHIEVEMENT UNLOCKED]', 3000, 'System'); break; case 'ui_click': this.showSubtitle('[CLICK]', 300, null); break; case 'ui_hover': this.showSubtitle('[HOVER]', 200, null); break; } } /** * Show fishing bobber visual cue */ showFishingBobberCue() { if (!this.settings.fishingBobberEnabled) return; const width = this.scene.cameras.main.width; const height = this.scene.cameras.main.height; // Create bobber indicator if it doesn't exist if (!this.fishingBobberIndicator) { this.fishingBobberIndicator = this.scene.add.container(width / 2, height / 2); this.fishingBobberIndicator.setDepth(10002); this.fishingBobberIndicator.setScrollFactor(0); // Circle background const circle = this.scene.add.circle(0, 0, 60, 0xff6600, 0.8); this.fishingBobberIndicator.add(circle); // Exclamation mark const exclamation = this.scene.add.text(0, 0, '!', { fontSize: '48px', fontFamily: 'Arial', fontStyle: 'bold', color: '#ffffff' }); exclamation.setOrigin(0.5); this.fishingBobberIndicator.add(exclamation); // Text below const text = this.scene.add.text(0, 80, 'FISH BITING!\nPress E', { fontSize: '20px', fontFamily: 'Arial', fontStyle: 'bold', color: '#ffffff', align: 'center' }); text.setOrigin(0.5); this.fishingBobberIndicator.add(text); } // Show and animate this.fishingBobberIndicator.setVisible(true); this.fishingBobberIndicator.setAlpha(0); // Fade in and pulse this.scene.tweens.add({ targets: this.fishingBobberIndicator, alpha: 1, duration: 200, onComplete: () => { // Pulse animation this.scene.tweens.add({ targets: this.fishingBobberIndicator, scale: { from: 1, to: 1.2 }, duration: 300, yoyo: true, repeat: 5, onComplete: () => { // Fade out this.scene.tweens.add({ targets: this.fishingBobberIndicator, alpha: 0, duration: 300, onComplete: () => { this.fishingBobberIndicator.setVisible(false); } }); } }); } }); console.log('🎣 Fishing bobber cue shown'); } /** * Set subtitle background opacity */ setSubtitleOpacity(opacity) { this.settings.subtitleOpacity = Phaser.Math.Clamp(opacity, 0, 1); if (this.subtitleBackground) { this.subtitleBackground.setAlpha(this.settings.subtitleOpacity); } this.saveSettings(); console.log('📊 Subtitle opacity set to:', this.settings.subtitleOpacity); } /** * Add custom speaker color */ addSpeakerColor(speaker, color) { this.speakerColors[speaker] = color; console.log(`🎨 Added speaker color: ${speaker} = ${color}`); } // ========== SETTINGS ========== toggleHeartbeat(enabled) { this.settings.heartbeatEnabled = enabled; if (!enabled) this.stopHeartbeat(); this.saveSettings(); } toggleDamageIndicator(enabled) { this.settings.damageIndicatorEnabled = enabled; this.saveSettings(); } toggleScreenFlash(enabled) { this.settings.screenFlashEnabled = enabled; this.saveSettings(); } toggleSubtitles(enabled) { this.settings.subtitlesEnabled = enabled; if (!enabled) this.hideSubtitle(); this.saveSettings(); console.log('💬 Subtitles:', enabled ? 'ENABLED' : 'DISABLED'); } toggleDirectionalArrows(enabled) { this.settings.directionalArrowsEnabled = enabled; if (!enabled) { this.hideDirectionalArrows(); } this.saveSettings(); console.log('➡️ Directional Arrows:', enabled ? 'ENABLED' : 'DISABLED'); } toggleSpeakerNames(enabled) { this.settings.speakerNamesEnabled = enabled; this.saveSettings(); console.log('👤 Speaker Names:', enabled ? 'ENABLED' : 'DISABLED'); } toggleFishingBobber(enabled) { this.settings.fishingBobberEnabled = enabled; this.saveSettings(); console.log('🎣 Fishing Bobber Cue:', enabled ? 'ENABLED' : 'DISABLED'); } /** * Set subtitle text size * @param {string} size - 'small', 'medium', 'large', 'very-large' */ setSubtitleSize(size) { if (!this.subtitleSizes[size]) { console.error(`Invalid subtitle size: ${size}. Valid options: small, medium, large, very-large`); return; } this.settings.subtitleSize = size; const sizes = this.subtitleSizes[size]; // Update text sizes if (this.subtitleText) { this.subtitleText.setFontSize(sizes.main); } if (this.subtitleSpeaker) { this.subtitleSpeaker.setFontSize(sizes.speaker); } if (this.subtitleArrows.left) { this.subtitleArrows.left.setFontSize(sizes.arrow); } if (this.subtitleArrows.right) { this.subtitleArrows.right.setFontSize(sizes.arrow); } // Adjust background height based on text size if (this.subtitleBackground) { const bgHeight = sizes.main * 4; // 4x font size for padding this.subtitleBackground.setSize(this.subtitleBackground.width, bgHeight); } this.saveSettings(); console.log(`📏 Subtitle size set to: ${size.toUpperCase()} (${sizes.main}px)`); } saveSettings() { localStorage.setItem('novafarma_visual_sound_cues', JSON.stringify(this.settings)); } loadSettings() { const saved = localStorage.getItem('novafarma_visual_sound_cues'); if (saved) { this.settings = { ...this.settings, ...JSON.parse(saved) }; } } // ========== UPDATE ========== update() { // Update heartbeat based on player health if (this.scene.player && this.scene.player.health !== undefined) { const healthPercent = (this.scene.player.health / this.scene.player.maxHealth) * 100; this.updateHeartbeat(healthPercent); } } destroy() { if (this.heartbeatTween) this.heartbeatTween.stop(); if (this.heartbeatSprite) this.heartbeatSprite.destroy(); if (this.subtitleText) this.subtitleText.destroy(); if (this.subtitleBackground) this.subtitleBackground.destroy(); } }