diff --git a/components/TypingGame.tsx b/components/TypingGame.tsx index 42dce95..018abc7 100644 --- a/components/TypingGame.tsx +++ b/components/TypingGame.tsx @@ -25,7 +25,7 @@ const FALLBACK_CONTENT = 'fff jjj fff jjj'; const EN_DASH = '\u2013'; const TypingGame: React.FC = ({ stage, subLevelId, content: contentProp, onFinish, onBack, onRetry, gameMode = 'STANDARD' }) => { - const { keyboardLayout } = useSettings(); + const { keyboardLayout, zeroMistakesMode } = useSettings(); const { t, language } = useI18n(); const { playTyping, playError } = useSound(); const keyboardLayoutConfig = KEYBOARD_LAYOUTS[keyboardLayout]; @@ -143,7 +143,14 @@ const TypingGame: React.FC = ({ stage, subLevelId, content: con e.preventDefault(); if (startTime === null) setStartTime(Date.now()); if (key === targetKey) return; // Richtige Taste mit Modifier – nicht vorrücken, kein Fehler + playError(); + + if (zeroMistakesMode) { + onRetry(); + return; + } + setMistakes(m => m + 1); setErrorCountByChar(prev => { const next = { ...prev }; @@ -198,6 +205,11 @@ const TypingGame: React.FC = ({ stage, subLevelId, content: con } else { playError(); + if (zeroMistakesMode) { + onRetry(); + return; + } + setMistakes(m => m + 1); setErrorCountByChar(prev => { const next = { ...prev }; @@ -211,7 +223,7 @@ const TypingGame: React.FC = ({ stage, subLevelId, content: con setErrorKey(null); }, 300); } - }, [content, inputIndex, mistakes, finishGame, startTime, onBack, errorCountByChar, stage.id, playTyping, playError]); + }, [content, inputIndex, mistakes, finishGame, startTime, onBack, errorCountByChar, stage.id, playTyping, playError, zeroMistakesMode, onRetry]); const handleKeyUp = useCallback((e: KeyboardEvent) => { const normalized = (k: string) => (k === 'Minus' ? '-' : k === 'Comma' ? ',' : k === 'Period' ? '.' : k === 'Enter' ? '\n' : k); diff --git a/contexts/SettingsContext.tsx b/contexts/SettingsContext.tsx index 15b1672..fdd7fff 100644 --- a/contexts/SettingsContext.tsx +++ b/contexts/SettingsContext.tsx @@ -4,6 +4,7 @@ import { KeyboardLayout, Language } from '../types'; const LANGUAGE_STORAGE_KEY = 'tippsy_language'; const KEYBOARD_STORAGE_KEY = 'tippsy_keyboard_layout'; const SOUND_STORAGE_KEY = 'tippsy_sound_enabled'; +const ZERO_MISTAKES_STORAGE_KEY = 'tippsy_zero_mistakes_mode'; const getStoredValue = (key: string) => { try { @@ -32,13 +33,21 @@ const getInitialSoundEnabled = (): boolean => { return true; }; +const getInitialZeroMistakesMode = (): boolean => { + const stored = getStoredValue(ZERO_MISTAKES_STORAGE_KEY); + if (stored === '1' || stored === 'true') return true; + return false; +}; + interface SettingsContextValue { language: Language; keyboardLayout: KeyboardLayout; soundEnabled: boolean; + zeroMistakesMode: boolean; setLanguage: (language: Language) => void; setKeyboardLayout: (layout: KeyboardLayout) => void; setSoundEnabled: (enabled: boolean) => void; + setZeroMistakesMode: (enabled: boolean) => void; } const SettingsContext = createContext(undefined); @@ -47,6 +56,7 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil const [language, setLanguage] = useState(() => getInitialLanguage()); const [keyboardLayout, setKeyboardLayout] = useState(() => getInitialKeyboardLayout(getInitialLanguage())); const [soundEnabled, setSoundEnabled] = useState(() => getInitialSoundEnabled()); + const [zeroMistakesMode, setZeroMistakesMode] = useState(() => getInitialZeroMistakesMode()); useEffect(() => { try { @@ -75,7 +85,24 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil } }, [soundEnabled]); - const value = useMemo(() => ({ language, keyboardLayout, soundEnabled, setLanguage, setKeyboardLayout, setSoundEnabled }), [language, keyboardLayout, soundEnabled]); + useEffect(() => { + try { + localStorage.setItem(ZERO_MISTAKES_STORAGE_KEY, zeroMistakesMode ? '1' : '0'); + } catch { + // ignore + } + }, [zeroMistakesMode]); + + const value = useMemo(() => ({ + language, + keyboardLayout, + soundEnabled, + zeroMistakesMode, + setLanguage, + setKeyboardLayout, + setSoundEnabled, + setZeroMistakesMode + }), [language, keyboardLayout, soundEnabled, zeroMistakesMode]); return {children}; }; diff --git a/i18n.ts b/i18n.ts index 05c0ae7..95acaae 100644 --- a/i18n.ts +++ b/i18n.ts @@ -192,6 +192,8 @@ export const translations = { languageHint: 'Choose the interface language and content.', keyboardHint: 'Match your physical keyboard layout.', soundHint: 'Play sounds when typing, on errors, and in the menu.', + zeroMistakes: 'Zero Mistake Mode', + zeroMistakesHint: 'Level restarts immediately upon any error. Only perfection counts!', english: 'English (default)', german: 'German', qwerty: 'US QWERTY', @@ -480,6 +482,8 @@ export const translations = { languageHint: 'Wähle die Sprache für Oberfläche und Inhalte.', keyboardHint: 'Passe das Layout an deine physische Tastatur an.', soundHint: 'Sounds beim Tippen, bei Fehlern und im Menü abspielen.', + zeroMistakes: 'Zero Mistake Modus', + zeroMistakesHint: 'Level wird bei einem Fehler sofort neu gestartet. Nur Perfektion zählt!', english: 'Englisch (Standard)', german: 'Deutsch', qwerty: 'US QWERTY', diff --git a/package-lock.json b/package-lock.json index 1de6cd8..55d6c8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "tippsy---10-finger-trainer", + "name": "tippsy-tries-typing", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "tippsy---10-finger-trainer", + "name": "tippsy-tries-typing", "version": "0.0.0", "dependencies": { "lucide-react": "^0.563.0", diff --git a/pages/Settings.tsx b/pages/Settings.tsx index df20753..f59ca0f 100644 --- a/pages/Settings.tsx +++ b/pages/Settings.tsx @@ -50,7 +50,7 @@ interface SettingsProps { } const Settings: React.FC = ({ onBack }) => { - const { language, keyboardLayout, soundEnabled, setLanguage, setKeyboardLayout, setSoundEnabled } = useSettings(); + const { language, keyboardLayout, soundEnabled, zeroMistakesMode, setLanguage, setKeyboardLayout, setSoundEnabled, setZeroMistakesMode } = useSettings(); const { t } = useI18n(); const { playMenuClick } = useSound(); const fileInputRef = useRef(null); @@ -194,6 +194,27 @@ const Settings: React.FC = ({ onBack }) => { +
+

{t('settings.zeroMistakes')}

+

{t('settings.zeroMistakesHint')}

+
+ + +
+
+

{t('settings.data')}

{t('settings.exportHint')}