import { css, keyframes } from '@emotion/react'; import { useState, useEffect, useCallback, useRef } from 'react'; import { adaptive } from '@toss/tds-colors'; // ──────────────────────────────────────────────────────────── // Types // ──────────────────────────────────────────────────────────── interface AchievementReward { gold?: number; permanentGoldMultiplier?: number; permanentSpawnMultiplier?: number; title?: string; boostType?: string; } interface Achievement { id: string; name: string; description: string; category: string; reward: AchievementReward; isHidden: boolean; } // ──────────────────────────────────────────────────────────── // Animations // ──────────────────────────────────────────────────────────── const slideUp = keyframes` 0% { transform: translateY(100px); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } `; const slideDown = keyframes` 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(100px); opacity: 0; } `; const titleReveal = keyframes` 0% { transform: scale(0.6) translateY(30px); opacity: 0; } 60% { transform: scale(1.08) translateY(-6px); opacity: 1; } 100% { transform: scale(1) translateY(0); opacity: 1; } `; const starSparkle = keyframes` 0% { transform: scale(0) rotate(0deg); opacity: 0; } 50% { transform: scale(1.3) rotate(20deg); opacity: 1; } 100% { transform: scale(1) rotate(0deg); opacity: 1; } `; const fadePop = keyframes` 0% { opacity: 0; } 100% { opacity: 1; } `; // ──────────────────────────────────────────────────────────── // Toast styles // ──────────────────────────────────────────────────────────── const toastContainerStyle = (exiting: boolean) => css` position: fixed; bottom: 84px; left: 16px; right: 16px; z-index: 600; animation: ${exiting ? slideDown : slideUp} 0.35s ease-out forwards; `; const toastCardStyle = css` background: linear-gradient(135deg, #1a0a00, #3d1a00); border: 1.5px solid #ffd700; border-radius: 16px; padding: 14px 16px; display: flex; align-items: center; gap: 12px; box-shadow: 0 8px 32px rgba(255, 180, 0, 0.3), 0 2px 8px rgba(0, 0, 0, 0.4); `; const toastIconStyle = css` font-size: 28px; flex-shrink: 0; animation: ${starSparkle} 0.5s ease-out forwards; `; const toastContentStyle = css` flex: 1; min-width: 0; `; const toastHeadingStyle = css` font-size: 11px; color: #ffcc80; font-weight: 600; letter-spacing: 0.8px; text-transform: uppercase; margin-bottom: 2px; `; const toastNameStyle = css` font-size: 15px; font-weight: 700; color: #ffd700; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `; const toastRewardStyle = css` font-size: 12px; color: #ffcc80; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `; const toastProgressBarStyle = css` position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: rgba(255, 215, 0, 0.15); border-radius: 0 0 16px 16px; overflow: hidden; `; const TOAST_DURATION_MS = 3000; const toastProgressFillStyle = css` height: 100%; background: linear-gradient(90deg, #ffd700, #ff9800); border-radius: 0 0 16px 16px; animation: shrink linear ${TOAST_DURATION_MS}ms forwards; @keyframes shrink { from { width: 100%; } to { width: 0%; } } `; // ──────────────────────────────────────────────────────────── // Title popup (full-screen for title-granting achievements) // ──────────────────────────────────────────────────────────── const titleOverlayStyle = css` position: fixed; inset: 0; background: rgba(0, 0, 0, 0.92); z-index: 700; display: flex; flex-direction: column; align-items: center; justify-content: center; animation: ${fadePop} 0.3s ease-out; `; const titleCardStyle = css` display: flex; flex-direction: column; align-items: center; gap: 12px; animation: ${titleReveal} 0.7s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; `; const titleGlowEmoji = css` font-size: 72px; `; const titlePopupHeading = css` font-size: 13px; color: #ffcc80; letter-spacing: 2px; text-transform: uppercase; `; const titlePopupName = css` font-size: 26px; font-weight: 800; color: #ffd700; text-align: center; text-shadow: 0 0 30px rgba(255, 215, 0, 0.8); padding: 0 24px; `; const titlePopupSub = css` font-size: 13px; color: #b39ddb; text-align: center; `; const titlePopupDismissStyle = css` margin-top: 32px; padding: 12px 36px; border-radius: 24px; background: rgba(255, 215, 0, 0.12); border: 1px solid rgba(255, 215, 0, 0.4); color: #ffd700; font-size: 15px; font-weight: 600; cursor: pointer; `; // ──────────────────────────────────────────────────────────── // Helper // ──────────────────────────────────────────────────────────── const CATEGORY_ICON: Record = { beginner: '🌱', intermediate: '⚡', advanced: '🔥', prestige: '✨', }; function formatReward(reward: AchievementReward): string { const parts: string[] = []; if (reward.gold) parts.push(`💰 +${reward.gold.toLocaleString()}G`); if (reward.permanentGoldMultiplier) parts.push(`🏅 골드 +${(reward.permanentGoldMultiplier * 100).toFixed(0)}%`); if (reward.permanentSpawnMultiplier) parts.push(`🏅 스폰 +${(reward.permanentSpawnMultiplier * 100).toFixed(0)}%`); if (reward.title) parts.push(`👑 칭호 획득`); return parts.join(' · '); } // ──────────────────────────────────────────────────────────── // AchievementToast component // ──────────────────────────────────────────────────────────── interface ToastItem { id: string; achievement: Achievement; } export function AchievementToast() { const [queue, setQueue] = useState([]); const [current, setCurrent] = useState(null); const [exiting, setExiting] = useState(false); const [titlePopup, setTitlePopup] = useState(null); const dismissTimer = useRef | null>(null); const clearDismissTimer = useCallback(() => { if (dismissTimer.current !== null) { clearTimeout(dismissTimer.current); dismissTimer.current = null; } }, []); const dismissCurrent = useCallback(() => { clearDismissTimer(); setExiting(true); setTimeout(() => { setExiting(false); setCurrent(null); }, 350); }, [clearDismissTimer]); // Show next toast when current is done useEffect(() => { if (current === null && queue.length > 0) { const [next, ...rest] = queue; setQueue(rest); setCurrent(next); } }, [current, queue]); // Auto-dismiss after TOAST_DURATION_MS useEffect(() => { if (current === null) return; clearDismissTimer(); // If this achievement has a title reward, show full-screen popup if (current.achievement.reward.title) { dismissTimer.current = setTimeout(() => { dismissCurrent(); setTitlePopup(current.achievement); }, TOAST_DURATION_MS); } else { dismissTimer.current = setTimeout(dismissCurrent, TOAST_DURATION_MS); } return clearDismissTimer; }, [current, dismissCurrent, clearDismissTimer]); // Listen to game store custom events useEffect(() => { const handler = (e: Event) => { const ach = (e as CustomEvent).detail; const item: ToastItem = { id: `${ach.id}-${Date.now()}`, achievement: ach, }; setQueue((prev) => [...prev, item]); }; window.addEventListener('newAchievement', handler); return () => window.removeEventListener('newAchievement', handler); }, []); const icon = current ? (CATEGORY_ICON[current.achievement.category] ?? '🏅') : ''; const rewardText = current ? formatReward(current.achievement.reward) : ''; return ( <> {current !== null && (
{icon}
🏆 업적 달성
{current.achievement.name}
{rewardText &&
{rewardText}
}
)} {titlePopup !== null && (
setTitlePopup(null)}>
👑
✨ 칭호 잠금해제 ✨
{titlePopup.reward.title}
{titlePopup.name}
)} ); }