This commit is contained in:
2026-01-16 02:43:46 +01:00
parent bc2225ad64
commit 3ae8d39f9c
218 changed files with 87850 additions and 353 deletions

View File

@@ -0,0 +1,746 @@
/**
* 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();
}
}