import { useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { StyleTheme } from "../context/settings"; import { UmaCharacter, loadUmaCharacters } from "../utils/umaCharacters"; import "./WheelConfetti.scss"; type WheelTheme = StyleTheme; type Props = { theme: WheelTheme, /** Mění se s každým výsledkem — nová hodnota spustí novou salvu. */ burstKey: number, /** Velká salva (finální výsledek) vs. menší (mezivýsledek). */ big?: boolean, }; type UmaImage = { src: string, round: boolean }; const SIMPLE_COLORS = ['#16a34a', '#0ea5e9', '#f59e0b', '#8b5cf6', '#ec4899', '#14b8a6', '#ef4444']; const UMA_EMOJI = ['🐴', '🏇', '🥕', '🏆', '🌸', '✨', '⭐', '🍙', '🎀', '🥇']; // Hlášky z fandomu Umamusume (Umapyoi Densetsu, Teio step, hachimī, Golshi, Oguri „okawari"...) const UMA_BADGES = ['1着!', 'Umazing!', 'Umapyoi!', 'Teio step!', 'Hachimī~', 'Golshi approves', 'Okawari!', 'Pakapaka~', 'ウマい!']; const UMA_BADGE_COLORS = ['#ff6fa3', '#2e9a3a', '#43b8f0', '#f2a91a']; const DURATION_MS = 6400; const rand = (min: number, max: number) => min + Math.random() * (max - min); const pick = (list: T[]) => list[Math.floor(Math.random() * list.length)]; type Piece = { id: number, style: React.CSSProperties, content: React.ReactNode, className: string }; type Launch = 'cannon-left' | 'cannon-right' | 'burst' | 'rain'; const LAUNCHES: Launch[] = ['cannon-left', 'cannon-left', 'cannon-right', 'cannon-right', 'burst', 'rain']; /** * Výchozí pozice a dráha jednoho kousku: děla z dolních rohů střílí šikmo vzhůru, * „výbuch" se rozletí ze středu a déšť padá opožděně shora. Kousek nejprve vyletí * do vrcholu (--x1/--y1) a pak dopadne mimo obrazovku (--x2/--y2). */ function launchStyle(launch: Launch): React.CSSProperties { let left: number, top: number, x1: number, y1: number, x2: number, y2: number, delay: number; switch (launch) { case 'cannon-left': case 'cannon-right': { const dir = launch === 'cannon-left' ? 1 : -1; left = launch === 'cannon-left' ? rand(-3, 3) : rand(97, 103); top = rand(88, 100); x1 = dir * rand(12, 62); y1 = -rand(45, 88); x2 = x1 + dir * rand(6, 22); y2 = rand(12, 25); delay = rand(0, 450); break; } case 'burst': { const angle = rand(0, Math.PI * 2); const radius = rand(18, 48); left = 50; top = 42; x1 = Math.cos(angle) * radius; y1 = Math.sin(angle) * radius * 0.8 - rand(5, 15); x2 = x1 * 1.35; y2 = rand(70, 85); delay = rand(0, 250); break; } default: { left = rand(0, 100); top = -12; x1 = rand(-8, 8); y1 = rand(35, 55); x2 = x1 + rand(-10, 10); y2 = rand(115, 125); delay = rand(400, 1800); } } const spin = rand(180, 720) * (Math.random() < 0.5 ? -1 : 1); return { left: `${left}vw`, top: `${top}vh`, animationName: launch === 'rain' ? 'confetti-rain' : 'confetti-throw', animationDelay: `${delay}ms`, animationDuration: `${rand(2600, 3900)}ms`, ['--x1' as any]: `${x1}vw`, ['--y1' as any]: `${y1}vh`, ['--x2' as any]: `${x2}vw`, ['--y2' as any]: `${y2}vh`, ['--r1' as any]: `${spin * 0.4}deg`, ['--r2' as any]: `${spin}deg`, }; } function buildPieces(theme: WheelTheme, big: boolean, images: UmaImage[]): Piece[] { if (theme === 'simple') { const count = big ? 170 : 80; return Array.from({ length: count }, (_, id) => ({ id, className: 'confetti-paper', content: null, style: { ...launchStyle(pick(LAUNCHES)), width: `${rand(6, 11)}px`, height: `${rand(10, 18)}px`, background: pick(SIMPLE_COLORS), borderRadius: Math.random() < 0.3 ? '50%' : '2px', }, })); } const count = big ? 50 : 24; return Array.from({ length: count }, (_, id) => { const roll = Math.random(); // Samolepky se tolik netočí, ať jsou čitelné const base: React.CSSProperties = { ...launchStyle(pick(LAUNCHES)), ['--r1' as any]: `${rand(-25, 25)}deg`, ['--r2' as any]: `${rand(-40, 40)}deg`, }; if (images.length && roll < 0.45) { const image = pick(images); return { id, className: `confetti-uma confetti-uma-img${image.round ? ' round' : ''}`, style: base, content: }; } if (roll < 0.7) { return { id, className: 'confetti-uma confetti-uma-badge', content: pick(UMA_BADGES), style: { ...base, background: pick(UMA_BADGE_COLORS) } }; } return { id, className: 'confetti-uma confetti-uma-emoji', style: { ...base, fontSize: `${rand(28, 52)}px` }, content: pick(UMA_EMOJI) }; }); } /** Salva konfet po výsledku kola štěstí, ve vzhledu podle zvoleného stylu. */ export default function WheelConfetti({ theme, burstKey, big = false }: Readonly) { const [characters, setCharacters] = useState([]); const [active, setActive] = useState(false); useEffect(() => { if (theme === 'umamusume') loadUmaCharacters().then(setCharacters); }, [theme]); useEffect(() => { if (!burstKey) return; setActive(true); const t = setTimeout(() => setActive(false), DURATION_MS + 900); return () => clearTimeout(t); }, [burstKey]); const pieces = useMemo( () => buildPieces(theme, big, characters.flatMap(c => c.images.map(src => ({ src, round: !!c.round })))), // eslint-disable-next-line react-hooks/exhaustive-deps [burstKey], ); // Při finálním výsledku „promluví" náhodná postava svou hláškou const speaker = useMemo(() => { const withQuote = characters.filter(c => c.quote); return theme === 'umamusume' && big && withQuote.length ? pick(withQuote) : undefined; // eslint-disable-next-line react-hooks/exhaustive-deps }, [burstKey]); if (!active || window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return null; return createPortal(
{theme === 'umamusume' && big &&
🏆 1着 🏆
} {pieces.map(p => {p.content})} {speaker && (

„{speaker.quote}"

— {speaker.name}
)}
, document.body, ); }