"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { Shield, Mail, X, AlertTriangle, MailCheck, RotateCcw } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; const GAME_WIDTH = 400; const GAME_HEIGHT = 520; const INBOX_Y = GAME_HEIGHT - 40; const SPAWN_INTERVAL_START = 900; const SPAWN_INTERVAL_MIN = 340; const GAME_DURATION = 30; const ENEMY_SPEED_START = 1.2; const ENEMY_SPEED_INCREASE = 0.04; const MAX_MISSES = 3; interface Enemy { id: number; x: number; y: number; speed: number; type: "spam" | "phishing" | "legit"; } type GameState = "idle" | "playing" | "over"; export function SpamSiegeGame({ onClose }: { onClose: () => void }) { const [gameState, setGameState] = useState("idle"); const [enemies, setEnemies] = useState([]); const [score, setScore] = useState(0); const [timeLeft, setTimeLeft] = useState(GAME_DURATION); const [misses, setMisses] = useState(0); const [survived, setSurvived] = useState(false); const nextId = useRef(0); const animFrameRef = useRef(0); const lastTimeRef = useRef(0); const spawnTimerRef = useRef(0); const gameStateRef = useRef("idle"); const elapsedRef = useRef(0); const clickedRef = useRef(new Set()); const enemiesRef = useRef([]); const missesRef = useRef(0); const scoreRef = useRef(0); useEffect(() => { gameStateRef.current = gameState; }, [gameState]); const endGame = useCallback((didSurvive: boolean) => { setSurvived(didSurvive); setGameState("over"); }, []); const startGame = useCallback(() => { setGameState("playing"); setEnemies([]); setScore(0); setTimeLeft(GAME_DURATION); setMisses(0); setSurvived(false); nextId.current = 0; spawnTimerRef.current = 0; elapsedRef.current = 0; clickedRef.current = new Set(); enemiesRef.current = []; missesRef.current = 0; scoreRef.current = 0; lastTimeRef.current = performance.now(); }, []); const spawnEnemy = useCallback(() => { const id = nextId.current++; const rand = Math.random(); const type = rand > 0.75 ? "legit" : rand > 0.45 ? "phishing" : "spam"; const x = 20 + Math.random() * (GAME_WIDTH - 60); const speed = ENEMY_SPEED_START + (elapsedRef.current / 1000) * ENEMY_SPEED_INCREASE; enemiesRef.current = [...enemiesRef.current, { id, x, y: -32, speed, type }]; setEnemies(enemiesRef.current); }, []); const handleClick = useCallback( (ev: React.MouseEvent, enemy: Enemy) => { ev.stopPropagation(); if (clickedRef.current.has(enemy.id)) return; clickedRef.current.add(enemy.id); enemiesRef.current = enemiesRef.current.filter((e) => e.id !== enemy.id); setEnemies(enemiesRef.current); if (enemy.type === "legit") { missesRef.current += 1; setMisses(missesRef.current); scoreRef.current = Math.max(0, scoreRef.current - 15); setScore(scoreRef.current); if (missesRef.current >= MAX_MISSES) endGame(false); } else { scoreRef.current += enemy.type === "phishing" ? 15 : 10; setScore(scoreRef.current); } }, [endGame] ); useEffect(() => { if (gameState !== "playing") return; const tick = (now: number) => { if (gameStateRef.current !== "playing") return; const dt = now - lastTimeRef.current; lastTimeRef.current = now; elapsedRef.current += dt; const newTimeLeft = GAME_DURATION - Math.floor(elapsedRef.current / 1000); setTimeLeft(Math.max(0, newTimeLeft)); if (newTimeLeft <= 0) { endGame(true); return; } spawnTimerRef.current += dt; const spawnInterval = Math.max( SPAWN_INTERVAL_MIN, SPAWN_INTERVAL_START - (elapsedRef.current / 1000) * 35 ); if (spawnTimerRef.current >= spawnInterval) { spawnTimerRef.current = 0; spawnEnemy(); } const nextEnemies: Enemy[] = []; let missed = 0; let scoreDelta = 0; for (const e of enemiesRef.current) { const ny = e.y + e.speed * (dt / 16); if (ny >= INBOX_Y) { if (e.type === "legit") scoreDelta += 5; else missed++; } else { nextEnemies.push({ ...e, y: ny }); } } enemiesRef.current = nextEnemies; setEnemies(nextEnemies); if (scoreDelta > 0) { scoreRef.current += scoreDelta; setScore(scoreRef.current); } if (missed > 0) { missesRef.current += missed; setMisses(missesRef.current); if (missesRef.current >= MAX_MISSES) { endGame(false); return; } } animFrameRef.current = requestAnimationFrame(tick); }; animFrameRef.current = requestAnimationFrame(tick); return () => cancelAnimationFrame(animFrameRef.current); }, [gameState, spawnEnemy, endGame]); return (
e.stopPropagation()} >
Spam Siege
Score {score} Time {timeLeft}s
Misses{" "} = MAX_MISSES - 1 ? "text-destructive" : "text-foreground" )} > {misses}/{MAX_MISSES}
Inbox
{enemies.map((e) => { const variant = e.type === "phishing" ? "text-warning border-warning/40 bg-warning/10 hover:bg-warning/20" : e.type === "legit" ? "text-success border-success/40 bg-success/10 hover:bg-success/20" : "text-destructive border-destructive/40 bg-destructive/10 hover:bg-destructive/20"; const Icon = e.type === "phishing" ? AlertTriangle : e.type === "legit" ? MailCheck : Mail; return ( ); })} {gameState === "idle" && (

Spam Siege

Click spam and phishing before they hit your inbox. Don't block legitimate mail. Three misses and it's over.

Spam Phishing Legit
)} {gameState === "over" && (

{survived ? "Inbox held" : "Inbox overrun"}

Final score{" "} {score}

)}
); }