fixes
This commit is contained in:
125
party/index.ts
125
party/index.ts
@@ -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;
|
||||||
|
|
||||||
|
// 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;
|
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));
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -10,16 +10,33 @@ 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 () => {
|
||||||
|
if (isLoaded && isSignedIn && user) {
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
type: 'user-join',
|
type: 'user-join',
|
||||||
@@ -27,6 +44,14 @@ export function usePartyKit() {
|
|||||||
userName: user.username || user.fullName || user.emailAddresses[0].emailAddress,
|
userName: user.username || user.fullName || user.emailAddresses[0].emailAddress,
|
||||||
token
|
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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user