// Shared in-game shell: HUD with score/timer/lives + pause const GameShell = ({ children, score, timeLeft, totalTime, lives, totalLives, onQuit, label }) => { const t = window.MP_I18N.t; const pct = totalTime ? Math.max(0, Math.min(100, (timeLeft / totalTime) * 100)) : 0; return (
{label || ''}
{t('score')}
{score ?? 0}
{totalTime != null && ( <>
{t('time')}
{Math.max(0, Math.ceil(timeLeft || 0))}
)} {totalLives != null && ( <>
{t('lives')}
{Array.from({ length: totalLives }).map((_, i) => ( = lives ? ' gone' : '')} /> ))}
)}
{children}
); }; // Big problem display with op coloring const Problem = ({ a, b, op = '×', blank, status }) => { const cls = 'problem ' + (status === 'wrong' ? 'shake' : status === 'right' ? 'pop' : ''); return (
{blank === 'a' ? ? : {a}} {op} {blank === 'b' ? ? : {b}} = {blank === 'r' ? ? : {a * b}}
); }; // Numpad for typed answers const Numpad = ({ value, onChange, onSubmit }) => { const press = (k) => { window.MP_SOUND.tap(); if (k === 'del') onChange(String(value).slice(0, -1)); else if (k === 'ok') onSubmit(); else if (String(value).length < 4) onChange(String(value) + k); }; return (
{['1','2','3','4','5','6','7','8','9'].map(n => ( ))}
); }; // Multi-choice options const Options = ({ choices, onPick, picked, correct }) => (
{choices.map((c, i) => { let cls = 'option-btn'; if (picked != null) { if (c === correct) cls += ' correct'; else if (c === picked) cls += ' wrong'; else cls += ' disabled'; } return ( ); })}
); // Countdown "3, 2, 1, GO!" const Countdown = ({ onDone }) => { const [n, setN] = React.useState(3); React.useEffect(() => { if (n < 0) { onDone(); return; } window.MP_SOUND.countdown(); const t = setTimeout(() => setN(n - 1), 700); return () => clearTimeout(t); }, [n]); return (
{n > 0 ? n : window.MP_I18N.t('go')}
); }; window.GameShell = GameShell; window.Problem = Problem; window.Numpad = Numpad; window.Options = Options; window.Countdown = Countdown;