This commit is contained in:
yoyuzh
2026-02-27 14:29:05 +08:00
commit d669738967
41 changed files with 10270 additions and 0 deletions

178
vue/public/race/audio.js Normal file
View File

@@ -0,0 +1,178 @@
'use strict';
///////////////////////////////////////////////////////////////////////////////
// Audio settings
let soundEnable = 1;
let soundVolume = .3;
///////////////////////////////////////////////////////////////////////////////
class Sound
{
constructor(zzfxSound)
{
if (!soundEnable) return;
// generate zzfx sound now for fast playback
this.randomness = zzfxSound[1] || 0;
this.samples = zzfxG(...zzfxSound);
}
play(volume=1, pitch=1)
{
if (!soundEnable) return;
// play the sound
const playbackRate = pitch + this.randomness*rand(-pitch,pitch);
return playSamples(this.samples, volume, playbackRate);
}
playNote(semitoneOffset, pos, volume)
{ return this.play(pos, volume, 2**(semitoneOffset/12), 0); }
}
///////////////////////////////////////////////////////////////////////////////
let audioContext;
function playSamples(samples, volume, rate)
{
const sampleRate=zzfxR;
if (!soundEnable || isTouchDevice && !audioContext)
return;
if (!audioContext)
audioContext = new AudioContext; // create audio context
// prevent sounds from building up if they can't be played
if (audioContext.state != 'running')
{
// fix stalled audio
audioContext.resume();
return; // prevent suspended sounds from building up
}
// create buffer and source
const buffer = audioContext.createBuffer(1, samples.length, sampleRate),
source = audioContext.createBufferSource();
// copy samples to buffer and setup source
buffer.getChannelData(0).set(samples);
source.buffer = buffer;
source.playbackRate.value = rate;
// create and connect gain node (createGain is more widely spported then GainNode construtor)
const gainNode = audioContext.createGain();
gainNode.gain.value = soundVolume*volume;
gainNode.connect(audioContext.destination);
// connect source to stereo panner and gain
//source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)})).connect(gainNode);
source.connect(gainNode);
// play and return sound
source.start();
return source;
}
///////////////////////////////////////////////////////////////////////////////
// ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.1 by Frank Force
const zzfxR = 44100;
function zzfxG
(
// parameters
volume = 1, randomness, frequency = 220, attack = 0, sustain = 0,
release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
)
{
// init parameters
let PI2 = PI*2, sampleRate = zzfxR,
startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
startFrequency = frequency *= PI2 / sampleRate, // no randomness
// rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
// biquad LP/HP filter
quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
b0 = (1 + sign(filter) * cos) / 2 / a0,
b1 = -(sign(filter) + cos) / a0, b2 = b0,
x2 = 0, x1 = 0, y2 = 0, y1 = 0;
// scale by sample rate
attack = attack * sampleRate + 9; // minimum attack to prevent pop
decay *= sampleRate;
sustain *= sampleRate;
release *= sampleRate;
delay *= sampleRate;
deltaSlide *= 500 * PI2 / sampleRate**3;
modulation *= PI2 / sampleRate;
pitchJump *= PI2 / sampleRate;
pitchJumpTime *= sampleRate;
repeatTime = repeatTime * sampleRate | 0;
ASSERT(shape != 3 && shape != 2); // need save space
// generate waveform
for(length = attack + decay + sustain + release + delay | 0;
i < length; b[i++] = s * volume) // sample
{
if (!(++c%(bitCrush*100|0))) // bit crush
{
s = shape? shape>1?
//shape>2? shape>3? // wave shape
//Math.sin(t**3) : // 4 noise
//clamp(Math.tan(t),1,-1): // 3 tan
1-(2*t/PI2%2+2)%2: // 2 saw
1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
Math.sin(t); // 0 sin
s = (repeatTime ?
1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
: 1) *
sign(s)*(abs(s)**shapeCurve) * // curve
(i < attack ? i/attack : // attack
i < attack + decay ? // decay
1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
i < attack + decay + sustain ? // sustain
sustainVolume : // sustain volume
i < length - delay ? // release
(length - i - delay)/release * // release falloff
sustainVolume : // release volume
0); // post release
s = delay ? s/2 + (delay > i ? 0 : // delay
(i<length-delay? 1 : (length-i)/delay) * // release delay
b[i-delay|0]/2/volume) : s; // sample delay
if (filter) // apply filter
s = y1 = b2*x2 + b1*(x2=x1) + b0*(x1=s) - a2*y2 - a1*(y2=y1);
}
f = (frequency += slide += deltaSlide) *// frequency
Math.cos(modulation*tm++); // modulation
t += f + f*noise*Math.sin(i**5); // noise
if (j && ++j > pitchJumpTime) // pitch jump
{
frequency += pitchJump; // apply pitch jump
startFrequency += pitchJump; // also apply to start
j = 0; // stop pitch jump time
}
if (repeatTime && !(++r % repeatTime)) // repeat
{
frequency = startFrequency; // reset frequency
slide = startSlide; // reset slide
j = j || 1; // reset pitch jump time
}
}
return b;
}

261
vue/public/race/debug.js Normal file
View File

