diff --git a/frontend/components/about/InteractiveGame.tsx b/frontend/components/about/InteractiveGame.tsx
index e36be3ed..47f34c70 100644
--- a/frontend/components/about/InteractiveGame.tsx
+++ b/frontend/components/about/InteractiveGame.tsx
@@ -2,62 +2,192 @@
import { useState, useEffect, useRef, useCallback } from "react"
import { motion, AnimatePresence } from "framer-motion"
-import { Heart, Bug, Play, X, RotateCcw, Zap } from "lucide-react"
+import { Heart, Bug, Play, X, RotateCcw, Zap, Skull, Rabbit } from "lucide-react"
+
+const GAME_WIDTH_DESKTOP = 540
+const GAME_WIDTH_MOBILE = 320
+const IDLE_WIDTH_DESKTOP = 280
+const IDLE_WIDTH_MOBILE = 240
+
+const GROUND_Y_DESKTOP = 32
+const GROUND_Y_MOBILE = 24
+const PLAYER_X_DESKTOP = 64
+const PLAYER_X_MOBILE = 40
+const PLAYER_SIZE_DESKTOP = 40
+const PLAYER_SIZE_MOBILE = 32
+const JUMP_HEIGHT_DESKTOP = 90
+const JUMP_HEIGHT_MOBILE = 70
+const JUMP_DURATION = 500
+
+type ObstacleType = 'ground' | 'flying' | 'fast' | 'tall'
+
+interface Obstacle {
+ type: ObstacleType
+ x: number
+ size: number
+ heightOffset: number
+ speedMultiplier: number
+}
+
+const OBSTACLE_CONFIGS: Record
> = {
+ ground: { type: 'ground', size: 32, heightOffset: 0, speedMultiplier: 1 },
+ flying: { type: 'flying', size: 28, heightOffset: 45, speedMultiplier: 1 },
+ fast: { type: 'fast', size: 24, heightOffset: 0, speedMultiplier: 1.2 },
+ tall: { type: 'tall', size: 38, heightOffset: 0, speedMultiplier: 0.95 },
+}
+
+const LEVEL_THRESHOLDS = [
+ { score: 0, types: ['ground'] as ObstacleType[], name: 'Level 1', baseSpeed: 3.5 },
+ { score: 8, types: ['ground', 'fast'] as ObstacleType[], name: 'Level 2', baseSpeed: 4 },
+ { score: 18, types: ['ground', 'fast', 'flying'] as ObstacleType[], name: 'Level 3', baseSpeed: 4.5 },
+ { score: 30, types: ['ground', 'fast', 'flying', 'tall'] as ObstacleType[], name: 'Level 4', baseSpeed: 5 },
+]
+
+function getBaseSpeed(score: number): number {
+ let speed = 3.5
+ for (const level of LEVEL_THRESHOLDS) {
+ if (score >= level.score) {
+ speed = level.baseSpeed
+ }
+ }
+ return speed
+}
+
+function getAvailableTypes(score: number): ObstacleType[] {
+ let types: ObstacleType[] = ['ground']
+ for (const level of LEVEL_THRESHOLDS) {
+ if (score >= level.score) {
+ types = level.types
+ }
+ }
+ return types
+}
+
+function getCurrentLevel(score: number): string {
+ let name = 'Level 1'
+ for (const level of LEVEL_THRESHOLDS) {
+ if (score >= level.score) {
+ name = level.name
+ }
+ }
+ return name
+}
+
+function getRandomObstacle(score: number, gameWidth: number): Obstacle {
+ const types = getAvailableTypes(score)
+ let selectedType: ObstacleType
+ const rand = Math.random()
+
+ if (types.length === 1) {
+ selectedType = 'ground'
+ } else if (types.length === 2) {
+ selectedType = rand < 0.7 ? 'ground' : types[1]
+ } else if (types.length === 3) {
+ if (rand < 0.5) selectedType = 'ground'
+ else if (rand < 0.75) selectedType = types[1]
+ else selectedType = types[2]
+ } else {
+ if (rand < 0.4) selectedType = 'ground'
+ else if (rand < 0.6) selectedType = types[1]
+ else if (rand < 0.8) selectedType = types[2]
+ else selectedType = types[3]
+ }
+
+ return {
+ ...OBSTACLE_CONFIGS[selectedType],
+ x: gameWidth + 40,
+ }
+}
export function InteractiveGame() {
const [mode, setMode] = useState<'idle' | 'preview' | 'playing'>('idle')
const [gameOver, setGameOver] = useState(false)
const [score, setScore] = useState(0)
-
- const [highScore, setHighScore] = useState(() => {
- if (typeof window !== 'undefined') {
- const saved = localStorage.getItem('devlovers_highscore')
- return saved ? parseInt(saved, 10) : 0
- }
- return 0
- })
+ const [levelUpFlash, setLevelUpFlash] = useState(false)
+ const [isMobile, setIsMobile] = useState(false)
- const [isJumping, setIsJumping] = useState(false)
- const [gameSpeed, setGameSpeed] = useState(1.8)
- const [resetKey, setResetKey] = useState(0)
+ const gameWidth = isMobile ? GAME_WIDTH_MOBILE : GAME_WIDTH_DESKTOP
+ const idleWidth = isMobile ? IDLE_WIDTH_MOBILE : IDLE_WIDTH_DESKTOP
+ const playerX = isMobile ? PLAYER_X_MOBILE : PLAYER_X_DESKTOP
+ const groundY = isMobile ? GROUND_Y_MOBILE : GROUND_Y_DESKTOP
+ const playerSize = isMobile ? PLAYER_SIZE_MOBILE : PLAYER_SIZE_DESKTOP
+ const jumpHeight = isMobile ? JUMP_HEIGHT_MOBILE : JUMP_HEIGHT_DESKTOP
+ const obstacleEndX = -60
- const pillRef = useRef(null)
+ const [highScore, setHighScore] = useState(0)
+
+ useEffect(() => {
+ const checkMobile = () => setIsMobile(window.innerWidth < 640)
+ checkMobile()
+ window.addEventListener('resize', checkMobile)
+ return () => window.removeEventListener('resize', checkMobile)
+ }, [])
+
+ useEffect(() => {
+ const saved = localStorage.getItem('devlovers_highscore')
+ if (saved) setHighScore(parseInt(saved, 10))
+ }, [])
+
+ const [playerY, setPlayerY] = useState(0)
+ const [obstacle, setObstacle] = useState(() => getRandomObstacle(0, GAME_WIDTH_DESKTOP))
+ const [gameSpeed, setGameSpeed] = useState(3.5)
+
+ const isJumpingRef = useRef(false)
+ const jumpStartTime = useRef(0)
+ const lastTimeRef = useRef(0)
const requestRef = useRef(null)
- const playerRef = useRef(null)
- const obstacleRef = useRef(null)
+ const pillRef = useRef(null)
+ const hasPassedRef = useRef(false)
+ const prevLevelRef = useRef('Level 1')
const exitGame = useCallback(() => {
setMode('idle')
setGameOver(false)
setScore(0)
- setGameSpeed(1.8)
+ setPlayerY(0)
+ setObstacle(getRandomObstacle(0, gameWidth))
+ setGameSpeed(3.5)
+ isJumpingRef.current = false
+ hasPassedRef.current = false
+ prevLevelRef.current = 'Level 1'
if (requestRef.current !== null) cancelAnimationFrame(requestRef.current)
- }, [])
+ }, [gameWidth])
const handleRetry = useCallback(() => {
setGameOver(false)
setScore(0)
- setGameSpeed(1.8)
- setResetKey(prev => prev + 1)
- }, [])
+ setPlayerY(0)
+ setObstacle(getRandomObstacle(0, gameWidth))
+ setGameSpeed(3.5)
+ isJumpingRef.current = false
+ hasPassedRef.current = false
+ lastTimeRef.current = 0
+ prevLevelRef.current = 'Level 1'
+ }, [gameWidth])
- const handleGameOver = useCallback(() => {
+ const handleGameOver = useCallback((finalScore: number) => {
setGameOver(true)
- setHighScore(prev => {
- const newHigh = Math.max(prev, score)
- if (typeof window !== 'undefined') {
- localStorage.setItem('devlovers_highscore', newHigh.toString())
- }
- return newHigh
- })
- }, [score])
+ if (finalScore > highScore) {
+ setHighScore(finalScore)
+ localStorage.setItem('devlovers_highscore', finalScore.toString())
+ }
+ }, [highScore])
const jump = useCallback(() => {
- if (!isJumping && mode === 'playing' && !gameOver) {
- setIsJumping(true)
- setTimeout(() => setIsJumping(false), 550)
+ if (!isJumpingRef.current && mode === 'playing' && !gameOver) {
+ isJumpingRef.current = true
+ jumpStartTime.current = performance.now()
}
- }, [isJumping, mode, gameOver])
+ }, [mode, gameOver])
+
+ useEffect(() => {
+ const currentLevel = getCurrentLevel(score)
+ if (currentLevel !== prevLevelRef.current && score > 0) {
+ prevLevelRef.current = currentLevel
+ setLevelUpFlash(true)
+ setTimeout(() => setLevelUpFlash(false), 500)
+ }
+ }, [score])
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
@@ -74,8 +204,11 @@ export function InteractiveGame() {
if (mode !== 'playing') return
if (e.code === "Space" || e.code === "ArrowUp") {
e.preventDefault()
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
- gameOver ? handleRetry() : jump()
+ if (gameOver) {
+ handleRetry()
+ } else {
+ jump()
+ }
}
if (e.code === "Escape") exitGame()
}
@@ -84,42 +217,136 @@ export function InteractiveGame() {
}, [mode, gameOver, jump, handleRetry, exitGame])
useEffect(() => {
- if (mode !== 'playing' || gameOver) return
+ if (mode !== 'playing' || gameOver) {
+ if (requestRef.current !== null) cancelAnimationFrame(requestRef.current)
+ return
+ }
+
+ const gameLoop = (currentTime: number) => {
+ if (lastTimeRef.current === 0) {
+ lastTimeRef.current = currentTime
+ }
+
+ const deltaTime = (currentTime - lastTimeRef.current) / 1000
+ lastTimeRef.current = currentTime
+
+ let newPlayerY = 0
+ if (isJumpingRef.current) {
+ const jumpElapsed = currentTime - jumpStartTime.current
+ const jumpProgress = jumpElapsed / JUMP_DURATION
+
+ if (jumpProgress >= 1) {
+ isJumpingRef.current = false
+ newPlayerY = 0
+ } else {
+ newPlayerY = jumpHeight * Math.sin(jumpProgress * Math.PI)
+ }
+ }
+ setPlayerY(newPlayerY)
- const checkFrame = () => {
- const player = playerRef.current
- const obstacle = obstacleRef.current
+ setObstacle(prevObstacle => {
+ const effectiveSpeed = gameSpeed * prevObstacle.speedMultiplier
+ let newX = prevObstacle.x - effectiveSpeed * deltaTime * 100
+ const obstacleRight = newX + prevObstacle.size
- if (player && obstacle) {
- const p = player.getBoundingClientRect()
- const o = obstacle.getBoundingClientRect()
+ if (!hasPassedRef.current && obstacleRight < playerX) {
+ hasPassedRef.current = true
+ setScore(s => {
+ const newScore = s + 1
+ const newBaseSpeed = getBaseSpeed(newScore)
+ setGameSpeed(newBaseSpeed)
+ return newScore
+ })
+ }
+
+ if (newX < obstacleEndX) {
+ hasPassedRef.current = false
+ return getRandomObstacle(score, gameWidth)
+ }
+
+ return { ...prevObstacle, x: newX }
+ })
+
+ setObstacle(currentObstacle => {
+ const pLeft = playerX + 8
+ const pRight = playerX + playerSize - 8
+ const playerBottom = groundY + newPlayerY
+ const playerTop = playerBottom + playerSize - 8
- const paddingX = 16
- const paddingY = 12
-
- const isColliding =
- p.right - paddingX > o.left + paddingX &&
- p.left + paddingX < o.right - paddingX &&
- p.bottom - paddingY > o.top + paddingY
+ const oLeft = currentObstacle.x + 6
+ const oRight = currentObstacle.x + currentObstacle.size - 6
+ const obstacleBottom = groundY + currentObstacle.heightOffset
+ const obstacleTop = obstacleBottom + currentObstacle.size - 4
+
+ const isColliding =
+ pRight > oLeft &&
+ pLeft < oRight &&
+ playerBottom < obstacleTop &&
+ playerTop > obstacleBottom
if (isColliding) {
- handleGameOver()
- return
+ setScore(s => {
+ handleGameOver(s)
+ return s
+ })
}
- setScore(s => {
- const newScore = s + 1
- if (newScore % 300 === 0) {
- setGameSpeed(prev => Math.max(0.7, prev * 0.95))
- }
- return newScore
- })
- }
- requestRef.current = requestAnimationFrame(checkFrame)
+ return currentObstacle
+ })
+
+ requestRef.current = requestAnimationFrame(gameLoop)
}
- requestRef.current = requestAnimationFrame(checkFrame)
- return () => { if (requestRef.current !== null) cancelAnimationFrame(requestRef.current) }
- }, [mode, gameOver, handleGameOver, resetKey])
+
+ lastTimeRef.current = 0
+ requestRef.current = requestAnimationFrame(gameLoop)
+
+ return () => {
+ if (requestRef.current !== null) cancelAnimationFrame(requestRef.current)
+ }
+ }, [mode, gameOver, gameSpeed, handleGameOver, score, playerX, obstacleEndX, gameWidth, groundY, playerSize, jumpHeight])
+
+ const renderObstacle = (obs: Obstacle) => {
+ const baseClasses = "transition-colors"
+
+ switch (obs.type) {
+ case 'flying':
+ return (
+
+ )
+ case 'fast':
+ return (
+
+
+
+ )
+ case 'tall':
+ return (
+
+ )
+ default:
+ return (
+
+ )
+ }
+ }
return (
mode === 'idle' && setMode('preview')}
onHoverEnd={() => mode === 'preview' && setMode('idle')}
- className="relative mb-14 select-none border border-neutral-200 dark:border-white/10 bg-white/80 dark:bg-[#0a0a0a]/80 backdrop-blur-xl shadow-2xl z-50 mx-auto group overflow-hidden"
+ className="relative mb-8 md:mb-14 select-none border border-neutral-200/50 dark:border-white/10 bg-white/40 dark:bg-white/5 backdrop-blur-md shadow-xl z-50 mx-auto group overflow-hidden"
onClick={() => mode !== 'playing' && setMode('playing')}
>
+
+ {levelUpFlash && (
+
+ )}
+
+
{mode === 'idle' ? (
-
-
-
-
+
+
+
DevLovers Arcade
- READY
+ READY
) : (
-
-
-
+
+
- Status
+
+ {getCurrentLevel(score)}
+
{gameOver ? "CRASHED" : "RUNNING"}
-
-
+
+
+
+
+ {getCurrentLevel(score)}
+
+
+
+
- High
-
- {Math.floor(highScore / 5).toString().padStart(4, '0')}
+ High
+
+ {highScore.toString().padStart(3, '0')}
-
+
- Score
-
- {Math.floor(score / 5).toString().padStart(5, '0')}
+ Score
+
+ {score.toString().padStart(3, '0')}
-
-
-
-
-
-
-
-
-
+
+
+
+
10 ? 'rotate(-15deg) scale(0.95)' : 'rotate(0deg) scale(1)',
+ transition: 'transform 0.1s ease-out'
+ }}
+ >
+
{mode === 'playing' && (
-
-
+ {renderObstacle(obstacle)}
)}
@@ -240,8 +487,8 @@ export function InteractiveGame() {
)}
{gameOver && (
-
@@ -249,8 +496,8 @@ export function InteractiveGame() {
System Failure
- {e.stopPropagation(); handleRetry();}}
+ {e.stopPropagation(); handleRetry();}}
className="px-6 py-2.5 bg-[#ff005b] text-white text-[10px] font-bold uppercase tracking-widest rounded-lg shadow-lg hover:brightness-110 active:scale-95 transition-all flex items-center gap-2"
>
Retry
@@ -258,19 +505,12 @@ export function InteractiveGame() {
)}
-
-
+
+
)}
-
-
)
}
diff --git a/frontend/components/about/SponsorsWall.tsx b/frontend/components/about/SponsorsWall.tsx
index 1b2b30d8..07415439 100644
--- a/frontend/components/about/SponsorsWall.tsx
+++ b/frontend/components/about/SponsorsWall.tsx
@@ -11,9 +11,10 @@ interface SponsorsWallProps {
sponsors?: Sponsor[]
}
+const MAX_SPONSORS = 10
+
export function SponsorsWall({ sponsors = [] }: SponsorsWallProps) {
-
- const displaySponsors = sponsors
+ const displaySponsors = sponsors.slice(0, MAX_SPONSORS)
return (
diff --git a/frontend/components/about/TopicsSection.tsx b/frontend/components/about/TopicsSection.tsx
index e3e82bd3..48b70326 100644
--- a/frontend/components/about/TopicsSection.tsx
+++ b/frontend/components/about/TopicsSection.tsx
@@ -67,11 +67,11 @@ function TopicCard({ topic, index }: { topic: Topic, index: number }) {
-
diff --git a/frontend/components/shared/DynamicGridBackground.tsx b/frontend/components/shared/DynamicGridBackground.tsx
index 554117f9..fb8f4d99 100644
--- a/frontend/components/shared/DynamicGridBackground.tsx
+++ b/frontend/components/shared/DynamicGridBackground.tsx
@@ -8,11 +8,13 @@ import { cn } from '@/lib/utils';
type DynamicGridBackgroundProps = {
className?: string;
children?: ReactNode;
+ showStaticGrid?: boolean;
};
export function DynamicGridBackground({
className,
children,
+ showStaticGrid = false,
}: DynamicGridBackgroundProps) {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
@@ -29,9 +31,11 @@ export function DynamicGridBackground({
onMouseMove={handleMouseMove}
className={cn('group/dynamic relative overflow-hidden', className)}
>
-
+ {showStaticGrid && (
+
+ )}