'use strict'; function drawCars() { for(const v of vehicles) v.draw(); } function updateCars() { // spawn in more vehicles const playerIsSlow = titleScreenMode || playerVehicle.velocity.z < 20; const trafficPosOffset = playerIsSlow? 0 : 16e4; // check in front/behind const trafficLevel = (playerVehicle.pos.z+trafficPosOffset)/checkpointDistance; const trafficLevelInfo = getLevelInfo(trafficLevel); const trafficDensity = trafficLevelInfo.trafficDensity; const maxVehicleCount = 10*trafficDensity; if (trafficDensity) if (vehicles.length!o.destroyed); } function spawnVehicle(z) { if (disableAiVehicles) return; const v = new Vehicle(z); vehicles.push(v); v.update(); } /////////////////////////////////////////////////////////////////////////////// class Vehicle { constructor(z, color) { this.pos = vec3(0,0,z); this.color = color; this.isBraking = this.drawTurn = this.drawPitch = this.wheelTurn = 0; this.collisionSize = vec3(230,200,380); this.velocity = vec3(); if (!this.color) { this.color = // random color randInt(9) ? hsl(rand(), rand(.5,.9),.5) : randInt(2) ? WHITE : hsl(0,0,.1); // not player if no color //if (!isPlayer) { if (this.isTruck = randInt(2)) // random trucks { this.collisionSize.z = 450; this.truckColor = hsl(rand(),rand(.5,1),rand(.2,1)); } // do not pick same lane as player if behind const levelInfo = getLevelInfo(this.pos.z/checkpointDistance); this.lane = randInt(levelInfo.laneCount); if (!titleScreenMode && z < playerVehicle.pos.z) this.lane = playerVehicle.pos.x > 0 ? 0 : levelInfo.laneCount-1; this.laneOffset = this.getLaneOffset(); this.velocity.z = this.getTargetSpeed(); } } } getTargetSpeed() { const levelInfo = getLevelInfo(this.pos.z/checkpointDistance); const lane = levelInfo.laneCount - 1 - this.lane; // flip side return max(120,120 + lane*20); // faster on left } getLaneOffset() { const levelInfo = getLevelInfo(this.pos.z/checkpointDistance); const o = (levelInfo.laneCount-1)*laneWidth/2; return this.lane*laneWidth - o; } update() { ASSERT(this != playerVehicle); // update ai vehicles const targetSpeed = this.getTargetSpeed(); const accel = this.isBraking ? (--this.isBraking, -1) : this.velocity.z < targetSpeed ? .5 : this.velocity.z > targetSpeed+10 ? -.5 : 0; const trackInfo = new TrackSegmentInfo(this.pos.z); const trackInfo2 = new TrackSegmentInfo(this.pos.z+trackSegmentLength); const level = this.pos.z/checkpointDistance | 0; const levelInfo = getLevelInfo(level); { // update lanes this.lane = min(this.lane, levelInfo.laneCount-1); //if (rand() < .01 && this.pos.z > playerVehicle.pos.z) // this.lane = randInt(levelInfo.laneCount); // move into lane const targetLaneOffset = this.getLaneOffset(); this.laneOffset = lerp(.01, this.laneOffset, targetLaneOffset); const lanePos = this.laneOffset; this.pos.x = lanePos; } // update physics this.pos.z += this.velocity.z = max(0, this.velocity.z+accel); // slow down if too close to other vehicles const x = this.laneOffset; for(const v of vehicles) { // slow down if behind if (v != this && v != playerVehicle) if (this.pos.z < v.pos.z + 500 && this.pos.z > v.pos.z - 2e3) if (abs(x-v.laneOffset) < 500) // lane space { if (this.pos.z >= v.pos.z) this.destroyed = 1; // get rid of overlaps this.velocity.z = min(this.velocity.z, v.velocity.z++); // clamp velocity & push this.isBraking = 30; break; } } // move ai vehicles this.pos.x = trackInfo.pos.x + x; this.pos.y = trackInfo.offset.y; // get projected track angle const delta = trackInfo2.pos.subtract(trackInfo.pos); this.drawTurn = Math.atan2(delta.x, delta.z); this.wheelTurn = this.drawTurn / 2; this.drawPitch = trackInfo.pitch; // remove in front or behind const playerDelta = this.pos.z - playerVehicle.pos.z; this.destroyed |= playerDelta > 7e4 || playerDelta < -2e3; } draw() { const trackInfo = new TrackSegmentInfo(this.pos.z); const vehicleHeight = 75; const p = this.pos.copy(); p.y += vehicleHeight; p.z = p.z - cameraOffset; if (p.z < 0 && !freeCamMode) { // causes glitches if rendered return; // behind camera } /*{ // test cube //p.y = trackInfo.offset.y; const heading = this.drawTurn+PI/2; const trackPitch = trackInfo.pitch; const m2 = buildMatrix(p.add(vec3(0,-vehicleHeight,0)), vec3(trackPitch,0,0)); const m1 = m2.multiply(buildMatrix(0, vec3(0,heading,0), 0)); cubeMesh.render(m1.multiply(buildMatrix(0, 0, vec3(50,20,2e3))), this.color); // return }*/ // car const heading = this.drawTurn; const trackPitch = trackInfo.pitch; const carPitch = this.drawPitch; const mHeading = buildMatrix(0, vec3(0,heading), 0); const m1 = buildMatrix(p, vec3(carPitch,0)).multiply(mHeading); const mcar = m1.multiply(buildMatrix(0, 0, vec3(450,this.isTruck?700:500,450))); { // shadow glSetDepthTest(this != playerVehicle,0); // no depth test for player shadow glPolygonOffset(60); const lightOffset = vec3(0,0,-60).rotateY(worldHeading); const shadowColor = rgb(0,0,0,.5); const shadowPosBase = vec3(p.x,trackInfo.pos.y,p.z).addSelf(lightOffset); const shadowSize = vec3(-720,200,600); // why x negative? const m2 = buildMatrix(shadowPosBase, vec3(trackPitch,0)).multiply(mHeading); const mshadow = m2.multiply(buildMatrix(0, 0, shadowSize)); shadowMesh.renderTile(mshadow, shadowColor, spriteList.carShadow.spriteTile); glPolygonOffset(); glSetDepthTest(); } carMesh.render(mcar, this.color); //cubeMesh.render(m1.multiply(buildMatrix(0, 0, this.collisionSize)), BLACK); // collis let bumperY = 130, bumperZ = -440; if (this.isTruck) { bumperY = 50; bumperZ = -560; const truckO = vec3(0,290,-250); const truckColor = this.truckColor; const truckSize = vec3(240,truckO.y,300); glPolygonOffset(20); cubeMesh.render(m1.multiply(buildMatrix(truckO, 0, truckSize)), truckColor); } glPolygonOffset(); // turn it off! if (optimizedCulling) { const distanceFromPlayer = this.pos.z - playerVehicle.pos.z; if (distanceFromPlayer > 4e4) return; // cull too far } // wheels const wheelRadius = 110; const wheelSpinScale = 400; const wheelSize = vec3(50,wheelRadius,wheelRadius); const wheelM1 = buildMatrix(0,vec3(this.pos.z/wheelSpinScale,this.wheelTurn),wheelSize); const wheelM2 = buildMatrix(0,vec3(this.pos.z/wheelSpinScale,0),wheelSize); const wheelColor = hsl(0,0,.2); const wheelOffset1 = vec3(240,25,220); const wheelOffset2 = vec3(240,25,-300); for (let i=4;i--;) { const wo = i<2? wheelOffset1 : wheelOffset2; glPolygonOffset(this.isTruck && i>1 && 20); const o = vec3(i%2?wo.x:-wo.x, wo.y, i<2? wo.z : wo.z); carWheel.render(m1.multiply(buildMatrix(o)).multiply(i<2 ? wheelM1 :wheelM2), wheelColor); } // decals glPolygonOffset(40); // bumpers cubeMesh.render(m1.multiply(buildMatrix(vec3(0,bumperY,bumperZ), 0, vec3(140,50,20))), hsl(0,0,.1)); // break lights const isBraking = this.isBraking; for(let i=2;i--;) { const color = isBraking ? hsl(0,1,.5) : hsl(0,1,.2); glEnableLighting = !isBraking; // make it full bright when braking cubeMesh.render(m1.multiply(buildMatrix(vec3((i?1:-1)*180,bumperY-25,bumperZ-10), 0, vec3(40,25,5))), color); glEnableLighting = 1; cubeMesh.render(m1.multiply(buildMatrix(vec3((i?1:-1)*180,bumperY+25,bumperZ-10), 0, vec3(40,25,5))), WHITE); } if (this == playerVehicle) { // only player needs front bumper cubeMesh.render(m1.multiply(buildMatrix(vec3(0,10,440), 0, vec3(240,30,30))), hsl(0,0,.5)); // license plate quadMesh.renderTile(m1.multiply(buildMatrix(vec3(0,bumperY-80,bumperZ-20), vec3(0,PI,0), vec3(80,25,1))),WHITE, spriteList.carLicense.spriteTile); // top number const m3 = buildMatrix(0,vec3(0,PI)); // flip for some reason quadMesh.renderTile(m1.multiply(buildMatrix(vec3(0,230,-200), vec3(PI/2-.2,0,0), vec3(140)).multiply(m3)),WHITE, spriteList.carNumber.spriteTile); } glPolygonOffset(); } } /////////////////////////////////////////////////////////////////////////////// class PlayerVehicle extends Vehicle { constructor(z, color) { super(z, color, 1); this.playerTurn = this.bumpTime = this.onGround = this.engineTime = 0; this.hitTimer = new Timer; } draw() { titleScreenMode || super.draw(); } update() { if (titleScreenMode) { this.pos.z += this.velocity.z = 20; return; } const playHitSound=()=> { if (!this.hitTimer.active()) { sound_hit.play(percent(this.velocity.z, 0, 50)); this.hitTimer.set(.5); } } const hitBump=(amount = .98)=> { this.velocity.z *= amount; if (this.bumpTime < 0) { sound_bump.play(percent(this.velocity.z, 0, 50)); this.bumpTime = 500*rand(1,1.5); this.velocity.y += min(50, this.velocity.z)*rand(.1,.2); } } this.bumpTime -= this.velocity.z; if (!freeRide && checkpointSoundCount > 0 && !checkpointSoundTimer.active()) { sound_checkpoint.play(); checkpointSoundTimer.set(.26); checkpointSoundCount--; } const playerDistance = playerVehicle.pos.z; if (!gameOverTimer.isSet()) if (playerDistance > nextCheckpointDistance) { // checkpoint ++playerLevel; nextCheckpointDistance += checkpointDistance; checkpointTimeLeft += extraCheckpointTime; if (enhancedMode) checkpointTimeLeft = min(60,checkpointTimeLeft); if (playerLevel >= levelGoal && !gameOverTimer.isSet()) { // end of game playerWin = 1; sound_win.play(); gameOverTimer.set(); if (!(debug && debugSkipped)) if (!freeRide) { bestDistance = 0; // reset best distance if (raceTime < bestTime || !bestTime) { // new fastest time bestTime = raceTime; playerNewRecord = 1; } writeSaveData(); } } else { //speak('CHECKPOINT'); checkpointSoundCount = 3; } } // check for collisions if (!testDrive) for(const v of vehicles) { const d = this.pos.subtract(v.pos); const s = this.collisionSize.add(v.collisionSize); if (v != this && abs(d.x) < s.x && abs(d.z) < s.z) { // collision const oldV = this.velocity.z; this.velocity.z = v.velocity.z/2; //console.log(v.velocity.z, oldV*.9); v.velocity.z = max(v.velocity.z, oldV*.9); // push other car this.velocity.x = 99*sign(d.x); // push away from car playHitSound(); } } // get player input let playerInputTurn = keyIsDown('ArrowRight') - keyIsDown('ArrowLeft'); let playerInputGas = keyIsDown('ArrowUp'); let playerInputBrake = keyIsDown('Space') || keyIsDown('ArrowDown'); if (isUsingGamepad) { playerInputTurn = gamepadStick(0).x; playerInputGas = gamepadIsDown(0) || gamepadIsDown(7); playerInputBrake = gamepadIsDown(1) || gamepadIsDown(2) || gamepadIsDown(3) || gamepadIsDown(6); const analogGas = gamepadGetValue(7); if (analogGas) playerInputGas = analogGas; const analogBrake = gamepadGetValue(6); if (analogBrake) playerInputBrake = analogBrake; } if (playerInputGas) mouseControl = 0; if (debug && (mouseWasPressed(0) || mouseWasPressed(2) || isUsingGamepad && gamepadWasPressed(0))) testDrive = 0; if (mouseControl || mouseIsDown(0)) { mouseControl = 1; playerInputTurn = clamp(5*(mousePos.x-.5),-1,1); playerInputGas = mouseIsDown(0); playerInputBrake = mouseIsDown(2); if (isTouchDevice && mouseIsDown(0)) { const touch = 1.8 - 2*mousePos.y; playerInputGas = percent(touch, .1, .2); playerInputBrake = touch < 0; playerInputTurn = clamp(3*(mousePos.x-.5),-1,1); } } if (freeCamMode) playerInputGas = playerInputTurn = playerInputBrake = 0; if (testDrive) playerInputGas = 1, playerInputTurn=0; if (gameOverTimer.isSet()) playerInputGas = playerInputTurn = playerInputBrake = 0; this.isBraking = playerInputBrake; const sound_velocity = max(40+playerInputGas*50,this.velocity.z); this.engineTime += sound_velocity*sound_velocity/5e4; if (this.engineTime > 1) { if (--this.engineTime > 1) this.engineTime = 0; const f = sound_velocity; sound_engine.play(.1,f*f/4e3+rand(.1)); } const playerTrackInfo = new TrackSegmentInfo(this.pos.z); const playerTrackSegment = playerTrackInfo.segmentIndex; // gravity const gravity = -3; // gravity to apply in y axis this.velocity.y += gravity; // player settings const forwardDamping = .998; // dampen player z speed const lateralDamping = .5; // dampen player x speed const playerAccel = 1; // player acceleration const playerBrake = 2; // player acceleration when braking const playerMaxSpeed = 200; // limit max player speed const speedPercent = this.velocity.z/playerMaxSpeed; const centrifugal = .5; // update physics const velocityAdjusted = this.velocity.copy(); const trackHeadingScale = 20; const trackHeading = Math.atan2(trackHeadingScale*playerTrackInfo.offset.x, trackSegmentLength); const trackScaling = 1 / (1 + (this.pos.x/(2*laneWidth)) * Math.tan(-trackHeading)); velocityAdjusted.z *= trackScaling; this.pos.addSelf(velocityAdjusted); // clamp player x position const maxPlayerX = playerTrackInfo.width + 500; this.pos.x = clamp(this.pos.x, -maxPlayerX, maxPlayerX); // check if on ground const wasOnGround = this.onGround; this.onGround = this.pos.y < playerTrackInfo.offset.y; if (this.onGround) { this.pos.y = playerTrackInfo.offset.y; const trackPitch = playerTrackInfo.pitch; this.drawPitch = lerp(.2,this.drawPitch, trackPitch); // bounce off track const trackNormal = vec3(0, 1, 0).rotateX(trackPitch); const elasticity = 1.2; const normalDotVel = this.velocity.dot(trackNormal); const reflectVelocity = trackNormal.scale(-elasticity * normalDotVel); if (!gameOverTimer.isSet()) // dont roll in game over this.velocity.addSelf(reflectVelocity); if (!wasOnGround) { const p = percent(reflectVelocity.length(), 20, 80); sound_bump.play(p*2,.5); } const trackSegment = track[playerTrackSegment]; if (trackSegment && !trackSegment.sideStreet) // side streets are not offroad if (abs(this.pos.x) > playerTrackInfo.width - this.collisionSize.x && !testDrive) hitBump(); // offroad // update velocity if (playerInputBrake) this.velocity.z -= playerBrake*playerInputBrake; else if (playerInputGas) { // extra boost at low speeds //const lowSpeedPercent = this.velocity.z**2/1e4; const lowSpeedPercent = percent(this.velocity.z, 150, 0)**2; const accel = playerInputGas*playerAccel*lerp(speedPercent, 1, .5) * lerp(lowSpeedPercent, 1, 3); //console.log(lerp(lowSpeedPercent, 1, 9)) // apply acceleration in angle of road //const accelVec = vec3(0,0,accel).rotateX(trackSegment.pitch); //this.velocity.addSelf(accelVec); this.velocity.z += accel; } else if (this.velocity.z < 30) this.velocity.z *= .9; // slow to stop // dampen z velocity & clamp this.velocity.z = max(0, this.velocity.z*forwardDamping); this.velocity.x *= lateralDamping; } else { // in air this.drawPitch *= .99; // level out pitch this.onGround = 0; } { // turning let desiredPlayerTurn = startCountdown ? 0 : playerInputTurn; if (testDrive) { desiredPlayerTurn = clamp(-this.pos.x/2e3, -1, 1); this.pos.x = clamp(this.pos.x, -playerTrackInfo.width, playerTrackInfo.width); } // scale desired turn input desiredPlayerTurn *= .4; const playerMaxTurnStart = 50; // fade on turning visual const turnVisualRamp = clamp(this.velocity.z/playerMaxTurnStart,0,.1); this.wheelTurn = lerp(.1, this.wheelTurn, 1.3*desiredPlayerTurn); this.playerTurn = lerp(.05, this.playerTurn, desiredPlayerTurn); this.drawTurn = lerp(turnVisualRamp, this.drawTurn, this.playerTurn); // centripetal force const centripetalForce = -velocityAdjusted.z * playerTrackInfo.offset.x * centrifugal; this.pos.x += centripetalForce // apply turn velocity and slip const physicsTurn = this.onGround ? this.playerTurn : 0; const maxStaticFriction = 30; const slip = maxStaticFriction/max(maxStaticFriction,abs(centripetalForce)); const turnStrength = .8; const turnForce = turnStrength * physicsTurn * this.velocity.z; this.velocity.x += turnForce*slip; } if (playerWin) this.drawTurn = lerp(gameOverTimer.get(), this.drawTurn, -1); if (startCountdown) this.velocity.z = 0; // wait to start if (gameOverTimer.isSet()) this.velocity = this.velocity.scale(.95); if (!testDrive) { // check for collisions const collisionCheckDistance = 20; // segments to check for(let i = -collisionCheckDistance; i < collisionCheckDistance; ++i) { const segmentIndex = playerTrackSegment+i; const trackSegment = track[segmentIndex]; if (!trackSegment) continue; // collidable objects for(const trackObject of trackSegment.trackObjects) { if (!trackObject.collideSize) continue; // check for overlap const pos = trackSegment.offset.add(trackObject.offset); const dp = this.pos.subtract(pos); const csx = this.collisionSize.x+trackObject.collideSize; if (abs(dp.z) > 430 || abs(dp.x) > csx) continue; if (trackObject.sprite.isBump) { trackObject.collideSize = 0; // prevent colliding again hitBump(.8); // hit a bump } else if (trackObject.sprite.isSlow) { trackObject.collideSize = 0; // prevent colliding again sound_bump.play(percent(this.velocity.z, 0, 50)*3,.2); // just slow down the player this.velocity.z *= .85; } else { // push player away const onSideOfTrack = abs(pos.x)+csx+200 > playerTrackInfo.width; const pushDirection = onSideOfTrack ? -pos.x : // push towards center dp.x; // push away from object this.velocity.x = 99*sign(pushDirection); this.velocity.z *= .7; playHitSound(); } } } } } }