added mascot clicking
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
127
src/App.tsx
127
src/App.tsx
@@ -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
BIN
src/assets/bozo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
BIN
src/assets/codebug.gif
Normal file
BIN
src/assets/codebug.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
src/assets/evil-neurosama.gif
Normal file
BIN
src/assets/evil-neurosama.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
BIN
src/assets/lalan.gif
Normal file
BIN
src/assets/lalan.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/neuro-neurosama.gif
Normal file
BIN
src/assets/neuro-neurosama.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
BIN
src/assets/shoominion.png
Normal file
BIN
src/assets/shoominion.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
61
src/components/ClickableMascot.tsx
Normal file
61
src/components/ClickableMascot.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
19
src/types.ts
19
src/types.ts
@@ -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 {
|
||||||
|
|||||||
2
todo.md
2
todo.md
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user