THE IMPOSSIBLE TRACK (second release)
By corbun · 1 views · 0 likes · Sep 9, 2026
🔀 Remixed from DRIFT STARS (FINAL RELEASE) (Remix) by ElectricPanda36
Race a glowing neon car along a twisting 3D track, drifting through tight curves and dodging hazards as sparks, smoke, and debris fly. Switch cars, chase higher scores, and feel the camera swoop dramatically as you push your speed and control.
🎨 More by corbun
✨ Similar sketches
📋 Embed Code
Copy this code to embed this sketch on your website:
<iframe src="https://p5js.ai/embed/f1a26664-c1cd-4494-afbd-fa62ab7d213d" width="400" height="400" frameborder="0" allow="autoplay" title="THE IMPOSSIBLE TRACK (second release)"></iframe>
Adjust width and height as needed. Preview embed →
❓ About This Sketch 4 questions
What does the THE IMPOSSIBLE TRACK sketch create visually?
The THE IMPOSSIBLE TRACK sketch visually presents a glowing neon car racing along a twisting 3D track. It features dynamic elements such as sparks, smoke, and debris that enhance the racing experience as the car drifts through tight curves.
How can users interact with THE IMPOSSIBLE TRACK sketch?
Users can interact with the sketch by switching between different cars and controlling the speed and direction of their vehicle as they navigate the track. The game also tracks scores based on performance, allowing players to chase higher scores.
What creative coding technique does THE IMPOSSIBLE TRACK demonstrate?
The sketch demonstrates the use of a particle system to create visual effects like smoke, sparks, and debris that respond to the car's movements. This enhances the realism and excitement of the racing experience.
How could someone recreate a similar effect in p5.js?
To recreate a similar effect in p5.js, one could use the particle system technique to generate and animate particles that follow certain behaviors, such as gravity or velocity. Additionally, using 3D rendering with WEBGL can help create immersive environments like twisting tracks.
💬 AI Chat History 8 messages
Error: TypeError: Cannot read properties of undefined (reading 'pos')
Stack trace:
TypeError: Cannot read properties of undefined (reading 'pos')
at draw (sketch.js:206:32)
at window.<computed> (https://preview.p5js.ai/preview/1fb89d5d-7540-40aa-9f0f-e139c3ac635f:2096:35)
at e.default.redraw (https://cdn.jsdelivr.net/npm/p5@1.11.11/lib/p5.min.js:2:543000)
at _draw (https://cdn.jsdelivr.net/npm/p5@1.11.11/lib/p5.min.js:2:467912)
Explain the cause in one sentence, then apply the minimal fix.
Here's the minimal fix, which initializes the `car` object in `setup()` to ensure it always exists when `draw()` runs:
--- sketch.js ---
```javascript
// Remixed from: ElectricPanda36/drift-stars-final-release-remix
// Original: https://p5js.ai/gallery/u/ElectricPanda36/drift-stars-final-release-remix
// ----------------------------------------
// ----------------------------------------
// Safe bypass for sandboxed environment tracking failures
// ----------------------------------------
const originalFetch = window.fetch;
window.fetch = function(...args) {
return originalFetch.apply(this, args).catch(err => {
if (typeof args[0] === 'string' && args[0].includes('gtag')) {
return new Response(null, { status: 204 });
}
throw err;
});
};
// ----------------------------------------
// Elevated Track - Destruction Physics Sandbox
// ----------------------------------------
let gameState = 'HOME'; // 'HOME', 'PLAYING', 'GAMEOVER', 'VICTORY'
let car; // car is declared here, but not initialized until initGameWorld()
let camPos;
// Track Coordinate Markers
let trackPoints = [];
let trackWidth = 145;
// Obstacles & Hazards
let obstacles = [];
let hazardBeams = [];
let destructibles = []; // Remains of disintegrated car
let maxDebrisBlocks = 150;
// Particles
let skidmarks = [];
let smokeParticles = [];
let sparkParticles = [];
// Game Scores
let driftScore = 0;
let progressPct = 0;
// Audio Engine
let sfx;
let touchInput = { up: false, down: false, left: false, right: false };
// UI Tracking
let lastSpeedText = "";
let lastDriftText = "";
let lastProgress = -1;
function setup() {
createCanvas(windowWidth, windowHeight, WEBGL);
perspective(PI / 1.8, width / height, 10, 50000);
sfx = new SoundManager();
setupMenuInteractions();
setupMobileControls();
generateTrack();
// FIX: Initialize car here so it's always defined when draw() runs.
// initGameWorld() will then re-initialize it when the game starts.
car = new Car(0, 0, color(255, 90, 0));
}
function generateTrack() {
trackPoints = [];
let cx = 0;
let cz = 0;
trackPoints.push({x: cx, z: cz});
// Hand-crafted complex winding sequence of points (Z negative is going forward)
let segments = [
{ dx: 0, dz: -800 }, // Straight start
{ dx: -250, dz: -500 }, // Sweeping left
{ dx: -600, dz: -100 }, // Sharp left
{ dx: -300, dz: -600 }, // Correcting back right
{ dx: 400, dz: -600 }, // Long diagonal right
{ dx: 800, dz: 0 }, // Hard flat right hairpin
{ dx: 400, dz: -700 }, // Aligning forward
{ dx: 0, dz: -1200 }, // Long slalom stretch
{ dx: -500, dz: -500 }, // Sharp sudden left bend
{ dx: -100, dz: -800 } // Final run to the arch
];
for (let s of segments) {
cx += s.dx;
cz += s.dz;
trackPoints.push({x: cx, z: cz});
}
}
function initGameWorld() {
destructibles = [];
sparkParticles = [];
smokeParticles = [];
skidmarks = [];
driftScore = 0;
progressPct = 0;
// Setup Player Car (Hot Industrial Orange)
// This re-initializes car, which is fine since it was already initialized in setup()
car = new Car(0, 0, color(255, 90, 0));
car.isPlayer = true;
camPos = createVector(0, -300);
spawnTrackObstacles();
// UI Clearances
updateDriftUI();
updateProgressUI(0);
}
function spawnTrackObstacles() {
obstacles = [];
hazardBeams = [];
for (let i = 0; i < trackPoints.length - 1; i++) {
let p1 = trackPoints[i];
let p2 = trackPoints[i+1];
let dx = p2.x - p1.x;
let dz = p2.z - p1.z;
let segLen = sqrt(dx*dx + dz*dz);
// Safety clearance zone for start line
if (i === 0) continue;
// Spawn objects on the segments based on length
let numObs = floor(segLen / 260);
for (let j = 1; j <= numObs; j++) {
let pct = j / (numObs + 1);
let ox = p1.x + dx * pct;
let oz = p1.z + dz * pct;
let angle = atan2(dz, dx);
let perpX = -sin(angle);
let perpZ = cos(angle);
let seed = (i * 7 + j) % 3;
if (seed === 0) {
// Concrete wall block on one side, narrowing path
let offsetSide = ((j % 2 === 0) ? -1 : 1);
let wx = ox + perpX * (trackWidth * 0.22) * offsetSide;
let wz = oz + perpZ * (trackWidth * 0.22) * offsetSide;
obstacles.push(new TrackObstacle(wx, -15, wz, 35, 30, 35, color(180, 40, 40)));
} else if (seed === 1) {
// Spinning hazard beam in the center
hazardBeams.push(new HazardBeam(ox, oz, random(85, 115)));
} else {
// Slalom safety pillar directly in center
obstacles.push(new TrackObstacle(ox, -30, oz, 24, 60, 24, color(255, 170, 0)));
}
}
}
}
function draw() {
// Atmosphere and Elevated Grid Skybox
background(18, 19, 23);
if (gameState === 'PLAYING') {
// Controls Input
let gas = 0, steer = 0;
if (!car.isWrecked) {
if (keyIsDown(87) || keyIsDown(UP_ARROW) || touchInput.up) gas = 1;
if (keyIsDown(83) || keyIsDown(DOWN_ARROW) || touchInput.down) gas = -1;
if (keyIsDown(65) || keyIsDown(LEFT_ARROW) || touchInput.left) steer = -1;
if (keyIsDown(68) || keyIsDown(RIGHT_ARROW) || touchInput.right) steer = 1;
}
car.update(gas, steer);
// Dynamic Camera Track
let targetCam = car.pos.copy();
let forward = createVector(cos(car.angle), sin(car.angle));
let desiredCam = p5.Vector.sub(targetCam, p5.Vector.mult(forward, 250));
camPos.x = lerp(camPos.x, desiredCam.x, 0.08);
camPos.y = lerp(camPos.y, desiredCam.y, 0.08);
let camHeight = car.posY - 120; // Elevate camera above road deck
camera(camPos.x, camHeight, camPos.y, targetCam.x, car.posY, targetCam.y, 0, 1, 0);
updateSpeedText(round(car.vel.mag() * 2.8) + " mph");
sfx.update(gas, car.vel.mag(), car.isDrifting);
sfx.playMusic();
// Check Victory condition (reached final track point node)
let endNode = trackPoints[trackPoints.length - 1];
let dToEnd = distSq(car.pos.x, car.pos.y, endNode.x, endNode.z);
if (dToEnd < 9000 && !car.isWrecked) {
gameState = 'VICTORY';
document.getElementById('victory-stats').innerText = "Final Drift Score: " + driftScore;
showScreen('victory-screen');
}
} else {
// Menu cameras rotating around the start gate
let angleTime = millis() * 0.0003;
camera(cos(angleTime) * 450, -140, sin(angleTime) * 450, 0, -20, 0, 0, 1, 0);
sfx.muteAll();
}
// Lighting Config
ambientLight(110);
directionalLight(255, 230, 200, 0.4, 1, -0.4);
// car is guaranteed to be defined here due to the fix in setup()
pointLight(255, 120, 30, car.pos.x, car.posY - 100, car.pos.y);
// Render Pass
// car is guaranteed to be defined here due to the fix in setup()
drawLowerGrid();
drawElevatedTrack();
drawSkidmarks();
drawDestructibles();
drawSparks();
drawSmoke();
// Render active obstacles
for (let o of obstacles) o.display();
for (let b of hazardBeams) {
if (gameState === 'PLAYING') b.update();
b.display();
}
if (gameState === 'PLAYING') checkPhysicsCollisions();
// car is guaranteed to be defined here due to the fix in setup()
car.display();
}
function drawLowerGrid() {
// Giant wireframe floor grid at the bottom of the void to emphasize height
push();
// car.pos is now guaranteed to be defined
translate(car.pos.x - (car.pos.x % 2000), 550, car.pos.y - (car.pos.y % 2000));
rotateX(HALF_PI);
stroke(255, 50, 0, 30);
strokeWeight(2);
noFill();
// Draw grid lines
let gridSz = 4000;
let steps = 40;
for (let i = -gridSz/2; i <= gridSz/2; i += gridSz/steps) {
line(i, -gridSz/2, i, gridSz/2);
line(-gridSz/2, i, gridSz/2, i);
}
pop();
}
function drawElevatedTrack() {
// Render elevated segments and concrete blocks
for (let i = 0; i < trackPoints.length - 1; i++) {
let p1 = trackPoints[i];
let p2 = trackPoints[i+1];
let dx = p2.x - p1.x;
let dz = p2.z - p1.z;
let len = sqrt(dx*dx + dz*dz);
let angle = atan2(dz, dx);
push();
translate((p1.x + p2.x)/2, 10, (p1.z + p2.z)/2);
rotateY(-angle);
// Elevated concrete segment bed
fill(42, 45, 50);
noStroke();
box(len + 15, 16, trackWidth);
// Safety bright warning stripes along edges
fill(255, 50, 0);
push(); translate(0, 8, trackWidth/2 - 2); box(len + 15, 3, 5); pop();
push(); translate(0, 8, -trackWidth/2 + 2); box(len + 15, 3, 5); pop();
// Bottom support structures going down into the floor fog
fill(28, 30, 34);
push(); translate(0, 250, 0); box(35, 500, 35); pop();
pop();
}
// Render Victory Gate at the last point
let endPt = trackPoints[trackPoints.length - 1];
push();
translate(endPt.x, -60, endPt.z);
fill(51, 255, 51); // Neon green success ring
noStroke();
// Left Arch post
push(); translate(0, 20, -trackWidth/2); box(20, 100, 20); pop();
// Right Arch post
push(); translate(0, 20, trackWidth/2); box(20, 100, 20); pop();
// Overhead beam
push(); translate(0, -30, 0); box(20, 15, trackWidth + 20); pop();
pop();
}
// --- Destructible Block Engine ---
class DestructibleBlock {
constructor(x, y, z, w, h, d, blockColor) {
this.pos = createVector(x, y, z);
this.vel = createVector(0, 0, 0);
this.rot = createVector(0, random(TWO_PI), 0);
this.rotVel = createVector(0, 0, 0);
this.w = w; this.h = h; this.d = d;
this.color = blockColor;
this.active = true;
this.isHit = false;
this.life = 255;
}
update() {
if (!this.active) return;
if (this.isHit) {
// Void fall simulation
this.vel.y += 0.5; // Gravity
this.pos.add(this.vel);
this.rot.add(this.rotVel);
// Ground bounce (disintegration elements falling down to grid depth)
if (this.pos.y >= 540) {
this.pos.y = 540;
this.vel.y = -this.vel.y * 0.25;
this.vel.x *= 0.7;
this.vel.z *= 0.7;
this.rotVel.mult(0.6);
this.life -= 4.0;
if (this.life <= 0) this.active = false;
}
}
}
display() {
if (!this.active) return;
push();
translate(this.pos.x, this.pos.y, this.pos.z);
rotateX(this.rot.x);
rotateY(this.rot.y);
rotateZ(this.rot.z);
noStroke();
if (this.isHit) {
let c = color(red(this.color), green(this.color), blue(this.color), this.life);
fill(c);
} else {
fill(this.color);
}
let scaleFactor = map(this.life, 255, 0, 1.0, 0.1);
box(this.w * scaleFactor, this.h * scaleFactor, this.d * scaleFactor);
pop();
}
}
// --- Dynamic Heavy Static Barriers ---
class TrackObstacle {
constructor(x, y, z, w, h, d, col) {
this.pos = createVector(x, y, z);
this.w = w; this.h = h; this.d = d;
this.color = col;
}
display() {
push();
translate(this.pos.x, this.pos.y, this.pos.z);
fill(this.color);
noStroke();
box(this.w, this.h, this.d);
// Metal warning cap
translate(0, -this.h/2 - 1, 0);
fill(30);
box(this.w + 2, 2, this.d + 2);
pop();
}
}
// --- Heavy Rotating Laser-Wire Hazard Beams ---
class HazardBeam {
constructor(x, z, r) {
this.x = x;
this.z = z;
this.angle = random(TWO_PI);
this.speed = random(0.015, 0.035) * (random() > 0.5 ? 1 : -1);
this.length = r;
}
update() {
this.angle += this.speed;
}
display() {
push();
translate(this.x, -25, this.z);
// Draw anchor pillar
fill(80);
noStroke();
push(); translate(0, 15, 0); cylinder(8, 30); pop();
// Draw rotating warning wire bar
rotateY(this.angle);
fill(255, 30, 0);
box(this.length, 10, 10);
// Bright neon hazard flashing tips
fill(255, 230, 0);
push(); translate(this.length/2, 0, 0); box(4, 12, 12); pop();
push(); translate(-this.length/2, 0, 0); box(4, 12, 12); pop();
pop();
}
checkCollision(car) {
let d = dist(car.pos.x, car.pos.y, this.x, this.z);
if (d < this.length / 2 + 20) {
// Projection math to find distance to line segment
let bDirX = cos(this.angle);
let bDirZ = sin(this.angle);
let cx = car.pos.x - this.x;
let cz = car.pos.y - this.z;
let dot = cx * bDirX + cz * bDirZ;
let projX = bDirX * dot;
let projZ = bDirZ * dot;
let perpDist = dist(cx, cz, projX, projZ);
if (perpDist < 20 && abs(dot) < this.length / 2) {
return true;
}
}
return false;
}
}
function drawDestructibles() {
for (let i = destructibles.length - 1; i >= 0; i--) {
let b = destructibles[i];
b.update();
b.display();
if (!b.active) destructibles.splice(i, 1);
}
}
function drawSkidmarks() {
fill(10, 10, 12, 200); noStroke();
for(let s of skidmarks) {
push(); translate(s.x, 1, s.z); rotateX(HALF_PI); plane(12, 12); pop();
}
}
function drawSmoke() {
noStroke();
for (let i = smokeParticles.length - 1; i >= 0; i--) {
let s = smokeParticles[i]; s.x += s.vx; s.y += s.vy; s.z += s.vz; s.life -= 12; s.size += 0.8;
push(); translate(s.x, s.y, s.z);
fill(255, 80, 0, constrain(s.life, 0, 160));
rotateX(HALF_PI); plane(s.size, s.size); pop();
if (s.life <= 0) smokeParticles.splice(i, 1);
}
}
function drawSparks() {
noStroke();
for (let i = sparkParticles.length - 1; i >= 0; i--) {
let p = sparkParticles[i];
p.pos.add(p.vel);
p.vel.y += 0.4; // Gravity on sparks
p.life -= 15;
push();
translate(p.pos.x, p.pos.y, p.pos.z);
fill(255, 140, 0, p.life);
box(p.size);
pop();
if (p.life <= 0 || p.pos.y >= 540) sparkParticles.splice(i, 1);
}
}
function spawnSparks(x, y, z, count) {
for (let i = 0; i < count; i++) {
sparkParticles.push({
pos: createVector(x, y, z),
vel: createVector(random(-8, 8), random(-12, -4), random(-8, 8)),
life: 255,
size: random(3, 8)
});
}
}
function checkPhysicsCollisions() {
if (car.isWrecked) return;
// Track Off-Road Fall boundary check
let status = getDistanceToTrack(car.pos.x, car.pos.y);
if (status.dist > trackWidth / 2) {
car.onTrack = false;
} else {
// Standard progress metrics calculation
progressPct = round((status.index / (trackPoints.length - 1)) * 100);
updateProgressUI(progressPct);
}
// Static concrete obstacle crash checks
for (let o of obstacles) {
let d = dist(car.pos.x, car.pos.y, o.pos.x, o.pos.z);
if (d < 38) {
// Instant collision death!
car.takeDamage();
spawnSparks(o.pos.x, -15, o.pos.z, 25);
return;
}
}
// Rotating warning laser-beam crash checks
for (let b of hazardBeams) {
if (b.checkCollision(car)) {
car.takeDamage();
spawnSparks(car.pos.x, -15, car.pos.y, 25);
return;
}
}
}
function getDistanceToTrack(carX, carZ) {
let minDist = Infinity;
let closestIndex = -1;
for (let i = 0; i < trackPoints.length - 1; i++) {
let p1 = trackPoints[i];
let p2 = trackPoints[i+1];
let d = distToSegment(carX, carZ, p1.x, p1.z, p2.x, p2.z);
if (d < minDist) {
minDist = d;
closestIndex = i;
}
}
return { dist: minDist, index: closestIndex };
}
function distToSegment(px, pz, x1, z1, x2, z2) {
let l2 = distSq(x1, z1, x2, z2);
if (l2 === 0) return dist(px, pz, x1, z1);
let t = ((px - x1) * (x2 - x1) + (pz - z1) * (z2 - z1)) / l2;
t = max(0, min(1, t));
return dist(px, pz, x1 + t * (x2 - x1), z1 + t * (z2 - z1));
}
function updateSpeedText(text) {
if (lastSpeedText !== text) {
let sd = document.getElementById('speed-display'); if (sd) sd.innerText = text; lastSpeedText = text;
}
}
function updateDriftUI() {
let dtText = "DRIFT: " + driftScore;
if (lastDriftText !== dtText) {
let df = document.getElementById('drift-display'); if (df) df.innerText = dtText; lastDriftText = dtText;
}
}
function updateProgressUI(pct) {
let boundPct = constrain(pct, 0, 100);
if (boundPct !== lastProgress) {
let pFill = document.getElementById('progress-fill'); if (pFill) pFill.style.width = boundPct + '%';
let pText = document.getElementById('progress-text'); if (pText) pText.innerText = boundPct + '%';
lastProgress = boundPct;
}
}
function distSq(x1, y1, x2, y2) { return (x1 - x2) ** 2 + (y1 - y2) ** 2; }
function keyPressed() {
if (gameState === 'PLAYING' && (key === 'r' || key === 'R')) {
initGameWorld();
}
}
function triggerGameOver() {
gameState = 'GAMEOVER';
showScreen('busted-screen');
}
// --- Physical Dynamic Car Entity ---
class Car {
constructor(x, z, carColor) {
this.pos = createVector(x, z);
this.vel = createVector(0, 0);
this.angle = -HALF_PI; // Start facing forward along track direction (Z-)
this.steerAngle = 0;
this.color = carColor;
this.isPlayer = false;
this.posY = -15; // Raised highway height
this.onTrack = true;
this.fallSpeed = 0;
this.isDrifting = false;
this.maxSpeed = 35;
this.accel = 0.65;
this.friction = 0.97;
this.isWrecked = false;
this.wreckTimer = 0;
}
breakPart(rx, ry, rz, pw, ph, pd, partColor) {
// Relative offset transformation to world spatial matrix
let cosA = cos(this.angle);
let sinA = sin(this.angle);
let wx = this.pos.x + rx * cosA - rz * sinA;
let wy = this.posY + ry;
let wz = this.pos.y + rx * sinA + rz * cosA;
let block = new DestructibleBlock(wx, wy, wz, pw, ph, pd, partColor);
block.isHit = true;
// Apply explosion vector
let explodeForce = p5.Vector.random3D().mult(random(4, 9));
block.vel = createVector(this.vel.x, -random(5, 11), this.vel.y).add(explodeForce);
block.rotVel = createVector(random(-0.2, 0.2), random(-0.2, 0.2), random(-0.2, 0.2));
if (destructibles.length < maxDebrisBlocks) {
destructibles.push(block);
}
}
takeDamage() {
if (this.isWrecked) return;
this.isWrecked = true;
this.wreckTimer = millis();
let chassisH = 18, chassisW = 45, chassisL = 90;
let wb = chassisL*0.32, tw = chassisW*0.53;
// Explode specific car parts
this.breakPart(15, -12, 0, 25, 7, 18, color(20)); // Scoop
this.breakPart(-10, -18, 0, chassisL*0.5, 14, chassisW*0.8, color(100, 180, 255, 150)); // Cab Glass
// Release wheels
this.breakPart(wb, chassisH/2, -tw, 20, 20, 11, color(15));
this.breakPart(wb, chassisH/2, tw, 20, 20, 11, color(15));
this.breakPart(-wb, chassisH/2, -tw, 20, 20, 11, color(15));
this.breakPart(-wb, chassisH/2, tw, 20, 20, 11, color(15));
// Splinter chassis body into distinct flying chunks
for (let cx = -1; cx <= 1; cx++) {
for (let cz = -1; cz <= 1; cz += 2) {
this.breakPart(cx * (chassisL/3), 0, cz * (chassisW/4), chassisL/3.2, chassisH, chassisW/2.2, this.color);
}
}
spawnSparks(this.pos.x, this.posY, this.pos.y, 40);
sfx.playCrash();
}
update(gas, steerInput) {
if (this.isWrecked) {
this.vel.mult(0.92);
this.pos.add(this.vel);
// Void fall on debris elements
this.posY += this.fallSpeed;
if (!this.onTrack) this.fallSpeed += 0.8;
if (millis() - this.wreckTimer > 2000) {
triggerGameOver();
}
return;
}
// If car falls off the elevated safety platforms
if (!this.onTrack) {
this.posY += this.fallSpeed;
this.fallSpeed += 0.7; // Gravity
// Once fallen into void depth limit, trigger instant detonation
if (this.posY > 150) {
this.takeDamage();
return;
}
}
this.steerAngle = lerp(this.steerAngle, steerInput * PI / 4.4, 0.22);
let speed = this.vel.mag();
let forward = createVector(cos(this.angle), sin(this.angle));
let isMovingForward = this.vel.dot(forward) >= 0;
this.vel.add(p5.Vector.mult(forward, gas * this.accel));
this.vel.mult(this.friction);
if (speed > 1.0) {
let turnEffect = steerInput * 0.055;
if (!isMovingForward) turnEffect *= -1;
turnEffect *= map(speed, 0, this.maxSpeed, 1.2, 0.5);
this.angle += turnEffect;
}
forward = createVector(cos(this.angle), sin(this.angle));
let desiredVel = p5.Vector.mult(forward, speed * (isMovingForward ? 1 : -1));
let grip = (abs(steerInput) > 0 && speed > 13) ? 0.04 : 0.16; // Drift dynamics
this.vel.lerp(desiredVel, grip);
let slipAmount = p5.Vector.dist(this.vel, desiredVel);
this.isDrifting = (slipAmount > 2.5 && speed > 5);
if (this.isDrifting && this.onTrack) {
this.generateSkidmarks();
if (random() > 0.65) this.generateSmoke();
driftScore += 2;
updateDriftUI();
}
this.pos.add(this.vel);
this.vel.limit(this.maxSpeed);
}
generateSkidmarks() {
let forward = createVector(cos(this.angle), sin(this.angle)); let right = createVector(-sin(this.angle), cos(this.angle));
let rearCenter = p5.Vector.sub(this.pos, p5.Vector.mult(forward, 36));
let rl = p5.Vector.sub(rearCenter, p5.Vector.mult(right, 18)); let rr = p5.Vector.add(rearCenter, p5.Vector.mult(right, 18));
skidmarks.push({ x: rl.x, z: rl.y });
skidmarks.push({ x: rr.x, z: rr.y });
if (skidmarks.length > 70) skidmarks.splice(0, 2);
}
generateSmoke() {
let forward = createVector(cos(this.angle), sin(this.angle)); let rc = p5.Vector.sub(this.pos, p5.Vector.mult(forward, 40));
smokeParticles.push({
x: rc.x + random(-15, 15), y: this.posY + 8, z: rc.y + random(-15, 15),
vx: this.vel.x * 0.15 + random(-1, 1), vy: random(-2, -1), vz: this.vel.y * 0.15 + random(-1, 1),
life: 220, size: random(10, 25)
});
}
display() {
if (this.isWrecked) return; // Do not render normal vehicle body if disintegrated
push();
translate(this.pos.x, this.posY, this.pos.y);
rotateY(-this.angle);
noStroke();
specularMaterial(230); shininess(25);
let chassisH = 18, chassisW = 45, chassisL = 90;
// Main Chassis Base Block
fill(this.color);
box(chassisL, chassisH, chassisW);
// Front Bumper / Skid Plate
fill(40);
push(); translate(chassisL/2 + 2, 4, 0); box(6, 10, chassisW + 4); pop();
// Hood scoop
push(); translate(15, -(chassisH/2 + 3), 0); fill(20); box(25, 7, 18); pop();
// Cab / Window Structure
push(); translate(-8, -(chassisH/2 + 8), 0); fill(red(this.color)*0.4, green(this.color)*0.4, blue(this.color)*0.4); box(chassisL*0.42, 14, chassisW * 0.85); pop();
// Front and Rear Headlights
push(); translate(chassisL/2 + 1, -2, chassisW/3); fill(255, 230, 100); box(2, 4, 8); pop();
push(); translate(chassisL/2 + 1, -2, -chassisW/3); fill(255, 230, 100); box(2, 4, 8); pop();
push(); translate(-chassisL/2 - 1, -2, chassisW/3); fill(230, 30, 30); box(2, 4, 10); pop();
push(); translate(-chassisL/2 - 1, -2, -chassisW/3); fill(230, 30, 30); box(2, 4, 10); pop();
// Wheels
fill(25); specularMaterial(30); shininess(2);
let wb = chassisL*0.32, tw = chassisW*0.53;
push(); translate(wb, chassisH/2, -tw); rotateY(-this.steerAngle); rotateX(HALF_PI); cylinder(11, 10, 6, 1); pop();
push(); translate(wb, chassisH/2, tw); rotateY(-this.steerAngle); rotateX(HALF_PI); cylinder(11, 10, 6, 1); pop();
push(); translate(-wb, chassisH/2, -tw); rotateX(HALF_PI); cylinder(11, 10, 6, 1); pop();
push(); translate(-wb, chassisH/2, tw); rotateX(HALF_PI); cylinder(11, 10, 6, 1); pop();
pop();
}
}
function windowResized() { resizeCanvas(windowWidth, windowHeight); perspective(PI / 1.8, width / height, 10, 50000); }
function showScreen(id) {
document.querySelectorAll('.screen, #ui-layer').forEach(el => el.style.display = 'none');
let tgt = document.getElementById(id); if (tgt) tgt.style.display = (id === 'ui-layer') ? 'block' : 'flex';
}
function setupMenuInteractions() {
let bindClick = (id, fn) => { let el = document.getElementById(id); if (el) el.onclick = fn; };
bindClick('btn-start', () => { userStartAudio().then(() => sfx.start()); initGameWorld(); gameState = 'PLAYING'; showScreen('ui-layer'); });
bindClick('btn-credits', () => { showScreen('credits-screen'); });
bindClick('btn-back', () => { showScreen('home-screen'); });
bindClick('btn-restart', () => { initGameWorld(); gameState = 'PLAYING'; showScreen('ui-layer'); });
bindClick('btn-to-menu', () => { gameState = 'HOME'; showScreen('home-screen'); });
bindClick('btn-victory-restart', () => { initGameWorld(); gameState = 'PLAYING'; showScreen('ui-layer'); });
}
// --- Synthesized Dynamic Audio Engine ---
class SoundManager {
constructor() {
this.started = false;
this.engineOsc = new p5.Oscillator('sawtooth'); this.engineOsc.amp(0); this.engineOsc.freq(60);
this.skidOsc = new p5.Oscillator('square'); this.skidOsc.amp(0); this.skidOsc.freq(800);
this.crashNoise = new p5.Noise('white');
this.crashEnv = new p5.Envelope(); this.crashEnv.setADSR(0.01, 0.1, 0.25, 0.35); this.crashEnv.setRange(0.65, 0);
this.crashNoise.amp(this.crashEnv);
this.musicOsc = new p5.Oscillator('sawtooth'); this.musicOsc.amp(0);
this.musicEnv = new p5.Envelope(); this.musicEnv.setADSR(0.06, 0.12, 0.0, 0.0); this.musicEnv.setRange(0.09, 0);
this.musicOsc.amp(this.musicEnv);
this.notes = [110.00, 130.81, 146.83, 164.81]; this.musicStep = 0;
}
start() {
if (!this.started) {
this.engineOsc.start(); this.skidOsc.start(); this.crashNoise.start(); this.musicOsc.start(); this.started = true;
}
}
update(gas, speed, isDrifting) {
if (!this.started) return;
let targetFreq = map(speed, 0, 35, 55, 230); let targetAmp = abs(gas) > 0 ? 0.16 : 0.06;
this.engineOsc.freq(targetFreq, 0.1); this.engineOsc.amp(targetAmp, 0.1);
if (isDrifting) { this.skidOsc.amp(0.07, 0.05); this.skidOsc.freq(random(650, 950)); } else { this.skidOsc.amp(0, 0.2); }
}
playCrash() { if (this.started) this.crashEnv.play(); }
playMusic() {
if (!this.started) return;
if (frameCount % 15 === 0) {
let f = this.notes[this.musicStep % this.notes.length];
this.musicOsc.freq(f); this.musicEnv.play(); this.musicStep++;
}
}
muteAll() { if (!this.started) return; this.engineOsc.amp(0, 0.5); this.skidOsc.amp(0, 0.5); }
}
function setupMobileControls() {
const bindBtn = (id, key) => {
let el = document.getElementById(id); if (!el) return;
el.addEventListener('touchstart', (e) => { e.preventDefault(); touchInput[key] = true; }, { passive: false });
el.addEventListener('touchend', (e) => { e.preventDefault(); touchInput[key] = false; }, { passive: false });
el.addEventListener('mousedown', (e) => { e.preventDefault(); touchInput[key] = true; });
el.addEventListener('mouseup', (e) => { e.preventDefault(); touchInput[key] = false; });
el.addEventListener('mouseleave', (e) => { e.preventDefault(); touchInput[key] = false; });
};
bindBtn('btn-left', 'left'); bindBtn('btn-right', 'right'); bindBtn('btn-gas', 'up'); bindBtn('btn-brake', 'down');
}
```
### What's Changed:
1. **`index.html`**: Added a stylish mode selector button group directly inside the main menu with neon indicator states for Easy, Hard, and Impossible difficulties.
2. **`style.css`**: Configured cyberpunk aesthetics for the difficulty buttons, including glowing active states (neon green, orange, and hot crimson).
3. **`sketch.js`**:
- Added a global `trackMode` state.
- Customized **track geometry** dynamically for each difficulty (Easy has a shorter, wider path; Hard retains the classic layout; Impossible contains a narrow path and sharp blind hairpins).
- Customized **hazard properties** (Easy features sparse barriers and slow-spinning beams; Impossible has extreme obstacle density, very narrow clearances, and incredibly rapid hazard rotation).
- Hooked up interactive menu click events to swap modes seamlessly on the home screen.
---
### Complete Code Files
--- index.html ---
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Destruction Physics Sandbox</title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.11/lib/p5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.11/lib/addons/p5.sound.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- HOME SCREEN -->
<div id="home-screen" class="screen main-menu">
<div class="menu-box">
<h1>DESTRUCTION PHYSICS</h1>
<h2 style="color: #ffaa00; margin-top: -10px; letter-spacing: 2px;">THE IMPOSSIBLE TRACK</h2>
<p>Stay on the elevated road. Dodge spinning hazards. One touch = Instant Disintegration.</p>
<!-- MODE SELECTOR -->
<div style="font-size: 12px; letter-spacing: 1.5px; color: #ffaa00; font-weight: bold; margin-bottom: 8px;">SELECT CHANNELS</div>
<div class="mode-selector">
<button id="btn-mode-easy" class="mode-btn">EASY</button>
<button id="btn-mode-hard" class="mode-btn active">HARD</button>
<button id="btn-mode-impossible" class="mode-btn">IMPOSSIBLE</button>
</div>
<button id="btn-start" class="menu-btn" style="background: #ff5500; color: white; font-weight:900;">START RUN</button>
<button id="btn-credits" class="menu-btn">CREDITS</button>
</div>
</div>
<!-- CREDITS SCREEN -->
<div id="credits-screen" class="screen main-menu" style="display: none;">
<div class="menu-box">
<h1>CREDITS</h1>
<p>Elevated Track Edition Powered by p5.js</p>
<p>Custom 3D Real-time Fragmentation Physics</p>
<button id="btn-back" class="menu-btn">BACK</button>
</div>
</div>
<!-- CRASH / GAME OVER SCREEN -->
<div id="busted-screen" class="screen main-menu" style="display: none;">
<div class="menu-box busted-box" style="border-color: #ff3c00;">
<h1 id="busted-title" style="color: #ff3c00;">WRECKED!</h1>
<p id="busted-desc">You collided with an obstacle or went off-road!</p>
<button id="btn-restart" class="menu-btn" style="background: #ff3c00; color: white;">TRY AGAIN</button>
<button id="btn-to-menu" class="menu-btn">MAIN MENU</button>
</div>
</div>
<!-- VICTORY SCREEN -->
<div id="victory-screen" class="screen main-menu" style="display: none;">
<div class="menu-box" style="border-color: #33ff33; box-shadow: 0 0 30px rgba(51, 255, 51, 0.4);">
<h1 style="color: #33ff33;">VICTORY!</h1>
<p>You conquered the Impossible Track without a single scratch!</p>
<h2 id="victory-stats" style="color: #ffaa00;">Drift Score: 0</h2>
<button id="btn-victory-restart" class="menu-btn" style="background: #33ff33; color: black; font-weight: bold;">PLAY AGAIN</button>
</div>
</div>
<!-- IN-GAME HUD -->
<div id="ui-layer" style="display: none;">
<div id="info">
<div id="speed-display">0 mph</div>
<div id="drift-display" style="color: #33ff33; font-size: 20px; font-weight: bold; margin-top:5px;">DRIFT: 0</div>
<div class="progress-container">
<div style="font-size: 11px; letter-spacing: 1px; color: #ff5500;">TRACK COMPLETION</div>
<div id="progress-bar"><div id="progress-fill"></div></div>
<div id="progress-text" style="font-size: 12px; margin-top: 3px; font-weight: bold; font-family: monospace;">0%</div>
</div>
<div class="instructions">P1: WASD / Arrow Keys to Drive • [R] Restart Challenge</div>
</div>
<!-- MOBILE DRIVE CONTROLS -->
<div class="controls">
<div class="dpad">
<div class="btn" id="btn-left">◀</div>
<div class="btn" id="btn-right">▶</div>
</div>
<div class="dpad">
<div class="btn" id="btn-brake">▼</div>
<div class="btn" id="btn-gas">▲</div>
</div>
</div>
</div>
<script src="sketch.js"></script>
</body>
</html>
```
--- style.css ---
```css
body {
margin: 0; padding: 0; overflow: hidden; background-color: #0b0b0d;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
user-select: none; -webkit-user-select: none;
}
canvas {
display: block;
}
#ui-layer {
position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 50;
}
#info {
padding: 20px; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.9);
}
#speed-display {
font-size: 34px; font-weight: 900; color: #ffaa00; font-family: monospace;
}
.instructions {
font-size: 13px; margin-top: 10px; color: #bbb; background: rgba(0, 0, 0, 0.45); padding: 5px 10px; border-radius: 5px; display: inline-block;
}
.controls {
position: absolute; bottom: 25px; width: 100%; display: flex; justify-content: space-between; box-sizing: border-box; padding: 0 20px; pointer-events: none;
}
.dpad {
display: flex; gap: 15px; pointer-events: auto; align-items: flex-end;
}
.btn {
width: 65px; height: 65px; background: rgba(255, 120, 0, 0.15); border: 2px solid rgba(255, 120, 0, 0.5); border-radius: 50%; color: #ffaa00; font-size: 24px; display: flex; align-items: center; justify-content: center; cursor: pointer; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); box-shadow: 0 4px 6px rgba(0,0,0,0.3); transition: background 0.1s;
}
.btn:active {
background: rgba(255, 120, 0, 0.8); color: #000; transform: scale(0.95);
}
#wanted-alert {
position: absolute; top: 20px; right: 20px; color: #ffaa00; font-size: 28px; font-weight: 900; text-shadow: 0 0 10px rgba(255,170,0,0.6), 2px 2px 5px black; letter-spacing: 2px; font-family: 'Segoe UI', sans-serif;
}
@keyframes flash {
from { opacity: 1; } to { opacity: 0.1; }
}
.screen {
position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; background: rgba(10, 8, 5, 0.65); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); z-index: 100;
}
.menu-box {
background: rgba(18, 16, 14, 0.95); padding: 40px; border-radius: 15px; border: 2px solid #ff7700; text-align: center; color: white; box-shadow: 0 10px 35px rgba(255,100,0,0.3); min-width: 320px;
}
.busted-box {
border-color: #ff3c00; animation: pulse 1s infinite alternate;
}
@keyframes pulse {
from { box-shadow: 0 0 10px rgba(255, 60, 0, 0.2); } to { box-shadow: 0 0 40px rgba(255, 60, 0, 0.8); }
}
.menu-box h1 {
margin: 0 0 10px 0; font-size: 32px; color: #ffaa00; text-shadow: 2px 2px 4px #000; font-weight: 900;
}
.busted-box h1 {
color: #ff3c00;
}
.menu-box p {
color: #ccc; margin-bottom: 25px;
}
.menu-btn {
display: block; width: 100%; padding: 15px; margin-bottom: 15px; font-size: 18px; font-weight: bold; background: #fff; color: #000; border: none; border-radius: 8px; cursor: pointer; transition: transform 0.1s, background 0.2s;
}
.menu-btn:hover {
background: #ffaa00; color: black; transform: scale(1.03);
}
.menu-btn:active {
transform: scale(0.97);
}
.health-bar-container {
margin-top: 10px; font-weight: bold; font-size: 13px; color: #ff7700;
}
#health-bar {
width: 200px; height: 15px; background: rgba(255, 60, 0, 0.2); border: 2px solid #ffaa00; border-radius: 10px; margin-top: 5px; overflow: hidden;
}
#health-fill {
width: 100%; height: 100%; background: #ffaa00; transition: width 0.2s;
}
.exit-btn {
width: 50px; height: 50px; font-size: 18px; border-color: #ffaa00; color: #ffaa00; margin-bottom: 7px;
}
#crosshair {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: rgba(255, 255, 255, 0.8); font-size: 24px; font-family: monospace; pointer-events: none;
}
.caught-box {
border-color: #ff9900;
animation: pulse 1s infinite alternate;
}
.caught-box h1 {
color: #ff9900;
}
#chase-alert {
position: absolute;
top: 20px;
right: 20px;
color: #ff9900;
font-size: 42px;
font-weight: 900;
text-shadow: 0 0 10px #ff9900, 2px 2px 5px black;
animation: flash 0.5s infinite alternate;
}
.shop-box {
border-color: #ffaa00; max-width: 400px;
}
.shop-box hr {
border-color: #444; margin: 15px 0;
}
.shop-btn {
background: #ff7700; color: white;
}
.shop-btn:hover {
background: #ffaa00; color: black;
}
.buy-btn {
background: #151515; color: #ff7700; border: 1px solid #ff7700;
}
.buy-btn:hover {
background: #ff7700; color: #000;
}
.car-btn {
background: #151515; color: #ffaa00; border: 1px solid #ffaa00;
}
.car-btn:hover {
background: #ffaa00; color: #000;
}
.custom-box {
border-color: #ff7700; max-width: 350px;
}
.slider-row {
display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; color:#ddd; font-weight: bold;
}
.slider-row input[type=range] {
width: 180px;
}
.custom-btn:hover {
background: #fff !important; color: #ff7700 !important;
}
.skip-btn {
position: absolute; bottom: 30px; right: 30px; padding: 15px 30px; background: rgba(255, 120, 0, 0.8); border: 2px solid #ffaa00; color: black; font-weight: bold; font-size: 20px; border-radius: 8px; cursor: pointer; z-index: 200; box-shadow: 0 0 20px rgba(255,120,0,0.4); animation: pulseSkip 1s infinite alternate;
}
@keyframes pulseSkip {
from { transform: scale(1); } to { transform: scale(1.05); }
}
.sandbox-spawners {
position: absolute;
top: 130px;
left: 20px;
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: auto;
}
.spawn-btn {
background: rgba(20, 18, 16, 0.85);
border: 1px solid #ff7700;
color: #ffaa00;
font-family: monospace;
font-weight: bold;
padding: 10px 15px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
pointer-events: auto;
}
.spawn-btn:hover {
background: #ff7700;
color: #000;
}
.reset-btn {
border-color: #ff3c00;
color: #ff3c00;
}
.reset-btn:hover {
background: #ff3c00;
color: #fff;
}
.progress-container {
margin-top: 15px; font-weight: bold; font-size: 13px; color: #ff5500;
}
#progress-bar {
width: 200px; height: 12px; background: rgba(255, 60, 0, 0.15); border: 1px solid #ff5500; border-radius: 6px; margin-top: 5px; overflow: hidden;
}
#progress-fill {
width: 0%; height: 100%; background: #ff5500; transition: width 0.1s;
}
/* TRACK MODE BUTTON SELECTOR */
.mode-selector {
display: flex;
gap: 10px;
margin-bottom: 25px;
justify-content: center;
}
.mode-btn {
background: rgba(20, 18, 16, 0.8);
border: 2px solid #333;
color: #777;
padding: 10px 12px;
font-size: 12px;
font-weight: 900;
border-radius: 8px;
cursor: pointer;
flex: 1;
transition: all 0.25s ease;
}
.mode-btn:hover {
border-color: #ffaa00;
color: white;
}
#btn-mode-easy.active {
border-color: #33ff33;
color: #33ff33;
box-shadow: 0 0 12px rgba(51, 255, 51, 0.25);
background: rgba(51, 255, 51, 0.08);
}
#btn-mode-hard.active {
border-color: #ff8800;
color: #ff8800;
box-shadow: 0 0 12px rgba(255, 136, 0, 0.25);
background: rgba(255, 136, 0, 0.08);
}
#btn-mode-impossible.active {
border-color: #ff0055;
color: #ff0055;
box-shadow: 0 0 12px rgba(255, 0, 85, 0.25);
background: rgba(255, 0, 85, 0.08);
}
```
--- sketch.js ---
```javascript
// Remixed from: ElectricPanda36/drift-stars-final-release-remix
// Original: https://p5js.ai/gallery/u/ElectricPanda36/drift-stars-final-release-remix
// ----------------------------------------
// ----------------------------------------
// Safe bypass for sandboxed environment tracking failures
// ----------------------------------------
const originalFetch = window.fetch;
window.fetch = function(...args) {
return originalFetch.apply(this, args).catch(err => {
if (typeof args[0] === 'string' && args[0].includes('gtag')) {
return new Response(null, { status: 204 });
}
throw err;
});
};
// ----------------------------------------
// Elevated Track - Destruction Physics Sandbox
// ----------------------------------------
let gameState = 'HOME'; // 'HOME', 'PLAYING', 'GAMEOVER', 'VICTORY'
let trackMode = 'HARD'; // 'EASY', 'HARD', 'IMPOSSIBLE'
let car;
let camPos;
// Track Coordinate Markers
let trackPoints = [];
let trackWidth = 145;
// Obstacles & Hazards
let obstacles = [];
let hazardBeams = [];
let destructibles = []; // Remains of disintegrated car
let maxDebrisBlocks = 150;
// Particles
let skidmarks = [];
let smokeParticles = [];
let sparkParticles = [];
// Game Scores
let driftScore = 0;
let progressPct = 0;
// Audio Engine
let sfx;
let touchInput = { up: false, down: false, left: false, right: false };
// UI Tracking
let lastSpeedText = "";
let lastDriftText = "";
let lastProgress = -1;
function setup() {
createCanvas(windowWidth, windowHeight, WEBGL);
perspective(PI / 1.8, width / height, 10, 50000);
sfx = new SoundManager();
setupMenuInteractions();
setupMobileControls();
generateTrack();
// Initialize car
car = new Car(0, 0, color(255, 90, 0));
}
function generateTrack() {
trackPoints = [];
let cx = 0;
let cz = 0;
trackPoints.push({x: cx, z: cz});
let segments = [];
if (trackMode === 'EASY') {
// Shorter, gentler turns
segments = [
{ dx: 0, dz: -900 },
{ dx: -200, dz: -700 },
{ dx: 200, dz: -700 },
{ dx: 0, dz: -1100 }
];
} else if (trackMode === 'HARD') {
// Standard hand-crafted winding sequence
segments = [
{ dx: 0, dz: -800 },
{ dx: -250, dz: -500 },
{ dx: -600, dz: -100 },
{ dx: -300, dz: -600 },
{ dx: 400, dz: -600 },
{ dx: 800, dz: 0 },
{ dx: 400, dz: -700 },
{ dx: 0, dz: -1200 },
{ dx: -500, dz: -500 },
{ dx: -100, dz: -800 }
];
} else if (trackMode === 'IMPOSSIBLE') {
// Brutal hairpins, long drops, extreme blind turns
segments = [
{ dx: 0, dz: -600 },
{ dx: -500, dz: -350 }, // Extremely sharp diagonal left
{ dx: 600, dz: -450 }, // Immediate snap right
{ dx: -700, dz: -150 }, // Sudden sharp low angle
{ dx: 850, dz: 100 }, // Wicked high-speed reverse loop
{ dx: -900, dz: -400 }, // Ultra long blind stretch
{ dx: 300, dz: -900 },
{ dx: -500, dz: -500 },
{ dx: 700, dz: -600 },
{ dx: -300, dz: -1000 } // Super narrow straight run
];
}
for (let s of segments) {
cx += s.dx;
cz += s.dz;
trackPoints.push({x: cx, z: cz});
}
}
function initGameWorld() {
destructibles = [];
sparkParticles = [];
smokeParticles = [];
skidmarks = [];
driftScore = 0;
progressPct = 0;
// Set dimensions depending on Difficulty Selection
if (trackMode === 'EASY') {
trackWidth = 190; // Very wide road
} else if (trackMode === 'HARD') {
trackWidth = 145; // Standard road
} else if (trackMode === 'IMPOSSIBLE') {
trackWidth = 95; // Ultra narrow deck
}
// Regenerate track layout
generateTrack();
// Setup Player Car (Hot Industrial Orange)
car = new Car(0, 0, color(255, 90, 0));
car.isPlayer = true;
camPos = createVector(0, -300);
spawnTrackObstacles();
// UI Clearances
updateDriftUI();
updateProgressUI(0);
}
function spawnTrackObstacles() {
obstacles = [];
hazardBeams = [];
let scaleDivisor = 260; // Hard mode standard
if (trackMode === 'EASY') scaleDivisor = 420;
if (trackMode === 'IMPOSSIBLE') scaleDivisor = 150; // Dense barriers!
for (let i = 0; i < trackPoints.length - 1; i++) {
let p1 = trackPoints[i];
let p2 = trackPoints[i+1];
let dx = p2.x - p1.x;
let dz = p2.z - p1.z;
let segLen = sqrt(dx*dx + dz*dz);
// Safety clearance zone for start line
if (i === 0) continue;
let numObs = floor(segLen / scaleDivisor);
for (let j = 1; j <= numObs; j++) {
let pct = j / (numObs + 1);
let ox = p1.x + dx * pct;
let oz = p1.z + dz * pct;
let angle = atan2(dz, dx);
let perpX = -sin(angle);
let perpZ = cos(angle);
let seed = (i * 7 + j) % 3;
if (seed === 0) {
// Concrete wall block narrowing path
let offsetSide = ((j % 2 === 0) ? -1 : 1);
let offsetDistance = trackWidth * 0.22;
if (trackMode === 'EASY') offsetDistance = trackWidth * 0.32; // Place closer to edge
if (trackMode === 'IMPOSSIBLE') offsetDistance = trackWidth * 0.15; // Place closer to center
let wx = ox + perpX * offsetDistance * offsetSide;
let wz = oz + perpZ * offsetDistance * offsetSide;
obstacles.push(new TrackObstacle(wx, -15, wz, 35, 30, 35, color(180, 40, 40)));
} else if (seed === 1) {
// Spinning hazard beam in the center
let beamRadius = random(85, 115);
let speedMultiplier = 1.0;
if (trackMode === 'EASY') {
beamRadius = random(60, 80);
speedMultiplier = 0.5; // Slower spinning
} else if (trackMode === 'IMPOSSIBLE') {
beamRadius = random(105, 125); // Spans almost the whole narrow road
speedMultiplier = 2.2; // Blazing fast!
}
let hazard = new HazardBeam(ox, oz, beamRadius);
hazard.speed *= speedMultiplier;
hazardBeams.push(hazard);
} else {
// Slalom safety pillar directly in center
obstacles.push(new TrackObstacle(ox, -30, oz, 24, 60, 24, color(255, 170, 0)));
}
}
}
}
function draw() {
// Atmosphere and Elevated Grid Skybox
background(18, 19, 23);
if (gameState === 'PLAYING') {
// Controls Input
let gas = 0, steer = 0;
if (!car.isWrecked) {
if (keyIsDown(87) || keyIsDown(UP_ARROW) || touchInput.up) gas = 1;
if (keyIsDown(83) || keyIsDown(DOWN_ARROW) || touchInput.down) gas = -1;
if (keyIsDown(65) || keyIsDown(LEFT_ARROW) || touchInput.left) steer = -1;
if (keyIsDown(68) || keyIsDown(RIGHT_ARROW) || touchInput.right) steer = 1;
}
car.update(gas, steer);
// Dynamic Camera Track
let targetCam = car.pos.copy();
let forward = createVector(cos(car.angle), sin(car.angle));
let desiredCam = p5.Vector.sub(targetCam, p5.Vector.mult(forward, 250));
camPos.x = lerp(camPos.x, desiredCam.x, 0.08);
camPos.y = lerp(camPos.y, desiredCam.y, 0.08);
let camHeight = car.posY - 120; // Elevate camera above road deck
camera(camPos.x, camHeight, camPos.y, targetCam.x, car.posY, targetCam.y, 0, 1, 0);
updateSpeedText(round(car.vel.mag() * 2.8) + " mph");
sfx.update(gas, car.vel.mag(), car.isDrifting);
sfx.playMusic();
// Check Victory condition (reached final track point node)
let endNode = trackPoints[trackPoints.length - 1];
let dToEnd = distSq(car.pos.x, car.pos.y, endNode.x, endNode.z);
if (dToEnd < 9000 && !car.isWrecked) {
gameState = 'VICTORY';
document.getElementById('victory-stats').innerText = "Final Drift Score: " + driftScore;
showScreen('victory-screen');
}
} else {
// Menu cameras rotating around the start gate
let angleTime = millis() * 0.0003;
camera(cos(angleTime) * 450, -140, sin(angleTime) * 450, 0, -20, 0, 0, 1, 0);
sfx.muteAll();
}
// Lighting Config
ambientLight(110);
directionalLight(255, 230, 200, 0.4, 1, -0.4);
pointLight(255, 120, 30, car.pos.x, car.posY - 100, car.pos.y);
// Render Pass
drawLowerGrid();
drawElevatedTrack();
drawSkidmarks();
drawDestructibles();
drawSparks();
drawSmoke();
// Render active obstacles
for (let o of obstacles) o.display();
for (let b of hazardBeams) {
if (gameState === 'PLAYING') b.update();
b.display();
}
if (gameState === 'PLAYING') checkPhysicsCollisions();
car.display();
}
function drawLowerGrid() {
// Giant wireframe floor grid at the bottom of the void to emphasize height
push();
translate(car.pos.x - (car.pos.x % 2000), 550, car.pos.y - (car.pos.y % 2000));
rotateX(HALF_PI);
stroke(255, 50, 0, 30);
strokeWeight(2);
noFill();
// Draw grid lines
let gridSz = 4000;
let steps = 40;
for (let i = -gridSz/2; i <= gridSz/2; i += gridSz/steps) {
line(i, -gridSz/2, i, gridSz/2);
line(-gridSz/2, i, gridSz/2, i);
}
pop();
}
function drawElevatedTrack() {
// Render elevated segments and concrete blocks
for (let i = 0; i < trackPoints.length - 1; i++) {
let p1 = trackPoints[i];
let p2 = trackPoints[i+1];
let dx = p2.x - p1.x;
let dz = p2.z - p1.z;
let len = sqrt(dx*dx + dz*dz);
let angle = atan2(dz, dx);
push();
translate((p1.x + p2.x)/2, 10, (p1.z + p2.z)/2);
rotateY(-angle);
// Elevated concrete segment bed
fill(42, 45, 50);
noStroke();
box(len + 15, 16, trackWidth);
// Safety bright warning stripes along edges
fill(255, 50, 0);
push(); translate(0, 8, trackWidth/2 - 2); box(len + 15, 3, 5); pop();
push(); translate(0, 8, -trackWidth/2 + 2); box(len + 15, 3, 5); pop();
// Bottom support structures going down into the floor fog
fill(28, 30, 34);
push(); translate(0, 250, 0); box(35, 500, 35); pop();
pop();
}
// Render Victory Gate at the last point
let endPt = trackPoints[trackPoints.length - 1];
push();
translate(endPt.x, -60, endPt.z);
fill(51, 255, 51); // Neon green success ring
noStroke();
// Left Arch post
push(); translate(0, 20, -trackWidth/2); box(20, 100, 20); pop();
// Right Arch post
push(); translate(0, 20, trackWidth/2); box(20, 100, 20); pop();
// Overhead beam
push(); translate(0, -30, 0); box(20, 15, trackWidth + 20); pop();
pop();
}
// --- Destructible Block Engine ---
class DestructibleBlock {
constructor(x, y, z, w, h, d, blockColor) {
this.pos = createVector(x, y, z);
this.vel = createVector(0, 0, 0);
this.rot = createVector(0, random(TWO_PI), 0);
this.rotVel = createVector(0, 0, 0);
this.w = w; this.h = h; this.d = d;
this.color = blockColor;
this.active = true;
this.isHit = false;
this.life = 255;
}
update() {
if (!this.active) return;
if (this.isHit) {
// Void fall simulation
this.vel.y += 0.5; // Gravity
this.pos.add(this.vel);
this.rot.add(this.rotVel);
// Ground bounce (disintegration elements falling down to grid depth)
if (this.pos.y >= 540) {
this.pos.y = 540;
this.vel.y = -this.vel.y * 0.25;
this.vel.x *= 0.7;
this.vel.z *= 0.7;
this.rotVel.mult(0.6);
this.life -= 4.0;
if (this.life <= 0) this.active = false;
}
}
}
display() {
if (!this.active) return;
push();
translate(this.pos.x, this.pos.y, this.pos.z);
rotateX(this.rot.x);
rotateY(this.rot.y);
rotateZ(this.rot.z);
noStroke();
if (this.isHit) {
let c = color(red(this.color), green(this.color), blue(this.color), this.life);
fill(c);
} else {
fill(this.color);
}
let scaleFactor = map(this.life, 255, 0, 1.0, 0.1);
box(this.w * scaleFactor, this.h * scaleFactor, this.d * scaleFactor);
pop();
}
}
// --- Dynamic Heavy Static Barriers ---
class TrackObstacle {
constructor(x, y, z, w, h, d, col) {
this.pos = createVector(x, y, z);
this.w = w; this.h = h; this.d = d;
this.color = col;
}
display() {
push();
translate(this.pos.x, this.pos.y, this.pos.z);
fill(this.color);
noStroke();
box(this.w, this.h, this.d);
// Metal warning cap
translate(0, -this.h/2 - 1, 0);
fill(30);
box(this.w + 2, 2, this.d + 2);
pop();
}
}
// --- Heavy Rotating Laser-Wire Hazard Beams ---
class HazardBeam {
constructor(x, z, r) {
this.x = x;
this.z = z;
this.angle = random(TWO_PI);
this.speed = random(0.015, 0.035) * (random() > 0.5 ? 1 : -1);
this.length = r;
}
update() {
this.angle += this.speed;
}
display() {
push();
translate(this.x, -25, this.z);
// Draw anchor pillar
fill(80);
noStroke();
push(); translate(0, 15, 0); cylinder(8, 30); pop();
// Draw rotating warning wire bar
rotateY(this.angle);
fill(255, 30, 0);
box(this.length, 10, 10);
// Bright neon hazard flashing tips
fill(255, 230, 0);
push(); translate(this.length/2, 0, 0); box(4, 12, 12); pop();
push(); translate(-this.length/2, 0, 0); box(4, 12, 12); pop();
pop();
}
checkCollision(car) {
let d = dist(car.pos.x, car.pos.y, this.x, this.z);
if (d < this.length / 2 + 20) {
// Projection math to find distance to line segment
let bDirX = cos(this.angle);
let bDirZ = sin(this.angle);
let cx = car.pos.x - this.x;
let cz = car.pos.y - this.z;
let dot = cx * bDirX + cz * bDirZ;
let projX = bDirX * dot;
let projZ = bDirZ * dot;
let perpDist = dist(cx, cz, projX, projZ);
if (perpDist < 20 && abs(dot) < this.length / 2) {
return true;
}
}
return false;
}
}
function drawDestructibles() {
for (let i = destructibles.length - 1; i >= 0; i--) {
let b = destructibles[i];
b.update();
b.display();
if (!b.active) destructibles.splice(i, 1);
}
}
function drawSkidmarks() {
fill(10, 10, 12, 200); noStroke();
for(let s of skidmarks) {
push(); translate(s.x, 1, s.z); rotateX(HALF_PI); plane(12, 12); pop();
}
}
function drawSmoke() {
noStroke();
for (let i = smokeParticles.length - 1; i >= 0; i--) {
let s = smokeParticles[i]; s.x += s.vx; s.y += s.vy; s.z += s.vz; s.life -= 12; s.size += 0.8;
push(); translate(s.x, s.y, s.z);
fill(255, 80, 0, constrain(s.life, 0, 160));
rotateX(HALF_PI); plane(s.size, s.size); pop();
if (s.life <= 0) smokeParticles.splice(i, 1);
}
}
function drawSparks() {
noStroke();
for (let i = sparkParticles.length - 1; i >= 0; i--) {
let p = sparkParticles[i];
p.pos.add(p.vel);
p.vel.y += 0.4; // Gravity on sparks
p.life -= 15;
push();
translate(p.pos.x, p.pos.y, p.pos.z);
fill(255, 140, 0, p.life);
box(p.size);
pop();
if (p.life <= 0 || p.pos.y >= 540) sparkParticles.splice(i, 1);
}
}
function spawnSparks(x, y, z, count) {
for (let i = 0; i < count; i++) {
sparkParticles.push({
pos: createVector(x, y, z),
vel: createVector(random(-8, 8), random(-12, -4), random(-8, 8)),
life: 255,
size: random(3, 8)
});
}
}
function checkPhysicsCollisions() {
if (car.isWrecked) return;
// Track Off-Road Fall boundary check
let status = getDistanceToTrack(car.pos.x, car.pos.y);
if (status.dist > trackWidth / 2) {
car.onTrack = false;
} else {
// Standard progress metrics calculation
progressPct = round((status.index / (trackPoints.length - 1)) * 100);
updateProgressUI(progressPct);
}
// Static concrete obstacle crash checks
for (let o of obstacles) {
let d = dist(car.pos.x, car.pos.y, o.pos.x, o.pos.z);
if (d < 38) {
// Instant collision death!
car.takeDamage();
spawnSparks(o.pos.x, -15, o.pos.z, 25);
return;
}
}
// Rotating warning laser-beam crash checks
for (let b of hazardBeams) {
if (b.checkCollision(car)) {
car.takeDamage();
spawnSparks(car.pos.x, -15, car.pos.y, 25);
return;
}
}
}
function getDistanceToTrack(carX, carZ) {
let minDist = Infinity;
let closestIndex = -1;
for (let i = 0; i < trackPoints.length - 1; i++) {
let p1 = trackPoints[i];
let p2 = trackPoints[i+1];
let d = distToSegment(carX, carZ, p1.x, p1.z, p2.x, p2.z);
if (d < minDist) {
minDist = d;
closestIndex = i;
}
}
return { dist: minDist, index: closestIndex };
}
function distToSegment(px, pz, x1, z1, x2, z2) {
let l2 = distSq(x1, z1, x2, z2);
if (l2 === 0) return dist(px, pz, x1, z1);
let t = ((px - x1) * (x2 - x1) + (pz - z1) * (z2 - z1)) / l2;
t = max(0, min(1, t));
return dist(px, pz, x1 + t * (x2 - x1), z1 + t * (z2 - z1));
}
function updateSpeedText(text) {
if (lastSpeedText !== text) {
let sd = document.getElementById('speed-display'); if (sd) sd.innerText = text; lastSpeedText = text;
}
}
function updateDriftUI() {
let dtText = "DRIFT: " + driftScore;
if (lastDriftText !== dtText) {
let df = document.getElementById('drift-display'); if (df) df.innerText = dtText; lastDriftText = dtText;
}
}
function updateProgressUI(pct) {
let boundPct = constrain(pct, 0, 100);
if (boundPct !== lastProgress) {
let pFill = document.getElementById('progress-fill'); if (pFill) pFill.style.width = boundPct + '%';
let pText = document.getElementById('progress-text'); if (pText) pText.innerText = boundPct + '%';
lastProgress = boundPct;
}
}
function distSq(x1, y1, x2, y2) { return (x1 - x2) ** 2 + (y1 - y2) ** 2; }
function keyPressed() {
if (gameState === 'PLAYING' && (key === 'r' || key === 'R')) {
initGameWorld();
}
}
function triggerGameOver() {
gameState = 'GAMEOVER';
showScreen('busted-screen');
}
// --- Physical Dynamic Car Entity ---
class Car {
constructor(x, z, carColor) {
this.pos = createVector(x, z);
this.vel = createVector(0, 0);
this.angle = -HALF_PI; // Start facing forward along track direction (Z-)
this.steerAngle = 0;
this.color = carColor;
this.isPlayer = false;
this.posY = -15; // Raised highway height
this.onTrack = true;
this.fallSpeed = 0;
this.isDrifting = false;
this.maxSpeed = 35;
this.accel = 0.65;
this.friction = 0.97;
this.isWrecked = false;
this.wreckTimer = 0;
}
breakPart(rx, ry, rz, pw, ph, pd, partColor) {
// Relative offset transformation to world spatial matrix
let cosA = cos(this.angle);
let sinA = sin(this.angle);
let wx = this.pos.x + rx * cosA - rz * sinA;
let wy = this.posY + ry;
let wz = this.pos.y + rx * sinA + rz * cosA;
let block = new DestructibleBlock(wx, wy, wz, pw, ph, pd, partColor);
block.isHit = true;
// Apply explosion vector
let explodeForce = p5.Vector.random3D().mult(random(4, 9));
block.vel = createVector(this.vel.x, -random(5, 11), this.vel.y).add(explodeForce);
block.rotVel = createVector(random(-0.2, 0.2), random(-0.2, 0.2), random(-0.2, 0.2));
if (destructibles.length < maxDebrisBlocks) {
destructibles.push(block);
}
}
takeDamage() {
if (this.isWrecked) return;
this.isWrecked = true;
this.wreckTimer = millis();
let chassisH = 18, chassisW = 45, chassisL = 90;
let wb = chassisL*0.32, tw = chassisW*0.53;
// Explode specific car parts
this.breakPart(15, -12, 0, 25, 7, 18, color(20)); // Scoop
this.breakPart(-10, -18, 0, chassisL*0.5, 14, chassisW*0.8, color(100, 180, 255, 150)); // Cab Glass
// Release wheels
this.breakPart(wb, chassisH/2, -tw, 20, 20, 11, color(15));
this.breakPart(wb, chassisH/2, tw, 20, 20, 11, color(15));
this.breakPart(-wb, chassisH/2, -tw, 20, 20, 11, color(15));
this.breakPart(-wb, chassisH/2, tw, 20, 20, 11, color(15));
// Splinter chassis body into distinct flying chunks
for (let cx = -1; cx <= 1; cx++) {
for (let cz = -1; cz <= 1; cz += 2) {
this.breakPart(cx * (chassisL/3), 0, cz * (chassisW/4), chassisL/3.2, chassisH, chassisW/2.2, this.color);
}
}
spawnSparks(this.pos.x, this.posY, this.pos.y, 40);
sfx.playCrash();
}
update(gas, steerInput) {
if (this.isWrecked) {
this.vel.mult(0.92);
this.pos.add(this.vel);
// Void fall on debris elements
this.posY += this.fallSpeed;
if (!this.onTrack) this.fallSpeed += 0.8;
if (millis() - this.wreckTimer > 2000) {
triggerGameOver();
}
return;
}
// If car falls off the elevated safety platforms
if (!this.onTrack) {
this.posY += this.fallSpeed;
this.fallSpeed += 0.7; // Gravity
// Once fallen into void depth limit, trigger instant detonation
if (this.posY > 150) {
this.takeDamage();
return;
}
}
this.steerAngle = lerp(this.steerAngle, steerInput * PI / 4.4, 0.22);
let speed = this.vel.mag();
let forward = createVector(cos(this.angle), sin(this.angle));
let isMovingForward = this.vel.dot(forward) >= 0;
this.vel.add(p5.Vector.mult(forward, gas * this.accel));
this.vel.mult(this.friction);
if (speed > 1.0) {
let turnEffect = steerInput * 0.055;
if (!isMovingForward) turnEffect *= -1;
turnEffect *= map(speed, 0, this.maxSpeed, 1.2, 0.5);
this.angle += turnEffect;
}
forward = createVector(cos(this.angle), sin(this.angle));
let desiredVel = p5.Vector.mult(forward, speed * (isMovingForward ? 1 : -1));
let grip = (abs(steerInput) > 0 && speed > 13) ? 0.04 : 0.16; // Drift dynamics
this.vel.lerp(desiredVel, grip);
let slipAmount = p5.Vector.dist(this.vel, desiredVel);
this.isDrifting = (slipAmount > 2.5 && speed > 5);
if (this.isDrifting && this.onTrack) {
this.generateSkidmarks();
if (random() > 0.65) this.generateSmoke();
driftScore += 2;
updateDriftUI();
}
this.pos.add(this.vel);
this.vel.limit(this.maxSpeed);
}
generateSkidmarks() {
let forward = createVector(cos(this.angle), sin(this.angle)); let right = createVector(-sin(this.angle), cos(this.angle));
let rearCenter = p5.Vector.sub(this.pos, p5.Vector.mult(forward, 36));
let rl = p5.Vector.sub(rearCenter, p5.Vector.mult(right, 18)); let rr = p5.Vector.add(rearCenter, p5.Vector.mult(right, 18));
skidmarks.push({ x: rl.x, z: rl.y });
skidmarks.push({ x: rr.x, z: rr.y });
if (skidmarks.length > 70) skidmarks.splice(0, 2);
}
generateSmoke() {
let forward = createVector(cos(this.angle), sin(this.angle)); let rc = p5.Vector.sub(this.pos, p5.Vector.mult(forward, 40));
smokeParticles.push({
x: rc.x + random(-15, 15), y: this.posY + 8, z: rc.y + random(-15, 15),
vx: this.vel.x * 0.15 + random(-1, 1), vy: random(-2, -1), vz: this.vel.y * 0.15 + random(-1, 1),
life: 220, size: random(10, 25)
});
}
display() {
if (this.isWrecked) return; // Do not render vehicle body if disintegrated
push();
translate(this.pos.x, this.posY, this.pos.y);
rotateY(-this.angle);
noStroke();
specularMaterial(230); shininess(25);
let chassisH = 18, chassisW = 45, chassisL = 90;
// Main Chassis Base Block
fill(this.color);
box(chassisL, chassisH, chassisW);
// Front Bumper / Skid Plate
fill(40);
push(); translate(chassisL/2 + 2, 4, 0); box(6, 10, chassisW + 4); pop();
// Hood scoop
push(); translate(15, -(chassisH/2 + 3), 0); fill(20); box(25, 7, 18); pop();
// Cab / Window Structure
push(); translate(-8, -(chassisH/2 + 8), 0); fill(red(this.color)*0.4, green(this.color)*0.4, blue(this.color)*0.4); box(chassisL*0.42, 14, chassisW * 0.85); pop();
// Front and Rear Headlights
push(); translate(chassisL/2 + 1, -2, chassisW/3); fill(255, 230, 100); box(2, 4, 8); pop();
push(); translate(chassisL/2 + 1, -2, -chassisW/3); fill(255, 230, 100); box(2, 4, 8); pop();
push(); translate(-chassisL/2 - 1, -2, chassisW/3); fill(230, 30, 30); box(2, 4, 10); pop();
push(); translate(-chassisL/2 - 1, -2, -chassisW/3); fill(230, 30, 30); box(2, 4, 10); pop();
// Wheels
fill(25); specularMaterial(30); shininess(2);
let wb = chassisL*0.32, tw = chassisW*0.53;
push(); translate(wb, chassisH/2, -tw); rotateY(-this.steerAngle); rotateX(HALF_PI); cylinder(11, 10, 6, 1); pop();
push(); translate(wb, chassisH/2, tw); rotateY(-this.steerAngle); rotateX(HALF_PI); cylinder(11, 10, 6, 1); pop();
push(); translate(-wb, chassisH/2, -tw); rotateX(HALF_PI); cylinder(11, 10, 6, 1); pop();
push(); translate(-wb, chassisH/2, tw); rotateX(HALF_PI); cylinder(11, 10, 6, 1); pop();
pop();
}
}
function windowResized() { resizeCanvas(windowWidth, windowHeight); perspective(PI / 1.8, width / height, 10, 50000); }
function showScreen(id) {
document.querySelectorAll('.screen, #ui-layer').forEach(el => el.style.display = 'none');
let tgt = document.getElementById(id); if (tgt) tgt.style.display = (id === 'ui-layer') ? 'block' : 'flex';
}
function setupMenuInteractions() {
let bindClick = (id, fn) => { let el = document.getElementById(id); if (el) el.onclick = fn; };
bindClick('btn-start', () => { userStartAudio().then(() => sfx.start()); initGameWorld(); gameState = 'PLAYING'; showScreen('ui-layer'); });
bindClick('btn-credits', () => { showScreen('credits-screen'); });
bindClick('btn-back', () => { showScreen('home-screen'); });
bindClick('btn-restart', () => { initGameWorld(); gameState = 'PLAYING'; showScreen('ui-layer'); });
bindClick('btn-to-menu', () => { gameState = 'HOME'; showScreen('home-screen'); });
bindClick('btn-victory-restart', () => { initGameWorld(); gameState = 'PLAYING'; showScreen('ui-layer'); });
// Mode Selection Triggers
const setTrackMode = (mode) => {
trackMode = mode;
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
let activeBtn = document.getElementById('btn-mode-' + mode.toLowerCase());
if (activeBtn) activeBtn.addClass('active'); // using standard vanilla ClassList inside setup wrapper
};
bindClick('btn-mode-easy', () => {
trackMode = 'EASY';
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById('btn-mode-easy').classList.add('active');
});
bindClick('btn-mode-hard', () => {
trackMode = 'HARD';
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById('btn-mode-hard').classList.add('active');
});
bindClick('btn-mode-impossible', () => {
trackMode = 'IMPOSSIBLE';
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById('btn-mode-impossible').classList.add('active');
});
}
// --- Synthesized Dynamic Audio Engine ---
class SoundManager {
constructor() {
this.started = false;
this.engineOsc = new p5.Oscillator('sawtooth'); this.engineOsc.amp(0); this.engineOsc.freq(60);
this.skidOsc = new p5.Oscillator('square'); this.skidOsc.amp(0); this.skidOsc.freq(800);
this.crashNoise = new p5.Noise('white');
this.crashEnv = new p5.Envelope(); this.crashEnv.setADSR(0.01, 0.1, 0.25, 0.35); this.crashEnv.setRange(0.65, 0);
this.crashNoise.amp(this.crashEnv);
this.musicOsc = new p5.Oscillator('sawtooth'); this.musicOsc.amp(0);
this.musicEnv = new p5.Envelope(); this.musicEnv.setADSR(0.06, 0.12, 0.0, 0.0); this.musicEnv.setRange(0.09, 0);
this.musicOsc.amp(this.musicEnv);
this.notes = [110.00, 130.81, 146.83, 164.81]; this.musicStep = 0;
}
start() {
if (!this.started) {
this.engineOsc.start(); this.skidOsc.start(); this.crashNoise.start(); this.musicOsc.start(); this.started = true;
}
}
update(gas, speed, isDrifting) {
if (!this.started) return;
let targetFreq = map(speed, 0, 35, 55, 230); let targetAmp = abs(gas) > 0 ? 0.16 : 0.06;
this.engineOsc.freq(targetFreq, 0.1); this.engineOsc.amp(targetAmp, 0.1);
if (isDrifting) { this.skidOsc.amp(0.07, 0.05); this.skidOsc.freq(random(650, 950)); } else { this.skidOsc.amp(0, 0.2); }
}
playCrash() { if (this.started) this.crashEnv.play(); }
playMusic() {
if (!this.started) return;
if (frameCount % 15 === 0) {
let f = this.notes[this.musicStep % this.notes.length];
this.musicOsc.freq(f); this.musicEnv.play(); this.musicStep++;
}
}
muteAll() { if (!this.started) return; this.engineOsc.amp(0, 0.5); this.skidOsc.amp(0, 0.5); }
}
function setupMobileControls() {
const bindBtn = (id, key) => {
let el = document.getElementById(id); if (!el) return;
el.addEventListener('touchstart', (e) => { e.preventDefault(); touchInput[key] = true; }, { passive: false });
el.addEventListener('touchend', (e) => { e.preventDefault(); touchInput[key] = false; }, { passive: false });
el.addEventListener('mousedown', (e) => { e.preventDefault(); touchInput[key] = true; });
el.addEventListener('mouseup', (e) => { e.preventDefault(); touchInput[key] = false; });
el.addEventListener('mouseleave', (e) => { e.preventDefault(); touchInput[key] = false; });
};
bindBtn('btn-left', 'left'); bindBtn('btn-right', 'right'); bindBtn('btn-gas', 'up'); bindBtn('btn-brake', 'down');
}
```
### 🛠️ What was added and improved:
1. **Dynamic Car Garage & Shop Screen**: Included directly on the main menu, with custom stats, performance specifications, custom description, and custom-tuned price points.
2. **Persistent Player Progression (`localStorage`)**: Your total credits, active car choice, and unlocked car roster are saved locally. You won't lose your collection on refresh!
3. **Drift-to-Earn Economy**:
- Drifting gives you points that convert directly to **Credits (CR)**.
- Wrecking converts your score to credits directly.
- Reaching the victory arch gives you a **Double Victory CR Bonus**!
4. **4 High-Performance Upgradable Cars**:
- 🔶 **Stinger** (Orange - Unlocked, balanced specs).
- 🔷 **Neon Glide** (Cyan - Faster, slick drift slide).
- 🔮 **Phantasm** (Hot Pink - Insanely fast, extreme drift capabilities).
- 🌟 **Apex Gold** (Liquid Gold - Ultimate speed, luxury control grip).
5. **Physical Upgrades & Aesthetic Customization**: Changing cars updates the actual 3D rendering color, and when you disintegrate, **all flying debris chunks match the upgraded car paint color**.
6. **Custom Synthesized Audio Shop Beeps**: Hand-crafted synthesizers trigger satisfying purchase/equip double-beeps directly inside the shop.
7. **Latent Bug Fixes**: Corrected a standard DOM bug in difficulty selection where `.addClass()` (a jQuery function) was called on a raw DOM element.
---
### Complete Code Files
--- index.html ---
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Destruction Physics Sandbox</title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.11/lib/p5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.11/lib/addons/p5.sound.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- HOME SCREEN -->
<div id="home-screen" class="screen main-menu">
<div class="menu-box">
<h1>DESTRUCTION PHYSICS</h1>
<h2 style="color: #ffaa00; margin-top: -10px; letter-spacing: 2px;">THE IMPOSSIBLE TRACK</h2>
<p>Stay on the elevated road. Dodge spinning hazards. One touch = Instant Disintegration.</p>
<!-- MODE SELECTOR -->
<div style="font-size: 12px; letter-spacing: 1.5px; color: #ffaa00; font-weight: bold; margin-bottom: 8px;">SELECT CHANNELS</div>
<div class="mode-selector">
<button id="btn-mode-easy" class="mode-btn">EASY</button>
<button id="btn-mode-hard" class="mode-btn active">HARD</button>
<button id="btn-mode-impossible" class="mode-btn">IMPOSSIBLE</button>
</div>
<button id="btn-start" class="menu-btn" style="background: #ff5500; color: white; font-weight:900;">START RUN</button>
<button id="btn-shop" class="menu-btn" style="background: #00e5ff; color: black; font-weight:bold;">GARAGE & SHOP</button>
<button id="btn-credits" class="menu-btn">CREDITS</button>
</div>
</div>
<!-- SHOP / GARAGE SCREEN -->
<div id="shop-screen" class="screen main-menu" style="display: none;">
<div class="menu-box shop-box" style="max-width: 550px; width: 90%;">
<h1>GARAGE & SHOP</h1>
<div style="font-size: 18px; color: #ffaa00; margin-bottom: 20px; font-weight: bold; font-family: monospace;">
CREDITS: <span id="shop-credits-display">0</span> CR
</div>
<div class="shop-grid">
<div class="shop-item" id="item-stinger">
<h3 style="color: #ff5500;">STINGER</h3>
<div class="car-spec">Top Speed: 35 mph<br>Drift Drift: Balanced<br>Paint: Industrial Orange</div>
<button class="menu-btn shop-action-btn" id="btn-buy-stinger">EQUIP</button>
</div>
<div class="shop-item" id="item-neon">
<h3 style="color: #00e5ff;">NEON GLIDE</h3>
<div class="car-spec">Top Speed: 38 mph<br>Drift Drift: High Slide<br>Paint: Electric Cyan</div>
<div class="cost-tag" id="cost-neon">1,200 CR</div>
<button class="menu-btn shop-action-btn" id="btn-buy-neon">BUY</button>
</div>
<div class="shop-item" id="item-phantasm">
<h3 style="color: #ff00ff;">PHANTASM</h3>
<div class="car-spec">Top Speed: 42 mph<br>Drift Drift: Wild Ghost<br>Paint: Plasma Pink</div>
<div class="cost-tag" id="cost-phantasm">2,500 CR</div>
<button class="menu-btn shop-action-btn" id="btn-buy-phantasm">BUY</button>
</div>
<div class="shop-item" id="item-apex">
<h3 style="color: #ffd700;">APEX GOLD</h3>
<div class="car-spec">Top Speed: 46 mph<br>Drift Drift: Pro Grip<br>Paint: Liquid Gold</div>
<div class="cost-tag" id="cost-apex">5,000 CR</div>
<button class="menu-btn shop-action-btn" id="btn-buy-apex">BUY</button>
</div>
</div>
<button id="btn-shop-back" class="menu-btn" style="margin-top: 20px; background: #333; color: white;">BACK</button>
</div>
</div>
<!-- CREDITS SCREEN -->
<div id="credits-screen" class="screen main-menu" style="display: none;">
<div class="menu-box">
<h1>CREDITS</h1>
<p>Elevated Track Edition Powered by p5.js</p>
<p>Custom 3D Real-time Fragmentation Physics</p>
<button id="btn-back" class="menu-btn">BACK</button>
</div>
</div>
<!-- CRASH / GAME OVER SCREEN -->
<div id="busted-screen" class="screen main-menu" style="display: none;">
<div class="menu-box busted-box" style="border-color: #ff3c00;">
<h1 id="busted-title" style="color: #ff3c00;">WRECKED!</h1>
<p id="busted-desc">You collided with an obstacle or went off-road!</p>
<button id="btn-restart" class="menu-btn" style="background: #ff3c00; color: white;">TRY AGAIN</button>
<button id="btn-to-menu" class="menu-btn">MAIN MENU</button>
</div>
</div>
<!-- VICTORY SCREEN -->
<div id="victory-screen" class="screen main-menu" style="display: none;">
<div class="menu-box" style="border-color: #33ff33; box-shadow: 0 0 30px rgba(51, 255, 51, 0.4);">
<h1 style="color: #33ff33;">VICTORY!</h1>
<p>You conquered the Impossible Track without a single scratch!</p>
<h2 id="victory-stats" style="color: #ffaa00;">Drift Score: 0</h2>
<button id="btn-victory-restart" class="menu-btn" style="background: #33ff33; color: black; font-weight: bold;">PLAY AGAIN</button>
<button id="btn-victory-to-menu" class="menu-btn">MAIN MENU</button>
</div>
</div>
<!-- IN-GAME HUD -->
<div id="ui-layer" style="display: none;">
<div id="info">
<div id="speed-display">0 mph</div>
<div id="drift-display" style="color: #33ff33; font-size: 20px; font-weight: bold; margin-top:5px;">DRIFT: 0</div>
<div id="credits-hud-display" style="color: #ffaa00; font-size: 16px; font-weight: bold; margin-top:5px; font-family: monospace;">CREDITS: 0 CR</div>
<div class="progress-container">
<div style="font-size: 11px; letter-spacing: 1px; color: #ff5500;">TRACK COMPLETION</div>
<div id="progress-bar"><div id="progress-fill"></div></div>
<div id="progress-text" style="font-size: 12px; margin-top: 3px; font-weight: bold; font-family: monospace;">0%</div>
</div>
<div class="instructions">P1: WASD / Arrow Keys to Drive • [R] Restart Challenge</div>
</div>
<!-- MOBILE DRIVE CONTROLS -->
<div class="controls">
<div class="dpad">
<div class="btn" id="btn-left">◀</div>
<div class="btn" id="btn-right">▶</div>
</div>
<div class="dpad">
<div class="btn" id="btn-brake">▼</div>
<div class="btn" id="btn-gas">▲</div>
</div>
</div>
</div>
<script src="sketch.js"></script>
</body>
</html>
```
--- style.css ---
```css
body {
margin: 0; padding: 0; overflow: hidden; background-color: #0b0b0d;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
user-select: none; -webkit-user-select: none;
}
canvas {
display: block;
}
#ui-layer {
position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 50;
}
#info {
padding: 20px; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.9);
}
#speed-display {
font-size: 34px; font-weight: 900; color: #ffaa00; font-family: monospace;
}
.instructions {
font-size: 13px; margin-top: 10px; color: #bbb; background: rgba(0, 0, 0, 0.45); padding: 5px 10px; border-radius: 5px; display: inline-block;
}
.controls {
position: absolute; bottom: 25px; width: 100%; display: flex; justify-content: space-between; box-sizing: border-box; padding: 0 20px; pointer-events: none;
}
.dpad {
display: flex; gap: 15px; pointer-events: auto; align-items: flex-end;
}
.btn {
width: 65px; height: 65px; background: rgba(255, 120, 0, 0.15); border: 2px solid rgba(255, 120, 0, 0.5); border-radius: 50%; color: #ffaa00; font-size: 24px; display: flex; align-items: center; justify-content: center; cursor: pointer; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); box-shadow: 0 4px 6px rgba(0,0,0,0.3); transition: background 0.1s;
}
.btn:active {
background: rgba(255, 120, 0, 0.8); color: #000; transform: scale(0.95);
}
.screen {
position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; background: rgba(10, 8, 5, 0.65); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); z-index: 100;
}
.menu-box {
background: rgba(18, 16, 14, 0.95); padding: 40px; border-radius: 15px; border: 2px solid #ff7700; text-align: center; color: white; box-shadow: 0 10px 35px rgba(255,100,0,0.3); min-width: 320px;
}
.busted-box {
border-color: #ff3c00; animation: pulse 1s infinite alternate;
}
@keyframes pulse {
from { box-shadow: 0 0 10px rgba(255, 60, 0, 0.2); } to { box-shadow: 0 0 40px rgba(255, 60, 0, 0.8); }
}
.menu-box h1 {
margin: 0 0 10px 0; font-size: 32px; color: #ffaa00; text-shadow: 2px 2px 4px #000; font-weight: 900;
}
.busted-box h1 {
color: #ff3c00;
}
.menu-box p {
color: #ccc; margin-bottom: 25px;
}
.menu-btn {
display: block; width: 100%; padding: 15px; margin-bottom: 15px; font-size: 18px; font-weight: bold; background: #fff; color: #000; border: none; border-radius: 8px; cursor: pointer; transition: transform 0.1s, background 0.2s;
}
.menu-btn:hover {
background: #ffaa00; color: black; transform: scale(1.03);
}
.menu-btn:active {
transform: scale(0.97);
}
/* Garage / Shop Styles */
.shop-box {
border-color: #00e5ff;
box-shadow: 0 10px 35px rgba(0, 229, 255, 0.2);
}
.shop-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
max-height: 290px;
overflow-y: auto;
padding: 5px;
margin-bottom: 15px;
}
.shop-item {
background: rgba(28, 25, 23, 0.9);
border: 1px solid #444;
border-radius: 8px;
padding: 12px;
text-align: center;
transition: border-color 0.2s, transform 0.2s;
}
.shop-item.selected {
border-color: #ffaa00;
box-shadow: 0 0 10px rgba(255, 170, 0, 0.2);
}
.shop-item h3 {
margin: 0 0 5px 0;
font-size: 16px;
font-weight: 900;
}
.car-spec {
font-size: 11px;
color: #aaa;
line-height: 1.4;
margin-bottom: 8px;
font-family: monospace;
}
.cost-tag {
font-size: 13px;
color: #ffaa00;
font-weight: bold;
font-family: monospace;
margin-bottom: 8px;
}
.shop-action-btn {
padding: 6px 12px !important;
font-size: 12px !important;
margin-bottom: 0 !important;
}
.progress-container {
margin-top: 15px; font-weight: bold; font-size: 13px; color: #ff5500;
}
#progress-bar {
width: 200px; height: 12px; background: rgba(255, 60, 0, 0.15); border: 1px solid #ff5500; border-radius: 6px; margin-top: 5px; overflow: hidden;
}
#progress-fill {
width: 0%; height: 100%; background: #ff5500; transition: width 0.1s;
}
.mode-selector {
display: flex;
gap: 10px;
margin-bottom: 25px;
justify-content: center;
}
.mode-btn {
background: rgba(20, 18, 16, 0.8);
border: 2px solid #333;
color: #777;
padding: 10px 12px;
font-size: 12px;
font-weight: 900;
border-radius: 8px;
cursor: pointer;
flex: 1;
transition: all 0.25s ease;
}
.mode-btn:hover {
border-color: #ffaa00;
color: white;
}
#btn-mode-easy.active {
border-color: #33ff33;
color: #33ff33;
box-shadow: 0 0 12px rgba(51, 255, 51, 0.25);
background: rgba(51, 255, 51, 0.08);
}
#btn-mode-hard.active {
border-color: #ff8800;
color: #ff8800;
box-shadow: 0 0 12px rgba(255, 136, 0, 0.25);
background: rgba(255, 136, 0, 0.08);
}
#btn-mode-impossible.active {
border-color: #ff0055;
color: #ff0055;
box-shadow: 0 0 12px rgba(255, 0, 85, 0.25);
background: rgba(255, 0, 85, 0.08);
}
```
--- sketch.js ---
```javascript
// Remixed from: ElectricPanda36/drift-stars-final-release-remix
// Original: https://p5js.ai/gallery/u/ElectricPanda36/drift-stars-final-release-remix
// ----------------------------------------
// Safe bypass for sandboxed environment tracking failures
const originalFetch = window.fetch;
window.fetch = function(...args) {
return originalFetch.apply(this, args).catch(err => {
if (typeof args[0] === 'string' && args[0].includes('gtag')) {
return new Response(null, { status: 204 });
}
throw err;
});
};
// ----------------------------------------
// Global Persistent State - Declared at top
// ----------------------------------------
let credits = 0;
let unlockedCars = ['STINGER'];
let activeCarType = 'STINGER';
// Car Specifications Preset Configuration
const CAR_PRESETS = {
STINGER: { name: "Stinger", color: [255, 90, 0], cost: 0, maxSpeed: 35, accel: 0.65, grip: 0.16 },
NEON: { name: "Neon Glide", color: [0, 229, 255], cost: 1200, maxSpeed: 38, accel: 0.70, grip: 0.12 },
PHANTASM: { name: "Phantasm", color: [255, 0, 255], cost: 2500, maxSpeed: 42, accel: 0.80, grip: 0.09 },
APEX: { name: "Apex Gold", color: [255, 215, 0], cost: 5000, maxSpeed: 46, accel: 0.90, grip: 0.18 }
};
let gameState = 'HOME'; // 'HOME', 'PLAYING', 'GAMEOVER', 'VICTORY'
let trackMode = 'HARD'; // 'EASY', 'HARD', 'IMPOSSIBLE'
let car;
let camPos;
// Track Coordinate Markers
let trackPoints = [];
let trackWidth = 145;
// Obstacles & Hazards
let obstacles = [];
let hazardBeams = [];
let destructibles = []; // Remains of disintegrated car
let maxDebrisBlocks = 150;
// Particles
let skidmarks = [];
let smokeParticles = [];
let sparkParticles = [];
// Game Scores
let driftScore = 0;
let progressPct = 0;
// Audio Engine
let sfx;
let touchInput = { up: false, down: false, left: false, right: false };
// UI Tracking
let lastSpeedText = "";
let lastDriftText = "";
let lastProgress = -1;
function setup() {
createCanvas(windowWidth, windowHeight, WEBGL);
perspective(PI / 1.8, width / height, 10, 50000);
loadPlayerData(); // Load unlocked inventory and credit score
sfx = new SoundManager();
setupMenuInteractions();
setupMobileControls();
generateTrack();
// Initialize current equipped player car
car = new Car(0, 0);
}
function loadPlayerData() {
try {
let savedCredits = localStorage.getItem('drifter_credits_v1');
if (savedCredits !== null) credits = parseInt(savedCredits);
let savedCars = localStorage.getItem('drifter_unlocked_cars_v1');
if (savedCars !== null) unlockedCars = JSON.parse(savedCars);
let savedActive = localStorage.getItem('drifter_active_car_v1');
if (savedActive !== null) activeCarType = savedActive;
} catch (e) {
console.log("No stored player save found. Starting clean slate.");
}
}
function savePlayerData() {
try {
localStorage.setItem('drifter_credits_v1', credits);
localStorage.setItem('drifter_unlocked_cars_v1', JSON.stringify(unlockedCars));
localStorage.setItem('drifter_active_car_v1', activeCarType);
} catch (e) {
console.log("Storage writing error: ", e);
}
}
function generateTrack() {
trackPoints = [];
let cx = 0;
let cz = 0;
trackPoints.push({x: cx, z: cz});
let segments = [];
if (trackMode === 'EASY') {
segments = [
{ dx: 0, dz: -900 },
{ dx: -200, dz: -700 },
{ dx: 200, dz: -700 },
{ dx: 0, dz: -1100 }
];
} else if (trackMode === 'HARD') {
segments = [
{ dx: 0, dz: -800 },
{ dx: -250, dz: -500 },
{ dx: -600, dz: -100 },
{ dx: -300, dz: -600 },
{ dx: 400, dz: -600 },
{ dx: 800, dz: 0 },
{ dx: 400, dz: -700 },
{ dx: 0, dz: -1200 },
{ dx: -500, dz: -500 },
{ dx: -100, dz: -800 }
];
} else if (trackMode === 'IMPOSSIBLE') {
segments = [
{ dx: 0, dz: -600 },
{ dx: -500, dz: -350 },
{ dx: 600, dz: -450 },
{ dx: -700, dz: -150 },
{ dx: 850, dz: 100 },
{ dx: -900, dz: -400 },
{ dx: 300, dz: -900 },
{ dx: -500, dz: -500 },
{ dx: 700, dz: -600 },
{ dx: -300, dz: -1000 }
];
}
for (let s of segments) {
cx += s.dx;
cz += s.dz;
trackPoints.push({x: cx, z: cz});
}
}
function initGameWorld() {
destructibles = [];
sparkParticles = [];
smokeParticles = [];
skidmarks = [];
driftScore = 0;
progressPct = 0;
if (trackMode === 'EASY') {
trackWidth = 190;
} else if (trackMode === 'HARD') {
trackWidth = 145;
} else if (trackMode === 'IMPOSSIBLE') {
trackWidth = 95;
}
generateTrack();
car = new Car(0, 0);
car.isPlayer = true;
camPos = createVector(0, -300);
spawnTrackObstacles();
updateDriftUI();
updateProgressUI(0);
let crHud = document.getElementById('credits-hud-display');
if (crHud) crHud.innerText = "CREDITS: " + credits + " CR";
}
function spawnTrackObstacles() {
obstacles = [];
hazardBeams = [];
let scaleDivisor = 260;
if (trackMode === 'EASY') scaleDivisor = 420;
if (trackMode === 'IMPOSSIBLE') scaleDivisor = 150;
for (let i = 0; i < trackPoints.length - 1; i++) {
let p1 = trackPoints[i];
let p2 = trackPoints[i+1];
let dx = p2.x - p1.x;
let dz = p2.z - p1.z;
let segLen = sqrt(dx*dx + dz*dz);
if (i === 0) continue;
let numObs = floor(segLen / scaleDivisor);
for (let j = 1; j <= numObs; j++) {
let pct = j / (numObs + 1);
let ox = p1.x + dx * pct;
let oz = p1.z + dz * pct;
let angle = atan2(dz, dx);
let perpX = -sin(angle);
let perpZ = cos(angle);
let seed = (i * 7 + j) % 3;
if (seed === 0) {
let offsetSide = ((j % 2 === 0) ? -1 : 1);
let offsetDistance = trackWidth * 0.22;
if (trackMode === 'EASY') offsetDistance = trackWidth * 0.32;
if (trackMode === 'IMPOSSIBLE') offsetDistance = trackWidth * 0.15;
let wx = ox + perpX * offsetDistance * offsetSide;
let wz = oz + perpZ * offsetDistance * offsetSide;
obstacles.push(new TrackObstacle(wx, -15, wz, 35, 30, 35, color(180, 40, 40)));
} else if (seed === 1) {
let beamRadius = random(85, 115);
let speedMultiplier = 1.0;
if (trackMode === 'EASY') {
beamRadius = random(60, 80);
speedMultiplier = 0.5;
} else if (trackMode === 'IMPOSSIBLE') {
beamRadius = random(105, 125);
speedMultiplier = 2.2;
}
let hazard = new HazardBeam(ox, oz, beamRadius);
hazard.speed *= speedMultiplier;
hazardBeams.push(hazard);
} else {
obstacles.push(new TrackObstacle(ox, -30, oz, 24, 60, 24, color(255, 170, 0)));
}
}
}
}
function draw() {
background(18, 19, 23);
if (gameState === 'PLAYING') {
let gas = 0, steer = 0;
if (!car.isWrecked) {
if (keyIsDown(87) || keyIsDown(UP_ARROW) || touchInput.up) gas = 1;
if (keyIsDown(83) || keyIsDown(DOWN_ARROW) || touchInput.down) gas = -1;
if (keyIsDown(65) || keyIsDown(LEFT_ARROW) || touchInput.left) steer = -1;
if (keyIsDown(68) || keyIsDown(RIGHT_ARROW) || touchInput.right) steer = 1;
}
car.update(gas, steer);
let targetCam = car.pos.copy();
let forward = createVector(cos(car.angle), sin(car.angle));
let desiredCam = p5.Vector.sub(targetCam, p5.Vector.mult(forward, 250));
camPos.x = lerp(camPos.x, desiredCam.x, 0.08);
camPos.y = lerp(camPos.y, desiredCam.y, 0.08);
let camHeight = car.posY - 120;
camera(camPos.x, camHeight, camPos.y, targetCam.x, car.posY, targetCam.y, 0, 1, 0);
updateSpeedText(round(car.vel.mag() * 2.8) + " mph");
sfx.update(gas, car.vel.mag(), car.isDrifting);
sfx.playMusic();
// Check Victory boundary
let endNode = trackPoints[trackPoints.length - 1];
let dToEnd = distSq(car.pos.x, car.pos.y, endNode.x, endNode.z);
if (dToEnd < 9000 && !car.isWrecked) {
gameState = 'VICTORY';
// Award double bonus credits for finishing!
let gained = driftScore * 2;
credits += gained;
savePlayerData();
let vict = document.getElementById('victory-stats');
if (vict) vict.innerHTML = `Final Drift Score: ${driftScore}<br><span style="color: #33ff33; font-weight: bold; font-size: 20px;">+${gained} CR Victory Multiplier!</span>`;
showScreen('victory-screen');
}
} else {
let angleTime = millis() * 0.0003;
camera(cos(angleTime) * 450, -140, sin(angleTime) * 450, 0, -20, 0, 0, 1, 0);
sfx.muteAll();
}
ambientLight(110);
directionalLight(255, 230, 200, 0.4, 1, -0.4);
pointLight(255, 120, 30, car.pos.x, car.posY - 100, car.pos.y);
drawLowerGrid();
drawElevatedTrack();
drawSkidmarks();
drawDestructibles();
drawSparks();
drawSmoke();
for (let o of obstacles) o.display();
for (let b of hazardBeams) {
if (gameState === 'PLAYING') b.update();
b.display();
}
if (gameState === 'PLAYING') checkPhysicsCollisions();
car.display();
}
function drawLowerGrid() {
push();
translate(car.pos.x - (car.pos.x % 2000), 550, car.pos.y - (car.pos.y % 2000));
rotateX(HALF_PI);
stroke(255, 50, 0, 30);
strokeWeight(2);
noFill();
let gridSz = 4000;
let steps = 40;
for (let i = -gridSz/2; i <= gridSz/2; i += gridSz/steps) {
line(i, -gridSz/2, i, gridSz/2);
line(-gridSz/2, i, gridSz/2, i);
}
pop();
}
function drawElevatedTrack() {
for (let i = 0; i < trackPoints.length - 1; i++) {
let p1 = trackPoints[i];
let p2 = trackPoints[i+1];
let dx = p2.x - p1.x;
let dz = p2.z - p1.z;
let len = sqrt(dx*dx + dz*dz);
let angle = atan2(dz, dx);
push();
translate((p1.x + p2.x)/2, 10, (p1.z + p2.z)/2);
rotateY(-angle);
fill(42, 45, 50);
noStroke();
box(len + 15, 16, trackWidth);
fill(255, 50, 0);
push(); translate(0, 8, trackWidth/2 - 2); box(len + 15, 3, 5); pop();
push(); translate(0, 8, -trackWidth/2 + 2); box(len + 15, 3, 5); pop();
fill(28, 30, 34);
push(); translate(0, 250, 0); box(35, 500, 35); pop();
pop();
}
let endPt = trackPoints[trackPoints.length - 1];
push();
translate(endPt.x, -60, endPt.z);
fill(51, 255, 51);
noStroke();
push(); translate(0, 20, -trackWidth/2); box(20, 100, 20); pop();
push(); translate(0, 20, trackWidth/2); box(20, 100, 20); pop();
push(); translate(0, -30, 0); box(20, 15, trackWidth + 20); pop();
pop();
}
// --- Destructible Block Engine ---
class DestructibleBlock {
constructor(x, y, z, w, h, d, blockColor) {
this.pos = createVector(x, y, z);
this.vel = createVector(0, 0, 0);
this.rot = createVector(0, random(TWO_PI), 0);
this.rotVel = createVector(0, 0, 0);
this.w = w; this.h = h; this.d = d;
this.color = blockColor;
this.active = true;
this.isHit = false;
this.life = 255;
}
update() {
if (!this.active) return;
if (this.isHit) {
this.vel.y += 0.5;
this.pos.add(this.vel);
this.rot.add(this.rotVel);
if (this.pos.y >= 540) {
this.pos.y = 540;
this.vel.y = -this.vel.y * 0.25;
this.vel.x *= 0.7;
this.vel.z *= 0.7;
this.rotVel.mult(0.6);
this.life -= 4.0;
if (this.life <= 0) this.active = false;
}
}
}
display() {
if (!this.active) return;
push();
translate(this.pos.x, this.pos.y, this.pos.z);
rotateX(this.rot.x);
rotateY(this.rot.y);
rotateZ(this.rot.z);
noStroke();
if (this.isHit) {
let c = color(red(this.color), green(this.color), blue(this.color), this.life);
fill(c);
} else {
fill(this.color);
}
let scaleFactor = map(this.life, 255, 0, 1.0, 0.1);
box(this.w * scaleFactor, this.h * scaleFactor, this.d * scaleFactor);
pop();
}
}
class TrackObstacle {
constructor(x, y, z, w, h, d, col) {
this.pos = createVector(x, y, z);
this.w = w; this.h = h; this.d = d;
this.color = col;
}
display() {
push();
translate(this.pos.x, this.pos.y, this.pos.z);
fill(this.color);
noStroke();
box(this.w, this.h, this.d);
translate(0, -this.h/2 - 1, 0);
fill(30);
box(this.w + 2, 2, this.d + 2);
pop();
}
}
class HazardBeam {
constructor(x, z, r) {
this.x = x;
this.z = z;
this.angle = random(TWO_PI);
this.speed = random(0.015, 0.035) * (random() > 0.5 ? 1 : -1);
this.length = r;
}
update() {
this.angle += this.speed;
}
display() {
push();
translate(this.x, -25, this.z);
fill(80);
noStroke();
push(); translate(0, 15, 0); cylinder(8, 30); pop();
rotateY(this.angle);
fill(255, 30, 0);
box(this.length, 10, 10);
fill(255, 230, 0);
push(); translate(this.length/2, 0, 0); box(4, 12, 12); pop();
push(); translate(-this.length/2, 0, 0); box(4, 12, 12); pop();
pop();
}
checkCollision(car) {
let d = dist(car.pos.x, car.pos.y, this.x, this.z);
if (d < this.length / 2 + 20) {
let bDirX = cos(this.angle);
let bDirZ = sin(this.angle);
let cx = car.pos.x - this.x;
let cz = car.pos.y - this.z;
let dot = cx * bDirX + cz * bDirZ;
let projX = bDirX * dot;
let projZ = bDirZ * dot;
let perpDist = dist(cx, cz, projX, projZ);
if (perpDist < 20 && abs(dot) < this.length / 2) {
return true;
}
}
return false;
}
}
function drawDestructibles() {
for (let i = destructibles.length - 1; i >= 0; i--) {
let b = destructibles[i];
b.update();
b.display();
if (!b.active) destructibles.splice(i, 1);
}
}
function drawSkidmarks() {
fill(10, 10, 12, 200); noStroke();
for(let s of skidmarks) {
push(); translate(s.x, 1, s.z); rotateX(HALF_PI); plane(12, 12); pop();
}
}
function drawSmoke() {
noStroke();
for (let i = smokeParticles.length - 1; i >= 0; i--) {
let s = smokeParticles[i]; s.x += s.vx; s.y += s.vy; s.z += s.vz; s.life -= 12; s.size += 0.8;
push(); translate(s.x, s.y, s.z);
fill(255, 80, 0, constrain(s.life, 0, 160));
rotateX(HALF_PI); plane(s.size, s.size); pop();
if (s.life <= 0) smokeParticles.splice(i, 1);
}
}
function drawSparks() {
noStroke();
for (let i = sparkParticles.length - 1; i >= 0; i--) {
let p = sparkParticles[i];
p.pos.add(p.vel);
p.vel.y += 0.4;
p.life -= 15;
push();
translate(p.pos.x, p.pos.y, p.pos.z);
fill(255, 140, 0, p.life);
box(p.size);
pop();
if (p.life <= 0 || p.pos.y >= 540) sparkParticles.splice(i, 1);
}
}
function spawnSparks(x, y, z, count) {
for (let i = 0; i < count; i++) {
sparkParticles.push({
pos: createVector(x, y, z),
vel: createVector(random(-8, 8), random(-12, -4), random(-8, 8)),
life: 255,
size: random(3, 8)
});
}
}
function checkPhysicsCollisions() {
if (car.isWrecked) return;
let status = getDistanceToTrack(car.pos.x, car.pos.y);
if (status.dist > trackWidth / 2) {
car.onTrack = false;
} else {
progressPct = round((status.index / (trackPoints.length - 1)) * 100);
updateProgressUI(progressPct);
}
for (let o of obstacles) {
let d = dist(car.pos.x, car.pos.y, o.pos.x, o.pos.z);
if (d < 38) {
car.takeDamage();
spawnSparks(o.pos.x, -15, o.pos.z, 25);
return;
}
}
for (let b of hazardBeams) {
if (b.checkCollision(car)) {
car.takeDamage();
spawnSparks(car.pos.x, -15, car.pos.y, 25);
return;
}
}
}
function getDistanceToTrack(carX, carZ) {
let minDist = Infinity;
let closestIndex = -1;
for (let i = 0; i < trackPoints.length - 1; i++) {
let p1 = trackPoints[i];
let p2 = trackPoints[i+1];
let d = distToSegment(carX, carZ, p1.x, p1.z, p2.x, p2.z);
if (d < minDist) {
minDist = d;
closestIndex = i;
}
}
return { dist: minDist, index: closestIndex };
}
function distToSegment(px, pz, x1, z1, x2, z2) {
let l2 = distSq(x1, z1, x2, z2);
if (l2 === 0) return dist(px, pz, x1, z1);
let t = ((px - x1) * (x2 - x1) + (pz - z1) * (z2 - z1)) / l2;
t = max(0, min(1, t));
return dist(px, pz, x1 + t * (x2 - x1), z1 + t * (z2 - z1));
}
function updateSpeedText(text) {
if (lastSpeedText !== text) {
let sd = document.getElementById('speed-display'); if (sd) sd.innerText = text; lastSpeedText = text;
}
}
function updateDriftUI() {
let dtText = "DRIFT: " + driftScore;
if (lastDriftText !== dtText) {
let df = document.getElementById('drift-display'); if (df) df.innerText = dtText; lastDriftText = dtText;
}
}
function updateProgressUI(pct) {
let boundPct = constrain(pct, 0, 100);
if (boundPct !== lastProgress) {
let pFill = document.getElementById('progress-fill'); if (pFill) pFill.style.width = boundPct + '%';
let pText = document.getElementById('progress-text'); if (pText) pText.innerText = boundPct + '%';
lastProgress = boundPct;
}
}
function distSq(x1, y1, x2, y2) { return (x1 - x2) ** 2 + (y1 - y2) ** 2; }
function keyPressed() {
if (gameState === 'PLAYING' && (key === 'r' || key === 'R')) {
initGameWorld();
}
}
function triggerGameOver() {
gameState = 'GAMEOVER';
// Convert Score to currency and write to memory
let earned = driftScore;
credits += earned;
savePlayerData();
let bustBox = document.getElementById('busted-desc');
if (bustBox) {
bustBox.innerHTML = `You collided with an obstacle or went off-road!<br><br><span style="color: #ffaa00; font-family: monospace; font-size: 16px; font-weight: bold;">+${earned} CR Transferred to Balance</span>`;
}
showScreen('busted-screen');
}
// --- Dynamic Customizable Car Class ---
class Car {
constructor(x, z) {
this.pos = createVector(x, z);
this.vel = createVector(0, 0);
this.angle = -HALF_PI;
this.steerAngle = 0;
// Custom Tuning Parameters via Selection Shop Configs
let config = CAR_PRESETS[activeCarType] || CAR_PRESETS.STINGER;
this.color = color(config.color[0], config.color[1], config.color[2]);
this.maxSpeed = config.maxSpeed;
this.accel = config.accel;
this.gripFactor = config.grip;
this.isPlayer = false;
this.posY = -15;
this.onTrack = true;
this.fallSpeed = 0;
this.isDrifting = false;
this.friction = 0.97;
this.isWrecked = false;
this.wreckTimer = 0;
}
breakPart(rx, ry, rz, pw, ph, pd, partColor) {
let cosA = cos(this.angle);
let sinA = sin(this.angle);
let wx = this.pos.x + rx * cosA - rz * sinA;
let wy = this.posY + ry;
let wz = this.pos.y + rx * sinA + rz * cosA;
let block = new DestructibleBlock(wx, wy, wz, pw, ph, pd, partColor);
block.isHit = true;
let explodeForce = p5.Vector.random3D().mult(random(4, 9));
block.vel = createVector(this.vel.x, -random(5, 11), this.vel.y).add(explodeForce);
block.rotVel = createVector(random(-0.2, 0.2), random(-0.2, 0.2), random(-0.2, 0.2));
if (destructibles.length < maxDebrisBlocks) {
destructibles.push(block);
}
}
takeDamage() {
if (this.isWrecked) return;
this.isWrecked = true;
this.wreckTimer = millis();
let chassisH = 18, chassisW = 45, chassisL = 90;
let wb = chassisL*0.32, tw = chassisW*0.53;
this.breakPart(15, -12, 0, 25, 7, 18, color(20));
this.breakPart(-10, -18, 0, chassisL*0.5, 14, chassisW*0.8, color(100, 180, 255, 150));
this.breakPart(wb, chassisH/2, -tw, 20, 20, 11, color(15));
this.breakPart(wb, chassisH/2, tw, 20, 20, 11, color(15));
this.breakPart(-wb, chassisH/2, -tw, 20, 20, 11, color(15));
this.breakPart(-wb, chassisH/2, tw, 20, 20, 11, color(15));
for (let cx = -1; cx <= 1; cx++) {
for (let cz = -1; cz <= 1; cz += 2) {
this.breakPart(cx * (chassisL/3), 0, cz * (chassisW/4), chassisL/3.2, chassisH, chassisW/2.2, this.color);
}
}
spawnSparks(this.pos.x, this.posY, this.pos.y, 40);
sfx.playCrash();
}
update(gas, steerInput) {
if (this.isWrecked) {
this.vel.mult(0.92);
this.pos.add(this.vel);
this.posY += this.fallSpeed;
if (!this.onTrack) this.fallSpeed += 0.8;
if (millis() - this.wreckTimer > 2000) {
triggerGameOver();
}
return;
}
if (!this.onTrack) {
this.posY += this.fallSpeed;
this.fallSpeed += 0.7;
if (this.posY > 150) {
this.takeDamage();
return;
}
}
this.steerAngle = lerp(this.steerAngle, steerInput * PI / 4.4, 0.22);
let speed = this.vel.mag();
let forward = createVector(cos(this.angle), sin(this.angle));
let isMovingForward = this.vel.dot(forward) >= 0;
this.vel.add(p5.Vector.mult(forward, gas * this.accel));
this.vel.mult(this.friction);
if (speed > 1.0) {
let turnEffect = steerInput * 0.055;
if (!isMovingForward) turnEffect *= -1;
turnEffect *= map(speed, 0, this.maxSpeed, 1.2, 0.5);
this.angle += turnEffect;
}
forward = createVector(cos(this.angle), sin(this.angle));
let desiredVel = p5.Vector.mult(forward, speed * (isMovingForward ? 1 : -1));
// Dynamic Drift Grip factor calculated through current active model config
let grip = (abs(steerInput) > 0 && speed > 13) ? (this.gripFactor * 0.25) : this.gripFactor;
this.vel.lerp(desiredVel, grip);
let slipAmount = p5.Vector.dist(this.vel, desiredVel);
this.isDrifting = (slipAmount > 2.5 && speed > 5);
if (this.isDrifting && this.onTrack) {
this.generateSkidmarks();
if (random() > 0.65) this.generateSmoke();
driftScore += 2;
updateDriftUI();
}
this.pos.add(this.vel);
this.vel.limit(this.maxSpeed);
}
generateSkidmarks() {
let forward = createVector(cos(this.angle), sin(this.angle)); let right = createVector(-sin(this.angle), cos(this.angle));
let rearCenter = p5.Vector.sub(this.pos, p5.Vector.mult(forward, 36));
let rl = p5.Vector.sub(rearCenter, p5.Vector.mult(right, 18)); let rr = p5.Vector.add(rearCenter, p5.Vector.mult(right, 18));
skidmarks.push({ x: rl.x, z: rl.y });
skidmarks.push({ x: rr.x, z: rr.y });
if (skidmarks.length > 70) skidmarks.splice(0, 2);
}
generateSmoke() {
let forward = createVector(cos(this.angle), sin(this.angle)); let rc = p5.Vector.sub(this.pos, p5.Vector.mult(forward, 40));
smokeParticles.push({
x: rc.x + random(-15, 15), y: this.posY + 8, z: rc.y + random(-15, 15),
vx: this.vel.x * 0.15 + random(-1, 1), vy: random(-2, -1), vz: this.vel.y * 0.15 + random(-1, 1),
life: 220, size: random(10, 25)
});
}
display() {
if (this.isWrecked) return;
push();
translate(this.pos.x, this.posY, this.pos.y);
rotateY(-this.angle);
noStroke();
specularMaterial(230); shininess(25);
let chassisH = 18, chassisW = 45, chassisL = 90;
fill(this.color);
box(chassisL, chassisH, chassisW);
fill(40);
push(); translate(chassisL/2 + 2, 4, 0); box(6, 10, chassisW + 4); pop();
push(); translate(15, -(chassisH/2 + 3), 0); fill(20); box(25, 7, 18); pop();
push(); translate(-8, -(chassisH/2 + 8), 0); fill(red(this.color)*0.4, green(this.color)*0.4, blue(this.color)*0.4); box(chassisL*0.42, 14, chassisW * 0.85); pop();
push(); translate(chassisL/2 + 1, -2, chassisW/3); fill(255, 230, 100); box(2, 4, 8); pop();
push(); translate(chassisL/2 + 1, -2, -chassisW/3); fill(255, 230, 100); box(2, 4, 8); pop();
push(); translate(-chassisL/2 - 1, -2, chassisW/3); fill(230, 30, 30); box(2, 4, 10); pop();
push(); translate(-chassisL/2 - 1, -2, -chassisW/3); fill(230, 30, 30); box(2, 4, 10); pop();
fill(25); specularMaterial(30); shininess(2);
let wb = chassisL*0.32, tw = chassisW*0.53;
push(); translate(wb, chassisH/2, -tw); rotateY(-this.steerAngle); rotateX(HALF_PI); cylinder(11, 10, 6, 1); pop();
push(); translate(wb, chassisH/2, tw); rotateY(-this.steerAngle); rotateX(HALF_PI); cylinder(11, 10, 6, 1); pop();
push(); translate(-wb, chassisH/2, -tw); rotateX(HALF_PI); cylinder(11, 10, 6, 1); pop();
push(); translate(-wb, chassisH/2, tw); rotateX(HALF_PI); cylinder(11, 10, 6, 1); pop();
pop();
}
}
function windowResized() { resizeCanvas(windowWidth, windowHeight); perspective(PI / 1.8, width / height, 10, 50000); }
function showScreen(id) {
document.querySelectorAll('.screen, #ui-layer').forEach(el => el.style.display = 'none');
let tgt = document.getElementById(id); if (tgt) tgt.style.display = (id === 'ui-layer') ? 'block' : 'flex';
}
function updateShopUI() {
let bal = document.getElementById('shop-credits-display');
if (bal) bal.innerText = credits;
for (let key in CAR_PRESETS) {
let carData = CAR_PRESETS[key];
let buyBtn = document.getElementById('btn-buy-' + key.toLowerCase());
if (!buyBtn) continue;
let container = document.getElementById('item-' + key.toLowerCase());
if (container) {
if (activeCarType === key) {
container.style.borderColor = "#ffaa00";
} else {
container.style.borderColor = "#444";
}
}
let labelCost = document.getElementById('cost-' + key.toLowerCase());
if (unlockedCars.includes(key)) {
if (labelCost) labelCost.style.display = 'none';
if (activeCarType === key) {
buyBtn.innerText = "EQUIPPED";
buyBtn.style.background = "#ffaa00";
buyBtn.style.color = "#000";
buyBtn.disabled = true;
} else {
buyBtn.innerText = "EQUIP";
buyBtn.style.background = "#00e5ff";
buyBtn.style.color = "#000";
buyBtn.disabled = false;
}
} else {
if (labelCost) labelCost.style.display = 'block';
buyBtn.innerText = "BUY";
if (credits >= carData.cost) {
buyBtn.style.background = "#ff5500";
buyBtn.style.color = "#fff";
buyBtn.disabled = false;
} else {
buyBtn.style.background = "#333";
buyBtn.style.color = "#777";
buyBtn.disabled = true;
}
}
}
}
function setupMenuInteractions() {
let bindClick = (id, fn) => { let el = document.getElementById(id); if (el) el.onclick = fn; };
bindClick('btn-start', () => { userStartAudio().then(() => sfx.start()); initGameWorld(); gameState = 'PLAYING'; showScreen('ui-layer'); });
bindClick('btn-credits', () => { showScreen('credits-screen'); });
bindClick('btn-back', () => { showScreen('home-screen'); });
bindClick('btn-restart', () => { initGameWorld(); gameState = 'PLAYING'; showScreen('ui-layer'); });
bindClick('btn-to-menu', () => { gameState = 'HOME'; showScreen('home-screen'); });
bindClick('btn-victory-restart', () => { initGameWorld(); gameState = 'PLAYING'; showScreen('ui-layer'); });
bindClick('btn-victory-to-menu', () => { gameState = 'HOME'; showScreen('home-screen'); });
// Shop Screen Controls
bindClick('btn-shop', () => {
userStartAudio().then(() => sfx.start());
updateShopUI();
showScreen('shop-screen');
});
bindClick('btn-shop-back', () => {
showScreen('home-screen');
});
// Shop purchase trigger mappings
for (let key in CAR_PRESETS) {
bindClick('btn-buy-' + key.toLowerCase(), () => {
let carPreset = CAR_PRESETS[key];
if (unlockedCars.includes(key)) {
activeCarType = key;
sfx.playUnlock();
} else {
if (credits >= carPreset.cost) {
credits -= carPreset.cost;
unlockedCars.push(key);
activeCarType = key;
sfx.playUnlock();
}
}
savePlayerData();
updateShopUI();
});
}
// Difficulty Channel Selection Logic
bindClick('btn-mode-easy', () => {
trackMode = 'EASY';
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById('btn-mode-easy').classList.add('active');
});
bindClick('btn-mode-hard', () => {
trackMode = 'HARD';
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById('btn-mode-hard').classList.add('active');
});
bindClick('btn-mode-impossible', () => {
trackMode = 'IMPOSSIBLE';
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById('btn-mode-impossible').classList.add('active');
});
}
// --- Synthesized Dynamic Audio Engine ---
class SoundManager {
constructor() {
this.started = false;
this.engineOsc = new p5.Oscillator('sawtooth'); this.engineOsc.amp(0); this.engineOsc.freq(60);
this.skidOsc = new p5.Oscillator('square'); this.skidOsc.amp(0); this.skidOsc.freq(800);
this.crashNoise = new p5.Noise('white');
this.crashEnv = new p5.Envelope(); this.crashEnv.setADSR(0.01, 0.1, 0.25, 0.35); this.crashEnv.setRange(0.65, 0);
this.crashNoise.amp(this.crashEnv);
this.musicOsc = new p5.Oscillator('sawtooth'); this.musicOsc.amp(0);
this.musicEnv = new p5.Envelope(); this.musicEnv.setADSR(0.06, 0.12, 0.0, 0.0); this.musicEnv.setRange(0.09, 0);
this.musicOsc.amp(this.musicEnv);
this.notes = [110.00, 130.81, 146.83, 164.81]; this.musicStep = 0;
}
start() {
if (!this.started) {
this.engineOsc.start(); this.skidOsc.start(); this.crashNoise.start(); this.musicOsc.start(); this.started = true;
}
}
update(gas, speed, isDrifting) {
if (!this.started) return;
let targetFreq = map(speed, 0, 35, 55, 230); let targetAmp = abs(gas) > 0 ? 0.16 : 0.06;
this.engineOsc.freq(targetFreq, 0.1); this.engineOsc.amp(targetAmp, 0.1);
if (isDrifting) { this.skidOsc.amp(0.07, 0.05); this.skidOsc.freq(random(650, 950)); } else { this.skidOsc.amp(0, 0.2); }
}
playCrash() { if (this.started) this.crashEnv.play(); }
playMusic() {
if (!this.started) return;
if (frameCount % 15 === 0) {
let f = this.notes[this.musicStep % this.notes.length];
this.musicOsc.freq(f); this.musicEnv.play(); this.musicStep++;
}
}
playUnlock() {
if (!this.started) return;
let clickOsc = new p5.Oscillator('sine');
let clickEnv = new p5.Envelope();
clickEnv.setADSR(0.01, 0.04, 0.1, 0.1);
clickEnv.setRange(0.15, 0);
clickOsc.amp(clickEnv);
clickOsc.start();
clickOsc.freq(880);
clickEnv.play();
setTimeout(() => {
clickOsc.freq(1320);
clickEnv.play();
setTimeout(() => clickOsc.stop(), 200);
}, 85);
}
muteAll() { if (!this.started) return; this.engineOsc.amp(0, 0.5); this.skidOsc.amp(0, 0.5); }
}
function setupMobileControls() {
const bindBtn = (id, key) => {
let el = document.getElementById(id); if (!el) return;
el.addEventListener('touchstart', (e) => { e.preventDefault(); touchInput[key] = true; }, { passive: false });
el.addEventListener('touchend', (e) => { e.preventDefault(); touchInput[key] = false; }, { passive: false });
el.addEventListener('mousedown', (e) => { e.preventDefault(); touchInput[key] = true; });
el.addEventListener('mouseup', (e) => { e.preventDefault(); touchInput[key] = false; });
el.addEventListener('mouseleave', (e) => { e.preventDefault(); touchInput[key] = false; });
};
bindBtn('btn-left', 'left'); bindBtn('btn-right', 'right'); bindBtn('btn-gas', 'up'); bindBtn('btn-brake', 'down');
}
```
--- index.html ---
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Destruction Physics Sandbox</title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.11/lib/p5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.11/lib/addons/p5.sound.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- HOME SCREEN -->
<div id="home-screen" class="screen main-menu">
<div class="menu-box">
<h1>DESTRUCTION PHYSICS</h1>
<h2 style="color: #ffaa00; margin-top: -10px; letter-spacing: 2px;">THE IMPOSSIBLE TRACK</h2>
<p>Stay on the elevated road. Dodge spinning hazards. One touch = Instant Disintegration.</p>
<!-- MODE SELECTOR -->
<div style="font-size: 12px; letter-spacing: 1.5px; color: #ffaa00; font-weight: bold; margin-bottom: 8px;">SELECT CHANNELS</div>
<div class="mode-selector">
<button id="btn-mode-easy" class="mode-btn">EASY</button>
<button id="btn-mode-hard" class="mode-btn active">HARD</button>
<button id="btn-mode-impossible" class="mode-btn">IMPOSSIBLE</button>
</div>
<button id="btn-start" class="menu-btn" style="background: #ff5500; color: white; font-weight:900;">START RUN</button>
<button id="btn-shop" class="menu-btn" style="background: #00e5ff; color: black; font-weight:bold;">GARAGE & SHOP</button>
<button id="btn-credits" class="menu-btn">CREDITS</button>
</div>
</div>
<!-- SHOP / GARAGE SCREEN -->
<div id="shop-screen" class="screen main-menu" style="display: none;">
<div class="menu-box shop-box" style="max-width: 550px; width: 90%;">
<h1>GARAGE & SHOP</h1>
<div style="font-size: 18px; color: #ffaa00; margin-bottom: 20px; font-weight: bold; font-family: monospace;">
CREDITS: <span id="shop-credits-display">0</span> CR
</div>
<div class="shop-grid">
<div class="shop-item" id="item-stinger">
<h3 style="color: #ff5500;">STINGER</h3>
<div class="car-spec">Top Speed: 35 mph<br>Drift Drift: Balanced<br>Paint: Industrial Orange</div>
<button class="menu-btn shop-action-btn" id="btn-buy-stinger">EQUIP</button>
</div>
<div class="shop-item" id="item-neon">
<h3 style="color: #00e5ff;">NEON GLIDE</h3>
<div class="car-spec">Top Speed: 38 mph<br>Drift Drift: High Slide<br>Paint: Electric Cyan</div>
<div class="cost-tag" id="cost-neon">1,200 CR</div>
<button class="menu-btn shop-action-btn" id="btn-buy-neon">BUY</button>
</div>
<div class="shop-item" id="item-phantasm">
<h3 style="color: #ff00ff;">PHANTASM</h3>
<div class="car-spec">Top Speed: 42 mph<br>Drift Drift: Wild Ghost<br>Paint: Plasma Pink</div>
<div class="cost-tag" id="cost-phantasm">2,500 CR</div>
<button class="menu-btn shop-action-btn" id="btn-buy-phantasm">BUY</button>
</div>
<div class="shop-item" id="item-apex">
<h3 style="color: #ffd700;">APEX GOLD</h3>
<div class="car-spec">Top Speed: 46 mph<br>Drift Drift: Pro Grip<br>Paint: Liquid Gold</div>
<div class="cost-tag" id="cost-apex">5,000 CR</div>
<button class="menu-btn shop-action-btn" id="btn-buy-apex">BUY</button>
</div>
</div>
<button id="btn-shop-back" class="menu-btn" style="margin-top: 20px; background: #333; color: white;">BACK</button>
</div>
</div>
<!-- CREDITS SCREEN -->
<div id="credits-screen" class="screen main-menu" style="display: none;">
<div class="menu-box">
<h1>CREDITS</h1>
<p style="font-size: 18px; margin-bottom: 20px;">Created by <span style="color: #ffaa00; font-weight: bold; letter-spacing: 1px;">CORBUN</span></p>
<p>Elevated Track Edition Powered by p5.js</p>
<p>Custom 3D Real-time Fragmentation Physics</p>
<button id="btn-back" class="menu-btn">BACK</button>
</div>
</div>
<!-- CRASH / GAME OVER SCREEN -->
<div id="busted-screen" class="screen main-menu" style="display: none;">
<div class="menu-box busted-box" style="border-color: #ff3c00;">
<h1 id="busted-title" style="color: #ff3c00;">WRECKED!</h1>
<p id="busted-desc">You collided with an obstacle or went off-road!</p>
<button id="btn-restart" class="menu-btn" style="background: #ff3c00; color: white;">TRY AGAIN</button>
<button id="btn-to-menu" class="menu-btn">MAIN MENU</button>
</div>
</div>
<!-- VICTORY SCREEN -->
<div id="victory-screen" class="screen main-menu" style="display: none;">
<div class="menu-box" style="border-color: #33ff33; box-shadow: 0 0 30px rgba(51, 255, 51, 0.4);">
<h1 style="color: #33ff33;">VICTORY!</h1>
<p>You conquered the Impossible Track without a single scratch!</p>
<h2 id="victory-stats" style="color: #ffaa00;">Drift Score: 0</h2>
<button id="btn-victory-restart" class="menu-btn" style="background: #33ff33; color: black; font-weight: bold;">PLAY AGAIN</button>
<button id="btn-victory-to-menu" class="menu-btn">MAIN MENU</button>
</div>
</div>
<!-- IN-GAME HUD -->
<div id="ui-layer" style="display: none;">
<div id="info">
<div id="speed-display">0 mph</div>
<div id="drift-display" style="color: #33ff33; font-size: 20px; font-weight: bold; margin-top:5px;">DRIFT: 0</div>
<div id="credits-hud-display" style="color: #ffaa00; font-size: 16px; font-weight: bold; margin-top:5px; font-family: monospace;">CREDITS: 0 CR</div>
<div class="progress-container">
<div style="font-size: 11px; letter-spacing: 1px; color: #ff5500;">TRACK COMPLETION</div>
<div id="progress-bar"><div id="progress-fill"></div></div>
<div id="progress-text" style="font-size: 12px; margin-top: 3px; font-weight: bold; font-family: monospace;">0%</div>
</div>
<div class="instructions">P1: WASD / Arrow Keys to Drive • [R] Restart Challenge</div>
</div>
<!-- MOBILE DRIVE CONTROLS -->
<div class="controls">
<div class="dpad">
<div class="btn" id="btn-left">◀</div>
<div class="btn" id="btn-right">▶</div>
</div>
<div class="dpad">
<div class="btn" id="btn-brake">▼</div>
<div class="btn" id="btn-gas">▲</div>
</div>
</div>
</div>
<script src="sketch.js"></script>
</body>
</html>
```
💬 Comments
Loading comments...