/** * minigames/typing.js * ⌨️ Tipp-Rennen — Tippe den Text so schnell wie möglich! */ window.MG_typing = (function() { const ID = 'typing'; const EMOJI = '⌨️'; const NAME = 'Tipp-Rennen'; const DESC = 'Tippe das angezeigte Wort so schnell wie möglich!'; const CONTROLS = 'Tastatur'; const MULTI = 3; const WORD_POOLS = { easy: ['Katze','Hund','Baum','Haus','Ball','Buch','Schule','Spiel','Kind','Mond'], medium: ['Abenteuer','Computer','Programm','Zauber','Kristall','Phantom','Roboter','Galaxie'], hard: ['Dinosaurier','Wissenschaft','Programmierung','Abenteuerland','Weltentdecker'], }; function run(wrap, W, H, cfg, onDone) { const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); const theme = cfg.theme || { primary: '#f43f5e' }; const WIN_WPM = cfg.winWpm || 30; // mind. 30 WPM zum Gewinnen const ROUNDS = 3; const allWords = [...WORD_POOLS.easy, ...WORD_POOLS.medium]; let words = []; for (let i = 0; i < ROUNDS; i++) { const pool = i < 2 ? WORD_POOLS.easy : WORD_POOLS.medium; words.push(pool[Math.floor(Math.random() * pool.length)]); } let round = 0; let typed = ''; let startTs = null; let times = []; let stopped = false; let raf, endTimer, result = null; // HTML-Input über dem Canvas const input = document.createElement('input'); input.type = 'text'; input.autocomplete = 'off'; input.autocorrect = 'off'; input.autocapitalize = 'none'; input.spellcheck = false; input.style.cssText = ` position:absolute;left:-9999px;top:0;opacity:0;width:1px;height:1px; `; wrap.style.position = 'relative'; wrap.appendChild(input); setTimeout(() => input.focus(), 100); canvas.addEventListener('click', () => input.focus()); input.addEventListener('input', () => { typed = input.value; if (!startTs && typed.length > 0) startTs = performance.now(); const word = words[round]; if (typed.toLowerCase() === word.toLowerCase()) { const elapsed = (performance.now() - startTs) / 1000 / 60; // Minuten const wpm = Math.round(word.length / 5 / elapsed); // Standard: 5 Zeichen = 1 Wort times.push({ word, wpm }); typed = ''; input.value = ''; startTs = null; round++; if (round >= ROUNDS) { const avgWpm = Math.round(times.reduce((a, b) => a + b.wpm, 0) / times.length); result = avgWpm >= WIN_WPM ? 'win' : 'lose'; endTimer = setTimeout(() => onDone(result === 'win'), 1200); } } }); function loop(ts) { if (stopped) return; raf = requestAnimationFrame(loop); ctx.fillStyle = '#050508'; ctx.fillRect(0, 0, W, H); if (round >= ROUNDS) { const avgWpm = times.length ? Math.round(times.reduce((a,b) => a+b.wpm, 0) / times.length) : 0; MGAPI.resultScreen(ctx, W, H, result === 'win', result === 'win' ? `${avgWpm} WPM — richtig schnell!` : `${avgWpm} WPM — weiter üben!`); return; } const word = words[round]; const elapsed = startTs ? (ts - startTs) / 1000 : 0; // Fortschrittsbalken (Timing-Druck) const timeLimit = 10; const prog = Math.min(1, elapsed / timeLimit); if (prog >= 1 && !endTimer && !result) { result = 'lose'; endTimer = setTimeout(() => onDone(false), 800); } const barW = W - 40; MGAPI.roundRect(ctx, 20, H - 28, barW, 10, 4, 'rgba(255,255,255,0.07)', null); const barColor = prog < 0.6 ? theme.primary : prog < 0.8 ? '#f59e0b' : '#ef4444'; MGAPI.roundRect(ctx, 20, H - 28, barW * (1 - prog), 10, 4, barColor, null); // Rundenanzeige MGAPI.text(ctx, `⌨️ Runde ${round + 1} / ${ROUNDS}`, W / 2, 20, { size: 12, color: theme.primary }); // Zu tippendes Wort MGAPI.roundRect(ctx, W/2-140, H*0.2, 280, 60, 12, 'rgba(255,255,255,0.05)', `${theme.primary}44`); MGAPI.text(ctx, word, W / 2, H * 0.2 + 34, { size: 28, family: "'Fredoka One',cursive", color: '#fff' }); // Eingabe-Anzeige (zeichenweiser Vergleich) const charW = 26; const startX = W / 2 - (word.length * charW) / 2; const charY = H * 0.55; MGAPI.text(ctx, 'Tippe:', W / 2, charY - 24, { size: 11, color: 'rgba(255,255,255,0.4)' }); for (let i = 0; i < word.length; i++) { const cx = startX + i * charW + charW / 2; const tc = (typed[i] || '').toLowerCase(); const wc = word[i].toLowerCase(); let color; if (!typed[i]) color = 'rgba(255,255,255,0.2)'; else if (tc === wc) color = '#22c55e'; else color = '#ef4444'; MGAPI.roundRect(ctx, startX + i * charW, charY - 4, charW - 2, 32, 4, 'rgba(255,255,255,0.04)', `${color}66`); MGAPI.text(ctx, typed[i] || word[i], cx, charY + 12, { size: 18, color }); } // Cursor-Blinken if (startTs || typed.length === 0) { const cx = startX + Math.min(typed.length, word.length) * charW + 2; if (Math.floor(ts / 500) % 2 === 0) { ctx.fillStyle = theme.primary; ctx.fillRect(cx, charY, 3, 32); } } // letzten WPM anzeigen if (times.length > 0) { const last = times[times.length - 1]; MGAPI.text(ctx, `Letztes: ${last.wpm} WPM`, W / 2, H - 42, { size: 11, color: 'rgba(255,255,255,0.35)' }); } } raf = requestAnimationFrame(loop); return { stop() { stopped = true; clearTimeout(endTimer); cancelAnimationFrame(raf); if (input.parentNode) input.parentNode.removeChild(input); }, }; } function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; })();