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

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" />