@@ -18,6 +18,12 @@ type RunnerState = {
1818 obstacles : Obstacle [ ] ;
1919} ;
2020
21+ export type StickmanInboxRunnerProps = {
22+ autoplay ?: boolean ;
23+ className ?: string ;
24+ showControlsHint ?: boolean ;
25+ } ;
26+
2127const WIDTH = 320 ;
2228const HEIGHT = 152 ;
2329const GROUND_Y = 116 ;
@@ -64,19 +70,31 @@ function intersects(a: { left: number; right: number; top: number; bottom: numbe
6470 return a . left < b . right && a . right > b . left && a . top < b . bottom && a . bottom > b . top ;
6571}
6672
67- export function StickmanInboxRunner ( { className = "" } : { className ?: string } ) {
73+ export function StickmanInboxRunner ( {
74+ autoplay = false ,
75+ className = "" ,
76+ showControlsHint = true ,
77+ } : StickmanInboxRunnerProps ) {
6878 const [ game , setGame ] = useState < RunnerState > ( ( ) => initialState ( ) ) ;
6979 const containerRef = useRef < HTMLDivElement | null > ( null ) ;
7080 const jumpQueuedRef = useRef ( 0 ) ;
7181 const crouchActiveRef = useRef ( false ) ;
7282 const obstacleIdRef = useRef ( 0 ) ;
73- const spawnCooldownRef = useRef ( 900 ) ;
83+ const autoplayPatternRef = useRef < ObstacleKind [ ] > ( [ "envelope" , "thread" , "envelope" , "thread" ] ) ;
84+ const autoplayPatternIndexRef = useRef ( 0 ) ;
85+ const spawnCooldownRef = useRef ( autoplay ? 460 : 900 ) ;
7486 const spawnElapsedRef = useRef ( 0 ) ;
7587
7688 useEffect ( ( ) => {
7789 containerRef . current ?. focus ( ) ;
7890 } , [ ] ) ;
7991
92+ useEffect ( ( ) => {
93+ spawnCooldownRef . current = autoplay ? 460 : 900 ;
94+ spawnElapsedRef . current = 0 ;
95+ autoplayPatternIndexRef . current = 0 ;
96+ } , [ autoplay ] ) ;
97+
8098 useEffect ( ( ) => {
8199 const handleKeyDown = ( event : KeyboardEvent | ReactKeyboardEvent < HTMLDivElement > ) => {
82100 if ( [ "ArrowUp" , "ArrowDown" , "Space" , "KeyW" , "KeyS" ] . includes ( event . code ) ) {
@@ -144,7 +162,51 @@ export function StickmanInboxRunner({ className = "" }: { className?: string })
144162 setGame ( ( previous ) => {
145163 let y = previous . y ;
146164 let velocityY = previous . velocityY ;
165+ const speed = 3.25 * timeScale ;
166+ let obstacles = previous . obstacles
167+ . map ( ( obstacle ) => ( { ...obstacle , x : obstacle . x - speed } ) )
168+ . filter ( ( obstacle ) => obstacle . x > - 60 ) ;
169+
170+ spawnElapsedRef . current += dt ;
171+ if ( spawnElapsedRef . current >= spawnCooldownRef . current ) {
172+ obstacleIdRef . current += 1 ;
173+ const kind : ObstacleKind = autoplay
174+ ? autoplayPatternRef . current [
175+ autoplayPatternIndexRef . current ++ % autoplayPatternRef . current . length
176+ ]
177+ : Math . random ( ) > 0.32
178+ ? "envelope"
179+ : "thread" ;
180+ obstacles = [ ...obstacles , { id : obstacleIdRef . current , kind, x : WIDTH + 16 } ] ;
181+ spawnElapsedRef . current = 0 ;
182+ spawnCooldownRef . current = autoplay ? 640 : 900 + Math . random ( ) * 700 ;
183+ }
184+
147185 const grounded = y <= 0.001 ;
186+ const nextObstacle = obstacles . find ( ( obstacle ) => {
187+ const box = obstacleBox ( obstacle ) ;
188+ return box . right >= PLAYER_X - 6 ;
189+ } ) ;
190+ const nextObstacleBox = nextObstacle ? obstacleBox ( nextObstacle ) : null ;
191+ const autoplayCrouch =
192+ autoplay &&
193+ grounded &&
194+ nextObstacle ?. kind === "thread" &&
195+ nextObstacleBox !== null &&
196+ nextObstacleBox . left <= PLAYER_X + PLAYER_WIDTH + 26 &&
197+ nextObstacleBox . right >= PLAYER_X - 4 ;
198+
199+ if (
200+ autoplay &&
201+ grounded &&
202+ nextObstacle ?. kind === "envelope" &&
203+ nextObstacleBox !== null &&
204+ nextObstacleBox . left <= PLAYER_X + PLAYER_WIDTH + 34
205+ ) {
206+ jumpQueuedRef . current = Math . max ( jumpQueuedRef . current , 1 ) ;
207+ velocityY = Math . max ( velocityY , 11.2 ) ;
208+ jumpQueuedRef . current = 0 ;
209+ }
148210
149211 if ( grounded && jumpQueuedRef . current > 0 ) {
150212 velocityY = Math . max ( velocityY , 11.2 ) ;
@@ -157,21 +219,7 @@ export function StickmanInboxRunner({ className = "" }: { className?: string })
157219 velocityY = 0 ;
158220 }
159221
160- const crouching = crouchActiveRef . current && y === 0 ;
161- const speed = 3.25 * timeScale ;
162- let obstacles = previous . obstacles
163- . map ( ( obstacle ) => ( { ...obstacle , x : obstacle . x - speed } ) )
164- . filter ( ( obstacle ) => obstacle . x > - 60 ) ;
165-
166- spawnElapsedRef . current += dt ;
167- if ( spawnElapsedRef . current >= spawnCooldownRef . current ) {
168- obstacleIdRef . current += 1 ;
169- const kind : ObstacleKind = Math . random ( ) > 0.32 ? "envelope" : "thread" ;
170- obstacles = [ ...obstacles , { id : obstacleIdRef . current , kind, x : WIDTH + 16 } ] ;
171- spawnElapsedRef . current = 0 ;
172- spawnCooldownRef . current = 900 + Math . random ( ) * 700 ;
173- }
174-
222+ const crouching = ( crouchActiveRef . current || autoplayCrouch ) && y === 0 ;
175223 const playerHeight = crouching ? CROUCH_HEIGHT : STANDING_HEIGHT ;
176224 const playerBox = {
177225 left : PLAYER_X ,
@@ -291,9 +339,13 @@ export function StickmanInboxRunner({ className = "" }: { className?: string })
291339 < span > Best { Math . max ( game . best , score ) } </ span >
292340 < span > Resets { game . crashes } </ span >
293341 </ div >
294- < p className = "mt-1 text-center text-[10px] uppercase tracking-[0.18em] text-muted-foreground/80" >
295- Click or hover here, then press W, S, arrows, or space.
296- </ p >
342+ { showControlsHint ? (
343+ < p className = "mt-1 text-center text-[10px] uppercase tracking-[0.18em] text-muted-foreground/80" >
344+ { autoplay
345+ ? "Autoplay is on. Press W, S, arrows, or space to take over."
346+ : "Click or hover here, then press W, S, arrows, or space." }
347+ </ p >
348+ ) : null }
297349 </ div >
298350 ) ;
299351}
0 commit comments