/** * 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; // Settings this.settings = { heartbeatEnabled: true, damageIndicatorEnabled: true, screenFlashEnabled: true, subtitlesEnabled: true }; // Visual elements this.heartbeatSprite = null; this.damageIndicators = []; this.subtitleText = null; this.subtitleBackground = null; // Heartbeat state this.heartbeatActive = false; this.heartbeatTween = null; this.init(); console.log('✅ VisualSoundCueSystem initialized'); } init() { // Create heartbeat indicator (top-left corner) this.createHeartbeatIndicator(); // Create subtitle container (bottom center) this.createSubtitleContainer(); // Load settings from localStorage this.loadSettings(); } createHeartbeatIndicator() { const x = 200; const y = 30; // Heart emoji as sprite this.heartbeatSprite = this.scene.add.text(x, y, 'â¤ī¸', { fontSize: '48px' }); this.heartbeatSprite.setOrigin(0.5); this.heartbeatSprite.setDepth(10000); this.heartbeatSprite.setScrollFactor(0); this.heartbeatSprite.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, 80, 0x000000, 0.8 ); this.subtitleBackground.setOrigin(0.5); this.subtitleBackground.setDepth(9999); this.subtitleBackground.setScrollFactor(0); this.subtitleBackground.setVisible(false); // Text this.subtitleText = this.scene.add.text( width / 2, height - 100, '', { fontSize: '20px', fontFamily: 'Arial', color: '#ffffff', align: 'center', wordWrap: { width: width - 120 } } ); this.subtitleText.setOrigin(0.5); this.subtitleText.setDepth(10000); this.subtitleText.setScrollFactor(0); this.subtitleText.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.heartbeatSprite.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.heartbeatSprite, 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.heartbeatSprite.setVisible(false); if (this.heartbeatTween) { this.heartbeatTween.stop(); this.heartbeatTween = null; } this.heartbeatSprite.setScale(1); this.heartbeatSprite.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) { if (!this.settings.subtitlesEnabled) return; // Format text with speaker name if provided let displayText = text; if (speaker) { displayText = `[${speaker}]: ${text}`; } this.subtitleText.setText(displayText); this.subtitleText.setVisible(true); this.subtitleBackground.setVisible(true); // Auto-hide after duration this.scene.time.delayedCall(duration, () => { this.hideSubtitle(); }); console.log('đŸ’Ŧ Subtitle shown:', displayText); } hideSubtitle() { this.subtitleText.setVisible(false); this.subtitleBackground.setVisible(false); } // ========== 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); break; case 'pickup': this.showSubtitle(`[PICKED UP: ${data.item || 'Item'}]`, 1500); break; case 'harvest': this.showSubtitle('[CROP HARVESTED]', 1500); break; case 'build': this.showSubtitle('[BUILDING PLACED]', 1500); break; case 'danger': this.showScreenFlash('danger', '[DANGER!]'); break; case 'night': this.showScreenFlash('warning', '[NIGHT FALLING]'); break; case 'achievement': this.showScreenFlash('success', data.message || '[ACHIEVEMENT UNLOCKED]'); break; } } // ========== 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(); } 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(); } }