From 0e1eb369d50353a795cb6fb0475803984d471f1e Mon Sep 17 00:00:00 2001 From: Arjun S <37960163+arjunindia@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:34:04 +0000 Subject: [PATCH] rat poiuson --- party/index.ts | 301 ++++++++++++++------------ src/App.tsx | 60 ++++- src/components/AdminPage.tsx | 11 +- src/components/Leaderboard.tsx | 24 +- src/components/RatPoisonChallenge.tsx | 87 ++++++++ src/components/UpgradeShop.tsx | 2 +- src/hooks/usePartyKit.ts | 33 ++- src/types.ts | 5 + tsconfig.json | 3 + 9 files changed, 365 insertions(+), 161 deletions(-) create mode 100644 src/components/RatPoisonChallenge.tsx diff --git a/party/index.ts b/party/index.ts index 45d3c13..3483d18 100644 --- a/party/index.ts +++ b/party/index.ts @@ -25,6 +25,7 @@ export interface MascotTier { rarity: number; } + interface UserState { name: string; clicks: number; @@ -34,6 +35,11 @@ interface UserState { upgrades: Record; clickMultiplier: number; autoClickRate: number; + ratPoisonChallenge?: { + challengeString: string; + expiresAt: number; + }; + ratPoisonImmunityUntil?: number; } interface GameState { @@ -81,144 +87,22 @@ interface EditUserMessage extends AuthenticatedMessage { upgrades: Record; } -type Message = ClickMessage | PurchaseUpgradeMessage | ApplyMultiplierBonusMessage | UserJoinMessage | AdminBroadcastMessage | EditUserMessage; // Updated Message type +interface ThrowRatPoisonMessage extends AuthenticatedMessage { + type: 'throw-rat-poison'; + targetUserId: string; +} -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: '/assets/bozo.png', - multiplier: 1.002, - rarity: 1.0, - }, - { - level: 1, - imageSrc: '/assets/shoominion.png', - multiplier: 1.003, - rarity: 0.8, - }, - { - level: 5, - imageSrc: '/assets/codebug.gif', - multiplier: 1.005, - rarity: 0.6, - }, - { - level: 10, - imageSrc: '/assets/lalan.gif', - multiplier: 1.007, - rarity: 0.4, - }, - { - level: 15, - imageSrc: '/assets/neuro-neurosama.gif', - multiplier: 1.010, - rarity: 0.2, - }, - { - level: 20, - imageSrc: '/assets/evil-neurosama.gif', - multiplier: 1.015, - 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`, - ] - }, - { - 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 - } -]; +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 MILESTONES = [ { threshold: 1000, id: 'first-thousand', background: 'rainbow', image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif' }, @@ -325,12 +209,14 @@ export default class GameServer implements Party.Server { clicks: 0, lastSeen: Date.now(), bonusMultiplier: 1, // Initialize bonus multiplier - upgrades: UPGRADES.reduce((acc, upgrade) => { + upgrades: ALL_UPGRADES.reduce((acc, upgrade) => { acc[upgrade.id] = { owned: 0, cost: upgrade.baseCost }; return acc; }, {} as Record), clickMultiplier: 1, autoClickRate: 0, + ratPoisonChallenge: undefined, + ratPoisonImmunityUntil: undefined, }; } this.gameState.users[currentUserId].lastSeen = Date.now(); @@ -383,6 +269,20 @@ export default class GameServer implements Party.Server { 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(); @@ -399,6 +299,127 @@ export default class GameServer implements Party.Server { } } + 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) { + return; // Cannot poison self or non-existent user + } + + 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!` + })); + } + 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('No news titles found for rat poison challenge.'); + return; + } + + const challengeString = newsTitles[Math.floor(Math.random() * newsTitles.length)]; + const challengeDuration = 10 * 1000; // 10 seconds to solve + const expiresAt = now + challengeDuration; + + targetUserState.ratPoisonChallenge = { + challengeString, + expiresAt, + }; + + // 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 + 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) { + return; // No active challenge + } + + const now = Date.now(); + if (now > userState.ratPoisonChallenge.expiresAt) { + // Challenge expired + 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.' + })); + } + } else { + // Challenge failed (wrong 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) return; + + const clicksLost = Math.floor(userState.clicks * 0.30); + userState.clicks -= clicksLost; + userState.ratPoisonImmunityUntil = Date.now() + (20 * 1000); // 20 seconds immunity + + 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) { @@ -407,10 +428,6 @@ export default class GameServer implements Party.Server { // // No explicit action needed here beyond what onMessage does. // } - // 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 - handleClick(data: ClickMessage, authenticatedUserId: string) { const now = Date.now(); const userState = this.gameState.users[authenticatedUserId]; diff --git a/src/App.tsx b/src/App.tsx index 4ed6c69..90e1f7f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,21 +16,24 @@ 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 } = usePartyKit(); // Renamed sendShoominionClickBonus + const { gameState, sendClick, purchaseUpgrade, userId, sendMascotClickBonus, lastMessage, sendSolveRatPoison } = usePartyKit(); // Renamed sendShoominionClickBonus const [celebrationMessage, setCelebrationMessage] = useState(null); const [previousMilestones, setPreviousMilestones] = useState>({}); const [mascotEntities, setMascotEntities] = useState([]); - const timeoutRef = useRef(null); + const timeoutRef = useRef(null); const gameStateRef = useRef(gameState); // Ref to hold the latest gameState - const [location, setLocation] = useLocation(); // Get current location from wouter + const [location] = useLocation(); // Get current location from wouter const [adminBroadcastMessage, setAdminBroadcastMessage] = useState(null); // New state for admin messages const [showSecretVideo, setShowSecretVideo] = useState(false); // New state for secret video + const [ratPoisonChallenge, setRatPoisonChallenge] = useState<{ challenge: string; expiresAt: number } | null>(null); + const [ratPoisonResult, setRatPoisonResult] = useState<{ success: boolean; message: string } | null>(null); - const userState = isSignedIn && userId ? gameState.users[userId] : null; + const userState = isSignedIn && userId ? gameState?.users[userId] : null; // Admin user ID from .env const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID; @@ -59,7 +62,8 @@ function App() { // Effect for secret video useEffect(() => { - if (userState?.upgrades?.['secretVideo']?.owned > 0) { + if (!userState) return; // Ensure userState is not null/undefined + if (userState.upgrades?.['secretVideo']?.owned > 0) { setShowSecretVideo(true); } }, [userState]); @@ -67,11 +71,23 @@ function App() { // 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 + } else if (parsedMessage.type === 'rat-poison-challenge') { + console.log('App.tsx: Received rat-poison-challenge:', parsedMessage); // Log challenge message + setRatPoisonChallenge({ challenge: parsedMessage.challenge, expiresAt: parsedMessage.expiresAt }); + console.log('App.tsx: ratPoisonChallenge state set to:', { challenge: parsedMessage.challenge, expiresAt: parsedMessage.expiresAt }); // Log state after setting + } else if (parsedMessage.type === 'rat-poison-result') { + setRatPoisonResult({ success: parsedMessage.success, message: parsedMessage.message }); + setTimeout(() => setRatPoisonResult(null), 5000); // Result message disappears after 5 seconds + } else if (parsedMessage.type === 'rat-poison-feedback') { + // Display feedback to the attacker + setAdminBroadcastMessage(parsedMessage.message); // Re-using admin message display for simplicity + setTimeout(() => setAdminBroadcastMessage(null), 5000); } } catch (error) { console.error('Failed to parse last message in App.tsx:', error); @@ -175,6 +191,11 @@ function App() { setMascotEntities((prev) => prev.filter((m) => m.id !== id)); // Update state variable }; + const handleSolveRatPoison = (solvedString: string) => { + sendSolveRatPoison(solvedString); + setRatPoisonChallenge(null); + }; + if (!isLoaded || !gameState) { return (
@@ -231,11 +252,24 @@ function App() {
)} - {/* News Marquee */} - {userState?.upgrades['news']?.owned > 0 && UPGRADES.find(u => u.id === 'news')?.newsTitles && ( - u.id === 'news')!.newsTitles!} /> + {/* Rat Poison Result Message */} + {ratPoisonResult && ( +
+
+ {ratPoisonResult.message} +
+
)} + {/* News Marquee */} + {(() => { + const newsUpgrade = UPGRADES.find(u => u.id === 'news'); + if (userState?.upgrades['news']?.owned > 0 && newsUpgrade?.newsTitles) { + return ; + } + return null; + })()} +
{/* Header */}
@@ -314,6 +348,16 @@ function App() {
)} + {/* Rat Poison Challenge Modal */} + {ratPoisonChallenge && ( + setRatPoisonChallenge(null)} + /> + )} + {/* Sparkle Effects */}
{[...Array(10)].map((_, i) => ( diff --git a/src/components/AdminPage.tsx b/src/components/AdminPage.tsx index 7269c57..e26dac0 100644 --- a/src/components/AdminPage.tsx +++ b/src/components/AdminPage.tsx @@ -69,7 +69,7 @@ const AdminPage: React.FC = () => { const handleEditUser = () => { if (selectedUser) { - editUser(selectedUser.id, clicks, autoClickRate, upgrades); + editUser(selectedUser.id, clicks, upgrades); // Removed autoClickRate } }; @@ -134,9 +134,12 @@ const AdminPage: React.FC = () => { key={id} className={`cursor-pointer p-3 rounded-md mb-2 ${selectedUser?.id === id ? 'bg-blue-500' : 'bg-gray-600'}`} onClick={() => { - setSelectedUser({ id, ...user }); - setClicks(user.clicks); - setUpgrades(user.upgrades); + 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} diff --git a/src/components/Leaderboard.tsx b/src/components/Leaderboard.tsx index 469b2aa..4a217f8 100644 --- a/src/components/Leaderboard.tsx +++ b/src/components/Leaderboard.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { GameState } from '../types'; +import { usePartyKit } from '../hooks/usePartyKit'; interface LeaderboardProps { gameState: GameState; @@ -7,6 +8,7 @@ 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); @@ -21,6 +23,7 @@ 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 (
- - {user.clicks.toLocaleString()} - +
+ + {user.clicks.toLocaleString()} + + {!isCurrentUser && ( + + )} +
); diff --git a/src/components/RatPoisonChallenge.tsx b/src/components/RatPoisonChallenge.tsx new file mode 100644 index 0000000..e732e87 --- /dev/null +++ b/src/components/RatPoisonChallenge.tsx @@ -0,0 +1,87 @@ +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(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 ( +
+
+

RAT POISON CHALLENGE!

+

Type the following string:

+

{challengeString}

+ +
+
+
+ +
+ 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 + /> + +
+ {timeLeft === 0 &&

Time's up!

} +
+
+ ); +} diff --git a/src/components/UpgradeShop.tsx b/src/components/UpgradeShop.tsx index e684dc4..7e0b3d9 100644 --- a/src/components/UpgradeShop.tsx +++ b/src/components/UpgradeShop.tsx @@ -22,7 +22,7 @@ const getMascotName = (imageSrc: string): string => { .join(' '); }; -export function UpgradeShop({ gameState, userState, onPurchase }: UpgradeShopProps) { // Changed from userClicks +export function UpgradeShop({ userState, onPurchase }: UpgradeShopProps) { // Changed from userClicks if (!userState) { return null; // Or a loading/signed-out state } diff --git a/src/hooks/usePartyKit.ts b/src/hooks/usePartyKit.ts index 0c85296..6ca7e23 100644 --- a/src/hooks/usePartyKit.ts +++ b/src/hooks/usePartyKit.ts @@ -68,7 +68,7 @@ export function usePartyKit() { return () => { ws.close(); }; - }, [isLoaded, guestId, guestName]); // Removed isSignedIn, user, getToken from dependencies + }, [isLoaded, isSignedIn, user, getToken, guestId, guestName]); const sendClick = useCallback(async () => { if (socket && isSignedIn && user) { @@ -120,7 +120,7 @@ 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, autoClickRate: number, upgrades: any) => { + const editUser = useCallback(async (targetUserId: string, clicks: number, upgrades: Record) => { if (socket && isSignedIn && user) { const token = await getToken(); socket.send(JSON.stringify({ @@ -130,12 +130,37 @@ export function usePartyKit() { userName: user.username || user.fullName || user.emailAddresses[0].emailAddress, targetUserId, clicks, - autoClickRate, 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]); + return { gameState, sendClick, @@ -147,5 +172,7 @@ export function usePartyKit() { userName: currentUserName, getToken, // Expose getToken editUser, + throwRatPoison, + sendSolveRatPoison, }; } diff --git a/src/types.ts b/src/types.ts index bac136e..6ebefa3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,6 +6,11 @@ export interface UserState { upgrades: Record; clickMultiplier: number; autoClickRate: number; + ratPoisonChallenge?: { + challengeString: string; + expiresAt: number; + }; + ratPoisonImmunityUntil?: number; } export interface GameState { diff --git a/tsconfig.json b/tsconfig.json index 1ffef60..2e8cf21 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,7 @@ { + "compilerOptions": { + "types": ["node"] + }, "files": [], "references": [ { "path": "./tsconfig.app.json" },