diff --git a/party/index.ts b/party/index.ts index bc724cd..60cc4ef 100644 --- a/party/index.ts +++ b/party/index.ts @@ -4,7 +4,7 @@ import { createClerkClient, verifyToken } from '@clerk/backend'; interface GameState { totalClicks: number; - users: Record; + users: Record; // Added bonusMultiplier upgrades: Record; milestones: Record; clickMultiplier: number; @@ -28,18 +28,24 @@ interface PurchaseUpgradeMessage extends AuthenticatedMessage { upgradeId: string; } +interface ApplyMultiplierBonusMessage extends AuthenticatedMessage { // New message type + type: 'apply-multiplier-bonus'; + multiplierBonus: number; +} + interface UserJoinMessage extends AuthenticatedMessage { type: 'user-join'; } -type Message = ClickMessage | PurchaseUpgradeMessage | UserJoinMessage; +type Message = ClickMessage | PurchaseUpgradeMessage | ApplyMultiplierBonusMessage | UserJoinMessage; // Updated Message type const UPGRADES = { clickMultiplier: { baseCost: 10, multiplier: 1.5, clickBonus: 1 }, autoClicker: { baseCost: 50, multiplier: 2, autoClickRate: 1 }, megaBonus: { baseCost: 200, multiplier: 2.5, clickBonus: 5 }, hyperClicker: { baseCost: 1000, multiplier: 3, autoClickRate: 10 }, - quantumClicker: { baseCost: 5000, multiplier: 4, clickBonus: 50 } + quantumClicker: { baseCost: 5000, multiplier: 4, clickBonus: 50 }, + friendBoost: { baseCost: 2000, multiplier: 3, clickMultiplierBonus: 1.02 } // Renamed from shoominions upgrade }; const MILESTONES = [ @@ -103,11 +109,16 @@ export default class GameServer implements Party.Server { this.gameState.users[authenticatedUserId] = { name: data.userName, // Use the name provided by the client, which comes from Clerk clicks: 0, - lastSeen: Date.now() + lastSeen: Date.now(), + bonusMultiplier: 1 // Initialize bonus multiplier }; } this.gameState.users[authenticatedUserId].lastSeen = Date.now(); this.gameState.users[authenticatedUserId].name = data.userName; // Update name in case it changed + // Ensure bonusMultiplier is initialized if it somehow wasn't + if (this.gameState.users[authenticatedUserId].bonusMultiplier === undefined) { + this.gameState.users[authenticatedUserId].bonusMultiplier = 1; + } switch (data.type) { case 'user-join': @@ -119,6 +130,9 @@ export default class GameServer implements Party.Server { case 'purchase-upgrade': this.handlePurchaseUpgrade(data, authenticatedUserId); break; + case 'apply-multiplier-bonus': // Handle new message type + this.handleApplyMultiplierBonus(data, authenticatedUserId); + break; } } catch (error) { console.error('Clerk token verification failed:', error); @@ -145,7 +159,9 @@ export default class GameServer implements Party.Server { } handleClick(data: ClickMessage, authenticatedUserId: string) { - const clickValue = this.gameState.clickMultiplier; + // Apply global click multiplier and user-specific bonus multiplier + const userBonusMultiplier = this.gameState.users[authenticatedUserId]?.bonusMultiplier || 1; + const clickValue = this.gameState.clickMultiplier * userBonusMultiplier; this.gameState.totalClicks += clickValue; @@ -154,7 +170,8 @@ export default class GameServer implements Party.Server { this.gameState.users[authenticatedUserId] = { name: data.userName, // Use the name from the client, which comes from Clerk clicks: 0, - lastSeen: Date.now() + lastSeen: Date.now(), + bonusMultiplier: 1 // Initialize if user was just created }; } @@ -164,6 +181,16 @@ export default class GameServer implements Party.Server { this.checkMilestones(); } + handleApplyMultiplierBonus(data: ApplyMultiplierBonusMessage, authenticatedUserId: string) { + if (!this.gameState.users[authenticatedUserId]) { + console.warn(`User ${authenticatedUserId} not found for multiplier bonus application.`); + return; + } + // Apply the compounding multiplier bonus + this.gameState.users[authenticatedUserId].bonusMultiplier *= data.multiplierBonus; + console.log(`User ${authenticatedUserId} bonus multiplier updated to: ${this.gameState.users[authenticatedUserId].bonusMultiplier}`); + } + handlePurchaseUpgrade(data: PurchaseUpgradeMessage, authenticatedUserId: string) { const upgrade = UPGRADES[data.upgradeId as keyof typeof UPGRADES]; const currentUpgrade = this.gameState.upgrades[data.upgradeId]; @@ -193,6 +220,8 @@ export default class GameServer implements Party.Server { if ('autoClickRate' in config && config.autoClickRate) { this.gameState.autoClickRate += config.autoClickRate * upgrade.owned; } + // Note: clickMultiplierBonus from upgrades.ts is handled client-side for spawning frequency + // and applied per-click on the server via handleApplyMultiplierBonus }); this.setupAutoClicker(); diff --git a/src/App.tsx b/src/App.tsx index 1202341..820da48 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; import { usePartyKit } from './hooks/usePartyKit'; import { ClickButton } from './components/ClickButton'; import { Counter } from './components/Counter'; @@ -9,16 +9,21 @@ 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 function App() { const { isSignedIn, isLoaded } = useAuth(); - const { gameState, sendClick, purchaseUpgrade, userId } = usePartyKit(); + const { gameState, sendClick, purchaseUpgrade, userId, sendMascotClickBonus } = usePartyKit(); // Renamed sendShoominionClickBonus const [celebrationMessage, setCelebrationMessage] = useState(null); const [previousMilestones, setPreviousMilestones] = useState>({}); + const [mascotEntities, setMascotEntities] = useState([]); + const timeoutRef = useRef(null); + // Effect for milestone celebrations useEffect(() => { if (gameState) { - // Check for new milestones const newMilestones = Object.keys(gameState.milestones).filter( (milestoneId) => gameState.milestones[milestoneId] && @@ -37,6 +42,103 @@ function App() { } }, [gameState, previousMilestones]); + // Effect for spawning mascots + useEffect(() => { + if (!gameState) return; + + const friendBoostUpgrade = UPGRADES.find(u => u.id === 'friendBoost') as Upgrade | undefined; + const ownedFriendBoost = gameState.upgrades['friendBoost']?.owned || 0; + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + if (ownedFriendBoost > 0 && friendBoostUpgrade && friendBoostUpgrade.mascotTiers) { + const baseInterval = 10000; // 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 + + const currentMascotTiers = friendBoostUpgrade.mascotTiers; // Ensure mascotTiers is not undefined + + const spawnMascot = () => { + const buttonContainer = document.getElementById('click-button-container'); + if (!buttonContainer) { + console.warn('Click button container 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, gameState]); + + 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 (
@@ -49,6 +151,8 @@ function App() { // Only calculate userClicks if userId is defined (i.e., user is signed in) const userClicks = isSignedIn && userId ? gameState.users[userId]?.clicks || 0 : 0; + const userBonusMultiplier = isSignedIn && userId ? gameState.users[userId]?.bonusMultiplier || 1 : 1; + const effectiveClickMultiplier = gameState.clickMultiplier * userBonusMultiplier; return (
@@ -82,12 +186,25 @@ function App() { {/* Main Content */}
{/* Left Column - Click Button */} -
+
{/* Added id and relative positioning */} + {/* Render Mascots */} + {mascotEntities.map((mascot: ClickableMascotType) => ( + + ))}
{/* Middle Column - Upgrades */} diff --git a/src/assets/bozo.png b/src/assets/bozo.png new file mode 100644 index 0000000..36ee66f Binary files /dev/null and b/src/assets/bozo.png differ diff --git a/src/assets/codebug.gif b/src/assets/codebug.gif new file mode 100644 index 0000000..16fd958 Binary files /dev/null and b/src/assets/codebug.gif differ diff --git a/src/assets/evil-neurosama.gif b/src/assets/evil-neurosama.gif new file mode 100644 index 0000000..9c84721 Binary files /dev/null and b/src/assets/evil-neurosama.gif differ diff --git a/src/assets/lalan.gif b/src/assets/lalan.gif new file mode 100644 index 0000000..83609ea Binary files /dev/null and b/src/assets/lalan.gif differ diff --git a/src/assets/neuro-neurosama.gif b/src/assets/neuro-neurosama.gif new file mode 100644 index 0000000..b522e09 Binary files /dev/null and b/src/assets/neuro-neurosama.gif differ diff --git a/src/assets/shoominion.png b/src/assets/shoominion.png new file mode 100644 index 0000000..dd9dca8 Binary files /dev/null and b/src/assets/shoominion.png differ diff --git a/src/components/ClickableMascot.tsx b/src/components/ClickableMascot.tsx new file mode 100644 index 0000000..2424a7d --- /dev/null +++ b/src/components/ClickableMascot.tsx @@ -0,0 +1,61 @@ +import React, { useEffect, useState } from 'react'; + +interface ClickableMascotProps { // Renamed from ShoominionProps + id: string; + x: number; + y: number; + multiplierBonus: number; + imageSrc: string; // New: Source for the mascot image + onClick: (id: string, multiplierBonus: number) => void; + onRemove: (id: string) => void; +} + +export const ClickableMascot: React.FC = ({ // Renamed from Shoominion + id, + x, + y, + multiplierBonus, + imageSrc, // Destructure new prop + onClick, + onRemove, +}) => { + const [isVisible, setIsVisible] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => { + setIsVisible(false); + // Give some time for fade-out animation before actual removal + setTimeout(() => onRemove(id), 500); + }, 7000); // Mascot disappears after 7 seconds if not clicked + + return () => clearTimeout(timer); + }, [id, onRemove]); + + const handleClick = () => { + setIsVisible(false); + onClick(id, multiplierBonus); + // Give some time for fade-out animation before actual removal + setTimeout(() => onRemove(id), 500); + }; + + return ( +
+ Clickable Mascot +
+ ); +}; diff --git a/src/components/UpgradeShop.tsx b/src/components/UpgradeShop.tsx index befbc59..33e9503 100644 --- a/src/components/UpgradeShop.tsx +++ b/src/components/UpgradeShop.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { UPGRADES } from '../config/upgrades'; -import { GameState } from '../types'; +import { GameState, MascotTier } from '../types'; // Import MascotTier interface UpgradeShopProps { gameState: GameState; @@ -8,6 +8,16 @@ interface UpgradeShopProps { onPurchase: (upgradeId: string) => void; } +// Helper function to get mascot name from image source +const getMascotName = (imageSrc: string): string => { + const fileName = imageSrc.split('/').pop() || ''; + const nameWithoutExtension = fileName.split('.')[0]; + return nameWithoutExtension + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +}; + export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopProps) { return (
@@ -21,6 +31,21 @@ export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopPr const cost = gameState.upgrades[upgrade.id]?.cost || upgrade.baseCost; const canAfford = userClicks >= cost; + let description = upgrade.description; + + // Custom description for Friend Boost upgrade + if (upgrade.id === 'friendBoost' && upgrade.mascotTiers) { + const nextMascotTier = upgrade.mascotTiers.find( + (tier: MascotTier) => owned < tier.level + ); + + if (nextMascotTier) { + description = `Spawns various clickable friends. Next: ${getMascotName(nextMascotTier.imageSrc)} at level ${nextMascotTier.level}`; + } else { + description = 'All mascots unlocked! Spawns various clickable friends.'; + } + } + return (
)}
-

