ratpoison

This commit is contained in:
Arjun S
2025-08-04 11:55:18 +00:00
parent 0e1eb369d5
commit 6136ba8d8d
4 changed files with 99 additions and 31 deletions

View File

@@ -305,9 +305,25 @@ export default class GameServer implements Party.Server {
const attackerUserState = this.gameState.users[authenticatedUserId]; const attackerUserState = this.gameState.users[authenticatedUserId];
if (!targetUserState || !attackerUserState || targetUserId === 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 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(); const now = Date.now();
if (targetUserState.ratPoisonImmunityUntil && targetUserState.ratPoisonImmunityUntil > now) { if (targetUserState.ratPoisonImmunityUntil && targetUserState.ratPoisonImmunityUntil > now) {
// Target is immune, notify attacker // 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!` 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; return;
} }
@@ -325,7 +342,7 @@ export default class GameServer implements Party.Server {
const newsUpgrade = ALL_UPGRADES.find(u => u.id === 'news'); const newsUpgrade = ALL_UPGRADES.find(u => u.id === 'news');
const newsTitles = newsUpgrade?.newsTitles || []; const newsTitles = newsUpgrade?.newsTitles || [];
if (newsTitles.length === 0) { 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; return;
} }
@@ -337,6 +354,9 @@ export default class GameServer implements Party.Server {
challengeString, challengeString,
expiresAt, 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 // Notify target user to start challenge
const targetConn = this.userConnections.get(targetUserId); const targetConn = this.userConnections.get(targetUserId);
@@ -352,6 +372,7 @@ export default class GameServer implements Party.Server {
setTimeout(() => { setTimeout(() => {
if (targetUserState.ratPoisonChallenge && targetUserState.ratPoisonChallenge.expiresAt === expiresAt) { if (targetUserState.ratPoisonChallenge && targetUserState.ratPoisonChallenge.expiresAt === expiresAt) {
// Challenge not solved in time // Challenge not solved in time
console.log(`Rat Poison: Challenge for ${targetUserState.name} (${targetUserId}) expired.`);
this.applyRatPoisonPenalty(targetUserId); this.applyRatPoisonPenalty(targetUserId);
this.broadcast(); this.broadcast();
} }
@@ -363,12 +384,14 @@ export default class GameServer implements Party.Server {
handleSolveRatPoison(data: SolveRatPoisonMessage, authenticatedUserId: string) { handleSolveRatPoison(data: SolveRatPoisonMessage, authenticatedUserId: string) {
const userState = this.gameState.users[authenticatedUserId]; const userState = this.gameState.users[authenticatedUserId];
if (!userState || !userState.ratPoisonChallenge) { if (!userState || !userState.ratPoisonChallenge) {
console.log(`Rat Poison: No active challenge for user ${authenticatedUserId}. Aborting solve attempt.`);
return; // No active challenge return; // No active challenge
} }
const now = Date.now(); const now = Date.now();
if (now > userState.ratPoisonChallenge.expiresAt) { if (now > userState.ratPoisonChallenge.expiresAt) {
// Challenge expired // Challenge expired
console.log(`Rat Poison: User ${authenticatedUserId} attempted to solve expired challenge.`);
this.applyRatPoisonPenalty(authenticatedUserId); this.applyRatPoisonPenalty(authenticatedUserId);
} else if (data.challengeString === userState.ratPoisonChallenge.challengeString) { } else if (data.challengeString === userState.ratPoisonChallenge.challengeString) {
// Challenge solved successfully // Challenge solved successfully
@@ -381,8 +404,10 @@ export default class GameServer implements Party.Server {
message: 'Challenge solved! You are immune for 2 minutes.' 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 { } else {
// Challenge failed (wrong string) // Challenge failed (wrong string)
console.log(`Rat Poison: User ${authenticatedUserId} failed challenge (incorrect string).`);
this.applyRatPoisonPenalty(authenticatedUserId); this.applyRatPoisonPenalty(authenticatedUserId);
const userConn = this.userConnections.get(authenticatedUserId); const userConn = this.userConnections.get(authenticatedUserId);
if (userConn) { if (userConn) {
@@ -400,11 +425,15 @@ export default class GameServer implements Party.Server {
applyRatPoisonPenalty(userId: string) { applyRatPoisonPenalty(userId: string) {
const userState = this.gameState.users[userId]; 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); const clicksLost = Math.floor(userState.clicks * 0.30);
userState.clicks -= clicksLost; userState.clicks -= clicksLost;
userState.ratPoisonImmunityUntil = Date.now() + (20 * 1000); // 20 seconds immunity 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); const userConn = this.userConnections.get(userId);
if (userConn) { if (userConn) {

View File

@@ -20,7 +20,7 @@ import { RatPoisonChallenge } from './components/RatPoisonChallenge'; // Import
function App() { function App() {
const { isSignedIn, isLoaded, userId: clerkUserId } = useAuth(); // Get clerkUserId from useAuth const { isSignedIn, isLoaded, userId: clerkUserId } = useAuth(); // Get clerkUserId from useAuth
const { gameState, sendClick, purchaseUpgrade, userId, sendMascotClickBonus, lastMessage, sendSolveRatPoison } = usePartyKit(); // Renamed sendShoominionClickBonus const { gameState, sendClick, purchaseUpgrade, userId, sendMascotClickBonus, lastMessage, sendSolveRatPoison, ratPoisonChallengeData, ratPoisonResultData, ratPoisonFeedbackData, clearRatPoisonChallenge } = usePartyKit(); // Renamed sendShoominionClickBonus
const [celebrationMessage, setCelebrationMessage] = useState<string | null>(null); const [celebrationMessage, setCelebrationMessage] = useState<string | null>(null);
const [previousMilestones, setPreviousMilestones] = useState<Record<string, boolean>>({}); const [previousMilestones, setPreviousMilestones] = useState<Record<string, boolean>>({});
const [mascotEntities, setMascotEntities] = useState<ClickableMascotType[]>([]); const [mascotEntities, setMascotEntities] = useState<ClickableMascotType[]>([]);
@@ -30,10 +30,11 @@ function App() {
const [location] = useLocation(); // Get current location from wouter const [location] = useLocation(); // Get current location from wouter
const [adminBroadcastMessage, setAdminBroadcastMessage] = useState<string | null>(null); // New state for admin messages const [adminBroadcastMessage, setAdminBroadcastMessage] = useState<string | null>(null); // New state for admin messages
const [showSecretVideo, setShowSecretVideo] = useState(false); // New state for secret video const [showSecretVideo, setShowSecretVideo] = useState(false); // New state for secret video
const [ratPoisonChallenge, setRatPoisonChallenge] = useState<{ challenge: string; expiresAt: number } | null>(null); // Removed local ratPoisonChallenge and ratPoisonResult states, now directly from usePartyKit
const [ratPoisonResult, setRatPoisonResult] = useState<{ success: boolean; message: string } | null>(null);
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 // Admin user ID from .env
const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID; const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID;
@@ -62,8 +63,7 @@ function App() {
// Effect for secret video // Effect for secret video
useEffect(() => { useEffect(() => {
if (!userState) return; // Ensure userState is not null/undefined if ((userState?.upgrades?.['secretVideo']?.owned || 0) > 0) {
if (userState.upgrades?.['secretVideo']?.owned > 0) {
setShowSecretVideo(true); setShowSecretVideo(true);
} }
}, [userState]); }, [userState]);
@@ -77,24 +77,39 @@ function App() {
if (parsedMessage.type === 'admin-message') { if (parsedMessage.type === 'admin-message') {
setAdminBroadcastMessage(parsedMessage.message); setAdminBroadcastMessage(parsedMessage.message);
setTimeout(() => setAdminBroadcastMessage(null), 7000); // Message disappears after 7 seconds setTimeout(() => setAdminBroadcastMessage(null), 7000); // Message disappears after 7 seconds
} 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) { } catch (error) {
console.error('Failed to parse last message in App.tsx:', error); console.error('Failed to parse last message in App.tsx:', error);
} }
} }
}, [lastMessage]); }, [lastMessage]);
// Effect for displaying rat poison result
useEffect(() => {
if (ratPoisonResultData) {
console.log('App.tsx: ratPoisonResultData updated:', ratPoisonResultData);
// The modal will be rendered directly by ratPoisonChallengeData
// This effect is just for the temporary result message
const timeout = setTimeout(() => {
// Clear the result data after a delay
// This requires a function in usePartyKit to clear it, or pass a prop to RatPoisonChallenge
// For now, we'll just let the message disappear.
}, 5000);
return () => clearTimeout(timeout);
}
}, [ratPoisonResultData]);
// Effect for displaying rat poison feedback
useEffect(() => {
if (ratPoisonFeedbackData) {
console.log('App.tsx: ratPoisonFeedbackData updated:', ratPoisonFeedbackData);
setAdminBroadcastMessage(ratPoisonFeedbackData); // Re-using admin message display for simplicity
const timeout = setTimeout(() => setAdminBroadcastMessage(null), 5000);
return () => clearTimeout(timeout);
}
}, [ratPoisonFeedbackData]);
// Effect to keep gameStateRef updated // Effect to keep gameStateRef updated
useEffect(() => { useEffect(() => {
gameStateRef.current = gameState; gameStateRef.current = gameState;
@@ -105,7 +120,7 @@ function App() {
if (!userState) return; if (!userState) return;
const friendBoostUpgrade = UPGRADES.find(u => u.id === 'friendBoost') as Upgrade | undefined; const friendBoostUpgrade = UPGRADES.find(u => u.id === 'friendBoost') as Upgrade | undefined;
const ownedFriendBoost = userState.upgrades['friendBoost']?.owned || 0; const ownedFriendBoost = (userState?.upgrades?.['friendBoost']?.owned || 0);
if (timeoutRef.current) { if (timeoutRef.current) {
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
@@ -193,7 +208,11 @@ function App() {
const handleSolveRatPoison = (solvedString: string) => { const handleSolveRatPoison = (solvedString: string) => {
sendSolveRatPoison(solvedString); 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) { if (!isLoaded || !gameState) {
@@ -253,10 +272,10 @@ function App() {
)} )}
{/* Rat Poison Result Message */} {/* Rat Poison Result Message */}
{ratPoisonResult && ( {ratPoisonResultData && (
<div className="fixed top-40 left-0 right-0 z-50 flex justify-center pt-8"> <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 ${ratPoisonResult.success ? 'bg-green-600' : 'bg-red-600'}`}> <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'}`}>
{ratPoisonResult.message} {ratPoisonResultData.message}
</div> </div>
</div> </div>
)} )}
@@ -349,12 +368,12 @@ function App() {
)} )}
{/* Rat Poison Challenge Modal */} {/* Rat Poison Challenge Modal */}
{ratPoisonChallenge && ( {ratPoisonChallengeData && (
<RatPoisonChallenge <RatPoisonChallenge
challengeString={ratPoisonChallenge.challenge} challengeString={ratPoisonChallengeData.challengeString}
expiresAt={ratPoisonChallenge.expiresAt} expiresAt={ratPoisonChallengeData.expiresAt}
onSolve={handleSolveRatPoison} onSolve={handleSolveRatPoison}
onClose={() => setRatPoisonChallenge(null)} onClose={clearRatPoisonChallenge} // Call the clear function from usePartyKit
/> />
)} )}

View File

@@ -10,7 +10,10 @@ export function usePartyKit() {
const { user } = useUser(); const { user } = useUser();
const [gameState, setGameState] = useState<GameState | null>(null); const [gameState, setGameState] = useState<GameState | null>(null);
const [socket, setSocket] = useState<PartySocket | null>(null); const [socket, setSocket] = useState<PartySocket | null>(null);
const [lastMessage, setLastMessage] = useState<string | null>(null); // New state for last message 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);
// Generate a persistent guest ID if the user is not signed in // Generate a persistent guest ID if the user is not signed in
const [guestId] = useState(() => { const [guestId] = useState(() => {
let storedGuestId = localStorage.getItem('bozo_guest_id'); let storedGuestId = localStorage.getItem('bozo_guest_id');
@@ -59,8 +62,15 @@ export function usePartyKit() {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.type === 'game-state') { if (data.type === 'game-state') {
setGameState(data.state); setGameState(data.state);
} else if (data.type === 'rat-poison-result') {
console.log('usePartyKit: Setting ratPoisonResultData:', { success: data.success, message: data.message });
setRatPoisonResultData({ success: data.success, message: data.message });
} else if (data.type === 'rat-poison-feedback') {
console.log('usePartyKit: Setting ratPoisonFeedbackData:', data.message);
setRatPoisonFeedbackData(data.message);
} }
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); setSocket(ws);
@@ -161,6 +171,11 @@ export function usePartyKit() {
} }
}, [socket, isSignedIn, user, getToken]); }, [socket, isSignedIn, user, getToken]);
const clearRatPoisonChallenge = useCallback(() => {
setRatPoisonResultData(null);
setRatPoisonFeedbackData(null);
}, []);
return { return {
gameState, gameState,
sendClick, sendClick,
@@ -174,5 +189,9 @@ export function usePartyKit() {
editUser, editUser,
throwRatPoison, throwRatPoison,
sendSolveRatPoison, sendSolveRatPoison,
ratPoisonResultData,
ratPoisonFeedbackData,
ratPoisonChallengeData: gameState?.users[currentUserId || '']?.ratPoisonChallenge || null,
clearRatPoisonChallenge,
}; };
} }

View File

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