From 6136ba8d8d13c7626b549dfcd75f3dd863e22454 Mon Sep 17 00:00:00 2001 From: Arjun S <37960163+arjunindia@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:55:18 +0000 Subject: [PATCH] ratpoison --- party/index.ts | 33 +++++++++++++++++-- src/App.tsx | 71 +++++++++++++++++++++++++--------------- src/hooks/usePartyKit.ts | 23 +++++++++++-- tsconfig.node.json | 3 +- 4 files changed, 99 insertions(+), 31 deletions(-) diff --git a/party/index.ts b/party/index.ts index 3483d18..1ff3e61 100644 --- a/party/index.ts +++ b/party/index.ts @@ -305,9 +305,25 @@ export default class GameServer implements Party.Server { 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 @@ -318,6 +334,7 @@ export default class GameServer implements Party.Server { 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; } @@ -325,7 +342,7 @@ export default class GameServer implements Party.Server { 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.'); + console.warn('Rat Poison: No news titles found for rat poison challenge.'); return; } @@ -337,6 +354,9 @@ export default class GameServer implements Party.Server { 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); @@ -352,6 +372,7 @@ export default class GameServer implements Party.Server { 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(); } @@ -363,12 +384,14 @@ export default class GameServer implements Party.Server { 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 @@ -381,8 +404,10 @@ export default class GameServer implements Party.Server { 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) { @@ -400,11 +425,15 @@ export default class GameServer implements Party.Server { applyRatPoisonPenalty(userId: string) { const userState = this.gameState.users[userId]; - if (!userState) return; + 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) { diff --git a/src/App.tsx b/src/App.tsx index 90e1f7f..ea3e95a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,7 +20,7 @@ import { RatPoisonChallenge } from './components/RatPoisonChallenge'; // Import function App() { const { isSignedIn, isLoaded, userId: clerkUserId } = useAuth(); // Get clerkUserId from useAuth - const { gameState, sendClick, purchaseUpgrade, userId, sendMascotClickBonus, lastMessage, sendSolveRatPoison } = usePartyKit(); // Renamed sendShoominionClickBonus + const { gameState, sendClick, purchaseUpgrade, userId, sendMascotClickBonus, lastMessage, sendSolveRatPoison, ratPoisonChallengeData, ratPoisonResultData, ratPoisonFeedbackData, clearRatPoisonChallenge } = usePartyKit(); // Renamed sendShoominionClickBonus const [celebrationMessage, setCelebrationMessage] = useState(null); const [previousMilestones, setPreviousMilestones] = useState>({}); const [mascotEntities, setMascotEntities] = useState([]); @@ -30,10 +30,11 @@ function App() { 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); + // Removed local ratPoisonChallenge and ratPoisonResult states, now directly from usePartyKit - const userState = isSignedIn && userId ? gameState?.users[userId] : null; + 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; @@ -62,8 +63,7 @@ function App() { // Effect for secret video useEffect(() => { - if (!userState) return; // Ensure userState is not null/undefined - if (userState.upgrades?.['secretVideo']?.owned > 0) { + if ((userState?.upgrades?.['secretVideo']?.owned || 0) > 0) { setShowSecretVideo(true); } }, [userState]); @@ -77,24 +77,39 @@ function App() { 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); } + // 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; @@ -105,7 +120,7 @@ function App() { if (!userState) return; const friendBoostUpgrade = UPGRADES.find(u => u.id === 'friendBoost') as Upgrade | undefined; - const ownedFriendBoost = userState.upgrades['friendBoost']?.owned || 0; + const ownedFriendBoost = (userState?.upgrades?.['friendBoost']?.owned || 0); if (timeoutRef.current) { clearTimeout(timeoutRef.current); @@ -193,7 +208,11 @@ function App() { const handleSolveRatPoison = (solvedString: string) => { sendSolveRatPoison(solvedString); - setRatPoisonChallenge(null); + // 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) { @@ -253,10 +272,10 @@ function App() { )} {/* Rat Poison Result Message */} - {ratPoisonResult && ( + {ratPoisonResultData && (
-
- {ratPoisonResult.message} +
+ {ratPoisonResultData.message}
)} @@ -349,12 +368,12 @@ function App() { )} {/* Rat Poison Challenge Modal */} - {ratPoisonChallenge && ( + {ratPoisonChallengeData && ( setRatPoisonChallenge(null)} + onClose={clearRatPoisonChallenge} // Call the clear function from usePartyKit /> )} diff --git a/src/hooks/usePartyKit.ts b/src/hooks/usePartyKit.ts index 6ca7e23..ccf1878 100644 --- a/src/hooks/usePartyKit.ts +++ b/src/hooks/usePartyKit.ts @@ -10,7 +10,10 @@ export function usePartyKit() { const { user } = useUser(); const [gameState, setGameState] = useState(null); const [socket, setSocket] = useState(null); - const [lastMessage, setLastMessage] = useState(null); // New state for last message + const [lastMessage, setLastMessage] = useState(null); + // ratPoisonChallengeData will now be derived from gameState + const [ratPoisonResultData, setRatPoisonResultData] = useState<{ success: boolean; message: string } | null>(null); + const [ratPoisonFeedbackData, setRatPoisonFeedbackData] = useState(null); // Generate a persistent guest ID if the user is not signed in const [guestId] = useState(() => { let storedGuestId = localStorage.getItem('bozo_guest_id'); @@ -59,8 +62,15 @@ 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); } - setLastMessage(event.data); // Store the raw last 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 }; setSocket(ws); @@ -161,6 +171,11 @@ export function usePartyKit() { } }, [socket, isSignedIn, user, getToken]); + const clearRatPoisonChallenge = useCallback(() => { + setRatPoisonResultData(null); + setRatPoisonFeedbackData(null); + }, []); + return { gameState, sendClick, @@ -174,5 +189,9 @@ export function usePartyKit() { editUser, throwRatPoison, sendSolveRatPoison, + ratPoisonResultData, + ratPoisonFeedbackData, + ratPoisonChallengeData: gameState?.users[currentUserId || '']?.ratPoisonChallenge || null, + clearRatPoisonChallenge, }; } diff --git a/tsconfig.node.json b/tsconfig.node.json index 0d3d714..3b45f56 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -16,7 +16,8 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "types": ["node"] }, "include": ["vite.config.ts"] }