import React, { useState, useEffect, useRef, useReducer, useMemo } from 'react'; import { Trophy, Zap, ShoppingBag, ArrowLeft, Shield, Star, Settings, Activity, Cpu, Layers, HardDrive, Flame, Crosshair, MapPin, Gauge, AlertOctagon, ChevronRight, Lock, Unlock, Play, Pause, RotateCcw } from 'lucide-react'; import { StorageService } from '../services/storageService'; /** * SPEEDZONE: OMEGA PROTOCOL (V3.1) * ================================ * Removed in-game currency. * Progression is now based on Pilot Level (XP). * Game Over awards global Karma points. */ // --- 1. CONSTANTS & CONFIGURATION --- const DB_KEY = 'SPEEDZONE_OMEGA_V2'; // Version bump for data migration const FPS = 60; const LANE_WIDTH = 140; const CANVAS_WIDTH = 600; const CANVAS_HEIGHT = 800; // Color Palettes for different Biomes const BIOMES = [ { id: 'NEON_CITY', name: 'Neo-Tokyo', road: '#1e293b', line: '#facc15', grid: 'rgba(250, 204, 21, 0.1)', fog: 'rgba(30, 41, 59, 0)', sky: '#0f172a' }, { id: 'WASTELAND', name: 'Mars Outpost', road: '#2b1212', line: '#f97316', grid: 'rgba(249, 115, 22, 0.1)', fog: 'rgba(43, 18, 18, 0)', sky: '#1a0505' }, { id: 'VOID', name: 'The Null Zone', road: '#000000', line: '#a855f7', grid: 'rgba(168, 85, 247, 0.15)', fog: 'rgba(0,0,0,0)', sky: '#000000' } ]; // Ships unlock at specific levels const SHIPS = [ { id: 'alpha', name: 'Yellow Cab', requiredLevel: 1, baseSpeed: 1.0, baseHandling: 1.0, baseNitro: 1.0, color: '#facc15' }, { id: 'beta', name: 'Turbo Taxi', requiredLevel: 5, baseSpeed: 1.1, baseHandling: 0.8, baseNitro: 1.3, color: '#fbbf24' }, { id: 'gamma', name: 'Crazy Cab', requiredLevel: 10, baseSpeed: 1.3, baseHandling: 1.4, baseNitro: 1.1, color: '#f59e0b' }, { id: 'omega', name: 'King Cab', requiredLevel: 20, baseSpeed: 1.6, baseHandling: 1.5, baseNitro: 2.0, color: '#d97706' }, ]; const POWERUPS = { SHIELD: { color: '#06b6d4', duration: 5000 }, MAGNET: { color: '#eab308', duration: 8000 }, DOUBLE: { color: '#8b5cf6', duration: 10000 }, }; // --- 2. TYPES & INTERFACES --- type GameState = 'MENU' | 'PLAYING' | 'PAUSED' | 'GAMEOVER' | 'GARAGE'; interface Entity { id: string; x: number; y: number; w: number; h: number; type: 'PLAYER' | 'RIVAL' | 'HAZARD_STATIC' | 'COIN' | 'POWERUP'; subtype?: string; // e.g., 'TRUCK', 'SHIELD' color: string; speed: number; lane: number; dead?: boolean; } interface Particle { id: number; x: number; y: number; vx: number; vy: number; life: number; // 0.0 to 1.0 decay: number; color: string; size: number; type: 'SPARK' | 'SMOKE' | 'GLOW' | 'TEXT' | 'FIRE'; text?: string; } interface UserProfile { xp: number; level: number; highScore: number; activeShip: string; upgrades: { speed: number; // Level 0-5 handling: number; // Level 0-5 nitro: number; // Level 0-5 }; } // --- 3. UTILITY FUNCTIONS --- const randomRange = (min: number, max: number) => Math.random() * (max - min) + min; const lerp = (start: number, end: number, t: number) => start * (1 - t) + end * t; const checkRectCollision = (r1: Entity, r2: Entity, padding = 0) => { return ( r1.x < r2.x + r2.w - padding && r1.x + r1.w > r2.x + padding && r1.y < r2.y + r2.h - padding && r1.h + r1.y > r2.y + padding ); }; // --- 4. MAIN ENGINE COMPONENT --- export const SpeedZone: React.FC<{ onExit: () => void }> = ({ onExit }) => { // --- REACT STATE (UI) --- const canvasRef = useRef(null); const [gameState, setGameState] = useState('MENU'); const [profile, setProfile] = useState(() => { try { const saved = localStorage.getItem(DB_KEY); return saved ? JSON.parse(saved) : { xp: 0, level: 1, highScore: 0, activeShip: 'alpha', upgrades: { speed: 0, handling: 0, nitro: 0 } }; } catch { return { xp: 0, level: 1, highScore: 0, activeShip: 'alpha', upgrades: { speed: 0, handling: 0, nitro: 0 } }; } }); // Persistent Save useEffect(() => { localStorage.setItem(DB_KEY, JSON.stringify(profile)); }, [profile]); // --- GAME ENGINE STATE (MUTABLE REFS) --- const engine = useRef({ // Core lastTime: 0, deltaTime: 0, score: 0, distance: 0, speed: 0, targetSpeed: 0, // World biomeIdx: 0, roadOffset: 0, shake: 0, // Player lane: 1, // 0, 1, 2, 3 x: 300, y: 600, nitro: 100, // FUEL: 0-100 isNitro: false, shield: 0, // Time remaining magnet: 0, doublePoints: 0, // Entities entities: [] as Entity[], particles: [] as Particle[], // Timers lastSpawn: 0, }); const requestRef = useRef(0); const triggerShake = (amount: number) => { engine.current.shake = amount; }; const spawnParticle = (x: number, y: number, type: Particle['type'], color: string, count = 1, text?: string) => { for (let i = 0; i < count; i++) { engine.current.particles.push({ id: Math.random(), x, y, vx: (Math.random() - 0.5) * (type === 'SPARK' ? 10 : type === 'FIRE' ? 2 : 4), vy: (Math.random() - 0.5) * (type === 'SPARK' ? 10 : 4) + (type === 'SMOKE' ? 5 : type === 'FIRE' ? 10 : 0), life: 1.0, decay: type === 'TEXT' ? 0.015 : type === 'FIRE' ? 0.05 : randomRange(0.02, 0.05), color, size: randomRange(2, type === 'SMOKE' ? 15 : type === 'FIRE' ? 20 : 6), type, text }); } }; const spawnEntity = (currentSpeed: number) => { const lane = Math.floor(Math.random() * 4); // 4 Lanes const x = (lane * LANE_WIDTH) + (CANVAS_WIDTH - (4 * LANE_WIDTH)) / 2 + (LANE_WIDTH/2) - 35; // Centered const typeRoll = Math.random(); let entity: Entity = { id: Math.random().toString(), x, y: -200, w: 70, h: 140, lane, speed: 0, type: 'HAZARD_STATIC', color: '#fff' }; if (typeRoll > 0.95) { entity.type = 'POWERUP'; entity.w = 40; entity.h = 40; entity.color = '#fff'; const pType = Math.random(); if (pType < 0.33) { entity.subtype = 'SHIELD'; entity.color = POWERUPS.SHIELD.color; } else if (pType < 0.66) { entity.subtype = 'MAGNET'; entity.color = POWERUPS.MAGNET.color; } else { entity.subtype = 'DOUBLE'; entity.color = POWERUPS.DOUBLE.color; } } else if (typeRoll > 0.8) { // Coin acts as small XP boost entity.type = 'COIN'; entity.w = 30; entity.h = 30; entity.color = '#fbbf24'; } else if (typeRoll > 0.6) { entity.type = 'RIVAL'; entity.w = 70; entity.h = 130; entity.color = '#1e293b'; entity.speed = currentSpeed * 0.7; } else { entity.type = 'HAZARD_STATIC'; entity.w = 80; entity.h = 150; entity.color = '#64748b'; entity.speed = currentSpeed * 0.4; } engine.current.entities.push(entity); }; const update = (time: number) => { if (gameState !== 'PLAYING') return; const S = engine.current; if (!S.lastTime) S.lastTime = time; const dt = (time - S.lastTime) / 16.67; S.lastTime = time; S.deltaTime = dt; const activeShip = SHIPS.find(s => s.id === profile.activeShip) || SHIPS[0]; const upgrades = profile.upgrades; const maxSpeed = (activeShip.baseSpeed + (upgrades.speed * 0.1)) * 20; const handling = (activeShip.baseHandling + (upgrades.handling * 0.1)) * 0.2; const nitroBurn = 0.5 - (upgrades.nitro * 0.05); if (S.isNitro && S.nitro > 0) { S.targetSpeed = maxSpeed * 2.0; S.nitro -= nitroBurn * dt; triggerShake(3); if (Math.random() > 0.3) { spawnParticle(S.x + 15, S.y + 130, 'FIRE', '#ef4444', 1); spawnParticle(S.x + 55, S.y + 130, 'FIRE', '#f59e0b', 1); } } else { S.targetSpeed = maxSpeed; } S.nitro = Math.max(0, Math.min(100, S.nitro)); S.speed = lerp(S.speed, S.targetSpeed, 0.05 * dt); const oldDistanceBlock = Math.floor(S.distance / 100); S.distance += S.speed * dt; const newDistanceBlock = Math.floor(S.distance / 100); if (newDistanceBlock > oldDistanceBlock) { S.nitro = Math.min(100, S.nitro + 10); } S.score += Math.floor(S.speed * 0.1 * (S.doublePoints > 0 ? 2 : 1) * dt); const targetX = (S.lane * LANE_WIDTH) + (CANVAS_WIDTH - (4 * LANE_WIDTH)) / 2 + (LANE_WIDTH/2) - 35; S.x = lerp(S.x, targetX, handling * dt); S.x = Math.max(0, Math.min(CANVAS_WIDTH - 70, S.x)); if (S.shield > 0) S.shield -= 16 * dt; if (S.magnet > 0) S.magnet -= 16 * dt; if (S.doublePoints > 0) S.doublePoints -= 16 * dt; const spawnRate = Math.max(400, 1500 - (S.speed * 20)); if (time - S.lastSpawn > spawnRate) { spawnEntity(S.speed); S.lastSpawn = time; } S.entities.forEach((e) => { const relSpeed = S.speed - e.speed; e.y += relSpeed * dt; if (e.type === 'RIVAL') { e.speed += 0.01 * dt; } if (S.magnet > 0 && e.type === 'COIN') { e.x = lerp(e.x, S.x, 0.1 * dt); e.y = lerp(e.y, S.y, 0.1 * dt); } const playerRect: Entity = { id: 'player', x: S.x + 10, y: S.y + 10, w: 50, h: 110, type: 'PLAYER', lane: 0, speed: 0, color: '' }; if (checkRectCollision(playerRect, e)) { if (e.type === 'COIN') { e.dead = true; // Coins act as small score/xp boosts now, no credits S.score += 50; spawnParticle(e.x, e.y, 'TEXT', '#fbbf24', 1, '+50'); } else if (e.type === 'POWERUP') { e.dead = true; if (e.subtype === 'SHIELD') S.shield = POWERUPS.SHIELD.duration; if (e.subtype === 'MAGNET') S.magnet = POWERUPS.MAGNET.duration; if (e.subtype === 'DOUBLE') S.doublePoints = POWERUPS.DOUBLE.duration; spawnParticle(S.x, S.y, 'GLOW', e.color, 10); } else if (!e.dead) { if (S.shield > 0) { e.dead = true; S.shield = 0; triggerShake(10); spawnParticle(e.x, e.y, 'SPARK', '#06b6d4', 20); } else { handleGameOver(); } } } if (e.y > CANVAS_HEIGHT + 100) e.dead = true; }); S.entities = S.entities.filter(e => !e.dead); for (let i = S.particles.length - 1; i >= 0; i--) { const p = S.particles[i]; p.x += p.vx * dt; p.y += p.vy * dt; p.life -= p.decay * dt; if (p.life <= 0) S.particles.splice(i, 1); } if (S.shake > 0) S.shake *= 0.9; draw(); requestRef.current = requestAnimationFrame(update); }; const draw = () => { const ctx = canvasRef.current?.getContext('2d'); if (!ctx) return; const S = engine.current; const biome = BIOMES[S.biomeIdx]; ctx.save(); if (S.shake > 0.5) { ctx.translate((Math.random() - 0.5) * S.shake, (Math.random() - 0.5) * S.shake); } const grad = ctx.createLinearGradient(0, 0, 0, CANVAS_HEIGHT); grad.addColorStop(0, biome.sky); grad.addColorStop(1, biome.road); ctx.fillStyle = grad; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); ctx.strokeStyle = biome.grid; ctx.lineWidth = 2; const offsetX = (CANVAS_WIDTH - (4 * LANE_WIDTH)) / 2; for (let i = 0; i <= 4; i++) { const x = offsetX + (i * LANE_WIDTH); ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, CANVAS_HEIGHT); ctx.stroke(); } S.roadOffset = (S.roadOffset + S.speed * 0.5) % 100; ctx.strokeStyle = biome.grid; for (let i = 0; i < 20; i++) { const y = (i * 50 + S.roadOffset) % CANVAS_HEIGHT; ctx.beginPath(); ctx.moveTo(offsetX, y); ctx.lineTo(CANVAS_WIDTH - offsetX, y); ctx.stroke(); } S.entities.forEach(e => { ctx.save(); ctx.translate(e.x + e.w/2, e.y + e.h/2); ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(-e.w/2 + 10, -e.h/2 + 10, e.w, e.h); if (e.type === 'COIN') { ctx.fillStyle = e.color; ctx.beginPath(); ctx.arc(0, 0, e.w/2, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = '20px monospace'; ctx.textAlign = 'center'; ctx.fillText('★', 0, 6); } else if (e.type === 'POWERUP') { ctx.fillStyle = e.color; ctx.rotate(Date.now() / 200); ctx.fillRect(-e.w/2, -e.h/2, e.w, e.h); } else { ctx.fillStyle = e.color; ctx.beginPath(); ctx.roundRect(-e.w/2, -e.h/2, e.w, e.h, 10); ctx.fill(); ctx.fillStyle = '#ef4444'; ctx.fillRect(-e.w/2 + 5, e.h/2 - 10, 15, 5); ctx.fillRect(e.w/2 - 20, e.h/2 - 10, 15, 5); ctx.fillStyle = '#334155'; ctx.fillRect(-e.w/2 + 5, -e.h/2 + 10, e.w - 10, 20); } ctx.restore(); }); const shipDef = SHIPS.find(s => s.id === profile.activeShip); ctx.save(); ctx.translate(S.x + 35, S.y + 70); const tilt = (S.lane * LANE_WIDTH + offsetX + LANE_WIDTH/2 - 35 - S.x) * -0.08; ctx.rotate(tilt * Math.PI / 180); if (S.shield > 0) { ctx.shadowBlur = 20; ctx.shadowColor = '#06b6d4'; ctx.strokeStyle = '#06b6d4'; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(0, 0, 80, 0, Math.PI*2); ctx.stroke(); } ctx.fillStyle = shipDef?.color || '#facc15'; ctx.shadowBlur = S.isNitro ? 20 : 5; ctx.shadowColor = shipDef?.color || '#facc15'; ctx.beginPath(); ctx.roundRect(-30, -60, 60, 120, 12); ctx.fill(); ctx.fillStyle = '#eab308'; ctx.beginPath(); ctx.roundRect(-25, -20, 50, 50, 8); ctx.fill(); ctx.fillStyle = '#1e293b'; ctx.beginPath(); ctx.roundRect(-22, -45, 44, 15, 4); ctx.fill(); ctx.beginPath(); ctx.roundRect(-22, 35, 44, 10, 4); ctx.fill(); ctx.fillStyle = '#fff'; ctx.fillRect(-15, -5, 30, 10); ctx.fillStyle = '#000'; ctx.font = 'bold 8px monospace'; ctx.textAlign = 'center'; ctx.fillText('TAXI', 0, 3); ctx.fillStyle = '#000'; for(let i=0; i<6; i++) { if (i%2===0) ctx.fillRect(-30, -50 + i*20, 5, 10); else ctx.fillRect(25, -50 + i*20, 5, 10); } ctx.fillStyle = '#fef08a'; ctx.shadowBlur = 10; ctx.shadowColor = '#fef08a'; ctx.beginPath(); ctx.roundRect(-25, -62, 12, 6, 2); ctx.roundRect(13, -62, 12, 6, 2); ctx.fill(); ctx.shadowBlur = 0; ctx.fillStyle = '#dc2626'; ctx.beginPath(); ctx.roundRect(-25, 58, 12, 4, 2); ctx.roundRect(13, 58, 12, 4, 2); ctx.fill(); if (S.isNitro && S.nitro > 0) { ctx.globalCompositeOperation = 'lighter'; ctx.fillStyle = '#ef4444'; ctx.beginPath(); ctx.moveTo(-18, 62); ctx.lineTo(-24, 62 + Math.random() * 40 + 20); ctx.lineTo(-12, 62); ctx.fill(); ctx.beginPath(); ctx.moveTo(18, 62); ctx.lineTo(24, 62 + Math.random() * 40 + 20); ctx.lineTo(12, 62); ctx.fill(); ctx.fillStyle = '#facc15'; ctx.beginPath(); ctx.moveTo(-18, 62); ctx.lineTo(-22, 62 + Math.random() * 20 + 10); ctx.lineTo(-14, 62); ctx.fill(); ctx.beginPath(); ctx.moveTo(18, 62); ctx.lineTo(22, 62 + Math.random() * 20 + 10); ctx.lineTo(14, 62); ctx.fill(); ctx.globalCompositeOperation = 'source-over'; } ctx.restore(); S.particles.forEach(p => { ctx.globalAlpha = p.life; if (p.type === 'TEXT') { ctx.fillStyle = '#fff'; ctx.font = 'bold 20px Arial'; ctx.fillText(p.text || '', p.x, p.y); } else if (p.type === 'FIRE') { ctx.fillStyle = p.color; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x - p.size/2, p.y + p.size); ctx.lineTo(p.x + p.size/2, p.y + p.size); ctx.fill(); } else { ctx.fillStyle = p.color; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI*2); ctx.fill(); } }); ctx.globalAlpha = 1.0; const gradV = ctx.createRadialGradient(CANVAS_WIDTH/2, CANVAS_HEIGHT/2, 200, CANVAS_WIDTH/2, CANVAS_HEIGHT/2, 500); gradV.addColorStop(0, 'transparent'); gradV.addColorStop(1, 'rgba(0,0,0,0.8)'); ctx.fillStyle = gradV; ctx.fillRect(0,0,CANVAS_WIDTH, CANVAS_HEIGHT); ctx.restore(); }; const handleGameOver = () => { setGameState('GAMEOVER'); const finalScore = Math.floor(engine.current.score); // Award Karma to global profile with transaction log StorageService.addPoints(Math.floor(finalScore / 10), finalScore, 'game_reward', 'Speed Zone Completion'); // Update Local Profile Level const xpEarned = finalScore; setProfile(prev => ({ ...prev, xp: prev.xp + xpEarned, level: Math.floor((prev.xp + xpEarned) / 1000) + 1, highScore: Math.max(prev.highScore, finalScore) })); }; const handleStart = () => { engine.current = { ...engine.current, score: 0, distance: 0, speed: 0, entities: [], particles: [], shield: 0, magnet: 0, doublePoints: 0, nitro: 100, lastTime: 0 }; setGameState('PLAYING'); requestRef.current = requestAnimationFrame(update); }; const handleTouch = (e: React.TouchEvent | React.MouseEvent) => { if (gameState !== 'PLAYING') return; const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return; const clientX = 'touches' in e ? e.touches[0].clientX : (e as React.MouseEvent).clientX; const x = clientX - rect.left; const mid = rect.width / 2; if (x < mid && engine.current.lane > 0) engine.current.lane--; else if (x >= mid && engine.current.lane < 3) engine.current.lane++; }; const handleInput = (e: KeyboardEvent) => { if (gameState !== 'PLAYING') return; if (e.key === 'ArrowLeft' && engine.current.lane > 0) engine.current.lane--; if (e.key === 'ArrowRight' && engine.current.lane < 3) engine.current.lane++; if (e.key === 'ArrowUp') engine.current.isNitro = true; if (e.key === 'Escape') setGameState(prev => prev === 'PAUSED' ? 'PLAYING' : 'PAUSED'); }; const handleKeyUp = (e: KeyboardEvent) => { if (e.key === 'ArrowUp') engine.current.isNitro = false; }; useEffect(() => { window.addEventListener('keydown', handleInput); window.addEventListener('keyup', handleKeyUp); return () => { window.removeEventListener('keydown', handleInput); window.removeEventListener('keyup', handleKeyUp); cancelAnimationFrame(requestRef.current); }; }, [gameState]); useEffect(() => { if (gameState === 'PLAYING') { engine.current.lastTime = performance.now(); requestRef.current = requestAnimationFrame(update); } }, [gameState]); return (
{gameState === 'PLAYING' && (
{Math.floor(engine.current.speed * 6)} MPH
SCORE: {Math.floor(engine.current.score)}
{engine.current.shield > 0 &&
SHIELD ACTIVE
} {engine.current.magnet > 0 &&
MAGNET ACTIVE
} {engine.current.doublePoints > 0 &&
2X SCORE
}
Nitro Fuel {Math.floor(engine.current.nitro)}%

+10% Charge per 100m

)}
{gameState === 'MENU' && (
Omega Protocol V3.1.0

Speed
Zone

Pilot Rank
LVL {profile.level}
Best Score
{profile.highScore.toLocaleString()}

Ready to Race

Earn Pilot XP to unlock advanced chassis prototypes. Global Karma awarded on completion.

)} {/* --- GARAGE UI --- */} {gameState === 'GARAGE' && (

The Hangar

Level Up to Unlock

Current Level
{profile.level}

Chassis Selection

{SHIPS.map(ship => { const unlocked = profile.level >= ship.requiredLevel; const active = profile.activeShip === ship.id; return ( ); })}

Performance Tuning

{[ { id: 'speed', label: 'Engine Output', icon: Gauge, desc: 'Increases top speed cap.' }, { id: 'handling', label: 'Traction Control', icon: Activity, desc: 'Improves lane switching response.' }, { id: 'nitro', label: 'Reactor Efficiency', icon: Flame, desc: 'Reduces nitro fuel consumption.' } ].map((stat: any) => { // Upgrades unlock every 5 levels for simplicity in this no-currency model // Actually, let's just make upgrades tied to level. // Level 1 = 0 upgrades. Level 2 = 1 upgrade available to apply? // Simplest: Auto-scale upgrades with Level up to max 5. const level = Math.min(5, Math.floor((profile.level - 1) / 2)); return (
{stat.label}
{stat.desc}
TIER {level}
{[0,1,2,3,4].map(i => (
))}

Auto-scales with Pilot Level

); })}
)} {gameState === 'GAMEOVER' && (

Wrecked

Critical Hull Failure

Total Distance
{Math.floor(engine.current.distance)}M
Score
{Math.floor(engine.current.score)}
)}
); }; export default SpeedZone;