1 Commits

Author SHA1 Message Date
cloudflare-workers-and-pages[bot]
5477db8259 Update wrangler config name to bozoclicker 2025-08-03 19:51:00 +00:00
25 changed files with 282 additions and 992 deletions

3
.bolt/config.json Normal file
View File

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

5
.bolt/prompt Normal file
View File

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

69
package-lock.json generated
View File

@@ -15,7 +15,6 @@
"partysocket": "^1.1.4", "partysocket": "^1.1.4",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-youtube": "^10.1.0",
"wouter": "^3.7.1" "wouter": "^3.7.1"
}, },
"devDependencies": { "devDependencies": {
@@ -2559,7 +2558,8 @@
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
}, },
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.2", "version": "3.3.2",
@@ -3179,11 +3179,6 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true "dev": true
}, },
"node_modules/load-script": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
"integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA=="
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -3448,6 +3443,7 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -3839,16 +3835,6 @@
"integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==",
"license": "Unlicense" "license": "Unlicense"
}, },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3901,11 +3887,6 @@
"react": "^18.3.1" "react": "^18.3.1"
} }
}, },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.14.2", "version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -3915,22 +3896,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-youtube": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-10.1.0.tgz",
"integrity": "sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==",
"dependencies": {
"fast-deep-equal": "3.1.3",
"prop-types": "15.8.1",
"youtube-player": "5.5.2"
},
"engines": {
"node": ">= 14.x"
},
"peerDependencies": {
"react": ">=0.14.1"
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -4102,11 +4067,6 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/sister": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz",
"integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA=="
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -4908,29 +4868,6 @@
"stacktracey": "^2.1.8" "stacktracey": "^2.1.8"
} }
}, },
"node_modules/youtube-player": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz",
"integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==",
"dependencies": {
"debug": "^2.6.6",
"load-script": "^1.0.0",
"sister": "^3.0.0"
}
},
"node_modules/youtube-player/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/youtube-player/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/zod": { "node_modules/zod": {
"version": "3.25.76", "version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",

View File

@@ -17,7 +17,6 @@
"partysocket": "^1.1.4", "partysocket": "^1.1.4",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-youtube": "^10.1.0",
"wouter": "^3.7.1" "wouter": "^3.7.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -15,7 +15,6 @@ export interface Upgrade {
mascotTiers?: MascotTier[]; mascotTiers?: MascotTier[];
oneTime?: boolean; oneTime?: boolean;
newsTitles?: string[]; newsTitles?: string[];
youtubeId?: string;
} }
export interface MascotTier { export interface MascotTier {
@@ -25,27 +24,13 @@ export interface MascotTier {
rarity: number; rarity: number;
} }
interface UserState {
name: string;
clicks: number;
lastSeen: number;
bonusMultiplier: number;
lastClickTime?: number;
upgrades: Record<string, { owned: number; cost: number }>;
clickMultiplier: number;
autoClickRate: number;
ratPoisonChallenge?: {
challengeString: string;
expiresAt: number;
};
ratPoisonImmunityUntil?: number;
}
interface GameState { interface GameState {
totalClicks: number; totalClicks: number;
users: Record<string, UserState>; users: Record<string, { name: string; clicks: number; lastSeen: number; bonusMultiplier: number }>;
upgrades: Record<string, { owned: number; cost: number }>;
milestones: Record<string, boolean>; milestones: Record<string, boolean>;
clickMultiplier: number;
autoClickRate: number;
currentBackground: string; currentBackground: string;
currentClickImage: string; currentClickImage: string;
} }
@@ -80,48 +65,143 @@ interface UserJoinMessage extends AuthenticatedMessage {
type: 'user-join'; type: 'user-join';
} }
interface EditUserMessage extends AuthenticatedMessage { type Message = ClickMessage | PurchaseUpgradeMessage | ApplyMultiplierBonusMessage | UserJoinMessage | AdminBroadcastMessage; // Updated Message type
type: 'edit-user';
targetUserId: string;
clicks: number;
upgrades: Record<string, { owned: number; cost: number }>;
}
interface ThrowRatPoisonMessage extends AuthenticatedMessage { const UPGRADES: Upgrade[] = [
type: 'throw-rat-poison'; {
targetUserId: string; id: 'clickMultiplier',
} name: '🖱️ Mega Click',
description: '+1 click power per purchase',
interface SolveRatPoisonMessage extends AuthenticatedMessage { baseCost: 10,
type: 'solve-rat-poison'; multiplier: 1.5,
challengeString: string; clickBonus: 1,
} icon: '🖱️'
},
type Message = ClickMessage | PurchaseUpgradeMessage | ApplyMultiplierBonusMessage | UserJoinMessage | AdminBroadcastMessage | EditUserMessage | ThrowRatPoisonMessage | SolveRatPoisonMessage; // Updated Message type {
id: 'autoClicker',
import { UPGRADES as ALL_UPGRADES } from '../src/config/upgrades'; name: '🤖 Auto Clicker',
description: '+1 click per second',
const UPGRADES = ALL_UPGRADES.filter(upgrade => !upgrade.oneTime); baseCost: 50,
const ONE_TIME_UPGRADES = ALL_UPGRADES.filter(upgrade => upgrade.oneTime); 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: '🌟'
},
{
id: 'friendBoost',
name: '🤝 Friend Boost',
description: 'Spawns various clickable friends for a compounding click boost',
baseCost: 2000,
multiplier: 3,
icon: '🤝',
mascotTiers: [
{
level: 0,
imageSrc: '/src/assets/bozo.png',
multiplier: 1.02,
rarity: 1.0,
},
{
level: 1,
imageSrc: '/src/assets/shoominion.png',
multiplier: 1.03,
rarity: 0.8,
},
{
level: 5,
imageSrc: '/src/assets/codebug.gif',
multiplier: 1.05,
rarity: 0.6,
},
{
level: 10,
imageSrc: '/src/assets/lalan.gif',
multiplier: 1.07,
rarity: 0.4,
},
{
level: 15,
imageSrc: '/src/assets/neuro-neurosama.gif',
multiplier: 1.10,
rarity: 0.2,
},
{
level: 20,
imageSrc: '/src/assets/evil-neurosama.gif',
multiplier: 1.15,
rarity: 0.1,
},
],
},
{
id: 'news',
name: '📰 Bozo News Network',
description: 'Unlock the latest (fake) news headlines!',
baseCost: 50000, // A higher cost for a unique, one-time unlock
multiplier: 1, // No direct click/auto-click bonus
icon: '📰',
oneTime: true,
newsTitles: [
`Its Bo's birthday!`,
`Haru Urara looses another race, forced to eat pickle filled burger.`,
`Cupid crashes out once again, completely expected.`,
`Bo has been spotted in the wild, please do not approach.`,
`Bozo Clicker is now the number game in the world!`,
`Bo states that he did win the hidden gem vtuber award, it's just that he just didn't feel like it so Reya was really kind to step up.`,
`Reya has been spotted in the wild, please do not approach.`,
`What? Stop reading? I wrote this when you were playing roblox with cupid`,
`It's 1pm in the night and my mom is calling me to sleep, I don't want to sleep I'm making this stupid game`,
`FU BO`,
`When are you watching paint dry again?`,
`Insert Girls Kissing`,
`We luub bo`,
`MOOD DOWN`,
`All this clicking in this game wont give you your money back from those horse races`,
`UMAMUSUME PRETTY DERBY UPDATE : NEW SSSR IS ANNOUNCED - ITS [REDACTED]`,
`LEts' get maried bo`,
`My AI autocomplete on my code editor is shipping you with Reya for some reason. It literally completed Reya in this sentence`,
`DUDE`,
`There's a pipebomb in your DMs`,
`om`,
`You are the Glue to my life like how Tokai Teio is glue(stick) to a child's art project`,
]
}
];
const MILESTONES = [ const MILESTONES = [
{ threshold: 1000, id: 'first-thousand', background: 'rainbow', image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif' }, { threshold: 1000, id: 'first-thousand', background: 'rainbow', image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif' },
{ threshold: 10000, id: 'ten-thousand', background: 'cyberpunk', image: 'https://cdn.discordapp.com/emojis/1297946545626288312.webp' }, { threshold: 5000, id: 'five-thousand', background: 'matrix', image: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif' },
{ threshold: 50000, id: 'epic-milestone', background: 'space', image: 'https://media.discordapp.net/stickers/1377379131649429554.gif?size=160&quality=lossless' }, { threshold: 10000, id: 'ten-thousand', background: 'cyberpunk', image: 'https://media1.tenor.com/m/YsWlbVbRWFQAAAAd/rat-spinning.gif' },
{ threshold: 100000, id: 'legendary', background: 'musume', image: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif' }, { threshold: 50000, id: 'epic-milestone', background: 'space', image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif' },
{ threshold: 200000, id: 'two-hundred-thousand', background: 'glitch', image: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif' }, { threshold: 100000, id: 'legendary', background: 'glitch', image: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif' },
{ threshold: 696969, id: 'ultimate', background: 'ultimate', image: 'https://media.discordapp.net/stickers/1397981135266648064.webp?size=160&quality=lossless' }, { threshold: 500000, id: 'ultimate', background: 'ultimate', image: 'https://media1.tenor.com/m/YsWlbVbRWFQAAAAd/rat-spinning.gif' },
{ threshold: 1000000, id: 'god-tier', background: 'god-tier', image: 'https://media.discordapp.net/stickers/1294812453112123453.webp?quality=lossless' }, { threshold: 1000000, id: 'god-tier', background: 'god-tier', image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif' }
{ threshold: 5000000, id: 'mega-bozo', background: 'mega-bozo', image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif' },
{ threshold: 8008008, id: 'galactic-bozo', background: 'galactic-bozo', image: 'https://cdn.discordapp.com/emojis/1297946545626288312.webp' },
{ threshold: 10000000, id: 'cosmic-bozo', background: 'cosmic-bozo', image: 'https://media.discordapp.net/stickers/1377379131649429554.gif?size=160&quality=lossless' },
{ threshold: 100000000, id: 'interdimensional-bozo', background: 'interdimensional', image: 'https://tenor.com/view/horizontally-spinning-rat-gif-2222021229431992839.gif' },
{ threshold: 500000000, id: 'multiversal-bozo', background: 'multiversal', image: 'https://tenor.com/view/spinning-rat-gif-26132.gif' },
{ threshold: 1000000000, id: 'billionaire-bozo', background: 'billionaire', image: 'https://tenor.com/view/dancing-rat-rat-dance-gif-25151223.gif' },
{ threshold: 10000000000, id: 'ten-billion-bozo', background: 'ten-billion', image: 'https://tenor.com/view/horizontally-spinning-rat-gif-2222021229431992839.gif' },
{ threshold: 100000000000, id: 'hundred-billion-bozo', background: 'hundred-billion', image: 'https://tenor.com/view/spinning-rat-gif-26132.gif' },
{ threshold: 500000000000, id: 'half-trillion-bozo', background: 'half-trillion', image: 'https://tenor.com/view/dancing-rat-rat-dance-gif-25151223.gif' },
{ threshold: 1000000000000, id: 'trillionaire-bozo', background: 'trillionaire', image: 'https://tenor.com/view/dancing-rat-rat-dance-gif-25151223.gif' }
]; ];
export default class GameServer implements Party.Server { export default class GameServer implements Party.Server {
@@ -137,7 +217,13 @@ export default class GameServer implements Party.Server {
gameState: GameState = { gameState: GameState = {
totalClicks: 0, totalClicks: 0,
users: {}, users: {},
upgrades: UPGRADES.reduce((acc, upgrade) => {
acc[upgrade.id] = { owned: 0, cost: upgrade.baseCost };
return acc;
}, {} as Record<string, { owned: number; cost: number }>),
milestones: {}, milestones: {},
clickMultiplier: 1,
autoClickRate: 0,
currentBackground: 'default', currentBackground: 'default',
currentClickImage: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif' currentClickImage: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif'
}; };
@@ -208,15 +294,7 @@ export default class GameServer implements Party.Server {
name: currentUserName, name: currentUserName,
clicks: 0, clicks: 0,
lastSeen: Date.now(), lastSeen: Date.now(),
bonusMultiplier: 1, // Initialize bonus multiplier bonusMultiplier: 1 // Initialize bonus multiplier
upgrades: ALL_UPGRADES.reduce((acc, upgrade) => {
acc[upgrade.id] = { owned: 0, cost: upgrade.baseCost };
return acc;
}, {} as Record<string, { owned: number; cost: number }>),
clickMultiplier: 1,
autoClickRate: 0,
ratPoisonChallenge: undefined,
ratPoisonImmunityUntil: undefined,
}; };
} }
this.gameState.users[currentUserId].lastSeen = Date.now(); this.gameState.users[currentUserId].lastSeen = Date.now();
@@ -262,210 +340,11 @@ export default class GameServer implements Party.Server {
console.warn(`Unauthorized admin broadcast attempt from ${currentUserId}.`); console.warn(`Unauthorized admin broadcast attempt from ${currentUserId}.`);
} }
break; break;
case 'edit-user':
if (isAuthenticated && currentUserId === this.party.env.CLERK_ADMIN_USERID) {
this.handleEditUser(data);
} else {
console.warn(`Unauthorized edit user attempt from ${currentUserId}.`);
}
break;
case 'throw-rat-poison':
if (isAuthenticated) {
this.handleThrowRatPoison(data, currentUserId);
} else {
console.warn(`Unauthenticated throw-rat-poison from ${currentUserId} ignored.`);
}
break;
case 'solve-rat-poison':
if (isAuthenticated) {
this.handleSolveRatPoison(data, currentUserId);
} else {
console.warn(`Unauthenticated solve-rat-poison from ${currentUserId} ignored.`);
}
break;
} }
this.broadcast(); this.broadcast();
} }
handleEditUser(data: EditUserMessage) {
const { targetUserId, clicks, upgrades } = data;
const userState = this.gameState.users[targetUserId];
if (userState) {
userState.clicks = clicks;
userState.upgrades = upgrades;
this.updateGameMultipliers(targetUserId);
}
}
handleThrowRatPoison(data: ThrowRatPoisonMessage, authenticatedUserId: string) {
const { targetUserId } = data;
const targetUserState = this.gameState.users[targetUserId];
const attackerUserState = this.gameState.users[authenticatedUserId];
if (!targetUserState || !attackerUserState || targetUserId === authenticatedUserId) {
console.log(`Rat Poison: Attempt to poison self or non-existent user (${targetUserId}). Aborting.`);
return; // Cannot poison self or non-existent user
}
// Check if the target user is currently online
if (!this.userConnections.has(targetUserId)) {
const attackerConn = this.userConnections.get(authenticatedUserId);
if (attackerConn) {
attackerConn.send(JSON.stringify({
type: 'rat-poison-feedback',
message: `${targetUserState.name} is currently offline and cannot be targeted.`
}));
}
console.log(`Rat Poison: Target user ${targetUserState.name} (${targetUserId}) is offline. Attacker: ${attackerUserState.name} (${authenticatedUserId}).`);
return;
}
const now = Date.now();
if (targetUserState.ratPoisonImmunityUntil && targetUserState.ratPoisonImmunityUntil > now) {
// Target is immune, notify attacker
const attackerConn = this.userConnections.get(authenticatedUserId);
if (attackerConn) {
attackerConn.send(JSON.stringify({
type: 'rat-poison-feedback',
message: `${targetUserState.name} is currently immune to rat poison!`
}));
}
console.log(`Rat Poison: Target user ${targetUserState.name} (${targetUserId}) is immune until ${new Date(targetUserState.ratPoisonImmunityUntil).toISOString()}. Attacker: ${attackerUserState.name} (${authenticatedUserId}).`);
return;
}
// Get news headlines for challenge string
const newsUpgrade = ALL_UPGRADES.find(u => u.id === 'news');
const newsTitles = newsUpgrade?.newsTitles || [];
if (newsTitles.length === 0) {
console.warn('Rat Poison: No news titles found for rat poison challenge.');
return;
}
const challengeString = [
"What's 9+10",
"Keep on rollin baby",
"MOOD DOWN",
"Haru Urara",
"I am actually a nice guy",
"fembo is still alive",
"I loke tokai teio",
"not so fast my queen",
"random stuff here",
"what are you doing",
"goo goo baby time",
`FU BO`,
`When are you watching paint dry again?`,
`Insert Girls Kissing`,
`We luub bo`,
`MOOD DOWN`,
][Math.floor(Math.random() * newsTitles.length)];
const challengeDuration = 10 * 1000; // 10 seconds to solve
const expiresAt = now + challengeDuration;
targetUserState.ratPoisonChallenge = {
challengeString,
expiresAt,
};
targetUserState.ratPoisonImmunityUntil = expiresAt; // User is immune for the duration of the challenge
console.log(`Rat Poison: Challenge issued to ${targetUserState.name} (${targetUserId}). String: "${challengeString}", Expires: ${new Date(expiresAt).toISOString()}.`);
// Notify target user to start challenge
const targetConn = this.userConnections.get(targetUserId);
if (targetConn) {
targetConn.send(JSON.stringify({
type: 'rat-poison-challenge',
challenge: challengeString,
expiresAt,
}));
}
// Set a timeout for the challenge to expire if not solved
setTimeout(() => {
if (targetUserState.ratPoisonChallenge && targetUserState.ratPoisonChallenge.expiresAt === expiresAt) {
// Challenge not solved in time
console.log(`Rat Poison: Challenge for ${targetUserState.name} (${targetUserId}) expired.`);
this.applyRatPoisonPenalty(targetUserId);
this.broadcast();
}
}, challengeDuration + 500); // Give a small buffer
this.broadcast();
}
handleSolveRatPoison(data: SolveRatPoisonMessage, authenticatedUserId: string) {
const userState = this.gameState.users[authenticatedUserId];
if (!userState || !userState.ratPoisonChallenge) {
console.log(`Rat Poison: No active challenge for user ${authenticatedUserId}. Aborting solve attempt.`);
return; // No active challenge
}
const now = Date.now();
if (now > userState.ratPoisonChallenge.expiresAt) {
// Challenge expired
console.log(`Rat Poison: User ${authenticatedUserId} attempted to solve expired challenge.`);
this.applyRatPoisonPenalty(authenticatedUserId);
} else if (data.challengeString === userState.ratPoisonChallenge.challengeString) {
// Challenge solved successfully
userState.ratPoisonImmunityUntil = now + (2 * 60 * 1000); // 2 minutes immunity
const userConn = this.userConnections.get(authenticatedUserId);
if (userConn) {
userConn.send(JSON.stringify({
type: 'rat-poison-result',
success: true,
message: 'Challenge solved! You are immune for 2 minutes.'
}));
}
console.log(`Rat Poison: User ${authenticatedUserId} successfully solved challenge. Immunity until ${new Date(userState.ratPoisonImmunityUntil).toISOString()}.`);
} else {
// Challenge failed (wrong string)
console.log(`Rat Poison: User ${authenticatedUserId} failed challenge (incorrect string).`);
this.applyRatPoisonPenalty(authenticatedUserId);
const userConn = this.userConnections.get(authenticatedUserId);
if (userConn) {
userConn.send(JSON.stringify({
type: 'rat-poison-result',
success: false,
message: 'Incorrect string! You lost clicks and are immune for 20 seconds.'
}));
}
}
userState.ratPoisonChallenge = undefined; // Clear the challenge
this.broadcast();
}
applyRatPoisonPenalty(userId: string) {
const userState = this.gameState.users[userId];
if (!userState) {
console.log(`Rat Poison: Attempted to apply penalty to non-existent user ${userId}.`);
return;
}
const clicksLost = Math.floor(userState.clicks * 0.30);
userState.clicks -= clicksLost;
userState.ratPoisonImmunityUntil = Date.now() + (20 * 1000); // 20 seconds immunity
console.log(`Rat Poison: Penalty applied to ${userState.name} (${userId}). Lost ${clicksLost} clicks. Immunity until ${new Date(userState.ratPoisonImmunityUntil).toISOString()}.`);
const userConn = this.userConnections.get(userId);
if (userConn) {
userConn.send(JSON.stringify({
type: 'rat-poison-result',
success: false,
message: `You failed the challenge and lost ${clicksLost} clicks! You are immune for 20 seconds.`
}));
}
}
// Define a minimum interval between clicks (e.g., 100ms for 10 clicks/second)
// This helps prevent simple auto-clicker scripts by rate-limiting server-side.
private readonly MIN_CLICK_INTERVAL = 50; // milliseconds
// handleUserJoin is now fully integrated into onMessage and can be removed or simplified. // handleUserJoin is now fully integrated into onMessage and can be removed or simplified.
// Removing it as its logic is now directly in onMessage. // Removing it as its logic is now directly in onMessage.
// handleUserJoin(data: UserJoinMessage) { // handleUserJoin(data: UserJoinMessage) {
@@ -475,29 +354,24 @@ export default class GameServer implements Party.Server {
// } // }
handleClick(data: ClickMessage, authenticatedUserId: string) { handleClick(data: ClickMessage, authenticatedUserId: string) {
const now = Date.now();
const userState = this.gameState.users[authenticatedUserId];
// Ensure user exists (should be handled by onMessage's token verification)
if (!userState) {
console.warn(`User ${authenticatedUserId} not found for click handling.`);
return;
}
// Implement rate limiting
if (userState.lastClickTime && (now - userState.lastClickTime < this.MIN_CLICK_INTERVAL)) {
console.warn(`Rate limit exceeded for user ${authenticatedUserId}. Click ignored.`);
return;
}
// Apply global click multiplier and user-specific bonus multiplier // Apply global click multiplier and user-specific bonus multiplier
const userBonusMultiplier = userState.bonusMultiplier || 1; const userBonusMultiplier = this.gameState.users[authenticatedUserId]?.bonusMultiplier || 1;
const clickValue = userState.clickMultiplier * userBonusMultiplier; const clickValue = this.gameState.clickMultiplier * userBonusMultiplier;
this.gameState.totalClicks += clickValue; this.gameState.totalClicks += clickValue;
userState.clicks += clickValue;
userState.lastSeen = now; // Ensure user exists (should be handled by onMessage's token verification)
userState.lastClickTime = now; // Update last valid click time if (!this.gameState.users[authenticatedUserId]) {
this.gameState.users[authenticatedUserId] = {
name: data.userName, // Use the name from the client, which comes from Clerk
clicks: 0,
lastSeen: Date.now(),
bonusMultiplier: 1 // Initialize if user was just created
};
}
this.gameState.users[authenticatedUserId].clicks += clickValue;
this.gameState.users[authenticatedUserId].lastSeen = Date.now();
this.checkMilestones(); this.checkMilestones();
} }
@@ -537,11 +411,9 @@ export default class GameServer implements Party.Server {
handlePurchaseUpgrade(data: PurchaseUpgradeMessage, authenticatedUserId: string) { handlePurchaseUpgrade(data: PurchaseUpgradeMessage, authenticatedUserId: string) {
const upgradeConfig = UPGRADES.find(u => u.id === data.upgradeId); const upgradeConfig = UPGRADES.find(u => u.id === data.upgradeId);
const userState = this.gameState.users[authenticatedUserId]; const currentUpgradeState = this.gameState.upgrades[data.upgradeId];
if (!upgradeConfig || !userState) return; if (!upgradeConfig || !currentUpgradeState) return;
const currentUpgradeState = userState.upgrades[data.upgradeId];
// Prevent purchasing one-time upgrades if already owned // Prevent purchasing one-time upgrades if already owned
if (upgradeConfig.oneTime && currentUpgradeState.owned > 0) { if (upgradeConfig.oneTime && currentUpgradeState.owned > 0) {
@@ -549,9 +421,9 @@ export default class GameServer implements Party.Server {
return; return;
} }
// Check affordability against user's clicks // Check affordability against totalClicks
if (userState.clicks >= currentUpgradeState.cost) { if (this.gameState.totalClicks >= currentUpgradeState.cost) {
userState.clicks -= currentUpgradeState.cost; // Deduct from user's clicks this.gameState.totalClicks -= currentUpgradeState.cost; // Deduct from totalClicks
currentUpgradeState.owned += 1; currentUpgradeState.owned += 1;
// For one-time upgrades, cost doesn't change after first purchase // For one-time upgrades, cost doesn't change after first purchase
@@ -559,33 +431,28 @@ export default class GameServer implements Party.Server {
currentUpgradeState.cost = Math.floor(upgradeConfig.baseCost * Math.pow(upgradeConfig.multiplier, currentUpgradeState.owned)); currentUpgradeState.cost = Math.floor(upgradeConfig.baseCost * Math.pow(upgradeConfig.multiplier, currentUpgradeState.owned));
} }
this.updateGameMultipliers(authenticatedUserId); this.updateGameMultipliers();
} }
} }
updateGameMultipliers(userId: string, autoClickRateOverride?: number) { updateGameMultipliers() {
const userState = this.gameState.users[userId]; this.gameState.clickMultiplier = 1;
if (!userState) return; this.gameState.autoClickRate = 0;
userState.clickMultiplier = 1; Object.entries(this.gameState.upgrades).forEach(([upgradeId, upgradeState]) => {
userState.autoClickRate = 0;
Object.entries(userState.upgrades).forEach(([upgradeId, upgradeState]) => {
const config = UPGRADES.find(u => u.id === upgradeId); const config = UPGRADES.find(u => u.id === upgradeId);
if (!config) return; if (!config) return; // Should not happen if UPGRADES is consistent
if (config.clickBonus) { if (config.clickBonus) {
userState.clickMultiplier += config.clickBonus * upgradeState.owned; this.gameState.clickMultiplier += config.clickBonus * upgradeState.owned;
} }
if (config.autoClickRate) { if (config.autoClickRate) {
userState.autoClickRate += config.autoClickRate * upgradeState.owned; this.gameState.autoClickRate += config.autoClickRate * upgradeState.owned;
} }
// Note: clickMultiplierBonus from upgrades.ts is handled client-side for spawning frequency
// and applied per-click on the server via handleApplyMultiplierBonus
}); });
if (autoClickRateOverride !== undefined) {
userState.autoClickRate = autoClickRateOverride;
}
this.setupAutoClicker(); this.setupAutoClicker();
} }
@@ -594,21 +461,13 @@ export default class GameServer implements Party.Server {
clearInterval(this.autoClickInterval); clearInterval(this.autoClickInterval);
} }
this.autoClickInterval = setInterval(() => { if (this.gameState.autoClickRate > 0) {
let totalAutoClicks = 0; this.autoClickInterval = setInterval(() => {
Object.values(this.gameState.users).forEach(user => { this.gameState.totalClicks += this.gameState.autoClickRate;
if (user.autoClickRate > 0) {
user.clicks += user.autoClickRate;
totalAutoClicks += user.autoClickRate;
}
});
if (totalAutoClicks > 0) {
this.gameState.totalClicks += totalAutoClicks;
this.checkMilestones(); this.checkMilestones();
this.broadcast(); this.broadcast();
} }, 1000);
}, 1000); }
} }
checkMilestones() { checkMilestones() {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -15,26 +15,18 @@ import { UPGRADES } from './config/upgrades'; // Import UPGRADES for upgrade con
import { useLocation, Link } from 'wouter'; // Import wouter hooks import { useLocation, Link } from 'wouter'; // Import wouter hooks
import AdminPage from './components/AdminPage'; // Import AdminPage import AdminPage from './components/AdminPage'; // Import AdminPage
import { NewsMarquee } from './components/NewsMarquee'; // Import NewsMarquee import { NewsMarquee } from './components/NewsMarquee'; // Import NewsMarquee
import YouTube from 'react-youtube'; // Import YouTube component
import { RatPoisonChallenge } from './components/RatPoisonChallenge'; // Import RatPoisonChallenge
function App() { function App() {
const { isSignedIn, isLoaded, userId: clerkUserId } = useAuth(); // Get clerkUserId from useAuth const { isSignedIn, isLoaded, userId: clerkUserId } = useAuth(); // Get clerkUserId from useAuth
const { gameState, sendClick, purchaseUpgrade, userId, sendMascotClickBonus, lastMessage, sendSolveRatPoison, ratPoisonChallengeData, ratPoisonResultData, ratPoisonFeedbackData, clearRatPoisonChallenge } = usePartyKit(); // Renamed sendShoominionClickBonus const { gameState, sendClick, purchaseUpgrade, userId, sendMascotClickBonus, lastMessage } = usePartyKit(); // Renamed sendShoominionClickBonus
const [celebrationMessage, setCelebrationMessage] = useState<string | null>(null); const [celebrationMessage, setCelebrationMessage] = useState<string | null>(null);
const [previousMilestones, setPreviousMilestones] = useState<Record<string, boolean>>({}); const [previousMilestones, setPreviousMilestones] = useState<Record<string, boolean>>({});
const [mascotEntities, setMascotEntities] = useState<ClickableMascotType[]>([]); const [mascotEntities, setMascotEntities] = useState<ClickableMascotType[]>([]);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const gameStateRef = useRef(gameState); // Ref to hold the latest gameState const gameStateRef = useRef(gameState); // Ref to hold the latest gameState
const clickButtonContainerRef = useRef<HTMLDivElement>(null); // New ref for the click button container
const [location] = useLocation(); // Get current location from wouter const [location] = useLocation(); // Get current location from wouter
const [adminBroadcastMessage, setAdminBroadcastMessage] = useState<string | null>(null); // New state for admin messages const [adminBroadcastMessage, setAdminBroadcastMessage] = useState<string | null>(null); // New state for admin messages
const [showSecretVideo, setShowSecretVideo] = useState(false); // New state for secret video
// Removed local ratPoisonChallenge and ratPoisonResult states, now directly from usePartyKit
console.log('App.tsx: Current ratPoisonChallengeData:', ratPoisonChallengeData);
const userState = isSignedIn && userId ? (gameState?.users[userId] || null) : null;
// Admin user ID from .env // Admin user ID from .env
const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID; const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID;
@@ -61,55 +53,21 @@ function App() {
} }
}, [gameState, previousMilestones]); }, [gameState, previousMilestones]);
// Effect for secret video
useEffect(() => {
if ((userState?.upgrades?.['secretVideo']?.owned || 0) > 0) {
setShowSecretVideo(true);
}
}, [userState]);
// Effect for receiving admin broadcast messages // Effect for receiving admin broadcast messages
useEffect(() => { useEffect(() => {
if (lastMessage) { if (lastMessage) {
console.log('App.tsx: Received lastMessage:', lastMessage); // Log the raw message
try { try {
const parsedMessage = JSON.parse(lastMessage); const parsedMessage = JSON.parse(lastMessage);
if (parsedMessage.type === 'admin-message') { if (parsedMessage.type === 'admin-message') {
setAdminBroadcastMessage(parsedMessage.message); setAdminBroadcastMessage(parsedMessage.message);
setTimeout(() => setAdminBroadcastMessage(null), 7000); // Message disappears after 7 seconds setTimeout(() => setAdminBroadcastMessage(null), 7000); // Message disappears after 7 seconds
} }
// rat-poison-challenge, rat-poison-result, rat-poison-feedback are now handled by dedicated states from usePartyKit
} catch (error) { } catch (error) {
console.error('Failed to parse last message in App.tsx:', error); console.error('Failed to parse last message in App.tsx:', error);
} }
} }
}, [lastMessage]); }, [lastMessage]);
// Effect for displaying rat poison result
useEffect(() => {
if (ratPoisonResultData) {
console.log('App.tsx: ratPoisonResultData updated:', ratPoisonResultData);
// The modal will be rendered directly by ratPoisonChallengeData
// This effect is just for the temporary result message
const timeout = setTimeout(() => {
// Clear the result data after a delay
// This requires a function in usePartyKit to clear it, or pass a prop to RatPoisonChallenge
// For now, we'll just let the message disappear.
}, 5000);
return () => clearTimeout(timeout);
}
}, [ratPoisonResultData]);
// Effect for displaying rat poison feedback
useEffect(() => {
if (ratPoisonFeedbackData) {
console.log('App.tsx: ratPoisonFeedbackData updated:', ratPoisonFeedbackData);
setAdminBroadcastMessage(ratPoisonFeedbackData); // Re-using admin message display for simplicity
const timeout = setTimeout(() => setAdminBroadcastMessage(null), 5000);
return () => clearTimeout(timeout);
}
}, [ratPoisonFeedbackData]);
// Effect to keep gameStateRef updated // Effect to keep gameStateRef updated
useEffect(() => { useEffect(() => {
gameStateRef.current = gameState; gameStateRef.current = gameState;
@@ -117,17 +75,18 @@ function App() {
// Effect for spawning mascots // Effect for spawning mascots
useEffect(() => { useEffect(() => {
if (!userState) return; // Use gameStateRef.current for initial check and to get latest state inside spawnMascot
if (!gameStateRef.current) return;
const friendBoostUpgrade = UPGRADES.find(u => u.id === 'friendBoost') as Upgrade | undefined; const friendBoostUpgrade = UPGRADES.find(u => u.id === 'friendBoost') as Upgrade | undefined;
const ownedFriendBoost = ((userState?.upgrades || {})['friendBoost']?.owned || 0); const ownedFriendBoost = gameStateRef.current.upgrades['friendBoost']?.owned || 0;
if (timeoutRef.current) { if (timeoutRef.current) {
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
} }
if (ownedFriendBoost > 0 && friendBoostUpgrade && friendBoostUpgrade.mascotTiers) { if (ownedFriendBoost > 0 && friendBoostUpgrade && friendBoostUpgrade.mascotTiers) {
const baseInterval = 18000; // Further increased base interval for less frequent spawns const baseInterval = 10000; // Increased base interval for less frequent spawns
const minInterval = 1000; // Increased min interval const minInterval = 1000; // Increased min interval
const interval = Math.max(minInterval, baseInterval / (1 + ownedFriendBoost * 0.2)); // Adjusted scaling const interval = Math.max(minInterval, baseInterval / (1 + ownedFriendBoost * 0.2)); // Adjusted scaling
console.log(`Spawning mascots every ${interval} ms for friendBoost level ${ownedFriendBoost}`); console.log(`Spawning mascots every ${interval} ms for friendBoost level ${ownedFriendBoost}`);
@@ -140,13 +99,25 @@ function App() {
const currentGameState = gameStateRef.current; const currentGameState = gameStateRef.current;
if (!currentGameState) return; // Should not happen if initial check passes if (!currentGameState) return; // Should not happen if initial check passes
const buttonContainer = clickButtonContainerRef.current; // Use the ref
if (!buttonContainer) {
console.warn('Click button container ref not found!');
timeoutRef.current = setTimeout(spawnMascot, 1000); // Retry after 1 second
return;
}
const rect = buttonContainer.getBoundingClientRect();
const spawnAreaPadding = 150;
const minX = rect.left - spawnAreaPadding;
const maxX = rect.right + spawnAreaPadding;
const minY = rect.top - spawnAreaPadding;
const maxY = rect.bottom + spawnAreaPadding;
const viewportWidth = window.innerWidth; const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight; const viewportHeight = window.innerHeight;
// Spawn anywhere on the viewport, with a small padding to avoid edges const randomX = Math.max(0, Math.min(viewportWidth, minX + Math.random() * (maxX - minX)));
const padding = 50; const randomY = Math.max(0, Math.min(viewportHeight, minY + Math.random() * (maxY - minY)));
const randomX = Math.random() * (viewportWidth - padding * 2) + padding;
const randomY = Math.random() * (viewportHeight - padding * 2) + padding;
// Filter available mascots based on ownedFriendBoost level // Filter available mascots based on ownedFriendBoost level
const availableMascots = currentMascotTiers.filter( const availableMascots = currentMascotTiers.filter(
@@ -195,7 +166,7 @@ function App() {
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
} }
}; };
}, [userState]); // Only depend on userState }, [gameState?.upgrades['friendBoost']?.owned]); // Only depend on friendBoost level
const handleMascotClick = (id: string, multiplierBonus: number) => { // Renamed handler const handleMascotClick = (id: string, multiplierBonus: number) => { // Renamed handler
sendMascotClickBonus(multiplierBonus); // Renamed function call sendMascotClickBonus(multiplierBonus); // Renamed function call
@@ -206,15 +177,6 @@ function App() {
setMascotEntities((prev) => prev.filter((m) => m.id !== id)); // Update state variable setMascotEntities((prev) => prev.filter((m) => m.id !== id)); // Update state variable
}; };
const handleSolveRatPoison = (solvedString: string) => {
sendSolveRatPoison(solvedString);
// The challenge data will be cleared by usePartyKit or by the modal's onClose
// To explicitly clear the modal, we can set ratPoisonChallengeData to null here.
// However, it's better if usePartyKit handles clearing it after a successful solve/fail.
// For now, we'll leave it as is, assuming the server will eventually clear it.
clearRatPoisonChallenge(); // Clear the challenge data when solved or closed
};
if (!isLoaded || !gameState) { if (!isLoaded || !gameState) {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-900 to-pink-900"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-900 to-pink-900">
@@ -225,10 +187,8 @@ function App() {
); );
} }
const userBonusMultiplier = isSignedIn && userId ? gameState.users[userId]?.bonusMultiplier || 1 : 1;
const effectiveClickMultiplier = gameState.clickMultiplier * userBonusMultiplier;
const userBonusMultiplier = userState?.bonusMultiplier || 1;
const effectiveClickMultiplier = (userState?.clickMultiplier || 1) * userBonusMultiplier;
// Render the AdminPage if the current location is /admin // Render the AdminPage if the current location is /admin
if (location === '/admin') { if (location === '/admin') {
@@ -271,50 +231,24 @@ function App() {
</div> </div>
)} )}
{/* Rat Poison Result Message */}
{ratPoisonResultData && (
<div className="fixed top-40 left-0 right-0 z-50 flex justify-center pt-8">
<div className={`px-8 py-4 rounded-full text-2xl font-bold shadow-2xl animate-pulse border-4 border-white ${ratPoisonResultData.success ? 'bg-green-600' : 'bg-red-600'}`}>
{ratPoisonResultData.message}
</div>
</div>
)}
{/* News Marquee */} {/* News Marquee */}
{(() => { {gameState.upgrades['news']?.owned > 0 && UPGRADES.find(u => u.id === 'news')?.newsTitles && (
const newsUpgrade = UPGRADES.find(u => u.id === 'news'); <NewsMarquee titles={UPGRADES.find(u => u.id === 'news')!.newsTitles!} />
if ( )}
userState &&
userState.upgrades &&
userState.upgrades['news'] &&
userState.upgrades['news'].owned > 0 &&
newsUpgrade?.newsTitles
) {
return <NewsMarquee titles={(newsUpgrade as Upgrade).newsTitles!} />;
}
return null;
})()}
<div className="container mx-auto px-4 py-8 relative z-10"> <div className="container mx-auto px-4 py-8 relative z-10">
{/* Header */} {/* Header */}
<div className="mb-8"> <div className="mb-8">
<Counter <Counter
totalClicks={gameState.totalClicks} totalClicks={gameState.totalClicks}
autoClickRate={userState?.autoClickRate || 0} autoClickRate={gameState.autoClickRate}
/> />
</div> </div>
{/* Main Content */} {/* Main Content */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Seizure Warning */}
<div className="fixed top-0 left-0 right-0 z-50 flex justify-center pt-4">
<div className="bg-red-800 text-white px-6 py-3 rounded-lg text-lg font-bold shadow-xl border-2 border-red-500 animate-pulse">
Seizure Warning: This game contains rapidly flashing lights and animations.
</div>
</div>
{/* Left Column - Click Button */} {/* Left Column - Click Button */}
<div id="click-button-container" className="lg:col-span-1 flex flex-col items-center justify-center relative"> {/* Added ref, id and relative positioning */} <div ref={clickButtonContainerRef} id="click-button-container" className="lg:col-span-1 flex flex-col items-center justify-center relative"> {/* Added ref, id and relative positioning */}
<ClickButton <ClickButton
onClick={sendClick} onClick={sendClick}
imageUrl={gameState.currentClickImage} imageUrl={gameState.currentClickImage}
@@ -339,7 +273,7 @@ function App() {
<div className="lg:col-span-1"> <div className="lg:col-span-1">
<UpgradeShop <UpgradeShop
gameState={gameState} gameState={gameState}
userState={userState} totalClicks={gameState.totalClicks}
onPurchase={purchaseUpgrade} onPurchase={purchaseUpgrade}
/> />
</div> </div>
@@ -364,32 +298,6 @@ function App() {
</div> </div>
</div> </div>
{/* Secret Video Player */}
{showSecretVideo && (
<div className="fixed bottom-4 left-4 z-50">
<YouTube
videoId={UPGRADES.find(u => u.id === 'secretVideo')?.youtubeId || ''}
opts={{
height: '195',
width: '320',
playerVars: {
autoplay: 1,
},
}}
/>
</div>
)}
{/* Rat Poison Challenge Modal */}
{ratPoisonChallengeData && (
<RatPoisonChallenge
challengeString={ratPoisonChallengeData.challengeString}
expiresAt={ratPoisonChallengeData.expiresAt}
onSolve={handleSolveRatPoison}
onClose={clearRatPoisonChallenge} // Call the clear function from usePartyKit
/>
)}
{/* Sparkle Effects */} {/* Sparkle Effects */}
<div className="fixed inset-0 pointer-events-none z-20"> <div className="fixed inset-0 pointer-events-none z-20">
{[...Array(10)].map((_, i) => ( {[...Array(10)].map((_, i) => (

View File

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 233 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useUser } from '@clerk/clerk-react'; import { useUser } from '@clerk/clerk-react';
import { usePartyKit } from '../hooks/usePartyKit'; import { usePartyKit } from '../hooks/usePartyKit';
import { UserState } from '../types';
interface AdminMessage { interface AdminMessage {
type: 'admin-message'; type: 'admin-message';
@@ -11,15 +10,11 @@ interface AdminMessage {
const AdminPage: React.FC = () => { const AdminPage: React.FC = () => {
const { user } = useUser(); const { user } = useUser();
const { sendMessage, lastMessage, getToken, gameState, editUser } = usePartyKit(); const { sendMessage, lastMessage, getToken } = usePartyKit();
const [broadcastMessage, setBroadcastMessage] = useState(''); const [broadcastMessage, setBroadcastMessage] = useState('');
const [targetUserId, setTargetUserId] = useState(''); const [targetUserId, setTargetUserId] = useState('');
const [receivedMessages, setReceivedMessages] = useState<AdminMessage[]>([]); const [receivedMessages, setReceivedMessages] = useState<AdminMessage[]>([]);
const [isAdmin, setIsAdmin] = useState(false); const [isAdmin, setIsAdmin] = useState(false);
const [selectedUser, setSelectedUser] = useState<UserState & { id: string } | null>(null);
const [clicks, setClicks] = useState(0);
const [autoClickRate, setAutoClickRate] = useState(0);
const [upgrades, setUpgrades] = useState<Record<string, { owned: number; cost: number }>>({});
// Replace with your actual admin user ID from .env or similar // Replace with your actual admin user ID from .env or similar
const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID; const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID;
@@ -67,12 +62,6 @@ const AdminPage: React.FC = () => {
} }
}; };
const handleEditUser = () => {
if (selectedUser) {
editUser(selectedUser.id, clicks, upgrades); // Removed autoClickRate
}
};
if (!isAdmin) { if (!isAdmin) {
return ( return (
<div className="flex items-center justify-center min-h-screen bg-gray-900 text-white"> <div className="flex items-center justify-center min-h-screen bg-gray-900 text-white">
@@ -122,83 +111,6 @@ const AdminPage: React.FC = () => {
</div> </div>
<div className="p-6 bg-gray-800 rounded-lg shadow-lg"> <div className="p-6 bg-gray-800 rounded-lg shadow-lg">
<h2 className="text-2xl font-semibold mb-4">User Management</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<h3 className="text-xl font-semibold mb-4">Users</h3>
<div className="max-h-96 overflow-y-auto bg-gray-700 p-4 rounded-md">
<ul>
{gameState &&
Object.entries(gameState.users).map(([id, user]) => (
<li
key={id}
className={`cursor-pointer p-3 rounded-md mb-2 ${selectedUser?.id === id ? 'bg-blue-500' : 'bg-gray-600'}`}
onClick={() => {
const clickedUser = gameState?.users[id]; // Get the user from gameState
if (clickedUser) { // Check if it's not undefined
setSelectedUser({ id, ...clickedUser });
setClicks(clickedUser.clicks);
setUpgrades(clickedUser.upgrades);
}
}}
>
{user.name}
</li>
))}
</ul>
</div>
</div>
{selectedUser && (
<div>
<h3 className="text-xl font-semibold mb-4">Edit User</h3>
<div className="mb-4">
<label htmlFor="clicks" className="block text-lg font-medium mb-2">
Clicks:
</label>
<input
type="number"
id="clicks"
className="w-full p-3 rounded-md bg-gray-700 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={clicks}
onChange={(e) => setClicks(Number(e.target.value))}
/>
</div>
<div className="mb-4">
<label htmlFor="autoClickRate" className="block text-lg font-medium mb-2">
Auto Click Rate:
</label>
<input
type="number"
id="autoClickRate"
className="w-full p-3 rounded-md bg-gray-700 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={autoClickRate}
onChange={(e) => setAutoClickRate(Number(e.target.value))}
/>
</div>
<div className="mb-4">
<label htmlFor="upgrades" className="block text-lg font-medium mb-2">
Upgrades (JSON):
</label>
<textarea
id="upgrades"
className="w-full p-3 rounded-md bg-gray-700 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={10}
value={JSON.stringify(upgrades, null, 2)}
onChange={(e) => setUpgrades(JSON.parse(e.target.value))}
></textarea>
</div>
<button
onClick={handleEditUser}
className="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-md transition duration-300 ease-in-out"
>
Save Changes
</button>
</div>
)}
</div>
</div>
<div className="p-6 bg-gray-800 rounded-lg shadow-lg mt-8">
<h2 className="text-2xl font-semibold mb-4">Received Admin Messages</h2> <h2 className="text-2xl font-semibold mb-4">Received Admin Messages</h2>
<div className="max-h-64 overflow-y-auto bg-gray-700 p-4 rounded-md"> <div className="max-h-64 overflow-y-auto bg-gray-700 p-4 rounded-md">
{receivedMessages.length === 0 ? ( {receivedMessages.length === 0 ? (

View File

@@ -33,11 +33,11 @@ export function Background({ background }: BackgroundProps) {
backgroundSize: '200px 100px', backgroundSize: '200px 100px',
animation: 'stars 50s linear infinite' animation: 'stars 50s linear infinite'
}; };
case 'musume': case 'glitch':
return { return {
background: 'url("/special/umamusume.gif") no-repeat center center fixed', background: 'linear-gradient(45deg, #ff0000, #00ff00, #0000ff, #ffff00, #ff00ff, #00ffff)',
backgroundSize: 'cover', backgroundSize: '400% 400%',
animation: 'none' animation: 'glitch 2s ease infinite'
}; };
case 'ultimate': case 'ultimate':
return { return {
@@ -47,52 +47,10 @@ export function Background({ background }: BackgroundProps) {
}; };
case 'god-tier': case 'god-tier':
return { return {
background: 'linear-gradient(135deg, #000000 0%, #ffffff 100%)', background: 'url(/src/special/bathtub.gif) no-repeat center center fixed',
backgroundSize: '200% 200%', backgroundSize: 'contain',
animation: 'god-tier 10s ease infinite' animation: 'god-tier 10s ease infinite'
}; };
case 'interdimensional':
return {
background: 'url("https://media1.tenor.com/m/2h4_nJ8yG7AAAAAC/inter-dimension-traveler.gif") no-repeat center center fixed',
backgroundSize: 'cover',
animation: 'none'
};
case 'multiversal':
return {
background: 'url("https://media1.tenor.com/m/E2h_b5s2_3swAAAAC/unlimited-power.gif") no-repeat center center fixed',
backgroundSize: 'cover',
animation: 'none'
};
case 'billionaire':
return {
background: 'url("https://media1.tenor.com/m/6m-rain-wallstreet-make-it-rain.gif") no-repeat center center fixed',
backgroundSize: 'cover',
animation: 'none'
};
case 'ten-billion':
return {
background: 'url("https://media1.tenor.com/m/rookie-numbers-those-are-rookie-numbers.gif") no-repeat center center fixed',
backgroundSize: 'cover',
animation: 'none'
};
case 'hundred-billion':
return {
background: 'url("https://media1.tenor.com/m/galaxy-brain-animated.gif") no-repeat center center fixed',
backgroundSize: 'cover',
animation: 'none'
};
case 'half-trillion':
return {
background: 'url("https://media1.tenor.com/m/challenger-approaching-super-smash-bros.gif") no-repeat center center fixed',
backgroundSize: 'cover',
animation: 'none'
};
case 'trillionaire':
return {
background: 'url("https://media1.tenor.com/m/Kq_b5s2_3swAAAAC/unlimited-power.gif") no-repeat center center fixed',
backgroundSize: 'cover',
animation: 'none'
};
default: default:
return { return {

View File

@@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { GameState } from '../types'; import { GameState } from '../types';
import { usePartyKit } from '../hooks/usePartyKit';
interface LeaderboardProps { interface LeaderboardProps {
gameState: GameState; gameState: GameState;
@@ -8,7 +7,6 @@ interface LeaderboardProps {
} }
export function Leaderboard({ gameState, currentUserId }: LeaderboardProps) { export function Leaderboard({ gameState, currentUserId }: LeaderboardProps) {
const { throwRatPoison } = usePartyKit();
const sortedUsers = Object.entries(gameState.users) const sortedUsers = Object.entries(gameState.users)
.sort(([, a], [, b]) => b.clicks - a.clicks) .sort(([, a], [, b]) => b.clicks - a.clicks)
.slice(0, 10); .slice(0, 10);
@@ -23,7 +21,6 @@ export function Leaderboard({ gameState, currentUserId }: LeaderboardProps) {
{sortedUsers.map(([userId, user], index) => { {sortedUsers.map(([userId, user], index) => {
const isCurrentUser = userId === currentUserId; const isCurrentUser = userId === currentUserId;
const isTopThree = index < 3; const isTopThree = index < 3;
const isImmune = user.ratPoisonImmunityUntil && user.ratPoisonImmunityUntil > Date.now();
return ( return (
<div <div
@@ -45,24 +42,9 @@ export function Leaderboard({ gameState, currentUserId }: LeaderboardProps) {
{user.name} {isCurrentUser ? '(You)' : ''} {user.name} {isCurrentUser ? '(You)' : ''}
</span> </span>
</div> </div>
<div className="flex items-center space-x-2"> <span className={`text-xl font-bold ${isCurrentUser ? 'text-black' : 'text-yellow-300'}`}>
<span className={`text-xl font-bold ${isCurrentUser ? 'text-black' : 'text-yellow-300'}`}> {user.clicks.toLocaleString()}
{user.clicks.toLocaleString()} </span>
</span>
{!isCurrentUser && (
<button
onClick={() => throwRatPoison(userId)}
disabled={isImmune}
className={`px-3 py-1 rounded-md text-sm font-bold transition-colors duration-200 ${
isImmune
? 'bg-gray-500 text-gray-300 cursor-not-allowed'
: 'bg-red-500 hover:bg-red-600 text-white'
}`}
>
{isImmune ? 'Immune' : 'Throw Poison'}
</button>
)}
</div>
</div> </div>
</div> </div>
); );

View File

@@ -1,87 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
interface RatPoisonChallengeProps {
challengeString: string;
expiresAt: number;
onSolve: (solvedString: string) => void;
onClose: () => void;
}
export function RatPoisonChallenge({ challengeString, expiresAt, onSolve, onClose }: RatPoisonChallengeProps) {
const [inputValue, setInputValue] = useState('');
const [timeLeft, setTimeLeft] = useState(0);
const [initialDuration, setInitialDuration] = useState(0);
const timerRef = useRef<number | null>(null);
useEffect(() => {
const now = Date.now();
const duration = expiresAt - now;
setInitialDuration(Math.ceil(duration / 1000));
const calculateTimeLeft = () => {
const remaining = Math.max(0, expiresAt - Date.now());
setTimeLeft(Math.ceil(remaining / 1000));
};
calculateTimeLeft(); // Initial calculation
timerRef.current = setInterval(calculateTimeLeft, 1000);
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}, [expiresAt]);
useEffect(() => {
if (timeLeft === 0 && Date.now() >= expiresAt) {
// Challenge expired, automatically close or handle failure
onSolve(''); // Send empty string to indicate failure due to timeout
onClose();
}
}, [timeLeft, expiresAt, onSolve, onClose]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSolve(inputValue);
onClose();
};
const progressPercentage = initialDuration > 0 ? (timeLeft / initialDuration) * 100 : 0;
return (
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 pointer-events-auto">
<div className="bg-gradient-to-br from-purple-700 to-indigo-900 p-8 rounded-lg shadow-2xl border-4 border-pink-500 text-white text-center max-w-md mx-auto">
<h2 className="text-3xl font-bold mb-4 text-yellow-300">RAT POISON CHALLENGE!</h2>
<p className="text-lg mb-6">Type the following string:</p>
<p className="text-2xl font-mono bg-gray-800 p-4 rounded-md break-all mb-6 border-2 border-gray-600">{challengeString}</p>
<div className="w-full bg-gray-700 rounded-full h-4 mb-4">
<div
className="bg-green-500 h-4 rounded-full transition-all duration-1000 ease-linear"
style={{ width: `${progressPercentage}%` }}
></div>
</div>
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className="p-3 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Type here..."
autoFocus
/>
<button
type="submit"
className="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-md transition duration-300 ease-in-out transform hover:scale-105"
disabled={timeLeft === 0}
>
SUBMIT ({timeLeft}s)
</button>
</form>
{timeLeft === 0 && <p className="text-red-400 mt-4">Time's up!</p>}
</div>
</div>
);
}

View File

@@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { UPGRADES } from '../config/upgrades'; import { UPGRADES } from '../config/upgrades';
import { GameState, MascotTier, UserState } from '../types'; // Import MascotTier import { GameState, MascotTier } from '../types'; // Import MascotTier
interface UpgradeShopProps { interface UpgradeShopProps {
gameState: GameState; gameState: GameState;
userState: UserState | null; totalClicks: number; // Changed from userClicks
onPurchase: (upgradeId: string) => void; onPurchase: (upgradeId: string) => void;
} }
@@ -22,13 +22,7 @@ const getMascotName = (imageSrc: string): string => {
.join(' '); .join(' ');
}; };
export function UpgradeShop({ userState, onPurchase }: UpgradeShopProps) { // Changed from userClicks export function UpgradeShop({ gameState, totalClicks, onPurchase }: UpgradeShopProps) { // Changed from userClicks
if (!userState) {
return null; // Or a loading/signed-out state
}
const { clicks: userClicks, upgrades: userUpgrades } = userState;
return ( return (
<div className="bg-gradient-to-b from-purple-800 to-pink-600 p-6 rounded-xl border-4 border-cyan-400 shadow-2xl"> <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' }}> <h2 className="text-3xl font-bold text-yellow-300 mb-6 text-center" style={{ fontFamily: 'Comic Sans MS, cursive' }}>
@@ -37,9 +31,9 @@ export function UpgradeShop({ userState, onPurchase }: UpgradeShopProps) { // Ch
<div className="space-y-4"> <div className="space-y-4">
{UPGRADES.map((upgrade) => { {UPGRADES.map((upgrade) => {
const owned = userUpgrades[upgrade.id]?.owned || 0; const owned = gameState.upgrades[upgrade.id]?.owned || 0;
const cost = userUpgrades[upgrade.id]?.cost || upgrade.baseCost; const cost = gameState.upgrades[upgrade.id]?.cost || upgrade.baseCost;
const canAfford = userClicks >= cost; const canAfford = totalClicks >= cost; // Changed from userClicks
// If it's a one-time upgrade and already owned, don't display it // If it's a one-time upgrade and already owned, don't display it
if (upgrade.oneTime && owned > 0) { if (upgrade.oneTime && owned > 0) {
@@ -99,7 +93,7 @@ export function UpgradeShop({ userState, onPurchase }: UpgradeShopProps) { // Ch
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<div className="text-2xl font-bold text-yellow-300"> <div className="text-2xl font-bold text-yellow-300">
Your Clicks: {userClicks.toLocaleString()} Total Clicks: {totalClicks.toLocaleString()}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -10,13 +10,22 @@ export const MILESTONES: Milestone[] = [
image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif', image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif',
reward: '🌈 Rainbow Background Unlocked!' reward: '🌈 Rainbow Background Unlocked!'
}, },
{
threshold: 5000,
id: 'five-thousand',
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: 10000, threshold: 10000,
id: 'ten-thousand', id: 'ten-thousand',
name: 'Cyber Rat', name: 'Cyber Rat',
description: 'Welcome to the future', description: 'Welcome to the future',
background: 'cyberpunk', background: 'cyberpunk',
image: 'https://cdn.discordapp.com/emojis/1297946545626288312.webp', image: 'https://media1.tenor.com/m/YsWlbVbRWFQAAAAd/rat-spinning.gif',
reward: '🦾 Cyberpunk Aesthetic Engaged!' reward: '🦾 Cyberpunk Aesthetic Engaged!'
}, },
{ {
@@ -25,34 +34,25 @@ export const MILESTONES: Milestone[] = [
name: 'Space Cadet', name: 'Space Cadet',
description: 'To infinity and beyond!', description: 'To infinity and beyond!',
background: 'space', background: 'space',
image: 'https://media.discordapp.net/stickers/1377379131649429554.gif?size=160&quality=lossless', image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif',
reward: '🚀 Space Background Unlocked!' reward: '🚀 Space Background Unlocked!'
}, },
{ {
threshold: 100000, threshold: 100000,
id: 'legendary', id: 'legendary',
name: 'musume mode', name: 'Glitch in the Matrix',
description: 'Reality is breaking down, the girls are turning into horses!', description: 'Reality is breaking down',
background: 'musume',
image: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif',
reward: '🐴 Musume Background Unlocked!'
},
{
threshold: 200000,
id: 'two-hundred-thousand',
name: 'Major Milestone',
description: 'You are progressing rapidly!',
background: 'glitch', background: 'glitch',
image: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif', image: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif',
reward: '✨ New Background Unlocked!' reward: '⚡ Glitch Effect Activated!'
}, },
{ {
threshold: 696969, threshold: 500000,
id: 'ultimate', id: 'ultimate',
name: 'Ultimate Bozo', name: 'Ultimate Bozo',
description: 'You have achieved peak bozo status', description: 'You have achieved peak bozo status',
background: 'ultimate', background: 'ultimate',
image: 'https://media.discordapp.net/stickers/1397981135266648064.webp?size=160&quality=lossless', image: 'https://media1.tenor.com/m/YsWlbVbRWFQAAAAd/rat-spinning.gif',
reward: '👑 Ultimate Power Unlocked!' reward: '👑 Ultimate Power Unlocked!'
}, },
{ {
@@ -61,97 +61,7 @@ export const MILESTONES: Milestone[] = [
name: 'God Tier Bozo', name: 'God Tier Bozo',
description: 'You are the ultimate bozo', description: 'You are the ultimate bozo',
background: 'god-tier', background: 'god-tier',
image: 'https://media.discordapp.net/stickers/1294812453112123453.webp?quality=lossless',
reward: '🌟 God Mode Activated!'
},
{
threshold: 5000000,
id: 'mega-bozo',
name: 'Mega Bozo',
description: 'You are a true clicking machine!',
background: 'mega-bozo',
image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif', image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif',
reward: '💥 Mega Bozo Background Unlocked!' reward: '🌟 God Mode Activated!'
},
{
threshold: 8008008,
id: 'galactic-bozo',
name: 'Galactic Bozo',
description: 'Your clicks echo across the galaxy!',
background: 'galactic-bozo',
image: 'https://cdn.discordapp.com/emojis/1297946545626288312.webp',
reward: '🌌 Galactic Background Unlocked!'
},
{
threshold: 10000000,
id: 'cosmic-bozo',
name: 'Cosmic Bozo',
description: 'You are one with the universe of bozos!',
background: 'cosmic-bozo',
image: 'https://media.discordapp.net/stickers/1377379131649429554.gif?size=160&quality=lossless',
reward: '🌠 Cosmic Background Unlocked!'
},
{
threshold: 100000000,
id: 'interdimensional-bozo',
name: 'Interdimensional Bozo',
description: 'You are breaking the barriers of reality!',
background: 'interdimensional',
image: 'https://tenor.com/view/horizontally-spinning-rat-gif-2222021229431992839.gif',
reward: '🌀 Interdimensional Background Unlocked!'
},
{
threshold: 500000000,
id: 'multiversal-bozo',
name: 'Multiversal Bozo',
description: 'Your bozo energy resonates across multiverses!',
background: 'multiversal',
image: 'https://tenor.com/view/spinning-rat-gif-26132.gif',
reward: '🌌 Multiversal Background Unlocked!'
},
{
threshold: 1000000000,
id: 'billionaire-bozo',
name: 'Billionaire Bozo',
description: 'You have achieved a billion clicks!',
background: 'billionaire',
image: 'https://tenor.com/view/dancing-rat-rat-dance-gif-25151223.gif',
reward: '💰 Billionaire Background Unlocked!'
},
{
threshold: 10000000000,
id: 'ten-billion-bozo',
name: 'Ten Billion Bozo',
description: 'The clicks are overflowing!',
background: 'ten-billion',
image: 'https://tenor.com/view/horizontally-spinning-rat-gif-2222021229431992839.gif',
reward: '📈 Ten Billion Background Unlocked!'
},
{
threshold: 100000000000,
id: 'hundred-billion-bozo',
name: 'Hundred Billion Bozo',
description: 'You are a legend in the world of bozos!',
background: 'hundred-billion',
image: 'https://tenor.com/view/spinning-rat-gif-26132.gif',
reward: '🏆 Hundred Billion Background Unlocked!'
},
{
threshold: 500000000000,
id: 'half-trillion-bozo',
name: 'Half Trillion Bozo',
description: 'Almost there, you can taste the trillions!',
background: 'half-trillion',
image: 'https://tenor.com/view/dancing-rat-rat-dance-gif-25151223.gif',
reward: '🚀 Half Trillion Background Unlocked!'
},
{
threshold: 1000000000000,
id: 'trillionaire-bozo',
name: 'Trillionaire Bozo',
description: 'You are the ultimate bozo, a true trillionaire!',
background: 'trillionaire',
image: 'https://tenor.com/view/dancing-rat-rat-dance-gif-25151223.gif',
reward: '👑 Trillionaire Background Unlocked!'
} }
]; ];

View File

@@ -56,41 +56,41 @@ export const UPGRADES: Upgrade[] = [
mascotTiers: [ mascotTiers: [
{ {
level: 0, level: 0,
imageSrc: '/assets/bozo.png', imageSrc: '/src/assets/bozo.png',
multiplier: 1.002, multiplier: 1.02,
rarity: 1.0, rarity: 1.0,
}, },
{ {
level: 1, level: 1,
imageSrc: '/assets/shoominion.png', imageSrc: '/src/assets/shoominion.png',
multiplier: 1.003, multiplier: 1.03,
rarity: 0.8, rarity: 0.8,
}, },
{ {
level: 5, level: 5,
imageSrc: '/assets/codebug.gif', imageSrc: '/src/assets/codebug.gif',
multiplier: 1.005, multiplier: 1.05,
rarity: 0.6, rarity: 0.6,
}, },
{ {
level: 10, level: 10,
imageSrc: '/assets/lalan.gif', imageSrc: '/src/assets/lalan.gif',
multiplier: 1.007, multiplier: 1.07,
rarity: 0.4, rarity: 0.4,
}, },
{ {
level: 15, level: 15,
imageSrc: '/assets/neuro-neurosama.gif', imageSrc: '/src/assets/neuro-neurosama.gif',
multiplier: 1.010, multiplier: 1.10,
rarity: 0.2, rarity: 0.2,
}, },
{ {
level: 20, level: 20,
imageSrc: '/assets/evil-neurosama.gif', imageSrc: '/src/assets/evil-neurosama.gif',
multiplier: 1.015, multiplier: 1.15,
rarity: 0.1, rarity: 0.1,
}, },
] ],
}, },
{ {
id: 'news', id: 'news',
@@ -124,15 +124,5 @@ export const UPGRADES: Upgrade[] = [
`om`, `om`,
`You are the Glue to my life like how Tokai Teio is glue(stick) to a child's art project`, `You are the Glue to my life like how Tokai Teio is glue(stick) to a child's art project`,
] ]
},
{
id: 'secretVideo',
name: '🎬 Secret Video',
description: 'Unlock a secret video. Plays automatically!',
baseCost: 1000000, // Very expensive
multiplier: 1,
icon: '🎬',
oneTime: true,
youtubeId: 'ONzntmMFXGE' // The YouTube video ID
} }
]; ];

View File

@@ -3,17 +3,14 @@ import PartySocket from 'partysocket';
import { GameState } from '../types'; import { GameState } from '../types';
import { useAuth, useUser } from '@clerk/clerk-react'; import { useAuth, useUser } from '@clerk/clerk-react';
const PARTY_HOST = import.meta.env.DEV ? 'https://solid-rotary-phone-pg69xw5vj7rc7r55-1999.app.github.dev' : 'bozo-clicker.arjunindia.partykit.dev'; const PARTY_HOST = import.meta.env.DEV ? 'localhost:1999' : 'bozo-clicker.arjunindia.partykit.dev';
export function usePartyKit() { export function usePartyKit() {
const { getToken, isLoaded, isSignedIn } = useAuth(); const { getToken, isLoaded, isSignedIn } = useAuth();
const { user } = useUser(); const { user } = useUser();
const [gameState, setGameState] = useState<GameState | null>(null); const [gameState, setGameState] = useState<GameState | null>(null);
const [socket, setSocket] = useState<PartySocket | null>(null); const [socket, setSocket] = useState<PartySocket | null>(null);
const [lastMessage, setLastMessage] = useState<string | null>(null); const [lastMessage, setLastMessage] = useState<string | null>(null); // New state for last message
// ratPoisonChallengeData will now be derived from gameState
const [ratPoisonResultData, setRatPoisonResultData] = useState<{ success: boolean; message: string } | null>(null);
const [ratPoisonFeedbackData, setRatPoisonFeedbackData] = useState<string | null>(null);
// Generate a persistent guest ID if the user is not signed in // Generate a persistent guest ID if the user is not signed in
const [guestId] = useState(() => { const [guestId] = useState(() => {
let storedGuestId = localStorage.getItem('bozo_guest_id'); let storedGuestId = localStorage.getItem('bozo_guest_id');
@@ -62,15 +59,8 @@ export function usePartyKit() {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.type === 'game-state') { if (data.type === 'game-state') {
setGameState(data.state); setGameState(data.state);
} else if (data.type === 'rat-poison-result') {
console.log('usePartyKit: Setting ratPoisonResultData:', { success: data.success, message: data.message });
setRatPoisonResultData({ success: data.success, message: data.message });
} else if (data.type === 'rat-poison-feedback') {
console.log('usePartyKit: Setting ratPoisonFeedbackData:', data.message);
setRatPoisonFeedbackData(data.message);
} }
// No longer setting ratPoisonChallengeData directly here, it will be derived from gameState setLastMessage(event.data); // Store the raw last message
setLastMessage(event.data); // Still store the raw last message for other general purposes if needed
}; };
setSocket(ws); setSocket(ws);
@@ -78,7 +68,7 @@ export function usePartyKit() {
return () => { return () => {
ws.close(); ws.close();
}; };
}, [isLoaded, isSignedIn, user, getToken, guestId, guestName]); }, [isLoaded, guestId, guestName]); // Removed isSignedIn, user, getToken from dependencies
const sendClick = useCallback(async () => { const sendClick = useCallback(async () => {
if (socket && isSignedIn && user) { if (socket && isSignedIn && user) {
@@ -130,52 +120,6 @@ export function usePartyKit() {
const currentUserId = isSignedIn && user ? user.id : undefined; const currentUserId = isSignedIn && user ? user.id : undefined;
const currentUserName = isSignedIn && user ? (user.username || user.fullName || user.emailAddresses[0].emailAddress) : undefined; const currentUserName = isSignedIn && user ? (user.username || user.fullName || user.emailAddresses[0].emailAddress) : undefined;
const editUser = useCallback(async (targetUserId: string, clicks: number, upgrades: Record<string, { owned: number; cost: number }>) => {
if (socket && isSignedIn && user) {
const token = await getToken();
socket.send(JSON.stringify({
type: 'edit-user',
token,
userId: user.id,
userName: user.username || user.fullName || user.emailAddresses[0].emailAddress,
targetUserId,
clicks,
upgrades
}));
}
}, [socket, isSignedIn, user, getToken]);
const throwRatPoison = useCallback(async (targetUserId: string) => {
if (socket && isSignedIn && user) {
const token = await getToken();
socket.send(JSON.stringify({
type: 'throw-rat-poison',
userId: user.id,
userName: user.username || user.fullName || user.emailAddresses[0].emailAddress,
token,
targetUserId,
}));
}
}, [socket, isSignedIn, user, getToken]);
const sendSolveRatPoison = useCallback(async (challengeString: string) => {
if (socket && isSignedIn && user) {
const token = await getToken();
socket.send(JSON.stringify({
type: 'solve-rat-poison',
userId: user.id,
userName: user.username || user.fullName || user.emailAddresses[0].emailAddress,
token,
challengeString,
}));
}
}, [socket, isSignedIn, user, getToken]);
const clearRatPoisonChallenge = useCallback(() => {
setRatPoisonResultData(null);
setRatPoisonFeedbackData(null);
}, []);
return { return {
gameState, gameState,
sendClick, sendClick,
@@ -185,13 +129,6 @@ export function usePartyKit() {
lastMessage, // Expose lastMessage lastMessage, // Expose lastMessage
userId: currentUserId, userId: currentUserId,
userName: currentUserName, userName: currentUserName,
getToken, // Expose getToken getToken // Expose getToken
editUser,
throwRatPoison,
sendSolveRatPoison,
ratPoisonResultData,
ratPoisonFeedbackData,
ratPoisonChallengeData: gameState?.users[currentUserId || '']?.ratPoisonChallenge || null,
clearRatPoisonChallenge,
}; };
} }

View File

Before

Width:  |  Height:  |  Size: 664 KiB

After

Width:  |  Height:  |  Size: 664 KiB

View File

@@ -1,22 +1,10 @@
export interface UserState {
name: string;
clicks: number;
lastSeen: number;
bonusMultiplier: number;
upgrades: Record<string, { owned: number; cost: number }>;
clickMultiplier: number;
autoClickRate: number;
ratPoisonChallenge?: {
challengeString: string;
expiresAt: number;
};
ratPoisonImmunityUntil?: number;
}
export interface GameState { export interface GameState {
totalClicks: number; totalClicks: number;
users: Record<string, UserState>; users: Record<string, { name: string; clicks: number; lastSeen: number; bonusMultiplier: number }>; // Added bonusMultiplier
upgrades: Record<string, { owned: number; cost: number }>;
milestones: Record<string, boolean>; milestones: Record<string, boolean>;
clickMultiplier: number;
autoClickRate: number;
currentBackground: string; currentBackground: string;
currentClickImage: string; currentClickImage: string;
} }
@@ -34,7 +22,6 @@ export interface Upgrade {
mascotTiers?: MascotTier[]; // New: for defining mascot tiers for friendBoost mascotTiers?: MascotTier[]; // New: for defining mascot tiers for friendBoost
oneTime?: boolean; // New: Indicates if the upgrade is a one-time purchase oneTime?: boolean; // New: Indicates if the upgrade is a one-time purchase
newsTitles?: string[]; // New: Array of news titles for the news upgrade newsTitles?: string[]; // New: Array of news titles for the news upgrade
youtubeId?: string; // New: YouTube ID for the upgrade
} }
export interface MascotTier { export interface MascotTier {

View File

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

View File

@@ -16,8 +16,7 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true
"types": ["node"]
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts"]
} }