added mascot clicking

This commit is contained in:
2025-08-03 21:05:07 +05:30
parent 7f48a70473
commit 40b8f367fe
15 changed files with 341 additions and 19 deletions

View File

@@ -4,7 +4,7 @@ import { createClerkClient, verifyToken } from '@clerk/backend';
interface GameState { interface GameState {
totalClicks: number; totalClicks: number;
users: Record<string, { name: string; clicks: number; lastSeen: number }>; users: Record<string, { name: string; clicks: number; lastSeen: number; bonusMultiplier: number }>; // Added bonusMultiplier
upgrades: Record<string, { owned: number; cost: number }>; upgrades: Record<string, { owned: number; cost: number }>;
milestones: Record<string, boolean>; milestones: Record<string, boolean>;
clickMultiplier: number; clickMultiplier: number;
@@ -28,18 +28,24 @@ interface PurchaseUpgradeMessage extends AuthenticatedMessage {
upgradeId: string; upgradeId: string;
} }
interface ApplyMultiplierBonusMessage extends AuthenticatedMessage { // New message type
type: 'apply-multiplier-bonus';
multiplierBonus: number;
}
interface UserJoinMessage extends AuthenticatedMessage { interface UserJoinMessage extends AuthenticatedMessage {
type: 'user-join'; type: 'user-join';
} }
type Message = ClickMessage | PurchaseUpgradeMessage | UserJoinMessage; type Message = ClickMessage | PurchaseUpgradeMessage | ApplyMultiplierBonusMessage | UserJoinMessage; // Updated Message type
const UPGRADES = { const UPGRADES = {
clickMultiplier: { baseCost: 10, multiplier: 1.5, clickBonus: 1 }, clickMultiplier: { baseCost: 10, multiplier: 1.5, clickBonus: 1 },
autoClicker: { baseCost: 50, multiplier: 2, autoClickRate: 1 }, autoClicker: { baseCost: 50, multiplier: 2, autoClickRate: 1 },
megaBonus: { baseCost: 200, multiplier: 2.5, clickBonus: 5 }, megaBonus: { baseCost: 200, multiplier: 2.5, clickBonus: 5 },
hyperClicker: { baseCost: 1000, multiplier: 3, autoClickRate: 10 }, hyperClicker: { baseCost: 1000, multiplier: 3, autoClickRate: 10 },
quantumClicker: { baseCost: 5000, multiplier: 4, clickBonus: 50 } quantumClicker: { baseCost: 5000, multiplier: 4, clickBonus: 50 },
friendBoost: { baseCost: 2000, multiplier: 3, clickMultiplierBonus: 1.02 } // Renamed from shoominions upgrade
}; };
const MILESTONES = [ const MILESTONES = [
@@ -103,11 +109,16 @@ export default class GameServer implements Party.Server {
this.gameState.users[authenticatedUserId] = { this.gameState.users[authenticatedUserId] = {
name: data.userName, // Use the name provided by the client, which comes from Clerk name: data.userName, // Use the name provided by the client, which comes from Clerk
clicks: 0, clicks: 0,
lastSeen: Date.now() lastSeen: Date.now(),
bonusMultiplier: 1 // Initialize bonus multiplier
}; };
} }
this.gameState.users[authenticatedUserId].lastSeen = Date.now(); this.gameState.users[authenticatedUserId].lastSeen = Date.now();
this.gameState.users[authenticatedUserId].name = data.userName; // Update name in case it changed 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) { switch (data.type) {
case 'user-join': case 'user-join':
@@ -119,6 +130,9 @@ export default class GameServer implements Party.Server {
case 'purchase-upgrade': case 'purchase-upgrade':
this.handlePurchaseUpgrade(data, authenticatedUserId); this.handlePurchaseUpgrade(data, authenticatedUserId);
break; 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);
@@ -145,7 +159,9 @@ export default class GameServer implements Party.Server {
} }
handleClick(data: ClickMessage, authenticatedUserId: string) { handleClick(data: ClickMessage, authenticatedUserId: string) {
const clickValue = this.gameState.clickMultiplier; // Apply global click multiplier and user-specific bonus multiplier
const userBonusMultiplier = this.gameState.users[authenticatedUserId]?.bonusMultiplier || 1;
const clickValue = this.gameState.clickMultiplier * userBonusMultiplier;
this.gameState.totalClicks += clickValue; this.gameState.totalClicks += clickValue;
@@ -154,7 +170,8 @@ export default class GameServer implements Party.Server {
this.gameState.users[authenticatedUserId] = { this.gameState.users[authenticatedUserId] = {
name: data.userName, // Use the name from the client, which comes from Clerk name: data.userName, // Use the name from the client, which comes from Clerk
clicks: 0, clicks: 0,
lastSeen: Date.now() lastSeen: Date.now(),
bonusMultiplier: 1 // Initialize if user was just created
}; };
} }
@@ -164,6 +181,16 @@ export default class GameServer implements Party.Server {
this.checkMilestones(); this.checkMilestones();
} }
handleApplyMultiplierBonus(data: ApplyMultiplierBonusMessage, authenticatedUserId: string) {
if (!this.gameState.users[authenticatedUserId]) {
console.warn(`User ${authenticatedUserId} not found for multiplier bonus application.`);
return;
}
// Apply the compounding multiplier bonus
this.gameState.users[authenticatedUserId].bonusMultiplier *= data.multiplierBonus;
console.log(`User ${authenticatedUserId} bonus multiplier updated to: ${this.gameState.users[authenticatedUserId].bonusMultiplier}`);
}
handlePurchaseUpgrade(data: PurchaseUpgradeMessage, authenticatedUserId: string) { handlePurchaseUpgrade(data: PurchaseUpgradeMessage, authenticatedUserId: string) {
const upgrade = UPGRADES[data.upgradeId as keyof typeof UPGRADES]; const upgrade = UPGRADES[data.upgradeId as keyof typeof UPGRADES];
const currentUpgrade = this.gameState.upgrades[data.upgradeId]; const currentUpgrade = this.gameState.upgrades[data.upgradeId];
@@ -193,6 +220,8 @@ export default class GameServer implements Party.Server {
if ('autoClickRate' in config && config.autoClickRate) { if ('autoClickRate' in config && config.autoClickRate) {
this.gameState.autoClickRate += config.autoClickRate * upgrade.owned; this.gameState.autoClickRate += config.autoClickRate * upgrade.owned;
} }
// Note: clickMultiplierBonus from upgrades.ts is handled client-side for spawning frequency
// and applied per-click on the server via handleApplyMultiplierBonus
}); });
this.setupAutoClicker(); this.setupAutoClicker();

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'; import { useEffect, useState, useRef } from 'react';
import { usePartyKit } from './hooks/usePartyKit'; import { usePartyKit } from './hooks/usePartyKit';
import { ClickButton } from './components/ClickButton'; import { ClickButton } from './components/ClickButton';
import { Counter } from './components/Counter'; import { Counter } from './components/Counter';
@@ -9,16 +9,21 @@ import { Background } from './components/Background';
import { MILESTONES } from './config/milestones'; import { MILESTONES } from './config/milestones';
import { SignedIn, SignedOut, SignInButton, UserButton } from '@clerk/clerk-react'; import { SignedIn, SignedOut, SignInButton, UserButton } from '@clerk/clerk-react';
import { useAuth } from '@clerk/clerk-react'; import { useAuth } from '@clerk/clerk-react';
import { ClickableMascot } from './components/ClickableMascot'; // Import ClickableMascot component
import { ClickableMascot as ClickableMascotType, Upgrade } from './types'; // Import ClickableMascotType and Upgrade
import { UPGRADES } from './config/upgrades'; // Import UPGRADES for upgrade config
function App() { function App() {
const { isSignedIn, isLoaded } = useAuth(); const { isSignedIn, isLoaded } = useAuth();
const { gameState, sendClick, purchaseUpgrade, userId } = usePartyKit(); const { gameState, sendClick, purchaseUpgrade, userId, sendMascotClickBonus } = 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 timeoutRef = useRef<NodeJS.Timeout | null>(null);
// Effect for milestone celebrations
useEffect(() => { useEffect(() => {
if (gameState) { if (gameState) {
// Check for new milestones
const newMilestones = Object.keys(gameState.milestones).filter( const newMilestones = Object.keys(gameState.milestones).filter(
(milestoneId) => (milestoneId) =>
gameState.milestones[milestoneId] && gameState.milestones[milestoneId] &&
@@ -37,6 +42,103 @@ function App() {
} }
}, [gameState, previousMilestones]); }, [gameState, previousMilestones]);
// Effect for spawning mascots
useEffect(() => {
if (!gameState) return;
const friendBoostUpgrade = UPGRADES.find(u => u.id === 'friendBoost') as Upgrade | undefined;
const ownedFriendBoost = gameState.upgrades['friendBoost']?.owned || 0;
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
if (ownedFriendBoost > 0 && friendBoostUpgrade && friendBoostUpgrade.mascotTiers) {
const baseInterval = 10000; // Increased base interval for less frequent spawns
const minInterval = 1000; // Increased min interval
const interval = Math.max(minInterval, baseInterval / (1 + ownedFriendBoost * 0.2)); // Adjusted scaling
const currentMascotTiers = friendBoostUpgrade.mascotTiers; // Ensure mascotTiers is not undefined
const spawnMascot = () => {
const buttonContainer = document.getElementById('click-button-container');
if (!buttonContainer) {
console.warn('Click button container not found!');
timeoutRef.current = setTimeout(spawnMascot, 1000); // Retry after 1 second
return;
}
const rect = buttonContainer.getBoundingClientRect();
const spawnAreaPadding = 150;
const minX = rect.left - spawnAreaPadding;
const maxX = rect.right + spawnAreaPadding;
const minY = rect.top - spawnAreaPadding;
const maxY = rect.bottom + spawnAreaPadding;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const randomX = Math.max(0, Math.min(viewportWidth, minX + Math.random() * (maxX - minX)));
const randomY = Math.max(0, Math.min(viewportHeight, minY + Math.random() * (maxY - minY)));
// Filter available mascots based on ownedFriendBoost level
const availableMascots = currentMascotTiers.filter(
(tier) => ownedFriendBoost >= tier.level
);
if (availableMascots.length === 0) {
console.warn('No mascots available to spawn for current friendBoost level.');
timeoutRef.current = setTimeout(spawnMascot, interval);
return;
}
// Weighted random selection based on rarity
const totalRarity = availableMascots.reduce((sum, tier) => sum + tier.rarity, 0);
let randomWeight = Math.random() * totalRarity;
let selectedMascotTier = availableMascots[0]; // Default to first available
for (const tier of availableMascots) {
randomWeight -= tier.rarity;
if (randomWeight <= 0) {
selectedMascotTier = tier;
break;
}
}
const newMascot: ClickableMascotType = {
id: `mascot-${Date.now()}-${Math.random()}`,
x: randomX,
y: randomY,
multiplierBonus: selectedMascotTier.multiplier,
imageSrc: selectedMascotTier.imageSrc,
};
setMascotEntities((prev) => [...prev, newMascot]);
timeoutRef.current = setTimeout(spawnMascot, interval);
};
spawnMascot(); // Start the first spawn
} else {
setMascotEntities([]); // Clear all mascots if friendBoost is 0
}
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [gameState?.upgrades['friendBoost']?.owned, gameState]);
const handleMascotClick = (id: string, multiplierBonus: number) => { // Renamed handler
sendMascotClickBonus(multiplierBonus); // Renamed function call
setMascotEntities((prev) => prev.filter((m) => m.id !== id)); // Update state variable
};
const handleMascotRemove = (id: string) => { // Renamed handler
setMascotEntities((prev) => prev.filter((m) => m.id !== id)); // Update state variable
};
if (!isLoaded || !gameState) { if (!isLoaded || !gameState) {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-900 to-pink-900"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-900 to-pink-900">
@@ -49,6 +151,8 @@ function App() {
// Only calculate userClicks if userId is defined (i.e., user is signed in) // Only calculate userClicks if userId is defined (i.e., user is signed in)
const userClicks = isSignedIn && userId ? gameState.users[userId]?.clicks || 0 : 0; 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;
return ( return (
<div className="min-h-screen relative overflow-x-hidden"> <div className="min-h-screen relative overflow-x-hidden">
@@ -82,12 +186,25 @@ function App() {
{/* Main Content */} {/* Main Content */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left Column - Click Button */} {/* Left Column - Click Button */}
<div className="lg:col-span-1 flex flex-col items-center justify-center"> <div id="click-button-container" className="lg:col-span-1 flex flex-col items-center justify-center relative"> {/* Added id and relative positioning */}
<ClickButton <ClickButton
onClick={sendClick} onClick={sendClick}
imageUrl={gameState.currentClickImage} imageUrl={gameState.currentClickImage}
clickMultiplier={gameState.clickMultiplier} clickMultiplier={effectiveClickMultiplier}
/> />
{/* Render Mascots */}
{mascotEntities.map((mascot: ClickableMascotType) => (
<ClickableMascot
key={mascot.id}
id={mascot.id}
x={mascot.x}
y={mascot.y}
multiplierBonus={mascot.multiplierBonus}
imageSrc={mascot.imageSrc}
onClick={handleMascotClick}
onRemove={handleMascotRemove}
/>
))}
</div> </div>
{/* Middle Column - Upgrades */} {/* Middle Column - Upgrades */}

BIN
src/assets/bozo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

BIN
src/assets/codebug.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
src/assets/lalan.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
src/assets/shoominion.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -0,0 +1,61 @@
import React, { useEffect, useState } from 'react';
interface ClickableMascotProps { // Renamed from ShoominionProps
id: string;
x: number;
y: number;
multiplierBonus: number;
imageSrc: string; // New: Source for the mascot image
onClick: (id: string, multiplierBonus: number) => void;
onRemove: (id: string) => void;
}
export const ClickableMascot: React.FC<ClickableMascotProps> = ({ // Renamed from Shoominion
id,
x,
y,
multiplierBonus,
imageSrc, // Destructure new prop
onClick,
onRemove,
}) => {
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
const timer = setTimeout(() => {
setIsVisible(false);
// Give some time for fade-out animation before actual removal
setTimeout(() => onRemove(id), 500);
}, 7000); // Mascot disappears after 7 seconds if not clicked
return () => clearTimeout(timer);
}, [id, onRemove]);
const handleClick = () => {
setIsVisible(false);
onClick(id, multiplierBonus);
// Give some time for fade-out animation before actual removal
setTimeout(() => onRemove(id), 500);
};
return (
<div
className={`absolute cursor-pointer transition-opacity duration-500 ${
isVisible ? 'opacity-100' : 'opacity-0'
}`}
style={{
left: `${x}px`,
top: `${y}px`,
transform: 'translate(-50%, -50%)', // Center the image on the coordinates
zIndex: 30, // Ensure it's above other elements but below popups
}}
onClick={handleClick}
>
<img
src={imageSrc} // Use imageSrc prop
alt="Clickable Mascot" // Updated alt text
className="w-24 h-24 animate-bounce-slow"
/>
</div>
);
};

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { UPGRADES } from '../config/upgrades'; import { UPGRADES } from '../config/upgrades';
import { GameState } from '../types'; import { GameState, MascotTier } from '../types'; // Import MascotTier
interface UpgradeShopProps { interface UpgradeShopProps {
gameState: GameState; gameState: GameState;
@@ -8,6 +8,16 @@ interface UpgradeShopProps {
onPurchase: (upgradeId: string) => void; onPurchase: (upgradeId: string) => void;
} }
// Helper function to get mascot name from image source
const getMascotName = (imageSrc: string): string => {
const fileName = imageSrc.split('/').pop() || '';
const nameWithoutExtension = fileName.split('.')[0];
return nameWithoutExtension
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopProps) { export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopProps) {
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">
@@ -21,6 +31,21 @@ export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopPr
const cost = gameState.upgrades[upgrade.id]?.cost || upgrade.baseCost; const cost = gameState.upgrades[upgrade.id]?.cost || upgrade.baseCost;
const canAfford = userClicks >= cost; const canAfford = userClicks >= cost;
let description = upgrade.description;
// Custom description for Friend Boost upgrade
if (upgrade.id === 'friendBoost' && upgrade.mascotTiers) {
const nextMascotTier = upgrade.mascotTiers.find(
(tier: MascotTier) => owned < tier.level
);
if (nextMascotTier) {
description = `Spawns various clickable friends. Next: ${getMascotName(nextMascotTier.imageSrc)} at level ${nextMascotTier.level}`;
} else {
description = 'All mascots unlocked! Spawns various clickable friends.';
}
}
return ( return (
<div <div
key={upgrade.id} key={upgrade.id}
@@ -42,7 +67,7 @@ export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopPr
</span> </span>
)} )}
</div> </div>
<p className="text-cyan-200 text-sm mt-1">{upgrade.description}</p> <p className="text-cyan-200 text-sm mt-1">{description}</p>
</div> </div>
<div className="text-right"> <div className="text-right">
@@ -64,4 +89,4 @@ export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopPr
</div> </div>
</div> </div>
); );
} }

View File

@@ -45,5 +45,51 @@ export const UPGRADES: Upgrade[] = [
multiplier: 4, multiplier: 4,
clickBonus: 50, clickBonus: 50,
icon: '🌟' icon: '🌟'
},
{
id: 'friendBoost',
name: '🤝 Friend Boost',
description: 'Spawns various clickable friends for a compounding click boost',
baseCost: 2000,
multiplier: 3,
icon: '🤝',
mascotTiers: [
{
level: 0,
imageSrc: '/src/assets/bozo.png',
multiplier: 1.02,
rarity: 1.0,
},
{
level: 1,
imageSrc: '/src/assets/shoominion.png',
multiplier: 1.03,
rarity: 0.8,
},
{
level: 5,
imageSrc: '/src/assets/codebug.gif',
multiplier: 1.05,
rarity: 0.6,
},
{
level: 10,
imageSrc: '/src/assets/lalan.gif',
multiplier: 1.07,
rarity: 0.4,
},
{
level: 15,
imageSrc: '/src/assets/neuro-neurosama.gif',
multiplier: 1.10,
rarity: 0.2,
},
{
level: 20,
imageSrc: '/src/assets/evil-neurosama.gif',
multiplier: 1.15,
rarity: 0.1,
},
],
} }
]; ];

View File

@@ -67,10 +67,24 @@ export function usePartyKit() {
} }
}, [socket, isSignedIn, user, getToken]); }, [socket, isSignedIn, user, getToken]);
const sendMascotClickBonus = useCallback(async (multiplierBonus: number) => { // Renamed function
if (socket && isSignedIn && user) {
const token = await getToken();
socket.send(JSON.stringify({
type: 'apply-multiplier-bonus',
userId: user.id,
userName: user.username || user.fullName || user.emailAddresses[0].emailAddress,
token,
multiplierBonus
}));
}
}, [socket, isSignedIn, user, getToken]);
return { return {
gameState, gameState,
sendClick, sendClick,
purchaseUpgrade, purchaseUpgrade,
sendMascotClickBonus, // Export the renamed function
userId: user?.id, userId: user?.id,
userName: user?.username || user?.fullName || user?.emailAddresses[0]?.emailAddress userName: user?.username || user?.fullName || user?.emailAddresses[0]?.emailAddress
}; };

