diff --git a/party/index.ts b/party/index.ts index 60cc4ef..19bd723 100644 --- a/party/index.ts +++ b/party/index.ts @@ -89,74 +89,102 @@ export default class GameServer implements Party.Server { async onMessage(message: string, sender: Party.Connection) { const data = JSON.parse(message) as Message; + let currentUserId: string; + let currentUserName: string; + let isAuthenticated = false; + // Verify the Clerk token for authenticated messages if ('token' in data && data.token) { try { const sessionClaims = await verifyToken(data.token, { jwtKey: this.party.env.CLERK_JWT_KEY as string, }); - const authenticatedUserId = sessionClaims.sub; + currentUserId = sessionClaims.sub; + isAuthenticated = true; // Ensure the userId from the client matches the authenticated userId - if (authenticatedUserId !== data.userId) { - console.warn(`User ID mismatch: Client sent ${data.userId}, but token is for ${authenticatedUserId}`); + if (currentUserId !== data.userId) { + console.warn(`User ID mismatch: Client sent ${data.userId}, but token is for ${currentUserId}`); sender.close(); // Close connection for potential tampering return; } - - // Update user info on the server with authenticated data - if (!this.gameState.users[authenticatedUserId]) { - this.gameState.users[authenticatedUserId] = { - name: data.userName, // Use the name provided by the client, which comes from Clerk - clicks: 0, - lastSeen: Date.now(), - bonusMultiplier: 1 // Initialize bonus multiplier - }; - } - this.gameState.users[authenticatedUserId].lastSeen = Date.now(); - this.gameState.users[authenticatedUserId].name = data.userName; // Update name in case it changed - // Ensure bonusMultiplier is initialized if it somehow wasn't - if (this.gameState.users[authenticatedUserId].bonusMultiplier === undefined) { - this.gameState.users[authenticatedUserId].bonusMultiplier = 1; - } - - switch (data.type) { - case 'user-join': - // User join is handled by the token verification and user state update above - break; - case 'click': - this.handleClick(data, authenticatedUserId); - break; - case 'purchase-upgrade': - this.handlePurchaseUpgrade(data, authenticatedUserId); - break; - case 'apply-multiplier-bonus': // Handle new message type - this.handleApplyMultiplierBonus(data, authenticatedUserId); - break; - } + currentUserName = data.userName; // Use name from client, which comes from Clerk } catch (error) { console.error('Clerk token verification failed:', error); sender.close(); // Close connection if token is invalid return; } } else { - // For messages without a token (e.g., initial connection before token is sent, or unauthenticated actions) - // For this game, we only allow authenticated actions. - console.warn('Received unauthenticated message, closing connection.'); - sender.close(); - return; + // Handle unauthenticated messages (guest users) + currentUserId = data.userId; + currentUserName = data.userName; + isAuthenticated = false; + + // Only allow 'user-join' for unauthenticated users. + // Other actions like 'click', 'purchase-upgrade', 'apply-multiplier-bonus' are ignored. + if (data.type !== 'user-join') { + console.warn(`Received unauthenticated message of type '${data.type}' from ${currentUserId}. Action ignored.`); + this.broadcast(); // Still broadcast game state to keep guests updated + return; + } + } + + // Only update user info in gameState for authenticated users + if (isAuthenticated) { + if (!this.gameState.users[currentUserId]) { + this.gameState.users[currentUserId] = { + name: currentUserName, + clicks: 0, + lastSeen: Date.now(), + bonusMultiplier: 1 // Initialize bonus multiplier + }; + } + this.gameState.users[currentUserId].lastSeen = Date.now(); + this.gameState.users[currentUserId].name = currentUserName; // Update name in case it changed + // Ensure bonusMultiplier is initialized if it somehow wasn't + if (this.gameState.users[currentUserId].bonusMultiplier === undefined) { + this.gameState.users[currentUserId].bonusMultiplier = 1; + } + } + + switch (data.type) { + case 'user-join': + // For authenticated users, user join is handled by the user state update above. + // For guest users, no action is needed here as they are not added to gameState.users. + break; + case 'click': + if (isAuthenticated) { + this.handleClick(data, currentUserId); + } else { + console.warn(`Unauthenticated click from ${currentUserId} ignored.`); + } + break; + case 'purchase-upgrade': + if (isAuthenticated) { + this.handlePurchaseUpgrade(data, currentUserId); + } else { + console.warn(`Unauthenticated purchase-upgrade from ${currentUserId} ignored.`); + } + break; + case 'apply-multiplier-bonus': + if (isAuthenticated) { + this.handleApplyMultiplierBonus(data, currentUserId); + } else { + console.warn(`Unauthenticated apply-multiplier-bonus from ${currentUserId} ignored.`); + } + break; } this.broadcast(); } - // handleUserJoin is now largely integrated into onMessage's token verification - // Keeping it for now, but it might become redundant or simplified. - handleUserJoin(data: UserJoinMessage) { - // This function is now mostly handled by the onMessage token verification logic - // It ensures the user exists in gameState.users and updates lastSeen/name. - // No explicit action needed here beyond what onMessage does. - } + // 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) { + // // This function is now mostly handled by the onMessage token verification logic + // // It ensures the user exists in gameState.users and updates lastSeen/name. + // // No explicit action needed here beyond what onMessage does. + // } handleClick(data: ClickMessage, authenticatedUserId: string) { // Apply global click multiplier and user-specific bonus multiplier @@ -197,10 +225,9 @@ export default class GameServer implements Party.Server { if (!upgrade || !currentUpgrade) return; - const userClicks = this.gameState.users[authenticatedUserId]?.clicks || 0; - - if (userClicks >= currentUpgrade.cost) { - this.gameState.users[authenticatedUserId].clicks -= currentUpgrade.cost; + // Check affordability against totalClicks + if (this.gameState.totalClicks >= currentUpgrade.cost) { + this.gameState.totalClicks -= currentUpgrade.cost; // Deduct from totalClicks currentUpgrade.owned += 1; currentUpgrade.cost = Math.floor(upgrade.baseCost * Math.pow(upgrade.multiplier, currentUpgrade.owned)); diff --git a/src/App.tsx b/src/App.tsx index 820da48..d6db817 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -149,8 +149,6 @@ function App() { ); } - // Only calculate userClicks if userId is defined (i.e., user is signed in) - const userClicks = isSignedIn && userId ? gameState.users[userId]?.clicks || 0 : 0; const userBonusMultiplier = isSignedIn && userId ? gameState.users[userId]?.bonusMultiplier || 1 : 1; const effectiveClickMultiplier = gameState.clickMultiplier * userBonusMultiplier; @@ -211,7 +209,7 @@ function App() {
diff --git a/src/components/UpgradeShop.tsx b/src/components/UpgradeShop.tsx index 33e9503..86d8a2f 100644 --- a/src/components/UpgradeShop.tsx +++ b/src/components/UpgradeShop.tsx @@ -4,7 +4,7 @@ import { GameState, MascotTier } from '../types'; // Import MascotTier interface UpgradeShopProps { gameState: GameState; - userClicks: number; + totalClicks: number; // Changed from userClicks onPurchase: (upgradeId: string) => void; } @@ -12,13 +12,17 @@ interface UpgradeShopProps { const getMascotName = (imageSrc: string): string => { const fileName = imageSrc.split('/').pop() || ''; const nameWithoutExtension = fileName.split('.')[0]; + // remove the word neurosama if it exists in the name + if (nameWithoutExtension.includes('neurosama')) { + return nameWithoutExtension.replace('neurosama', '').trim(); + } return nameWithoutExtension .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); }; -export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopProps) { +export function UpgradeShop({ gameState, totalClicks, onPurchase }: UpgradeShopProps) { // Changed from userClicks return (

@@ -29,7 +33,7 @@ export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopPr {UPGRADES.map((upgrade) => { const owned = gameState.upgrades[upgrade.id]?.owned || 0; const cost = gameState.upgrades[upgrade.id]?.cost || upgrade.baseCost; - const canAfford = userClicks >= cost; + const canAfford = totalClicks >= cost; // Changed from userClicks let description = upgrade.description; @@ -84,7 +88,7 @@ export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopPr
- Your Clicks: {userClicks.toLocaleString()} + Total Clicks: {totalClicks.toLocaleString()}

diff --git a/src/hooks/usePartyKit.ts b/src/hooks/usePartyKit.ts index 0688b5d..4ced641 100644 --- a/src/hooks/usePartyKit.ts +++ b/src/hooks/usePartyKit.ts @@ -10,23 +10,48 @@ export function usePartyKit() { const { user } = useUser(); const [gameState, setGameState] = useState(null); const [socket, setSocket] = useState(null); + // Generate a persistent guest ID if the user is not signed in + const [guestId] = useState(() => { + let storedGuestId = localStorage.getItem('bozo_guest_id'); + if (!storedGuestId) { + storedGuestId = `guest_${Math.random().toString(36).substring(2, 15)}`; + localStorage.setItem('bozo_guest_id', storedGuestId); + } + return storedGuestId; + }); + const [guestName] = useState(() => { + let storedGuestName = localStorage.getItem('bozo_guest_name'); + if (!storedGuestName) { + storedGuestName = `Guest${Math.floor(Math.random() * 10000)}`; + localStorage.setItem('bozo_guest_name', storedGuestName); + } + return storedGuestName; + }); useEffect(() => { - if (!isLoaded || !isSignedIn || !user) return; - + // Always attempt to connect the socket const ws = new PartySocket({ host: PARTY_HOST, room: 'bozo-clicker' }); ws.onopen = async () => { - const token = await getToken(); - ws.send(JSON.stringify({ - type: 'user-join', - userId: user.id, - userName: user.username || user.fullName || user.emailAddresses[0].emailAddress, - token - })); + if (isLoaded && isSignedIn && user) { + const token = await getToken(); + ws.send(JSON.stringify({ + type: 'user-join', + userId: user.id, + userName: user.username || user.fullName || user.emailAddresses[0].emailAddress, + token + })); + } else { + // If not signed in, send a generic user-join message for guest + ws.send(JSON.stringify({ + type: 'user-join', + userId: guestId, + userName: guestName + })); + } }; ws.onmessage = (event) => { @@ -41,7 +66,7 @@ export function usePartyKit() { return () => { ws.close(); }; - }, [isLoaded, isSignedIn, user, getToken]); + }, [isLoaded, guestId, guestName]); // Removed isSignedIn, user, getToken from dependencies const sendClick = useCallback(async () => { if (socket && isSignedIn && user) { @@ -67,7 +92,7 @@ export function usePartyKit() { } }, [socket, isSignedIn, user, getToken]); - const sendMascotClickBonus = useCallback(async (multiplierBonus: number) => { // Renamed function + const sendMascotClickBonus = useCallback(async (multiplierBonus: number) => { if (socket && isSignedIn && user) { const token = await getToken(); socket.send(JSON.stringify({ @@ -80,12 +105,17 @@ export function usePartyKit() { } }, [socket, isSignedIn, user, getToken]); + // Determine the userId and userName to expose based on sign-in status + // Only expose Clerk user ID/name if signed in, otherwise undefined + const currentUserId = isSignedIn && user ? user.id : undefined; + const currentUserName = isSignedIn && user ? (user.username || user.fullName || user.emailAddresses[0].emailAddress) : undefined; + return { gameState, sendClick, purchaseUpgrade, - sendMascotClickBonus, // Export the renamed function - userId: user?.id, - userName: user?.username || user?.fullName || user?.emailAddresses[0]?.emailAddress + sendMascotClickBonus, + userId: currentUserId, + userName: currentUserName }; }