Rally
thinkingcap qwen3.6 27b · basic

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 33,773 bytes · SHA-256 0d5eaa8a150f
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Rally Racer 3D</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:'Segoe UI',Arial,sans-serif;color:#fff}
#gameCanvas{display:block;width:100%;height:100%}
#hud{position:absolute;top:0;left:0;pointer-events:none;z-index:10}
#speedometer{position:absolute;bottom:20px;right:20px;text-align:right;font-size:14px}
#speedVal{font-size:48px;font-weight:bold;color:#ff6600;text-shadow:0 0 10px rgba(255,102,0,.5)}
#gearVal{font-size:32px;color:#fff;margin-top:-5px}
#rpmBar{width:150px;height:8px;background:rgba(0,0,0,.6);border-radius:4px;margin-top:4px;overflow:hidden}
#rpmFill{height:100%;background:linear-gradient(90deg,#0f0,#ff0,#f00);width:0%;transition:width .1s}
#lapInfo{position:absolute;top:20px;left:20px;font-size:16px;text-shadow:1px 1px 3px #000}
#timer{font-size:28px;font-weight:bold;color:#ffcc00;margin-top:4px}
#bestLap{color:#aaa;font-size:14px;margin-top:2px}
#countdown{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:120px;font-weight:bold;text-shadow:0 0 30px rgba(255,102,0,.8);z-index:20;pointer-events:none;display:none}
#pauseMenu,#finishScreen{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(0,0,0,.7);z-index:30;pointer-events:auto}
#pauseMenu h2,#finishScreen h2{font-size:48px;margin-bottom:20px;color:#ff6600}
.menuBtn{padding:12px 40px;font-size:20px;background:#ff6600;border:none;color:#fff;cursor:pointer;border-radius:8px;margin:8px;pointer-events:auto;font-weight:bold}
.menuBtn:hover{background:#ff8833}
#finishStats{text-align:center;margin-bottom:20px;line-height:1.8;font-size:20px}
#minimap{position:absolute;top:20px;right:20px;width:160px;height:160px;border-radius:50%;border:2px solid rgba(255,255,255,.3);background:rgba(0,0,0,.4)}
#touchControls{position:absolute;bottom:0;left:0;width:100%;height:180px;display:none;z-index:15;pointer-events:auto}
.touchBtn{position:absolute;border-radius:50%;border:2px solid rgba(255,255,255,.4);background:rgba(255,255,255,.15);color:#fff;font-size:24px;display:flex;align-items:center;justify-content:center;user-select:none;-webkit-user-select:none}
.touchBtn:active{background:rgba(255,255,255,.4)}
#dpad{position:absolute;left:20px;bottom:10px;width:140px;height:140px}
#btnUp{left:45px;top:0;width:50px;height:50px}
#btnDown{left:45px;top:90px;width:50px;height:50px}
#btnLeft{left:0;top:45px;width:50px;height:50px}
#btnRight{left:90px;top:45px;width:50px;height:50px}
#btnGas{right:20px;bottom:10px;width:70px;height:70px;background:rgba(0,200,0,.25)}
#btnBrake{right:100px;bottom:10px;width:60px;height:60px;background:rgba(200,0,0,.25)}
#btnHandbrake{right:170px;bottom:10px;width:50px;height:50px;background:rgba(200,200,0,.25);font-size:14px}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="hud">
<div id="lapInfo">Lap <span id="curLap">1</span>/3<br><span id="timer">00:00.0</span><br><span id="bestLap">Best: --:--.--</span></div>
<div id="speedometer"><span id="speedVal">0</span> km/h<div id="gearVal">N</div><div id="rpmBar"><div id="rpmFill"></div></div></div>
<canvas id="minimap" width="160" height="160"></canvas>
</div>
<div id="countdown"></div>
<div id="pauseMenu" style="display:none"><h2>PAUSED</h2><button class="menuBtn" onclick="RallyGame.resume()">Resume</button><button class="menuBtn" onclick="RallyGame.restart()">Restart</button></div>
<div id="finishScreen" style="display:none"><h2>RACE COMPLETE!</h2><div id="finishStats"></div><button class="menuBtn" onclick="RallyGame.restart()">Race Again</button></div>
<div id="touchControls">
<div id="dpad">
<div class="touchBtn" id="btnUp">▲</div>
<div class="touchBtn" id="btnDown">▼</div>
<div class="touchBtn" id="btnLeft">◀</div>
<div class="touchBtn" id="btnRight">▶</div>
</div>
<div class="touchBtn" id="btnGas">GAS</div>
<div class="touchBtn" id="btnBrake">BRK</div>
<div class="touchBtn" id="btnHandbrake">HB</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
"use strict";
var RallyGame=(function(){
// ==================== CONFIGURATION ====================
var CFG={
trackPoints:60,trackWidth:14,maxLaps:3,
maxSpeed:55,accelForce:28,brakeForce:35,reverseMax:15,
steerSpeed:2.8,driftFactor:0.92,gripAsphalt:0.95,gripDirt:0.7,gripGrass:0.45,gripMud:0.3,
cameraDist:18,cameraHeight:8,cameraLag:0.06,
fogNear:80,fogFar:250,
treeCount:300,rockCount:80,signCount:20,bannerCount:15
};
// ==================== GAME STATE ====================
var state={
phase:'countdown', // countdown,racing,pause,finished
lap:1,totalTime:0,lapTimes:[],bestLap:Infinity,
carPos:new THREE.Vector3(),carRot:0,carSpeed:0,carVelX:0,carVelZ:0,
gear:0,rpm:0,onTrack:true,surfaceType:'asphalt',
checkpointsPassed:new Set(),lastCheckpoint:-1,
countdownTimer:4,countdownValue:3
};
// ==================== INPUT ====================
var keys={};
var touchInput={up:false,down:false,left:false,right:false,gas:false,brake:false,handbrake:false};
document.addEventListener('keydown',function(e){keys[e.code]=true;if(['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].indexOf(e.code)>=0)e.preventDefault()});
document.addEventListener('keyup',function(e){keys[e.code]=false});
// Touch controls
if('ontouchstart' in window||navigator.maxTouchPoints>0){
document.getElementById('touchControls').style.display='block';
}
function setupTouchBtn(id,prop){
var el=document.getElementById(id);
if(!el)return;
el.addEventListener('touchstart',function(e){e.preventDefault();touchInput[prop]=true});
el.addEventListener('touchend',function(e){e.preventDefault();touchInput[prop]=false});
el.addEventListener('touchcancel',function(){touchInput[prop]=false});
}
setupTouchBtn('btnUp','up');setupTouchBtn('btnDown','down');
setupTouchBtn('btnLeft','left');setupTouchBtn('btnRight','right');
setupTouchBtn('btnGas','gas');setupTouchBtn('btnBrake','brake');
setupTouchBtn('btnHandbrake','handbrake');
function isKey(action){
switch(action){
case'up':return keys['ArrowUp']||keys['KeyW']||touchInput.up;
case'down':return keys['ArrowDown']||keys['KeyS']||touchInput.down;
case'left':return keys['ArrowLeft']||keys['KeyA']||touchInput.left;
case'right':return keys['ArrowRight']||keys['KeyD']||touchInput.right;
case'brake':return keys['Space']||touchInput.brake;
case'handbrake':return touchInput.handbrake;
}
}
// ==================== THREE.JS SETUP ====================
var scene,camera,renderer,clock;
var carGroup,carBody,carWheels=[];
var trackCurve,trackPoints3D=[];
var particles=[],skidMarks=[];
var minimapCtx=document.getElementById('minimap').getContext('2d');
function initThree(){
scene=new THREE.Scene();
scene.background=new THREE.Color(0x87CEEB);
scene.fog=new THREE.FogExp2(0xC8D8E8,0.004);
camera=new THREE.PerspectiveCamera(65,window.innerWidth/window.innerHeight,0.5,500);
renderer=new THREE.WebGLRenderer({canvas:document.getElementById('gameCanvas'),antialias:true});
renderer.setSize(window.innerWidth/window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio,2));
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
// Lighting
var ambient=new THREE.AmbientLight(0x6688aa,0.6);
scene.add(ambient);
var sun=new THREE.DirectionalLight(0xffeedd,1.0);
sun.position.set(50,80,30);
sun.castShadow=true;
sun.shadow.mapSize.width=2048;sun.shadow.mapSize.height=2048;
sun.shadow.camera.near=1;sun.shadow.camera.far=200;
sun.shadow.camera.left=-60;sun.shadow.camera.right=60;
sun.shadow.camera.top=60;sun.shadow.camera.bottom=-60;
scene.add(sun);
var hemi=new THREE.HemisphereLight(0x87CEEB,0x445522,0.3);
scene.add(hemi);
clock=new THREE.Clock();
}
// ==================== TRACK GENERATION ====================
function generateTrack(){
var pts=[];
var numPts=CFG.trackPoints;
for(var i=0;i<numPts;i++){
var t=i/numPts*Math.PI*2;
// Create an interesting rally track shape
var r=80+30*Math.sin(2*t)+15*Math.cos(3*t)+10*Math.sin(5*t);
var x=Math.cos(t)*r;
var z=Math.sin(t)*r;
var y=8*Math.sin(2*t)+5*Math.cos(3*t)+3*Math.sin(4*t+1);
pts.push(new THREE.Vector3(x,y,z));
}
trackCurve=new THREE.CatmullRomCurve3(pts,true,'catmullrom',0.5);
// Sample points along the curve for collision/checkpoint logic
var samples=600;
for(var i=0;i<samples;i++){
var t=i/samples;
trackPoints3D.push(trackCurve.getPointAt(t));
}
}
function buildTrackMesh(){
// Road surface
var roadGeo=new THREE.BufferGeometry();
var verts=[],uvs=[],indices=[];
var segs=600,hw=CFG.trackWidth/2;
for(var i=0;i<=segs;i++){
var t=i/segs;
var p=trackCurve.getPointAt(t);
var tan=trackCurve.getTangentAt(t);
var normal=new THREE.Vector3(-tan.z,0,tan.x).normalize();
var left=p.clone().add(normal.clone().multiplyScalar(hw));
var right=p.clone().sub(normal.clone().multiplyScalar(hw));
verts.push(left.x,left.y+0.1,left.z,right.x,right.y+0.1,right.z);
uvs.push(0,t*20,1,t*20);
if(i<segs){
var base=i*2;
indices.push(base,base+1,base+2,base+1,base+3,base+2);
}
}
roadGeo.setAttribute('position',new THREE.Float32BufferAttribute(verts,3));
roadGeo.setAttribute('uv',new THREE.Float32BufferAttribute(uvs,2));
roadGeo.setIndex(indices);
roadGeo.computeVertexNormals();
var roadMat=new THREE.MeshLambertMaterial({color:0x555555});
var roadMesh=new THREE.Mesh(roadGeo,roadMat);
roadMesh.receiveShadow=true;
scene.add(roadMesh);
// Road edges (gravel shoulders)
for(var side=-1;side<=1;side+=2){
var edgeVerts=[],edgeIdx=[];
for(var i=0;i<=segs;i++){
var t=i/segs;
var p=trackCurve.getPointAt(t);
var tan=trackCurve.getTangentAt(t);
var normal=new THREE.Vector3(-tan.z,0,tan.x).normalize();
var outer=p.clone().add(normal.clone().multiplyScalar(hw+2*side));
edgeVerts.push(outer.x,outer.y-0.1,outer.z);
if(i<segs){
var b=i;
edgeIdx.push(b,b+1,b+segs+1,b+1,b+segs+2,b+segs+1);
}
}
// Simpler: just add shoulder strips
}
// Ground plane
var groundGeo=new THREE.PlaneGeometry(600,600,50,50);
// Add some terrain variation
var gPos=groundGeo.attributes.position;
for(var i=0;i<gPos.count;i++){
var gx=gPos.getX(i),gz=gPos.getZ(i);
var dist=Math.sqrt(gx*gx+gz*gz);
if(dist>120){
gPos.setY(i,(Math.random()-0.5)*3);
}
}
groundGeo.computeVertexNormals();
var groundMat=new THREE.MeshLambertMaterial({color:0x4a7c3f});
var ground=new THREE.Mesh(groundGeo,groundMat);
ground.rotation.x=-Math.PI/2;
ground.position.y=-1;
ground.receiveShadow=true;
scene.add(ground);
}
// ==================== SCENERY ====================
function buildScenery(){
// Trees
var treeTrunkGeo=new THREE.CylinderGeometry(0.3,0.4,3,5);
var treeTrunkMat=new THREE.MeshLambertMaterial({color:0x8B4513});
var treeCrownGeo=new THREE.ConeGeometry(2,4,6);
var treeCrownMat=new THREE.MeshLambertMaterial({color:0x2d5a1e});
for(var i=0;i<CFG.treeCount;i++){
var t=Math.random();
var p=trackCurve.getPointAt(t);
var tan=trackCurve.getTangentAt(t);
var normal=new THREE.Vector3(-tan.z,0,tan.x).normalize();
var side=(Math.random()>0.5?1:-1)*(CFG.trackWidth/2+5+Math.random()*40);
var pos=p.clone().add(normal.clone().multiplyScalar(side));
pos.y=0;
var trunk=new THREE.Mesh(treeTrunkGeo,treeTrunkMat);
var crown=new THREE.Mesh(treeCrownGeo,treeCrownMat);
var treeGroup=new THREE.Group();
trunk.position.set(pos.x,pos.y+1.5,pos.z);
crown.position.set(pos.x,pos.y+4,pos.z);
trunk.castShadow=true;crown.castShadow=true;
treeGroup.add(trunk);treeGroup.add(crown);
var s=0.8+Math.random()*0.6;
treeGroup.scale.set(s,s,s);
scene.add(treeGroup);
}
// Rocks
var rockGeo=new THREE.DodecahedronGeometry(1,0);
var rockMat=new THREE.MeshLambertMaterial({color:0x888888});
for(var i=0;i<CFG.rockCount;i++){
var t=Math.random();
var p=trackCurve.getPointAt(t);
var tan=trackCurve.getTangentAt(t);
var normal=new THREE.Vector3(-tan.z,0,tan.x).normalize();
var side=(Math.random()>0.5?1:-1)*(CFG.trackWidth/2+3+Math.random()*25);
var pos=p.clone().add(normal.clone().multiplyScalar(side));
pos.y=0;
var rock=new THREE.Mesh(rockGeo,rockMat);
rock.position.set(pos.x,pos.y+0.5,pos.z);
var s=0.5+Math.random()*1.5;
rock.scale.set(s,s*0.6,s);
rock.rotation.set(Math.random(),Math.random(),Math.random());
rock.castShadow=true;
scene.add(rock);
}
// Signs (rally markers)
var signPostGeo=new THREE.CylinderGeometry(0.1,0.1,2,4);
var signPostMat=new THREE.MeshLambertMaterial({color:0xcccccc});
var signBoardGeo=new THREE.BoxGeometry(1.5,1,0.1);
for(var i=0;i<CFG.signCount;i++){
var t=i/CFG.signCount;
var p=trackCurve.getPointAt(t);
var tan=trackCurve.getTangentAt(t);
var normal=new THREE.Vector3(-tan.z,0,tan.x).normalize();
var side=(Math.random()>0.5?1:-1)*(CFG.trackWidth/2+1);
var pos=p.clone().add(normal.clone().multiplyScalar(side));
var post=new THREE.Mesh(signPostGeo,signPostMat);
post.position.set(pos.x,pos.y+1,pos.z);
scene.add(post);
var boardColors=[0xff4444,0x44ff44,0x4444ff,0xffff44,0xff44ff];
var board=new THREE.Mesh(signBoardGeo,new THREE.MeshLambertMaterial({color:boardColors[i%5]}));
board.position.set(pos.x,pos.y+2.2,pos.z);
scene.add(board);
}
// Banners between poles at start/finish
for(var i=0;i<CFG.bannerCount;i++){
var t=i/CFG.bannerCount;
var p=trackCurve.getPointAt(t);
var tan=trackCurve.getTangentAt(t);
var normal=new THREE.Vector3(-tan.z,0,tan.x).normalize();
for(var side=-1;side<=1;side+=2){
var polePos=p.clone().add(normal.clone().multiplyScalar(side*(CFG.trackWidth/2+0.5)));
var poleGeo=new THREE.CylinderGeometry(0.08,0.08,4,4);
var poleMat=new THREE.MeshLambertMaterial({color:0xdddddd});
var pole=new THREE.Mesh(poleGeo,poleMat);
pole.position.set(polePos.x,polePos.y+2,polePos.z);
scene.add(pole);
}
// Banner between poles
var bannerGeo=new THREE.PlaneGeometry(CFG.trackWidth,1.5);
var bannerColors=[0xff6600,0xffffff];
var bannerMat=new THREE.MeshLambertMaterial({color:bannerColors[i%2],side:THREE.DoubleSide});
var banner=new THREE.Mesh(bannerGeo,bannerMat);
banner.position.set(p.x,p.y+3.5,p.z);
banner.lookAt(p.clone().add(tan));
scene.add(banner);
}
// Guard rails along track edges
var railPostGeo=new THREE.CylinderGeometry(0.06,0.06,1.2,4);
var railMat=new THREE.MeshLambertMaterial({color:0xcccccc});
for(var i=0;i<300;i++){
var t=i/300;
var p=trackCurve.getPointAt(t);
var tan=trackCurve.getTangentAt(t);
var normal=new THREE.Vector3(-tan.z,0,tan.x).normalize();
for(var side=-1;side<=1;side+=2){
if(Math.random()>0.7)continue; // sparse placement
var rp=p.clone().add(normal.clone().multiplyScalar(side*(CFG.trackWidth/2+0.3)));
var post=new THREE.Mesh(railPostGeo,railMat);
post.position.set(rp.x,rp.y+0.6,rp.z);
scene.add(post);
}
}
}
// ==================== CAR MODEL ====================
function buildCar(){
carGroup=new THREE.Group();
// Body
var bodyGeo=new THREE.BoxGeometry(2,0.8,4.5);
var bodyMat=new THREE.MeshLambertMaterial({color:0xdd3311});
carBody=new THREE.Mesh(bodyGeo,bodyMat);
carBody.position.y=0.6;
carBody.castShadow=true;
carGroup.add(carBody);
// Cabin
var cabinGeo=new THREE.BoxGeometry(1.7,0.7,2);
var cabinMat=new THREE.MeshLambertMaterial({color:0x334455});
var cabin=new THREE.Mesh(cabinGeo,cabinMat);
cabin.position.set(0,1.2,-0.3);
cabin.castShadow=true;
carGroup.add(cabin);
// Spoiler
var spoilerGeo=new THREE.BoxGeometry(2.2,0.1,0.5);
var spoilerMat=new THREE.MeshLambertMaterial({color:0x222222});
var spoiler=new THREE.Mesh(spoilerGeo,spoilerMat);
spoiler.position.set(0,1.4,-2);
carGroup.add(spoiler);
// Spoiler supports
for(var x=-0.7;x<=0.7;x+=1.4){
var supGeo=new THREE.BoxGeometry(0.1,0.5,0.1);
var sup=new THREE.Mesh(supGeo,spoilerMat);
sup.position.set(x,1.15,-2);
carGroup.add(sup);
}
// Wheels
var wheelGeo=new THREE.CylinderGeometry(0.35,0.35,0.3,8);
var wheelMat=new THREE.MeshLambertMaterial({color:0x222222});
var wheelPositions=[[-1,0.35,-1.3],[1,0.35,-1.3],[-1,0.35,1.3],[1,0.35,1.3]];
for(var i=0;i<4;i++){
var w=new THREE.Mesh(wheelGeo,wheelMat);
w.rotation.z=Math.PI/2;
w.position.set(...wheelPositions[i]);
w.castShadow=true;
carWheels.push(w);
carGroup.add(w);
}
// Headlights
for(var x=-0.7;x<=0.7;x+=1.4){
var hlGeo=new THREE.SphereGeometry(0.15,6,6);
var hlMat=new THREE.MeshBasicMaterial({color:0xffffcc});
var hl=new THREE.Mesh(hlGeo,hlMat);
hl.position.set(x,0.6,2.3);
carGroup.add(hl);
}
// Taillights
for(var x=-0.7;x<=0.7;x+=1.4){
var tlGeo=new THREE.SphereGeometry(0.12,6,6);
var tlMat=new THREE.MeshBasicMaterial({color:0xff0000});
var tl=new THREE.Mesh(tlGeo,tlMat);
tl.position.set(x,0.6,-2.3);
carGroup.add(tl);
}
// Bumpers
var frontBumperGeo=new THREE.BoxGeometry(2.1,0.3,0.2);
var bumperMat=new THREE.MeshLambertMaterial({color:0x444444});
var fb=new THREE.Mesh(frontBumperGeo,bumperMat);
fb.position.set(0,0.35,2.35);
carGroup.add(fb);
var rb=new THREE.Mesh(frontBumperGeo.clone(),bumperMat);
rb.position.set(0,0.35,-2.35);
carGroup.add(rb);
scene.add(carGroup);
}
// ==================== PARTICLES ====================
function createParticle(pos,color,size,life){
var geo=new THREE.SphereGeometry(size,4,4);
var mat=new THREE.MeshBasicMaterial({color:color,transparent:true,opacity:0.8});
var mesh=new THREE.Mesh(geo,mat);
mesh.position.copy(pos);
scene.add(mesh);
particles.push({mesh:mesh,life:life,maxLife:life,vx:(Math.random()-0.5)*2,vy:Math.random()*3+1,vz:(Math.random()-0.5)*2});
}
function updateParticles(dt){
for(var i=particles.length-1;i>=0;i--){
var p=particles[i];
p.life-=dt;
if(p.life<=0){
scene.remove(p.mesh);
particles.splice(i,1);
continue;
}
p.mesh.position.x+=p.vx*dt;
p.mesh.position.y+=p.vy*dt;
p.mesh.position.z+=p.vz*dt;
p.vy-=5*dt; // gravity
p.mesh.material.opacity=p.life/p.maxLife;
}
}
function spawnDust(pos){
for(var i=0;i<3;i++){
createParticle(pos.clone().add(new THREE.Vector3((Math.random()-0.5)*2,0,(Math.random()-0.5)*2)),0xccaa77,0.4+Math.random()*0.3,1+Math.random());
}
}
function spawnSparks(pos){
for(var i=0;i<8;i++){
createParticle(pos.clone(),0xffaa00,0.15,0.5+Math.random()*0.3);
}
}
// ==================== AUDIO SYSTEM ====================
var audioCtx=null;
var engineOsc=null,engineGain=null;
var skidNode=null,skidGain=null;
function initAudio(){
try{
audioCtx=new(window.AudioContext||window.webkitAudioContext)();
// Engine sound - low frequency oscillator with noise
engineOsc=audioCtx.createOscillator();
engineGain=audioCtx.createGain();
var filter=audioCtx.createBiquadFilter();
filter.type='lowpass';filter.frequency.value=200;
engineOsc.connect(filter);
filter.connect(engineGain);
engineGain.connect(audioCtx.destination);
engineOsc.start();
engineGain.gain.value=0.05;
// Skid noise
var bufferSize=audioCtx.sampleRate*2;
var skidBuffer=audioCtx.createBuffer(1,bufferSize,audioCtx.sampleRate);
var skidData=skidBuffer.getChannelData(0);
for(var i=0;i<bufferSize;i++)skidData[i]=(Math.random()*2-1)*0.3;
skidNode=audioCtx.createBufferSource();
skidNode.buffer=skidBuffer;skidNode.loop=true;
skidGain=audioCtx.createGain();
var skidFilter=audioCtx.createBiquadFilter();
skidFilter.type='bandpass';skidFilter.frequency.value=800;skidFilter.Q.value=1;
skidNode.connect(skidFilter);skidFilter.connect(skidGain);
skidGain.connect(audioCtx.destination);
skidNode.start();
skidGain.gain.value=0;
}catch(e){console.warn('Audio init failed',e)}
}
function updateEngineSound(){
if(!engineOsc)return;
var speedRatio=Math.abs(state.carSpeed)/CFG.maxSpeed;
engineOsc.frequency.value=60+speedRatio*180;
engineGain.gain.value=0.03+speedRatio*0.08;
}
function playBeep(freq,dur){
if(!audioCtx)return;
var osc=audioCtx.createOscillator();
var gain=audioCtx.createGain();
osc.frequency.value=freq;osc.type='square';
osc.connect(gain);gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0.15,audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+dur);
osc.start();osc.stop(audioCtx.currentTime+dur);
}
function playCrash(){
if(!audioCtx)return;
var bufferSize=audioCtx.sampleRate*0.3;
var buf=audioCtx.createBuffer(1,bufferSize,audioCtx.sampleRate);
var data=buf.getChannelData(0);
for(var i=0;i<bufferSize;i++)data[i]=(Math.random()*2-1)*Math.exp(-i/bufferSize*5);
var src=audioCtx.createBufferSource();src.buffer=buf;
var g=audioCtx.createGain();g.gain.value=0.3;
src.connect(g);g.connect(audioCtx.destination);src.start();
}
function updateSkidSound(isDrifting,isOffRoad){
if(!skidGain)return;
skidGain.gain.value=(isDrifting||isOffRoad)?0.12:0;
}
// ==================== PHYSICS ====================
function getTrackInfo(pos){
// Find closest point on track and distance from center
var minDist=Infinity,closestT=0;
for(var i=0;i<trackPoints3D.length;i++){
var dx=pos.x-trackPoints3D[i].x;
var dz=pos.z-trackPoints3D[i].z;
var d=dx*dx+dz*dz;
if(d<minDist){minDist=d;closestT=i/trackPoints3D.length;}
}
var dist=Math.sqrt(minDist);
var halfW=CFG.trackWidth/2;
var surfaceType='asphalt';
var grip=CFG.gripAsphalt;
var speedMult=1.0;
if(dist<halfW){surfaceType='asphalt';grip=CFG.gripAsphalt;}
else if(dist<halfW+4){surfaceType='dirt';grip=CFG.gripDirt;speedMult=0.85;}
else if(dist<halfW+12){surfaceType='grass';grip=CFG.gripGrass;speedMult=0.6;}
else{surfaceType='mud';grip=CFG.gripMud;speedMult=0.4;}
return{dist:dist,surfaceType:surfaceType,grip:grip,speedMult:speedMult,t:closestT};
}
function updatePhysics(dt){
if(state.phase!=='racing')return;
var info=getTrackInfo(state.carPos);
state.surfaceType=info.surfaceType;
state.onTrack=info.dist<CFG.trackWidth/2;
// Acceleration / braking
var accel=isKey('up');
var brake=isKey('down')||isKey('brake');
var handbrake=isKey('handbrake');
var steerLeft=isKey('left');
var steerRight=isKey('right');
var maxSpd=CFG.maxSpeed*info.speedMult;
if(accel){
state.carSpeed+=CFG.accelForce*dt*(state.carSpeed<0?2:1);
}else if(brake){
if(state.carSpeed>1)state.carSpeed-=CFG.brakeForce*dt;
else state.carSpeed-=CFG.accelForce*0.5*dt;
}else{
// Friction
state.carSpeed*=Math.pow(0.98,dt*60);
}
if(state.carSpeed>maxSpd)state.carSpeed=maxSpd;
if(state.carSpeed<-CFG.reverseMax)state.carSpeed=-CFG.reverseMax;
if(Math.abs(state.carSpeed)<0.1&&!accel&&!brake)state.carSpeed=0;
// Steering
var steerAmount=0;
var speedFactor=Math.min(1,Math.abs(state.carSpeed)/20);
if(steerLeft)steerAmount=-CFG.steerSpeed*speedFactor*dt;
if(steerRight)steerAmount=CFG.steerSpeed*speedFactor*dt;
// Drifting
var isDrifting=Math.abs(state.carSpeed)>15&&Math.abs(steerAmount)>0.01&&(info.surfaceType!=='asphalt'||handbrake);
if(handbrake){state.carSpeed*=0.97;}
var grip=info.grip;
if(isDrifting||handbrake)grip*=CFG.driftFactor;
state.carRot+=steerAmount*Math.sign(state.carSpeed);
// Movement with drift physics
var moveX=Math.sin(state.carRot)*state.carSpeed*dt;
var moveZ=Math.cos(state.carRot)*state.carSpeed*dt;
// Blend velocity for drift feel
state.carVelX+=(moveX-state.carVelX)*grip*3;
state.carVelZ+=(moveZ-state.carVelZ)*grip*3;
state.carPos.x+=state.carVelX;
state.carPos.z+=state.carVelZ;
// Get track elevation at car position
var closestPt=trackPoints3D[Math.floor(info.t*trackPoints3D.length)%trackPoints3D.length];
if(closestPt){
state.carPos.y+=(closestPt.y-state.carPos.y)*0.1;
}
// Checkpoint system
checkCheckpoints(info.t);
// Obstacle collision (simple: slow down near track edges)
if(info.dist>CFG.trackWidth/2+5){
state.carSpeed*=0.98;
if(Math.random()<0.1)spawnDust(state.carPos.clone());
}
// Dust when off-road or drifting
if((info.surfaceType!=='asphalt'||isDrifting)&&Math.abs(state.carSpeed)>5){
spawnDust(state.carPos.clone().add(new THREE.Vector3(0,-0.2,0)));
}
// Gear simulation
var absSpd=Math.abs(state.carSpeed);
if(absSpd<1)state.gear=0;
else if(absSpd<15)state.gear=1;
else if(absSpd<30)state.gear=2;
else if(absSpd<45)state.gear=3;
else state.gear=4;
var gearRanges=[0,15,30,45,55];
var low=gearRanges[state.gear-1]||0;
var high=gearRanges[state.gear]||55;
state.rpm=Math.max(0,Math.min(8000,(absSpd-low)/(high-low)*7000+1000));
// Audio updates
updateEngineSound();
updateSkidSound(isDrifting,info.surfaceType!=='asphalt');
}
function checkCheckpoints(t){
var numCP=8;
var cpIndex=Math.floor(t*numCP)%numCP;
if(cpIndex!==state.lastCheckpoint){
// Allow some tolerance - must progress forward
var expected=(state.lastCheckpoint+1)%numCP;
if(cpIndex===expected||Math.abs(cpIndex-state.lastCheckpoint)<=2){
state.checkpointsPassed.add(cpIndex);
state.lastCheckpoint=cpIndex;
// Check for lap completion
if(state.checkpointsPassed.size>=numCP-1&&state.lap<=CFG.maxLaps){
var lapTime=state.totalTime-(state.lapTimes.reduce(function(a,b){return a+b},0));
if(lapTime>0){
state.lapTimes.push(lapTime);
if(lapTime<state.bestLap)state.bestLap=lapTime;
}
state.checkpointsPassed.clear();
state.lastCheckpoint=-1;
if(state.lap>=CFG.maxLaps){
finishRace();
}else{
state.lap++;
playBeep(880,0.2);
}
}
}
}
}
// ==================== CAMERA ====================
var camPos=new THREE.Vector3();
var camTarget=new THREE.Vector3();
var cinematicMode=false;
function updateCamera(){
var carWorldPos=state.carPos.clone();
var carDir=new THREE.Vector3(-Math.sin(state.carRot),0,-Math.cos(state.carRot));
if(cinematicMode){
// Higher, wider view
camTarget.copy(carWorldPos).add(new THREE.Vector3(0,5,0));
var desiredCam=carWorldPos.clone().sub(carDir.multiplyScalar(CFG.cameraDist*1.5)).add(new THREE.Vector3(0,CFG.cameraHeight*2,0));
camPos.lerp(desiredCam,CFG.cameraLag*2);
}else{
// Chase camera
var behind=carWorldPos.clone().sub(carDir.multiplyScalar(CFG.cameraDist));
behind.y+=CFG.cameraHeight;
camPos.lerp(behind,CFG.cameraLag);
camTarget.copy(carWorldPos).add(new THREE.Vector3(0,2,0));
}
camera.position.copy(camPos);
camera.lookAt(camTarget);
// Update shadow camera to follow car
var sun=scene.children.find(function(c){return c.isDirectionalLight});
if(sun){
sun.target.position.copy(carWorldPos);
sun.position.set(carWorldPos.x+50,80,carWorldPos.z+30);
}
}
// ==================== HUD UPDATE ====================
function updateHUD(){
var spd=Math.abs(Math.round(state.carSpeed*3.6)); // km/h
document.getElementById('speedVal').textContent=spd;
document.getElementById('gearVal').textContent=state.gear===0?'N':state.gear;
document.getElementById('rpmFill').style.width=(state.rpm/8000*100)+'%';
document.getElementById('curLap').textContent=Math.min(state.lap,CFG.maxLaps);
var mins=Math.floor(state.totalTime/60);
var secs=state.totalTime%60;
document.getElementById('timer').textContent=String(mins).padStart(2,'0')+':'+secs.toFixed(1).padStart(4,'0');
if(state.bestLap<Infinity){
var bm=Math.floor(state.bestLap/60);
var bs=state.bestLap%60;
document.getElementById('bestLap').textContent='Best: '+String(bm).padStart(2,'0')+':'+bs.toFixed(1).padStart(4,'0');
}
drawMinimap();
}
function drawMinimap(){
var ctx=minimapCtx;
var w=160,h=160,cx=80,cy=80,scale=0.5;
ctx.clearRect(0,0,w,h);
// Draw track
ctx.strokeStyle='rgba(255,255,255,0.4)';
ctx.lineWidth=3;
ctx.beginPath();
for(var i=0;i<=trackPoints3D.length;i++){
var p=trackPoints3D[i%trackPoints3D.length];
var mx=cx+p.x*scale;
var my=cy+p.z*scale;
if(i===0)ctx.moveTo(mx,my);else ctx.lineTo(mx,my);
}
ctx.closePath();
ctx.stroke();
// Draw car position
ctx.fillStyle='#ff3300';
ctx.beginPath();
var carMx=cx+state.carPos.x*scale;
var carMy=cy+state.carPos.z*scale;
ctx.arc(carMx,carMy,4,0,Math.PI*2);
ctx.fill();
// Direction indicator
ctx.strokeStyle='#ff3300';
ctx.lineWidth=2;
ctx.beginPath();
ctx.moveTo(carMx,carMy);
ctx.lineTo(carMx-Math.sin(state.carRot)*8,carMy-Math.cos(state.carRot)*8);
ctx.stroke();
}
// ==================== GAME FLOW ====================
function startCountdown(){
state.phase='countdown';
state.countdownValue=3;
var el=document.getElementById('countdown');
el.style.display='block';
function tick(){
if(state.countdownValue>0){
el.textContent=state.countdownValue;
playBeep(440,0.15);
state.countdownValue--;
setTimeout(tick,1000);
}else{
el.textContent='GO!';
playBeep(880,0.3);
setTimeout(function(){el.style.display='none';state.phase='racing';},500);
}
}
tick();
}
function finishRace(){
state.phase='finished';
var totalTime=state.totalTime;
var bm=Math.floor(totalTime/60),bs=totalTime%60;
var best=state.bestLap<Infinity?state.bestLap:0;
var bbm=Math.floor(best/60),bbs=best%60;
document.getElementById('finishStats').innerHTML=
'Total Time: '+String(bbm).padStart(2,'0')+':'+bs.toFixed(1).padStart(4,'0')+'<br>'+
'Best Lap: '+String(bbm).padStart(2,'0')+':'+bbs.toFixed(1).padStart(4,'0')+'<br>'+
'Laps Completed: '+CFG.maxLaps;
document.getElementById('finishScreen').style.display='flex';
// Finish tone
playBeep(523,0.2);setTimeout(function(){playBeep(659,0.2)},200);
setTimeout(function(){playBeep(784,0.4)},400);
}
function pauseGame(){
if(state.phase==='racing'){state.phase='pause';document.getElementById('pauseMenu').style.display='flex';}
}
RallyGame.resume=function(){
if(state.phase==='pause'){state.phase='racing';document.getElementById('pauseMenu').style.display='none';}
};
RallyGame.restart=function(){
document.getElementById('pauseMenu').style.display='none';
document.getElementById('finishScreen').style.display='none';
resetGameState();
startCountdown();
};
function resetGameState(){
state.lap=1;state.totalTime=0;state.lapTimes=[];state.bestLap=Infinity;
state.carSpeed=0;state.carRot=0;state.carVelX=0;state.carVelZ=0;
state.checkpointsPassed.clear();state.lastCheckpoint=-1;
// Reset car position to start of track
var startPos=trackCurve.getPointAt(0);
var startTan=trackCurve.getTangentAt(0);
state.carPos.copy(startPos);
state.carRot=Math.atan2(-startTan.x,-startTan.z);
// Clear particles
for(var i=particles.length-1;i>=0;i--){scene.remove(particles[i].mesh);}
particles=[];
}
// ==================== MAIN LOOP ====================
function animate(){
requestAnimationFrame(animate);
var dt=Math.min(clock.getDelta(),0.05); // cap delta time
if(state.phase==='racing'){
state.totalTime+=dt;
updatePhysics(dt);
}
// Update car visual
if(carGroup){
carGroup.position.copy(state.carPos);
carGroup.rotation.y=state.carRot;
// Wheel rotation
var wheelSpin=Math.abs(state.carSpeed)*dt*0.5;
for(var i=0;i<carWheels.length;i++){
carWheels[i].rotation.x+=wheelSpin;
}
// Car body tilt on turns
var turnAmount=(isKey('left')?-1:0)+(isKey('right')?1:0);
carBody.rotation.z=turnAmount*Math.min(1,Math.abs(state.carSpeed)/30)*0.15;
}
updateCamera();
updateParticles(dt);
updateHUD();
renderer.render(scene,camera);
}
// ==================== INITIALIZATION ====================
function init(){
initThree();
generateTrack();
buildTrackMesh();
buildScenery();
buildCar();
// Position car at start
var startPos=trackCurve.getPointAt(0);
var startTan=trackCurve.getTangentAt(0);
state.carPos.copy(startPos);
state.carRot=Math.atan2(-startTan.x,-startTan.z);
// Handle window resize
window.addEventListener('resize',function(){
camera.aspect=window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth,window.innerHeight);
});
// Pause key
document.addEventListener('keydown',function(e){
if(e.code==='KeyP')pauseGame();
if(e.code==='KeyR'&&state.phase!=='countdown'){resetGameState();startCountdown();}
if(e.code==='KeyC')cinematicMode=!cinematicMode;
});
// Start audio on first interaction
document.addEventListener('click',function(){if(!audioCtx)initAudio();},{once:true});
document.addEventListener('touchstart',function(){if(!audioCtx)initAudio();},{once:true});
startCountdown();
animate();
}
// Expose resume/restart globally for button onclick handlers
RallyGame.resume=RallyGame.resume;
RallyGame.restart=RallyGame.restart;
init();
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"basic","sessionId":"ef168b4a-436e-48cc-ad26-a169a3d4dc14","tokensIn":1127,"tokensOut":11433,"tokensTotal":12560,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-19T02:33:21.844Z"} -->