Compare commits
1 Commits
main
...
update_wor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5477db8259 |
3
.bolt/config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"template": "bolt-vite-react-ts"
|
||||
}
|
||||
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.
|
||||
69
package-lock.json
generated
@@ -15,7 +15,6 @@
|
||||
"partysocket": "^1.1.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-youtube": "^10.1.0",
|
||||
"wouter": "^3.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -2559,7 +2558,8 @@
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"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": {
|
||||
"version": "3.3.2",
|
||||
@@ -3179,11 +3179,6 @@
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"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": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@@ -3448,6 +3443,7 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -3839,16 +3835,6 @@
|
||||
"integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==",
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -3901,11 +3887,6 @@
|
||||
"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": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||
@@ -3915,22 +3896,6 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -4102,11 +4067,6 @@
|
||||
"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": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -4908,29 +4868,6 @@
|
||||
"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": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"partysocket": "^1.1.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-youtube": "^10.1.0",
|
||||
"wouter": "^3.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
499
party/index.ts
@@ -15,7 +15,6 @@ export interface Upgrade {
|
||||
mascotTiers?: MascotTier[];
|
||||
oneTime?: boolean;
|
||||
newsTitles?: string[];
|
||||
youtubeId?: string;
|
||||
}
|
||||
|
||||
export interface MascotTier {
|
||||
@@ -25,27 +24,13 @@ export interface MascotTier {
|
||||
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 {
|
||||
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>;
|
||||
clickMultiplier: number;
|
||||
autoClickRate: number;
|
||||
currentBackground: string;
|
||||
currentClickImage: string;
|
||||
}
|
||||
@@ -80,48 +65,143 @@ interface UserJoinMessage extends AuthenticatedMessage {
|
||||
type: 'user-join';
|
||||
}
|
||||
|
||||
interface EditUserMessage extends AuthenticatedMessage {
|
||||
type: 'edit-user';
|
||||
targetUserId: string;
|
||||
clicks: number;
|
||||
upgrades: Record<string, { owned: number; cost: number }>;
|
||||
}
|
||||
type Message = ClickMessage | PurchaseUpgradeMessage | ApplyMultiplierBonusMessage | UserJoinMessage | AdminBroadcastMessage; // Updated Message type
|
||||
|
||||
interface ThrowRatPoisonMessage extends AuthenticatedMessage {
|
||||
type: 'throw-rat-poison';
|
||||
targetUserId: string;
|
||||
}
|
||||
|
||||
interface SolveRatPoisonMessage extends AuthenticatedMessage {
|
||||
type: 'solve-rat-poison';
|
||||
challengeString: string;
|
||||
}
|
||||
|
||||
type Message = ClickMessage | PurchaseUpgradeMessage | ApplyMultiplierBonusMessage | UserJoinMessage | AdminBroadcastMessage | EditUserMessage | ThrowRatPoisonMessage | SolveRatPoisonMessage; // Updated Message type
|
||||
|
||||
import { UPGRADES as ALL_UPGRADES } from '../src/config/upgrades';
|
||||
|
||||
const UPGRADES = ALL_UPGRADES.filter(upgrade => !upgrade.oneTime);
|
||||
const ONE_TIME_UPGRADES = ALL_UPGRADES.filter(upgrade => upgrade.oneTime);
|
||||
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: '🌟'
|
||||
},
|
||||
{
|
||||
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 = [
|
||||
{ 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: 50000, id: 'epic-milestone', background: 'space', image: 'https://media.discordapp.net/stickers/1377379131649429554.gif?size=160&quality=lossless' },
|
||||
{ threshold: 100000, id: 'legendary', background: 'musume', image: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif' },
|
||||
{ threshold: 200000, id: 'two-hundred-thousand', 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: 1000000, id: 'god-tier', background: 'god-tier', image: 'https://media.discordapp.net/stickers/1294812453112123453.webp?quality=lossless' },
|
||||
{ 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' }
|
||||
{ threshold: 5000, id: 'five-thousand', background: 'matrix', image: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif' },
|
||||
{ threshold: 10000, id: 'ten-thousand', background: 'cyberpunk', image: 'https://media1.tenor.com/m/YsWlbVbRWFQAAAAd/rat-spinning.gif' },
|
||||
{ threshold: 50000, id: 'epic-milestone', background: 'space', image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif' },
|
||||
{ threshold: 100000, id: 'legendary', background: 'glitch', image: 'https://media1.tenor.com/m/pV74fmh_NLgAAAAd/louie-rat-spinning-rat.gif' },
|
||||
{ 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://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif' }
|
||||
];
|
||||
|
||||
export default class GameServer implements Party.Server {
|
||||
@@ -137,7 +217,13 @@ export default class GameServer implements Party.Server {
|
||||
gameState: GameState = {
|
||||
totalClicks: 0,
|
||||
users: {},
|
||||
upgrades: UPGRADES.reduce((acc, upgrade) => {
|
||||
acc[upgrade.id] = { owned: 0, cost: upgrade.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'
|
||||
};
|
||||
@@ -208,15 +294,7 @@ export default class GameServer implements Party.Server {
|
||||
name: currentUserName,
|
||||
clicks: 0,
|
||||
lastSeen: Date.now(),
|
||||
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,
|
||||
bonusMultiplier: 1 // Initialize bonus multiplier
|
||||
};
|
||||
}
|
||||
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}.`);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
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.
|
||||
// Removing it as its logic is now directly in onMessage.
|
||||
// handleUserJoin(data: UserJoinMessage) {
|
||||
@@ -475,29 +354,24 @@ export default class GameServer implements Party.Server {
|
||||
// }
|
||||
|
||||
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
|
||||
const userBonusMultiplier = userState.bonusMultiplier || 1;
|
||||
const clickValue = userState.clickMultiplier * userBonusMultiplier;
|
||||
const userBonusMultiplier = this.gameState.users[authenticatedUserId]?.bonusMultiplier || 1;
|
||||
const clickValue = this.gameState.clickMultiplier * userBonusMultiplier;
|
||||
|
||||
this.gameState.totalClicks += clickValue;
|
||||
userState.clicks += clickValue;
|
||||
userState.lastSeen = now;
|
||||
userState.lastClickTime = now; // Update last valid click time
|
||||
|
||||
// Ensure user exists (should be handled by onMessage's token verification)
|
||||
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();
|
||||
}
|
||||
@@ -537,11 +411,9 @@ export default class GameServer implements Party.Server {
|
||||
|
||||
handlePurchaseUpgrade(data: PurchaseUpgradeMessage, authenticatedUserId: string) {
|
||||
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;
|
||||
|
||||
const currentUpgradeState = userState.upgrades[data.upgradeId];
|
||||
if (!upgradeConfig || !currentUpgradeState) return;
|
||||
|
||||
// Prevent purchasing one-time upgrades if already owned
|
||||
if (upgradeConfig.oneTime && currentUpgradeState.owned > 0) {
|
||||
@@ -549,9 +421,9 @@ export default class GameServer implements Party.Server {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check affordability against user's clicks
|
||||
if (userState.clicks >= currentUpgradeState.cost) {
|
||||
userState.clicks -= currentUpgradeState.cost; // Deduct from user's clicks
|
||||
// Check affordability against totalClicks
|
||||
if (this.gameState.totalClicks >= currentUpgradeState.cost) {
|
||||
this.gameState.totalClicks -= currentUpgradeState.cost; // Deduct from totalClicks
|
||||
currentUpgradeState.owned += 1;
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
this.updateGameMultipliers(authenticatedUserId);
|
||||
this.updateGameMultipliers();
|
||||
}
|
||||
}
|
||||
|
||||
updateGameMultipliers(userId: string, autoClickRateOverride?: number) {
|
||||
const userState = this.gameState.users[userId];
|
||||
if (!userState) return;
|
||||
updateGameMultipliers() {
|
||||
this.gameState.clickMultiplier = 1;
|
||||
this.gameState.autoClickRate = 0;
|
||||
|
||||
userState.clickMultiplier = 1;
|
||||
userState.autoClickRate = 0;
|
||||
|
||||
Object.entries(userState.upgrades).forEach(([upgradeId, upgradeState]) => {
|
||||
Object.entries(this.gameState.upgrades).forEach(([upgradeId, upgradeState]) => {
|
||||
const config = UPGRADES.find(u => u.id === upgradeId);
|
||||
if (!config) return;
|
||||
if (!config) return; // Should not happen if UPGRADES is consistent
|
||||
|
||||
if (config.clickBonus) {
|
||||
userState.clickMultiplier += config.clickBonus * upgradeState.owned;
|
||||
this.gameState.clickMultiplier += config.clickBonus * upgradeState.owned;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -594,21 +461,13 @@ export default class GameServer implements Party.Server {
|
||||
clearInterval(this.autoClickInterval);
|
||||
}
|
||||
|
||||
this.autoClickInterval = setInterval(() => {
|
||||
let totalAutoClicks = 0;
|
||||
Object.values(this.gameState.users).forEach(user => {
|
||||
if (user.autoClickRate > 0) {
|
||||
user.clicks += user.autoClickRate;
|
||||
totalAutoClicks += user.autoClickRate;
|
||||
}
|
||||
});
|
||||
|
||||
if (totalAutoClicks > 0) {
|
||||
this.gameState.totalClicks += totalAutoClicks;
|
||||
if (this.gameState.autoClickRate > 0) {
|
||||
this.autoClickInterval = setInterval(() => {
|
||||
this.gameState.totalClicks += this.gameState.autoClickRate;
|
||||
this.checkMilestones();
|
||||
this.broadcast();
|
||||
}
|
||||
}, 1000);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
checkMilestones() {
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 MiB |
156
src/App.tsx
@@ -15,26 +15,18 @@ import { UPGRADES } from './config/upgrades'; // Import UPGRADES for upgrade con
|
||||
import { useLocation, Link } from 'wouter'; // Import wouter hooks
|
||||
import AdminPage from './components/AdminPage'; // Import AdminPage
|
||||
import { NewsMarquee } from './components/NewsMarquee'; // Import NewsMarquee
|
||||
import YouTube from 'react-youtube'; // Import YouTube component
|
||||
import { RatPoisonChallenge } from './components/RatPoisonChallenge'; // Import RatPoisonChallenge
|
||||
|
||||
function App() {
|
||||
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 [previousMilestones, setPreviousMilestones] = useState<Record<string, boolean>>({});
|
||||
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 clickButtonContainerRef = useRef<HTMLDivElement>(null); // New ref for the click button container
|
||||
const [location] = useLocation(); // Get current location from wouter
|
||||
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
|
||||
const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID;
|
||||
@@ -61,55 +53,21 @@ function App() {
|
||||
}
|
||||
}, [gameState, previousMilestones]);
|
||||
|
||||
// Effect for secret video
|
||||
useEffect(() => {
|
||||
if ((userState?.upgrades?.['secretVideo']?.owned || 0) > 0) {
|
||||
setShowSecretVideo(true);
|
||||
}
|
||||
}, [userState]);
|
||||
|
||||
// Effect for receiving admin broadcast messages
|
||||
useEffect(() => {
|
||||
if (lastMessage) {
|
||||
console.log('App.tsx: Received lastMessage:', lastMessage); // Log the raw message
|
||||
try {
|
||||
const parsedMessage = JSON.parse(lastMessage);
|
||||
if (parsedMessage.type === 'admin-message') {
|
||||
setAdminBroadcastMessage(parsedMessage.message);
|
||||
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) {
|
||||
console.error('Failed to parse last message in App.tsx:', error);
|
||||
}
|
||||
}
|
||||
}, [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
|
||||
useEffect(() => {
|
||||
gameStateRef.current = gameState;
|
||||
@@ -117,17 +75,18 @@ function App() {
|
||||
|
||||
// Effect for spawning mascots
|
||||
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 ownedFriendBoost = ((userState?.upgrades || {})['friendBoost']?.owned || 0);
|
||||
const ownedFriendBoost = gameStateRef.current.upgrades['friendBoost']?.owned || 0;
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
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 interval = Math.max(minInterval, baseInterval / (1 + ownedFriendBoost * 0.2)); // Adjusted scaling
|
||||
console.log(`Spawning mascots every ${interval} ms for friendBoost level ${ownedFriendBoost}`);
|
||||
@@ -140,13 +99,25 @@ function App() {
|
||||
const currentGameState = gameStateRef.current;
|
||||
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 viewportHeight = window.innerHeight;
|
||||
|
||||
// Spawn anywhere on the viewport, with a small padding to avoid edges
|
||||
const padding = 50;
|
||||
const randomX = Math.random() * (viewportWidth - padding * 2) + padding;
|
||||
const randomY = Math.random() * (viewportHeight - padding * 2) + padding;
|
||||
const randomX = Math.max(0, Math.min(viewportWidth, minX + Math.random() * (maxX - minX)));
|
||||
const randomY = Math.max(0, Math.min(viewportHeight, minY + Math.random() * (maxY - minY)));
|
||||
|
||||
// Filter available mascots based on ownedFriendBoost level
|
||||
const availableMascots = currentMascotTiers.filter(
|
||||
@@ -195,7 +166,7 @@ function App() {
|
||||
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
|
||||
sendMascotClickBonus(multiplierBonus); // Renamed function call
|
||||
@@ -206,15 +177,6 @@ function App() {
|
||||
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) {
|
||||
return (
|
||||
<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 = userState?.bonusMultiplier || 1;
|
||||
const effectiveClickMultiplier = (userState?.clickMultiplier || 1) * userBonusMultiplier;
|
||||
const userBonusMultiplier = isSignedIn && userId ? gameState.users[userId]?.bonusMultiplier || 1 : 1;
|
||||
const effectiveClickMultiplier = gameState.clickMultiplier * userBonusMultiplier;
|
||||
|
||||
// Render the AdminPage if the current location is /admin
|
||||
if (location === '/admin') {
|
||||
@@ -271,50 +231,24 @@ function App() {
|
||||
</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 */}
|
||||
{(() => {
|
||||
const newsUpgrade = UPGRADES.find(u => u.id === 'news');
|
||||
if (
|
||||
userState &&
|
||||
userState.upgrades &&
|
||||
userState.upgrades['news'] &&
|
||||
userState.upgrades['news'].owned > 0 &&
|
||||
newsUpgrade?.newsTitles
|
||||
) {
|
||||
return <NewsMarquee titles={(newsUpgrade as Upgrade).newsTitles!} />;
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
{gameState.upgrades['news']?.owned > 0 && UPGRADES.find(u => u.id === 'news')?.newsTitles && (
|
||||
<NewsMarquee titles={UPGRADES.find(u => u.id === 'news')!.newsTitles!} />
|
||||
)}
|
||||
|
||||
<div className="container mx-auto px-4 py-8 relative z-10">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Counter
|
||||
totalClicks={gameState.totalClicks}
|
||||
autoClickRate={userState?.autoClickRate || 0}
|
||||
autoClickRate={gameState.autoClickRate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<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 */}
|
||||
<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
|
||||
onClick={sendClick}
|
||||
imageUrl={gameState.currentClickImage}
|
||||
@@ -339,7 +273,7 @@ function App() {
|
||||
<div className="lg:col-span-1">
|
||||
<UpgradeShop
|
||||
gameState={gameState}
|
||||
userState={userState}
|
||||
totalClicks={gameState.totalClicks}
|
||||
onPurchase={purchaseUpgrade}
|
||||
/>
|
||||
</div>
|
||||
@@ -364,32 +298,6 @@ function App() {
|
||||
</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 */}
|
||||
<div className="fixed inset-0 pointer-events-none z-20">
|
||||
{[...Array(10)].map((_, i) => (
|
||||
|
||||
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useUser } from '@clerk/clerk-react';
|
||||
import { usePartyKit } from '../hooks/usePartyKit';
|
||||
import { UserState } from '../types';
|
||||
|
||||
interface AdminMessage {
|
||||
type: 'admin-message';
|
||||
@@ -11,15 +10,11 @@ interface AdminMessage {
|
||||
|
||||
const AdminPage: React.FC = () => {
|
||||
const { user } = useUser();
|
||||
const { sendMessage, lastMessage, getToken, gameState, editUser } = usePartyKit();
|
||||
const { sendMessage, lastMessage, getToken } = usePartyKit();
|
||||
const [broadcastMessage, setBroadcastMessage] = useState('');
|
||||
const [targetUserId, setTargetUserId] = useState('');
|
||||
const [receivedMessages, setReceivedMessages] = useState<AdminMessage[]>([]);
|
||||
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
|
||||
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) {
|
||||
return (
|
||||
<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 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>
|
||||
<div className="max-h-64 overflow-y-auto bg-gray-700 p-4 rounded-md">
|
||||
{receivedMessages.length === 0 ? (
|
||||
|
||||
@@ -33,11 +33,11 @@ export function Background({ background }: BackgroundProps) {
|
||||
backgroundSize: '200px 100px',
|
||||
animation: 'stars 50s linear infinite'
|
||||
};
|
||||
case 'musume':
|
||||
case 'glitch':
|
||||
return {
|
||||
background: 'url("/special/umamusume.gif") no-repeat center center fixed',
|
||||
backgroundSize: 'cover',
|
||||
animation: 'none'
|
||||
background: 'linear-gradient(45deg, #ff0000, #00ff00, #0000ff, #ffff00, #ff00ff, #00ffff)',
|
||||
backgroundSize: '400% 400%',
|
||||
animation: 'glitch 2s ease infinite'
|
||||
};
|
||||
case 'ultimate':
|
||||
return {
|
||||
@@ -47,52 +47,10 @@ export function Background({ background }: BackgroundProps) {
|
||||
};
|
||||
case 'god-tier':
|
||||
return {
|
||||
background: 'linear-gradient(135deg, #000000 0%, #ffffff 100%)',
|
||||
backgroundSize: '200% 200%',
|
||||
background: 'url(/src/special/bathtub.gif) no-repeat center center fixed',
|
||||
backgroundSize: 'contain',
|
||||
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:
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { GameState } from '../types';
|
||||
import { usePartyKit } from '../hooks/usePartyKit';
|
||||
|
||||
interface LeaderboardProps {
|
||||
gameState: GameState;
|
||||
@@ -8,7 +7,6 @@ interface LeaderboardProps {
|
||||
}
|
||||
|
||||
export function Leaderboard({ gameState, currentUserId }: LeaderboardProps) {
|
||||
const { throwRatPoison } = usePartyKit();
|
||||
const sortedUsers = Object.entries(gameState.users)
|
||||
.sort(([, a], [, b]) => b.clicks - a.clicks)
|
||||
.slice(0, 10);
|
||||
@@ -23,7 +21,6 @@ export function Leaderboard({ gameState, currentUserId }: LeaderboardProps) {
|
||||
{sortedUsers.map(([userId, user], index) => {
|
||||
const isCurrentUser = userId === currentUserId;
|
||||
const isTopThree = index < 3;
|
||||
const isImmune = user.ratPoisonImmunityUntil && user.ratPoisonImmunityUntil > Date.now();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -45,24 +42,9 @@ export function Leaderboard({ gameState, currentUserId }: LeaderboardProps) {
|
||||
{user.name} {isCurrentUser ? '(You)' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className={`text-xl font-bold ${isCurrentUser ? 'text-black' : 'text-yellow-300'}`}>
|
||||
{user.clicks.toLocaleString()}
|
||||
</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>
|
||||
<span className={`text-xl font-bold ${isCurrentUser ? 'text-black' : 'text-yellow-300'}`}>
|
||||
{user.clicks.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { UPGRADES } from '../config/upgrades';
|
||||
import { GameState, MascotTier, UserState } from '../types'; // Import MascotTier
|
||||
import { GameState, MascotTier } from '../types'; // Import MascotTier
|
||||
|
||||
interface UpgradeShopProps {
|
||||
gameState: GameState;
|
||||
userState: UserState | null;
|
||||
totalClicks: number; // Changed from userClicks
|
||||
onPurchase: (upgradeId: string) => void;
|
||||
}
|
||||
|
||||
@@ -22,13 +22,7 @@ const getMascotName = (imageSrc: string): string => {
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
export function UpgradeShop({ userState, onPurchase }: UpgradeShopProps) { // Changed from userClicks
|
||||
if (!userState) {
|
||||
return null; // Or a loading/signed-out state
|
||||
}
|
||||
|
||||
const { clicks: userClicks, upgrades: userUpgrades } = userState;
|
||||
|
||||
export function UpgradeShop({ gameState, totalClicks, onPurchase }: UpgradeShopProps) { // Changed from userClicks
|
||||
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' }}>
|
||||
@@ -37,9 +31,9 @@ export function UpgradeShop({ userState, onPurchase }: UpgradeShopProps) { // Ch
|
||||
|
||||
<div className="space-y-4">
|
||||
{UPGRADES.map((upgrade) => {
|
||||
const owned = userUpgrades[upgrade.id]?.owned || 0;
|
||||
const cost = userUpgrades[upgrade.id]?.cost || upgrade.baseCost;
|
||||
const canAfford = userClicks >= cost;
|
||||
const owned = gameState.upgrades[upgrade.id]?.owned || 0;
|
||||
const cost = gameState.upgrades[upgrade.id]?.cost || upgrade.baseCost;
|
||||
const canAfford = totalClicks >= cost; // Changed from userClicks
|
||||
|
||||
// If it's a one-time upgrade and already owned, don't display it
|
||||
if (upgrade.oneTime && owned > 0) {
|
||||
@@ -99,7 +93,7 @@ export function UpgradeShop({ userState, onPurchase }: UpgradeShopProps) { // Ch
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<div className="text-2xl font-bold text-yellow-300">
|
||||
Your Clicks: {userClicks.toLocaleString()}
|
||||
Total Clicks: {totalClicks.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,13 +10,22 @@ export const MILESTONES: Milestone[] = [
|
||||
image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif',
|
||||
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,
|
||||
id: 'ten-thousand',
|
||||
name: 'Cyber Rat',
|
||||
description: 'Welcome to the future',
|
||||
background: 'cyberpunk',
|
||||
image: 'https://cdn.discordapp.com/emojis/1297946545626288312.webp',
|
||||
image: 'https://media1.tenor.com/m/YsWlbVbRWFQAAAAd/rat-spinning.gif',
|
||||
reward: '🦾 Cyberpunk Aesthetic Engaged!'
|
||||
},
|
||||
{
|
||||
@@ -25,34 +34,25 @@ export const MILESTONES: Milestone[] = [
|
||||
name: 'Space Cadet',
|
||||
description: 'To infinity and beyond!',
|
||||
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!'
|
||||
},
|
||||
{
|
||||
threshold: 100000,
|
||||
id: 'legendary',
|
||||
name: 'musume mode',
|
||||
description: 'Reality is breaking down, the girls are turning into horses!',
|
||||
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!',
|
||||
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: '✨ New Background Unlocked!'
|
||||
reward: '⚡ Glitch Effect Activated!'
|
||||
},
|
||||
{
|
||||
threshold: 696969,
|
||||
threshold: 500000,
|
||||
id: 'ultimate',
|
||||
name: 'Ultimate Bozo',
|
||||
description: 'You have achieved peak bozo status',
|
||||
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!'
|
||||
},
|
||||
{
|
||||
@@ -61,97 +61,7 @@ export const MILESTONES: Milestone[] = [
|
||||
name: 'God Tier Bozo',
|
||||
description: 'You are the ultimate bozo',
|
||||
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',
|
||||
reward: '💥 Mega Bozo Background Unlocked!'
|
||||
},
|
||||
{
|
||||
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!'
|
||||
reward: '🌟 God Mode Activated!'
|
||||
}
|
||||
];
|
||||
@@ -56,41 +56,41 @@ export const UPGRADES: Upgrade[] = [
|
||||
mascotTiers: [
|
||||
{
|
||||
level: 0,
|
||||
imageSrc: '/assets/bozo.png',
|
||||
multiplier: 1.002,
|
||||
imageSrc: '/src/assets/bozo.png',
|
||||
multiplier: 1.02,
|
||||
rarity: 1.0,
|
||||
},
|
||||
{
|
||||
level: 1,
|
||||
imageSrc: '/assets/shoominion.png',
|
||||
multiplier: 1.003,
|
||||
imageSrc: '/src/assets/shoominion.png',
|
||||
multiplier: 1.03,
|
||||
rarity: 0.8,
|
||||
},
|
||||
{
|
||||
level: 5,
|
||||
imageSrc: '/assets/codebug.gif',
|
||||
multiplier: 1.005,
|
||||
imageSrc: '/src/assets/codebug.gif',
|
||||
multiplier: 1.05,
|
||||
rarity: 0.6,
|
||||
},
|
||||
{
|
||||
level: 10,
|
||||
imageSrc: '/assets/lalan.gif',
|
||||
multiplier: 1.007,
|
||||
imageSrc: '/src/assets/lalan.gif',
|
||||
multiplier: 1.07,
|
||||
rarity: 0.4,
|
||||
},
|
||||
{
|
||||
level: 15,
|
||||
imageSrc: '/assets/neuro-neurosama.gif',
|
||||
multiplier: 1.010,
|
||||
imageSrc: '/src/assets/neuro-neurosama.gif',
|
||||
multiplier: 1.10,
|
||||
rarity: 0.2,
|
||||
},
|
||||
{
|
||||
level: 20,
|
||||
imageSrc: '/assets/evil-neurosama.gif',
|
||||
multiplier: 1.015,
|
||||
imageSrc: '/src/assets/evil-neurosama.gif',
|
||||
multiplier: 1.15,
|
||||
rarity: 0.1,
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'news',
|
||||
@@ -124,15 +124,5 @@ export const UPGRADES: Upgrade[] = [
|
||||
`om`,
|
||||
`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
|
||||
}
|
||||
];
|
||||
|
||||
@@ -3,17 +3,14 @@ import PartySocket from 'partysocket';
|
||||
import { GameState } from '../types';
|
||||
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() {
|
||||
const { getToken, isLoaded, isSignedIn } = useAuth();
|
||||
const { user } = useUser();
|
||||
const [gameState, setGameState] = useState<GameState | null>(null);
|
||||
const [socket, setSocket] = useState<PartySocket | null>(null);
|
||||
const [lastMessage, setLastMessage] = useState<string | null>(null);
|
||||
// ratPoisonChallengeData will now be derived from gameState
|
||||
const [ratPoisonResultData, setRatPoisonResultData] = useState<{ success: boolean; message: string } | null>(null);
|
||||
const [ratPoisonFeedbackData, setRatPoisonFeedbackData] = useState<string | null>(null);
|
||||
const [lastMessage, setLastMessage] = useState<string | null>(null); // New state for last message
|
||||
// Generate a persistent guest ID if the user is not signed in
|
||||
const [guestId] = useState(() => {
|
||||
let storedGuestId = localStorage.getItem('bozo_guest_id');
|
||||
@@ -62,15 +59,8 @@ export function usePartyKit() {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'game-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); // Still store the raw last message for other general purposes if needed
|
||||
setLastMessage(event.data); // Store the raw last message
|
||||
};
|
||||
|
||||
setSocket(ws);
|
||||
@@ -78,7 +68,7 @@ export function usePartyKit() {
|
||||
return () => {
|
||||
ws.close();
|
||||
};
|
||||
}, [isLoaded, isSignedIn, user, getToken, guestId, guestName]);
|
||||
}, [isLoaded, guestId, guestName]); // Removed isSignedIn, user, getToken from dependencies
|
||||
|
||||
const sendClick = useCallback(async () => {
|
||||
if (socket && isSignedIn && user) {
|
||||
@@ -130,52 +120,6 @@ export function usePartyKit() {
|
||||
const currentUserId = isSignedIn && user ? user.id : 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 {
|
||||
gameState,
|
||||
sendClick,
|
||||
@@ -185,13 +129,6 @@ export function usePartyKit() {
|
||||
lastMessage, // Expose lastMessage
|
||||
userId: currentUserId,
|
||||
userName: currentUserName,
|
||||
getToken, // Expose getToken
|
||||
editUser,
|
||||
throwRatPoison,
|
||||
sendSolveRatPoison,
|
||||
ratPoisonResultData,
|
||||
ratPoisonFeedbackData,
|
||||
ratPoisonChallengeData: gameState?.users[currentUserId || '']?.ratPoisonChallenge || null,
|
||||
clearRatPoisonChallenge,
|
||||
getToken // Expose getToken
|
||||
};
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 664 KiB After Width: | Height: | Size: 664 KiB |
21
src/types.ts
@@ -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 {
|
||||
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>;
|
||||
clickMultiplier: number;
|
||||
autoClickRate: number;
|
||||
currentBackground: string;
|
||||
currentClickImage: string;
|
||||
}
|
||||
@@ -34,7 +22,6 @@ export interface Upgrade {
|
||||
mascotTiers?: MascotTier[]; // New: for defining mascot tiers for friendBoost
|
||||
oneTime?: boolean; // New: Indicates if the upgrade is a one-time purchase
|
||||
newsTitles?: string[]; // New: Array of news titles for the news upgrade
|
||||
youtubeId?: string; // New: YouTube ID for the upgrade
|
||||
}
|
||||
|
||||
export interface MascotTier {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["node"]
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"types": ["node"]
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||