function draw() {
if (gameState === 'ADMIN_LOGIN' || gameState === 'ADMIN_PANEL') {
background(15, 0, 15); return;
}
// --- CUTSCENES ---
if (gameState === 'CUSTOMIZER') {
background(10); ambientLight(60); spotLight(255, 255, 255, 0, -400, 0, 0, 1, 0, PI/3, 50);
let t = millis(); camera(cos(t * 0.001) * 200, -100, sin(t * 0.001) * 200, 0, -20, 0, 0, 1, 0);
push(); fill(20); noStroke(); rotateX(HALF_PI); plane(2000, 2000); pop();
if (previewCar) previewCar.display();
return;
}
if (gameState === 'CUTSCENE_BUY_CAR') {
background(5); ambientLight(40); spotLight(255, 255, 255, 0, -400, 0, 0, 1, 0, PI/3, 50);
let t = millis() - cutsceneTimer;
camera(cos(t * 0.0015) * 250, -120, sin(t * 0.0015) * 250, 0, -20, 0, 0, 1, 0);
push(); fill(20); noStroke(); rotateX(HALF_PI); plane(2000, 2000); pop();
cutsceneData.car.display();
if (t > 4000) skipCutscene();
return;
}
// Game Weather Rendering
if (gameMode === 'COPNADO') { background(30, 40, 50); if (random() > 0.98) background(200); } // Lightning flashes
else background(135, 206, 235); // Normal sky
if (gameState === 'PLAYING') {
// P1 Controls
let gas1 = 0, steer1 = 0;
if (keyIsDown(87) || touchInput.up) gas1 = 1;
if (keyIsDown(83) || touchInput.down) gas1 = -1;
if (keyIsDown(65) || touchInput.left) steer1 = -1;
if (keyIsDown(68) || touchInput.right) steer1 = 1;
if (gameMode !== 'COOP') {
if (keyIsDown(UP_ARROW)) gas1 = 1; if (keyIsDown(DOWN_ARROW)) gas1 = -1;
if (keyIsDown(LEFT_ARROW)) steer1 = -1; if (keyIsDown(RIGHT_ARROW)) steer1 = 1;
}
car.update(gas1, steer1);
// P2 Controls
if (gameMode === 'COOP' && car2) {
let gas2 = 0, steer2 = 0;
if (keyIsDown(UP_ARROW)) gas2 = 1; if (keyIsDown(DOWN_ARROW)) gas2 = -1;
if (keyIsDown(LEFT_ARROW)) steer2 = -1; if (keyIsDown(RIGHT_ARROW)) steer2 = 1;
car2.update(gas2, steer2);
}
// Dynamic Camera tracking one or both players
let centerPos = car.pos.copy();
let maxSpd = car.vel.mag();
let baseCamDist = 250;
if (gameMode === 'COOP' && car2) {
centerPos.add(car2.pos).mult(0.5);
baseCamDist = max(250, dist(car.pos.x, car.pos.y, car2.pos.x, car2.pos.y) + 150);
maxSpd = max(maxSpd, car2.vel.mag());
}
let forward = createVector(cos(car.angle), sin(car.angle));
let desiredCamPos = p5.Vector.sub(centerPos, p5.Vector.mult(forward, baseCamDist));
let speedHeight = map(maxSpd, 0, 50, -180, -280);
if (gameMode === 'COOP') speedHeight -= baseCamDist * 0.3;
camPos.x = lerp(camPos.x, desiredCamPos.x, 0.08);
camPos.y = lerp(camPos.y, desiredCamPos.y, 0.08);
camera(camPos.x, speedHeight, camPos.y, centerPos.x, 0, centerPos.y, 0, 1, 0);
updateSpeedText(round(car.vel.mag() * 3) + " mph");
if (wantedStars > 0 || gameMode === 'COPNADO') processWantedEscalation();
sfx.update(gas1, car.vel.mag(), car.isDrifting);
sfx.playMusic();
// Infinite map obstacles
for (let obs of obstacles) {
if (distSq(obs.x, obs.z, centerPos.x, centerPos.y) > 16000000) {
let ang = random(TWO_PI); let r = random(2000, 3500);
obs.x = centerPos.x + cos(ang)*r; obs.z = centerPos.y + sin(ang)*r;
}
}
// Copnado Events
if (gameMode === 'COPNADO' && tornado) {
tornado.update();
if (distSq(car.pos.x, car.pos.y, tornado.pos.x, tornado.pos.y) < 40000) {
triggerGameOver('INTERCEPTED!', 'You successfully gathered data from the Copnado!');
document.querySelector('.busted-box h1').style.color = '#33ff33';
document.querySelector('.busted-box').style.borderColor = '#33ff33';
}
}
} else if (gameState === 'ON_FOOT') {
car.update(0, 0); updatePlayerFPS();
let dSq = distSq(playerPos.x, playerPos.z, car.pos.x, car.pos.y);
if (dSq < 22500) updateSpeedText("Press E to Enter Car");
else updateSpeedText("ON FOOT");
sfx.update(0, 0, false); sfx.playMusic();
} else if (gameState === 'CUTSCENE_BUSTED') {
let t = millis() - cutsceneTimer;
let targetX = bustedOnFoot ? playerPos.x : car.pos.x;
let targetZ = bustedOnFoot ? playerPos.z : car.pos.y;
camera(targetX + cos(t * 0.001) * 300, -150, targetZ + sin(t * 0.001) * 300, targetX, 0, targetZ, 0, 1, 0);
car.vel.mult(0.85); car.update(0, 0);
if(car2) { car2.vel.mult(0.85); car2.update(0,0); }
sfx.muteAll();
if (car.health <= 0 && !bustedOnFoot && random() > 0.3) {
let forward = createVector(cos(car.angle), sin(car.angle));
let enginePos = p5.Vector.add(car.pos, p5.Vector.mult(forward, 45));
smokeParticles.push({
x: enginePos.x + random(-15, 15), y: -10, z: enginePos.y + random(-15, 15),
vx: random(-0.5, 0.5), vy: random(-4, -1), vz: random(-0.5, 0.5),
life: 255, size: random(15, 35), isBlack: true
});
}
if (t > 7000) skipCutscene();
} else {
car.update(0, 0);
camera(car.pos.x + cos(millis() * 0.0003) * 600, -250, car.pos.y + sin(millis() * 0.0003) * 600, car.pos.x, 0, car.pos.y, 0, 1, 0);
sfx.muteAll();
}
// Lighting Configuration
if (gameState === 'CUTSCENE_BUSTED' && gameMode !== 'COPNADO') {
if (millis() % 400 < 200) ambientLight(255, 50, 50); else ambientLight(50, 50, 255);
directionalLight(200, 200, 200, -1, 1, -1);
} else {
ambientLight(120);
directionalLight(255, 255, 255, 0.5, 1, -0.5);
pointLight(255, 255, 255, car.pos.x, -200, car.pos.y);
}
drawGround(); drawSkidmarks(); drawObstacles(); drawSmoke();
if (gameMode === 'COPNADO' && tornado) tornado.display();
if (gameState === 'CUTSCENE_BUSTED' || gameState === 'GAMEOVER') {
for (let npc of npcs) { npc.update(0, 0); npc.display(); }
for (let c of cops) { c.update(0, 0); c.display(); }
} else {
updateNPCs(); updateCops();
}
if (gameState === 'PLAYING' || gameState === 'ON_FOOT') checkCollisions();
car.display();
if (car2) car2.display();
}