Start repository
This commit is contained in:
3
.bolt/config.json
Normal file
3
.bolt/config.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"template": "bolt-vite-react-ts"
|
||||||
|
}
|
||||||
5
.bolt/prompt
Normal file
5
.bolt/prompt
Normal 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
25
.gitignore
vendored
Normal 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
|
||||||
28
eslint.config.js
Normal file
28
eslint.config.js
Normal 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
13
index.html
Normal 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
4697
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal 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
183
party/index.ts
Normal 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
4
partykit.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "bozo-clicker",
|
||||||
|
"main": "party/index.ts"
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
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" />
|
||||||
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal 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
24
tsconfig.app.json
Normal 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
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
22
tsconfig.node.json
Normal file
22
tsconfig.node.json
Normal 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
10
vite.config.ts
Normal 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'],
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user