← Rally results

Rally

thinkingcap qwen3.6 27b · basic

Initial view of Rally

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"} -->