import { useEffect, useState, useRef } from 'react'; import { usePartyKit } from './hooks/usePartyKit'; import { ClickButton } from './components/ClickButton'; import { Counter } from './components/Counter'; import { UpgradeShop } from './components/UpgradeShop'; import { Milestones } from './components/Milestones'; import { Leaderboard } from './components/Leaderboard'; import { Background } from './components/Background'; import { MILESTONES } from './config/milestones'; import { SignedIn, SignedOut, SignInButton, UserButton } from '@clerk/clerk-react'; import { useAuth } from '@clerk/clerk-react'; import { ClickableMascot } from './components/ClickableMascot'; // Import ClickableMascot component import { ClickableMascot as ClickableMascotType, Upgrade } from './types'; // Import ClickableMascotType and Upgrade import { UPGRADES } from './config/upgrades'; // Import UPGRADES for upgrade config import { useLocation, Link } from 'wouter'; // Import wouter hooks import AdminPage from './components/AdminPage'; // Import AdminPage import { NewsMarquee } from './components/NewsMarquee'; // Import NewsMarquee function App() { const { isSignedIn, isLoaded, userId: clerkUserId } = useAuth(); // Get clerkUserId from useAuth const { gameState, sendClick, purchaseUpgrade, userId, sendMascotClickBonus, lastMessage } = usePartyKit(); // Renamed sendShoominionClickBonus const [celebrationMessage, setCelebrationMessage] = useState(null); const [previousMilestones, setPreviousMilestones] = useState>({}); const [mascotEntities, setMascotEntities] = useState([]); const timeoutRef = useRef(null); const gameStateRef = useRef(gameState); // Ref to hold the latest gameState const clickButtonContainerRef = useRef(null); // New ref for the click button container const [location] = useLocation(); // Get current location from wouter const [adminBroadcastMessage, setAdminBroadcastMessage] = useState(null); // New state for admin messages // Admin user ID from .env const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID; const isAdmin = clerkUserId === CLERK_ADMIN_USERID; // Effect for milestone celebrations useEffect(() => { if (gameState) { const newMilestones = Object.keys(gameState.milestones).filter( (milestoneId) => gameState.milestones[milestoneId] && !previousMilestones[milestoneId] ); if (newMilestones.length > 0) { const milestone = MILESTONES.find(m => m.id === newMilestones[0]); if (milestone) { setCelebrationMessage(milestone.reward); setTimeout(() => setCelebrationMessage(null), 5000); } } setPreviousMilestones(gameState.milestones); } }, [gameState, previousMilestones]); // Effect for receiving admin broadcast messages useEffect(() => { if (lastMessage) { try { const parsedMessage = JSON.parse(lastMessage); if (parsedMessage.type === 'admin-message') { setAdminBroadcastMessage(parsedMessage.message); setTimeout(() => setAdminBroadcastMessage(null), 7000); // Message disappears after 7 seconds } } catch (error) { console.error('Failed to parse last message in App.tsx:', error); } } }, [lastMessage]); // Effect to keep gameStateRef updated useEffect(() => { gameStateRef.current = gameState; }, [gameState]); // Effect for spawning mascots useEffect(() => { // Use gameStateRef.current for initial check and to get latest state inside spawnMascot if (!gameStateRef.current) return; const friendBoostUpgrade = UPGRADES.find(u => u.id === 'friendBoost') as Upgrade | undefined; const ownedFriendBoost = gameStateRef.current.upgrades['friendBoost']?.owned || 0; if (timeoutRef.current) { clearTimeout(timeoutRef.current); } if (ownedFriendBoost > 0 && friendBoostUpgrade && friendBoostUpgrade.mascotTiers) { const baseInterval = 12000; // Increased base interval for less frequent spawns const minInterval = 1000; // Increased min interval const interval = Math.max(minInterval, baseInterval / (1 + ownedFriendBoost * 0.2)); // Adjusted scaling console.log(`Spawning mascots every ${interval} ms for friendBoost level ${ownedFriendBoost}`); const currentMascotTiers = friendBoostUpgrade.mascotTiers; // Ensure mascotTiers is not undefined const spawnMascot = () => { // Access the latest gameState from the ref inside the closure const currentGameState = gameStateRef.current; if (!currentGameState) return; // Should not happen if initial check passes const buttonContainer = clickButtonContainerRef.current; // Use the ref if (!buttonContainer) { console.warn('Click button container ref not found!'); timeoutRef.current = setTimeout(spawnMascot, 1000); // Retry after 1 second return; } const rect = buttonContainer.getBoundingClientRect(); const spawnAreaPadding = 150; const minX = rect.left - spawnAreaPadding; const maxX = rect.right + spawnAreaPadding; const minY = rect.top - spawnAreaPadding; const maxY = rect.bottom + spawnAreaPadding; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const randomX = Math.max(0, Math.min(viewportWidth, minX + Math.random() * (maxX - minX))); const randomY = Math.max(0, Math.min(viewportHeight, minY + Math.random() * (maxY - minY))); // Filter available mascots based on ownedFriendBoost level const availableMascots = currentMascotTiers.filter( (tier) => ownedFriendBoost >= tier.level ); if (availableMascots.length === 0) { console.warn('No mascots available to spawn for current friendBoost level.'); timeoutRef.current = setTimeout(spawnMascot, interval); return; } // Weighted random selection based on rarity const totalRarity = availableMascots.reduce((sum, tier) => sum + tier.rarity, 0); let randomWeight = Math.random() * totalRarity; let selectedMascotTier = availableMascots[0]; // Default to first available for (const tier of availableMascots) { randomWeight -= tier.rarity; if (randomWeight <= 0) { selectedMascotTier = tier; break; } } const newMascot: ClickableMascotType = { id: `mascot-${Date.now()}-${Math.random()}`, x: randomX, y: randomY, multiplierBonus: selectedMascotTier.multiplier, imageSrc: selectedMascotTier.imageSrc, }; setMascotEntities((prev) => [...prev, newMascot]); timeoutRef.current = setTimeout(spawnMascot, interval); }; spawnMascot(); // Start the first spawn } else { setMascotEntities([]); // Clear all mascots if friendBoost is 0 } return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, [gameState?.upgrades['friendBoost']?.owned]); // Only depend on friendBoost level const handleMascotClick = (id: string, multiplierBonus: number) => { // Renamed handler sendMascotClickBonus(multiplierBonus); // Renamed function call setMascotEntities((prev) => prev.filter((m) => m.id !== id)); // Update state variable }; const handleMascotRemove = (id: string) => { // Renamed handler setMascotEntities((prev) => prev.filter((m) => m.id !== id)); // Update state variable }; if (!isLoaded || !gameState) { return (
Connecting to the Bozo Network...
); } const userBonusMultiplier = isSignedIn && userId ? gameState.users[userId]?.bonusMultiplier || 1 : 1; const effectiveClickMultiplier = gameState.clickMultiplier * userBonusMultiplier; // Render the AdminPage if the current location is /admin if (location === '/admin') { return ; } return (
{/* User Button and Admin Link */}
{isAdmin && ( Admin Page )}
{/* Celebration Message */} {celebrationMessage && (
🎉 {celebrationMessage} 🎉
)} {/* Admin Broadcast Message */} {adminBroadcastMessage && (
📢 Admin Message: {adminBroadcastMessage} 📢
)} {/* News Marquee */} {gameState.upgrades['news']?.owned > 0 && UPGRADES.find(u => u.id === 'news')?.newsTitles && ( u.id === 'news')!.newsTitles!} /> )}
{/* Header */}
{/* Main Content */}
{/* Left Column - Click Button */}
{/* Added ref, id and relative positioning */} {/* Render Mascots */} {mascotEntities.map((mascot: ClickableMascotType) => ( ))}
{/* Middle Column - Upgrades */}
{/* Right Column - Milestones and Leaderboard */}
{/* Footer */}

✨ Keep clicking, fellow bozos! ✨

This game is synchronized in real-time with all players!

{/* Sparkle Effects */}
{[...Array(10)].map((_, i) => (
✨
))}
{/* If not logged in, show popup overlaying the game to login */}

Welcome to Bozo Clicker!

Please sign in to start clicking!

); } export default App;