@@ -54,6 +54,28 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
5454 const [ isMounted , setIsMounted ] = useState ( false ) ;
5555 const [ isFullscreen , setIsFullscreen ] = useState ( false ) ;
5656 const [ showControls , setShowControls ] = useState ( false ) ; // For collapsible controls
57+
58+ // Speed/Difficulty mode
59+ type SpeedMode = 'snail' | 'casual' | 'coffee' | 'rocket' | 'lightspeed' ;
60+ const [ speedMode , setSpeedMode ] = useState < SpeedMode > ( 'coffee' ) ;
61+ const [ showSpeedSelector , setShowSpeedSelector ] = useState ( false ) ;
62+
63+ // Load saved speed mode from localStorage on mount
64+ useEffect ( ( ) => {
65+ if ( typeof window !== 'undefined' ) {
66+ const savedSpeed = localStorage . getItem ( 'infiniteRunnerSpeedMode' ) as SpeedMode | null ;
67+ if ( savedSpeed && SPEED_CONFIGS [ savedSpeed ] ) {
68+ setSpeedMode ( savedSpeed ) ;
69+ }
70+ }
71+ } , [ ] ) ;
72+
73+ // Save speed mode to localStorage when it changes
74+ useEffect ( ( ) => {
75+ if ( typeof window !== 'undefined' && isMounted ) {
76+ localStorage . setItem ( 'infiniteRunnerSpeedMode' , speedMode ) ;
77+ }
78+ } , [ speedMode , isMounted ] ) ;
5779
5880 // Audio refs
5981 const menuMusicRef = useRef < HTMLAudioElement | null > ( null ) ;
@@ -125,6 +147,7 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
125147 const lastPowerUpRef = useRef ( 0 ) ;
126148 const levelTimerRef = useRef ( 0 ) ;
127149 const frameCountRef = useRef ( 0 ) ;
150+ const lastFrameTimeRef = useRef < number > ( 0 ) ; // For delta time calculation
128151
129152 // Constants
130153 const GRAVITY = 0.6 ;
@@ -136,6 +159,36 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
136159 const PLAYER_HEIGHT = 50 ;
137160 const DUCKING_HEIGHT = 20 ; // Make ducking more effective (lower to ground)
138161 const MAX_JUMPS = 2 ; // Allow double jump
162+ const TARGET_FPS = 60 ; // Target frame rate for consistent gameplay
163+
164+ // Speed mode configuration - normalized for 60 FPS
165+ const SPEED_CONFIGS = {
166+ snail : {
167+ baseSpeed : 3 ,
168+ label : '🐌 Snail Mail' ,
169+ description : 'Perfect for beginners or a chill vibe'
170+ } ,
171+ casual : {
172+ baseSpeed : 4.5 ,
173+ label : '🚶 Casual Stroll' ,
174+ description : 'Nice and easy, no rush'
175+ } ,
176+ coffee : {
177+ baseSpeed : 6 ,
178+ label : '☕ Coffee Break' ,
179+ description : 'Just right - recommended!'
180+ } ,
181+ rocket : {
182+ baseSpeed : 8 ,
183+ label : '🚀 Rocket Fuel' ,
184+ description : 'Fast-paced action for pros'
185+ } ,
186+ lightspeed : {
187+ baseSpeed : 10 ,
188+ label : '⚡ Lightspeed' ,
189+ description : 'Insane mode - are you ready?'
190+ }
191+ } ;
139192
140193 // Web3 Quiz Questions
141194 const quizQuestions = [
@@ -841,6 +894,9 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
841894 return ;
842895 }
843896
897+ // Get the base speed from selected mode
898+ const modeSpeed = SPEED_CONFIGS [ speedMode ] . baseSpeed ;
899+
844900 setGameState ( 'playing' ) ;
845901 setScore ( 0 ) ;
846902 setLevel ( 1 ) ;
@@ -852,8 +908,8 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
852908 setIsJumping ( false ) ;
853909 setIsDucking ( false ) ;
854910 setJumpCount ( 0 ) ;
855- setGameSpeed ( 5 ) ;
856- setBaseSpeed ( 5 ) ;
911+ setGameSpeed ( modeSpeed ) ;
912+ setBaseSpeed ( modeSpeed ) ;
857913 setObstacles ( [ ] ) ;
858914 setCoins ( [ ] ) ;
859915 setPowerUps ( [ ] ) ;
@@ -871,6 +927,7 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
871927 lastPowerUpRef . current = 0 ;
872928 levelTimerRef . current = 0 ;
873929 frameCountRef . current = 0 ;
930+ lastFrameTimeRef . current = 0 ;
874931
875932 // Give player 5 seconds of invulnerability at start
876933 setInvulnerableUntil ( Date . now ( ) + 5000 ) ;
@@ -882,7 +939,7 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
882939 }
883940
884941 // Don't auto-enter fullscreen - let user choose with button
885- } , [ account , addToast ] ) ;
942+ } , [ account , addToast , speedMode ] ) ;
886943
887944 // Handle keyboard input
888945 useEffect ( ( ) => {
@@ -981,11 +1038,21 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
9811038 return ;
9821039 }
9831040
984- const gameLoop = ( ) => {
985- // Update player physics
986- setPlayerVelocity ( v => v + GRAVITY ) ;
1041+ const gameLoop = ( currentTime : number ) => {
1042+ // Calculate delta time for frame-rate independence
1043+ if ( lastFrameTimeRef . current === 0 ) {
1044+ lastFrameTimeRef . current = currentTime ;
1045+ }
1046+ const deltaTime = currentTime - lastFrameTimeRef . current ;
1047+ lastFrameTimeRef . current = currentTime ;
1048+
1049+ // Normalize to 60 FPS (16.67ms per frame)
1050+ const deltaMultiplier = deltaTime / ( 1000 / TARGET_FPS ) ;
1051+
1052+ // Update player physics with delta time
1053+ setPlayerVelocity ( v => v + ( GRAVITY * deltaMultiplier ) ) ;
9871054 setPlayerY ( y => {
988- const newY = y + playerVelocity ;
1055+ const newY = y + ( playerVelocity * deltaMultiplier ) ;
9891056 if ( newY >= GROUND_Y ) {
9901057 setIsJumping ( false ) ;
9911058 setJumpCount ( 0 ) ; // Reset jump count when landing
@@ -994,9 +1061,10 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
9941061 return newY ;
9951062 } ) ;
9961063
997- // Update background
998- setBgOffset ( offset => ( offset + gameSpeed ) % 1000 ) ;
999- setBgImageOffset ( offset => ( offset + gameSpeed * 0.5 ) % 2000 ) ; // Slower parallax for background image
1064+ // Update background with delta time normalization
1065+ const normalizedSpeed = gameSpeed * deltaMultiplier ;
1066+ setBgOffset ( offset => ( offset + normalizedSpeed ) % 1000 ) ;
1067+ setBgImageOffset ( offset => ( offset + normalizedSpeed * 0.5 ) % 2000 ) ; // Slower parallax for background image
10001068
10011069 // Update score - only if not in safe zone
10021070 const isInSafeZone = Date . now ( ) < invulnerableUntil ;
@@ -1146,7 +1214,7 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
11461214 }
11471215 }
11481216
1149- // Move obstacles with dynamic vertical movement
1217+ // Move obstacles with dynamic vertical movement (normalized with delta time)
11501218 setObstacles ( obs =>
11511219 obs
11521220 . map ( o => {
@@ -1161,22 +1229,22 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
11611229 newY = Math . max ( GROUND_Y - 220 , Math . min ( GROUND_Y - 50 , newY ) ) ;
11621230 }
11631231
1164- return { ...o , x : o . x - gameSpeed , y : newY } ;
1232+ return { ...o , x : o . x - normalizedSpeed , y : newY } ;
11651233 } )
11661234 . filter ( o => o . x > - 100 )
11671235 ) ;
11681236
1169- // Move coins
1237+ // Move coins (normalized with delta time)
11701238 setCoins ( c =>
11711239 c
1172- . map ( coin => ( { ...coin , x : coin . x - gameSpeed } ) )
1240+ . map ( coin => ( { ...coin , x : coin . x - normalizedSpeed } ) )
11731241 . filter ( coin => coin . x > - 50 )
11741242 ) ;
11751243
1176- // Move power-ups
1244+ // Move power-ups (normalized with delta time)
11771245 setPowerUps ( p =>
11781246 p
1179- . map ( powerUp => ( { ...powerUp , x : powerUp . x - gameSpeed } ) )
1247+ . map ( powerUp => ( { ...powerUp , x : powerUp . x - normalizedSpeed } ) )
11801248 . filter ( powerUp => powerUp . x > - 50 )
11811249 ) ;
11821250
@@ -1663,6 +1731,9 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
16631731 < div className = 'text-white font-bold text-2xl mb-1' > Score: { score } </ div >
16641732 < div className = 'text-white/80 text-sm' > High Score: { highScore } </ div >
16651733 < div className = 'text-cyan-400 text-sm font-semibold' > Level: { level } </ div >
1734+ < div className = 'text-purple-400 text-xs font-semibold mt-1' >
1735+ { SPEED_CONFIGS [ speedMode ] . label }
1736+ </ div >
16661737 { /* Power-up status indicator */ }
16671738 { isPoweredUp && (
16681739 < div className = 'text-yellow-400 text-xs font-bold mt-2 animate-pulse' >
@@ -2083,6 +2154,17 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
20832154 < p className = 'text-cyan-300 font-semibold text-sm' > 🎯 Level up every 1,000 points • 🧠 Answer Web3 quizzes</ p >
20842155 </ div >
20852156
2157+ { /* Current Speed Mode Indicator */ }
2158+ < div className = 'bg-gradient-to-r from-cyan-500/20 to-purple-500/20 rounded-xl p-3 mb-4 border border-cyan-400/30' >
2159+ < div className = 'flex items-center justify-center gap-2' >
2160+ < span className = 'text-white/70 text-sm' > Current Speed:</ span >
2161+ < span className = 'text-cyan-300 font-bold text-sm' > { SPEED_CONFIGS [ speedMode ] . label } </ span >
2162+ </ div >
2163+ < p className = 'text-white/50 text-xs mt-1' >
2164+ ⚙️ Change speed when starting or replaying
2165+ </ p >
2166+ </ div >
2167+
20862168 { /* Compact Controls */ }
20872169 < div className = 'bg-black/30 rounded-xl p-4 mb-4 border border-white/10' >
20882170 < div className = 'grid grid-cols-2 gap-2 text-white/70 text-xs' >
@@ -2465,10 +2547,42 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
24652547 </ div >
24662548
24672549 { /* Description */ }
2468- < p className = 'text-white/80 text-sm mb-6 leading-relaxed' >
2550+ < p className = 'text-white/80 text-sm mb-4 leading-relaxed' >
24692551 Starting this game will deduct 250 points from your account. You'll earn XP and rewards based on your performance!
24702552 </ p >
24712553
2554+ { /* Speed Mode Selector */ }
2555+ < div className = 'bg-gradient-to-br from-cyan-500/10 to-purple-500/10 rounded-2xl p-3 mb-6 border border-cyan-400/30' >
2556+ < div className = 'text-cyan-300 text-xs mb-2 font-semibold text-center' > ⚡ Game Speed</ div >
2557+ < div className = 'flex justify-center gap-2 mb-2' >
2558+ { ( Object . keys ( SPEED_CONFIGS ) as SpeedMode [ ] ) . map ( ( mode ) => {
2559+ const icon = SPEED_CONFIGS [ mode ] . label . split ( ' ' ) [ 0 ] ; // Extract emoji
2560+ return (
2561+ < button
2562+ key = { mode }
2563+ onClick = { ( ) => setSpeedMode ( mode ) }
2564+ title = { `${ SPEED_CONFIGS [ mode ] . label } \n${ SPEED_CONFIGS [ mode ] . description } ` }
2565+ className = { `relative w-12 h-12 rounded-lg border-2 transition-all duration-200 ${
2566+ speedMode === mode
2567+ ? 'bg-gradient-to-r from-cyan-500/30 to-purple-600/30 border-cyan-400 shadow-lg scale-110'
2568+ : 'bg-white/5 border-white/10 hover:border-white/30 hover:bg-white/10 hover:scale-105'
2569+ } `}
2570+ >
2571+ < div className = 'text-2xl' > { icon } </ div >
2572+ { speedMode === mode && (
2573+ < div className = 'absolute -top-1 -right-1 bg-cyan-400 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center' >
2574+ ✓
2575+ </ div >
2576+ ) }
2577+ </ button >
2578+ ) ;
2579+ } ) }
2580+ </ div >
2581+ < div className = 'text-white/50 text-[10px] text-center' >
2582+ Hover to see speed names • Frame-rate normalized!
2583+ </ div >
2584+ </ div >
2585+
24722586 { /* Buttons */ }
24732587 < div className = 'grid grid-cols-2 gap-3' >
24742588 < button
@@ -2513,10 +2627,42 @@ const InfiniteRunner: React.FC<InfiniteRunnerProps> = ({ gameId, gameTitle, embe
25132627 </ div >
25142628
25152629 { /* Description */ }
2516- < p className = 'text-white/80 text-sm mb-6 leading-relaxed' >
2630+ < p className = 'text-white/80 text-sm mb-4 leading-relaxed' >
25172631 Playing again will deduct 100 points from your account. Try to beat your high score of { highScore } points!
25182632 </ p >
25192633
2634+ { /* Speed Mode Selector */ }
2635+ < div className = 'bg-gradient-to-br from-cyan-500/10 to-purple-500/10 rounded-2xl p-3 mb-6 border border-cyan-400/30' >
2636+ < div className = 'text-cyan-300 text-xs mb-2 font-semibold text-center' > ⚡ Game Speed</ div >
2637+ < div className = 'flex justify-center gap-2 mb-2' >
2638+ { ( Object . keys ( SPEED_CONFIGS ) as SpeedMode [ ] ) . map ( ( mode ) => {
2639+ const icon = SPEED_CONFIGS [ mode ] . label . split ( ' ' ) [ 0 ] ; // Extract emoji
2640+ return (
2641+ < button
2642+ key = { mode }
2643+ onClick = { ( ) => setSpeedMode ( mode ) }
2644+ title = { `${ SPEED_CONFIGS [ mode ] . label } \n${ SPEED_CONFIGS [ mode ] . description } ` }
2645+ className = { `relative w-12 h-12 rounded-lg border-2 transition-all duration-200 ${
2646+ speedMode === mode
2647+ ? 'bg-gradient-to-r from-cyan-500/30 to-purple-600/30 border-cyan-400 shadow-lg scale-110'
2648+ : 'bg-white/5 border-white/10 hover:border-white/30 hover:bg-white/10 hover:scale-105'
2649+ } `}
2650+ >
2651+ < div className = 'text-2xl' > { icon } </ div >
2652+ { speedMode === mode && (
2653+ < div className = 'absolute -top-1 -right-1 bg-cyan-400 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center' >
2654+ ✓
2655+ </ div >
2656+ ) }
2657+ </ button >
2658+ ) ;
2659+ } ) }
2660+ </ div >
2661+ < div className = 'text-white/50 text-[10px] text-center' >
2662+ Hover to see speed names • Frame-rate normalized!
2663+ </ div >
2664+ </ div >
2665+
25202666 { /* Buttons */ }
25212667 < div className = 'grid grid-cols-2 gap-3' >
25222668 < button
0 commit comments