Smart Subtitles
This commit is contained in:
@@ -468,6 +468,10 @@ class GameScene extends Phaser.Scene {
|
||||
console.log('♿ Initializing Accessibility System...');
|
||||
this.accessibilitySystem = new AccessibilitySystem(this);
|
||||
|
||||
// Initialize Visual Sound Cue System (for deaf/hard-of-hearing players)
|
||||
console.log('👁️ Initializing Visual Sound Cue System...');
|
||||
this.visualSoundCueSystem = new VisualSoundCueSystem(this);
|
||||
|
||||
// Show epilepsy warning on first launch
|
||||
const hasSeenWarning = localStorage.getItem('novafarma_epilepsy_warning');
|
||||
if (!hasSeenWarning) {
|
||||
@@ -834,6 +838,9 @@ class GameScene extends Phaser.Scene {
|
||||
// NPC Spawner Update
|
||||
if (this.npcSpawner) this.npcSpawner.update(delta);
|
||||
|
||||
// Visual Sound Cue System Update
|
||||
if (this.visualSoundCueSystem) this.visualSoundCueSystem.update();
|
||||
|
||||
// Update NPCs
|
||||
for (const npc of this.npcs) {
|
||||
if (npc.update) npc.update(delta);
|
||||
|
||||
396
src/systems/VisualSoundCueSystem.js
Normal file
396
src/systems/VisualSoundCueSystem.js
Normal file
@@ -0,0 +1,396 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user