Files
novafarma/EMERGENCY_SYSTEMS_RECOVERY/VisualSoundCueSystem.js
2026-01-16 02:43:46 +01:00

747 lines
23 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();
}
}