@@ -0,0 +1,261 @@
'use strict';
const debug = 1;
let enhancedMode = 1;
let enableAsserts = 1;
let devMode = 0;
let downloadLink, debugMesh, debugTile, debugCapture, debugCanvas;
let debugGenerativeCanvas=0, debugInfo=0, debugSkipped=0;
let debugGenerativeCanvasCached, showMap;
let freeCamPos, freeCamRot, mouseDelta;
const js13kBuildLevel2 = 0; // more space is needed for js13k
function ASSERT(assert, output)
{ enableAsserts&&(output ? console.assert(assert, output) : console.assert(assert)); }
function LOG() { console.log(...arguments); }
///////////////////////////////////////////////////////////////////////////////
function debugInit()
{
freeCamPos = vec3();
freeCamRot = vec3();
mouseDelta = vec3();
debugCanvas = document.createElement('canvas');
downloadLink = document.createElement('a');
}
function debugUpdate()
{
if (!devMode)
return;
if (keyWasPressed('KeyG')) // free Cam
{
freeCamMode = !freeCamMode;
if (!freeCamMode)
{
document.exitPointerLock();
cameraPos = vec3();
cameraRot = vec3();
}
}
if (freeCamMode)
{
if (!document.pointerLockElement)
{
mainCanvas.requestPointerLock();
freeCamPos = cameraPos.copy();
freeCamRot = cameraRot.copy();
}
const input = vec3(
keyIsDown('KeyD') - keyIsDown('KeyA'),
keyIsDown('KeyE') - keyIsDown('KeyQ'),
keyIsDown('KeyW') - keyIsDown('KeyS'));
const moveSpeed = keyIsDown('ShiftLeft') ? 500 : 100;
const turnSpeed = 2;
const moveDirection = input.rotateX(freeCamRot.x).rotateY(-freeCamRot.y);
freeCamPos = freeCamPos.add(moveDirection.scale(moveSpeed));
freeCamRot = freeCamRot.add(vec3(mouseDelta.y,mouseDelta.x).scale(turnSpeed));
freeCamRot.x = clamp(freeCamRot.x, -PI/2, PI/2);
mouseDelta = vec3();
}
if (keyWasPressed('Digit1') || keyWasPressed('Digit2'))
{
const d = keyWasPressed('Digit2') ? 1 : -1;
playerVehicle.pos.z += d * checkpointDistance;
playerVehicle.pos.z = max(playerVehicle.pos.z, 0);
checkpointTimeLeft = 40;
debugSkipped = 1;
}
if (keyIsDown('Digit3') || keyIsDown('Digit4'))
{
const v = keyIsDown('Digit4') ? 1e3 : -1e3;
playerVehicle.pos.z += v;
playerVehicle.pos.z = max(playerVehicle.pos.z, 0);
const trackInfo = new TrackSegmentInfo(playerVehicle.pos.z);
playerVehicle.pos.y = trackInfo.offset.y;
playerVehicle.pos.x = 0;
// update world heading based on speed and track turn
const cameraTrackInfo = new TrackSegmentInfo(cameraOffset);
worldHeading += v*cameraTrackInfo.offset.x/turnWorldScale;
debugSkipped = 1;
}
if (keyWasPressed('Digit5'))
checkpointTimeLeft=12
if (keyWasPressed('Digit6'))
{
// randomize track
trackSeed = randInt(1e9);
//initGenerative();
const endLevel = levelInfoList.pop();
shuffle(endLevel.scenery);
shuffle(levelInfoList);
for(let i=levelInfoList.length; i--;)
{
const info = levelInfoList[i];
info.level = i;
info.randomize();
}
levelInfoList.push(endLevel);
buildTrack();
for(const s in spriteList)
{
const sprite = spriteList[s];
if (sprite instanceof GameSprite)
sprite.randomize();
}
const playerTrackInfo = new TrackSegmentInfo(playerVehicle.pos.z);
playerVehicle.pos.y = playerTrackInfo.offset.y;
//gameStart();
}
if (keyWasPressed('Digit7'))
debugGenerativeCanvas = !debugGenerativeCanvas;
if (keyWasPressed('Digit0'))
debugCapture = 1;
if (keyWasPressed('KeyQ') && !freeCamMode)
testDrive = !testDrive
if (keyWasPressed('KeyU'))
sound_win.play();
if (debug && keyWasPressed('KeyV'))
spawnVehicle(playerVehicle.pos.z-1300)
//if (!document.hasFocus())
// testDrive = 1;
}
function debugDraw()
{
if (!debug)
return;
if (debugInfo && !debugCapture)
drawHUDText((averageFPS|0) + 'fps / ' + glBatchCountTotal + ' / ' + glDrawCalls + ' / ' + vehicles.length, vec3(.98,.12),.03, undefined, 'monospace','right');
const c = mainCanvas;
const context = mainContext;
if (testDrive && !titleScreenMode && !freeRide)
drawHUDText('AUTO', vec3(.5,.95),.05,RED);
if (showMap)
{
// draw track map preview
context.save();
context.beginPath();
for(let k=2;k--;)
{
let x=0, v=0;
let p = vec3();
let d = vec3(0,-.5);
for(let i=0; i < 1e3; i++)
{
let j = playerVehicle.pos.z/trackSegmentLength+i-100|0;
if (!track[j])
continue;
const t = track[j];
const o = t.offset;
v += o.x;
p = p.add(d.rotateZ(v*.005));
if (j%5==0)
{
let y = o.y;
let w = t.width/199;
const h = k ? 5 : -y*.01;
context.fillStyle=hsl(y*.0001,1,k?0:.5,k?.5:1);
context.fillRect(c.width-200+p.x,c.height-100+p.y+h,w,w);
//context.fillRect(c.width-200+x/199,c.height-100-i/2+o,w,w);
}
}
}
context.restore();
}
if (debugGenerativeCanvas)
{
const s = 512;
//context.imageSmoothingEnabled = false;
context.drawImage(debugGenerativeCanvasCached, 0, 0, s, s);
// context.strokeRect(0, 0, s, s);
}
if (debugCapture)
{
debugCapture = 0;
const context = debugCanvas.getContext('2d');
debugCanvas.width = mainCanvas.width;
debugCanvas.height = mainCanvas.height;
context.fillStyle = '#000';
context.fillRect(0,0,mainCanvas.width,mainCanvas.height);
context.drawImage(glCanvas, 0, 0);
context.drawImage(mainCanvas, 0, 0);
debugSaveCanvas(debugCanvas);
}
{
// test render
//debugMesh = cylinderMesh;
debugMesh && debugMesh.render(buildMatrix(cameraPos.add(vec3(0,400,1000)), vec3(0,time,0), vec3(200)), WHITE);
//debugTile = vec3(0,1)
if (debugTile)
{
const s = 256*2, w = generativeTileSize, v = debugTile.scale(w);
const x = mainCanvas.width/2-s/2;
context.fillStyle = '#5f5';
context.fillRect(x, 0, s, s);
context.drawImage(debugGenerativeCanvasCached, v.x, v.y, w, w, x, 0, s, s);
context.strokeRect(x, 0, s, s);
//pushTrackObject(cameraPos.add(vec3(0,0,100)), vec3(100), WHITE, debugTile);
}
}
if (0) // world cube
{
const r = vec3(0,-worldHeading,0);
const m1 = buildMatrix(vec3(2220,1e3,2e3), r, vec3(200));
cubeMesh.render(m1, hsl(0,.8,.5));
}
if (0)
{
// test noise
context.fillStyle = '#fff';
context.fillRect(0, 0, 500, 500);
context.fillStyle = '#000';
for(let i=0; i < 1e3; i++)
{
const n = noise1D(i/129-time*9)*99;
context.fillRect(i, 200+n, 9, 9);
}
}
//cubeMesh.render(buildMatrix(vec3(0,-500,0), vec3(0), vec3(1e5,10,1e5)), RED); // ground
//cylinderMesh.render(buildMatrix(cameraPos.add(vec3(0,400,1000)), vec3(time,time/2,time/3), vec3(200)), WHITE);
//let t = new Tile(vec3(64*2,0), vec3(128));
//pushSprite(cameraPos.add(vec3(0,400,1000)), vec3(200), WHITE, t);
glRender();
}
///////////////////////////////////////////////////////////////////////////////
function debugSaveCanvas(canvas, filename='screenshot', type='image/png')
{ debugSaveDataURL(canvas.toDataURL(type), filename); }
function debugSaveText(text, filename='text', type='text/plain')
{ debugSaveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
function debugSaveDataURL(dataURL, filename)
{
downloadLink.download = filename;
downloadLink.href = dataURL;
downloadLink.click();
}

472
vue/public/race/draw.js Normal file
View File

@@ -0,0 +1,472 @@
'use strict';
let cubeMesh, quadMesh, shadowMesh, cylinderMesh, carMesh, carWheel;
const bleedPixels = 8;
const WHITE = rgb();
const BLACK = rgb(0,0,0);
const RED = rgb(1,0,0);
const ORANGE = rgb(1,.5,0);
const YELLOW = rgb(1,1,0);
const GREEN = rgb(0,1,0);
const CYAN = rgb(0,1,1);
const BLUE = rgb(0,0,1);
const PURPLE = rgb(.5,0,1);
const MAGENTA= rgb(1,0,1);
const GRAY = rgb(.5,.5,.5);
let spriteList;
let testGameSprite;
///////////////////////////////////////////////////////////////////////////////
function initSprites()
{
//spriteList
//(tilePos, size=1e3, sizeRandomness=0, windScale=0, collideSize=60)
spriteList = {};
// trees
spriteList.tree_palm = new GameSprite(vec3(0,1),1500,.2,.1,.04);
spriteList.tree_palm.trackFace = 1;
spriteList.tree_oak = new GameSprite(vec3(1,1),2e3,.5,.06,.1);
spriteList.tree_stump = new GameSprite(vec3(2,1),1e3,.6,.04);
spriteList.tree_dead = new GameSprite(vec3(3,1),1e3,.3,.1,.06);
spriteList.tree_pink = new GameSprite(vec3(4,1),1500,.3,.1,.04);
spriteList.tree_pink.trackFace = 1;
spriteList.tree_bush = new GameSprite(vec3(5,1),1e3,.5,.1,.06);
spriteList.tree_fall = new GameSprite(vec3(6,1),1500,.3,.1,.1);
//TB(spriteList.tree_flower = new GameSprite(vec3(7,1),2e3,.3,.05,200));
spriteList.tree_snow = new GameSprite(vec3(4,3),1300,.3,.06,.1)
spriteList.tree_yellow = new GameSprite(vec3(5,3),1e3,.3,.06,.1)
spriteList.tree_huge = new GameSprite(vec3(3,1),1e4,.5,.1,.1)
spriteList.tree_huge.colorHSL = vec3(.8, 0, .5);
spriteList.tree_huge.shadowScale = 0;
// smaller tree shadows
spriteList.tree_palm.shadowScale =
spriteList.tree_oak.shadowScale =
spriteList.tree_stump.shadowScale =
spriteList.tree_dead.shadowScale =
spriteList.tree_pink.shadowScale =
spriteList.tree_bush.shadowScale =
spriteList.tree_fall.shadowScale =
spriteList.tree_snow.shadowScale =
spriteList.tree_yellow.shadowScale = .7;
// grass and flowers
spriteList.grass_plain = new GameSprite(vec3(0,3),500,.5,1);
spriteList.grass_plain.colorHSL = vec3(.3, .4, .5);
spriteList.grass_dead = new GameSprite(vec3(0,3),600,.3,1);
spriteList.grass_dead.colorHSL = vec3(.13, .6, .7);
spriteList.grass_flower1 = new GameSprite(vec3(1,3),500,.3,1);
spriteList.grass_flower2 = new GameSprite(vec3(2,3),500,.3,1);
spriteList.grass_flower3 = new GameSprite(vec3(3,3),500,.3,1);
spriteList.grass_red = new GameSprite(vec3(0,3),700,.3,1)
spriteList.grass_red.colorHSL = vec3(0, .8, .5);
spriteList.grass_snow = new GameSprite(vec3(0,3),300,.5,1)
spriteList.grass_snow.colorHSL = vec3(.4, 1, .9);
spriteList.grass_large = new GameSprite(vec3(0,3),1e3,.5,1);
spriteList.grass_large.colorHSL = vec3(.4, .4, .5);
//spriteList.grass_huge = new GameSprite(vec3(0,3),1e4,.6,.5,5e3);
//spriteList.grass_huge.colorHSL = vec3(.8, .5, .5);
//spriteList.grass_huge.hueRandomness = .2;
// billboards
spriteList.billboards = [];
const PB = (s)=>spriteList.billboards.push(s);
PB(spriteList.sign_opGames = new GameSprite(vec3(5,2),600,0,.02,.5,0));
PB(spriteList.sign_js13k = new GameSprite(vec3(0,2),600,0,.02,1,0));
PB(spriteList.sign_zzfx = new GameSprite(vec3(1,2),500,0,.02,.5,0));
PB(spriteList.sign_avalanche = new GameSprite(vec3(7,2),600,0,.02,1,0));
PB(spriteList.sign_github = new GameSprite(vec3(2,2),750,0,.02,.5,0));
//PB(spriteList.sign_littlejs = new GameSprite(vec3(4,2),600,0,.02,1,0));
spriteList.sign_frankForce = new GameSprite(vec3(3,2),500,0,.02,1,0);
//PB(spriteList.sign_dwitter = new GameSprite(vec3(6,2),550,0,.02,1,0));
// signs
spriteList.sign_turn = new GameSprite(vec3(0,5),500,0,.05,.5);
spriteList.sign_turn.trackFace = 1; // signs face track
//spriteList.sign_curve = new GameSprite(vec3(1,5),500,0,.05,.5);
//spriteList.sign_curve.trackFace = 1; // signs face track
//spriteList.sign_warning = new GameSprite(vec3(2,5),500,0,.05,1,0);
//spriteList.sign_speed = new GameSprite(vec3(4,5),500,0,.05,50,0);
//spriteList.sign_interstate = new GameSprite(vec3(5,5),500,0,.05,50,0);
// rocks
spriteList.rock_tall = new GameSprite(vec3(1,4),1e3,.3,0,.6,0);
spriteList.rock_big = new GameSprite(vec3(2,4),800,.2,0,.57,0);
spriteList.rock_huge = new GameSprite(vec3(1,4),5e3,.7,0,.6,0);
spriteList.rock_huge.shadowScale = 0;
spriteList.rock_huge.colorHSL = vec3(.08, 1, .8);
spriteList.rock_huge.hueRandomness = .01;
spriteList.rock_huge2 = new GameSprite(vec3(2,4),8e3,.5,0,.25,0);
spriteList.rock_huge2.shadowScale = 0;
spriteList.rock_huge2.colorHSL = vec3(.05, 1, .8);
spriteList.rock_huge2.hueRandomness = .01;
spriteList.rock_huge3 = new GameSprite(vec3(2,4),8e3,.7,0,.5,0);
spriteList.rock_huge3.shadowScale = 0;
spriteList.rock_huge3.colorHSL = vec3(.05, 1, .8);
spriteList.rock_huge3.hueRandomness = .01;
spriteList.rock_weird = new GameSprite(vec3(2,4),5e3,.5,0,1,0);
spriteList.rock_weird.shadowScale = 0;
spriteList.rock_weird.colorHSL = vec3(.8, 1, .8);
spriteList.rock_weird.hueRandomness = .2;
spriteList.rock_weird2 = new GameSprite(vec3(1,4),1e3,.5,0,.5,0);
spriteList.rock_weird2.colorHSL = vec3(0, 0, .2);
spriteList.tunnel1 = new GameSprite(vec3(6,4),1e4,.0,0,0,0);
spriteList.tunnel1.shadowScale = 0;
spriteList.tunnel1.colorHSL = vec3(.05, 1, .8);
spriteList.tunnel1.tunnelArch = 1;
spriteList.tunnel2 = new GameSprite(vec3(7,4),5e3,0,0,0,0);
spriteList.tunnel2.shadowScale = 0;
spriteList.tunnel2.colorHSL = vec3(0, 0, .1);
spriteList.tunnel2.tunnelLong = 1;
spriteList.tunnel2Front = new GameSprite(vec3(7,4),5e3,0,0,0,0);
spriteList.tunnel2Front.shadowScale = 0;
spriteList.tunnel2Front.colorHSL = vec3(0,0,.8);
//spriteList.tunnel2_rock = new GameSprite(vec3(6,6),1e4,.2,0,.5,0);
//spriteList.tunnel2_rock.colorHSL = vec3(.15, .5, .8);
// hazards
spriteList.hazard_rocks = new GameSprite(vec3(3,4),600,.2,0,.9);
spriteList.hazard_rocks.shadowScale = 0;
spriteList.hazard_rocks.isBump = 1;
spriteList.hazard_rocks.spriteYOffset = -.02;
spriteList.hazard_sand = new GameSprite(vec3(4,4),600,.2,0,.9);
spriteList.hazard_sand.shadowScale = 0;
spriteList.hazard_sand.isSlow = 1;
spriteList.hazard_sand.spriteYOffset = -.02;
//spriteList.hazard_snow = new GameSprite(vec3(6,6),500,.1,0,300,0);
//spriteList.hazard_snow.isSlow = 1;
// special sprites
spriteList.water = new GameSprite(vec3(5,4),6e3,.5,1);
spriteList.water.shadowScale = 0;
spriteList.sign_start = new GameSprite(vec3(1,6),2300,0,.01,0,0);
spriteList.sign_start.shadowScale = 0;
spriteList.sign_goal = new GameSprite(vec3(0,6),2300,0,.01,0,0);
spriteList.sign_goal.shadowScale = 0;
spriteList.sign_checkpoint1 = new GameSprite(vec3(6,0),1e3,0,.01,0,0);
spriteList.sign_checkpoint1.shadowScale = 0;
spriteList.sign_checkpoint2 = new GameSprite(vec3(7,0),1e3,0,.01,0,0);
spriteList.sign_checkpoint2.shadowScale = 0;
spriteList.telephonePole = new GameSprite(vec3(0,4),1800,0,0,.03,0);
//spriteList.parts_girder = new GameSprite(vec3(0,6),500,0,.05,30,0);
spriteList.telephonePole.shadowScale = .3;
spriteList.grave_stone = new GameSprite(vec3(2,6),500,.3,.05,.5,0);
spriteList.grave_stone.lightnessRandomness = .5;
spriteList.light_tunnel = new GameSprite(vec3(0,0),200,0,0,0,0);
spriteList.light_tunnel.shadowScale = 0;
// horizon sprites
spriteList.horizon_city = new GameSprite(vec3(3,6),0,0,0,0,1);
spriteList.horizon_city.hueRandomness =
spriteList.horizon_city.lightnessRandomness = .15;
spriteList.horizon_city.colorHSL = vec3(1); // vary color
spriteList.horizon_islands = new GameSprite(vec3(7,6));
spriteList.horizon_islands.colorHSL = vec3(.25, .5, .6);
spriteList.horizon_islands.canMirror = 0;
spriteList.horizon_redMountains = new GameSprite(vec3(7,6));
spriteList.horizon_redMountains.colorHSL = vec3(.05, .7, .7);
spriteList.horizon_redMountains.canMirror = 0;
spriteList.horizon_brownMountains = new GameSprite(vec3(7,6));
spriteList.horizon_brownMountains.colorHSL = vec3(.1, .5, .6);
spriteList.horizon_brownMountains.canMirror = 0;
spriteList.horizon_smallMountains = new GameSprite(vec3(6,6));
spriteList.horizon_smallMountains.colorHSL = vec3(.1, .5, .6);
spriteList.horizon_smallMountains.canMirror = 0;
spriteList.horizon_desert = new GameSprite(vec3(6,6));
spriteList.horizon_desert.colorHSL = vec3(.15, .5, .8);
spriteList.horizon_desert.canMirror = 0;
spriteList.horizon_snow = new GameSprite(vec3(7,6));
spriteList.horizon_snow.colorHSL = vec3(0,0,1);
spriteList.horizon_snow.canMirror = 0;
spriteList.horizon_graveyard = new GameSprite(vec3(6,6));
spriteList.horizon_graveyard.colorHSL = vec3(.2, .4, .8);
spriteList.horizon_graveyard.canMirror = 0;
spriteList.horizon_weird = new GameSprite(vec3(7,6));
spriteList.horizon_weird.colorHSL = vec3(.7, .5, .6);
spriteList.horizon_weird.canMirror = 0;
if (!js13kBuildLevel2)
{
spriteList.horizon_mountains = new GameSprite(vec3(7,6));
spriteList.horizon_mountains.colorHSL = vec3(0, 0, .7);
spriteList.horizon_mountains.canMirror = 0;
}
// more sprites
spriteList.circle = new GameSprite(vec3());
spriteList.dot = new GameSprite(vec3(1,0));
spriteList.carShadow = new GameSprite(vec3(2,0));
spriteList.carLicense = new GameSprite(vec3(3,0));
spriteList.carNumber = new GameSprite(vec3(4,0));
}
// a sprite that can be placed on the track
class GameSprite
{
constructor(tilePos, size=1e3, sizeRandomness=0, windScale=0, collideScale=0, canMirror=1)
{
this.spriteTile = vec3(
(tilePos.x * generativeTileSize + bleedPixels) / generativeCanvasSize,
(tilePos.y * generativeTileSize + bleedPixels) / generativeCanvasSize,
);
this.size = size;
this.sizeRandomness = sizeRandomness;
this.windScale = windScale;
this.collideScale = collideScale;
this.canMirror = canMirror; // allow mirroring
this.trackFace = 0; // face track if close
this.spriteYOffset = 0; // how much to offset the sprite from the ground
this.shadowScale = 1.2;
// color
this.colorHSL = vec3(0,0,1);
this.hueRandomness = .05;
this.lightnessRandomness = .01;
}
getRandomSpriteColor()
{
const c = this.colorHSL.copy();
c.x += random.floatSign(this.hueRandomness);
c.z += random.floatSign(this.lightnessRandomness);
return c.getHSLColor();
}
getRandomSpriteScale() { return 1+random.floatSign(this.sizeRandomness); }
randomize()
{
this.colorHSL.x = random.float(-.1,.1);
this.colorHSL.y = clamp(this.colorHSL.y+random.float(-.1,.1));
this.colorHSL.z = clamp(this.colorHSL.z+random.float(-.1,.1));
this.hueRandomness = .05;
this.lightnessRandomness = .01;
}
}
///////////////////////////////////////////////////////////////////////////////
const getAspect =()=> mainCanvasSize.x/mainCanvasSize.y;
function drawInit()
{
{
// cube
const points = [vec3(-1,1),vec3(1,1),vec3(1,-1),vec3(-1,-1)];
cubeMesh = new Mesh().buildExtrude(points);
}
{
// quad
const points1 = [vec3(-1,1),vec3(1,1),vec3(-1,-1),vec3(1,-1)];
const uvs1 = points1.map(p=>p.multiply(vec3(.5,-.5,.5)).add(vec3(.5)));
quadMesh = new Mesh(points1, points1.map(p=>vec3(0,0,1)), uvs1);
shadowMesh = quadMesh.transform(0,vec3(PI/2,0));
}
{
// cylinder
const points = [];
const sides = 12;
for(let i=sides; i--;)
{
const a = i/sides*PI*2;
points.push(vec3(1,0).rotateZ(a));
}
cylinderMesh = new Mesh().buildExtrude(points);
}
{
// car bottom
const points =
[
vec3(-1,.5),
vec3(-.7,.4),
vec3(-.2,.5),
vec3(.1,.5),
vec3(1,.2),
vec3(1,.2),
vec3(1,0),
vec3(-1,0),
]
carMesh = new Mesh().buildExtrude(points,.5);
carMesh = carMesh.transform(0,vec3(0,-PI/2));
carWheel = cylinderMesh.transform(0,vec3(0,-PI/2));
}
}
///////////////////////////////////////////////////////////////////////////////
class Mesh
{
constructor(points, normals, uvs)
{
this.points = points;
this.normals = normals;
this.uvs = uvs;
}
render(transform, color)
{
glPushVerts(this.points, this.normals, color);
glRender(transform);
}
renderTile(transform, color, tile)
{
//ASSERT(tile instanceof SpriteTile);
const uvs = this.uvs.map(uv=>(vec3(spriteSize-spriteSize*uv.x+tile.x,uv.y*spriteSize+tile.y)));
// todo, figure out why this is backwards
//const uvs = this.uvs.map(uv=>uv.multiply(tile.size).add(tile.pos));
glPushVerts(this.points, this.normals, color, uvs);
glRender(transform);
}
buildExtrude(facePoints, size=1)
{
// convert list of 2d points into a 3d shape
const points = [], normals = [];
const vertCount = facePoints.length + 2;
for (let k=2; k--;)
for (let i=vertCount; i--;)
{
// build top and bottom of mesh
const j = clamp(i-1, 0, vertCount-3); // degenerate tri at ends
const h = j>>1;
let m = j%2 == vertCount%2 ? h : vertCount-3-h;
if (!k) // hack to fix glitch in mesh due to concave shape
m = mod(vertCount+2-m, facePoints.length);
const point = facePoints[m].copy();
point.z = k?size:-size;
points.push(point);
normals.push(vec3(0,0,point.z));
}
for (let i = facePoints.length; i--;)
{
// build sides of mesh
const point1 = facePoints[i];
const point2 = facePoints[(i+1)%facePoints.length];
const s = vec3(0,0,size);
const pointA = point1.add(s);
const pointB = point2.add(s);
const pointC = point1.subtract(s);
const pointD = point2.subtract(s);
const sidePoints = [pointA, pointA, pointB, pointC, pointD, pointD];
const normal = pointC.subtract(pointD).cross(pointA.subtract(pointC)).normalize();
for (const p of sidePoints)
{
points.push(p);
normals.push(normal);
}
}
return new Mesh(points, normals);
}
transform(pos, rot, scale)
{
const m = buildMatrix(pos, rot, scale);
const m2 = buildMatrix(0, rot);
return new Mesh(
this.points.map(p=>p.transform(m)),
this.normals.map(p=>p.transform(m2)),
this.uvs
);
}
/*combine(mesh, pos, rot, scale)
{
const m = buildMatrix(pos, rot, scale);
const m2 = buildMatrix(0, rot);
this.points.push(...mesh.points.map(p=>p.transform(m)));
this.normals && this.normals.push(...mesh.normals.map(p=>p.transform(m2)));
this.uvs && this.uvs.push(...mesh.uvs);
return this;
}*/
}
///////////////////////////////////////////////////////////////////////////////
function pushGradient(pos, size, color, color2)
{
const mesh = quadMesh;
const points = mesh.points.map(p=>p.multiply(size).addSelf(pos));
const colors = [color, color, color2, color2];
glPushColoredVerts(points, colors);
}
function pushSprite(pos, size, color, tile, skew=0)
{
const mesh = quadMesh;
const points = mesh.points.map(p=>vec3(p.x*abs(size.x)+pos.x, p.y*abs(size.y)+pos.y,pos.z));
// apply skew
const o = skew*size.y;
points[0].x += o;
points[1].x += o;
// apply texture
if (tile)
{
//ASSERT(tile instanceof SpriteTile);
let tilePosX = tile.x;
let tilePosY = tile.y;
let tileSizeX = spriteSize;
let tileSizeY = spriteSize;
if (size.x < 0)
tilePosX -= tileSizeX *= -1;
if (size.y < 0)
tilePosY -= tileSizeY *= -1;
const uvs = mesh.uvs.map(uv=>
vec3(uv.x*tileSizeX+tilePosX, uv.y*tileSizeY+tilePosY));
glPushVertsCapped(points, 0, color, uvs);
}
else
glPushVertsCapped(points, 0, color);
}
function pushShadow(pos, xSize, zSize)
{
if (optimizedCulling && pos.z > 2e4)
return; // cull far shadows
const color = rgb(0,0,0,.7)
const tile = spriteList.dot.spriteTile;
const mesh = shadowMesh;
const points = mesh.points.map(p=>vec3(p.x*xSize+pos.x,pos.y,p.z*zSize+pos.z));
const uvs = mesh.uvs.map(uv=>
vec3(uv.x*spriteSize+tile.x, uv.y*spriteSize+tile.y));
glPushVertsCapped(points, 0, color, uvs);
}
///////////////////////////////////////////////////////////////////////////////
// Fullscreen mode
/** Returns true if fullscreen mode is active
* @return {Boolean}
* @memberof Draw */
function isFullscreen() { return !!document.fullscreenElement; }
/** Toggle fullsceen mode
* @memberof Draw */
function toggleFullscreen()
{
const element = document.body;
if (isFullscreen())
{
if (document.exitFullscreen)
document.exitFullscreen();
}
else if (element.requestFullscreen)
element.requestFullscreen();
else if (element.webkitRequestFullscreen)
element.webkitRequestFullscreen();
else if (element.mozRequestFullScreen)
element.mozRequestFullScreen();
}

433
vue/public/race/game.js Normal file
View File

@@ -0,0 +1,433 @@
'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;
}

