Meta Description" name="description" />

Share this result

Previews are deleted daily. Get a permanent share link sent to your inbox:
Script
import React, { useState, useEffect, useRef, useMemo, Suspense } from 'react'; import Head from 'next/head'; import { Canvas, useFrame, useThree } from '@react-three/fiber'; import { Text, Box, Sphere, useTexture, PerspectiveCamera, Environment, Float, Stars, Trail } from '@react-three/drei'; import * as THREE from 'three'; import { create } from 'zustand'; import { useDrag } from '@use-gesture/react'; import { Volume2, VolumeX, Play, RotateCcw, Trophy } from 'lucide-react'; // --- Game Constants --- const LANE_WIDTH = 2.5; const JUMP_FORCE = 0.15; const GRAVITY = 0.008; const BASE_SPEED = 0.15; const MAX_SPEED = 0.4; const SPEED_INCREMENT = 0.0001; const SEGMENT_LENGTH = 20; const VISIBLE_SEGMENTS = 8; // --- Types --- type GameState = 'MENU' | 'PLAYING' | 'GAME_OVER'; interface GameStore { status: GameState; score: number; coins: number; speed: number; distance: number; lane: number; // -1 (Left), 0 (Center), 1 (Right) isJumping: boolean; isSliding: boolean; muted: boolean; actions: { start: () => void; restart: () => void; gameOver: () => void; setLane: (lane: number) => void; setJumping: (jumping: boolean) => void; setSliding: (sliding: boolean) => void; addScore: (amount: number) => void; addCoin: () => void; increaseSpeed: () => void; toggleMute: () => void; }; } // --- Store --- const useGameStore = create<GameStore>((set, get) => ({ status: 'MENU', score: 0, coins: 0, speed: BASE_SPEED, distance: 0, lane: 0, isJumping: false, isSliding: false, muted: false, actions: { start: () => set({ status: 'PLAYING', score: 0, coins: 0, speed: BASE_SPEED, distance: 0, lane: 0 }), restart: () => set({ status: 'PLAYING', score: 0, coins: 0, speed: BASE_SPEED, distance: 0, lane: 0 }), gameOver: () => set({ status: 'GAME_OVER' }), setLane: (lane) => set({ lane: Math.max(-1, Math.min(1, lane)) }), setJumping: (isJumping) => set({ isJumping }), setSliding: (isSliding) => set({ isSliding }), addScore: (amount) => set((state) => ({ score: state.score + amount })), addCoin: () => set((state) => ({ coins: state.coins + 1, score: state.score + 50 })), increaseSpeed: () => set((state) => ({ speed: Math.min(MAX_SPEED, state.speed + SPEED_INCREMENT) })), toggleMute: () => set((state) => ({ muted: !state.muted })), }, })); // --- Audio Manager (Procedural) --- class AudioManager { ctx: AudioContext | null = null; masterGain: GainNode | null = null; init() { if (typeof window !== 'undefined' && !this.ctx) { this.ctx = new (window.AudioContext || (window as any).webkitAudioContext)(); this.masterGain = this.ctx.createGain(); this.masterGain.connect(this.ctx.destination); this.masterGain.gain.value = 0.3; } } playTone(freq: number, type: OscillatorType, duration: number, startTime = 0) { if (!this.ctx || !this.masterGain || useGameStore.getState().muted) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = type; osc.frequency.setValueAtTime(freq, this.ctx.currentTime + startTime); gain.gain.setValueAtTime(0.5, this.ctx.currentTime + startTime); gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + startTime + duration); osc.connect(gain); gain.connect(this.masterGain); osc.start(this.ctx.currentTime + startTime); osc.stop(this.ctx.currentTime + startTime + duration); } playJump() { this.playTone(400, 'sine', 0.2); this.playTone(600, 'sine', 0.2, 0.1); } playCoin() { this.playTone(1200, 'square', 0.1); this.playTone(1800, 'square', 0.1, 0.05); } playCrash() { this.playTone(100, 'sawtooth', 0.4); this.playTone(50, 'sawtooth', 0.4, 0.1); } playBGM() { // Simple beat loop logic would go here, but omitted to keep complexity manageable. // We'll rely on game loop to trigger beats if needed, or just keep it SFX focused. } } const audioManager = new AudioManager(); // --- Components --- // The Player Character const Player = () => { const meshRef = useRef<THREE.Group>(null); const { lane, isJumping, isSliding, actions, status, speed } = useGameStore(); const [posY, setPosY] = useState(0); const [velY, setVelY] = useState(0); const [posZ, setPosZ] = useState(0); // Refs for physics loop const stateRef = useRef({ lane, isJumping, isSliding, posY, velY, posZ, speed, status }); useEffect(() => { stateRef.current = { lane, isJumping, isSliding, posY, velY, posZ, speed, status }; }, [lane, isJumping, isSliding, posY, velY, posZ, speed, status]); // Jump logic useEffect(() => { if (isJumping && stateRef.current.posY <= 0.01) { setVelY(JUMP_FORCE); audioManager.playJump(); setTimeout(() => actions.setJumping(false), 500); // Reset jump flag after animation intent } }, [isJumping, actions]); useFrame((state, delta) => { if (status !== 'PLAYING') return; // Physics: Lane LERP const targetX = lane * LANE_WIDTH; if (meshRef.current) { meshRef.current.position.x = THREE.MathUtils.lerp(meshRef.current.position.x, targetX, 0.2); // Physics: Gravity let newPosY = posY + velY; let newVelY = velY - GRAVITY; if (newPosY <= 0) { newPosY = 0; newVelY = 0; } setPosY(newPosY); setVelY(newVelY); meshRef.current.position.y = newPosY; // Continuous forward movement const moveZ = speed * (delta * 60); // normalize speed to 60fps const newPosZ = posZ - moveZ; setPosZ(newPosZ); meshRef.current.position.z = newPosZ; // Update Global Game Distance for generation useGameStore.setState({ distance: Math.abs(newPosZ) }); actions.increaseSpeed(); // Camera follow state.camera.position.z = newPosZ + 6; state.camera.position.x = THREE.MathUtils.lerp(state.camera.position.x, targetX * 0.5, 0.1); } }); // Expose player position to global scope for collision checks (hacky but effective for simple React prototyping) useEffect(() => { (window as any).playerZ = posZ; (window as any).playerX = meshRef.current?.position.x || 0; (window as any).playerY = posY; (window as any).isSliding = isSliding; }, [posZ, posY, isSliding]); return ( <group ref={meshRef} position={[0, 0, 0]}> <Trail width={1} length={4} color={new THREE.Color("#00ffff")} attenuation={(t) => t * t} > <group> {/* Body */} <Box args={[0.8, isSliding ? 0.5 : 1.5, 0.8]} position={[0, isSliding ? 0.25 : 0.75, 0]} castShadow > <meshStandardMaterial color="#ff0055" emissive="#ff0055" emissiveIntensity={0.5} /> </Box> {/* Head */} {!isSliding && ( <Box args={[0.5, 0.5, 0.5]} position={[0, 1.7, 0]}> <meshStandardMaterial color="#ffffff" /> </Box> )} {/* Jetpack Glint */} <Box args={[0.4, 0.6, 0.2]} position={[0, 1, 0.4]}> <meshStandardMaterial color="cyan" emissive="cyan" emissiveIntensity={0.8} /> </Box> </group> </Trail> </group> ); }; // --- World Generation --- // Types for generated objects type ObstacleType = 'BARRIER_LOW' | 'BARRIER_HIGH' | 'TRAIN' | 'COIN'; interface GameObj { id: string; type: ObstacleType; lane: number; z: number; active: boolean; } const Segment = ({ zOffset, objects, onCollect }: { zOffset: number, objects: GameObj[], onCollect: (id: string) => void }) => { const groupRef = useRef<THREE.Group>(null); useFrame(() => { // Simple Collision Detection const pZ = (window as any).playerZ || 0; const pX = (window as any).playerX || 0; const pY = (window as any).playerY || 0; const pSliding = (window as any).isSliding || false; objects.forEach(obj => { if (!obj.active) return; const objZ = zOffset + obj.z; // Basic Z intersection if (Math.abs(objZ - pZ) < 1.0) { const objX = obj.lane * LANE_WIDTH; // Basic X intersection if (Math.abs(objX - pX) < 1.0) { if (obj.type === 'COIN') { onCollect(obj.id); } else { // Obstacle logic let collision = false; if (obj.type === 'TRAIN') collision = true; // Train hits everything if (obj.type === 'BARRIER_HIGH') { // Must slide under. If not sliding, crash. if (!pSliding) collision = true; } if (obj.type === 'BARRIER_LOW') { // Must jump over. If Y < 1.0 (approx), crash. if (pY < 1.0) collision = true; } if (collision) { useGameStore.getState().actions.gameOver(); audioManager.playCrash(); } } } } }); }); return ( <group position={[0, 0, zOffset]} ref={groupRef}> {/* Floor */} <Box args={[LANE_WIDTH * 3 + 2, 0.2, SEGMENT_LENGTH]} position={[0, -0.1, SEGMENT_LENGTH / 2]} receiveShadow> <meshStandardMaterial color="#1a1a2e" roughness={0.8} /> </Box> {/* Lane Markers */} {[-1, 1].map((offset) => ( <Box key={offset} args={[0.1, 0.05, SEGMENT_LENGTH]} position={[offset * (LANE_WIDTH / 2), 0.01, SEGMENT_LENGTH / 2]}> <meshBasicMaterial color="#2a2a4e" /> </Box> ))} {objects.map(obj => { if (!obj.active) return null; const x = obj.lane * LANE_WIDTH; if (obj.type === 'COIN') { return ( <Float key={obj.id} speed={5} rotationIntensity={1} floatIntensity={1}> <Sphere args={[0.3, 16, 16]} position={[x, 1, obj.z]}> <meshStandardMaterial color="#ffd700" emissive="#ffd700" emissiveIntensity={0.6} /> </Sphere> </Float> ); } if (obj.type === 'TRAIN') { return ( <group key={obj.id} position={[x, 2, obj.z]}> <Box args={[2.2, 4, 8]} castShadow> <meshStandardMaterial color="#444" /> </Box> <Box args={[2.3, 0.5, 8.1]} position={[0, -1.5, 0]}> <meshStandardMaterial color="#ff4400" emissive="#ff4400" emissiveIntensity={0.8} /> </Box> </group> ); } if (obj.type === 'BARRIER_LOW') { return ( <Box key={obj.id} args={[2, 1, 0.5]} position={[x, 0.5, obj.z]} castShadow> <meshStandardMaterial color="#ff0000" /> <Box args={[2.1, 0.1, 0.1]} position={[0, 0.4, 0]}> <meshStandardMaterial color="yellow" emissive="yellow" /> </Box> </Box> ); } if (obj.type === 'BARRIER_HIGH') { return ( <group key={obj.id} position={[x, 0, obj.z]}> {/* Legs */} <Box args={[0.2, 3, 0.2]} position={[-0.8, 1.5, 0]}><meshStandardMaterial color="#888"/></Box> <Box args={[0.2, 3, 0.2]} position={[0.8, 1.5, 0]}><meshStandardMaterial color="#888"/></Box> {/* Top */} <Box args={[2, 1.2, 0.5]} position={[0, 2.5, 0]}> <meshStandardMaterial color="#ff0000" /> </Box> </group> ); } return null; })} </group> ); }; const WorldManager = () => { const playerDist = useGameStore(s => s.distance); const { addCoin } = useGameStore(s => s.actions); // State to hold active segments const [segments, setSegments] = useState<{ index: number; objects: GameObj[] }[]>([]); // Initialize useEffect(() => { // Create initial safe zone const initialSegments = []; for (let i = 0; i < 3; i++) { initialSegments.push({ index: -i, objects: [] }); } setSegments(initialSegments); }, []); // Update segments loop useFrame(() => { const currentSegIndex = Math.floor(playerDist / SEGMENT_LENGTH); // We want segments from currentSegIndex - 1 to currentSegIndex + VISIBLE_SEGMENTS setSegments(prev => { const neededIndices = []; for (let i = -1; i < VISIBLE_SEGMENTS; i++) { neededIndices.push(currentSegIndex + i); } // Filter out too old const kept = prev.filter(s => s.index >= currentSegIndex - 1); // Add new const newestIndex = kept.length > 0 ? kept[kept.length - 1].index : currentSegIndex - 2; const toAdd = []; for (let i = 1; i <= VISIBLE_SEGMENTS + 1; i++) { const targetIdx = currentSegIndex + i; if (!kept.find(s => s.index === targetIdx) && targetIdx > newestIndex) { toAdd.push(generateSegment(targetIdx)); } } if (toAdd.length === 0) return prev; return [...kept, ...toAdd].sort((a,b) => b.index - a.index); // Sort for fun, though map key handles order }); }); const handleCollect = (id: string) => { audioManager.playCoin(); addCoin(); setSegments(prev => prev.map(seg => ({ ...seg, objects: seg.objects.map(obj => obj.id === id ? { ...obj, active: false } : obj) }))); }; return ( <group> {segments.map(seg => ( <Segment key={seg.index} zOffset={-(seg.index * SEGMENT_LENGTH)} objects={seg.objects} onCollect={handleCollect} /> ))} {/* City Background - simplified as static distant meshes moving with player to fake infinity */} <BackgroundCity playerDist={playerDist} /> </group> ); }; const BackgroundCity = ({ playerDist }: { playerDist: number }) => { const buildings = useMemo(() => { const arr = []; for(let i=0; i<40; i++) { arr.push({ x: (Math.random() > 0.5 ? 1 : -1) * (15 + Math.random() * 30), z: Math.random() * 200 - 100, height: 10 + Math.random() * 40, width: 5 + Math.random() * 10 }); } return arr; }, []); // Move buildings to create parallax/endless feel relative to camera roughly const groupRef = useRef<THREE.Group>(null); useFrame(() => { if (groupRef.current) { groupRef.current.position.z = -playerDist; } }); return ( <group ref={groupRef}> {buildings.map((b, i) => ( <Box key={i} args={[b.width, b.height, b.width]} position={[b.x, b.height/2 - 5, b.z - 50]}> <meshStandardMaterial color={`hsl(${Math.random() * 60 + 200}, 50%, 20%)`} emissive="#111" /> </Box> ))} </group> ) } // Generator Logic let objIdCounter = 0; function generateSegment(index: number) { const objects: GameObj[] = []; // Empty start segment if (index < 2) return { index, objects }; // 1. Coins const coinPattern = Math.random(); if (coinPattern > 0.3) { const lane = Math.floor(Math.random() * 3) - 1; for(let z=2; z<SEGMENT_LENGTH-2; z+=2) { objects.push({ id: `c-${index}-${objIdCounter++}`, type: 'COIN', lane, z, active: true }); } } // 2. Obstacles const numObstacles = Math.floor(Math.random() * 2) + 1; for(let i=0; i<numObstacles; i++) { const z = 5 + Math.random() * (SEGMENT_LENGTH - 10); const lane = Math.floor(Math.random() * 3) - 1; const typeRnd = Math.random(); let type: ObstacleType = 'BARRIER_LOW'; if (typeRnd > 0.8) type = 'TRAIN'; else if (typeRnd > 0.5) type = 'BARRIER_HIGH'; else type = 'BARRIER_LOW'; // Don't overlap too much (simple check omitted for brevity, just purely random) objects.push({ id: `o-${index}-${objIdCounter++}`, type, lane, z, active: true }); } return { index, objects }; } // --- UI & Controls Overlay --- const UI = () => { const { status, score, coins, actions, muted } = useGameStore(); const { start, restart, toggleMute, setLane, setJumping, setSliding } = actions; // Keyboard Controls useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (status !== 'PLAYING') return; switch(e.key) { case 'ArrowLeft': case 'a': setLane(useGameStore.getState().lane - 1); break; case 'ArrowRight': case 'd': setLane(useGameStore.getState().lane + 1); break; case 'ArrowUp': case 'w': case ' ': setJumping(true); break; case 'ArrowDown': case 's': setSliding(true); setTimeout(() => setSliding(false), 800); break; } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [status, setLane, setJumping, setSliding]); // Touch Controls const bind = useDrag(({ swipe: [swipeX, swipeY] }) => { if (status !== 'PLAYING') return; if (swipeX === -1) setLane(useGameStore.getState().lane - 1); if (swipeX === 1) setLane(useGameStore.getState().lane + 1); if (swipeY === -1) setJumping(true); if (swipeY === 1) { setSliding(true); setTimeout(() => setSliding(false), 800); } }); return ( <div {...bind()} className="absolute inset-0 pointer-events-auto select-none overflow-hidden touch-none"> {/* HUD */} {status === 'PLAYING' && ( <div className="absolute top-4 left-4 right-4 flex justify-between text-white font-bold text-xl drop-shadow-md"> <div className="flex flex-col"> <span>SCORE: {Math.floor(score)}</span> <span className="text-yellow-400 text-sm">COINS: {coins}</span> </div> <button onClick={toggleMute} className="p-2 bg-black/30 rounded-full hover:bg-black/50"> {muted ? <VolumeX size={24} /> : <Volume2 size={24} />} </button> </div> )} {/* Menus */} {status === 'MENU' && ( <div className="absolute inset-0 flex items-center justify-center bg-black/80 z-50"> <div className="text-center space-y-6"> <h1 className="text-6xl font-black text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-purple-600 drop-shadow-[0_0_15px_rgba(0,255,255,0.5)]"> NEON RUNNER </h1> <p className="text-gray-300">Swipe or Arrows to Move • Up to Jump • Down to Slide</p> <button onClick={() => { audioManager.init(); start(); }} className="px-8 py-4 bg-cyan-600 hover:bg-cyan-500 text-white font-bold rounded-lg shadow-lg transform transition hover:scale-105 flex items-center gap-2 mx-auto" > <Play size={24} /> START GAME </button> </div> </div> )} {status === 'GAME_OVER' && ( <div className="absolute inset-0 flex items-center justify-center bg-black/90 z-50"> <div className="text-center space-y-6 p-8 border-2 border-red-500 rounded-2xl bg-black/50 backdrop-blur"> <h2 className="text-5xl font-bold text-red-500">CRASHED!</h2> <div className="text-2xl text-white space-y-2"> <p className="flex items-center justify-center gap-2"><Trophy className="text-yellow-500"/> Score: {Math.floor(score)}</p> <p className="text-yellow-400 text-lg">Coins: {coins}</p> </div> <button onClick={restart} className="px-8 py-4 bg-red-600 hover:bg-red-500 text-white font-bold rounded-lg shadow-lg transform transition hover:scale-105 flex items-center gap-2 mx-auto" > <RotateCcw size={24} /> TRY AGAIN </button> </div> </div> )} {/* Mobile Control Hints (Fade out) */} {status === 'PLAYING' && score < 100 && ( <div className="absolute bottom-20 w-full text-center text-white/30 animate-pulse"> Swipe to control </div> )} </div> ); }; export default function GamePage() { return ( <div className="w-full h-screen bg-[#050510] relative overflow-hidden"> <Head> <title>Neon Runner 3D</title> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" /> </Head> <Canvas shadows dpr={[1, 2]}> <PerspectiveCamera makeDefault position={[0, 5, 10]} fov={50} /> <ambientLight intensity={0.5} color="#4040a0" /> <directionalLight position={[10, 20, 5]} intensity={1.5} castShadow shadow-mapSize={[1024, 1024]} /> <Suspense fallback={null}> <Environment preset="city" /> <Stars radius={100} depth={50} count={5000} factor={4} saturation={0} fade speed={1} /> <group> <Player /> <WorldManager /> </group> </Suspense> <fog attach="fog" args={['#050510', 10, 60]} /> </Canvas> <UI /> </div> ); }
Landing Page
This ad does not have a landing page available
Network Timeline
Performance Summary

1

Requests

1

Domains

22KB

Transfer Size

22KB

Content Size

57.0ms

Dom Content Loaded

208.0ms

First Paint

58.0ms

Load Time
Domain Breakdown
Transfer Size (bytes)
Loading...
Content Size (bytes)
Loading...
Header Size (bytes)
Loading...
Requests
Loading...
Timings (ms)
Loading...
Total Time
Loading...
Content Breakdown
Transfer Size (bytes)
Loading...
Content Size (bytes)
Loading...
Header Size (bytes)
Loading...
Requests
Loading...