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((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(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 ( t * t} > {/* Body */} {/* Head */} {!isSliding && ( )} {/* Jetpack Glint */} ); }; // --- 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(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 ( {/* Floor */} {/* Lane Markers */} {[-1, 1].map((offset) => ( ))} {objects.map(obj => { if (!obj.active) return null; const x = obj.lane * LANE_WIDTH; if (obj.type === 'COIN') { return ( ); } if (obj.type === 'TRAIN') { return ( ); } if (obj.type === 'BARRIER_LOW') { return ( ); } if (obj.type === 'BARRIER_HIGH') { return ( {/* Legs */} {/* Top */} ); } return null; })} ); }; 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 ( {segments.map(seg => ( ))} {/* City Background - simplified as static distant meshes moving with player to fake infinity */} ); }; 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(null); useFrame(() => { if (groupRef.current) { groupRef.current.position.z = -playerDist; } }); return ( {buildings.map((b, i) => ( ))} ) } // 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 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 (
{/* HUD */} {status === 'PLAYING' && (
SCORE: {Math.floor(score)} COINS: {coins}
)} {/* Menus */} {status === 'MENU' && (

NEON RUNNER

Swipe or Arrows to Move • Up to Jump • Down to Slide

)} {status === 'GAME_OVER' && (

CRASHED!

Score: {Math.floor(score)}

Coins: {coins}

)} {/* Mobile Control Hints (Fade out) */} {status === 'PLAYING' && score < 100 && (
Swipe to control
)}
); }; export default function GamePage() { return (
Neon Runner 3D
); }