A Pen by mode-mercury on CodePen.
Created
May 27, 2025 02:59
-
-
Save mode-mercury/1dc75d21ccaac595da39029b7674638f to your computer and use it in GitHub Desktop.
Untitled
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> | |
| <title>Cyberpunk ASCII Hoverboard</title> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| colors: { | |
| neon: { | |
| pink: '#ff00ff', | |
| blue: '#00ffff', | |
| green: '#00ff00', | |
| yellow: '#ffff00' | |
| }, | |
| cyber: { | |
| dark: '#0D0221', | |
| medium: '#190535', | |
| light: '#2B0F54' | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| @keyframes glitch { | |
| 0% { transform: translate(0) } | |
| 20% { transform: translate(-2px, 2px) } | |
| 40% { transform: translate(-2px, -2px) } | |
| 60% { transform: translate(2px, 2px) } | |
| 80% { transform: translate(2px, -2px) } | |
| 100% { transform: translate(0) } | |
| } | |
| @keyframes pulse { | |
| 0% { transform: scale(1); opacity: 1; } | |
| 50% { transform: scale(1.1); opacity: 0.8; } | |
| 100% { transform: scale(1); opacity: 1; } | |
| } | |
| @keyframes float { | |
| 0% { transform: translateY(0); } | |
| 50% { transform: translateY(-10px); } | |
| 100% { transform: translateY(0); } | |
| } | |
| .glitch { | |
| animation: glitch 0.2s infinite; | |
| animation-play-state: paused; | |
| } | |
| .active-glitch { | |
| animation-play-state: running; | |
| } | |
| .scanlines::before { | |
| content: ""; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: repeating-linear-gradient( | |
| 0deg, | |
| rgba(0, 0, 0, 0.15), | |
| rgba(0, 0, 0, 0.15) 1px, | |
| transparent 1px, | |
| transparent 2px | |
| ); | |
| pointer-events: none; | |
| z-index: 20; | |
| } | |
| body { | |
| touch-action: none; | |
| overflow: hidden; | |
| } | |
| canvas { | |
| display: block; | |
| } | |
| #game-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| overflow: hidden; | |
| background-color: #0D0221; | |
| background-image: | |
| radial-gradient(circle at 30% 20%, rgba(255, 0, 255, 0.1), transparent 20%), | |
| radial-gradient(circle at 70% 60%, rgba(0, 255, 255, 0.1), transparent 20%); | |
| } | |
| /* ASCII effect styles */ | |
| #ascii-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| font-family: monospace; | |
| color: #fff; | |
| font-size: 8px; | |
| line-height: 8px; | |
| overflow: hidden; | |
| white-space: pre; | |
| pointer-events: none; | |
| opacity: 0.7; | |
| z-index: 10; | |
| mix-blend-mode: lighten; | |
| } | |
| .dark .score-container { | |
| background-color: rgba(10, 10, 10, 0.7); | |
| color: #00ffff; | |
| } | |
| .score-container { | |
| background-color: rgba(230, 230, 230, 0.7); | |
| color: #ff00ff; | |
| } | |
| .trick-text { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| font-family: monospace; | |
| font-size: 2rem; | |
| font-weight: bold; | |
| text-transform: uppercase; | |
| color: #ff00ff; | |
| text-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| pointer-events: none; | |
| z-index: 15; | |
| } | |
| .show-trick { | |
| opacity: 1; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-container" class="scanlines"> | |
| <div id="ascii-overlay"></div> | |
| <div id="trick-display" class="trick-text"></div> | |
| <div class="fixed top-4 left-4 right-4 flex justify-between z-10"> | |
| <div class="score-container rounded-lg p-2 font-mono text-lg font-bold"> | |
| SCORE: <span id="score">0</span> | |
| </div> | |
| <div class="score-container rounded-lg p-2 font-mono text-lg font-bold"> | |
| TRICKS: <span id="tricks">0</span> | |
| </div> | |
| </div> | |
| <div class="fixed bottom-4 left-4 z-10"> | |
| <div id="combo-meter" class="score-container rounded-lg p-2 font-mono text-lg font-bold hidden"> | |
| COMBO: x<span id="combo-count">0</span> | |
| </div> | |
| </div> | |
| <div class="fixed bottom-4 right-4 z-10"> | |
| <div id="power-up-indicator" class="score-container rounded-lg p-2 font-mono text-lg font-bold hidden"> | |
| <span id="power-up-type">SHIELD</span>: <span id="power-up-time">0</span>s | |
| </div> | |
| </div> | |
| <div id="level-up" class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 font-mono text-4xl font-bold text-neon-green z-20 hidden"> | |
| LEVEL UP! | |
| </div> | |
| <div id="start-screen" class="fixed inset-0 flex flex-col items-center justify-center bg-cyber-dark z-30 text-white font-mono"> | |
| <h1 class="text-4xl font-bold mb-6 text-neon-blue glitch">CYB3R_H0V3R</h1> | |
| <div class="mb-8 text-center"> | |
| <p class="text-xl mb-4 text-neon-pink">Top-down cyberpunk hoverboard</p> | |
| <p class="mb-2">• Use two fingers to control the board</p> | |
| <p class="mb-2">• Swipe to move and lean</p> | |
| <p class="mb-2">• Flick quickly to perform tricks</p> | |
| <p class="text-neon-green mt-4">T4P T0 B3GIN</p> | |
| </div> | |
| </div> | |
| <div id="game-over" class="fixed inset-0 flex flex-col items-center justify-center bg-cyber-dark z-30 text-white font-mono hidden"> | |
| <h1 class="text-4xl font-bold mb-6 text-neon-pink glitch">SYST3M_CR4SH</h1> | |
| <div class="mb-8 text-center"> | |
| <p class="text-xl mb-4">FINAL SCORE: <span id="final-score">0</span></p> | |
| <p class="text-xl mb-4">TRICKS LANDED: <span id="final-tricks">0</span></p> | |
| </div> | |
| <button id="restart-button" class="px-6 py-3 bg-neon-blue text-cyber-dark font-bold rounded hover:bg-neon-pink transition-colors"> | |
| R3B00T_SYST3M | |
| </button> | |
| </div> | |
| </div> | |
| <script> | |
| // Game global variables | |
| let scene, camera, renderer, board, player; | |
| let obstacles = []; | |
| let powerUps = []; | |
| let environment = []; | |
| let score = 0; | |
| let tricks = 0; | |
| let isMoving = false; | |
| let speed = 0; | |
| let velocity = new THREE.Vector3(0, 0, 0); | |
| let rotation = 0; | |
| let isGameRunning = false; | |
| let asciiChars = '@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,"^`\'. '; | |
| let touchStartPos = { x: 0, y: 0 }; | |
| let multiTouchStartPos = []; | |
| let lastTouchEndTime = 0; | |
| let isTrickInProgress = false; | |
| let obstacleSpawnTimer = 0; | |
| let powerUpSpawnTimer = 0; | |
| let gameTime = 0; | |
| let difficulty = 1; | |
| let hasShield = false; | |
| let shieldEndTime = 0; | |
| let lastGlitchUpdateTime = 0; | |
| let comboCount = 0; | |
| let comboTimer = 0; | |
| // Initialize the game | |
| function init() { | |
| initScene(); | |
| createBoard(); | |
| createEnvironment(); | |
| generateAsciiOverlay(); | |
| // Start animation loop | |
| animate(); | |
| // Set up event listeners | |
| window.addEventListener('resize', onWindowResize); | |
| document.getElementById('game-container').addEventListener('touchstart', handleTouchStart, false); | |
| document.getElementById('game-container').addEventListener('touchmove', handleTouchMove, false); | |
| document.getElementById('game-container').addEventListener('touchend', handleTouchEnd, false); | |
| // Start screen events | |
| document.getElementById('start-screen').addEventListener('click', startGame, false); | |
| document.getElementById('restart-button').addEventListener('click', restartGame, false); | |
| // Add tap to start text that's more visible | |
| const startText = document.createElement('div'); | |
| startText.className = 'text-3xl font-bold text-neon-green mt-6 animate-pulse'; | |
| startText.textContent = 'TAP ANYWHERE TO START'; | |
| document.getElementById('start-screen').appendChild(startText); | |
| // Dark mode detection | |
| if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { | |
| document.documentElement.classList.add('dark'); | |
| } | |
| window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { | |
| if (event.matches) { | |
| document.documentElement.classList.add('dark'); | |
| } else { | |
| document.documentElement.classList.remove('dark'); | |
| } | |
| }); | |
| } | |
| function startGame() { | |
| document.getElementById('start-screen').classList.add('hidden'); | |
| isGameRunning = true; | |
| score = 0; | |
| tricks = 0; | |
| updateScore(); | |
| updateTricks(); | |
| } | |
| function restartGame() { | |
| // Remove old obstacles | |
| for (let i = obstacles.length - 1; i >= 0; i--) { | |
| scene.remove(obstacles[i]); | |
| obstacles.splice(i, 1); | |
| } | |
| // Reset player position | |
| board.position.set(0, 0.5, 0); | |
| velocity.set(0, 0, 0); | |
| rotation = 0; | |
| board.rotation.y = 0; | |
| // Reset score and tricks | |
| score = 0; | |
| tricks = 0; | |
| updateScore(); | |
| updateTricks(); | |
| // Hide game over screen | |
| document.getElementById('game-over').classList.add('hidden'); | |
| isGameRunning = true; | |
| } | |
| function gameOver() { | |
| isGameRunning = false; | |
| document.getElementById('final-score').textContent = score; | |
| document.getElementById('final-tricks').textContent = tricks; | |
| document.getElementById('game-over').classList.remove('hidden'); | |
| } | |
| function initScene() { | |
| // Create scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x0D0221); | |
| // Create camera | |
| camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(0, 10, 12); | |
| camera.lookAt(0, 0, 0); | |
| // Create renderer | |
| renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| document.getElementById('game-container').appendChild(renderer.domElement); | |
| // Add lights | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(5, 10, 7.5); | |
| scene.add(directionalLight); | |
| // Add point lights for cyberpunk feel | |
| const pinkLight = new THREE.PointLight(0xff00ff, 1, 15); | |
| pinkLight.position.set(-5, 2, -5); | |
| scene.add(pinkLight); | |
| const cyanLight = new THREE.PointLight(0x00ffff, 1, 15); | |
| cyanLight.position.set(5, 2, -5); | |
| scene.add(cyanLight); | |
| } | |
| function createBoard() { | |
| // Create hoverboard geometry | |
| const boardGeometry = new THREE.BoxGeometry(2, 0.2, 4); | |
| const boardMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x00ffff, | |
| emissive: 0x003333, | |
| specular: 0x00ffff, | |
| shininess: 30 | |
| }); | |
| board = new THREE.Mesh(boardGeometry, boardMaterial); | |
| board.position.y = 0.5; | |
| scene.add(board); | |
| // Add player model (simple representation) | |
| const playerGeometry = new THREE.CylinderGeometry(0.3, 0.5, 1.8, 8); | |
| const playerMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0xff00ff, | |
| emissive: 0x330033, | |
| specular: 0xffffff, | |
| shininess: 20 | |
| }); | |
| player = new THREE.Mesh(playerGeometry, playerMaterial); | |
| player.position.y = 1.5; | |
| board.add(player); | |
| // Add board glow | |
| const boardLight = new THREE.PointLight(0x00ffff, 1, 5); | |
| boardLight.position.set(0, -0.5, 0); | |
| board.add(boardLight); | |
| } | |
| function createEnvironment() { | |
| // Create floor | |
| const floorGeometry = new THREE.PlaneGeometry(100, 100); | |
| const floorMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0x190535, | |
| side: THREE.DoubleSide, | |
| wireframe: true | |
| }); | |
| const floor = new THREE.Mesh(floorGeometry, floorMaterial); | |
| floor.rotation.x = Math.PI / 2; | |
| scene.add(floor); | |
| environment.push(floor); | |
| // Add grid lines for cyberpunk effect | |
| const gridHelper = new THREE.GridHelper(100, 50, 0xff00ff, 0x00ffff); | |
| scene.add(gridHelper); | |
| environment.push(gridHelper); | |
| // Add some decorative elements | |
| for (let i = 0; i < 20; i++) { | |
| const pillarGeometry = new THREE.BoxGeometry(1, Math.random() * 5 + 1, 1); | |
| const pillarMaterial = new THREE.MeshPhongMaterial({ | |
| color: Math.random() > 0.5 ? 0xff00ff : 0x00ffff, | |
| emissive: Math.random() > 0.5 ? 0x330033 : 0x003333, | |
| wireframe: true | |
| }); | |
| const pillar = new THREE.Mesh(pillarGeometry, pillarMaterial); | |
| const x = Math.random() * 80 - 40; | |
| const z = Math.random() * 80 - 40; | |
| pillar.position.set(x, pillarGeometry.parameters.height / 2, z); | |
| scene.add(pillar); | |
| environment.push(pillar); | |
| } | |
| } | |
| function generateAsciiOverlay() { | |
| const overlay = document.getElementById('ascii-overlay'); | |
| let asciiGrid = ''; | |
| const rows = Math.ceil(window.innerHeight / 8); | |
| const cols = Math.ceil(window.innerWidth / 4); | |
| for (let i = 0; i < rows; i++) { | |
| for (let j = 0; j < cols; j++) { | |
| asciiGrid += asciiChars[Math.floor(Math.random() * asciiChars.length)]; | |
| } | |
| asciiGrid += '\n'; | |
| } | |
| overlay.textContent = asciiGrid; | |
| } | |
| function createObstacle() { | |
| const types = [ | |
| { geo: new THREE.BoxGeometry(2, 1, 2), color: 0xff00ff }, | |
| { geo: new THREE.ConeGeometry(1, 2, 6), color: 0x00ffff }, | |
| { geo: new THREE.SphereGeometry(1, 8, 8), color: 0x00ff00 }, | |
| { geo: new THREE.TorusGeometry(1, 0.4, 8, 16), color: 0xff8800 }, | |
| { geo: new THREE.TetrahedronGeometry(1.2), color: 0xff0088 }, | |
| { geo: new THREE.OctahedronGeometry(1), color: 0x8800ff } | |
| ]; | |
| const selectedType = types[Math.floor(Math.random() * types.length)]; | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: selectedType.color, | |
| wireframe: true, | |
| transparent: true, | |
| opacity: 0.8 | |
| }); | |
| const obstacle = new THREE.Mesh(selectedType.geo, material); | |
| // Position the obstacle in front of the player but at random x position | |
| const x = Math.random() * 30 - 15; | |
| const z = -50 - Math.random() * 20; // Far ahead of the player | |
| obstacle.position.set(x, 1, z); | |
| // Add rotation for more visual interest | |
| obstacle.rotation.x = Math.random() * Math.PI; | |
| obstacle.rotation.y = Math.random() * Math.PI; | |
| obstacle.rotation.z = Math.random() * Math.PI; | |
| scene.add(obstacle); | |
| obstacles.push(obstacle); | |
| // Add point light to obstacle for glow effect | |
| const light = new THREE.PointLight(selectedType.color, 0.7, 5); | |
| light.position.set(0, 0, 0); | |
| obstacle.add(light); | |
| // Add auto-rotation animation | |
| obstacle.userData = { | |
| rotationSpeed: { | |
| x: (Math.random() - 0.5) * 0.02, | |
| y: (Math.random() - 0.5) * 0.02, | |
| z: (Math.random() - 0.5) * 0.02 | |
| } | |
| }; | |
| } | |
| function createPowerUp() { | |
| // Different types of power-ups | |
| const powerUpTypes = [ | |
| { type: 'shield', geo: new THREE.IcosahedronGeometry(1), color: 0x00ffff, duration: 10 }, | |
| { type: 'speed', geo: new THREE.OctahedronGeometry(1), color: 0xff00ff, duration: 8 }, | |
| { type: 'points', geo: new THREE.DodecahedronGeometry(1), color: 0xffff00, duration: 0 }, | |
| { type: 'trick_boost', geo: new THREE.TorusKnotGeometry(0.8, 0.3, 64, 8), color: 0x00ff00, duration: 15 } | |
| ]; | |
| const selectedPowerUp = powerUpTypes[Math.floor(Math.random() * powerUpTypes.length)]; | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: selectedPowerUp.color, | |
| wireframe: true, | |
| transparent: true, | |
| opacity: 0.9, | |
| emissive: selectedPowerUp.color, | |
| emissiveIntensity: 0.5 | |
| }); | |
| const powerUp = new THREE.Mesh(selectedPowerUp.geo, material); | |
| // Position the power-up | |
| const x = Math.random() * 30 - 15; | |
| const z = -50 - Math.random() * 20; | |
| powerUp.position.set(x, 1.5, z); | |
| // Add metadata | |
| powerUp.userData = { | |
| type: selectedPowerUp.type, | |
| duration: selectedPowerUp.duration, | |
| rotationSpeed: { | |
| x: (Math.random() - 0.5) * 0.05, | |
| y: 0.05, | |
| z: (Math.random() - 0.5) * 0.05 | |
| }, | |
| floatOffset: Math.random() * Math.PI * 2 | |
| }; | |
| // Add point light for glow effect | |
| const light = new THREE.PointLight(selectedPowerUp.color, 1, 5); | |
| light.position.set(0, 0, 0); | |
| powerUp.add(light); | |
| scene.add(powerUp); | |
| powerUps.push(powerUp); | |
| } | |
| function handleObstacles() { | |
| // Spawn new obstacles periodically | |
| obstacleSpawnTimer += 0.016; // Approximate time in seconds per frame | |
| if (obstacleSpawnTimer > 1.5) { // Spawn every 1.5 seconds | |
| createObstacle(); | |
| obstacleSpawnTimer = 0; | |
| } | |
| // Move obstacles toward player | |
| for (let i = obstacles.length - 1; i >= 0; i--) { | |
| const obstacle = obstacles[i]; | |
| obstacle.position.z += speed * 0.5; | |
| // Remove obstacles that are behind the player | |
| if (obstacle.position.z > 10) { | |
| scene.remove(obstacle); | |
| obstacles.splice(i, 1); | |
| continue; | |
| } | |
| // Check for collisions | |
| if (isGameRunning && !isTrickInProgress) { | |
| const boardPos = new THREE.Vector3(); | |
| board.getWorldPosition(boardPos); | |
| const distance = boardPos.distanceTo(obstacle.position); | |
| if (distance < 2) { | |
| if (hasShield) { | |
| // Shield protects from collision | |
| scene.remove(obstacle); | |
| obstacles.splice(i, 1); | |
| // Visual effect for shield hit | |
| const shieldMesh = board.getObjectByName('shield'); | |
| if (shieldMesh) { | |
| shieldMesh.material.opacity = 0.8; | |
| shieldMesh.material.color.set(0xff00ff); | |
| setTimeout(() => { | |
| if (shieldMesh) { | |
| shieldMesh.material.opacity = 0.4; | |
| shieldMesh.material.color.set(0x00ffff); | |
| } | |
| }, 300); | |
| } | |
| // Show feedback | |
| const trickDisplay = document.getElementById('trick-display'); | |
| trickDisplay.textContent = "SHIELD BLOCKED!"; | |
| trickDisplay.classList.add('show-trick'); | |
| setTimeout(() => { | |
| trickDisplay.classList.remove('show-trick'); | |
| }, 800); | |
| // Add points for shield block | |
| score += 250; | |
| updateScore(); | |
| } else { | |
| gameOver(); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function handleTouchStart(event) { | |
| event.preventDefault(); | |
| touchStartPos.x = event.touches[0].clientX; | |
| touchStartPos.y = event.touches[0].clientY; | |
| // Track multiple touches for board control | |
| multiTouchStartPos = []; | |
| for (let i = 0; i < event.touches.length; i++) { | |
| multiTouchStartPos.push({ | |
| x: event.touches[i].clientX, | |
| y: event.touches[i].clientY | |
| }); | |
| } | |
| } | |
| function handleTouchMove(event) { | |
| event.preventDefault(); | |
| if (!isGameRunning) return; | |
| if (event.touches.length >= 2) { | |
| // Two-finger control (lean/steer) | |
| const touch1 = { x: event.touches[0].clientX, y: event.touches[0].clientY }; | |
| const touch2 = { x: event.touches[1].clientX, y: event.touches[1].clientY }; | |
| // Calculate the midpoint between the two touches | |
| const midpoint = { | |
| x: (touch1.x + touch2.x) / 2, | |
| y: (touch1.y + touch2.y) / 2 | |
| }; | |
| // Calculate angle for rotation | |
| const startMidpoint = { | |
| x: (multiTouchStartPos[0].x + (multiTouchStartPos[1]?.x || multiTouchStartPos[0].x)) / 2, | |
| y: (multiTouchStartPos[0].y + (multiTouchStartPos[1]?.y || multiTouchStartPos[0].y)) / 2 | |
| }; | |
| // Side-to-side leaning (rotation) | |
| const xDiff = midpoint.x - startMidpoint.x; | |
| rotation = xDiff * 0.002; | |
| // Forward/backward leaning (speed) | |
| const yDiff = midpoint.y - startMidpoint.y; | |
| speed = -yDiff * 0.05; | |
| speed = Math.max(-0.8, Math.min(speed, 0.3)); // Limit max speed | |
| isMoving = true; | |
| } else if (event.touches.length === 1) { | |
| // Single finger movement | |
| const touch = { x: event.touches[0].clientX, y: event.touches[0].clientY }; | |
| const xDiff = touch.x - touchStartPos.x; | |
| // Calculate velocity based on swipe speed | |
| velocity.x = xDiff * 0.01; | |
| } | |
| } | |
| function handleTouchEnd(event) { | |
| event.preventDefault(); | |
| if (!isGameRunning) return; | |
| // Check for quick flick for tricks | |
| const currentTime = new Date().getTime(); | |
| const touchDuration = currentTime - lastTouchEndTime; | |
| if (touchDuration < 300 && !isTrickInProgress) { | |
| performTrick(); | |
| } | |
| lastTouchEndTime = currentTime; | |
| // Reset touch positions | |
| if (event.touches.length === 0) { | |
| // No touches left, gradually slow down | |
| isMoving = false; | |
| } else { | |
| // Update remaining touch positions | |
| touchStartPos.x = event.touches[0].clientX; | |
| touchStartPos.y = event.touches[0].clientY; | |
| multiTouchStartPos = []; | |
| for (let i = 0; i < event.touches.length; i++) { | |
| multiTouchStartPos.push({ | |
| x: event.touches[i].clientX, | |
| y: event.touches[i].clientY | |
| }); | |
| } | |
| } | |
| } | |
| function performTrick() { | |
| if (isTrickInProgress) return; | |
| isTrickInProgress = true; | |
| // Create trick animation | |
| const trickTypes = [ | |
| { name: "360° FLIP", points: 100, difficulty: 1 }, | |
| { name: "BACKFLIP", points: 150, difficulty: 2 }, | |
| { name: "OLLIE", points: 80, difficulty: 1 }, | |
| { name: "H4X0R SPIN", points: 200, difficulty: 2 }, | |
| { name: "GLITCH KICK", points: 120, difficulty: 1 }, | |
| { name: "BYTE BOUNCE", points: 180, difficulty: 2 }, | |
| { name: "D4T4 DRIFT", points: 250, difficulty: 3 }, | |
| { name: "MATRIX MORPH", points: 300, difficulty: 3 }, | |
| { name: "CYB3R_TORNADO", points: 350, difficulty: 3 }, | |
| { name: "QUANTUM FLIP", points: 400, difficulty: 4 } | |
| ]; | |
| // Higher difficulty tricks become more likely as player progresses | |
| let availableTricks = trickTypes.filter(trick => trick.difficulty <= Math.min(4, difficulty + 1)); | |
| if (availableTricks.length === 0) availableTricks = trickTypes; | |
| const selectedTrick = availableTricks[Math.floor(Math.random() * availableTricks.length)]; | |
| // Display trick name | |
| const trickDisplay = document.getElementById('trick-display'); | |
| trickDisplay.textContent = selectedTrick.name; | |
| trickDisplay.classList.add('show-trick', 'glitch', 'active-glitch'); | |
| // Add cool animation to the board | |
| const initialY = board.rotation.y; | |
| const initialX = board.rotation.x; | |
| const initialZ = board.rotation.z; | |
| // Perform one of several trick animations based on difficulty | |
| const trickType = Math.floor(Math.random() * 4); | |
| // Duration scales with difficulty (harder tricks take longer) | |
| let trickDuration = 0.3 + (selectedTrick.difficulty * 0.1); // 0.4 to 0.7 seconds | |
| let trickStart = performance.now(); | |
| let trickEnd = trickStart + trickDuration * 1000; | |
| // Create particle effect for the trick | |
| const particleCount = 20 * selectedTrick.difficulty; | |
| const particles = []; | |
| for (let i = 0; i < particleCount; i++) { | |
| const particleGeo = new THREE.SphereGeometry(0.05, 4, 4); | |
| const particleMat = new THREE.MeshBasicMaterial({ | |
| color: Math.random() > 0.5 ? 0xff00ff : 0x00ffff, | |
| transparent: true, | |
| opacity: 0.8 | |
| }); | |
| const particle = new THREE.Mesh(particleGeo, particleMat); | |
| // Position particle around the board | |
| particle.position.x = board.position.x + (Math.random() - 0.5) * 3; | |
| particle.position.y = board.position.y + Math.random() * 2; | |
| particle.position.z = board.position.z + (Math.random() - 0.5) * 5; | |
| // Add velocity for animation | |
| particle.userData = { | |
| velocity: new THREE.Vector3( | |
| (Math.random() - 0.5) * 0.2, | |
| Math.random() * 0.2, | |
| (Math.random() - 0.5) * 0.2 | |
| ), | |
| life: 1.0 | |
| }; | |
| scene.add(particle); | |
| particles.push(particle); | |
| } | |
| function animateTrick(time) { | |
| if (!isTrickInProgress) return; | |
| const progress = Math.min(1, (time - trickStart) / (trickEnd - trickStart)); | |
| switch (trickType) { | |
| case 0: // 360° board flip | |
| board.rotation.y = initialY + progress * Math.PI * 2; | |
| break; | |
| case 1: // Backflip | |
| board.rotation.x = initialX + progress * Math.PI * 2; | |
| break; | |
| case 2: // Barrel roll | |
| board.rotation.z = initialZ + progress * Math.PI * 2; | |
| break; | |
| case 3: // Complex trick | |
| board.rotation.x = initialX + progress * Math.PI; | |
| board.rotation.y = initialY + progress * Math.PI; | |
| board.rotation.z = initialZ + progress * Math.PI; | |
| break; | |
| } | |
| // Update particles | |
| for (let i = 0; i < particles.length; i++) { | |
| const particle = particles[i]; | |
| particle.position.x += particle.userData.velocity.x; | |
| particle.position.y += particle.userData.velocity.y; | |
| particle.position.z += particle.userData.velocity.z; | |
| // Fade out | |
| particle.userData.life -= 0.02; | |
| particle.material.opacity = particle.userData.life; | |
| // Gravity effect | |
| particle.userData.velocity.y -= 0.01; | |
| if (particle.userData.life <= 0) { | |
| scene.remove(particle); | |
| particles.splice(i, 1); | |
| i--; | |
| } | |
| } | |
| if (progress < 1) { | |
| requestAnimationFrame(animateTrick); | |
| } else { | |
| // Trick completed | |
| board.rotation.x = initialX; | |
| board.rotation.y = initialY; | |
| board.rotation.z = initialZ; | |
| // Update combo | |
| comboCount++; | |
| comboTimer = 0; | |
| // Calculate points with combo multiplier | |
| let pointsEarned = selectedTrick.points; | |
| if (comboCount > 1) { | |
| pointsEarned = Math.floor(pointsEarned * (1 + (comboCount - 1) * 0.5)); | |
| } | |
| // Update score and tricks | |
| tricks++; | |
| score += pointsEarned; | |
| updateScore(); | |
| updateTricks(); | |
| // Show points earned | |
| if (comboCount > 1) { | |
| trickDisplay.textContent = `${selectedTrick.name} x${comboCount} +${pointsEarned}pts`; | |
| } else { | |
| trickDisplay.textContent = `${selectedTrick.name} +${pointsEarned}pts`; | |
| } | |
| // Clean up any remaining particles | |
| for (let i = 0; i < particles.length; i++) { | |
| scene.remove(particles[i]); | |
| } | |
| particles.length = 0; | |
| // Hide trick text after a delay | |
| setTimeout(() => { | |
| trickDisplay.classList.remove('show-trick', 'active-glitch'); | |
| isTrickInProgress = false; | |
| }, 1000); | |
| } | |
| } | |
| requestAnimationFrame(animateTrick); | |
| } | |
| function updateScore() { | |
| document.getElementById('score').textContent = score; | |
| } | |
| function updateTricks() { | |
| document.getElementById('tricks').textContent = tricks; | |
| } | |
| function onWindowResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| generateAsciiOverlay(); | |
| } | |
| function handlePowerUps() { | |
| // Spawn new power-ups periodically | |
| powerUpSpawnTimer += 0.016; | |
| if (powerUpSpawnTimer > 8) { // Spawn every 8 seconds | |
| createPowerUp(); | |
| powerUpSpawnTimer = 0; | |
| } | |
| // Update existing power-ups | |
| for (let i = powerUps.length - 1; i >= 0; i--) { | |
| const powerUp = powerUps[i]; | |
| powerUp.position.z += speed * 0.5; | |
| // Animate powerup | |
| powerUp.rotation.x += powerUp.userData.rotationSpeed.x; | |
| powerUp.rotation.y += powerUp.userData.rotationSpeed.y; | |
| powerUp.rotation.z += powerUp.userData.rotationSpeed.z; | |
| // Add floating motion | |
| powerUp.position.y = 1.5 + Math.sin(Date.now() * 0.002 + powerUp.userData.floatOffset) * 0.3; | |
| // Remove if past player | |
| if (powerUp.position.z > 10) { | |
| scene.remove(powerUp); | |
| powerUps.splice(i, 1); | |
| continue; | |
| } | |
| // Check for collisions with player | |
| const boardPos = new THREE.Vector3(); | |
| board.getWorldPosition(boardPos); | |
| const distance = boardPos.distanceTo(powerUp.position); | |
| if (distance < 2.5) { | |
| // Collected power-up | |
| collectPowerUp(powerUp); | |
| scene.remove(powerUp); | |
| powerUps.splice(i, 1); | |
| } | |
| } | |
| // Update power-up indicators | |
| if (hasShield) { | |
| const remainingTime = Math.max(0, Math.floor((shieldEndTime - Date.now()) / 1000)); | |
| document.getElementById('power-up-type').textContent = 'SHIELD'; | |
| document.getElementById('power-up-time').textContent = remainingTime; | |
| document.getElementById('power-up-indicator').classList.remove('hidden'); | |
| if (remainingTime <= 0) { | |
| hasShield = false; | |
| document.getElementById('power-up-indicator').classList.add('hidden'); | |
| } | |
| } | |
| } | |
| function collectPowerUp(powerUp) { | |
| // Add visual feedback | |
| const type = powerUp.userData.type; | |
| let message = ''; | |
| switch (type) { | |
| case 'shield': | |
| message = 'SHIELD ACTIVATED'; | |
| hasShield = true; | |
| shieldEndTime = Date.now() + powerUp.userData.duration * 1000; | |
| // Add shield effect to board | |
| const shieldGeo = new THREE.SphereGeometry(2.5, 16, 16); | |
| const shieldMat = new THREE.MeshBasicMaterial({ | |
| color: 0x00ffff, | |
| wireframe: true, | |
| transparent: true, | |
| opacity: 0.4 | |
| }); | |
| const shield = new THREE.Mesh(shieldGeo, shieldMat); | |
| shield.name = 'shield'; | |
| board.add(shield); | |
| setTimeout(() => { | |
| const shieldMesh = board.getObjectByName('shield'); | |
| if (shieldMesh) board.remove(shieldMesh); | |
| }, powerUp.userData.duration * 1000); | |
| break; | |
| case 'speed': | |
| message = 'SPEED BOOST'; | |
| speed = -1.2; // Faster than normal max speed | |
| setTimeout(() => { | |
| speed = Math.min(speed, -0.5); // Reset to normal max after duration | |
| }, powerUp.userData.duration * 1000); | |
| break; | |
| case 'points': | |
| const pointBonus = 500; | |
| message = `+${pointBonus} POINTS`; | |
| score += pointBonus; | |
| updateScore(); | |
| break; | |
| case 'trick_boost': | |
| message = 'TRICK BOOST ACTIVE'; | |
| // No immediate effect, just sets up for double points on tricks | |
| setTimeout(() => { | |
| const trickDisplay = document.getElementById('trick-display'); | |
| trickDisplay.textContent = 'TRICK BOOST ENDED'; | |
| trickDisplay.classList.add('show-trick'); | |
| setTimeout(() => { | |
| trickDisplay.classList.remove('show-trick'); | |
| }, 1000); | |
| }, powerUp.userData.duration * 1000); | |
| break; | |
| } | |
| // Show message | |
| const trickDisplay = document.getElementById('trick-display'); | |
| trickDisplay.textContent = message; | |
| trickDisplay.classList.add('show-trick'); | |
| setTimeout(() => { | |
| trickDisplay.classList.remove('show-trick'); | |
| }, 1000); | |
| } | |
| function updateCombo() { | |
| // Update combo display | |
| if (comboCount > 1) { | |
| document.getElementById('combo-count').textContent = comboCount; | |
| document.getElementById('combo-meter').classList.remove('hidden'); | |
| } else { | |
| document.getElementById('combo-meter').classList.add('hidden'); | |
| } | |
| // Decay combo over time | |
| comboTimer += 0.016; | |
| if (comboTimer > 3) { // Reset combo after 3 seconds of no tricks | |
| comboCount = 0; | |
| comboTimer = 0; | |
| document.getElementById('combo-meter').classList.add('hidden'); | |
| } | |
| } | |
| function checkLevelUp() { | |
| // Increase difficulty based on score | |
| const newDifficulty = Math.floor(score / 5000) + 1; | |
| if (newDifficulty > difficulty) { | |
| difficulty = newDifficulty; | |
| // Show level up message | |
| const levelUp = document.getElementById('level-up'); | |
| levelUp.textContent = `LEVEL ${difficulty}`; | |
| levelUp.classList.remove('hidden'); | |
| levelUp.classList.add('glitch', 'active-glitch'); | |
| setTimeout(() => { | |
| levelUp.classList.add('hidden'); | |
| levelUp.classList.remove('active-glitch'); | |
| }, 2000); | |
| } | |
| } | |
| function updateAsciiEffect() { | |
| // Periodically update the ASCII overlay for a dynamic effect | |
| lastGlitchUpdateTime += 0.016; | |
| if (lastGlitchUpdateTime > 0.5) { // Update every half second | |
| if (Math.random() < 0.3) { // 30% chance to update | |
| generateAsciiOverlay(); | |
| } | |
| lastGlitchUpdateTime = 0; | |
| } | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| if (isGameRunning) { | |
| // Update game time | |
| gameTime += 0.016; | |
| // Update scores - faster scoring at higher difficulties | |
| score += Math.floor(speed * -10 * difficulty); | |
| updateScore(); | |
| // Update board position and rotation | |
| if (isMoving) { | |
| board.rotation.y = rotation * 2; | |
| velocity.x = rotation * 20; | |
| } else { | |
| // Gradually slow down | |
| speed *= 0.98; | |
| velocity.multiplyScalar(0.95); | |
| rotation *= 0.95; | |
| board.rotation.y *= 0.95; | |
| } | |
| // Apply velocity to board position | |
| board.position.x += velocity.x * 0.1; | |
| // Keep the board within bounds | |
| board.position.x = Math.max(-20, Math.min(20, board.position.x)); | |
| // Animate obstacles with their rotation speeds | |
| obstacles.forEach(obstacle => { | |
| if (obstacle.userData?.rotationSpeed) { | |
| obstacle.rotation.x += obstacle.userData.rotationSpeed.x; | |
| obstacle.rotation.y += obstacle.userData.rotationSpeed.y; | |
| obstacle.rotation.z += obstacle.userData.rotationSpeed.z; | |
| } | |
| }); | |
| // Update obstacles and power-ups | |
| handleObstacles(); | |
| handlePowerUps(); | |
| updateCombo(); | |
| checkLevelUp(); | |
| updateAsciiEffect(); | |
| // Update camera position to follow the board | |
| camera.position.x = board.position.x * 0.3; | |
| } | |
| // Render the scene | |
| renderer.render(scene, camera); | |
| } | |
| // Start the game | |
| window.onload = init; | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import tailwindcss from "https://esm.sh/tailwindcss"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/lib.min.js"></script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment