// 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 ( {Array.from({length: 20}).map((_,i)=>( ))} ); } // modernPlay return ( {Array.from({length: 30}).map((_,i)=>{ const x=(i*17)%100, y=(i*23)%100, r=(i%4)*0.3+0.2; 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;