This commit is contained in:
2025-08-03 21:49:49 +05:30
parent 40b8f367fe
commit f618a02ce3
4 changed files with 130 additions and 71 deletions

View File

@@ -89,74 +89,102 @@ export default class GameServer implements Party.Server {
async onMessage(message: string, sender: Party.Connection) { async onMessage(message: string, sender: Party.Connection) {
const data = JSON.parse(message) as Message; const data = JSON.parse(message) as Message;
let currentUserId: string;
let currentUserName: string;
let isAuthenticated = false;
// Verify the Clerk token for authenticated messages // Verify the Clerk token for authenticated messages
if ('token' in data && data.token) { if ('token' in data && data.token) {
try { try {
const sessionClaims = await verifyToken(data.token, { const sessionClaims = await verifyToken(data.token, {
jwtKey: this.party.env.CLERK_JWT_KEY as string, 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 // Ensure the userId from the client matches the authenticated userId
if (authenticatedUserId !== data.userId) { if (currentUserId !== data.userId) {
console.warn(`User ID mismatch: Client sent ${data.userId}, but token is for ${authenticatedUserId}`); console.warn(`User ID mismatch: Client sent ${data.userId}, but token is for ${currentUserId}`);
sender.close(); // Close connection for potential tampering sender.close(); // Close connection for potential tampering
return; return;
} }
currentUserName = data.userName; // Use name from client, which comes from Clerk
// 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;
}
} catch (error) { } catch (error) {
console.error('Clerk token verification failed:', error); console.error('Clerk token verification failed:', error);
sender.close(); // Close connection if token is invalid sender.close(); // Close connection if token is invalid
return; return;
} }
} else { } else {
// For messages without a token (e.g., initial connection before token is sent, or unauthenticated actions) // Handle unauthenticated messages (guest users)
// For this game, we only allow authenticated actions. currentUserId = data.userId;
console.warn('Received unauthenticated message, closing connection.'); currentUserName = data.userName;
sender.close(); isAuthenticated = false;
return;
// 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(); this.broadcast();
} }
// handleUserJoin is now largely integrated into onMessage's token verification // handleUserJoin is now fully integrated into onMessage and can be removed or simplified.
// Keeping it for now, but it might become redundant or simplified. // Removing it as its logic is now directly in onMessage.
handleUserJoin(data: UserJoinMessage) { // handleUserJoin(data: UserJoinMessage) {
// This function is now mostly handled by the onMessage token verification logic // // This function is now mostly handled by the onMessage token verification logic
// It ensures the user exists in gameState.users and updates lastSeen/name. // // It ensures the user exists in gameState.users and updates lastSeen/name.
// No explicit action needed here beyond what onMessage does. // // No explicit action needed here beyond what onMessage does.
} // }
handleClick(data: ClickMessage, authenticatedUserId: string) { handleClick(data: ClickMessage, authenticatedUserId: string) {
// Apply global click multiplier and user-specific bonus multiplier // Apply global click multiplier and user-specific bonus multiplier
@@ -197,10 +225,9 @@ export default class GameServer implements Party.Server {
if (!upgrade || !currentUpgrade) return; if (!upgrade || !currentUpgrade) return;
const userClicks = this.gameState.users[authenticatedUserId]?.clicks || 0; // Check affordability against totalClicks
if (this.gameState.totalClicks >= currentUpgrade.cost) {
if (userClicks >= currentUpgrade.cost) { this.gameState.totalClicks -= currentUpgrade.cost; // Deduct from totalClicks
this.gameState.users[authenticatedUserId].clicks -= currentUpgrade.cost;
currentUpgrade.owned += 1; currentUpgrade.owned += 1;
currentUpgrade.cost = Math.floor(upgrade.baseCost * Math.pow(upgrade.multiplier, currentUpgrade.owned)); currentUpgrade.cost = Math.floor(upgrade.baseCost * Math.pow(upgrade.multiplier, currentUpgrade.owned));

View File

@@ -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 userBonusMultiplier = isSignedIn && userId ? gameState.users[userId]?.bonusMultiplier || 1 : 1;
const effectiveClickMultiplier = gameState.clickMultiplier * userBonusMultiplier; const effectiveClickMultiplier = gameState.clickMultiplier * userBonusMultiplier;
@@ -211,7 +209,7 @@ function App() {
<div className="lg:col-span-1"> <div className="lg:col-span-1">
<UpgradeShop <UpgradeShop
gameState={gameState} gameState={gameState}
userClicks={userClicks} totalClicks={gameState.totalClicks}
onPurchase={purchaseUpgrade} onPurchase={purchaseUpgrade}
/> />
</div> </div>

View File

@@ -4,7 +4,7 @@ import { GameState, MascotTier } from '../types'; // Import MascotTier
interface UpgradeShopProps { interface UpgradeShopProps {
gameState: GameState; gameState: GameState;
userClicks: number; totalClicks: number; // Changed from userClicks
onPurchase: (upgradeId: string) => void; onPurchase: (upgradeId: string) => void;
} }
@@ -12,13 +12,17 @@ interface UpgradeShopProps {
const getMascotName = (imageSrc: string): string => { const getMascotName = (imageSrc: string): string => {
const fileName = imageSrc.split('/').pop() || ''; const fileName = imageSrc.split('/').pop() || '';
const nameWithoutExtension = fileName.split('.')[0]; 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 return nameWithoutExtension
.split('-') .split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1)) .map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' '); .join(' ');
}; };
export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopProps) { export function UpgradeShop({ gameState, totalClicks, onPurchase }: UpgradeShopProps) { // Changed from userClicks
return ( return (
<div className="bg-gradient-to-b from-purple-800 to-pink-600 p-6 rounded-xl border-4 border-cyan-400 shadow-2xl"> <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' }}> <h2 className="text-3xl font-bold text-yellow-300 mb-6 text-center" style={{ fontFamily: 'Comic Sans MS, cursive' }}>
@@ -29,7 +33,7 @@ export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopPr
{UPGRADES.map((upgrade) => { {UPGRADES.map((upgrade) => {
const owned = gameState.upgrades[upgrade.id]?.owned || 0; const owned = gameState.upgrades[upgrade.id]?.owned || 0;
const cost = gameState.upgrades[upgrade.id]?.cost || upgrade.baseCost; const cost = gameState.upgrades[upgrade.id]?.cost || upgrade.baseCost;
const canAfford = userClicks >= cost; const canAfford = totalClicks >= cost; // Changed from userClicks
let description = upgrade.description; let description = upgrade.description;
@@ -84,7 +88,7 @@ export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopPr
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<div className="text-2xl font-bold text-yellow-300"> <div className="text-2xl font-bold text-yellow-300">
Your Clicks: {userClicks.toLocaleString()} Total Clicks: {totalClicks.toLocaleString()}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -10,23 +10,48 @@ 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);
// 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(() => { useEffect(() => {
if (!isLoaded || !isSignedIn || !user) return; // Always attempt to connect the socket
const ws = new PartySocket({ const ws = new PartySocket({
host: PARTY_HOST, host: PARTY_HOST,
room: 'bozo-clicker' room: 'bozo-clicker'
}); });
ws.onopen = async () => { ws.onopen = async () => {
const token = await getToken(); if (isLoaded && isSignedIn && user) {
ws.send(JSON.stringify({ const token = await getToken();
type: 'user-join', ws.send(JSON.stringify({
userId: user.id, type: 'user-join',
userName: user.username || user.fullName || user.emailAddresses[0].emailAddress, userId: user.id,
token 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) => { ws.onmessage = (event) => {
@@ -41,7 +66,7 @@ export function usePartyKit() {
return () => { return () => {
ws.close(); ws.close();
}; };
}, [isLoaded, isSignedIn, user, getToken]); }, [isLoaded, guestId, guestName]); // Removed isSignedIn, user, getToken from dependencies
const sendClick = useCallback(async () => { const sendClick = useCallback(async () => {
if (socket && isSignedIn && user) { if (socket && isSignedIn && user) {
@@ -67,7 +92,7 @@ export function usePartyKit() {
} }
}, [socket, isSignedIn, user, getToken]); }, [socket, isSignedIn, user, getToken]);
const sendMascotClickBonus = useCallback(async (multiplierBonus: number) => { // Renamed function const sendMascotClickBonus = useCallback(async (multiplierBonus: number) => {
if (socket && isSignedIn && user) { if (socket && isSignedIn && user) {
const token = await getToken(); const token = await getToken();
socket.send(JSON.stringify({ socket.send(JSON.stringify({
@@ -80,12 +105,17 @@ export function usePartyKit() {
} }
}, [socket, isSignedIn, user, getToken]); }, [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 { return {
gameState, gameState,
sendClick, sendClick,
purchaseUpgrade, purchaseUpgrade,
sendMascotClickBonus, // Export the renamed function sendMascotClickBonus,
userId: user?.id, userId: currentUserId,
userName: user?.username || user?.fullName || user?.emailAddresses[0]?.emailAddress userName: currentUserName
}; };
} }