// ============================================================================= // APP — Router, state, top bar, toasts, tweaks // ============================================================================= const { useState, useEffect, useCallback } = React; const STORAGE_KEY = 'slot-legends-v1'; const DEFAULT_STATE = { credits: 500, collection: {}, // cardId → { count, firstSeen } packsToday: 2, nextReset: null, defeatedChamps: [], claimedCollections: [], unlockedBadges: [], }; function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return { ...DEFAULT_STATE }; return { ...DEFAULT_STATE, ...JSON.parse(raw) }; } catch { return { ...DEFAULT_STATE }; } } function saveState(s) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); } catch {} } function App() { // --- Tweaks --- const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "packsPerDay": 2, "packSize": 5, "showRarityWeights": false, "bestOf": 5, "primaryAccent": "cyan", "demoSeed": false }/*EDITMODE-END*/; const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); // --- Routing --- const [route, setRoute] = useState('hub'); const [routeParam, setRouteParam] = useState(null); // --- Game state --- const [state, setState] = useState(() => loadState()); const [toast, setToast] = useState(null); const [modal, setModal] = useState(null); // Persist whenever state changes useEffect(() => { saveState(state); }, [state]); // First-run demo seed → unlock 12 cards so battle/collection is interesting useEffect(() => { if (Object.keys(state.collection).length === 0 && t.demoSeed) { const seed = ['gates-olympus','sweet-bonanza','starburst','book-of-dead','wanted-dead-wild', 'reactoonz','san-quentin','le-bandit','fortune-tiger','wolf-gold','gemix','beam-boys']; const coll = {}; seed.forEach(id => { coll[id] = { count: 1, firstSeen: Date.now() }; }); setState(s => ({ ...s, collection: coll })); } }, [t.demoSeed]); const showToast = useCallback((msg, kind='info') => { setToast({ msg, kind, id: Date.now() }); setTimeout(() => setToast(null), 3000); }, []); // --- Actions --- const actions = { go: (r, param=null) => { setRoute(r); setRouteParam(param); window.scrollTo(0,0); }, battle: (champId) => { setRoute('battle'); setRouteParam(champId); window.scrollTo(0,0); }, openedPack: (cardIds) => { setState(s => { const coll = { ...s.collection }; let newCount = 0; cardIds.forEach(id => { if (!coll[id]) { coll[id] = { count: 1, firstSeen: Date.now() }; newCount++;} else { coll[id] = { ...coll[id], count: coll[id].count + 1 }; } }); const nextPacks = s.packsToday - 1; // Check for completed collections const nextClaimed = [...s.claimedCollections]; const nextCredits = s.credits; let creditsDelta = 0; window.GAME_DATA.COLLECTIONS.forEach(col => { if (s.claimedCollections.includes(col.id)) return; const have = window.GAME_DATA.CARDS.filter(c => c.provider === col.provider && coll[c.id]).length; if (have === col.total) { nextClaimed.push(col.id); creditsDelta += col.prize.credits; setTimeout(() => showToast(`◆ COLECCIÓN ${window.GAME_DATA.PROVIDERS[col.provider].name} COMPLETA · +${col.prize.credits} CRD`, 'success'), 200); } }); showToast(`+${cardIds.length} cartas (${newCount} nuevas)`, 'info'); const nextReset = nextPacks <= 0 ? (Date.now() + 24*3600*1000) : s.nextReset; return { ...s, collection: coll, packsToday: nextPacks, credits: nextCredits + creditsDelta, claimedCollections: nextClaimed, nextReset }; }); // Return to hub setTimeout(() => actions.go('hub'), 200); }, resetDaily: () => { setState(s => ({ ...s, packsToday: t.packsPerDay || 2, nextReset: null })); showToast('◆ SOBRES RECARGADOS', 'success'); }, championDefeated: (champId, reward) => { setState(s => { if (s.defeatedChamps.includes(champId)) return s; const coll = { ...s.collection }; if (reward.card && !coll[reward.card]) { coll[reward.card] = { count: 1, firstSeen: Date.now() }; } return { ...s, defeatedChamps: [...s.defeatedChamps, champId], credits: s.credits + (reward.credits || 0), collection: coll, }; }); showToast(`◆ ¡CAMPEÓN DERROTADO! +${reward.credits} CRD · 1× MÍTICA`, 'success'); }, resetGame: () => { if (confirm('¿Reiniciar todo el progreso? Se borrarán cartas, créditos y campeones.')) { setState({ ...DEFAULT_STATE }); showToast('◈ Progreso reiniciado', 'info'); } }, }; // Render route let screen = null; if (route === 'hub') screen = ; else if (route === 'packs') screen = ; else if (route === 'collection') screen = ; else if (route === 'champions') screen = ; else if (route === 'battle') screen = ; return (
{screen}
{toast && } setTweak('packsPerDay', v)}/> setTweak('packSize', v)}/> setTweak('demoSeed', v)}/> { const all = {}; window.GAME_DATA.CARDS.forEach(c => { all[c.id] = { count:1, firstSeen: Date.now() }; }); setState(s => ({...s, collection: { ...s.collection, ...all }})); showToast('◆ Colección desbloqueada (modo demo)', 'success'); }}/>
Balance: {state.credits.toLocaleString()} CRD
setState(s => ({...s, credits: s.credits + 5000}))}/>
); } // ─── TOP BAR ──────────────────────────────────────────────────────────── function TopBar({ route, state, actions }) { const nav = [ { id: 'hub', label: 'HUB' }, { id: 'packs', label: 'SOBRES' }, { id: 'collection', label: 'COLECCIÓN' }, { id: 'champions', label: 'DESAFÍOS' }, ]; const ownedCount = Object.keys(state.collection).length; const total = window.GAME_DATA.CARDS.length + 5; return (
actions.go('hub')}>
SL
SLOT · LEGENDS
{nav.map(n => ( ))}
CARTAS · {ownedCount}/{total}
SOBRES · {state.packsToday}/2
{state.credits.toLocaleString()}
); } // ─── TOAST ────────────────────────────────────────────────────────────── function Toast({ toast }) { const color = toast.kind === 'success' ? 'var(--accent-lime)' : toast.kind === 'error' ? 'var(--accent-orange)' : 'var(--accent-cyan)'; return (
{toast.msg}
); } // Wire up const root = ReactDOM.createRoot(document.getElementById('root')); root.render();