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

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useState, useRef } from 'react';
import { usePartyKit } from './hooks/usePartyKit';
import { ClickButton } from './components/ClickButton';
import { Counter } from './components/Counter';
@@ -9,16 +9,21 @@ import { Background } from './components/Background';
import { MILESTONES } from './config/milestones';
import { SignedIn, SignedOut, SignInButton, UserButton } 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() {
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 [previousMilestones, setPreviousMilestones] = useState<Record<string, boolean>>({});
const [mascotEntities, setMascotEntities] = useState<ClickableMascotType[]>([]);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
// Effect for milestone celebrations
useEffect(() => {
if (gameState) {
// Check for new milestones
const newMilestones = Object.keys(gameState.milestones).filter(
(milestoneId) =>
gameState.milestones[milestoneId] &&
@@ -37,6 +42,103 @@ function App() {
}
}, [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) {
return (
<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)
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 (
<div className="min-h-screen relative overflow-x-hidden">
@@ -82,12 +186,25 @@ function App() {
{/* Main Content */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* 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
onClick={sendClick}
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>
{/* 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 { UPGRADES } from '../config/upgrades';
import { GameState } from '../types';
import { GameState, MascotTier } from '../types'; // Import MascotTier
interface UpgradeShopProps {
gameState: GameState;
@@ -8,6 +8,16 @@ interface UpgradeShopProps {
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) {
return (
<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 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 (
<div
key={upgrade.id}
@@ -42,7 +67,7 @@ export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopPr
</span>
)}
</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 className="text-right">
@@ -64,4 +89,4 @@ export function UpgradeShop({ gameState, userClicks, onPurchase }: UpgradeShopPr
</div>
</div>
);
}
}

View File

@@ -45,5 +45,51 @@ export const UPGRADES: Upgrade[] = [
multiplier: 4,
clickBonus: 50,
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]);
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 {
gameState,
sendClick,
purchaseUpgrade,
sendMascotClickBonus, // Export the renamed function
userId: user?.id,
userName: user?.username || user?.fullName || user?.emailAddresses[0]?.emailAddress
};

View File

@@ -59,6 +59,19 @@ body {
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 */
::-webkit-scrollbar {
width: 8px;
@@ -133,4 +146,4 @@ body {
.retro-button:active {
transform: translateY(1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
}

View File

@@ -1,6 +1,6 @@
export interface GameState {
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 }>;
milestones: Record<string, boolean>;
clickMultiplier: number;
@@ -17,7 +17,24 @@ export interface Upgrade {
multiplier: number;
clickBonus?: number;
autoClickRate?: number;
clickMultiplierBonus?: number; // New: for compounding click boosts from mascots
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 {
@@ -28,4 +45,4 @@ export interface Milestone {
background: string;
image: string;
reward: string;
}
}