Start repository
This commit is contained in:
132
src/App.tsx
Normal file
132
src/App.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React, { useEffect, useState } 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';
|
||||
|
||||
function App() {
|
||||
const { gameState, sendClick, purchaseUpgrade, userId } = usePartyKit();
|
||||
const [celebrationMessage, setCelebrationMessage] = useState<string | null>(null);
|
||||
const [previousMilestones, setPreviousMilestones] = useState<Record<string, boolean>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (gameState) {
|
||||
// Check for new milestones
|
||||
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]);
|
||||
|
||||
if (!gameState) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-900 to-pink-900">
|
||||
<div className="text-white text-2xl font-bold animate-pulse">
|
||||
Connecting to the Bozo Network...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const userClicks = gameState.users[userId]?.clicks || 0;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative overflow-x-hidden">
|
||||
<Background background={gameState.currentBackground} />
|
||||
|
||||
{/* Celebration Message */}
|
||||
{celebrationMessage && (
|
||||
<div className="fixed top-0 left-0 right-0 z-50 flex justify-center pt-8">
|
||||
<div className="bg-gradient-to-r from-yellow-400 via-pink-500 to-purple-600 text-white px-8 py-4 rounded-full text-2xl font-bold shadow-2xl animate-bounce border-4 border-white">
|
||||
🎉 {celebrationMessage} 🎉
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="container mx-auto px-4 py-8 relative z-10">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Counter
|
||||
totalClicks={gameState.totalClicks}
|
||||
autoClickRate={gameState.autoClickRate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Left Column - Click Button */}
|
||||
<div className="lg:col-span-1 flex flex-col items-center justify-center">
|
||||
<ClickButton
|
||||
onClick={sendClick}
|
||||
imageUrl={gameState.currentClickImage}
|
||||
clickMultiplier={gameState.clickMultiplier}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Middle Column - Upgrades */}
|
||||
<div className="lg:col-span-1">
|
||||
<UpgradeShop
|
||||
gameState={gameState}
|
||||
userClicks={userClicks}
|
||||
onPurchase={purchaseUpgrade}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Milestones and Leaderboard */}
|
||||
<div className="lg:col-span-1 space-y-6">
|
||||
<Milestones gameState={gameState} />
|
||||
<Leaderboard gameState={gameState} currentUserId={userId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-12 text-center">
|
||||
<div className="bg-gradient-to-r from-pink-500 via-purple-500 to-cyan-500 p-4 rounded-xl border-4 border-yellow-300">
|
||||
<p className="text-white text-xl font-bold" style={{ fontFamily: 'Comic Sans MS, cursive' }}>
|
||||
✨ Keep clicking, fellow bozos! ✨
|
||||
</p>
|
||||
<p className="text-yellow-200 text-sm mt-2">
|
||||
This game is synchronized in real-time with all players!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sparkle Effects */}
|
||||
<div className="fixed inset-0 pointer-events-none z-20">
|
||||
{[...Array(10)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute text-2xl opacity-70 animate-pulse"
|
||||
style={{
|
||||
left: `${Math.random() * 100}%`,
|
||||
top: `${Math.random() * 100}%`,
|
||||
animationDelay: `${Math.random() * 3}s`,
|
||||
animationDuration: `${2 + Math.random() * 3}s`
|
||||
}}
|
||||
>
|
||||
✨
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
61
src/components/Background.tsx
Normal file
61
src/components/Background.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
|
||||
interface BackgroundProps {
|
||||
background: string;
|
||||
}
|
||||
|
||||
export function Background({ background }: BackgroundProps) {
|
||||
const getBackgroundStyle = () => {
|
||||
switch (background) {
|
||||
case 'rainbow':
|
||||
return {
|
||||
background: 'linear-gradient(45deg, #ff0000, #ff8000, #ffff00, #80ff00, #00ff00, #00ff80, #00ffff, #0080ff, #0000ff, #8000ff, #ff00ff, #ff0080)',
|
||||
backgroundSize: '400% 400%',
|
||||
animation: 'rainbow 10s ease infinite'
|
||||
};
|
||||
case 'matrix':
|
||||
return {
|
||||
background: 'linear-gradient(135deg, #000000 0%, #0d4a0d 50%, #000000 100%)',
|
||||
backgroundImage: 'radial-gradient(circle at 20% 50%, #00ff00 1px, transparent 1px), radial-gradient(circle at 80% 50%, #00ff00 1px, transparent 1px)',
|
||||
backgroundSize: '20px 20px',
|
||||
animation: 'matrix 20s linear infinite'
|
||||
};
|
||||
case 'cyberpunk':
|
||||
return {
|
||||
background: 'linear-gradient(135deg, #0a0a0a 0%, #1a0a2e 25%, #16213e 50%, #e94560 75%, #0f3460 100%)',
|
||||
backgroundSize: '400% 400%',
|
||||
animation: 'cyberpunk 15s ease infinite'
|
||||
};
|
||||
case 'space':
|
||||
return {
|
||||
background: 'radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%)',
|
||||
backgroundImage: 'radial-gradient(2px 2px at 20px 30px, #eee, transparent), radial-gradient(2px 2px at 40px 70px, #fff, transparent), radial-gradient(1px 1px at 90px 40px, #fff, transparent)',
|
||||
backgroundSize: '200px 100px',
|
||||
animation: 'stars 50s linear infinite'
|
||||
};
|
||||
case 'glitch':
|
||||
return {
|
||||
background: 'linear-gradient(45deg, #ff0000, #00ff00, #0000ff, #ffff00, #ff00ff, #00ffff)',
|
||||
backgroundSize: '400% 400%',
|
||||
animation: 'glitch 2s ease infinite'
|
||||
};
|
||||
case 'ultimate':
|
||||
return {
|
||||
background: 'conic-gradient(from 0deg, #ff0000, #ff8000, #ffff00, #80ff00, #00ff00, #00ff80, #00ffff, #0080ff, #0000ff, #8000ff, #ff00ff, #ff0080, #ff0000)',
|
||||
backgroundSize: '400% 400%',
|
||||
animation: 'ultimate 5s linear infinite'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 -z-10"
|
||||
style={getBackgroundStyle()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
55
src/components/ClickButton.tsx
Normal file
55
src/components/ClickButton.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface ClickButtonProps {
|
||||
onClick: () => void;
|
||||
imageUrl: string;
|
||||
clickMultiplier: number;
|
||||
}
|
||||
|
||||
export function ClickButton({ onClick, imageUrl, clickMultiplier }: ClickButtonProps) {
|
||||
const [clickEffect, setClickEffect] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
setClickEffect(true);
|
||||
onClick();
|
||||
setTimeout(() => setClickEffect(false), 200);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div
|
||||
className={`relative cursor-pointer transition-all duration-200 hover:scale-110 ${
|
||||
clickEffect ? 'scale-125 animate-pulse' : ''
|
||||
}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Spinning Rat"
|
||||
className="w-48 h-48 rounded-full border-4 border-pink-500 shadow-2xl hover:border-cyan-400 transition-all duration-300"
|
||||
style={{
|
||||
filter: 'drop-shadow(0 0 20px #ff1493) drop-shadow(0 0 40px #00bfff)',
|
||||
animation: 'spin 2s linear infinite'
|
||||
}}
|
||||
/>
|
||||
{clickEffect && (
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="text-yellow-300 text-4xl font-bold animate-bounce absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-8">
|
||||
+{clickMultiplier}
|
||||
</div>
|
||||
<div className="absolute inset-0 rounded-full bg-yellow-300 opacity-20 animate-ping"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-pink-300 font-bold text-xl">
|
||||
Click Power: {clickMultiplier}x
|
||||
</p>
|
||||
<p className="text-cyan-300 text-lg">
|
||||
Click the spinning rat!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
src/components/Counter.tsx
Normal file
26
src/components/Counter.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
interface CounterProps {
|
||||
totalClicks: number;
|
||||
autoClickRate: number;
|
||||
}
|
||||
|
||||
export function Counter({ totalClicks, autoClickRate }: CounterProps) {
|
||||
return (
|
||||
<div className="text-center space-y-4">
|
||||
<div className="bg-gradient-to-r from-purple-600 via-pink-500 to-cyan-400 p-6 rounded-xl border-4 border-yellow-300 shadow-2xl">
|
||||
<h1 className="text-6xl font-bold text-white mb-2" style={{ fontFamily: 'Comic Sans MS, cursive' }}>
|
||||
BOZO CLICKER
|
||||
</h1>
|
||||
<div className="text-4xl font-bold text-yellow-300 animate-pulse">
|
||||
{totalClicks.toLocaleString()} CLICKS
|
||||
</div>
|
||||
{autoClickRate > 0 && (
|
||||
<div className="text-lg text-green-300 mt-2">
|
||||
🤖 Auto-clicking at {autoClickRate}/sec
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
61
src/components/Leaderboard.tsx
Normal file
61
src/components/Leaderboard.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { GameState } from '../types';
|
||||
|
||||
interface LeaderboardProps {
|
||||
gameState: GameState;
|
||||
currentUserId: string;
|
||||
}
|
||||
|
||||
export function Leaderboard({ gameState, currentUserId }: LeaderboardProps) {
|
||||
const sortedUsers = Object.entries(gameState.users)
|
||||
.sort(([, a], [, b]) => b.clicks - a.clicks)
|
||||
.slice(0, 10);
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-b from-orange-600 to-red-600 p-6 rounded-xl border-4 border-pink-400 shadow-2xl">
|
||||
<h2 className="text-3xl font-bold text-yellow-300 mb-6 text-center" style={{ fontFamily: 'Comic Sans MS, cursive' }}>
|
||||
👑 BOZO LEADERBOARD 👑
|
||||
</h2>
|
||||
|
||||
<div className="space-y-3">
|
||||
{sortedUsers.map(([userId, user], index) => {
|
||||
const isCurrentUser = userId === currentUserId;
|
||||
const isTopThree = index < 3;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={userId}
|
||||
className={`p-3 rounded-lg border-2 transition-all duration-300 ${
|
||||
isCurrentUser
|
||||
? 'bg-gradient-to-r from-yellow-400 to-orange-400 border-white shadow-lg transform scale-105'
|
||||
: isTopThree
|
||||
? 'bg-gradient-to-r from-purple-500 to-pink-500 border-yellow-300'
|
||||
: 'bg-gradient-to-r from-gray-600 to-gray-700 border-gray-400'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-2xl font-bold">
|
||||
{index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `#${index + 1}`}
|
||||
</span>
|
||||
<span className={`font-bold ${isCurrentUser ? 'text-black' : 'text-white'}`}>
|
||||
{user.name} {isCurrentUser ? '(You)' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<span className={`text-xl font-bold ${isCurrentUser ? 'text-black' : 'text-yellow-300'}`}>
|
||||
{user.clicks.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<div className="text-lg text-yellow-200">
|
||||
Total Players: {Object.keys(gameState.users).length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
61
src/components/Milestones.tsx
Normal file
61
src/components/Milestones.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { MILESTONES } from '../config/milestones';
|
||||
import { GameState } from '../types';
|
||||
|
||||
interface MilestonesProps {
|
||||
gameState: GameState;
|
||||
}
|
||||
|
||||
export function Milestones({ gameState }: MilestonesProps) {
|
||||
const completedMilestones = MILESTONES.filter(m => gameState.milestones[m.id]);
|
||||
const nextMilestone = MILESTONES.find(m => !gameState.milestones[m.id]);
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-b from-green-600 to-blue-600 p-6 rounded-xl border-4 border-yellow-400 shadow-2xl">
|
||||
<h2 className="text-3xl font-bold text-yellow-300 mb-6 text-center" style={{ fontFamily: 'Comic Sans MS, cursive' }}>
|
||||
🏆 MILESTONES 🏆
|
||||
</h2>
|
||||
|
||||
{nextMilestone && (
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xl font-bold text-white mb-2">Next Goal:</h3>
|
||||
<div className="bg-gradient-to-r from-purple-500 to-pink-500 p-4 rounded-lg border-2 border-cyan-400">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-lg font-bold text-white">{nextMilestone.name}</span>
|
||||
<span className="text-yellow-300 font-bold">{nextMilestone.threshold.toLocaleString()}</span>
|
||||
</div>
|
||||
<p className="text-cyan-200 text-sm mb-3">{nextMilestone.description}</p>
|
||||
<div className="w-full bg-gray-700 rounded-full h-4">
|
||||
<div
|
||||
className="bg-gradient-to-r from-green-400 to-blue-500 h-4 rounded-full transition-all duration-500"
|
||||
style={{
|
||||
width: `${Math.min(100, (gameState.totalClicks / nextMilestone.threshold) * 100)}%`
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className="text-center mt-2 text-white">
|
||||
{gameState.totalClicks.toLocaleString()} / {nextMilestone.threshold.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{completedMilestones.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white mb-4">Completed:</h3>
|
||||
<div className="space-y-2 max-h-40 overflow-y-auto">
|
||||
{completedMilestones.map((milestone) => (
|
||||
<div key={milestone.id} className="bg-gradient-to-r from-green-500 to-emerald-500 p-3 rounded-lg border border-yellow-300">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-bold text-white">{milestone.name}</span>
|
||||
<span className="text-yellow-200">✅ {milestone.threshold.toLocaleString()}</span>
|
||||
</div>
|
||||
<p className="text-green-100 text-sm">{milestone.reward}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
src/components/UpgradeShop.tsx
Normal file
67
src/components/UpgradeShop.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { UPGRADES } from '../config/upgrades';
|
||||
import { GameState } from '../types';
|
||||
|
||||
interface UpgradeShopProps {
|
||||
gameState: GameState;
|
||||
userClicks: number;
|
||||
onPurchase: (upgradeId: string) => void;
|
||||
}
|
||||
|
||||
export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopProps) {
|
||||
return (
|
||||
<div className="bg-gradient-to-b from-purple-800 to-pink-600 p-6 rounded-xl border-4 border-cyan-400 shadow-2xl">
|
||||
<h2 className="text-3xl font-bold text-yellow-300 mb-6 text-center" style={{ fontFamily: 'Comic Sans MS, cursive' }}>
|
||||
✨ BOZO SHOP ✨
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{UPGRADES.map((upgrade) => {
|
||||
const owned = gameState.upgrades[upgrade.id]?.owned || 0;
|
||||
const cost = gameState.upgrades[upgrade.id]?.cost || upgrade.baseCost;
|
||||
const canAfford = userClicks >= cost;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={upgrade.id}
|
||||
className={`bg-gradient-to-r from-pink-500 to-purple-600 p-4 rounded-lg border-2 transition-all duration-300 ${
|
||||
canAfford
|
||||
? 'border-green-400 hover:scale-105 cursor-pointer hover:shadow-lg'
|
||||
: 'border-gray-500 opacity-60'
|
||||
}`}
|
||||
onClick={() => canAfford && onPurchase(upgrade.id)}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-2xl">{upgrade.icon}</span>
|
||||
<h3 className="text-lg font-bold text-white">{upgrade.name}</h3>
|
||||
{owned > 0 && (
|
||||
<span className="bg-yellow-400 text-black px-2 py-1 rounded-full text-sm font-bold">
|
||||
{owned}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-cyan-200 text-sm mt-1">{upgrade.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<div className={`text-xl font-bold ${canAfford ? 'text-green-300' : 'text-red-300'}`}>
|
||||
{cost.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-gray-300">clicks</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<div className="text-2xl font-bold text-yellow-300">
|
||||
Your Clicks: {userClicks.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
58
src/config/milestones.ts
Normal file
58
src/config/milestones.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Milestone } from '../types';
|
||||
|
||||
export const MILESTONES: Milestone[] = [
|
||||
{
|
||||
threshold: 100,
|
||||
id: 'first-hundred',
|
||||
name: 'First Steps',
|
||||
description: 'Welcome to the madness!',
|
||||
background: 'rainbow',
|
||||
image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif',
|
||||
reward: '🌈 Rainbow Background Unlocked!'
|
||||
},
|
||||
{
|
||||
threshold: 500,
|
||||
id: 'five-hundred',
|
||||
name: 'Getting Warmed Up',
|
||||
description: 'The rat spins faster...',
|
||||
background: 'matrix',
|
||||
image: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif',
|
||||
reward: '💊 Matrix Mode Activated!'
|
||||
},
|
||||
{
|
||||
threshold: 1000,
|
||||
id: 'one-thousand',
|
||||
name: 'Cyber Rat',
|
||||
description: 'Welcome to the future',
|
||||
background: 'cyberpunk',
|
||||
image: 'https://media1.tenor.com/m/YsWlbVbRWFQAAAAd/rat-spinning.gif',
|
||||
reward: '🦾 Cyberpunk Aesthetic Engaged!'
|
||||
},
|
||||
{
|
||||
threshold: 2500,
|
||||
id: 'epic-milestone',
|
||||
name: 'Space Cadet',
|
||||
description: 'To infinity and beyond!',
|
||||
background: 'space',
|
||||
image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif',
|
||||
reward: '🚀 Space Background Unlocked!'
|
||||
},
|
||||
{
|
||||
threshold: 5000,
|
||||
id: 'legendary',
|
||||
name: 'Glitch in the Matrix',
|
||||
description: 'Reality is breaking down',
|
||||
background: 'glitch',
|
||||
image: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif',
|
||||
reward: '⚡ Glitch Effect Activated!'
|
||||
},
|
||||
{
|
||||
threshold: 10000,
|
||||
id: 'ultimate',
|
||||
name: 'Ultimate Bozo',
|
||||
description: 'You have achieved peak bozo status',
|
||||
background: 'ultimate',
|
||||
image: 'https://media1.tenor.com/m/YsWlbVbRWFQAAAAd/rat-spinning.gif',
|
||||
reward: '👑 Ultimate Power Unlocked!'
|
||||
}
|
||||
];
|
||||
49
src/config/upgrades.ts
Normal file
49
src/config/upgrades.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Upgrade } from '../types';
|
||||
|
||||
export const UPGRADES: Upgrade[] = [
|
||||
{
|
||||
id: 'clickMultiplier',
|
||||
name: '🖱️ Mega Click',
|
||||
description: '+1 click power per purchase',
|
||||
baseCost: 10,
|
||||
multiplier: 1.5,
|
||||
clickBonus: 1,
|
||||
icon: '🖱️'
|
||||
},
|
||||
{
|
||||
id: 'autoClicker',
|
||||
name: '🤖 Auto Clicker',
|
||||
description: '+1 click per second',
|
||||
baseCost: 50,
|
||||
multiplier: 2,
|
||||
autoClickRate: 1,
|
||||
icon: '🤖'
|
||||
},
|
||||
{
|
||||
id: 'megaBonus',
|
||||
name: '💎 Mega Bonus',
|
||||
description: '+5 click power per purchase',
|
||||
baseCost: 200,
|
||||
multiplier: 2.5,
|
||||
clickBonus: 5,
|
||||
icon: '💎'
|
||||
},
|
||||
{
|
||||
id: 'hyperClicker',
|
||||
name: '⚡ Hyper Clicker',
|
||||
description: '+10 auto clicks per second',
|
||||
baseCost: 1000,
|
||||
multiplier: 3,
|
||||
autoClickRate: 10,
|
||||
icon: '⚡'
|
||||
},
|
||||
{
|
||||
id: 'quantumClicker',
|
||||
name: '🌟 Quantum Clicker',
|
||||
description: '+50 click power per purchase',
|
||||
baseCost: 5000,
|
||||
multiplier: 4,
|
||||
clickBonus: 50,
|
||||
icon: '🌟'
|
||||
}
|
||||
];
|
||||
68
src/hooks/usePartyKit.ts
Normal file
68
src/hooks/usePartyKit.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import PartySocket from 'partysocket';
|
||||
import { GameState } from '../types';
|
||||
|
||||
const PARTY_HOST = import.meta.env.DEV ? 'localhost:1998' : 'bozo-clicker.your-username.partykit.dev';
|
||||
|
||||
export function usePartyKit() {
|
||||
const [gameState, setGameState] = useState<GameState | null>(null);
|
||||
const [socket, setSocket] = useState<PartySocket | null>(null);
|
||||
const [userId] = useState(() => Math.random().toString(36).substring(7));
|
||||
const [userName] = useState(() => `Bozo${Math.floor(Math.random() * 1000)}`);
|
||||
|
||||
useEffect(() => {
|
||||
const ws = new PartySocket({
|
||||
host: PARTY_HOST,
|
||||
room: 'bozo-clicker'
|
||||
});
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'user-join',
|
||||
userId,
|
||||
userName
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'game-state') {
|
||||
setGameState(data.state);
|
||||
}
|
||||
};
|
||||
|
||||
setSocket(ws);
|
||||
|
||||
return () => {
|
||||
ws.close();
|
||||
};
|
||||
}, [userId, userName]);
|
||||
|
||||
const sendClick = useCallback(() => {
|
||||
if (socket) {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'click',
|
||||
userId,
|
||||
userName
|
||||
}));
|
||||
}
|
||||
}, [socket, userId, userName]);
|
||||
|
||||
const purchaseUpgrade = useCallback((upgradeId: string) => {
|
||||
if (socket) {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'purchase-upgrade',
|
||||
userId,
|
||||
upgradeId
|
||||
}));
|
||||
}
|
||||
}, [socket, userId]);
|
||||
|
||||
return {
|
||||
gameState,
|
||||
sendClick,
|
||||
purchaseUpgrade,
|
||||
userId,
|
||||
userName
|
||||
};
|
||||
}
|
||||
136
src/index.css
Normal file
136
src/index.css
Normal file
@@ -0,0 +1,136 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Comic+Neue:wght@400;700&display=swap');
|
||||
|
||||
body {
|
||||
font-family: 'Comic Neue', 'Comic Sans MS', cursive;
|
||||
}
|
||||
|
||||
@keyframes rainbow {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
@keyframes matrix {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
|
||||
@keyframes cyberpunk {
|
||||
0% { background-position: 0% 50%; }
|
||||
25% { background-position: 100% 50%; }
|
||||
50% { background-position: 100% 0%; }
|
||||
75% { background-position: 0% 100%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
@keyframes stars {
|
||||
0% { background-position: 0 0; }
|
||||
100% { background-position: 200px 100px; }
|
||||
}
|
||||
|
||||
@keyframes glitch {
|
||||
0% { background-position: 0% 50%; }
|
||||
10% { background-position: 100% 0%; }
|
||||
20% { background-position: 0% 100%; }
|
||||
30% { background-position: 100% 50%; }
|
||||
40% { background-position: 0% 0%; }
|
||||
50% { background-position: 100% 100%; }
|
||||
60% { background-position: 50% 0%; }
|
||||
70% { background-position: 50% 100%; }
|
||||
80% { background-position: 0% 50%; }
|
||||
90% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
@keyframes ultimate {
|
||||
0% { transform: rotate(0deg) scale(1); }
|
||||
25% { transform: rotate(90deg) scale(1.1); }
|
||||
50% { transform: rotate(180deg) scale(1); }
|
||||
75% { transform: rotate(270deg) scale(1.1); }
|
||||
100% { transform: rotate(360deg) scale(1); }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(45deg, #ff1493, #00bfff);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(45deg, #ff69b4, #1e90ff);
|
||||
}
|
||||
|
||||
/* Glitch text effect for ultimate milestone */
|
||||
.glitch-text {
|
||||
animation: glitch-text 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes glitch-text {
|
||||
0% { transform: translate(0); }
|
||||
20% { transform: translate(-2px, 2px); }
|
||||
40% { transform: translate(-2px, -2px); }
|
||||
60% { transform: translate(2px, 2px); }
|
||||
80% { transform: translate(2px, -2px); }
|
||||
100% { transform: translate(0); }
|
||||
}
|
||||
|
||||
/* Pulsing border effect */
|
||||
.pulse-border {
|
||||
animation: pulse-border 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-border {
|
||||
0% { box-shadow: 0 0 0 0 rgba(255, 20, 147, 0.7); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(255, 20, 147, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(255, 20, 147, 0); }
|
||||
}
|
||||
|
||||
/* Selection styling */
|
||||
::selection {
|
||||
background: linear-gradient(45deg, #ff1493, #00bfff);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Smooth transitions for all elements */
|
||||
* {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Retro button styles */
|
||||
.retro-button {
|
||||
background: linear-gradient(45deg, #ff1493, #00bfff);
|
||||
border: 3px solid #fff;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.retro-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.retro-button:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
31
src/types.ts
Normal file
31
src/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export interface GameState {
|
||||
totalClicks: number;
|
||||
users: Record<string, { name: string; clicks: number; lastSeen: number }>;
|
||||
upgrades: Record<string, { owned: number; cost: number }>;
|
||||
milestones: Record<string, boolean>;
|
||||
clickMultiplier: number;
|
||||
autoClickRate: number;
|
||||
currentBackground: string;
|
||||
currentClickImage: string;
|
||||
}
|
||||
|
||||
export interface Upgrade {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
baseCost: number;
|
||||
multiplier: number;
|
||||
clickBonus?: number;
|
||||
autoClickRate?: number;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface Milestone {
|
||||
threshold: number;
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
background: string;
|
||||
image: string;
|
||||
reward: string;
|
||||
}
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user