File diff suppressed because it is too large Load Diff

168
vue/public/race/hud.js Normal file
View File

@@ -0,0 +1,168 @@
'use strict';
const showTitle = 1;
function drawHUD()
{
if (freeCamMode)
return;
if (enhancedMode && paused)
{
// paused
drawHUDText('-暂停-', vec3(.5,.9), .08, undefined, 'monospace',undefined,900,undefined,undefined,undefined,3);
}
if (titleScreenMode)
{
if (showTitle)
for(let j=2;j--;)
{
// draw logo
const text = '零界时速';
const pos = vec3(.5,.3-j*.15).multiply(mainCanvasSize);
let size = mainCanvasSize.y/9;
const weight = 900;
const style = 'italic';
const font = 'arial';
if (enhancedMode && getAspect() < .6)
size = mainCanvasSize.x/5;
const context = mainContext;
context.strokeStyle = BLACK;
context.textAlign = 'center';
let totalWidth = 0;
for(let k=2;k--;)
for(let i=0;i<text.length;i++)
{
const p = Math.sin(i-time*2-j*2);
let size2 = (size + p*mainCanvasSize.y/20);
if (enhancedMode)
size2 *= lerp(time*2-2+j,0,1)
context.font = `${style} ${weight} ${size2}px ${font}`;
const c = text[i];
const w = context.measureText(c).width;
if (k)
{
totalWidth += w;
continue;
}
const x = pos.x+w/3-totalWidth/2;
for(let f = 2;f--;)
{
const o = f*mainCanvasSize.y/99;
context.fillStyle = hsl(.15-p/9,1,f?0:.75-p*.25);
context.fillText(c, x+o, pos.y+o);
}
pos.x += w;
}
}
if (!enhancedMode || time > 5)
{
if (bestTime && (!enhancedMode || time%20<10))
{
const timeString = formatTimeString(bestTime);
if (!js13kBuildLevel2)
drawHUDText('最佳时间', vec3(.5,.9), .07, undefined, 'monospace',undefined,900,undefined,undefined,undefined,3);
drawHUDText(timeString, vec3(.5,.97), .07, undefined, 'monospace',undefined,900,undefined,undefined,undefined,3);
}
else if (enhancedMode && !isTouchDevice)
{
drawHUDText('点击开始', vec3(.5,.97), .07, undefined, 'monospace',undefined,900,undefined,undefined,undefined,3);
}
}
}
else if (startCountdownTimer.active() || startCountdown)
{
// count down
const a = 1-time%1;
const t = !startCountdown && startCountdownTimer.active() ? '出发!' : startCountdown|0;
const c = (startCountdown?RED:GREEN).copy();
c.a = a;
drawHUDText(t, vec3(.5,.2), .25-a*.1, c, undefined,undefined,900,undefined,undefined,.03);
}
else
{
const wave1 = .04*(1 - abs(Math.sin(time*2)));
if (gameOverTimer.isSet())
{
// win screen
const c = playerWin?YELLOW:WHITE;
const wave2 = .04*(1 - abs(Math.sin(time*2+PI/2)));
drawHUDText(playerWin?'你':'游戏', vec3(.5,.2), .1+wave1, c, undefined,undefined,900,'italic',.5,undefined,4);
drawHUDText(playerWin?'获胜!':'结束!', vec3(.5,.3), .1+wave2, c, undefined,undefined,900,'italic',.5,undefined,4);
if (playerNewRecord || playerNewDistanceRecord && !bestTime)
drawHUDText('新纪录', vec3(.5,.6), .08+wave1/4, RED, 'monospace',undefined,900,undefined,undefined,undefined,3);
}
else if (!startCountdownTimer.active() && !freeRide)
{
// big center checkpoint time
const c = checkpointTimeLeft < 4 ? RED : checkpointTimeLeft < 11 ? YELLOW : WHITE;
const t = checkpointTimeLeft|0;
let y=.13, s=.14;
if (enhancedMode && getAspect() < .6)
y=.14, s=.1;
drawHUDText(t, vec3(.5,y), s, c, undefined,undefined,900,undefined,undefined,.04);
}
if (!freeRide)
{
if (playerWin)
{
// current time
const timeString = formatTimeString(raceTime);
if (!js13kBuildLevel2)
drawHUDText('时间', vec3(.5,.43), .08, undefined, 'monospace',undefined,900,undefined,undefined,undefined,3);
drawHUDText(timeString, vec3(.5), .08, undefined, 'monospace',undefined,900,undefined,undefined,undefined,3);
}
else
{
// current time
const timeString = formatTimeString(raceTime);
drawHUDText(timeString, vec3(.01,.05), .05, undefined, 'monospace','left');
// current stage
const level = debug&&testLevelInfo ? testLevelInfo.level+1 :playerLevel+1;
drawHUDText('关卡 '+level, vec3(.99,.05), .05, undefined, 'monospace','right');
}
}
}
if (debugInfo&&!titleScreenMode) // mph
{
const mph = playerVehicle.velocity.z|0;
const mphPos = vec3(.01,.95);
drawHUDText(mph+' 公里/时', mphPos, .08, undefined,undefined,'left',900,'italic');
}
}
///////////////////////////////////////////////////////////////////////////////
function drawHUDText(text, pos, size=.1, color=WHITE, font='arial', textAlign='center', weight=400, style='', width, shadowScale=.07, outline)
{
size *= mainCanvasSize.y;
if (width)
width *= mainCanvasSize.y;
pos = pos.multiply(mainCanvasSize);
const context = mainContext;
context.lineCap = context.lineJoin = 'round';
context.font = `${style} ${weight} ${size}px ${font}`;
context.textAlign = textAlign;
const shadowOffset = size*shadowScale;
context.fillStyle = rgb(0,0,0,color.a);
if (shadowOffset)
context.fillText(text, pos.x+shadowOffset, pos.y+shadowOffset, width);
context.lineWidth = outline;
outline && context.strokeText(text, pos.x, pos.y, width);
context.fillStyle = color;
context.fillText(text, pos.x, pos.y, width);
}

View File

@@ -0,0 +1,36 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Race Game</title>
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
}
</style>
</head>
<body>
<script src="./release.js"></script>
<script src="./utilities.js"></script>
<script src="./audio.js"></script>
<script src="./draw.js"></script>
<script src="./game.js"></script>
<script src="./generative.js"></script>
<script src="./hud.js"></script>
<script src="./input.js"></script>
<script src="./levels.js"></script>
<script src="./scene.js"></script>
<script src="./sounds.js"></script>
<script src="./track.js"></script>
<script src="./trackGen.js"></script>
<script src="./vehicle.js"></script>
<script src="./webgl.js"></script>
<script src="./main.js"></script>
</body>
</html>

402
vue/public/race/input.js Normal file
View File

