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