Skip to content

Instantly share code, notes, and snippets.

@mode-mercury
Created May 27, 2025 02:59
Show Gist options
  • Select an option

  • Save mode-mercury/1dc75d21ccaac595da39029b7674638f to your computer and use it in GitHub Desktop.

Select an option

Save mode-mercury/1dc75d21ccaac595da39029b7674638f to your computer and use it in GitHub Desktop.
Untitled
<!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>
import tailwindcss from "https://esm.sh/tailwindcss";
<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