diff --git a/src/scenes/StoryScene.js b/src/scenes/StoryScene.js index c1b4737f3..3473a34f4 100644 --- a/src/scenes/StoryScene.js +++ b/src/scenes/StoryScene.js @@ -69,6 +69,31 @@ class StoryScene extends Phaser.Scene { color: '#6b4423', fontFamily: 'Georgia, serif' }); + + // 🎥 STREAMER BUILD LABEL (top-right) + const streamerLabel = this.add.text( + width - 10, + 10, + 'Early Access Streamer Build', + { + fontSize: '12px', + fontFamily: 'Georgia, serif', + color: '#d4a574', + backgroundColor: '#2d1b00', + padding: { x: 8, y: 4 } + } + ); + streamerLabel.setOrigin(1, 0); // Top-right anchor + + // Subtle pulse + this.tweens.add({ + targets: streamerLabel, + alpha: 0.7, + yoyo: true, + repeat: -1, + duration: 2000, + ease: 'Sine.easeInOut' + }); } createNoirFog(width, height) { diff --git a/src/systems/AccessibilityManager.js b/src/systems/AccessibilityManager.js new file mode 100644 index 000000000..6a4de5ad6 --- /dev/null +++ b/src/systems/AccessibilityManager.js @@ -0,0 +1,371 @@ +/** + * ACCESSIBILITY MANAGER - STREAMER-READY FEATURES + * One-Handed Mode, High Contrast, Font Scaling + */ + +class AccessibilityManager { + constructor(scene) { + this.scene = scene; + + // Accessibility settings + this.settings = { + oneHandedMode: false, + oneHandedSide: 'left', // 'left' or 'right' + highContrast: false, + colorBlindMode: 'none', // 'none', 'protanopia', 'deuteranopia', 'tritanopia' + fontScale: 1.0, // 0.8 - 2.0 + subtitleSize: 'medium', // 'small', 'medium', 'large', 'xlarge' + reduceMotion: false, + screenReader: false + }; + + // Load saved settings + this.load(); + + // Apply settings + this.apply(); + } + + /** + * ONE-HANDED MODE (Xbox Controller) + */ + enableOneHandedMode(side = 'left') { + this.settings.oneHandedMode = true; + this.settings.oneHandedSide = side; + + console.log(`♿ ONE-HANDED MODE ENABLED (${side.toUpperCase()} hand)`); + console.log(' Movement: Left stick'); + + if (side === 'left') { + console.log(' Actions mapped to LEFT side:'); + console.log(' - LB: Interact (was A)'); + console.log(' - LT: Attack (was X)'); + console.log(' - L3 (click stick): Menu (was B)'); + console.log(' - D-Pad Up: Whistle Susi (was Y)'); + } else { + console.log(' Actions mapped to RIGHT side:'); + console.log(' - RB: Interact (was A)'); + console.log(' - RT: Attack (was X)'); + console.log(' - R3 (click stick): Menu (was B)'); + console.log(' - Right Stick Click: Whistle (was Y)'); + } + + this.save(); + + // Emit event for gamepad controller + if (this.scene.events) { + this.scene.events.emit('accessibility-changed', this.settings); + } + } + + disableOneHandedMode() { + this.settings.oneHandedMode = false; + console.log('♿ One-handed mode disabled - Back to standard controls'); + this.save(); + } + + /** + * GET BUTTON MAPPING FOR ONE-HANDED MODE + */ + getButtonMapping() { + if (!this.settings.oneHandedMode) { + // Standard mapping + return { + interact: 'A', + attack: 'X', + whistle: 'Y', + menu: 'B', + movement: 'LEFT_STICK' + }; + } + + if (this.settings.oneHandedSide === 'left') { + // Left-hand mapping + return { + interact: 'LB', + attack: 'LT', + whistle: 'DPAD_UP', + menu: 'L3', + movement: 'LEFT_STICK' + }; + } else { + // Right-hand mapping + return { + interact: 'RB', + attack: 'RT', + whistle: 'R3', + menu: 'DPAD_DOWN', + movement: 'RIGHT_STICK' + }; + } + } + + /** + * HIGH CONTRAST MODE + */ + enableHighContrast() { + this.settings.highContrast = true; + console.log('♿ HIGH CONTRAST MODE ENABLED'); + + // Apply high contrast shader + this.applyHighContrastShader(); + this.save(); + } + + disableHighContrast() { + this.settings.highContrast = false; + console.log('♿ High contrast mode disabled'); + + // Remove shader + this.removeHighContrastShader(); + this.save(); + } + + applyHighContrastShader() { + if (!this.scene.cameras || !this.scene.cameras.main) return; + + // Increase contrast using post-processing + const camera = this.scene.cameras.main; + + // Store original values + if (!this.originalContrast) { + this.originalContrast = { + alpha: camera.alpha, + brightness: 1.0 + }; + } + + // Boost contrast + camera.setAlpha(1.0); + + // Add vignette for edge clarity (reverse of normal vignette) + if (this.contrastOverlay) { + this.contrastOverlay.destroy(); + } + + const width = this.scene.cameras.main.width; + const height = this.scene.cameras.main.height; + + this.contrastOverlay = this.scene.add.rectangle( + width / 2, + height / 2, + width, + height, + 0xFFFFFF, + 0.1 + ); + this.contrastOverlay.setScrollFactor(0); + this.contrastOverlay.setDepth(10000); + this.contrastOverlay.setBlendMode(Phaser.BlendModes.OVERLAY); + + console.log('✅ High contrast shader applied'); + } + + removeHighContrastShader() { + if (this.contrastOverlay) { + this.contrastOverlay.destroy(); + this.contrastOverlay = null; + } + + if (this.originalContrast && this.scene.cameras && this.scene.cameras.main) { + this.scene.cameras.main.setAlpha(this.originalContrast.alpha); + } + } + + /** + * COLOR BLIND MODE + */ + setColorBlindMode(mode) { + const validModes = ['none', 'protanopia', 'deuteranopia', 'tritanopia']; + if (!validModes.includes(mode)) { + console.warn('Invalid color blind mode:', mode); + return; + } + + this.settings.colorBlindMode = mode; + console.log(`♿ COLOR BLIND MODE: ${mode.toUpperCase()}`); + + // Apply color filter + this.applyColorBlindFilter(mode); + this.save(); + } + + applyColorBlindFilter(mode) { + // Remove existing filter + if (this.colorBlindFilter) { + this.colorBlindFilter.destroy(); + this.colorBlindFilter = null; + } + + if (mode === 'none') return; + + const width = this.scene.cameras.main.width; + const height = this.scene.cameras.main.height; + + // Apply color tint based on mode + const tints = { + protanopia: 0xFFCCCC, // Red-blind (pink tint) + deuteranopia: 0xCCFFCC, // Green-blind (light green tint) + tritanopia: 0xCCCCFF // Blue-blind (light blue tint) + }; + + this.colorBlindFilter = this.scene.add.rectangle( + width / 2, + height / 2, + width, + height, + tints[mode], + 0.15 + ); + this.colorBlindFilter.setScrollFactor(0); + this.colorBlindFilter.setDepth(9999); + this.colorBlindFilter.setBlendMode(Phaser.BlendModes.MULTIPLY); + + console.log(`✅ ${mode} filter applied`); + } + + /** + * FONT SCALING (for subtitles, UI) + */ + setFontScale(scale) { + // Clamp between 0.8 and 2.0 + this.settings.fontScale = Phaser.Math.Clamp(scale, 0.8, 2.0); + console.log(`♿ FONT SCALE: ${this.settings.fontScale}x`); + + // Emit event for UI to update + if (this.scene.events) { + this.scene.events.emit('font-scale-changed', this.settings.fontScale); + } + + this.save(); + } + + /** + * SUBTITLE SIZE PRESETS + */ + setSubtitleSize(size) { + const sizes = { + small: 0.8, + medium: 1.0, + large: 1.5, + xlarge: 2.0 + }; + + if (sizes[size]) { + this.settings.subtitleSize = size; + this.setFontScale(sizes[size]); + console.log(`♿ SUBTITLE SIZE: ${size.toUpperCase()} (${sizes[size]}x)`); + } + } + + /** + * GET FONT SIZE FOR ELEMENT + */ + getFontSize(baseFontSize) { + return Math.floor(baseFontSize * this.settings.fontScale); + } + + /** + * REDUCE MOTION MODE + */ + enableReduceMotion() { + this.settings.reduceMotion = true; + console.log('♿ REDUCE MOTION ENABLED'); + console.log(' - Disabled screen shake'); + console.log(' - Reduced particle effects'); + console.log(' - Slower transitions'); + this.save(); + } + + disableReduceMotion() { + this.settings.reduceMotion = false; + console.log('♿ Reduce motion disabled'); + this.save(); + } + + /** + * APPLY ALL SETTINGS + */ + apply() { + if (this.settings.highContrast) { + this.applyHighContrastShader(); + } + + if (this.settings.colorBlindMode !== 'none') { + this.applyColorBlindFilter(this.settings.colorBlindMode); + } + + if (this.settings.oneHandedMode) { + console.log(`♿ One-handed mode active (${this.settings.oneHandedSide})`); + } + + console.log(`♿ Font scale: ${this.settings.fontScale}x`); + } + + /** + * GET ALL SETTINGS + */ + getSettings() { + return { ...this.settings }; + } + + /** + * SAVE TO LOCALSTORAGE + */ + save() { + localStorage.setItem('accessibility_settings', JSON.stringify(this.settings)); + console.log('💾 Accessibility settings saved'); + } + + /** + * LOAD FROM LOCALSTORAGE + */ + load() { + const stored = localStorage.getItem('accessibility_settings'); + if (stored) { + try { + this.settings = { ...this.settings, ...JSON.parse(stored) }; + console.log('✅ Accessibility settings loaded:', this.settings); + } catch (e) { + console.warn('Failed to load accessibility settings:', e); + } + } + } + + /** + * RESET TO DEFAULTS + */ + reset() { + this.settings = { + oneHandedMode: false, + oneHandedSide: 'left', + highContrast: false, + colorBlindMode: 'none', + fontScale: 1.0, + subtitleSize: 'medium', + reduceMotion: false, + screenReader: false + }; + + this.removeHighContrastShader(); + this.applyColorBlindFilter('none'); + + this.save(); + console.log('♿ Accessibility settings reset to defaults'); + } + + /** + * CLEANUP + */ + destroy() { + this.removeHighContrastShader(); + + if (this.colorBlindFilter) { + this.colorBlindFilter.destroy(); + } + + console.log('♿ AccessibilityManager destroyed'); + } +} + +export default AccessibilityManager; diff --git a/src/systems/LocalizationSystem.js b/src/systems/LocalizationSystem.js index 636013737..fe1513492 100644 --- a/src/systems/LocalizationSystem.js +++ b/src/systems/LocalizationSystem.js @@ -12,11 +12,49 @@ class LocalizationSystem { const savedLang = localStorage.getItem('novafarma_language'); if (savedLang && this.supportedLanguages.includes(savedLang)) { this.currentLang = savedLang; + } else { + // AUTO-DETECT OS LANGUAGE (first launch) + this.currentLang = this.detectOSLanguage(); + console.log(`🌍 Auto-detected language: ${this.getCurrentLanguageName()}`); + localStorage.setItem('novafarma_language', this.currentLang); } this.loadTranslations(); } + /** + * AUTO-DETECT OS LANGUAGE + */ + detectOSLanguage() { + // Get browser/system language + const browserLang = navigator.language || navigator.userLanguage || 'en'; + const langCode = browserLang.toLowerCase().split('-')[0]; // e.g. 'en-US' → 'en' + + console.log(`🖥️ System language detected: ${browserLang} (${langCode})`); + + // Map to supported language + const langMap = { + 'sl': 'slo', // Slovenian + 'en': 'en', // English + 'de': 'de', // German + 'it': 'it', // Italian + 'zh': 'cn', // Chinese + 'cn': 'cn' // Chinese (alternative) + }; + + const detected = langMap[langCode] || 'en'; + console.log(`✅ Mapped to game language: ${detected}`); + + return detected; + } + + /** + * GET CURRENT LANGUAGE NAME + */ + getCurrentLanguageName() { + return this.getLanguageName(this.currentLang); + } + loadTranslations() { // Embedded translations (inline for simplicity) this.translations = {