{upgrade.description}

+

{description}

@@ -64,4 +89,4 @@ export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopPr
); -} \ No newline at end of file +} diff --git a/src/config/upgrades.ts b/src/config/upgrades.ts index 7a44257..bf05f36 100644 --- a/src/config/upgrades.ts +++ b/src/config/upgrades.ts @@ -45,5 +45,51 @@ export const UPGRADES: Upgrade[] = [ multiplier: 4, clickBonus: 50, icon: '🌟' + }, + { + id: 'friendBoost', + name: '🤝 Friend Boost', + description: 'Spawns various clickable friends for a compounding click boost', + baseCost: 2000, + multiplier: 3, + icon: '🤝', + mascotTiers: [ + { + level: 0, + imageSrc: '/src/assets/bozo.png', + multiplier: 1.02, + rarity: 1.0, + }, + { + level: 1, + imageSrc: '/src/assets/shoominion.png', + multiplier: 1.03, + rarity: 0.8, + }, + { + level: 5, + imageSrc: '/src/assets/codebug.gif', + multiplier: 1.05, + rarity: 0.6, + }, + { + level: 10, + imageSrc: '/src/assets/lalan.gif', + multiplier: 1.07, + rarity: 0.4, + }, + { + level: 15, + imageSrc: '/src/assets/neuro-neurosama.gif', + multiplier: 1.10, + rarity: 0.2, + }, + { + level: 20, + imageSrc: '/src/assets/evil-neurosama.gif', + multiplier: 1.15, + rarity: 0.1, + }, + ], } -]; \ No newline at end of file +]; diff --git a/src/hooks/usePartyKit.ts b/src/hooks/usePartyKit.ts index 6b58139..0688b5d 100644 --- a/src/hooks/usePartyKit.ts +++ b/src/hooks/usePartyKit.ts @@ -67,10 +67,24 @@ export function usePartyKit() { } }, [socket, isSignedIn, user, getToken]); + const sendMascotClickBonus = useCallback(async (multiplierBonus: number) => { // Renamed function + if (socket && isSignedIn && user) { + const token = await getToken(); + socket.send(JSON.stringify({ + type: 'apply-multiplier-bonus', + userId: user.id, + userName: user.username || user.fullName || user.emailAddresses[0].emailAddress, + token, + multiplierBonus + })); + } + }, [socket, isSignedIn, user, getToken]); + return { gameState, sendClick, purchaseUpgrade, + sendMascotClickBonus, // Export the renamed function userId: user?.id, userName: user?.username || user?.fullName || user?.emailAddresses[0]?.emailAddress }; diff --git a/src/index.css b/src/index.css index 3391b27..1c5338f 100644 --- a/src/index.css +++ b/src/index.css @@ -59,6 +59,19 @@ body { to { transform: rotate(360deg); } } +@keyframes bounce-slow { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +} + +.animate-bounce-slow { + animation: bounce-slow 4s infinite ease-in-out; /* Increased duration for slower movement */ +} + /* Custom scrollbar */ ::-webkit-scrollbar { width: 8px; @@ -133,4 +146,4 @@ body { .retro-button:active { transform: translateY(1px); box-shadow: 0 2px 4px rgba(0,0,0,0.2); -} \ No newline at end of file +} diff --git a/src/types.ts b/src/types.ts index 44445e7..d4e130a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ export interface GameState { totalClicks: number; - users: Record; + users: Record; // Added bonusMultiplier upgrades: Record; milestones: Record; clickMultiplier: number; @@ -17,7 +17,24 @@ export interface Upgrade { multiplier: number; clickBonus?: number; autoClickRate?: number; + clickMultiplierBonus?: number; // New: for compounding click boosts from mascots icon: string; + mascotTiers?: MascotTier[]; // New: for defining mascot tiers for friendBoost +} + +export interface MascotTier { + level: number; + imageSrc: string; + multiplier: number; + rarity: number; // 0 to 1, 1 being most common +} + +export interface ClickableMascot { // Renamed from Shoominion + id: string; + x: number; + y: number; + multiplierBonus: number; // The multiplier this specific mascot provides + imageSrc: string; // New: Source for the mascot image } export interface Milestone { @@ -28,4 +45,4 @@ export interface Milestone { background: string; image: string; reward: string; -} \ No newline at end of file +} diff --git a/todo.md b/todo.md index 8df78a0..52318a5 100644 --- a/todo.md +++ b/todo.md @@ -9,4 +9,4 @@ Like, at first it's only a bozo you click, after a while, codebugs pop up as bon And some more -Auth integration not completed with clerk, needs partykit to be integrated with clerk +Auth integration not completed with clerk, needs partykit to be integrated with clerk (fixed)