@@ -0,0 +1,402 @@
'use strict';
const gamepadsEnable = enhancedMode;
const inputWASDEmulateDirection = enhancedMode;
const allowTouch = enhancedMode;
const isTouchDevice = allowTouch && window.ontouchstart !== undefined;
const touchGamepadEnable = enhancedMode;
const touchGamepadAlpha = .3;
///////////////////////////////////////////////////////////////////////////////
// Input user functions
const keyIsDown = (key) => inputData[key] & 1;
const keyWasPressed = (key) => inputData[key] & 2 ? 1 : 0;
const keyWasReleased = (key) => inputData[key] & 4 ? 1 : 0;
const clearInput = () => inputData = [];
let mousePos = vec3();
const mouseIsDown = keyIsDown;
const mouseWasPressed = keyWasPressed;
const mouseWasReleased = keyWasReleased;
let isUsingGamepad;
const gamepadIsDown = (key, gamepad=0) => !!(gamepadData[gamepad][key] & 1);
const gamepadWasPressed = (key, gamepad=0) => !!(gamepadData[gamepad][key] & 2);
const gamepadWasReleased = (key, gamepad=0) => !!(gamepadData[gamepad][key] & 4);
const gamepadStick = (stick, gamepad=0) =>
gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec3() : vec3();
const gamepadGetValue = (key, gamepad=0) => gamepadDataValues[gamepad][key];
///////////////////////////////////////////////////////////////////////////////
// Input event handlers
let inputData = []; // track what keys are down
function inputInit()
{
if (gamepadsEnable)
{
gamepadData = [];
gamepadStickData = [];
gamepadDataValues = [];
gamepadData[0] = [];
gamepadDataValues[0] = [];
}
onkeydown = (e)=>
{
isUsingGamepad = 0;
if (!e.repeat)
{
inputData[e.code] = 3;
if (inputWASDEmulateDirection)
inputData[remapKey(e.code)] = 3;
}
}
onkeyup = (e)=>
{
inputData[e.code] = 4;
if (inputWASDEmulateDirection)
inputData[remapKey(e.code)] = 4;
}
// mouse event handlers
onmousedown = (e)=>
{
isUsingGamepad = 0;
inputData[e.button] = 3;
mousePos = mouseToScreen(vec3(e.x,e.y));
}
onmouseup = (e)=> inputData[e.button] = inputData[e.button] & 2 | 4;
onmousemove = (e)=>
{
mousePos = mouseToScreen(vec3(e.x,e.y));
if (freeCamMode)
{
mouseDelta.x += e.movementX/mainCanvasSize.x;
mouseDelta.y += e.movementY/mainCanvasSize.y;
}
}
oncontextmenu = (e)=> false; // prevent right click menu
// handle remapping wasd keys to directions
const remapKey = (c) => inputWASDEmulateDirection ?
c == 'KeyW' ? 'ArrowUp' :
c == 'KeyS' ? 'ArrowDown' :
c == 'KeyA' ? 'ArrowLeft' :
c == 'KeyD' ? 'ArrowRight' : c : c;
// init touch input
isTouchDevice && touchInputInit();
}
function inputUpdate()
{
// clear input when lost focus (prevent stuck keys)
isTouchDevice || document.hasFocus() || clearInput();
gamepadsEnable && gamepadsUpdate();
}
function inputUpdatePost()
{
// clear input to prepare for next frame
for (const i in inputData)
inputData[i] &= 1;
}
// convert a mouse position to screen space
const mouseToScreen = (mousePos) =>
{
if (!clampAspectRatios)
{
// canvas always takes up full screen
return vec3(mousePos.x/mainCanvasSize.x,mousePos.y/mainCanvasSize.y);
}
else
{
const rect = mainCanvas.getBoundingClientRect();
return vec3(percent(mousePos.x, rect.left, rect.right), percent(mousePos.y, rect.top, rect.bottom));
}
}
///////////////////////////////////////////////////////////////////////////////
// gamepad input
// gamepad internal variables
let gamepadData, gamepadStickData, gamepadDataValues;
// gamepads are updated by engine every frame automatically
function gamepadsUpdate()
{
const applyDeadZones = (v)=>
{
const min=.2, max=.8;
const deadZone = (v)=>
v > min ? percent( v, min, max) :
v < -min ? -percent(-v, min, max) : 0;
return vec3(deadZone(v.x), deadZone(-v.y)).clampLength();
}
// update touch gamepad if enabled
isTouchDevice && touchGamepadUpdate();
// return if gamepads are disabled or not supported
if (!navigator || !navigator.getGamepads)
return;
// only poll gamepads when focused or in debug mode (allow playing when not focused in debug)
if (!devMode && !document.hasFocus())
return;
// poll gamepads
const gamepads = navigator.getGamepads();
for (let i = gamepads.length; i--;)
{
// get or create gamepad data
const gamepad = gamepads[i];
const data = gamepadData[i] || (gamepadData[i] = []);
const dataValue = gamepadDataValues[i] || (gamepadDataValues[i] = []);
const sticks = gamepadStickData[i] || (gamepadStickData[i] = []);
if (gamepad)
{
// read analog sticks
for (let j = 0; j < gamepad.axes.length-1; j+=2)
sticks[j>>1] = applyDeadZones(vec3(gamepad.axes[j],gamepad.axes[j+1]));
// read buttons
for (let j = gamepad.buttons.length; j--;)
{
const button = gamepad.buttons[j];
const wasDown = gamepadIsDown(j,i);
data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
dataValue[j] = percent(button.value||0,.1,.9); // apply deadzone
isUsingGamepad ||= !i && button.pressed;
}
const gamepadDirectionEmulateStick = 1;
if (gamepadDirectionEmulateStick)
{
// copy dpad to left analog stick when pressed
const dpad = vec3(
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
if (dpad.lengthSquared())
sticks[0] = dpad.clampLength();
}
}
}
}
///////////////////////////////////////////////////////////////////////////////
// touch input
// try to enable touch mouse
function touchInputInit()
{
// add non passive touch event listeners
let handleTouch = handleTouchDefault;
if (touchGamepadEnable)
{
// touch input internal variables
handleTouch = handleTouchGamepad;
touchGamepadButtons = [];
touchGamepadStick = vec3();
}
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
// override mouse events
onmousedown = onmouseup = ()=> 0;
// handle all touch events the same way
let wasTouching;
function handleTouchDefault(e)
{
// fix stalled audio requiring user interaction
if (soundEnable && !audioContext)
audioContext = new AudioContext; // create audio context
//if (soundEnable && audioContext && audioContext.state != 'running')
// sound_bump.play(); // play sound to fix audio
// check if touching and pass to mouse events
const touching = e.touches.length;
const button = 0; // all touches are left mouse button
if (touching)
{
// average all touch positions
const p = vec3();
for (let touch of e.touches)
{
p.x += touch.clientX/e.touches.length;
p.y += touch.clientY/e.touches.length;
}
mousePos = mouseToScreen(p);
wasTouching ? 0 : inputData[button] = 3;
}
else if (wasTouching)
inputData[button] = inputData[button] & 2 | 4;
// set was touching
wasTouching = touching;
// prevent default handling like copy and magnifier lens
if (document.hasFocus()) // allow document to get focus
e.preventDefault();
// must return true so the document will get focus
return true;
}
}
///////////////////////////////////////////////////////////////////////////////
// touch gamepad
// touch gamepad internal variables
let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick, touchGamepadSize;
// special handling for virtual gamepad mode
function handleTouchGamepad(e)
{
if (soundEnable)
{
if (!audioContext)
audioContext = new AudioContext; // create audio context
// fix stalled audio
if (audioContext.state != 'running')
audioContext.resume();
}
// clear touch gamepad input
touchGamepadStick = vec3();
touchGamepadButtons = [];
isUsingGamepad = true;
const touching = e.touches.length;
if (touching)
{
touchGamepadTimer.set();
if (paused || titleScreenMode || gameOverTimer.isSet())
{
// touch anywhere to press start
touchGamepadButtons[9] = 1;
return;
}
}
// get center of left and right sides
const stickCenter = vec3(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
const buttonCenter = mainCanvasSize.subtract(vec3(touchGamepadSize, touchGamepadSize));
const startCenter = mainCanvasSize.scale(.5);
// check each touch point
for (const touch of e.touches)
{
let touchPos = mouseToScreen(vec3(touch.clientX, touch.clientY));
touchPos = touchPos.multiply(mainCanvasSize);
if (touchPos.distance(stickCenter) < touchGamepadSize)
{
// virtual analog stick
touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize);
//touchGamepadStick = touchGamepadStick.clampLength(); // circular clamp
touchGamepadStick.x = clamp(touchGamepadStick.x,-1,1);
touchGamepadStick.y = clamp(touchGamepadStick.y,-1,1);
}
else if (touchPos.distance(buttonCenter) < touchGamepadSize)
{
// virtual face buttons
const button = touchPos.y > buttonCenter.y ? 1 : 0;
touchGamepadButtons[button] = 1;
}
else if (touchPos.distance(startCenter) < touchGamepadSize)
{
// hidden virtual start button in center
touchGamepadButtons[9] = 1;
}
}
// call default touch handler so normal touch events still work
//handleTouchDefault(e);
// prevent default handling like copy and magnifier lens
if (document.hasFocus()) // allow document to get focus
e.preventDefault();
// must return true so the document will get focus
return true;
}
// update the touch gamepad, called automatically by the engine
function touchGamepadUpdate()
{
if (!touchGamepadEnable)
return;
// adjust for thin canvas
touchGamepadSize = clamp(mainCanvasSize.y/8, 99, mainCanvasSize.x/2);
ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
if (!touchGamepadTimer.isSet())
return;
// read virtual analog stick
const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
sticks[0] = touchGamepadStick.copy();
// read virtual gamepad buttons
const data = gamepadData[0];
for (let i=10; i--;)
{
const wasDown = gamepadIsDown(i,0);
data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
}
}
// render the touch gamepad, called automatically by the engine
function touchGamepadRender()
{
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
return;
// fade off when not touching or paused
const alpha = percent(touchGamepadTimer.get(), 4, 3);
if (!alpha || paused)
return;
// setup the canvas
const context = mainContext;
context.save();
context.globalAlpha = alpha*touchGamepadAlpha;
context.strokeStyle = '#fff';
context.lineWidth = 3;
// draw left analog stick
context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
context.beginPath();
// draw circle shaped gamepad
const leftCenter = vec3(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
context.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
context.fill();
context.stroke();
// draw right face buttons
const rightCenter = vec3(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
for (let i=2; i--;)
{
const pos = rightCenter.add(vec3(0,(i?1:-1)*touchGamepadSize/2));
context.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
context.beginPath();
context.arc(pos.x, pos.y, touchGamepadSize/3, 0, 9);
context.fill();
context.stroke();
}
// set canvas back to normal
context.restore();
}

447
vue/public/race/levels.js Normal file
View File

@@ -0,0 +1,447 @@
'use strict';
let levelInfoList;
function initLevelInfos()
{
levelInfoList = [];
let LI, level=0;
// Level 1 - beach -
LI = new LevelInfo(level++, [
spriteList.grass_plain,
spriteList.tree_palm,
spriteList.rock_big,
], spriteList.tree_palm);
LI.horizonSpriteSize = .7;
LI.waterSide = -1;
//LI.tunnel = spriteList.tunnel2; // test tunnel
LI.billboardChance = .3 // more billboards at start
//LI.trafficDensity = .7; // less traffic start
// mostly straight with few well defined turns or bumps
LI.turnChance = .6;
LI.turnMin = .2;
//LI.turnMax = .6;
//LI.bumpChance = .5;
LI.bumpFreqMin = .2;
LI.bumpFreqMax = .4;
LI.bumpScaleMin = 10;
LI.bumpScaleMax = 20;
// Level 2 - forest -
LI = new LevelInfo(level++, [
spriteList.tree_oak,
spriteList.grass_plain,
spriteList.tree_bush,
spriteList.tree_stump,
spriteList.grass_flower1,
spriteList.grass_flower3,
spriteList.grass_flower2,
], spriteList.tree_bush, spriteList.horizon_smallMountains);
LI.horizonSpriteSize = 10;
LI.trackSideRate = 10;
LI.sceneryListBias = 9;
//LI.skyColorTop = WHITE;
LI.skyColorBottom = hsl(.5,.3,.5);
LI.roadColor = hsl(.05,.4,.2);
LI.groundColor = hsl(.2,.4,.4);
LI.cloudColor = hsl(0,0,1,.3);
LI.cloudHeight = .2;
LI.sunHeight = .7;
LI.billboardChance = .1 // less billboards in forest type areas
//LI.trafficDensity = .7; // less traffic in forest
// trail through forest
LI.turnChance = .7; // more small turns
//LI.turnMin = 0;
//LI.turnMax = .6;
LI.bumpChance = .8;
LI.bumpFreqMin = .4;
//LI.bumpFreqMax = .7;
//LI.bumpScaleMin = 50;
LI.bumpScaleMax = 140;
// Level 3 - desert -
// has long straight thin roads and tunnel
LI = new LevelInfo(level++, [
spriteList.grass_dead,
spriteList.tree_dead,
spriteList.rock_big,
spriteList.tree_stump,
], spriteList.telephonePole, spriteList.horizon_desert);
LI.trackSideRate = 50;
LI.trackSideChance = 1;
LI.skyColorTop = hsl(.15,1,.9);
LI.skyColorBottom = hsl(.5,.7,.6);
LI.roadColor = hsl(.1,.2,.2);
LI.lineColor = hsl(0,0,1,.5);
LI.groundColor = hsl(.1,.2,.5);
LI.trackSideForce = 1; // telephone poles on right side
LI.cloudHeight = .05;
LI.sunHeight = .9;
LI.sideStreets = 1;
LI.laneCount = 2;
LI.hazardType = spriteList.hazard_sand;
LI.hazardChance = .005;
LI.tunnel = spriteList.tunnel2;
LI.trafficDensity = .7; // less traffic in desert, only 2 lanes
LI.billboardRate = 87;
LI.billboardScale = 8;
// flat desert
//LI.turnChance = .5;
LI.turnMin = .2;
LI.turnMax = .6;
LI.bumpChance = 1;
//LI.bumpFreqMin = 0;
LI.bumpFreqMax = .2;
LI.bumpScaleMin = 30;
LI.bumpScaleMax = 60;
// Level 4 - snow area -
LI = new LevelInfo(level++, [
spriteList.grass_snow,
spriteList.tree_dead,
spriteList.tree_snow,
spriteList.rock_big,
spriteList.tree_stump,
], spriteList.tree_snow, spriteList.horizon_snow);
LI.sceneryListBias = 9;
LI.trackSideRate = 21;
LI.skyColorTop = hsl(.5,.2,.4);
LI.skyColorBottom = WHITE;
LI.roadColor = hsl(0,0,.5,.5);
LI.groundColor = hsl(.6,.3,.9);
LI.cloudColor = hsl(0,0,.8,.5);
LI.horizonSpriteSize = 2;
LI.lineColor = hsl(0,0,1,.5);
LI.sunHeight = .7;
LI.hazardType = spriteList.hazard_rocks;
LI.hazardChance = .002;
LI.trafficDensity = 1.2; // extra traffic through snow
// snowy mountains
//LI.turnChance = .5;
LI.turnMin = .4;
//LI.turnMax = .6;
LI.bumpChance = .8;
LI.bumpFreqMin = .2;
LI.bumpFreqMax = .6;
//LI.bumpFreqMax = .7;
LI.bumpScaleMin = 50;
LI.bumpScaleMax = 100;
// Level 5 - canyon -
// has winding roads, hills, and sand onground
LI = new LevelInfo(level++, [
spriteList.rock_huge,
spriteList.grass_dead,
spriteList.tree_fall,
spriteList.rock_huge2,
spriteList.grass_flower2,
spriteList.tree_dead,
spriteList.tree_stump,
spriteList.rock_big,
], spriteList.tree_fall,spriteList.horizon_brownMountains);
LI.sceneryListBias = 2;
LI.trackSideRate = 31;
LI.skyColorTop = hsl(.7,1,.7);
LI.skyColorBottom = hsl(.2,1,.9);
LI.roadColor = hsl(0,0,.15);
LI.groundColor = hsl(.1,.4,.5);
LI.cloudColor = hsl(0,0,1,.3);
LI.cloudHeight = .1;
LI.sunColor = hsl(0,1,.7);
//LI.laneCount = 3;
LI.billboardChance = .1 // less billboards in forest type areas
LI.trafficDensity = .7; // less traffic in canyon
// rocky canyon
LI.turnChance = 1; // must turn to block vision
LI.turnMin = .2;
LI.turnMax = .8;
LI.bumpChance = .9;
LI.bumpFreqMin = .4;
//LI.bumpFreqMax = .7;
//LI.bumpScaleMin = 50;
LI.bumpScaleMax = 120;
// Level 6 - red fields and city
LI = new LevelInfo(level++, [
spriteList.grass_red,
spriteList.tree_yellow,
spriteList.rock_big,
spriteList.tree_stump,
//spriteList.rock_wide,
], spriteList.tree_yellow,spriteList.horizon_city);
LI.trackSideRate = 31;
LI.skyColorTop = YELLOW;
LI.skyColorBottom = RED;
LI.roadColor = hsl(0,0,.1);
LI.lineColor = hsl(.15,1,.7);
LI.groundColor = hsl(.05,.5,.4);
LI.cloudColor = hsl(.15,1,.5,.5);
//LI.cloudHeight = .3;
LI.billboardRate = 23; // more billboards in city
LI.billboardChance = .5
LI.horizonSpriteSize = 1.5;
if (!js13kBuildLevel2)
LI.horizonFlipChance = .3;
LI.sunHeight = .5;
LI.sunColor = hsl(.15,1,.8);
LI.sideStreets = 1;
LI.laneCount = 5;
LI.trafficDensity = 2; // extra traffic in city
// in front of city
LI.turnChance = .3;
LI.turnMin = .5
LI.turnMax = .9; // bigger turns since lanes are wide
//LI.bumpChance = .5;
LI.bumpFreqMin = .3;
LI.bumpFreqMax = .6;
LI.bumpScaleMin = 80;
LI.bumpScaleMax = 200;
// Level 7 - graveyard -
LI = new LevelInfo(level++, [
spriteList.grass_dead,
spriteList.grass_plain,
spriteList.grave_stone,
spriteList.tree_dead,
spriteList.tree_stump,
], spriteList.tree_oak, spriteList.horizon_graveyard);
LI.sceneryListBias = 2;
LI.trackSideRate = 50;
LI.skyColorTop = hsl(.5,1,.5);
LI.skyColorBottom = hsl(0,1,.8);
LI.roadColor = hsl(.6,.3,.15);
LI.groundColor = hsl(.2,.3,.5);
LI.lineColor = hsl(0,0,1,.5);
LI.billboardChance = 0; // no ads in graveyard
LI.cloudColor = hsl(.15,1,.9,.3);
LI.horizonSpriteSize = 4;
LI.sunHeight = 1.5;
//LI.laneCount = 3;
//LI.trafficDensity = .7;
LI.trackSideChance = 1; // more trees
// thin road over hills in graveyard
//LI.turnChance = .5;
LI.turnMax = .6;
LI.bumpChance = .6;
LI.bumpFreqMin = LI.bumpFreqMax = .7;
LI.bumpScaleMin = 80;
//LI.bumpScaleMax = 150;
// Level 8 - jungle - dirt road, many trees
// has lots of physical hazards
LI = new LevelInfo(level++, [
spriteList.grass_large,
spriteList.tree_palm,
spriteList.grass_flower1,
spriteList.rock_tall,
spriteList.rock_big,
spriteList.rock_huge2,
], spriteList.rock_big, spriteList.horizon_redMountains);
LI.sceneryListBias = 5;
LI.trackSideRate = 25;
LI.skyColorTop = hsl(0,1,.8);
LI.skyColorBottom = hsl(.6,1,.6);
LI.lineColor = hsl(0,0,0,0);
LI.roadColor = hsl(0,.6,.2,.8);
LI.groundColor = hsl(.1,.5,.4);
LI.waterSide = 1;
LI.cloudColor = hsl(0,1,.96,.8);
LI.cloudWidth = .6;
//LI.cloudHeight = .3;
LI.sunHeight = .7;
LI.sunColor = hsl(.1,1,.7);
LI.hazardType = spriteList.rock_big;
LI.hazardChance = .2;
LI.trafficDensity = 0; // no other cars in jungle
// bumpy jungle road
LI.turnChance = .8;
//LI.turnMin = 0;
LI.turnMax = .3; // lots of slight turns
LI.bumpChance = 1;
LI.bumpFreqMin = .4;
LI.bumpFreqMax = .6;
LI.bumpScaleMin = 10;
LI.bumpScaleMax = 80;
// Level 9 - strange area
LI = new LevelInfo(level++, [
spriteList.grass_red,
spriteList.rock_weird,
spriteList.tree_huge,
], spriteList.rock_weird2, spriteList.horizon_weird);
LI.trackSideRate = 50;
LI.skyColorTop = hsl(.05,1,.8);
LI.skyColorBottom = hsl(.15,1,.7);
LI.lineColor = hsl(0,1,.9);
LI.roadColor = hsl(.6,1,.1);
LI.groundColor = hsl(.6,1,.6);
LI.cloudColor = hsl(.9,1,.5,.3);
LI.cloudHeight = .2;
LI.sunColor = BLACK;
LI.laneCount = 4;
LI.trafficDensity = 1.5; // extra traffic to increase difficulty here
// large strange hills
LI.turnChance = .7;
LI.turnMin = .3;
LI.turnMax = .8;
LI.bumpChance = 1;
LI.bumpFreqMin = .5;
LI.bumpFreqMax = .9;
LI.bumpScaleMin = 100;
LI.bumpScaleMax = 200;
// Level 10 - mountains - hilly, rocks on sides
LI = new LevelInfo(level++, [
spriteList.grass_plain,
spriteList.rock_huge3,
spriteList.grass_flower1,
spriteList.rock_huge2,
spriteList.rock_huge,
], spriteList.tree_pink);
LI.trackSideRate = 21;
LI.skyColorTop = hsl(.2,1,.9);
LI.skyColorBottom = hsl(.55,1,.5);
LI.roadColor = hsl(0,0,.1);
LI.groundColor = hsl(.1,.5,.7);
LI.cloudColor = hsl(0,0,1,.5);
LI.tunnel = spriteList.tunnel1;
if (js13kBuildLevel2)
LI.horizonSpriteSize = 0;
else
{
LI.sunHeight = .6;
LI.horizonSprite = spriteList.horizon_mountains
LI.horizonSpriteSize = 1;
}
// mountains, most difficult level
LI.turnChance = LI.turnMax = .8;
//LI.turnMin = 0;
LI.bumpChance = 1;
LI.bumpFreqMin = .3;
LI.bumpFreqMax = .9;
//LI.bumpScaleMin = 50;
LI.bumpScaleMax = 80;
// Level 11 - win area
LI = new LevelInfo(level++, [
spriteList.grass_flower1,
spriteList.grass_flower2,
spriteList.grass_flower3,
spriteList.grass_plain,
spriteList.tree_oak,
spriteList.tree_bush,
], spriteList.tree_oak);
LI.sceneryListBias = 1;
LI.groundColor = hsl(.2,.3,.5);
LI.trackSideRate = LI.billboardChance = 0;
LI.bumpScaleMin = 1e3; // hill in the distance
// match settings to previous level
if (js13kBuildLevel2)
LI.horizonSpriteSize = 0;
else
{
LI.sunHeight = .6;
LI.horizonSprite = spriteList.horizon_mountains
LI.horizonSpriteSize = 1;
}
}
const getLevelInfo = (level) => testLevelInfo || levelInfoList[level|0] || levelInfoList[0];
// info about how to build and draw each level
class LevelInfo
{
constructor(level, scenery, trackSideSprite,horizonSprite=spriteList.horizon_islands)
{
// add self to list
levelInfoList[level] = this;
if (debug)
{
for(const s of scenery)
ASSERT(s, 'missing scenery!');
}
this.level = level;
this.scenery = scenery;
this.trackSideSprite = trackSideSprite;
this.sceneryListBias = 29;
this.waterSide = 0;
this.billboardChance = .2;
this.billboardRate = 45;
this.billboardScale = 1;
this.trackSideRate = 5;
this.trackSideForce = 0;
this.trackSideChance = .5;
this.groundColor = hsl(.08,.2, .7);
this.skyColorTop = WHITE;
this.skyColorBottom = hsl(.57,1,.5);
this.lineColor = WHITE;
this.roadColor = hsl(0, 0, .5);
// horizon stuff
this.cloudColor = hsl(.15,1,.95,.7);
this.cloudWidth = 1;
this.cloudHeight = .3;
this.horizonSprite = horizonSprite;
this.horizonSpriteSize = 2;
this.sunHeight = .8;
this.sunColor = hsl(.15,1,.95);
// track generation
this.laneCount = 3;
this.trafficDensity = 1;
// default turns and bumps
this.turnChance = .5;
this.turnMin = 0;
this.turnMax = .6;
this.bumpChance = .5;
this.bumpFreqMin = 0; // no bumps
this.bumpFreqMax = .7; // more often bumps
this.bumpScaleMin = 50; // rapid bumps
this.bumpScaleMax = 150; // largest hills
}
randomize()
{
shuffle(this.scenery);
this.sceneryListBias = random.float(5,30);
this.groundColor = random.mutateColor(this.groundColor);
this.skyColorTop = random.mutateColor(this.skyColorTop);
this.skyColorBottom = random.mutateColor(this.skyColorBottom);
this.lineColor = random.mutateColor(this.lineColor);
this.roadColor = random.mutateColor(this.roadColor);
this.cloudColor = random.mutateColor(this.cloudColor);
this.sunColor = random.mutateColor(this.sunColor);
// track generation
this.laneCount = random.int(2,5);
this.trafficDensity = random.float(.5,1.5);
// default turns and bumps
this.turnChance = random.float();
this.turnMin = random.float();
this.turnMax = random.float();
this.bumpChance = random.float();
this.bumpFreqMin = random.float(.5); // no bumps
this.bumpFreqMax = random.float(); // more often bumps
this.bumpScaleMin = random.float(20,50); // rapid bumps
this.bumpScaleMax = random.float(50,150); // largest hills
this.hazardChance = 0;
}
}

41
vue/public/race/main.js Normal file
View File

@@ -0,0 +1,41 @@
'use strict';
/*
Dr1v3n Wild by Frank Force
A 13k game for js13kGames 2024
Controls
- Arrows or Mouse = Drive
- Spacebar = Brake
- F = Free Ride Mode
- Escape = Title Screen
Features
- 10 stages with unique visuals
- Fast custom WebGL rendering
- Procedural art (trees, rocks, scenery)
- Track generator
- Arcade style driving physics
- 2 types of AI vehicles
- Parallax horizon and sky
- ZZFX sounds
- Persistent save data
- Keyboard or mouse input
- All written from scratch in vanilla JS
*/
///////////////////////////////////////////////////
// debug settings
//devMode = debugInfo = 1
//soundVolume = 0
//debugGenerativeCanvas = 1
//autoPause = 0
//quickStart = 1
//disableAiVehicles = 1
///////////////////////////////////////////////////
gameInit();

View File

@@ -0,0 +1,16 @@
'use strict';
const debug = 0;
const enhancedMode = 1;
let debugInfo, debugMesh, debugTile, debugGenerativeCanvas, devMode;
const js13kBuildLevel2 = 0; // more space is needed for js13k
// disable debug features
function ASSERT() {}
function debugInit() {}
function drawDebug() {}
function debugUpdate() {}
function debugSaveCanvas() {}
function debugSaveText() {}
function debugDraw() {}
function debugSaveDataURL() {}

View File

@@ -0,0 +1,15 @@
'use strict';
const debug = 0;
let debugInfo, debugMesh, debugTile, debugGenerativeCanvas, devMode, enhancedMode;
const js13kBuildLevel2 = 1; // more space is needed for js13k
// disable debug features
function ASSERT() {}
function debugInit() {}
function drawDebug() {}
function debugUpdate() {}
function debugSaveCanvas() {}
function debugSaveText() {}
function debugDraw() {}
function debugSaveDataURL() {}

120
vue/public/race/scene.js Normal file
View File

@@ -0,0 +1,120 @@
'use strict';
function drawScene()
{
drawSky();
drawTrack();
drawCars();
drawTrackScenery();
}
function drawSky()
{
glEnableLighting = glEnableFog = 0;
glSetDepthTest(0,0);
random.setSeed(13);
// lerp level stuff
const levelFloat = cameraOffset/checkpointDistance;
const levelInfo = getLevelInfo(levelFloat);
const levelInfoLast = getLevelInfo(levelFloat-1);
const levelPercent = levelFloat%1;
const levelLerpPercent = percent(levelPercent, 0, levelLerpRange);
// sky
const skyTop = 13e2; // slightly above camera
const skyZ = 1e3;
const skyW = 5e3;
const skyH = 8e2;
{
// top/bottom gradient
const skyColorTop = levelInfoLast.skyColorTop.lerp(levelInfo.skyColorTop, levelLerpPercent);
const skyColorBottom = levelInfoLast.skyColorBottom.lerp(levelInfo.skyColorBottom, levelLerpPercent);
pushGradient(vec3(0,skyH,skyZ).addSelf(cameraPos), vec3(skyW,skyH), skyColorTop, skyColorBottom);
// light settings from sky
glLightDirection = vec3(0,1,1).rotateY(worldHeading).normalize();
glLightColor = skyColorTop.lerp(WHITE,.8).lerp(BLACK,.1);
glAmbientColor = skyColorBottom.lerp(WHITE,.8).lerp(BLACK,.6);
glFogColor = skyColorBottom.lerp(WHITE,.5);
}
const headingScale = -5e3;
const circleSpriteTile = spriteList.circle.spriteTile;
const dotSpriteTile = spriteList.dot.spriteTile;
{
// sun
const sunSize = 2e2;
const sunHeight = skyTop*lerp(levelLerpPercent, levelInfoLast.sunHeight, levelInfo.sunHeight);
const sunColor = levelInfoLast.sunColor.lerp(levelInfo.sunColor, levelLerpPercent);
const x = mod(worldHeading+PI,2*PI)-PI;
for(let i=0;i<1;i+=.05)
{
sunColor.a = i?(1-i)**7:1;
pushSprite(vec3( x*headingScale, sunHeight, skyZ).addSelf(cameraPos), vec3(sunSize*(1+i*30)), sunColor, i?dotSpriteTile:circleSpriteTile);
}
}
// clouds
const range = 1e4;
const windSpeed = 50;
for(let i=99;i--;)
{
const cloudColor = levelInfoLast.cloudColor.lerp(levelInfo.cloudColor, levelLerpPercent);
const cloudWidth = lerp(levelLerpPercent, levelInfoLast.cloudWidth, levelInfo.cloudWidth);
const cloudHeight = lerp(levelLerpPercent, levelInfoLast.cloudHeight, levelInfo.cloudHeight);
let x = worldHeading*headingScale + random.float(range) + time*windSpeed*random.float(1,1.5);
x = mod(x,range) - range/2;
const y = random.float(skyTop);
const s = random.float(3e2,8e2);
pushSprite(vec3( x, y, skyZ).addSelf(cameraPos), vec3(s*cloudWidth,s*cloudHeight), cloudColor, dotSpriteTile)
}
// parallax
const horizonSprite = levelInfo.horizonSprite;
const horizonSpriteTile = horizonSprite.spriteTile;
const horizonSpriteSize = levelInfo.horizonSpriteSize;
for(let i=99;i--;)
{
const p = i/99;
const ltp = lerp(p,1,.5);
const ltt = .1;
const levelTransition = levelFloat<.5 || levelFloat > levelGoal-.5 ? 1 : levelPercent < ltt ? (levelPercent/ltt)**ltp :
levelPercent > 1-ltt ? 1-((levelPercent-1)/ltt+1)**ltp : 1;
const parallax = lerp(p, .9, .98);
const s = random.float(1e2,2e2)*horizonSpriteSize* lerp(p,1,.5)
const size = vec3(random.float(1,2)*(horizonSprite.canMirror ? s*random.sign() : s),s,s);
const x = mod(worldHeading*headingScale/parallax + random.float(range),range) - range/2;
const yMax = size.y*.75;
if (!js13kBuildLevel2 && levelInfo.horizonFlipChance)
{
// horizon spites that can be flipped vertically
if (random.bool(levelInfo.horizonFlipChance))
size.y *= -1;
}
const y = lerp(levelTransition, -yMax*1.5, yMax);
const c = horizonSprite.getRandomSpriteColor();
pushSprite(vec3( x, y, skyZ).addSelf(cameraPos), size, c, horizonSpriteTile);
}
{
// get ahead of player for horizon ground color to match track
const lookAhead = .2;
const levelFloatAhead = levelFloat + lookAhead;
const levelInfo = getLevelInfo(levelFloatAhead);
const levelInfoLast = getLevelInfo(levelFloatAhead-1);
const levelPercent = levelFloatAhead%1;
const levelLerpPercent = percent(levelPercent, 0, levelLerpRange);
// horizon bottom
const groundColor = levelInfoLast.groundColor.lerp(levelInfo.groundColor, levelLerpPercent).brighten(.1);
pushSprite(vec3(0,-skyH,skyZ).addSelf(cameraPos), vec3(skyW,skyH), groundColor);
}
glRender();
glSetDepthTest();
glEnableLighting = glEnableFog = 1;
}

View File

@@ -0,0 +1,9 @@
'use strict';
const sound_beep = new Sound([,0,220,.01,.08,.05,,.5,,,,,,,,,.3,.9,.01,,-99]); // beep
const sound_engine = new Sound([,,40,.2,.5,.5,,,,,,,,300,,,,,,,-80]); // engine
const sound_hit = new Sound([,.3,90,,,.2,,3,,,,,,9,,.3,,.3,.01]); // crash
const sound_bump = new Sound([4,.2,400,.01,.01,.01,,.8,-60,-70,,,.03,.1,,,.1,.5,.01,.4,400]); // bump
const sound_checkpoint = new Sound([.3,0,980,,,,,3,,,,,,,,.03,,,,,500]); // checkpoint
const sound_win = new Sound([1.5,,110,.04,,2,,6,,1,330,.07,.05,,,,.4,.8,,.5,1e3]); // win
const sound_lose = new Sound([,,120,.1,,1,,3,,.6,,,,1,,.2,.4,.1,1,,500]); // lose

425
vue/public/race/track.js Normal file
View File

@@ -0,0 +1,425 @@
'use strict';
function trackPreUpdate()
{
// calcuate track x offsets and projections (iterate in reverse)
const cameraTrackInfo = new TrackSegmentInfo(cameraOffset);
const cameraTrackSegment = cameraTrackInfo.segmentIndex;
const cameraTrackSegmentPercent = cameraTrackInfo.percent;
const turnScale = 2;
for(let x=0, v=0, i=0; i<drawDistance; ++i)
{
const j = cameraTrackSegment+i;
if (!track[j])
continue;
// create track world position
const s = i < 1 ? 1-cameraTrackSegmentPercent : 1;
track[j].pos = track[j].offset.copy();
track[j].pos.x = x += v += turnScale*s*track[j].pos.x;
track[j].pos.z -= cameraOffset;
}
}
function drawTrack()
{
glEnableFog = 0; // track looks better without fog
drawRoad(1); // first draw just flat ground with z write
glSetDepthTest(0,0); // disable z testing
drawRoad(); // draw ground and road
// set evertyhing back to normal
glEnableFog = 1;
glSetDepthTest();
}
function drawRoad(zwrite)
{
// draw the road segments
const drawLineDistance = 500;
const cameraTrackInfo = new TrackSegmentInfo(cameraOffset);
const cameraTrackSegment = cameraTrackInfo.segmentIndex;
for(let i = drawDistance, segment1, segment2; i--; )
{
const segmentIndex = cameraTrackSegment+i;
segment1 = track[segmentIndex];
if (!segment1 || !segment2)
{
segment2 = segment1;
continue;
}
if (i % (lerp(i/drawDistance,1,8)|0)) // fade in road resolution
continue;
const p1 = segment1.pos;
const p2 = segment2.pos;
const normals = [segment1.normal, segment1.normal, segment2.normal, segment2.normal];
function pushRoadVerts(width, color, offset=0, width2=width, offset2=offset, oy=0)
{
const point1a = vec3(p1.x+width+offset, p1.y+oy, p1.z);
const point1b = vec3(p1.x-width+offset, p1.y+oy, p1.z);
const point2a = vec3(p2.x+width2+offset2, p2.y+oy, p2.z);
const point2b = vec3(p2.x-width2+offset2, p2.y+oy, p2.z);
const poly = [point1a, point1b, point2a, point2b];
color.a && glPushVertsCapped(poly, normals, color);
}
{
// ground
const color = segment1.colorGround;
const width = 1e5; // fill the width of the screen
pushRoadVerts(width, color);
}
if (!zwrite)
{
const roadHeight = 10;
// road
const color = segment1.colorRoad;
const width = segment1.width;
const width2 = segment2.width;
pushRoadVerts(width, color, undefined, width2,undefined,roadHeight);
if (i < drawLineDistance)
{
// lines on road
const w = segment1.width;
const lineBias = .2
const laneCount = 2*w/laneWidth - lineBias;
for(let j=1; j<laneCount; ++j)
{
const color = segment1.colorLine;
const lineWidth = 30;
const offset = j*laneWidth-segment1.width;
const offset2 = j*laneWidth-segment2.width;
pushRoadVerts(lineWidth, color, offset, undefined, offset2,roadHeight);
}
}
}
segment2 = segment1;
}
glRender();
}
function drawTrackScenery()
{
// this is last pass from back to front so do do not write to depth
glSetDepthTest(1, 0);
glEnableLighting = 0;
const cameraTrackInfo = new TrackSegmentInfo(cameraOffset);
const cameraTrackSegment = cameraTrackInfo.segmentIndex;
for(let i=drawDistance; i--; )
{
const segmentIndex = cameraTrackSegment+i;
const trackSegment = track[segmentIndex];
if (!trackSegment)
continue;
// draw objets for this segment
random.setSeed(trackSeed+segmentIndex);
for(const trackObject of trackSegment.trackObjects)
trackObject.draw();
// random scenery
const levelInfo = getLevelInfo(trackSegment.level);
const levelFloat = trackSegment.offset.z/checkpointDistance;
const levelInfoNext = getLevelInfo(levelFloat+1);
const levelLerpPercent = percent(levelFloat%1, 1-levelLerpRange, 1);
const w = trackSegment.width;
if (enhancedMode && trackSegment.level == 3)
{
// snow
const x = random.floatSign(1e4);
const h = 1e4;
const y = h-(random.float(h)+time*2e3)%h;
pushSprite(vec3(x + 1e3*trackSegment.getWind(),y).addSelf(trackSegment.pos), vec3(50), WHITE, spriteList.dot.spriteTile);
}
if (!trackSegment.sideStreet) // no sprites on side streets
for(let k=3;k--;)
{
const spriteSide = (segmentIndex+k)%2 ? 1 : -1;
if (spriteSide == levelInfo.waterSide)
{
// water
const sprite = spriteList.water;
const s = sprite.size*sprite.getRandomSpriteScale();
const o2 = w+random.float(12e3,8e4);
const o = spriteSide * o2;
// get taller in distance to cover horizon
const h = .4;
const wave = time-segmentIndex/70;
const p = vec3(o+2e3*Math.sin(wave),0).addSelf(trackSegment.pos);
const waveWind = 9*Math.cos(wave); // fake wind to make wave seam more alive
pushTrackObject(p, vec3(spriteSide*s,s*h,s), WHITE, sprite, waveWind);
}
else
{
// lerp in next level scenery at end
const sceneryLevelInfo = random.bool(levelLerpPercent) ? levelInfoNext : levelInfo;
// scenery on far side like grass and flowers
const sceneryList = sceneryLevelInfo.scenery;
const sceneryListBias = sceneryLevelInfo.sceneryListBias;
if (sceneryLevelInfo.scenery)
{
const sprite = random.fromList(sceneryList,sceneryListBias);
const s = sprite.size*sprite.getRandomSpriteScale();
// push farther away if big collision
const xm = w+sprite.size+6*sprite.collideScale*s;
const o = spriteSide * random.float(xm,3e4);
const p = vec3(o,0).addSelf(trackSegment.pos);
const wind = trackSegment.getWind();
const color = sprite.getRandomSpriteColor();
const scale = vec3(sprite.canMirror && random.bool() ? -s : s,s,s);
pushTrackObject(p, scale, color, sprite, wind);
}
}
}
}
glRender();
if (!js13kBuild) // final thing rendered, so no need to reset
{
glSetDepthTest();
glEnableLighting = 1;
}
}
function pushTrackObject(pos, scale, color, sprite, trackWind)
{
if (optimizedCulling)
{
const cullScale = 200;
if (cullScale*scale.y < pos.z)
return; // cull out small sprites
if (abs(pos.x)-abs(scale.x) > pos.z*4+2e3)
return; // out of view
if (pos.z < 0)
return; // behind camera
}
const shadowScale = sprite.shadowScale;
const wind = sprite.windScale * trackWind;
const yShadowOffset = freeCamMode ? cameraPos.y/20 : 10; // fix shadows in free cam mode
const spriteYOffset = scale.y*(1+sprite.spriteYOffset) + (freeCamMode?cameraPos.y/20:0);
pos.y += yShadowOffset;
if (shadowScale)
pushShadow(pos, scale.y*shadowScale, scale.y*shadowScale/6);
// draw on top of shadow
pos.y += spriteYOffset - yShadowOffset;
pushSprite(pos, scale, color, sprite.spriteTile, wind);
}
///////////////////////////////////////////////////////////////////////////////
/*function draw3DTrackScenery()
{
const cameraTrackSegment = cameraTrackInfo.segmentIndex;
// 3d scenery
for(let i=drawDistance, segment1, segment2; i--; )
{
segment2 = segment1;
const segmentIndex = cameraTrackSegment+i;
segment1 = track[segmentIndex];
if (!segment1 || !segment2)
continue;
if (segmentIndex%7)
continue
const d = segment1.pos.subtract(segment2.pos);
const heading = PI-Math.atan2(d.x, d.z);
// random scenery
random.setSeed(trackSeed+segmentIndex);
const w = segment1.width;
const o =(segmentIndex%2?1:-1)*(random.float(5e4,1e5))
const r = vec3(0,-heading,0);
const p = vec3(-o,0).addSelf(segment1.pos);
const s = vec3(random.float(500,1e3),random.float(1e3,4e3),random.float(500,1e3));
//const s = vec3(500,random.float(2e3,2e4),500);
const m4 = buildMatrix(p,r,s);
const c = hsl(0,0,random.float(.2,1));
cubeMesh.render(m4, c);
}
}
*/
///////////////////////////////////////////////////////////////////////////////
// an instance of a sprite
class TrackObject
{
constructor(trackSegment, sprite, offset, color=WHITE, sizeScale=1)
{
this.trackSegment = trackSegment;
this.sprite = sprite;
this.offset = offset;
this.color = color;
const scale = sprite.size * sizeScale;
this.scale = vec3(scale);
const trackWidth = trackSegment.width;
const trackside = offset.x < trackWidth*2 && offset.x > -trackWidth*2;
if (trackside && sprite.trackFace)
this.scale.x *= sign(offset.x);
else if (sprite.canMirror && random.bool())
this.scale.x *= -1;
this.collideSize = sprite.collideScale*abs(scale);
}
draw()
{
const trackSegment = this.trackSegment;
const pos = trackSegment.pos.add(this.offset);
const wind = trackSegment.getWind();
pushTrackObject(pos, this.scale, this.color, this.sprite, wind);
}
}
class TrackSegment
{
constructor(segmentIndex,offset,width)
{
if (segmentIndex >= levelGoal*checkpointTrackSegments)
width = 0; // no track after end
this.offset = offset;
this.width = width;
this.pitch = 0;
this.normal = vec3();
this.trackObjects = [];
const levelFloat = segmentIndex/checkpointTrackSegments;
const level = this.level = testLevelInfo ? testLevelInfo.level : levelFloat|0;
const levelInfo = getLevelInfo(level);
const levelInfoNext = getLevelInfo(levelFloat+1);
const levelLerpPercent = percent(levelFloat%1, 1-levelLerpRange, 1);
const checkpointLine = segmentIndex > 25 && segmentIndex < 30
|| segmentIndex%checkpointTrackSegments > checkpointTrackSegments-10;
const recordPoint = bestDistance/trackSegmentLength;
const recordPointLine = segmentIndex>>3 == recordPoint>>3;
this.sideStreet = levelInfo.sideStreets && ((segmentIndex%checkpointTrackSegments)%495<36);
{
// setup colors
const groundColor = levelInfo.groundColor.lerp(levelInfoNext.groundColor,levelLerpPercent);
const lineColor = levelInfo.lineColor.lerp(levelInfoNext.lineColor,levelLerpPercent);
const roadColor = levelInfo.roadColor.lerp(levelInfoNext.roadColor,levelLerpPercent);
const largeSegmentIndex = segmentIndex/9|0;
const stripe = largeSegmentIndex% 2 ? .1: 0;
this.colorGround = groundColor.brighten(Math.cos(segmentIndex*2/PI)/20);
this.colorRoad = roadColor.brighten(stripe&&.05);
if (recordPointLine)
this.colorRoad = hsl(0,.8,.5);
else if (checkpointLine)
this.colorRoad = WHITE; // starting line
this.colorLine = lineColor;
if (stripe)
this.colorLine.a = 0;
if (this.sideStreet)
this.colorLine = this.colorGround = this.colorRoad;
}
// spawn track objects
if (debug && testGameSprite)
{
// test sprite
this.addSprite(testGameSprite,random.floatSign(width/2,1e4));
}
else if (debug && testTrackBillboards)
{
// test billboard
const billboardSprite = random.fromList(spriteList.billboards);
this.addSprite(billboardSprite,random.floatSign(width/2,1e4));
}
else if (segmentIndex == levelGoal*checkpointTrackSegments)
{
// goal!
this.addSprite(spriteList.sign_goal);
}
else if (segmentIndex%checkpointTrackSegments == 0)
{
// checkpoint
if (segmentIndex < levelGoal*checkpointTrackSegments)
{
this.addSprite(spriteList.sign_checkpoint1,-width+500);
this.addSprite(spriteList.sign_checkpoint2, width-500);
}
}
if (segmentIndex == 30)
{
// starting area
this.addSprite(spriteList.sign_start);
// left
const ol = -(width+100);
this.addSprite(spriteList.sign_opGames,ol,1450);
this.addSprite(spriteList.sign_zzfx,ol,850);
this.addSprite(spriteList.sign_avalanche,ol);
// right
const or = width+100;
this.addSprite(spriteList.sign_frankForce,or,1500);
this.addSprite(spriteList.sign_github,or,350);
this.addSprite(spriteList.sign_js13k,or);
if (js13kBuild)
random.seed = 1055752394; // hack, reset seed for js13k
}
}
getWind()
{
const offset = this.offset;
const noiseScale = .001;
return Math.sin(time+(offset.x+offset.z)*noiseScale)/2;
}
addSprite(sprite,x=0,y=0,extraScale=1)
{
// add a sprite to the track as a new track object
const offset = vec3(x,y);
const sizeScale = extraScale*sprite.getRandomSpriteScale();
const color = sprite.getRandomSpriteColor();
const trackObject = new TrackObject(this, sprite, offset, color, sizeScale);
this.trackObjects.push(trackObject);
}
}
// get lerped info about a track segment
class TrackSegmentInfo
{
constructor(z)
{
const segment = this.segmentIndex = z/trackSegmentLength|0;
const percent = this.percent = z/trackSegmentLength%1;
if (track[segment] && track[segment+1])
{
if (track[segment].pos && track[segment+1].pos)
this.pos = track[segment].pos.lerp(track[segment+1].pos, percent);
else
this.pos = vec3(0,0,z);
this.pitch = lerp(percent, track[segment].pitch, track[segment+1].pitch);
this.offset = track[segment].offset.lerp(track[segment+1].offset, percent);
this.width = lerp(percent, track[segment].width,track[segment+1].width);
}
else
this.offset = this.pos = vec3(this.pitch = this.width = 0,0,z);
}
}

274
vue/public/race/trackGen.js Normal file
View File

@@ -0,0 +1,274 @@
'use strict';
const testTrackBillboards=0;
// build the road with procedural generation
function buildTrack()
{
// set random seed & time
random.setSeed(trackSeed);
track = [];
let sectionXEndDistance = 0;
let sectionYEndDistance = 0;
let sectionTurn = 0;
let noisePos = random.int(1e5);
let sectionBumpFrequency = 0;
let sectionBumpScale = 1;
let currentNoiseFrequency = 0;
let currentNoiseScale = 1;
let turn = 0;
// generate the road
const trackEnd = levelGoal*checkpointTrackSegments;
const roadTransitionRange = testQuick?min(checkpointTrackSegments,500):500;
for(let i=0; i < trackEnd + 5e4; ++i)
{
const levelFloat = i/checkpointTrackSegments;
const level = levelFloat|0;
const levelInfo = getLevelInfo(level);
const levelInfoLast = getLevelInfo(levelFloat-1);
const levelLerpPercent = percent(i%checkpointTrackSegments, 0, roadTransitionRange);
if (js13kBuild && i==31496)
random.setSeed(7); // mess with seed to randomize jungle
const roadGenWidth = laneWidth/2*lerp(levelLerpPercent, levelInfoLast.laneCount, levelInfo.laneCount);
let height = 0;
let width = roadGenWidth;
const startOfTrack = !level && i < 400;
const checkpointSegment = i%checkpointTrackSegments;
const levelBetweenRange = 100;
let isBetweenLevels = checkpointSegment < levelBetweenRange ||
checkpointSegment > checkpointTrackSegments - levelBetweenRange;
isBetweenLevels |= startOfTrack; // start of track
//const nextCheckpoint = (level+1)*checkpointTrackSegments;
if (isBetweenLevels)
{
// transition at start or end of level
sectionXEndDistance = sectionYEndDistance = sectionTurn = 0;
}
else
{
// turns
const turnChance = levelInfo.turnChance; // chance of turn
const turnMin = levelInfo.turnMin; // min turn
const turnMax = levelInfo.turnMax; // max turn
const sectionDistanceMin = 100;
const sectionDistanceMax = 400;
if (sectionXEndDistance-- < 0)
{
// pick random section distance
sectionXEndDistance = random.int(sectionDistanceMin,sectionDistanceMax);
sectionTurn = random.bool(turnChance) ? random.floatSign(turnMin,turnMax) : 0;
}
// bumps
const bumpChance = levelInfo.bumpChance; // chance of bump
const bumpFreqMin = levelInfo.bumpFreqMin; // no bumps
const bumpFreqMax = levelInfo.bumpFreqMax; // raipd bumps
const bumpScaleMin = levelInfo.bumpScaleMin; // small rapid bumps
const bumpScaleMax = levelInfo.bumpScaleMax; // large hills
if (sectionYEndDistance-- < 0)
{
// pick random section distance
sectionYEndDistance = random.int(sectionDistanceMin,sectionDistanceMax);
if (random.bool(bumpChance))
{
sectionBumpFrequency = random.float(bumpFreqMin,bumpFreqMax);
sectionBumpScale = random.float(bumpScaleMin,bumpScaleMax);
}
else
{
sectionBumpFrequency = 0;
sectionBumpScale = bumpScaleMin;
}
}
}
if (i > trackEnd - 500)
sectionTurn = 0; // no turns at end
turn = lerp(.02,turn, sectionTurn); // smooth out turns
// apply noise to height
const noiseFrequency = currentNoiseFrequency
= lerp(.01, currentNoiseFrequency, sectionBumpFrequency);
const noiseSize = currentNoiseScale
= lerp(.01, currentNoiseScale, sectionBumpScale);
//noiseFrequency = 1; noiseSize = 50;
if (currentNoiseFrequency)
noisePos += noiseFrequency/noiseSize;
const noiseConstant = 20;
height = noise1D(noisePos)*noiseConstant*noiseSize;
//turn = .7; height = 0;
//turn = Math.sin(i/100)*.7;
//height = noise1D((i-50)/99)*2700;turn =0; // jumps test
// create track segment
const o = vec3(turn, height, i*trackSegmentLength);
track[i] = new TrackSegment(i, o, width);
}
// second pass
let hazardWait = 0;
let tunnelOn = 0;
let tunnelTime = 0;
let trackSideChanceScale = 1;
for(let i=0; i < track.length; ++i)
{
// calculate pitch
const iCheckpoint = i%checkpointTrackSegments;
const t = track[i];
const levelInfo = getLevelInfo(t.level);
ASSERT(t.level == levelInfo.level || t.level > levelGoal);
const previous = track[i-1];
if (previous)
{
t.pitch = Math.atan2(previous.offset.y-t.offset.y, trackSegmentLength);
const d = vec3(0,t.offset.y-previous.offset.y, trackSegmentLength);
t.normal = d.cross(vec3(1,0)).normalize();
}
if (!iCheckpoint)
{
// reset level settings
trackSideChanceScale = 1;
}
if (t.sideStreet || i < 50)
{
tunnelOn = 0;
continue; // no objects on side streets
}
// check what kinds of turns are ahead
const lookAheadTurn = 150;
const lookAheadStep = 20;
let leftTurns = 0, rightTurns = 0;
for(let k=0; k<lookAheadTurn; k+=lookAheadStep)
{
const t2 = track[i+k];
if (!t2)
continue;
if (k < lookAheadTurn)
{
const x = t2.offset.x;
if (x > 0) leftTurns = max(leftTurns, x);
else rightTurns = max(rightTurns, -x);
}
}
// spawn road signs
const roadSignRate = 10;
const turnWarning = 0.5;
let signSide;
if (i < levelGoal*checkpointTrackSegments) // end of level
if (rightTurns > turnWarning || leftTurns > turnWarning)
{
// turn
signSide = sign(rightTurns - leftTurns);
if (i%roadSignRate == 0)
t.addSprite(spriteList.sign_turn,signSide*(t.width+500));
}
// todo prevent sprites from spawning near road signs?
//levelInfo.tunnel = spriteList.tunnel2; // test tuns
if (levelInfo.tunnel)
{
const isRockArch = levelInfo.tunnel.tunnelArch;
const isLongTunnel = levelInfo.tunnel.tunnelLong;
if (iCheckpoint > 100 && iCheckpoint < checkpointTrackSegments - 100)
{
const wasOn = tunnelOn;
if (tunnelTime-- < 0)
{
tunnelOn = !tunnelOn;
tunnelTime = tunnelOn?
isRockArch ? 10 : random.int(200,600) :
tunnelTime = random.int(300,600); // longer when off
}
if (tunnelOn)
{
// brighter front of tunnel
const sprite = isLongTunnel && !wasOn ?
spriteList.tunnel2Front : levelInfo.tunnel;
t.addSprite(sprite, 0);
if (isLongTunnel && i%50==0)
{
// lights on top of tunnel
const lightSprite = spriteList.light_tunnel;
const tunnelHeight = 1600;
t.addSprite(lightSprite, 0, tunnelHeight);
}
continue;
}
}
}
else
{
// restart tunnel wait
tunnelOn = tunnelTime = 0;
}
{
// sprites on sides of track
const billboardChance = levelInfo.billboardChance;
const billboardRate = levelInfo.billboardRate;
if (i%billboardRate == 0 && random.bool(billboardChance))
{
// random billboards
const extraScale = levelInfo.billboardScale; // larger in desert
const width = t.width*extraScale;
const count = spriteList.billboards.length;
const billboardSprite = spriteList.billboards[random.int(count)];
const billboardSide = signSide ? -signSide : random.sign();
t.addSprite(billboardSprite,billboardSide*random.float(width+600,width+800),0,extraScale);
continue;
}
if (levelInfo.trackSideSprite)
{
// vary how often side objects spawn
if (random.bool(.001))
{
trackSideChanceScale =
random.bool(.4) ? 1 : // normal to spawn often
random.bool(.1) ? 0 : // small chance of none
random.float(); // random scale
}
// track side objects
const trackSideRate = levelInfo.trackSideRate;
const trackSideChance = levelInfo.trackSideChance;
if (i%trackSideRate == 0 && random.bool(trackSideChance*trackSideChanceScale))
{
const trackSideForce = levelInfo.trackSideForce;
const side = trackSideForce || (i%(trackSideRate*2)<trackSideRate?1:-1);
t.addSprite(levelInfo.trackSideSprite, side*(t.width+random.float(700,1e3)));
continue;
}
}
if (iCheckpoint > 40 && iCheckpoint < checkpointTrackSegments - 40)
if (hazardWait-- < 0 && levelInfo.hazardType && random.bool(levelInfo.hazardChance))
{
// hazards on the ground in road to slow player
const sprite = levelInfo.hazardType;
t.addSprite(sprite,random.floatSign(t.width/.9));
// wait to spawn another hazard
hazardWait = random.float(40,80);
}
}
}
}

View File

@@ -0,0 +1,229 @@
'use strict';
///////////////////////////////////////////////////////////////////////////////
// Math Stuff
const PI = Math.PI;
const abs = (value) => Math.abs(value);
const min = (valueA, valueB) => Math.min(valueA, valueB);
const max = (valueA, valueB) => Math.max(valueA, valueB);
const sign = (value) => value < 0 ? -1 : 1;
const mod = (dividend, divisor=1) => ((dividend % divisor) + divisor) % divisor;
const clamp = (value, min=0, max=1) => value < min ? min : value > max ? max : value;
const clampAngle = (value) => ((value+PI) % (2*PI) + 2*PI) % (2*PI) - PI;
const percent = (value, valueA, valueB) => (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0;
const lerp = (percent, valueA, valueB) => valueA + clamp(percent) * (valueB-valueA);
const rand = (valueA=1, valueB=0) => lerp(Math.random(), valueA, valueB);
const randInt = (valueA, valueB=0) => rand(valueA, valueB)|0;
const smoothStep = (p) => p * p * (3 - 2 * p);
const isOverlapping = (posA, sizeA, posB, sizeB=vec3()) =>
abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x && abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
function buildMatrix(pos, rot, scale)
{
const R2D = 180/PI;
let m = new DOMMatrix;
pos && m.translateSelf(pos.x, pos.y, pos.z);
rot && m.rotateSelf(rot.x*R2D, rot.y*R2D, rot.z*R2D);
scale && m.scaleSelf(scale.x, scale.y, scale.z);
return m;
}
function shuffle(array)
{
for(let currentIndex = array.length; currentIndex;)
{
const randomIndex = random.int(currentIndex--);
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
}
return array;
}
function formatTimeString(t)
{
const timeS = t%60|0;
const timeM = t/60|0;
const timeMS = t%1*1e3|0;
return `${timeM}:${timeS<10?'0'+timeS:timeS}.${(timeMS<10?'00':timeMS<100?'0':'')+timeMS}`;
}
function noise1D(x)
{
const hash = x=>(new Random(x)).float(-1,1);
return lerp(smoothStep(mod(x,1)), hash(x), hash(x+1));
}
///////////////////////////////////////////////////////////////////////////////
// Vector3
const vec3 = (x, y, z)=> y == undefined && z == undefined ? new Vector3(x, x, x) : new Vector3(x, y, z);
const isVector3 = (v) => v instanceof Vector3;
const isNumber = (value) => typeof value === 'number';
const ASSERT_VEC3 = (v) => ASSERT(isVector3(v));
class Vector3
{
constructor(x=0, y=0, z=0)
{
ASSERT(isNumber(x) && isNumber(y) && isNumber(z));
this.x=x; this.y=y; this.z=z;
}
copy() { return vec3(this.x, this.y, this.z); }
add(v) { ASSERT_VEC3(v); return vec3(this.x + v.x, this.y + v.y, this.z + v.z); }
addSelf(v) { ASSERT_VEC3(v); this.x += v.x, this.y += v.y, this.z += v.z; return this }
subtract(v) { ASSERT_VEC3(v); return vec3(this.x - v.x, this.y - v.y, this.z - v.z); }
multiply(v) { ASSERT_VEC3(v); return vec3(this.x * v.x, this.y * v.y, this.z * v.z); }
divide(v) { ASSERT_VEC3(v); return vec3(this.x / v.x, this.y / v.y, this.z / v.z); }
scale(s) { ASSERT(isNumber(s)); return vec3(this.x * s, this.y * s, this.z * s); }
length() { return this.lengthSquared()**.5; }
lengthSquared() { return this.x**2 + this.y**2 + this.z**2; }
distance(v) { ASSERT_VEC3(v); return this.distanceSquared(v)**.5; }
distanceSquared(v) { ASSERT_VEC3(v); return this.subtract(v).lengthSquared(); }
normalize(length=1) { const l = this.length(); return l ? this.scale(length/l) : vec3(length); }
clampLength(length=1) { const l = this.length(); return l > length ? this.scale(length/l) : this; }
dot(v) { ASSERT_VEC3(v); return this.x*v.x + this.y*v.y + this.z*v.z; }
angleBetween(v) { ASSERT_VEC3(v); return Math.acos(clamp(this.dot(v), -1, 1)); }
clamp(a, b) { return vec3(clamp(this.x, a, b), clamp(this.y, a, b), clamp(this.z, a, b)); }
cross(v) { ASSERT_VEC3(v); return vec3(this.y*v.z-this.z*v.y, this.z*v.x-this.x*v.z, this.x*v.y-this.y*v.x); }
lerp(v, p) { ASSERT_VEC3(v); return v.subtract(this).scale(clamp(p)).addSelf(this); }
rotateX(a)
{
const c=Math.cos(a), s=Math.sin(a);
return vec3(this.x, this.y*c - this.z*s, this.y*s + this.z*c);
}
rotateY(a)
{
const c=Math.cos(a), s=Math.sin(a);
return vec3(this.x*c - this.z*s, this.y, this.x*s + this.z*c);
}
rotateZ(a)
{
const c=Math.cos(a), s=Math.sin(a);
return vec3(this.x*c - this.y*s, this.x*s + this.y*c, this.z);
}
transform(matrix)
{
const p = matrix.transformPoint(this);
return vec3(p.x, p.y, p.z);
}
getHSLColor(a=1) { return hsl(this.x, this.y, this.z, a); }
}
///////////////////////////////////////////////////////////////////////////////
// Color
const rgb = (r, g, b, a) => new Color(r, g, b, a);
const hsl = (h, s, l, a) => rgb().setHSLA(h, s, l, a);
const isColor = (c) => c instanceof Color;
class Color
{
constructor(r=1, g=1, b=1, a=1)
{
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
copy() { return rgb(this.r, this.g, this.b, this.a); }
lerp(c, percent)
{
ASSERT(isColor(c));
percent = clamp(percent);
return rgb(
lerp(percent, this.r, c.r),
lerp(percent, this.g, c.g),
lerp(percent, this.b, c.b),
lerp(percent, this.a, c.a),
);
}
brighten(amount=.1)
{
return rgb
(
clamp(this.r + amount),
clamp(this.g + amount),
clamp(this.b + amount),
this.a
);
}
setHSLA(h=0, s=0, l=1, a=1)
{
h = mod(h,1);
s = clamp(s);
l = clamp(l);
const q = l < .5 ? l*(1+s) : l+s-l*s, p = 2*l-q,
f = (p, q, t)=>
(t = mod(t,1))*6 < 1 ? p+(q-p)*6*t :
t*2 < 1 ? q :
t*3 < 2 ? p+(q-p)*(4-t*6) : p;
this.r = f(p, q, h + 1/3);
this.g = f(p, q, h);
this.b = f(p, q, h - 1/3);
this.a = a;
return this;
}
toString()
{ return `rgb(${this.r*255},${this.g*255},${this.b*255},${this.a})`; }
}
///////////////////////////////////////////////////////////////////////////////
// Random
class Random
{
constructor(seed) { this.setSeed(seed); }
setSeed(seed)
{
this.seed = seed+1|0;
this.float();this.float();this.float();// warmup
}
float(a=1, b=0)
{
// xorshift
this.seed ^= this.seed << 13;
this.seed ^= this.seed >>> 17;
this.seed ^= this.seed << 5;
if (js13kBuild)
return b + (a-b) * Math.abs(this.seed % 1e9) / 1e9; // bias low values due to float error
else
return b + (a-b) * Math.abs(this.seed % 1e8) / 1e8;
}
floatSign(a, b) { return this.float(a,b) * this.sign(); }
int(a, b) { return this.float(a, b)|0; }
bool(chance = .5) { return this.float() < chance; }
sign() { return this.bool() ? -1 : 1; }
circle(radius=0, bias = .5)
{
const r = this.float()**bias*radius;
const a = this.float(PI*2);
return vec3(r*Math.cos(a), r*Math.sin(a));
}
mutateColor(color, amount=.1, brightnessAmount=0)
{
return rgb
(
clamp(random.float(1,1-brightnessAmount)*(color.r + this.floatSign(amount))),
clamp(random.float(1,1-brightnessAmount)*(color.g + this.floatSign(amount))),
clamp(random.float(1,1-brightnessAmount)*(color.b + this.floatSign(amount))),
color.a
);
}
fromList(list,startBias=1) { return list[this.float()**startBias*list.length|0]; }
}
///////////////////////////////////////////////////////////////////////////////
class Timer
{
constructor(timeLeft)
{ this.time = timeLeft == undefined ? undefined : time + timeLeft; }
set(timeLeft=0) { this.time = time + timeLeft; }
unset() { this.time = undefined; }
isSet() { return this.time != undefined; }
active() { return time < this.time; }
elapsed() { return time >= this.time; }
get() { return this.isSet()? time - this.time : 0; }
}

625
vue/public/race/vehicle.js Normal file
View File

@@ -0,0 +1,625 @@
'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<maxVehicleCount && !gameOverTimer.isSet() && !vehicleSpawnTimer.active())
{
const spawnOffset = playerIsSlow ? -1300 : rand(5e4,6e4);
spawnVehicle(playerVehicle.pos.z + spawnOffset);
vehicleSpawnTimer.set(rand(1,2)/trafficDensity);
}
for(const v of vehicles)
v.update();
vehicles = vehicles.filter(o=>!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();
}
}
}
}
}
}

