import React, { useRef, useEffect, useState } from "react"; // ParkourGame.jsx // Single-file React component for a simple 2D parkour game using . // Requirements: Tailwind CSS in your project for styling (optional but recommended). // Controls: // - Arrow keys or A/D to move left/right // - Space to jump (supports double jump) // - Shift to dash (short burst) // - R to restart export default function ParkourGame() { const canvasRef = useRef(null); const animRef = useRef(null); const keys = useRef({}); const [running, setRunning] = useState(true); const [score, setScore] = useState(0); const [message, setMessage] = useState(''); useEffect(() => { const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); let W = (canvas.width = 1000); let H = (canvas.height = 520); // Game world scaling (so we can adapt later) const gravity = 0.9; const friction = 0.9; // Player const player = { x: 80, y: H - 120, w: 34, h: 48, vx: 0, vy: 0, speed: 3.6, maxSpeed: 8, jumpStrength: 15, jumpsLeft: 2, onGround: false, facing: 1, dashCooldown: 0, }; // Level: an array of platforms {x,y,w,h, type} let platforms = []; function makeLevel() { platforms = []; // ground platforms.push({ x: 0, y: H - 60, w: 2000, h: 60, type: 'ground' }); // some static platforms platforms.push({ x: 200, y: H - 140, w: 140, h: 18 }); platforms.push({ x: 380, y: H - 220, w: 120, h: 18 }); platforms.push({ x: 540, y: H - 300, w: 180, h: 18 }); platforms.push({ x: 800, y: H - 200, w: 140, h: 18 }); // moving platform platforms.push({ x: 1060, y: H - 260, w: 120, h: 18, type: 'moving', dir: 1, range: 120, startX: 1060 }); // gap + tall ledge platforms.push({ x: 1320, y: H - 160, w: 110, h: 18 }); platforms.push({ x: 1500, y: H - 300, w: 160, h: 18 }); // final platform and flag (goal) platforms.push({ x: 1820, y: H - 220, w: 180, h: 18, type: 'goal' }); } makeLevel(); // Camera follows player (simple) let camX = 0; // Time let last = performance.now(); function reset() { player.x = 80; player.y = H - 120; player.vx = 0; player.vy = 0; player.jumpsLeft = 2; player.onGround = false; player.dashCooldown = 0; camX = 0; setScore(0); setMessage(''); makeLevel(); } function update(dt) { // input const left = keys.current.ArrowLeft || keys.current.a; const right = keys.current.ArrowRight || keys.current.d; const jump = keys.current.Space; const dash = keys.current.ShiftLeft || keys.current.ShiftRight; // horizontal movement if (left && !right) { player.vx -= player.speed * 0.5; player.facing = -1; } else if (right && !left) { player.vx += player.speed * 0.5; player.facing = 1; } else { player.vx *= friction; } // clamp if (player.vx > player.maxSpeed) player.vx = player.maxSpeed; if (player.vx < -player.maxSpeed) player.vx = -player.maxSpeed; // dash if (dash && player.dashCooldown <= 0) { player.vx += player.facing * 12; player.dashCooldown = 45; // frames-ish } if (player.dashCooldown > 0) player.dashCooldown -= 1; // jump (on keydown -> we handle via keydown listener to allow single press) // gravity player.vy += gravity * (dt * 0.06); // apply velocity player.x += player.vx * (dt * 0.06); player.y += player.vy * (dt * 0.06); // platform movement platforms.forEach((p) => { if (p.type === 'moving') { p.x = p.startX + Math.sin(performance.now() / 500) * p.range; } }); // simple collision with platforms player.onGround = false; for (let p of platforms) { if (player.x + player.w > p.x && player.x < p.x + p.w) { if (player.y + player.h > p.y && player.y + player.h < p.y + p.h + 20 && player.vy >= 0) { // landed on top player.y = p.y - player.h; player.vy = 0; player.onGround = true; player.jumpsLeft = 2; if (p.type === 'goal') { setMessage('YOU MADE IT! Press R to play again.'); setRunning(false); } } } } // fall out of world -> respawn if (player.y > H + 500) { setMessage('You fell! Press R to restart.'); setRunning(false); } // camera camX = Math.max(0, player.x - 250); // scoring = how far right you went setScore(Math.max(0, Math.floor(player.x / 10))); } // Render function draw() { // clear ctx.fillStyle = '#81C7EA'; ctx.fillRect(0, 0, W, H); // sky gradient const g = ctx.createLinearGradient(0, 0, 0, H); g.addColorStop(0, '#9FE7FF'); g.addColorStop(1, '#81C7EA'); ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); ctx.save(); ctx.translate(-camX, 0); // parallax hills ctx.fillStyle = '#6BBF59'; ctx.beginPath(); ctx.ellipse(400, H - 20, 420, 80, 0, 0, Math.PI * 2); ctx.ellipse(1200, H - 20, 420, 80, 0, 0, Math.PI * 2); ctx.fill(); // draw platforms platforms.forEach((p) => { if (p.type === 'ground') ctx.fillStyle = '#5C8A2E'; else if (p.type === 'goal') ctx.fillStyle = '#FFD24D'; else ctx.fillStyle = '#8B5E3C'; ctx.fillRect(p.x, p.y, p.w, p.h); // outline ctx.strokeStyle = 'rgba(0,0,0,0.15)'; ctx.strokeRect(p.x, p.y, p.w, p.h); }); // draw player ctx.fillStyle = '#2B6CF6'; ctx.fillRect(player.x, player.y, player.w, player.h); // eyes (simple) ctx.fillStyle = 'white'; ctx.fillRect(player.x + (player.facing === 1 ? 20 : 6), player.y + 12, 6, 6); ctx.fillStyle = 'black'; ctx.fillRect(player.x + (player.facing === 1 ? 21 : 7), player.y + 13, 3, 3); // draw goal flag platforms.forEach((p) => { if (p.type === 'goal') { ctx.fillStyle = '#333'; ctx.fillRect(p.x + p.w - 22, p.y - 100, 6, 100); ctx.fillStyle = '#ff3333'; ctx.beginPath(); .moveTo } }); // HUD ctx.restore(); ctx.fillStyle = '#012'; ctx.font = '18px system-ui, Arial'; ctx.fillText('Score: ' + score, 14, 26); ctx.fillText('Controls: A/D or ←/→ to move, Space to jump, Shift to dash, R to restart', 14, 48); if (message) { ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(W / 2 - 260, H / 2 - 40, 520, 80); ctx.fillStyle = '#fff'; ctx.font = '20px system-ui, Arial'; ctx.fillText(message, W / 2 - 240, H / 2 + 6); } } // Main loop function loop(now) { const dt = now - last; last = now; if (running) update(dt); draw(); animRef.current = requestAnimationFrame(loop); } // Key handling function onKeyDown(e) { if (e.code === 'Space') { // jump pressed if (player.jumpsLeft > 0 && running) { player.vy = -player.jumpStrength; player.jumpsLeft -= 1; player.onGround = false; } } if (e.code === 'KeyR') { reset(); setRunning(true); } keys.current[e.code] = true; } function onKeyUp(e) { keys.current[e.code] = false; } window.addEventListener('keydown', onKeyDown); window.addEventListener('keyup', onKeyUp); animRef.current = requestAnimationFrame(loop); // cleanup return () => { cancelAnimationFrame(animRef.current); window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); }; }, [running]); // Minimal UI wrapper return (

Mini Parkour — React Canvas

Tip: Use double jump and dash to reach the goal platform on the right.

); }