// Game shell: progress map, header (timer, stars, attempts), activity host.
// Exposes as the entry point.
// (Hooks are accessed via React.useState etc. to avoid collision with other Babel files.)
// ---------- 8 Activities metadata ----------
const ACTIVITY_LIBRARY = {
memoria: { id: "memoria", title: "Memoria", subtitle: "Empareja sumas iguales", icon: "🧠", kind: "memory", type: "Memoria", x: 12, y: 78 },
asociacion: { id: "asociacion", title: "Asociación", subtitle: "Une cada cuenta con su resultado", icon: "🔗", kind: "match", type: "Asociación", x: 28, y: 60 },
ordenar: { id: "ordenar", title: "Ordenar", subtitle: "Coloca los números en orden", icon: "📊", kind: "order", type: "Ordenar", x: 44, y: 75 },
completar: { id: "completar", title: "Completar", subtitle: "Rellena los huecos que faltan", icon: "✏️", kind: "fill", type: "Completar", x: 58, y: 50 },
sopa_letras: { id: "sopa", title: "Sopa de letras",subtitle: "Encuentra los resultados", icon: "🔍", kind: "wordsearch",type: "Sopa de letras", x: 72, y: 65 },
crucigrama: { id: "crucigrama", title: "Crucigrama", subtitle: "Resuelve el crucigrama numérico", icon: "🧩", kind: "crossword", type: "Crucigrama", x: 36, y: 35 },
rompecabezas:{ id: "puzzle", title: "Rompecabezas", subtitle: "Reconstruye la imagen", icon: "🖼️", kind: "puzzle", type: "Rompecabezas", x: 64, y: 25 },
calculo: { id: "calculo", title: "Cálculo", subtitle: "Escribe el resultado correcto", icon: "⌨️", kind: "answer", type: "Cálculo", x: 86, y: 40 },
};
const ACTIVITY_ORDER = [
"memoria",
"asociacion",
"ordenar",
"completar",
"sopa_letras",
"crucigrama",
"rompecabezas",
"calculo",
];
function normalizeTipo(tipo) {
const aliases = {
asociacion_simple: "asociacion",
asociacion_compleja: "asociacion",
ordenar_elementos: "ordenar",
completar_textos: "completar",
rellenar_agujeros: "completar",
respuesta_escrita: "calculo",
puzzle_doble: "rompecabezas",
puzzle_intercambio: "rompecabezas",
puzzle_agujero: "rompecabezas",
};
return aliases[tipo] || tipo;
}
function buildActivities(gameData) {
const ejercicios = Array.isArray(gameData?.ejercicios) ? gameData.ejercicios : [];
const byTipo = {};
ejercicios.forEach((e) => {
const t = normalizeTipo(e?.tipo);
if (!ACTIVITY_LIBRARY[t]) return;
if (!byTipo[t]) byTipo[t] = [];
byTipo[t].push(e);
});
const activeTypes = ejercicios.length > 0
? ACTIVITY_ORDER.filter((t) => byTipo[t]?.length)
: ACTIVITY_ORDER;
return activeTypes.map((tipo) => {
const base = ACTIVITY_LIBRARY[tipo];
const ex = byTipo[tipo]?.[0];
return {
...base,
title: ex?.titulo || base.title,
subtitle: ex?.instrucciones || base.subtitle,
data: ex?.contenido || null,
};
});
}
// ---------- Local storage for progress (per theme) ----------
function useProgress(themeKey, progressKey) {
const scope = progressKey || themeKey;
const key = `mateaventura:progress:${scope}`;
const [progress, setProgress] = React.useState(() => {
try { return JSON.parse(localStorage.getItem(key)) || {}; } catch { return {}; }
});
React.useEffect(() => { localStorage.setItem(key, JSON.stringify(progress)); }, [progress, key]);
const complete = React.useCallback((id, stars) => {
setProgress(p => ({ ...p, [id]: { stars: Math.max(stars, p[id]?.stars || 0), done: true } }));
}, []);
const reset = () => setProgress({});
return [progress, complete, reset];
}
// ---------- Star display ----------
function Stars({ count, total = 3, size = 20, color }) {
return (
{Array.from({ length: total }).map((_, i) => (
))}
);
}
function Star({ filled, size, color }) {
return (
);
}
// ---------- Header ----------
function Header({ theme, title, subtitle, totalCompleted, totalExercises, scorePercentage, onBack, onReset, attempts, timer, onToggleSound, soundOn }) {
return (
{onBack && (
)}
{title}
{subtitle &&
{subtitle}
}
{timer != null && (
⏱ {formatTime(timer)}
)}
{attempts != null && (
❤ {attempts}
)}
{totalCompleted}/{totalExercises}
📊
{scorePercentage}%
{onReset && (
)}
);
}
function formatTime(s) {
const m = Math.floor(s / 60), r = s % 60;
return `${m}:${String(r).padStart(2,"0")}`;
}
// ---------- Adventure Map ----------
function AdventureMap({ theme, progress, onPick, themeKey, activities }) {
const path = React.useMemo(() => {
// Build a smooth curve through activity points
return activities.map(a => `${a.x},${a.y}`).join(" L ");
}, [activities]);
const isUnlocked = (i) => i === 0 || progress[activities[i-1].id]?.done;
return (
{activities.map((a, i) => {
const unlocked = isUnlocked(i);
const stars = progress[a.id]?.stars || 0;
const done = progress[a.id]?.done;
return (
);
})}
);
}
function MapBackground({ themeKey }) {
// Decorative scenery per theme — abstract hills/clouds, no copyrighted shapes
if (themeKey === "retro2000") {
return (
);
}
if (themeKey === "retroClean") {
return (
);
}
// modernPlay
return (
);
}
// ---------- Celebration overlay ----------
function Celebration({ stars, theme, onClose, message }) {
React.useEffect(() => { window.SFX && window.SFX.win(); }, []);
return (
e.stopPropagation()}>
{Array.from({length:18}).map((_,i)=>(
))}
¡Genial!
{message}
);
}
// ---------- Failure overlay ----------
function FailScreen({ theme, onRetry, onBack, message }) {
React.useEffect(() => { window.SFX && window.SFX.lose(); }, []);
return (
💔
¡Inténtalo otra vez!
{message}
);
}
// ---------- Main ----------
function MateAventura({ themeKey: initialKey = "retro2000", embedded = false, tweaks: extTweaks, gameData = null, progressKey = null }) {
const tweaks = extTweaks || {
theme: initialKey,
difficulty: "media",
speed: 1,
sound: true,
fontScale: 1,
};
const themeKey = tweaks.theme || initialKey;
const theme = window.THEMES[themeKey];
const activities = React.useMemo(() => buildActivities(gameData), [gameData]);
const [progress, complete, reset] = useProgress(themeKey, progressKey);
const [active, setActive] = React.useState(null);
const [result, setResult] = React.useState(null); // {stars, message} | "fail"
const [soundOn, setSoundOn] = React.useState(true);
React.useEffect(() => { window.SOUND_ENABLED = soundOn && (tweaks.sound !== false); }, [soundOn, tweaks.sound]);
const totalCompleted = activities.filter(a => progress[a.id]?.done).length;
const totalExercises = activities.length;
const totalStars = activities.reduce((s, a) => s + (progress[a.id]?.stars || 0), 0);
const maxPossibleStars = activities.length * 3;
const scorePercentage = maxPossibleStars > 0 ? Math.round((totalStars / maxPossibleStars) * 100) : 0;
const cssVars = {
"--bg": theme.bg, "--surface": theme.surface, "--panel": theme.panel,
"--ink": theme.ink, "--ink-soft": theme.inkSoft,
"--primary": theme.primary, "--primary-ink": theme.primaryInk,
"--secondary": theme.secondary, "--accent": theme.accent,
"--danger": theme.danger, "--success": theme.success, "--star": theme.star,
"--font-display": theme.fontDisplay, "--font-body": theme.fontBody,
"--radius": theme.radius, "--radius-sm": theme.radiusSm,
"--card-shadow": theme.cardShadow, "--node-shadow": theme.nodeShadow,
"--font-scale": tweaks.fontScale || 1,
"--speed": tweaks.speed || 1,
"--btn-bevel-bg": theme.btnBevel,
background: theme.bgPattern,
};
return (
setActive(null) : null}
onReset={!active ? () => { if(confirm("¿Reiniciar progreso?")) reset(); } : null}
onToggleSound={() => setSoundOn(s => !s)}
soundOn={soundOn}
/>
{!active && (
{ window.SFX && window.SFX.click(); setActive(a); }} />
)}
{active && (
{
complete(active.id, stars);
setResult({ stars, message });
}}
onFail={(message) => setResult({ fail: true, message })}
/>
)}
{result && !result.fail && (
{ setResult(null); setActive(null); }}/>
)}
{result && result.fail && (
{ setResult(null); setActive(null); }}
onRetry={() => { setResult(null); /* remount via key */ }}/>
)}
);
}
window.MateAventura = MateAventura;
window.ACTIVITIES = ACTIVITY_ORDER.map((t) => ACTIVITY_LIBRARY[t]);
window.Stars = Stars;