Files
my_site/vue/public/race/game.js
yoyuzh d669738967 init
2026-02-27 14:29:05 +08:00

433 lines
14 KiB
JavaScript

'use strict';
// debug settings
let testLevel;
let quickStart;
let disableAiVehicles;
let testDrive;
let freeCamMode;
let testLevelInfo;
let testQuick;
const js13kBuild = 1; // fixes for legacy code made during js13k
///////////////////////////////////////////////////
// settings
const pixelate = 0;
const canvasFixedSize = 0;
const frameRate = 60;
const timeDelta = 1/frameRate;
const pixelateScale = 3;
const clampAspectRatios = enhancedMode;
const optimizedCulling = 1;
const random = new Random;
let autoPause = enhancedMode;
let autoFullscreen = 0;
// setup
const laneWidth = 1400; // how wide is track
const trackSegmentLength = 100; // length of each segment
const drawDistance = 1e3; // how many track segments to draw
const cameraPlayerOffset = vec3(0,680,1050);
const checkpointTrackSegments = testQuick?1e3:4500;
const checkpointDistance = checkpointTrackSegments*trackSegmentLength;
const startCheckpointTime = 45;
const extraCheckpointTime = 40;
const levelLerpRange = .1;
const levelGoal = 10;
const playerStartZ = 2e3;
const turnWorldScale = 2e4;
const testStartZ = testLevel ? testLevel*checkpointDistance-1e3 : quickStart&&!testLevelInfo?5e3:0;
let mainCanvasSize;// = pixelate ? vec3(640, 420) : vec3(1280, 720);
let mainCanvas, mainContext;
let time, frame, frameTimeLastMS, averageFPS, frameTimeBufferMS, paused;
let checkpointTimeLeft, startCountdown, startCountdownTimer, gameOverTimer, nextCheckpointDistance;
let raceTime, playerLevel, playerWin, playerNewDistanceRecord, playerNewRecord;
let checkpointSoundCount, checkpointSoundTimer, vehicleSpawnTimer;
let titleScreenMode = 1, titleModeStartCount = 0;
let trackSeed = 1331;
///////////////////////////////
// game variables
let cameraPos, cameraRot, cameraOffset;
let worldHeading, mouseControl;
let track, vehicles, playerVehicle;
let freeRide;
///////////////////////////////
function gameInit()
{
if (enhancedMode)
{
console.log(`Dr1v3n Wild by Frank Force`);
console.log(`www.frankforce.com 🚗🌴`);
}
if (quickStart || testLevel)
titleScreenMode = 0;
debug && debugInit();
glInit();
document.body.appendChild(mainCanvas = document.createElement('canvas'));
mainContext = mainCanvas.getContext('2d');
const styleCanvas = 'position:absolute;' + // position
(clampAspectRatios?'top:50%;left:50%;transform:translate(-50%,-50%);':'') + // center
(pixelate?' image-rendering: pixelated':''); // pixelated
glCanvas.style.cssText = mainCanvas.style.cssText = styleCanvas;
if (!clampAspectRatios)
document.body.style.margin = '0px';
drawInit();
inputInit()
initGenerative();
initSprites();
initLevelInfos();
gameStart();
gameUpdate();
}
function gameStart()
{
time = frame = frameTimeLastMS = averageFPS = frameTimeBufferMS =
cameraOffset = checkpointTimeLeft = raceTime = playerLevel = playerWin = playerNewDistanceRecord = playerNewRecord = freeRide = checkpointSoundCount = 0;
startCountdown = quickStart || testLevel ? 0 : 4;
worldHeading = titleScreenMode ? rand(7) : .8;
checkpointTimeLeft = startCheckpointTime;
nextCheckpointDistance = checkpointDistance;
startCountdownTimer = new Timer;
gameOverTimer = new Timer;
vehicleSpawnTimer = new Timer;
checkpointSoundTimer = new Timer;
cameraPos = vec3();
cameraRot = vec3();
vehicles = [];
buildTrack();
vehicles.push(playerVehicle = new PlayerVehicle(testStartZ?testStartZ:playerStartZ, hsl(0,.8,.5)));
if (titleScreenMode)
{
const level = titleModeStartCount*2%9;
playerVehicle.pos.z = 8e4+level*checkpointDistance;
}
if (enhancedMode)
{
// match camera to ground at start
cameraOffset = playerVehicle.pos.z - cameraPlayerOffset.z;
const cameraTrackInfo = new TrackSegmentInfo(cameraOffset);
cameraPos.y = cameraTrackInfo.offset.y;
cameraRot.x = cameraTrackInfo.pitch/3;
}
}
function gameUpdateInternal()
{
if (titleScreenMode)
{
// update title screen
if (mouseWasPressed(0) || keyWasPressed('Space') || isUsingGamepad && (gamepadWasPressed(0)||gamepadWasPressed(9)))
{
titleScreenMode = 0;
gameStart();
}
if (time > 60)
{
// restart
++titleModeStartCount;
gameStart();
}
}
else
{
if (startCountdown > 0 && !startCountdownTimer.active())
{
--startCountdown;
sound_beep.play(1,startCountdown?1:2);
//speak(startCountdown || 'GO!' );
startCountdownTimer.set(1);
}
if (gameOverTimer.get() > 1 && (mouseWasPressed(0) || isUsingGamepad && (gamepadWasPressed(0)||gamepadWasPressed(9))) || gameOverTimer.get() > 9)
{
// go back to title screen after a while
titleScreenMode = 1;
titleModeStartCount = 0;
gameStart();
}
if (keyWasPressed('Escape') || isUsingGamepad && gamepadWasPressed(8))
{
// go back to title screen
sound_bump.play(2);
titleScreenMode = 1;
++titleModeStartCount;
gameStart();
}
/*if (keyWasPressed('KeyR'))
{
titleScreenMode = 0;
sound_lose.play(1,2);
gameStart();
}*/
if (freeRide)
{
// free ride mode
startCountdown = 0;
}
else if (keyWasPressed('KeyF'))
{
// enter free ride mode
freeRide = 1;
sound_lose.play(.5,3);
}
if (!startCountdown && !freeRide && !gameOverTimer.isSet())
{
// race mode
raceTime += timeDelta;
const lastCheckpointTimeLeft = checkpointTimeLeft;
checkpointTimeLeft -= timeDelta;
if (checkpointTimeLeft < 4)
if ((lastCheckpointTimeLeft|0) != (checkpointTimeLeft|0))
{
// low time warning
sound_beep.play(1,3);
}
const playerDistance = playerVehicle.pos.z;
const minRecordDistance = 5e3;
if (bestDistance && !playerNewDistanceRecord && playerDistance > bestDistance && playerDistance > minRecordDistance)
{
// new distance record
sound_win.play(1,2);
playerNewDistanceRecord = 1;
//speak('NEW RECORD');
}
if (checkpointTimeLeft <= 0)
{
if (!(debug && debugSkipped))
if (playerDistance > minRecordDistance)
if (!bestDistance || playerDistance > bestDistance)
{
playerNewDistanceRecord = 1;
bestDistance = playerDistance;
writeSaveData();
}
// game over
checkpointTimeLeft = 0;
//speak('GAME OVER');
gameOverTimer.set();
sound_lose.play();
}
}
}
updateCars();
}
function gameUpdate(frameTimeMS=0)
{
if (!clampAspectRatios)
mainCanvasSize = vec3(mainCanvas.width=innerWidth, mainCanvas.height=innerHeight);
else
{
// more complex aspect ratio handling
const innerAspect = innerWidth / innerHeight;
if (canvasFixedSize)
{
// clear canvas and set fixed size
mainCanvas.width = mainCanvasSize.x;
mainCanvas.height = mainCanvasSize.y;
}
else
{
const minAspect = .45, maxAspect = 3;
const correctedWidth = innerAspect > maxAspect ? innerHeight * maxAspect :
innerAspect < minAspect ? innerHeight * minAspect : innerWidth;
if (pixelate)
{
const w = correctedWidth / pixelateScale | 0;
const h = innerHeight / pixelateScale | 0;
mainCanvasSize = vec3(mainCanvas.width = w, mainCanvas.height = h);
}
else
mainCanvasSize = vec3(mainCanvas.width=correctedWidth, mainCanvas.height=innerHeight);
}
// fit to window by adding space on top or bottom if necessary
const fixedAspect = mainCanvas.width / mainCanvas.height;
mainCanvas.style.width = glCanvas.style.width = innerAspect < fixedAspect ? '100%' : '';
mainCanvas.style.height = glCanvas.style.height = innerAspect < fixedAspect ? '' : '100%';
}
if (enhancedMode)
{
document.body.style.cursor = // fun cursors!
!mouseControl ? 'auto': mouseIsDown(2) ? 'grabbing' : mouseIsDown(0) ? 'pointer' : 'grab';
if (paused)
{
// hack: special input handling when paused
inputUpdate();
if (keyWasPressed('Space') || keyWasPressed('KeyP')
|| mouseWasPressed(0) || isUsingGamepad && (gamepadWasPressed(0)||gamepadWasPressed(9)))
{
paused = 0;
sound_checkpoint.play(.5);
}
if (keyWasPressed('Escape') || isUsingGamepad && gamepadWasPressed(8))
{
// go back to title screen
paused = 0;
sound_bump.play(2);
titleScreenMode = 1;
++titleModeStartCount;
gameStart();
}
inputUpdatePost();
}
}
// update time keeping
let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
frameTimeLastMS = frameTimeMS;
const debugSpeedUp = devMode && (keyIsDown('Equal')|| keyIsDown('NumpadAdd')); // +
const debugSpeedDown = devMode && keyIsDown('Minus') || keyIsDown('NumpadSubtract'); // -
if (debug) // +/- to speed/slow time
frameTimeDeltaMS *= debugSpeedUp ? 20 : debugSpeedDown ? .1 : 1;
averageFPS = lerp(.05, averageFPS, 1e3/(frameTimeDeltaMS||1));
frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
// apply flux capacitor, improves smoothness of framerate in some browsers
let fluxCapacitor = 0;
if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
{
// the flux capacitor is what makes time travel possible
// force at least one update each frame since it is waiting for refresh
// -9 needed to prevent fast speeds on > 60fps monitors
fluxCapacitor = frameTimeBufferMS;
frameTimeBufferMS = 0;
}
// update multiple frames if necessary in case of slow framerate
for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3/frameRate)
{
// increment frame and update time
time = frame++ / frameRate;
gameUpdateInternal();
enhancedModeUpdate();
debugUpdate();
inputUpdate();
if (enhancedMode && !titleScreenMode)
if (keyWasPressed('KeyP') || isUsingGamepad && gamepadWasPressed(9))
if (!gameOverTimer.isSet())
{
// update pause
paused = 1;
sound_checkpoint.play(.5,.5);
}
updateCamera();
trackPreUpdate();
inputUpdatePost();
}
// add the time smoothing back in
frameTimeBufferMS += fluxCapacitor;
//mainContext.imageSmoothingEnabled = !pixelate;
//glContext.imageSmoothingEnabled = !pixelate;
glPreRender(mainCanvasSize);
drawScene();
touchGamepadRender();
drawHUD();
debugDraw();
requestAnimationFrame(gameUpdate);
}
function enhancedModeUpdate()
{
if (!enhancedMode)
return;
if (document.hasFocus())
{
if (autoFullscreen && !isFullscreen())
toggleFullscreen();
autoFullscreen = 0;
}
if (!titleScreenMode && !isTouchDevice && autoPause && !document.hasFocus())
paused = 1; // pause when losing focus
if (keyWasPressed('Home')) // dev mode
devMode || (debugInfo = devMode = 1);
if (keyWasPressed('KeyI')) // debug info
debugInfo = !debugInfo;
if (keyWasPressed('KeyM')) // toggle mute
{
if (soundVolume)
sound_bump.play(.4,3);
soundVolume = soundVolume ? 0 : .3;
if (soundVolume)
sound_bump.play();
}
if (keyWasPressed('KeyR')) // restart
{
titleScreenMode = 0;
sound_lose.play(1,2);
gameStart();
}
}
function updateCamera()
{
// update camera
const lastCameraOffset = cameraOffset;
cameraOffset = playerVehicle.pos.z - cameraPlayerOffset.z;
const cameraTrackInfo = new TrackSegmentInfo(cameraOffset);
const playerTrackInfo = new TrackSegmentInfo(playerVehicle.pos.z);
// update world heading based on speed and track turn
const v = cameraOffset - lastCameraOffset;
worldHeading += v*cameraTrackInfo.offset.x/turnWorldScale;
// put camera above player
cameraPos.y = playerTrackInfo.offset.y + (titleScreenMode?1e3:cameraPlayerOffset.y);
// move camera with player
cameraPos.x = playerVehicle.pos.x;
// slight tilt camera with road
cameraRot.x = lerp(.1,cameraRot.x, cameraTrackInfo.pitch/3);
if (freeCamMode)
{
cameraPos = freeCamPos.copy();
cameraRot = freeCamRot.copy();
}
}
///////////////////////////////////////
// save data
const saveName = 'DW';
let bestTime = localStorage[saveName+3]*1 || 0;
let bestDistance = localStorage[saveName+4]*1 || 0;
function writeSaveData()
{
localStorage[saveName+3] = bestTime;
localStorage[saveName+4] = bestDistance;
}