View File

@@ -59,6 +59,19 @@ body {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
@keyframes bounce-slow {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
.animate-bounce-slow {
animation: bounce-slow 4s infinite ease-in-out; /* Increased duration for slower movement */
}
/* Custom scrollbar */ /* Custom scrollbar */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
@@ -133,4 +146,4 @@ body {
.retro-button:active { .retro-button:active {
transform: translateY(1px); transform: translateY(1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.2); box-shadow: 0 2px 4px rgba(0,0,0,0.2);
} }

View File

@@ -1,6 +1,6 @@
export interface GameState { export interface GameState {
totalClicks: number; totalClicks: number;
users: Record<string, { name: string; clicks: number; lastSeen: number }>; users: Record<string, { name: string; clicks: number; lastSeen: number; bonusMultiplier: number }>; // Added bonusMultiplier
upgrades: Record<string, { owned: number; cost: number }>; upgrades: Record<string, { owned: number; cost: number }>;
milestones: Record<string, boolean>; milestones: Record<string, boolean>;
clickMultiplier: number; clickMultiplier: number;
@@ -17,7 +17,24 @@ export interface Upgrade {
multiplier: number; multiplier: number;
clickBonus?: number; clickBonus?: number;
autoClickRate?: number; autoClickRate?: number;
clickMultiplierBonus?: number; // New: for compounding click boosts from mascots
icon: string; icon: string;
mascotTiers?: MascotTier[]; // New: for defining mascot tiers for friendBoost
}
export interface MascotTier {
level: number;
imageSrc: string;
multiplier: number;
rarity: number; // 0 to 1, 1 being most common
}
export interface ClickableMascot { // Renamed from Shoominion
id: string;
x: number;
y: number;
multiplierBonus: number; // The multiplier this specific mascot provides
imageSrc: string; // New: Source for the mascot image
} }
export interface Milestone { export interface Milestone {
@@ -28,4 +45,4 @@ export interface Milestone {
background: string; background: string;
image: string; image: string;
reward: string; reward: string;
} }

View File

@@ -9,4 +9,4 @@ Like, at first it's only a bozo you click, after a while, codebugs pop up as bon
And some more And some more
Auth integration not completed with clerk, needs partykit to be integrated with clerk Auth integration not completed with clerk, needs partykit to be integrated with clerk (fixed)