305
vue/public/race/webgl.js Normal file
View File

@@ -0,0 +1,305 @@
'use strict';
/*
Small and fast dynamic webgl rendering engine for Dr1v3n Wild
Features
- batch rendering
- direct and ambient lighting
- fog with alpha blending
- texture mapping
- vertex color
Potential improvements
- everything is using dynamic buffer, which is slow but flexible
- it would be faster to use static buffers for static geometry
- the colors could be passed in as 32 bit integers rather then vec4s
- specular lighting would also be pretty easy to include
- the fog calculation could possibly be moved to the vertex shader
- a mip map of the passed in texture could be auto generated for smoother scaling
- additive blending would also be easy to implement
- there should be an easier way to set the fog range
*/
const glRenderScale = 100; // fixes floating point issues on some devices
const glSpecular = 0; // experimental specular test
let glCanvas, glContext, glShader, glVertexData;
let glBatchCount, glBatchCountTotal, glDrawCalls;
let glEnableLighting, glLightDirection, glLightColor, glAmbientColor;
let glEnableFog, glFogColor;
///////////////////////////////////////////////////////////////////////////////
// webgl setup
function glInit()
{
// create the canvas
const hasAlpha = false; // there should be no alpha for the background texture
document.body.appendChild(glCanvas = document.createElement('canvas'));
glContext = glCanvas.getContext('webgl2', {alpha: hasAlpha});
ASSERT(glContext, 'Failed to create WebGL canvas!');
// setup vertex and fragment shaders
glShader = glCreateProgram(
'#version 300 es\n' + // specify GLSL ES version
'precision highp float;'+ // use highp for better accuracy
'uniform vec4 l,g,a,f;' + // light direction, color, ambient light, fog
'uniform mat4 m,o;'+ // projection matrix, object matrix
'in vec4 p,n,u,c;'+ // in: position, normal, uv, color
'out vec4 v,d,q;'+ // out: uv, color, fog
'void main(){'+ // shader entry point
'gl_Position=m*o*p;'+ // transform position
'v=u,q=f;'+ // pass uv and fog to fragment shader
'd=c*vec4(a.xyz+g.xyz*max(0.,dot(l.xyz,'+ // lighting
'normalize((transpose(inverse(o))*n).xyz))),1);' + // transform light
'}' // end of shader
,
'#version 300 es\n' + // specify GLSL ES version
'precision highp float;'+ // use highp for better accuracy
'in vec4 v,d,q;'+ // uv, color, fog
'uniform sampler2D s;'+ // texture
'out vec4 c;'+ // out color
'void main(){'+ // shader entry point
'c=v.z>0.?d:texture(s,v.xy)*d;'+ // color or texture
'float f=gl_FragCoord.z/gl_FragCoord.w;'+ // fog depth
'v.w>0.?c:c=vec4(mix(c.xyz,q.xyz,clamp(f*f/1e10,0.,1.)),'+ // fog color
'c.a*clamp(4.-f/2e4,0.,1.));'+ // fog alpha
//'c.w);'+ // disable fog alpha
//'if (c.a == 0.) discard;'+ // discard if no alpha
'}' // end of shader
);
// set up the shader
glContext.useProgram(glShader);
glContext.bindBuffer(gl_ARRAY_BUFFER, glContext.createBuffer());
glContext.bufferData(gl_ARRAY_BUFFER, gl_VERTEX_BUFFER_SIZE, gl_DYNAMIC_DRAW);
glContext.blendFunc(gl_SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);
glSetCapability(gl_BLEND);
glSetCapability(gl_CULL_FACE); // not culling causeses thin black lines sometimes
glVertexData = new Float32Array(new ArrayBuffer(gl_VERTEX_BUFFER_SIZE));
// set vertex attributes
let offset = 0;
const vertexAttribute = (name)=>
{
const type = gl_FLOAT, stride = gl_VERTEX_BYTE_STRIDE;
const size = 4, byteCount = 4;
const location = glContext.getAttribLocation(glShader, name);
glContext.enableVertexAttribArray(location);
glContext.vertexAttribPointer(location, size, type, 0, stride, offset);
offset += size*byteCount;
}
vertexAttribute('p'); // position
vertexAttribute('n'); // normal
vertexAttribute('u'); // uv
vertexAttribute('c'); // color
}
function glCompileShader(source, type)
{
// build the shader
const shader = glContext.createShader(type);
glContext.shaderSource(shader, source);
glContext.compileShader(shader);
// check for errors
if (debug && !glContext.getShaderParameter(shader, gl_COMPILE_STATUS))
throw glContext.getShaderInfoLog(shader);
return shader;
}
function glCreateProgram(vsSource, fsSource)
{
// build the program
const program = glContext.createProgram();
glContext.attachShader(program, glCompileShader(vsSource, gl_VERTEX_SHADER));
glContext.attachShader(program, glCompileShader(fsSource, gl_FRAGMENT_SHADER));
glContext.linkProgram(program);
// check for errors
if (debug && !glContext.getProgramParameter(program, gl_LINK_STATUS))
throw glContext.getProgramInfoLog(program);
return program;
}
function glCreateTexture(image)
{
// build the texture
const texture = glContext.createTexture();
glContext.bindTexture(gl_TEXTURE_2D, texture);
glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
return texture;
}
function glPreRender(canvasSize)
{
// set size of canvas and viewport which also clears it
glContext.viewport(0, 0, glCanvas.width = canvasSize.x, glCanvas.height = canvasSize.y);
glDrawCalls = glBatchCount = glBatchCountTotal = 0; // reset draw counts
//debug && glContext.clearColor(1, 0, 1, 1); // test background color
//glContext.clear(gl_DEPTH_BUFFER_BIT|gl_COLOR_BUFFER_BIT); // auto cleared
// use point filtering for pixelated rendering
glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_NEAREST);
glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, gl_NEAREST);
// set up the camera transform
const viewMatrix = buildMatrix(cameraPos, cameraRot).inverse();
const combinedMatrix = glCreateProjectionMatrix().multiply(viewMatrix);
glContext.uniformMatrix4fv(glUniform('m'), 0, combinedMatrix.toFloat32Array());
}
function glRender(transform=new DOMMatrix)
{
// set up the lights and fog
const initUniform4f = (name, x, y, z)=> glContext.uniform4f(glUniform(name), x, y, z, 0);
const lightColor = glEnableLighting ? glLightColor : BLACK;
const ambientColor = glEnableLighting ? glAmbientColor : WHITE;
initUniform4f('g', lightColor.r, lightColor.g, lightColor.b);
initUniform4f('a', ambientColor.r, ambientColor.g, ambientColor.b);
initUniform4f('f', glFogColor.r, glFogColor.g, glFogColor.b);
initUniform4f('l', glLightDirection.x, glLightDirection.y, glLightDirection.z);
// render the verts
ASSERT(glBatchCount < gl_MAX_BATCH, 'Too many points!');
const vertexData = glVertexData.subarray(0, glBatchCount * gl_INDICIES_PER_VERT);
const m = transform.scaleSelf(glRenderScale, glRenderScale, glRenderScale);
glContext.uniformMatrix4fv(glUniform('o'), 0, m.toFloat32Array());
glContext.bufferSubData(gl_ARRAY_BUFFER, 0, vertexData);
glContext.drawArrays(gl_TRIANGLE_STRIP, 0, glBatchCount);
glBatchCountTotal += glBatchCount;
glBatchCount = 0;
++glDrawCalls;
}
///////////////////////////////////////////////////////////////////////////////
// webgl helper functions
const glUniform = (name) => glContext.getUniformLocation(glShader, name);
function glSetCapability(cap, enable=1)
{ enable ? glContext.enable(cap) : glContext.disable(cap); }
function glPolygonOffset(units=0)
{ glContext.polygonOffset(0, -units); glSetCapability(gl_POLYGON_OFFSET_FILL, !!units); }
function glSetDepthTest(depthTest=1, depthWrite=1)
{ glSetCapability(gl_DEPTH_TEST, !!depthTest); glContext.depthMask(!!depthWrite); }
function glCreateProjectionMatrix(fov=.5, near = 1, far = 1e4)
{
const aspect = glCanvas.width / glCanvas.height;
const f = 1 / Math.tan(fov), range = far - near;
return new DOMMatrix
([
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (near + far) / range, 2 * near * far / range,
0, 0, -1, 0
]);
}
///////////////////////////////////////////////////////////////////////////////
// drawing functions
const vectorOne = vec3(1); // no lighting/texture
// push a list of colored verts with optonal normals and uvs
function glPushVerts(points, normals, color, uvs)
{
const count = points.length;
if (!(count < gl_MAX_BATCH - glBatchCount))
glRender();
const na = vectorOne; // no lighting/texture
for(let i=count; i--;)
glPushVert(points[i], normals ? normals[i] : na, uvs ? uvs[i] : na, color);
}
// push a list of colored verts with optonal normals and uvs
// this is also capped with degenerate verts to close the shape
function glPushVertsCapped(points, normals, color, uvs)
{
// push points with extra degenerate verts to cap both sides
const count = points.length;
if (!(count+2 < gl_MAX_BATCH - glBatchCount))
glRender();
const na = vectorOne; // no lighting/texture
glPushVert(points[count-1], na, na, color);
for(let i=count; i--;)
glPushVert(points[i], normals ? normals[i] : na, uvs ? uvs[i] : na, color);
glPushVert(points[0], na, na, color);
}
// push a list of colored verts without normals or uvs
function glPushColoredVerts(points, colors)
{
// push points with a list of vertex colors
const count = points.length;
if (!(count+2 < gl_MAX_BATCH - glBatchCount))
glRender();
const na = vectorOne; // no lighting/texture
glPushVert(points[count-1], na, na, colors[count-1]);
for(let i=count; i--;)
glPushVert(points[i], na, na, colors[i]);
glPushVert(points[0], na, na, colors[0]);
}
// push a single vert to the buffer
function glPushVert(pos, normal, uv, color)
{
let offset = glBatchCount++ * gl_INDICIES_PER_VERT;
glVertexData[offset++] = pos.x/glRenderScale;
glVertexData[offset++] = pos.y/glRenderScale;
glVertexData[offset++] = pos.z/glRenderScale;
glVertexData[offset++] = 1;
glVertexData[offset++] = normal.x;
glVertexData[offset++] = normal.y;
glVertexData[offset++] = normal.z;
glVertexData[offset++] = 0;
glVertexData[offset++] = uv.x;
glVertexData[offset++] = uv.y;
glVertexData[offset++] = uv.z; // >0 if untextured
glVertexData[offset++] = !glEnableFog;
glVertexData[offset++] = color.r;
glVertexData[offset++] = color.g;
glVertexData[offset++] = color.b;
glVertexData[offset++] = color.a;
}
///////////////////////////////////////////////////////////////////////////////
// store webgl constants as integers so they can be minifed
const
gl_TRIANGLE_STRIP = 5,
gl_DEPTH_BUFFER_BIT = 256,
gl_SRC_ALPHA = 770,
gl_ONE_MINUS_SRC_ALPHA = 771,
gl_CULL_FACE = 2884,
gl_DEPTH_TEST = 2929,
gl_BLEND = 3042,
gl_TEXTURE_2D = 3553,
gl_UNSIGNED_BYTE = 5121,
gl_FLOAT = 5126,
gl_RGBA = 6408,
gl_NEAREST = 9728,
gl_TEXTURE_MAG_FILTER = 10240,
gl_TEXTURE_MIN_FILTER = 10241,
gl_COLOR_BUFFER_BIT = 16384,
gl_POLYGON_OFFSET_FILL = 32823,
gl_ARRAY_BUFFER = 34962,
gl_DYNAMIC_DRAW = 35048,
gl_FRAGMENT_SHADER = 35632,
gl_VERTEX_SHADER = 35633,
gl_COMPILE_STATUS = 35713,
gl_LINK_STATUS = 35714,
// constants for batch rendering
gl_MAX_BATCH = 2e4, // max verts per batch
gl_INDICIES_PER_VERT = (1 * 4) * 4, // vec4 * 4
gl_VERTEX_BYTE_STRIDE = gl_INDICIES_PER_VERT * 4, // 4 bytes per float
gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTEX_BYTE_STRIDE;