rat poiuson
This commit is contained in:
301
party/index.ts
301
party/index.ts
@@ -25,6 +25,7 @@ export interface MascotTier {
|
|||||||
rarity: number;
|
rarity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface UserState {
|
interface UserState {
|
||||||
name: string;
|
name: string;
|
||||||
clicks: number;
|
clicks: number;
|
||||||
@@ -34,6 +35,11 @@ interface UserState {
|
|||||||
upgrades: Record<string, { owned: number; cost: number }>;
|
upgrades: Record<string, { owned: number; cost: number }>;
|
||||||
clickMultiplier: number;
|
clickMultiplier: number;
|
||||||
autoClickRate: number;
|
autoClickRate: number;
|
||||||
|
ratPoisonChallenge?: {
|
||||||
|
challengeString: string;
|
||||||
|
expiresAt: number;
|
||||||
|
};
|
||||||
|
ratPoisonImmunityUntil?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GameState {
|
interface GameState {
|
||||||
@@ -81,144 +87,22 @@ interface EditUserMessage extends AuthenticatedMessage {
|
|||||||
upgrades: Record<string, { owned: number; cost: number }>;
|
upgrades: Record<string, { owned: number; cost: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Message = ClickMessage | PurchaseUpgradeMessage | ApplyMultiplierBonusMessage | UserJoinMessage | AdminBroadcastMessage | EditUserMessage; // Updated Message type
|
interface ThrowRatPoisonMessage extends AuthenticatedMessage {
|
||||||
|
type: 'throw-rat-poison';
|
||||||
|
targetUserId: string;
|
||||||
|
}
|
||||||
|
|
||||||
const UPGRADES: Upgrade[] = [
|
interface SolveRatPoisonMessage extends AuthenticatedMessage {
|
||||||
{
|
type: 'solve-rat-poison';
|
||||||
id: 'clickMultiplier',
|
challengeString: string;
|
||||||
name: '🖱️ Mega Click',
|
}
|
||||||
description: '+1 click power per purchase',
|
|
||||||
baseCost: 10,
|
type Message = ClickMessage | PurchaseUpgradeMessage | ApplyMultiplierBonusMessage | UserJoinMessage | AdminBroadcastMessage | EditUserMessage | ThrowRatPoisonMessage | SolveRatPoisonMessage; // Updated Message type
|
||||||
multiplier: 1.5,
|
|
||||||
clickBonus: 1,
|
import { UPGRADES as ALL_UPGRADES } from '../src/config/upgrades';
|
||||||
icon: '🖱️'
|
|
||||||
},
|
const UPGRADES = ALL_UPGRADES.filter(upgrade => !upgrade.oneTime);
|
||||||
{
|
const ONE_TIME_UPGRADES = ALL_UPGRADES.filter(upgrade => upgrade.oneTime);
|
||||||
id: 'autoClicker',
|
|
||||||
name: '🤖 Auto Clicker',
|
|
||||||
description: '+1 click per second',
|
|
||||||
baseCost: 50,
|
|
||||||
multiplier: 2,
|
|
||||||
autoClickRate: 1,
|
|
||||||
icon: '🤖'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'megaBonus',
|
|
||||||
name: '💎 Mega Bonus',
|
|
||||||
description: '+5 click power per purchase',
|
|
||||||
baseCost: 200,
|
|
||||||
multiplier: 2.5,
|
|
||||||
clickBonus: 5,
|
|
||||||
icon: '💎'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'hyperClicker',
|
|
||||||
name: '⚡ Hyper Clicker',
|
|
||||||
description: '+10 auto clicks per second',
|
|
||||||
baseCost: 1000,
|
|
||||||
multiplier: 3,
|
|
||||||
autoClickRate: 10,
|
|
||||||
icon: '⚡'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'quantumClicker',
|
|
||||||
name: '🌟 Quantum Clicker',
|
|
||||||
description: '+50 click power per purchase',
|
|
||||||
baseCost: 5000,
|
|
||||||
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: '/assets/bozo.png',
|
|
||||||
multiplier: 1.002,
|
|
||||||
rarity: 1.0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
level: 1,
|
|
||||||
imageSrc: '/assets/shoominion.png',
|
|
||||||
multiplier: 1.003,
|
|
||||||
rarity: 0.8,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
level: 5,
|
|
||||||
imageSrc: '/assets/codebug.gif',
|
|
||||||
multiplier: 1.005,
|
|
||||||
rarity: 0.6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
level: 10,
|
|
||||||
imageSrc: '/assets/lalan.gif',
|
|
||||||
multiplier: 1.007,
|
|
||||||
rarity: 0.4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
level: 15,
|
|
||||||
imageSrc: '/assets/neuro-neurosama.gif',
|
|
||||||
multiplier: 1.010,
|
|
||||||
rarity: 0.2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
level: 20,
|
|
||||||
imageSrc: '/assets/evil-neurosama.gif',
|
|
||||||
multiplier: 1.015,
|
|
||||||
rarity: 0.1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'news',
|
|
||||||
name: '📰 Bozo News Network',
|
|
||||||
description: 'Unlock the latest (fake) news headlines!',
|
|
||||||
baseCost: 50000, // A higher cost for a unique, one-time unlock
|
|
||||||
multiplier: 1, // No direct click/auto-click bonus
|
|
||||||
icon: '📰',
|
|
||||||
oneTime: true,
|
|
||||||
newsTitles: [
|
|
||||||
`Its Bo's birthday!`,
|
|
||||||
`Haru Urara looses another race, forced to eat pickle filled burger.`,
|
|
||||||
`Cupid crashes out once again, completely expected.`,
|
|
||||||
`Bo has been spotted in the wild, please do not approach.`,
|
|
||||||
`Bozo Clicker is now the number game in the world!`,
|
|
||||||
`Bo states that he did win the hidden gem vtuber award, it's just that he just didn't feel like it so Reya was really kind to step up.`,
|
|
||||||
`Reya has been spotted in the wild, please do not approach.`,
|
|
||||||
`What? Stop reading? I wrote this when you were playing roblox with cupid`,
|
|
||||||
`It's 1pm in the night and my mom is calling me to sleep, I don't want to sleep I'm making this stupid game`,
|
|
||||||
`FU BO`,
|
|
||||||
`When are you watching paint dry again?`,
|
|
||||||
`Insert Girls Kissing`,
|
|
||||||
`We luub bo`,
|
|
||||||
`MOOD DOWN`,
|
|
||||||
`All this clicking in this game wont give you your money back from those horse races`,
|
|
||||||
`UMAMUSUME PRETTY DERBY UPDATE : NEW SSSR IS ANNOUNCED - ITS [REDACTED]`,
|
|
||||||
`LEts' get maried bo`,
|
|
||||||
`My AI autocomplete on my code editor is shipping you with Reya for some reason. It literally completed Reya in this sentence`,
|
|
||||||
`DUDE`,
|
|
||||||
`There's a pipebomb in your DMs`,
|
|
||||||
`om`,
|
|
||||||
`You are the Glue to my life like how Tokai Teio is glue(stick) to a child's art project`,
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'secretVideo',
|
|
||||||
name: '🎬 Secret Video',
|
|
||||||
description: 'Unlock a secret video. Plays automatically!',
|
|
||||||
baseCost: 1000000, // Very expensive
|
|
||||||
multiplier: 1,
|
|
||||||
icon: '🎬',
|
|
||||||
oneTime: true,
|
|
||||||
youtubeId: 'ONzntmMFXGE' // The YouTube video ID
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const MILESTONES = [
|
const MILESTONES = [
|
||||||
{ threshold: 1000, id: 'first-thousand', background: 'rainbow', image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif' },
|
{ threshold: 1000, id: 'first-thousand', background: 'rainbow', image: 'https://media1.tenor.com/m/x8v1oNUOmg4AAAAd/spinning-rat-rat.gif' },
|
||||||
@@ -325,12 +209,14 @@ export default class GameServer implements Party.Server {
|
|||||||
clicks: 0,
|
clicks: 0,
|
||||||
lastSeen: Date.now(),
|
lastSeen: Date.now(),
|
||||||
bonusMultiplier: 1, // Initialize bonus multiplier
|
bonusMultiplier: 1, // Initialize bonus multiplier
|
||||||
upgrades: UPGRADES.reduce((acc, upgrade) => {
|
upgrades: ALL_UPGRADES.reduce((acc, upgrade) => {
|
||||||
acc[upgrade.id] = { owned: 0, cost: upgrade.baseCost };
|
acc[upgrade.id] = { owned: 0, cost: upgrade.baseCost };
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, { owned: number; cost: number }>),
|
}, {} as Record<string, { owned: number; cost: number }>),
|
||||||
clickMultiplier: 1,
|
clickMultiplier: 1,
|
||||||
autoClickRate: 0,
|
autoClickRate: 0,
|
||||||
|
ratPoisonChallenge: undefined,
|
||||||
|
ratPoisonImmunityUntil: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.gameState.users[currentUserId].lastSeen = Date.now();
|
this.gameState.users[currentUserId].lastSeen = Date.now();
|
||||||
@@ -383,6 +269,20 @@ export default class GameServer implements Party.Server {
|
|||||||
console.warn(`Unauthorized edit user attempt from ${currentUserId}.`);
|
console.warn(`Unauthorized edit user attempt from ${currentUserId}.`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'throw-rat-poison':
|
||||||
|
if (isAuthenticated) {
|
||||||
|
this.handleThrowRatPoison(data, currentUserId);
|
||||||
|
} else {
|
||||||
|
console.warn(`Unauthenticated throw-rat-poison from ${currentUserId} ignored.`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'solve-rat-poison':
|
||||||
|
if (isAuthenticated) {
|
||||||
|
this.handleSolveRatPoison(data, currentUserId);
|
||||||
|
} else {
|
||||||
|
console.warn(`Unauthenticated solve-rat-poison from ${currentUserId} ignored.`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.broadcast();
|
this.broadcast();
|
||||||
@@ -399,6 +299,127 @@ export default class GameServer implements Party.Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleThrowRatPoison(data: ThrowRatPoisonMessage, authenticatedUserId: string) {
|
||||||
|
const { targetUserId } = data;
|
||||||
|
const targetUserState = this.gameState.users[targetUserId];
|
||||||
|
const attackerUserState = this.gameState.users[authenticatedUserId];
|
||||||
|
|
||||||
|
if (!targetUserState || !attackerUserState || targetUserId === authenticatedUserId) {
|
||||||
|
return; // Cannot poison self or non-existent user
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (targetUserState.ratPoisonImmunityUntil && targetUserState.ratPoisonImmunityUntil > now) {
|
||||||
|
// Target is immune, notify attacker
|
||||||
|
const attackerConn = this.userConnections.get(authenticatedUserId);
|
||||||
|
if (attackerConn) {
|
||||||
|
attackerConn.send(JSON.stringify({
|
||||||
|
type: 'rat-poison-feedback',
|
||||||
|
message: `${targetUserState.name} is currently immune to rat poison!`
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get news headlines for challenge string
|
||||||
|
const newsUpgrade = ALL_UPGRADES.find(u => u.id === 'news');
|
||||||
|
const newsTitles = newsUpgrade?.newsTitles || [];
|
||||||
|
if (newsTitles.length === 0) {
|
||||||
|
console.warn('No news titles found for rat poison challenge.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const challengeString = newsTitles[Math.floor(Math.random() * newsTitles.length)];
|
||||||
|
const challengeDuration = 10 * 1000; // 10 seconds to solve
|
||||||
|
const expiresAt = now + challengeDuration;
|
||||||
|
|
||||||
|
targetUserState.ratPoisonChallenge = {
|
||||||
|
challengeString,
|
||||||
|
expiresAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Notify target user to start challenge
|
||||||
|
const targetConn = this.userConnections.get(targetUserId);
|
||||||
|
if (targetConn) {
|
||||||
|
targetConn.send(JSON.stringify({
|
||||||
|
type: 'rat-poison-challenge',
|
||||||
|
challenge: challengeString,
|
||||||
|
expiresAt,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a timeout for the challenge to expire if not solved
|
||||||
|
setTimeout(() => {
|
||||||
|
if (targetUserState.ratPoisonChallenge && targetUserState.ratPoisonChallenge.expiresAt === expiresAt) {
|
||||||
|
// Challenge not solved in time
|
||||||
|
this.applyRatPoisonPenalty(targetUserId);
|
||||||
|
this.broadcast();
|
||||||
|
}
|
||||||
|
}, challengeDuration + 500); // Give a small buffer
|
||||||
|
|
||||||
|
this.broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSolveRatPoison(data: SolveRatPoisonMessage, authenticatedUserId: string) {
|
||||||
|
const userState = this.gameState.users[authenticatedUserId];
|
||||||
|
if (!userState || !userState.ratPoisonChallenge) {
|
||||||
|
return; // No active challenge
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (now > userState.ratPoisonChallenge.expiresAt) {
|
||||||
|
// Challenge expired
|
||||||
|
this.applyRatPoisonPenalty(authenticatedUserId);
|
||||||
|
} else if (data.challengeString === userState.ratPoisonChallenge.challengeString) {
|
||||||
|
// Challenge solved successfully
|
||||||
|
userState.ratPoisonImmunityUntil = now + (2 * 60 * 1000); // 2 minutes immunity
|
||||||
|
const userConn = this.userConnections.get(authenticatedUserId);
|
||||||
|
if (userConn) {
|
||||||
|
userConn.send(JSON.stringify({
|
||||||
|
type: 'rat-poison-result',
|
||||||
|
success: true,
|
||||||
|
message: 'Challenge solved! You are immune for 2 minutes.'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Challenge failed (wrong string)
|
||||||
|
this.applyRatPoisonPenalty(authenticatedUserId);
|
||||||
|
const userConn = this.userConnections.get(authenticatedUserId);
|
||||||
|
if (userConn) {
|
||||||
|
userConn.send(JSON.stringify({
|
||||||
|
type: 'rat-poison-result',
|
||||||
|
success: false,
|
||||||
|
message: 'Incorrect string! You lost clicks and are immune for 20 seconds.'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userState.ratPoisonChallenge = undefined; // Clear the challenge
|
||||||
|
this.broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyRatPoisonPenalty(userId: string) {
|
||||||
|
const userState = this.gameState.users[userId];
|
||||||
|
if (!userState) return;
|
||||||
|
|
||||||
|
const clicksLost = Math.floor(userState.clicks * 0.30);
|
||||||
|
userState.clicks -= clicksLost;
|
||||||
|
userState.ratPoisonImmunityUntil = Date.now() + (20 * 1000); // 20 seconds immunity
|
||||||
|
|
||||||
|
const userConn = this.userConnections.get(userId);
|
||||||
|
if (userConn) {
|
||||||
|
userConn.send(JSON.stringify({
|
||||||
|
type: 'rat-poison-result',
|
||||||
|
success: false,
|
||||||
|
message: `You failed the challenge and lost ${clicksLost} clicks! You are immune for 20 seconds.`
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define a minimum interval between clicks (e.g., 100ms for 10 clicks/second)
|
||||||
|
// This helps prevent simple auto-clicker scripts by rate-limiting server-side.
|
||||||
|
private readonly MIN_CLICK_INTERVAL = 50; // milliseconds
|
||||||
|
|
||||||
// handleUserJoin is now fully integrated into onMessage and can be removed or simplified.
|
// handleUserJoin is now fully integrated into onMessage and can be removed or simplified.
|
||||||
// Removing it as its logic is now directly in onMessage.
|
// Removing it as its logic is now directly in onMessage.
|
||||||
// handleUserJoin(data: UserJoinMessage) {
|
// handleUserJoin(data: UserJoinMessage) {
|
||||||
@@ -407,10 +428,6 @@ export default class GameServer implements Party.Server {
|
|||||||
// // No explicit action needed here beyond what onMessage does.
|
// // No explicit action needed here beyond what onMessage does.
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Define a minimum interval between clicks (e.g., 100ms for 10 clicks/second)
|
|
||||||
// This helps prevent simple auto-clicker scripts by rate-limiting server-side.
|
|
||||||
private readonly MIN_CLICK_INTERVAL = 50; // milliseconds
|
|
||||||
|
|
||||||
handleClick(data: ClickMessage, authenticatedUserId: string) {
|
handleClick(data: ClickMessage, authenticatedUserId: string) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const userState = this.gameState.users[authenticatedUserId];
|
const userState = this.gameState.users[authenticatedUserId];
|
||||||
|
|||||||
60
src/App.tsx
60
src/App.tsx
@@ -16,21 +16,24 @@ import { useLocation, Link } from 'wouter'; // Import wouter hooks
|
|||||||
import AdminPage from './components/AdminPage'; // Import AdminPage
|
import AdminPage from './components/AdminPage'; // Import AdminPage
|
||||||
import { NewsMarquee } from './components/NewsMarquee'; // Import NewsMarquee
|
import { NewsMarquee } from './components/NewsMarquee'; // Import NewsMarquee
|
||||||
import YouTube from 'react-youtube'; // Import YouTube component
|
import YouTube from 'react-youtube'; // Import YouTube component
|
||||||
|
import { RatPoisonChallenge } from './components/RatPoisonChallenge'; // Import RatPoisonChallenge
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { isSignedIn, isLoaded, userId: clerkUserId } = useAuth(); // Get clerkUserId from useAuth
|
const { isSignedIn, isLoaded, userId: clerkUserId } = useAuth(); // Get clerkUserId from useAuth
|
||||||
const { gameState, sendClick, purchaseUpgrade, userId, sendMascotClickBonus, lastMessage } = usePartyKit(); // Renamed sendShoominionClickBonus
|
const { gameState, sendClick, purchaseUpgrade, userId, sendMascotClickBonus, lastMessage, sendSolveRatPoison } = 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 [mascotEntities, setMascotEntities] = useState<ClickableMascotType[]>([]);
|
||||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const timeoutRef = useRef<number | null>(null);
|
||||||
const gameStateRef = useRef(gameState); // Ref to hold the latest gameState
|
const gameStateRef = useRef(gameState); // Ref to hold the latest gameState
|
||||||
|
|
||||||
const [location, setLocation] = useLocation(); // Get current location from wouter
|
const [location] = useLocation(); // Get current location from wouter
|
||||||
const [adminBroadcastMessage, setAdminBroadcastMessage] = useState<string | null>(null); // New state for admin messages
|
const [adminBroadcastMessage, setAdminBroadcastMessage] = useState<string | null>(null); // New state for admin messages
|
||||||
const [showSecretVideo, setShowSecretVideo] = useState(false); // New state for secret video
|
const [showSecretVideo, setShowSecretVideo] = useState(false); // New state for secret video
|
||||||
|
const [ratPoisonChallenge, setRatPoisonChallenge] = useState<{ challenge: string; expiresAt: number } | null>(null);
|
||||||
|
const [ratPoisonResult, setRatPoisonResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||||
|
|
||||||
const userState = isSignedIn && userId ? gameState.users[userId] : null;
|
const userState = isSignedIn && userId ? gameState?.users[userId] : null;
|
||||||
|
|
||||||
// Admin user ID from .env
|
// Admin user ID from .env
|
||||||
const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID;
|
const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID;
|
||||||
@@ -59,7 +62,8 @@ function App() {
|
|||||||
|
|
||||||
// Effect for secret video
|
// Effect for secret video
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userState?.upgrades?.['secretVideo']?.owned > 0) {
|
if (!userState) return; // Ensure userState is not null/undefined
|
||||||
|
if (userState.upgrades?.['secretVideo']?.owned > 0) {
|
||||||
setShowSecretVideo(true);
|
setShowSecretVideo(true);
|
||||||
}
|
}
|
||||||
}, [userState]);
|
}, [userState]);
|
||||||
@@ -67,11 +71,23 @@ function App() {
|
|||||||
// Effect for receiving admin broadcast messages
|
// Effect for receiving admin broadcast messages
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lastMessage) {
|
if (lastMessage) {
|
||||||
|
console.log('App.tsx: Received lastMessage:', lastMessage); // Log the raw message
|
||||||
try {
|
try {
|
||||||
const parsedMessage = JSON.parse(lastMessage);
|
const parsedMessage = JSON.parse(lastMessage);
|
||||||
if (parsedMessage.type === 'admin-message') {
|
if (parsedMessage.type === 'admin-message') {
|
||||||
setAdminBroadcastMessage(parsedMessage.message);
|
setAdminBroadcastMessage(parsedMessage.message);
|
||||||
setTimeout(() => setAdminBroadcastMessage(null), 7000); // Message disappears after 7 seconds
|
setTimeout(() => setAdminBroadcastMessage(null), 7000); // Message disappears after 7 seconds
|
||||||
|
} else if (parsedMessage.type === 'rat-poison-challenge') {
|
||||||
|
console.log('App.tsx: Received rat-poison-challenge:', parsedMessage); // Log challenge message
|
||||||
|
setRatPoisonChallenge({ challenge: parsedMessage.challenge, expiresAt: parsedMessage.expiresAt });
|
||||||
|
console.log('App.tsx: ratPoisonChallenge state set to:', { challenge: parsedMessage.challenge, expiresAt: parsedMessage.expiresAt }); // Log state after setting
|
||||||
|
} else if (parsedMessage.type === 'rat-poison-result') {
|
||||||
|
setRatPoisonResult({ success: parsedMessage.success, message: parsedMessage.message });
|
||||||
|
setTimeout(() => setRatPoisonResult(null), 5000); // Result message disappears after 5 seconds
|
||||||
|
} else if (parsedMessage.type === 'rat-poison-feedback') {
|
||||||
|
// Display feedback to the attacker
|
||||||
|
setAdminBroadcastMessage(parsedMessage.message); // Re-using admin message display for simplicity
|
||||||
|
setTimeout(() => setAdminBroadcastMessage(null), 5000);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to parse last message in App.tsx:', error);
|
console.error('Failed to parse last message in App.tsx:', error);
|
||||||
@@ -175,6 +191,11 @@ function App() {
|
|||||||
setMascotEntities((prev) => prev.filter((m) => m.id !== id)); // Update state variable
|
setMascotEntities((prev) => prev.filter((m) => m.id !== id)); // Update state variable
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSolveRatPoison = (solvedString: string) => {
|
||||||
|
sendSolveRatPoison(solvedString);
|
||||||
|
setRatPoisonChallenge(null);
|
||||||
|
};
|
||||||
|
|
||||||
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">
|
||||||
@@ -231,11 +252,24 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* News Marquee */}
|
{/* Rat Poison Result Message */}
|
||||||
{userState?.upgrades['news']?.owned > 0 && UPGRADES.find(u => u.id === 'news')?.newsTitles && (
|
{ratPoisonResult && (
|
||||||
<NewsMarquee titles={UPGRADES.find(u => u.id === 'news')!.newsTitles!} />
|
<div className="fixed top-40 left-0 right-0 z-50 flex justify-center pt-8">
|
||||||
|
<div className={`px-8 py-4 rounded-full text-2xl font-bold shadow-2xl animate-pulse border-4 border-white ${ratPoisonResult.success ? 'bg-green-600' : 'bg-red-600'}`}>
|
||||||
|
{ratPoisonResult.message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* News Marquee */}
|
||||||
|
{(() => {
|
||||||
|
const newsUpgrade = UPGRADES.find(u => u.id === 'news');
|
||||||
|
if (userState?.upgrades['news']?.owned > 0 && newsUpgrade?.newsTitles) {
|
||||||
|
return <NewsMarquee titles={(newsUpgrade as Upgrade).newsTitles!} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
|
|
||||||
<div className="container mx-auto px-4 py-8 relative z-10">
|
<div className="container mx-auto px-4 py-8 relative z-10">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
@@ -314,6 +348,16 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Rat Poison Challenge Modal */}
|
||||||
|
{ratPoisonChallenge && (
|
||||||
|
<RatPoisonChallenge
|
||||||
|
challengeString={ratPoisonChallenge.challenge}
|
||||||
|
expiresAt={ratPoisonChallenge.expiresAt}
|
||||||
|
onSolve={handleSolveRatPoison}
|
||||||
|
onClose={() => setRatPoisonChallenge(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Sparkle Effects */}
|
{/* Sparkle Effects */}
|
||||||
<div className="fixed inset-0 pointer-events-none z-20">
|
<div className="fixed inset-0 pointer-events-none z-20">
|
||||||
{[...Array(10)].map((_, i) => (
|
{[...Array(10)].map((_, i) => (
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const AdminPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleEditUser = () => {
|
const handleEditUser = () => {
|
||||||
if (selectedUser) {
|
if (selectedUser) {
|
||||||
editUser(selectedUser.id, clicks, autoClickRate, upgrades);
|
editUser(selectedUser.id, clicks, upgrades); // Removed autoClickRate
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -134,9 +134,12 @@ const AdminPage: React.FC = () => {
|
|||||||
key={id}
|
key={id}
|
||||||
className={`cursor-pointer p-3 rounded-md mb-2 ${selectedUser?.id === id ? 'bg-blue-500' : 'bg-gray-600'}`}
|
className={`cursor-pointer p-3 rounded-md mb-2 ${selectedUser?.id === id ? 'bg-blue-500' : 'bg-gray-600'}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedUser({ id, ...user });
|
const clickedUser = gameState?.users[id]; // Get the user from gameState
|
||||||
setClicks(user.clicks);
|
if (clickedUser) { // Check if it's not undefined
|
||||||
setUpgrades(user.upgrades);
|
setSelectedUser({ id, ...clickedUser });
|
||||||
|
setClicks(clickedUser.clicks);
|
||||||
|
setUpgrades(clickedUser.upgrades);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{user.name}
|
{user.name}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { GameState } from '../types';
|
import { GameState } from '../types';
|
||||||
|
import { usePartyKit } from '../hooks/usePartyKit';
|
||||||
|
|
||||||
interface LeaderboardProps {
|
interface LeaderboardProps {
|
||||||
gameState: GameState;
|
gameState: GameState;
|
||||||
@@ -7,6 +8,7 @@ interface LeaderboardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Leaderboard({ gameState, currentUserId }: LeaderboardProps) {
|
export function Leaderboard({ gameState, currentUserId }: LeaderboardProps) {
|
||||||
|
const { throwRatPoison } = usePartyKit();
|
||||||
const sortedUsers = Object.entries(gameState.users)
|
const sortedUsers = Object.entries(gameState.users)
|
||||||
.sort(([, a], [, b]) => b.clicks - a.clicks)
|
.sort(([, a], [, b]) => b.clicks - a.clicks)
|
||||||
.slice(0, 10);
|
.slice(0, 10);
|
||||||
@@ -21,6 +23,7 @@ export function Leaderboard({ gameState, currentUserId }: LeaderboardProps) {
|
|||||||
{sortedUsers.map(([userId, user], index) => {
|
{sortedUsers.map(([userId, user], index) => {
|
||||||
const isCurrentUser = userId === currentUserId;
|
const isCurrentUser = userId === currentUserId;
|
||||||
const isTopThree = index < 3;
|
const isTopThree = index < 3;
|
||||||
|
const isImmune = user.ratPoisonImmunityUntil && user.ratPoisonImmunityUntil > Date.now();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -42,9 +45,24 @@ export function Leaderboard({ gameState, currentUserId }: LeaderboardProps) {
|
|||||||
{user.name} {isCurrentUser ? '(You)' : ''}
|
{user.name} {isCurrentUser ? '(You)' : ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
<span className={`text-xl font-bold ${isCurrentUser ? 'text-black' : 'text-yellow-300'}`}>
|
<span className={`text-xl font-bold ${isCurrentUser ? 'text-black' : 'text-yellow-300'}`}>
|
||||||
{user.clicks.toLocaleString()}
|
{user.clicks.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
|
{!isCurrentUser && (
|
||||||
|
<button
|
||||||
|
onClick={() => throwRatPoison(userId)}
|
||||||
|
disabled={isImmune}
|
||||||
|
className={`px-3 py-1 rounded-md text-sm font-bold transition-colors duration-200 ${
|
||||||
|
isImmune
|
||||||
|
? 'bg-gray-500 text-gray-300 cursor-not-allowed'
|
||||||
|
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isImmune ? 'Immune' : 'Throw Poison'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
87
src/components/RatPoisonChallenge.tsx
Normal file
87
src/components/RatPoisonChallenge.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
interface RatPoisonChallengeProps {
|
||||||
|
challengeString: string;
|
||||||
|
expiresAt: number;
|
||||||
|
onSolve: (solvedString: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RatPoisonChallenge({ challengeString, expiresAt, onSolve, onClose }: RatPoisonChallengeProps) {
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [timeLeft, setTimeLeft] = useState(0);
|
||||||
|
const [initialDuration, setInitialDuration] = useState(0);
|
||||||
|
const timerRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
const duration = expiresAt - now;
|
||||||
|
setInitialDuration(Math.ceil(duration / 1000));
|
||||||
|
|
||||||
|
const calculateTimeLeft = () => {
|
||||||
|
const remaining = Math.max(0, expiresAt - Date.now());
|
||||||
|
setTimeLeft(Math.ceil(remaining / 1000));
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateTimeLeft(); // Initial calculation
|
||||||
|
timerRef.current = setInterval(calculateTimeLeft, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [expiresAt]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (timeLeft === 0 && Date.now() >= expiresAt) {
|
||||||
|
// Challenge expired, automatically close or handle failure
|
||||||
|
onSolve(''); // Send empty string to indicate failure due to timeout
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, [timeLeft, expiresAt, onSolve, onClose]);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSolve(inputValue);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const progressPercentage = initialDuration > 0 ? (timeLeft / initialDuration) * 100 : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 pointer-events-auto">
|
||||||
|
<div className="bg-gradient-to-br from-purple-700 to-indigo-900 p-8 rounded-lg shadow-2xl border-4 border-pink-500 text-white text-center max-w-md mx-auto">
|
||||||
|
<h2 className="text-3xl font-bold mb-4 text-yellow-300">RAT POISON CHALLENGE!</h2>
|
||||||
|
<p className="text-lg mb-6">Type the following string:</p>
|
||||||
|
<p className="text-2xl font-mono bg-gray-800 p-4 rounded-md break-all mb-6 border-2 border-gray-600">{challengeString}</p>
|
||||||
|
|
||||||
|
<div className="w-full bg-gray-700 rounded-full h-4 mb-4">
|
||||||
|
<div
|
||||||
|
className="bg-green-500 h-4 rounded-full transition-all duration-1000 ease-linear"
|
||||||
|
style={{ width: `${progressPercentage}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
className="p-3 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Type here..."
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-md transition duration-300 ease-in-out transform hover:scale-105"
|
||||||
|
disabled={timeLeft === 0}
|
||||||
|
>
|
||||||
|
SUBMIT ({timeLeft}s)
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{timeLeft === 0 && <p className="text-red-400 mt-4">Time's up!</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ const getMascotName = (imageSrc: string): string => {
|
|||||||
.join(' ');
|
.join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
export function UpgradeShop({ gameState, userState, onPurchase }: UpgradeShopProps) { // Changed from userClicks
|
export function UpgradeShop({ userState, onPurchase }: UpgradeShopProps) { // Changed from userClicks
|
||||||
if (!userState) {
|
if (!userState) {
|
||||||
return null; // Or a loading/signed-out state
|
return null; // Or a loading/signed-out state
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function usePartyKit() {
|
|||||||
return () => {
|
return () => {
|
||||||
ws.close();
|
ws.close();
|
||||||
};
|
};
|
||||||
}, [isLoaded, guestId, guestName]); // Removed isSignedIn, user, getToken from dependencies
|
}, [isLoaded, isSignedIn, user, getToken, guestId, guestName]);
|
||||||
|
|
||||||
const sendClick = useCallback(async () => {
|
const sendClick = useCallback(async () => {
|
||||||
if (socket && isSignedIn && user) {
|
if (socket && isSignedIn && user) {
|
||||||
@@ -120,7 +120,7 @@ export function usePartyKit() {
|
|||||||
const currentUserId = isSignedIn && user ? user.id : undefined;
|
const currentUserId = isSignedIn && user ? user.id : undefined;
|
||||||
const currentUserName = isSignedIn && user ? (user.username || user.fullName || user.emailAddresses[0].emailAddress) : undefined;
|
const currentUserName = isSignedIn && user ? (user.username || user.fullName || user.emailAddresses[0].emailAddress) : undefined;
|
||||||
|
|
||||||
const editUser = useCallback(async (targetUserId: string, clicks: number, autoClickRate: number, upgrades: any) => {
|
const editUser = useCallback(async (targetUserId: string, clicks: number, upgrades: Record<string, { owned: number; cost: 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({
|
||||||
@@ -130,12 +130,37 @@ export function usePartyKit() {
|
|||||||
userName: user.username || user.fullName || user.emailAddresses[0].emailAddress,
|
userName: user.username || user.fullName || user.emailAddresses[0].emailAddress,
|
||||||
targetUserId,
|
targetUserId,
|
||||||
clicks,
|
clicks,
|
||||||
autoClickRate,
|
|
||||||
upgrades
|
upgrades
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [socket, isSignedIn, user, getToken]);
|
}, [socket, isSignedIn, user, getToken]);
|
||||||
|
|
||||||
|
const throwRatPoison = useCallback(async (targetUserId: string) => {
|
||||||
|
if (socket && isSignedIn && user) {
|
||||||
|
const token = await getToken();
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
type: 'throw-rat-poison',
|
||||||
|
userId: user.id,
|
||||||
|
userName: user.username || user.fullName || user.emailAddresses[0].emailAddress,
|
||||||
|
token,
|
||||||
|
targetUserId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [socket, isSignedIn, user, getToken]);
|
||||||
|
|
||||||
|
const sendSolveRatPoison = useCallback(async (challengeString: string) => {
|
||||||
|
if (socket && isSignedIn && user) {
|
||||||
|
const token = await getToken();
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
type: 'solve-rat-poison',
|
||||||
|
userId: user.id,
|
||||||
|
userName: user.username || user.fullName || user.emailAddresses[0].emailAddress,
|
||||||
|
token,
|
||||||
|
challengeString,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [socket, isSignedIn, user, getToken]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
gameState,
|
gameState,
|
||||||
sendClick,
|
sendClick,
|
||||||
@@ -147,5 +172,7 @@ export function usePartyKit() {
|
|||||||
userName: currentUserName,
|
userName: currentUserName,
|
||||||
getToken, // Expose getToken
|
getToken, // Expose getToken
|
||||||
editUser,
|
editUser,
|
||||||
|
throwRatPoison,
|
||||||
|
sendSolveRatPoison,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ export interface UserState {
|
|||||||
upgrades: Record<string, { owned: number; cost: number }>;
|
upgrades: Record<string, { owned: number; cost: number }>;
|
||||||
clickMultiplier: number;
|
clickMultiplier: number;
|
||||||
autoClickRate: number;
|
autoClickRate: number;
|
||||||
|
ratPoisonChallenge?: {
|
||||||
|
challengeString: string;
|
||||||
|
expiresAt: number;
|
||||||
|
};
|
||||||
|
ratPoisonImmunityUntil?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GameState {
|
export interface GameState {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "./tsconfig.app.json" },
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
|||||||
Reference in New Issue
Block a user