// 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 (
{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 => (
press(n)}>{n}
))}
press('del')} aria-label="borrar">⌫
press('0')}>0
press('ok')}>OK
);
};
// 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 (
onPick(c)} disabled={picked != null}>{c}
);
})}
);
// 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;