// ============================================================= // PACK OPENING — dramatic reveal sequence // 1. Pack on table → click → tear animation // 2. Cards fly out one by one, flip from back to front // 3. Reveal summary with NEW badges // ============================================================= const { useState: usePackState, useEffect: usePackEffect, useMemo: usePackMemo } = React; function PackOpening({ state, setState, onDone }) { // phase: idle → tearing → reveal (one card at a time) → summary const [phase, setPhase] = usePackState("idle"); const [pack, setPack] = usePackState(null); // generated cards const [results, setResults] = usePackState([]); // [{pokemon, isNew}] const [revealIdx, setRevealIdx] = usePackState(0); function startOpen() { if (state.packsAvailable <= 0) return; const cards = GameEngine.generatePack(); setPack(cards); setPhase("tearing"); // After tear animation, commit to state and begin reveal setTimeout(() => { setState(prev => { const next = { ...prev, packsAvailable: prev.packsAvailable - 1, packsOpenedTotal: prev.packsOpenedTotal + 1 }; const res = GameEngine.addCardsToCollection(next, cards); // Give coins for new dex entries const newCount = res.filter(r => r.isNew).length; next.coins = (next.coins || 0) + newCount * 5 + cards.length; setResults(res); return next; }); setPhase("reveal"); setRevealIdx(0); }, 900); } function nextCard() { if (revealIdx < (results.length - 1)) { setRevealIdx(revealIdx + 1); } else { setPhase("summary"); } } function openAnother() { setPack(null); setResults([]); setRevealIdx(0); setPhase("idle"); } return (
{/* Top bar */}
SECUENCIA DE APERTURA
SOBRE BÁSICO · 5 CARTAS
SOBRES RESTANTES: {state.packsAvailable}
{/* Background grid */}
{phase === "idle" && 0}/>} {phase === "tearing" && } {phase === "reveal" && results[revealIdx] && ( )} {phase === "summary" && ( 0}/> )}
); } // ---------- Idle pack ready to be opened ---------- function IdlePack({ onOpen, canOpen }) { const [hover, setHover] = usePackState(false); return (
Toca el sobre para abrirlo
setHover(true)} onMouseLeave={()=>setHover(false)} style={{ position:"relative", width:240, height:340, cursor: canOpen ? "pointer" : "not-allowed", transform: `perspective(1200px) rotateY(${hover ? -12 : -4}deg) rotateX(${hover ? 6 : 2}deg) translateY(${hover ? -8 : 0}px)`, transition: "transform 400ms cubic-bezier(0.16,1,0.3,1)", filter: canOpen ? "none" : "grayscale(1) brightness(0.5)", }}>
3 COMUNES · 1 INUSUAL · 1 RARA
★ CHANCE DE LEGENDARIA ★
); } function BigPack({ hover }) { return (
Sobre Pokémon {/* Holo shine sweep */}
{/* Subtle scanlines */}
); } // ---------- Tearing animation ---------- function TearingPack() { return (
); } // ---------- Reveal one card ---------- function RevealCard({ result, idx, total, onNext }) { const [flipped, setFlipped] = usePackState(false); const { pokemon, isNew } = result; const isShiny = pokemon.rarity === "holo" || pokemon.rarity === "legendary"; usePackEffect(() => { const t = setTimeout(() => setFlipped(true), 380); return () => clearTimeout(t); }, []); return (
CARTA {idx + 1} / {total}
{/* Legendary radial burst */} {pokemon.rarity === "legendary" && flipped && (
)} {/* Flip card */}
{/* Status under card */}
{flipped && ( <>
{isNew && ( ★ NUEVA ★ )} {RARITY[pokemon.rarity].symbol} {RARITY[pokemon.rarity].name.toUpperCase()}
o presiona ESPACIO
)}
flipped && onNext()}/>
); } function KeyHandler({ onSpace }) { usePackEffect(() => { const h = (e) => { if (e.code === "Space" || e.code === "Enter") { e.preventDefault(); onSpace(); } }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onSpace]); return null; } // ---------- Summary ---------- function Summary({ results, onAnother, onDone, canOpenAnother }) { const newCount = results.filter(r => r.isNew).length; return (
RESULTADO DEL SOBRE

{newCount === 0 ? "Sin nuevas, pero sumas duplicados" : newCount === 5 ? "¡SOBRE PERFECTO! Todas nuevas." : `${newCount} ${newCount === 1 ? "carta nueva" : "cartas nuevas"} a la Pokédex`}

+{results.length} cartas obtenidas · +{newCount * 5 + results.length} Pokécoins
{results.map((r, i) => (
{r.isNew && (
NUEVA
)}
))}
{canOpenAnother && ( )}
); } // ---------- Button helpers ---------- function btnGhost() { return { fontFamily:"var(--font-sans)", fontWeight:600, fontSize:13, letterSpacing:"0.15em", textTransform:"uppercase", color:"var(--fg-2)", background:"transparent", border:"1px solid var(--border-solid)", borderRadius: 2, padding:"12px 22px", cursor:"pointer", transition:"all 180ms", }; } function btnGhostCyan() { return { ...btnGhost(), color:"var(--accent-cyan)", border:"1px solid var(--accent-cyan)", boxShadow:"0 0 12px var(--accent-cyan-glow)", }; } window.PackOpening = PackOpening; window.btnGhost = btnGhost; window.btnGhostCyan = btnGhostCyan;