170 lines
6 KiB
JavaScript
170 lines
6 KiB
JavaScript
/**
|
|
* 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 };
|
|
|
|
})();
|