class Car {
constructor(x, z, carColor) {
this.pos = createVector(x, z);
this.vel = createVector(0, 0);
this.angle = 0;
this.steerAngle = 0;
this.color = carColor;
this.isCop = false;
this.copType = 'NONE';
this.isPlayer = false;
this.health = 100;
this.isDrifting = false;
this.mass = 1.0;
this.accel = 0.6;
this.maxSpeed = 35;
this.friction = 0.97;
this.turnSpeed = 0.06;
this.grip = 0.15;
this.driftGrip = 0.03;
}
takeDamage(amount) {
let dmgMultiplier = (this.isPlayer) ? (1.3 - (playerStats.armorTier * 0.3)) : 1;
this.health -= amount * dmgMultiplier;
updateHealthUI(this.health);
if (this.health <= 0 && gameState === 'PLAYING') {
triggerGameOver('TOTALED!', 'Your car took too much damage during the chase.');
}
}
update(gas, steerInput) {
this.steerAngle = lerp(this.steerAngle, steerInput * PI / 4, 0.2);
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 * this.turnSpeed;
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 currentTraction = this.grip;
if (abs(steerInput) > 0 && speed > 15) currentTraction = this.driftGrip;
this.vel.lerp(desiredVel, currentTraction);
let slipAmount = p5.Vector.dist(this.vel, desiredVel);
this.isDrifting = (slipAmount > 2.5 && speed > 5);
if (this.isDrifting) {
this.generateSkidmarks();
if (random() > 0.6 && smokeParticles.length < 20) this.generateSmoke();
if (this.isPlayer) {
cash += 2; // Earn drift points!
updateCashUI();
if (wantedStars === 0) triggerWanted(); // Instantly trigger cops on drift!
}
}
this.pos.add(this.vel);
this.vel.limit(this.maxSpeed);
this.checkObstacleCollisions();
}
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, 40));
let rl = p5.Vector.sub(rearCenter, p5.Vector.mult(right, 22));
let rr = p5.Vector.add(rearCenter, p5.Vector.mult(right, 22));
skidmarks.push({ x: rl.x, z: rl.y }); skidmarks.push({ x: rr.x, z: rr.y });
if (skidmarks.length > 30) 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, 45));
smokeParticles.push({
x: rc.x + random(-20, 20), y: -5, z: rc.y + random(-20, 20),
vx: this.vel.x * 0.2 + random(-1, 1), vy: random(-3, -1), vz: this.vel.y * 0.2 + random(-1, 1),
life: 255, size: random(10, 25)
});
}
checkObstacleCollisions() {
for (let obs of obstacles) {
if (distSq(this.pos.x, this.pos.y, obs.x, obs.z) < 1600) {
let bounceDir = createVector(this.pos.x - obs.x, this.pos.y - obs.z).normalize();
this.vel.add(bounceDir.mult(6));
this.vel.mult(0.5);
if (this.isPlayer) { this.takeDamage(2); sfx.playCrash(); }
}
}
}
display() {
push();
translate(this.pos.x, -15, this.pos.y);
rotateY(-this.angle);
noStroke();
let chassisH = 20, chassisW = 50, chassisL = 100;
if (this.isCop) {
if (this.copType === 'COP') fill(15);
else if (this.copType === 'SHERIFF') fill(139, 115, 85);
else if (this.copType === 'SWAT') { fill(25, 25, 50); chassisW = 60; chassisH = 35; }
else if (this.copType === 'MILITARY') { fill(75, 83, 32); chassisW = 65; chassisL = 110; chassisH = 40; }
else if (this.copType === 'SPECIAL_FORCES') fill(5);
} else {
fill(this.color);
}
box(chassisL, chassisH, chassisW);
push();
translate(-10, -(chassisH/2 + 9), 0);
if (this.isCop) {
if (this.copType === 'COP') fill(240);
else if (this.copType === 'SHERIFF') fill(210, 180, 140);
else fill(15);
} else {
fill(red(this.color)*0.8, green(this.color)*0.8, blue(this.color)*0.8);
}
box(chassisL*0.45, 18, chassisW * 0.88);
fill(20);
box(chassisL*0.47, 14, chassisW * 0.92);
pop();
if (this.isCop) {
push();
translate(-10, -(chassisH + 12), 0);
if (this.copType === 'MILITARY') {
fill(255, 100, 0);
if (millis() % 500 < 250) box(10, 8, 10);
} else {
if (millis() % 300 < 150) fill(255, 0, 0); else fill(0, 0, 255);
box(8, 6, 30);
}
pop();
}
fill(30);
let wb = chassisL*0.32, tw = chassisW*0.52;
push(); translate(wb, chassisH/2, -tw); rotateY(-this.steerAngle); rotateX(HALF_PI); cylinder(12, 10, 6, 1); pop();
push(); translate(wb, chassisH/2, tw); rotateY(-this.steerAngle); rotateX(HALF_PI); cylinder(12, 10, 6, 1); pop();
push(); translate(-wb, chassisH/2, -tw); rotateX(HALF_PI); cylinder(12, 10, 6, 1); pop();
push(); translate(-wb, chassisH/2, tw); rotateX(HALF_PI); cylinder(12, 10, 6, 1); pop();
pop();
}
}