Start repository

This commit is contained in:
Arjun S
2025-08-02 23:36:17 +05:30
commit d936bf4608
30 changed files with 5887 additions and 0 deletions

3
.bolt/config.json Normal file
View File

@@ -0,0 +1,3 @@
{
"template": "bolt-vite-react-ts"
}

5
.bolt/prompt Normal file
View File

@@ -0,0 +1,5 @@
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
Use icons from lucide-react for logos.

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env

1
README.md Normal file
View File

@@ -0,0 +1 @@
bozoclicker

28
eslint.config.js Normal file
View File

@@ -0,0 +1,28 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
);

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bozo Clicker - Realtime Collaborative Cookie Clicker</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4697
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "vite-react-typescript-starter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.344.0",
"partykit": "^0.0.115",
"partysocket": "^1.1.4",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.18",
"eslint": "^9.9.1",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.11",
"globals": "^15.9.0",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.5.3",
"typescript-eslint": "^8.3.0",
"vite": "^5.4.2"
}
}

183
party/index.ts Normal file
View File

@@ -0,0 +1,183 @@
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;
}
interface ClickMessage {
type: 'click';
userId: string;
userName: string;
}
interface PurchaseUpgradeMessage {
type: 'purchase-upgrade';
userId: string;
upgradeId: string;
}
interface UserJoinMessage {
type: 'user-join';
userId: string;
userName: string;
}
type Message = ClickMessage | PurchaseUpgradeMessage | UserJoinMessage;
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 }
};
const MILESTONES = [
{ threshold: 100, id: 'first-hundred', background: 'rainbow', image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif' },
{ threshold: 500, id: 'five-hundred', background: 'matrix', image: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif' },
{ threshold: 1000, id: 'one-thousand', background: 'cyberpunk', image: 'https://media1.tenor.com/m/YsWlbVbRWFQAAAAd/rat-spinning.gif' },
{ threshold: 2500, id: 'epic-milestone', background: 'space', image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif' },
{ threshold: 5000, id: 'legendary', background: 'glitch', image: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif' },
{ threshold: 10000, id: 'ultimate', background: 'ultimate', image: 'https://media1.tenor.com/m/YsWlbVbRWFQAAAAd/rat-spinning.gif' }
];
export default class GameServer implements Party.Server {
constructor(readonly party: Party.Party) {}
gameState: GameState = {
totalClicks: 0,
users: {},
upgrades: Object.keys(UPGRADES).reduce((acc, key) => {
acc[key] = { owned: 0, cost: UPGRADES[key as keyof typeof UPGRADES].baseCost };
return acc;
}, {} as Record<string, { owned: number; cost: number }>),
milestones: {},
clickMultiplier: 1,
autoClickRate: 0,
currentBackground: 'default',
currentClickImage: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif'
};
autoClickInterval?: NodeJS.Timeout;
onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {
conn.send(JSON.stringify({ type: 'game-state', state: this.gameState }));
}
onMessage(message: string, sender: Party.Connection) {
const data = JSON.parse(message) as Message;
switch (data.type) {
case 'user-join':
this.handleUserJoin(data);
break;
case 'click':
this.handleClick(data);
break;
case 'purchase-upgrade':
this.handlePurchaseUpgrade(data);
break;
}
this.broadcast();
}
handleUserJoin(data: UserJoinMessage) {
if (!this.gameState.users[data.userId]) {
this.gameState.users[data.userId] = {
name: data.userName,
clicks: 0,
lastSeen: Date.now()
};
}
this.gameState.users[data.userId].lastSeen = Date.now();
}
handleClick(data: ClickMessage) {
const clickValue = this.gameState.clickMultiplier;
this.gameState.totalClicks += clickValue;
if (!this.gameState.users[data.userId]) {
this.gameState.users[data.userId] = {
name: data.userName,
clicks: 0,
lastSeen: Date.now()
};
}
this.gameState.users[data.userId].clicks += clickValue;
this.gameState.users[data.userId].lastSeen = Date.now();
this.checkMilestones();
}
handlePurchaseUpgrade(data: PurchaseUpgradeMessage) {
const upgrade = UPGRADES[data.upgradeId as keyof typeof UPGRADES];
const currentUpgrade = this.gameState.upgrades[data.upgradeId];
if (!upgrade || !currentUpgrade) return;
const userClicks = this.gameState.users[data.userId]?.clicks || 0;
if (userClicks >= currentUpgrade.cost) {
this.gameState.users[data.userId].clicks -= currentUpgrade.cost;
currentUpgrade.owned += 1;
currentUpgrade.cost = Math.floor(upgrade.baseCost * Math.pow(upgrade.multiplier, currentUpgrade.owned));
this.updateGameMultipliers();
}
}
updateGameMultipliers() {
this.gameState.clickMultiplier = 1;
this.gameState.autoClickRate = 0;
Object.entries(this.gameState.upgrades).forEach(([upgradeId, upgrade]) => {
const config = UPGRADES[upgradeId as keyof typeof UPGRADES];
if (config.clickBonus) {
this.gameState.clickMultiplier += config.clickBonus * upgrade.owned;
}
if (config.autoClickRate) {
this.gameState.autoClickRate += config.autoClickRate * upgrade.owned;
}
});
this.setupAutoClicker();
}
setupAutoClicker() {
if (this.autoClickInterval) {
clearInterval(this.autoClickInterval);
}
if (this.gameState.autoClickRate > 0) {
this.autoClickInterval = setInterval(() => {
this.gameState.totalClicks += this.gameState.autoClickRate;
this.checkMilestones();
this.broadcast();
}, 1000);
}
}
checkMilestones() {
MILESTONES.forEach(milestone => {
if (this.gameState.totalClicks >= milestone.threshold && !this.gameState.milestones[milestone.id]) {
this.gameState.milestones[milestone.id] = true;
this.gameState.currentBackground = milestone.background;
this.gameState.currentClickImage = milestone.image;
}
});
}
broadcast() {
this.party.broadcast(JSON.stringify({ type: 'game-state', state: this.gameState }));
}
}
GameServer satisfies Party.Worker;

4
partykit.json Normal file
View File

@@ -0,0 +1,4 @@
{
"name": "bozo-clicker",
"main": "party/index.ts"
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

132
src/App.tsx Normal file
View 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;

View 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()}
/>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

8
tailwind.config.js Normal file
View File

@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
};

24
tsconfig.app.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

22
tsconfig.node.json Normal file
View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

10
vite.config.ts Normal file
View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
optimizeDeps: {
exclude: ['lucide-react'],
},
});