City Heist
by LunarGalaxy87779 lines30.4 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>City Heist</title>
<style>
html,body{margin:0;padding:0;overflow:hidden;background:#111;font-family:Arial,sans-serif;touch-action:none;}
#hud{position:fixed;top:10px;left:10px;color:#fff;z-index:10;text-shadow:1px 1px 2px #000;font-size:16px;}
#wanted{position:fixed;top:10px;right:10px;color:#ff3b3b;z-index:10;font-size:22px;text-shadow:1px 1px 2px #000;}
#msg{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;font-size:28px;z-index:20;text-shadow:2px 2px 4px #000;display:none;text-align:center;}
#controls{position:fixed;bottom:20px;left:0;right:0;display:flex;justify-content:space-between;padding:0 20px;z-index:10;}
.pad{display:flex;gap:10px;}
.btn{width:60px;height:60px;background:rgba(255,255,255,0.2);border:2px solid rgba(255,255,255,0.5);border-radius:12px;color:#fff;font-size:24px;display:flex;align-items:center;justify-content:center;user-select:none;}
#err{position:fixed;bottom:0;left:0;right:0;background:#900;color:#fff;font-size:12px;padding:5px;z-index:999;display:none;white-space:pre-wrap;}
#buyPrompt{position:fixed;bottom:110px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.75);color:#fff;padding:12px 18px;border-radius:10px;font-size:16px;z-index:15;display:none;text-align:center;}
#buyPrompt button{margin-top:8px;padding:8px 16px;font-size:16px;border-radius:8px;border:none;background:#33cc66;color:#fff;}
#speedFx{position:fixed;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:5;background:radial-gradient(ellipse at center, rgba(0,0,0,0) 55%, rgba(0,0,0,0.35) 100%);opacity:0;transition:opacity 0.15s;}
#loadScreen{position:fixed;top:0;left:0;right:0;bottom:0;background:linear-gradient(160deg,#0d1220,#1a2035);z-index:1000;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:opacity 0.6s ease;}
#loadScreen.hidden{opacity:0;pointer-events:none;}
#loadTitle{color:#fff;font-size:32px;font-weight:bold;letter-spacing:2px;margin-bottom:24px;text-shadow:0 0 20px rgba(255,200,100,0.4);}
#loadBarTrack{width:min(70vw,320px);height:8px;background:rgba(255,255,255,0.15);border-radius:6px;overflow:hidden;}
#loadBarFill{height:100%;width:0%;background:linear-gradient(90deg,#ffcc66,#ff9955);border-radius:6px;transition:width 0.25s ease;}
#loadStatus{color:#aab;font-size:13px;margin-top:14px;letter-spacing:1px;}
</style>
</head>
<body>
<div id="hud">Cash: $<span id="cash">0</span><br>Mission: <span id="mission">Drive around town</span><br>Owned: <span id="ownedCount">0</span></div>
<div id="wanted"></div>
<div id="msg"></div>
<div id="buyPrompt"></div>
<div id="speedFx"></div>
<div id="err"></div>
<div id="loadScreen">
<div id="loadTitle">CITY HEIST</div>
<div id="loadBarTrack"><div id="loadBarFill"></div></div>
<div id="loadStatus">Loading engine…</div>
</div>
<div id="controls">
<div class="pad">
<div class="btn" id="left">◀</div>
<div class="btn" id="right">▶</div>
</div>
<div class="pad">
<div class="btn" id="brake">🛑</div>
<div class="btn" id="gas">▲</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js" onerror="document.getElementById('err').style.display='block'; document.getElementById('err').textContent='Failed to load Three.js from CDN — check your internet connection and reload.';"></script>
<script>
if(typeof THREE === 'undefined'){
document.getElementById('err').style.display='block';
document.getElementById('err').textContent='THREE failed to load — try reloading the page.';
var lsFail = document.getElementById('loadScreen');
if(lsFail) lsFail.style.display = 'none';
} else {
try{
window.CITY_HEIST_STAGE = 'scene-setup';
function setLoadProgress(pct, label){
var fill = document.getElementById('loadBarFill');
var status = document.getElementById('loadStatus');
if(fill) fill.style.width = pct + '%';
if(status && label) status.textContent = label;
}
setLoadProgress(10, 'Loading engine…');
window.onerror = function(msg, source, lineno, colno, error){
var e=document.getElementById('err');
e.style.display='block';
var detail = (error && error.stack) ? error.stack : msg;
e.textContent = 'Error: ' + detail + ' (line ' + lineno + ':' + colno + ')';
};
window.addEventListener('unhandledrejection', function(ev){
var e=document.getElementById('err');
e.style.display='block';
e.textContent = 'Promise error: ' + ev.reason;
});
// ---------- Setup ----------
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(65, window.innerWidth/window.innerHeight, 0.1, 1000);
var renderer = new THREE.WebGLRenderer({antialias:false, powerPreference:'high-performance'});
renderer.setPixelRatio(1);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFShadowMap;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
document.body.appendChild(renderer.domElement);
renderer.domElement.style.position = 'fixed';
renderer.domElement.style.top = '0';
renderer.domElement.style.left = '0';
var RENDER_SCALE = 0.65; // internal render resolution scale for perf + upscale look
function applyRendererSize(){
var w = window.innerWidth, h = window.innerHeight;
renderer.setSize(w*RENDER_SCALE, h*RENDER_SCALE, false);
renderer.domElement.style.width = w + 'px';
renderer.domElement.style.height = h + 'px';
camera.aspect = w/h;
camera.updateProjectionMatrix();
}
applyRendererSize();
window.addEventListener('resize', applyRendererSize);
// dusk-ish sky for dramatic lighting + working headlights
scene.background = new THREE.Color(0x1a2035);
scene.fog = new THREE.Fog(0x1a2035, 25, 110);
var hemi = new THREE.HemisphereLight(0x8899cc, 0x222233, 0.6);
scene.add(hemi);
var sun = new THREE.DirectionalLight(0xffe8cc, 0.7);
sun.position.set(60,50,-40);
sun.castShadow = true;
sun.shadow.mapSize.set(512,512);
sun.shadow.camera.left=-80; sun.shadow.camera.right=80;
sun.shadow.camera.top=80; sun.shadow.camera.bottom=-80;
sun.shadow.bias = -0.0005;
scene.add(sun);
// ---------- City grid ----------
var CITY_SIZE = 240;
var BLOCK = 24;
var ROAD_W = 8;
// procedural window-lit facade texture for buildings (adds realism vs flat color)
function makeBuildingTexture(baseColor){
var c = document.createElement('canvas');
c.width = 128; c.height = 256;
var ctx = c.getContext('2d');
ctx.fillStyle = '#' + baseColor.toString(16).padStart(6,'0');
ctx.fillRect(0,0,128,256);
var rows = 16, cols = 6;
for(var r=0;r<rows;r++){
for(var cI=0;cI<cols;cI++){
var lit = Math.random() < 0.35;
ctx.fillStyle = lit ? 'rgba(255,230,150,0.9)' : 'rgba(20,25,35,0.6)';
ctx.fillRect(cI*(128/cols)+4, r*(256/rows)+4, 128/cols-8, 256/rows-8);
}
}
var tex = new THREE.CanvasTexture(c);
tex.wrapS = THREE.RepeatWrapping; tex.wrapT = THREE.RepeatWrapping;
return tex;
}
// environment cube camera for real-time car paint reflections
var envCubeTarget = new THREE.WebGLCubeRenderTarget(64, {format:THREE.RGBFormat});
var envCubeCamera = new THREE.CubeCamera(0.1, 200, envCubeTarget);
var groundMat = new THREE.MeshLambertMaterial({color:0x2e2e2e});
var ground = new THREE.Mesh(new THREE.PlaneGeometry(CITY_SIZE*2, CITY_SIZE*2), groundMat);
ground.rotation.x = -Math.PI/2;
ground.receiveShadow = true;
scene.add(ground);
var roadMat = new THREE.MeshLambertMaterial({color:0x3a3a3a});
var sidewalkMat = new THREE.MeshLambertMaterial({color:0x999999});
var buildingMats = [0x8899aa,0xaa8866,0x778866,0x997788,0x668899].map(function(c){
return new THREE.MeshStandardMaterial({map: makeBuildingTexture(c), color:0xffffff, roughness:0.85});
});
var buildings = [];
var roadCellsH = []; // horizontal road segment centers
var roadCellsV = [];
for(var gx=-CITY_SIZE/2; gx<CITY_SIZE/2; gx+=BLOCK){ for(var gz=-CITY_SIZE/2; gz<CITY_SIZE/2; gz+=BLOCK){
var roadH = new THREE.Mesh(new THREE.PlaneGeometry(BLOCK,ROAD_W), roadMat);
roadH.rotation.x=-Math.PI/2; roadH.position.set(gx+BLOCK/2,0.01,gz);
scene.add(roadH);
roadCellsH.push({x:gx+BLOCK/2, z:gz});
var roadV = new THREE.Mesh(new THREE.PlaneGeometry(ROAD_W,BLOCK), roadMat);
roadV.rotation.x=-Math.PI/2; roadV.position.set(gx,0.01,gz+BLOCK/2);
scene.add(roadV);
roadCellsV.push({x:gx, z:gz+BLOCK/2});
// sidewalk strip around block interior
var sw = new THREE.Mesh(new THREE.PlaneGeometry(BLOCK-ROAD_W-2, BLOCK-ROAD_W-2), sidewalkMat);
sw.rotation.x=-Math.PI/2; sw.position.set(gx+BLOCK/2, 0.005, gz+BLOCK/2);
scene.add(sw);
if(Math.random()>0.15){
var h = 6 + Math.random()*30;
var w = 8 + Math.random()*6;
var mat = buildingMats[Math.floor(Math.random()*buildingMats.length)];
var b = new THREE.Mesh(new THREE.BoxGeometry(w,h,w), mat);
b.position.set(gx+BLOCK/2 + (Math.random()-0.5)*2, h/2, gz+BLOCK/2 + (Math.random()-0.5)*2);
b.castShadow = true; b.receiveShadow = true;
scene.add(b);
var isProperty = Math.random() < 0.25;
var propType = h > 20 ? 'apartment' : 'house';
var price = propType==='apartment' ? Math.round((800 + Math.random()*1200)/10)*10 : Math.round((300 + Math.random()*500)/10)*10;
buildings.push({mesh:b, w:w, d:w, isProperty:isProperty, propType:propType, price:price, owned:false});
if(isProperty){
var signGeo = new THREE.PlaneGeometry(1.4,0.6);
var signMat = new THREE.MeshBasicMaterial({color: propType==='apartment'?0x3399ff:0x33cc66, side:THREE.DoubleSide});
var sign = new THREE.Mesh(signGeo, signMat);
sign.position.set(b.position.x, 2.2, b.position.z + w/2 + 0.05);
scene.add(sign);
buildings[buildings.length-1].sign = sign;
}
}
// street lamp at every other intersection for night ambience
if(Math.random() < 0.12){
var poleMat = new THREE.MeshStandardMaterial({color:0x222222, metalness:0.6, roughness:0.5});
var pole = new THREE.Mesh(new THREE.CylinderGeometry(0.08,0.08,4.5,8), poleMat);
pole.position.set(gx-1.5, 2.25, gz-1.5);
pole.castShadow = true;
scene.add(pole);
var lampGlow = new THREE.PointLight(0xffcc77, 1.2, 14);
lampGlow.position.set(gx-1.5, 4.4, gz-1.5);
scene.add(lampGlow);
var bulb = new THREE.Mesh(new THREE.SphereGeometry(0.18,8,8), new THREE.MeshStandardMaterial({color:0xffddaa, emissive:0xffcc66, emissiveIntensity:1.5}));
bulb.position.copy(lampGlow.position);
scene.add(bulb);
}
}
}
// ---------- Realistic-ish car builder ----------
var carPalette = [0xdd2222,0x2255ff,0xffffff,0x222222,0xffcc00,0x00994d,0x888888,0x8b0000];
function makeCar(color, isPlayer){
var car = new THREE.Group();
var bodyMat = isPlayer
? new THREE.MeshPhysicalMaterial({color:color, metalness:0.7, roughness:0.2, clearcoat:1, clearcoatRoughness:0.1, envMap: envCubeTarget.texture, envMapIntensity:1.2})
: new THREE.MeshStandardMaterial({color:color, metalness:0.5, roughness:0.4});
var glassMat = new THREE.MeshPhysicalMaterial({color:0x0a1018, metalness:0.2, roughness:0.05, transparent:true, opacity:0.7, transmission:0.3});
var trimMat = new THREE.MeshStandardMaterial({color:0x111111, metalness:0.6, roughness:0.4});
var lightMatF = new THREE.MeshStandardMaterial({color:0xffffee, emissive:0xffffcc, emissiveIntensity:1.4});
var lightMatR = new THREE.MeshStandardMaterial({color:0x660000, emissive:0xff0000, emissiveIntensity:1.0});
// lower body (rounded via two stacked boxes + bevel illusion using scale)
var lower = new THREE.Mesh(new THREE.BoxGeometry(1.9,0.55,4.2), bodyMat);
lower.position.y = 0.45;
lower.castShadow = isPlayer;
car.add(lower);
// hood/trunk taper using extra boxes
var hood = new THREE.Mesh(new THREE.BoxGeometry(1.8,0.1,1.1), bodyMat);
hood.position.set(0,0.73,-1.55);
car.add(hood);
var trunk = new THREE.Mesh(new THREE.BoxGeometry(1.8,0.1,1.0), bodyMat);
trunk.position.set(0,0.73,1.6);
car.add(trunk);
// cabin (greenhouse), tapered narrower than body
var cabin = new THREE.Mesh(new THREE.BoxGeometry(1.5,0.55,2.1), bodyMat);
cabin.position.set(0,1.05,-0.15);
cabin.castShadow = isPlayer;
car.add(cabin);
// windows
var windshield = new THREE.Mesh(new THREE.BoxGeometry(1.42,0.42,0.06), glassMat);
windshield.position.set(0,1.05,-1.18);
windshield.rotation.x = -0.25;
car.add(windshield);
var rearWindow = new THREE.Mesh(new THREE.BoxGeometry(1.42,0.4,0.06), glassMat);
rearWindow.position.set(0,1.05,0.88);
rearWindow.rotation.x = 0.3;
car.add(rearWindow);
var sideL = new THREE.Mesh(new THREE.BoxGeometry(0.05,0.4,1.8), glassMat);
sideL.position.set(-0.76,1.05,-0.15);
car.add(sideL);
var sideR = sideL.clone();
sideR.position.x = 0.76;
car.add(sideR);
// bumpers
var bumperF = new THREE.Mesh(new THREE.BoxGeometry(1.95,0.35,0.25), trimMat);
bumperF.position.set(0,0.35,-2.05);
car.add(bumperF);
var bumperR = bumperF.clone();
bumperR.position.z = 2.05;
car.add(bumperR);
// headlights / taillights
var hlGeo = new THREE.BoxGeometry(0.3,0.15,0.08);
var hlL = new THREE.Mesh(hlGeo, lightMatF); hlL.position.set(-0.7,0.55,-2.08); car.add(hlL);
var hlR = hlL.clone(); hlR.position.x = 0.7; car.add(hlR);
var tlGeo = new THREE.BoxGeometry(0.3,0.15,0.08);
var tlL = new THREE.Mesh(tlGeo, lightMatR); tlL.position.set(-0.7,0.55,2.08); car.add(tlL);
var tlR = tlL.clone(); tlR.position.x = 0.7; car.add(tlR);
// real headlight spotlights (player only — perf) + taillight glow
if(isPlayer){
var headL = new THREE.SpotLight(0xfff4dd, 6, 30, Math.PI/7, 0.4, 1.5);
headL.position.set(-0.7,0.6,-2.1);
var headLTarget = new THREE.Object3D();
headLTarget.position.set(-0.7,0,-12);
car.add(headLTarget);
headL.target = headLTarget;
car.add(headL);
var headR = headL.clone();
headR.position.set(0.7,0.6,-2.1);
var headRTarget = new THREE.Object3D();
headRTarget.position.set(0.7,0,-12);
car.add(headRTarget);
headR.target = headRTarget;
car.add(headR);
}
var tailGlow = new THREE.PointLight(0xff2222, 0.5, 2.5);
tailGlow.position.set(0,0.55,2.1);
car.add(tailGlow);
// wheels with rims
var wheelGeo = new THREE.CylinderGeometry(0.42,0.42,0.32,16);
var wheelMat = new THREE.MeshStandardMaterial({color:0x0a0a0a, roughness:0.9});
var rimMat = new THREE.MeshStandardMaterial({color:0xcccccc, metalness:0.9, roughness:0.2});
var wheelPositions = [[-0.98,0.42,1.35],[0.98,0.42,1.35],[-0.98,0.42,-1.35],[0.98,0.42,-1.35]];
car.wheels = [];
wheelPositions.forEach(function(p){
var wGroup = new THREE.Group();
var w = new THREE.Mesh(wheelGeo, wheelMat);
w.rotation.z = Math.PI/2;
w.castShadow = isPlayer;
wGroup.add(w);
var rim = new THREE.Mesh(new THREE.CylinderGeometry(0.2,0.2,0.34,10), rimMat);
rim.rotation.z = Math.PI/2;
wGroup.add(rim);
wGroup.position.set(p[0],p[1],p[2]);
car.add(wGroup);
car.wheels.push(wGroup);
});
car.userData.color = color;
return car;
}
var player = makeCar(0xcc1122, true);
player.position.set(0,0,0);
scene.add(player);
var playerState = {speed:0, heading:0, cash:0, wanted:0, ownedCount:0};
var nearbyProperty = null;
function updateBuyPrompt(){
var el = document.getElementById('buyPrompt');
nearbyProperty = null;
for(var i=0;i<buildings.length;i++){
var bdef = buildings[i];
if(!bdef.isProperty || bdef.owned) continue;
var dx = bdef.mesh.position.x - player.position.x;
var dz = bdef.mesh.position.z - player.position.z;
if(Math.sqrt(dx*dx+dz*dz) < bdef.w/2 + 5){
nearbyProperty = bdef;
break;
}
}
if(nearbyProperty){
el.style.display = 'block';
el.innerHTML = 'Buy this ' + nearbyProperty.propType + ' for $' + nearbyProperty.price + '?<br><button id="buyBtn">Buy</button>';
var btn = document.getElementById('buyBtn');
btn.onclick = function(){
if(nearbyProperty && !nearbyProperty.owned && playerState.cash >= nearbyProperty.price){
playerState.cash -= nearbyProperty.price;
nearbyProperty.owned = true;
playerState.ownedCount++;
document.getElementById('cash').textContent = playerState.cash;
document.getElementById('ownedCount').textContent = playerState.ownedCount;
if(nearbyProperty.sign){
nearbyProperty.sign.material.color.set(0xffd700);
}
showMsg('You bought a ' + nearbyProperty.propType + '!');
el.style.display = 'none';
} else if(nearbyProperty && playerState.cash < nearbyProperty.price){
showMsg("Can't afford it yet");
}
};
} else {
el.style.display = 'none';
}
}
// ---------- NPC traffic cars ----------
var npcCars = [];
function nearestRoadSnap(x,z){
// snap to nearest road lane center
var bx = Math.round((x)/BLOCK)*BLOCK;
var bz = Math.round((z)/BLOCK)*BLOCK;
return {x:bx, z:bz};
}
function spawnNpcCar(){
var color = carPalette[Math.floor(Math.random()*carPalette.length)];
var c = makeCar(color, false);
var horiz = Math.random()<0.5;
var idx = Math.floor(Math.random()* (horiz?roadCellsH.length:roadCellsV.length));
var cell = horiz ? roadCellsH[idx] : roadCellsV[idx];
c.position.set(cell.x + (Math.random()-0.5)*2, 0, cell.z + (Math.random()-0.5)*2);
c.rotation.y = horiz ? Math.PI/2 : 0;
scene.add(c);
npcCars.push({mesh:c, dir:horiz?'h':'v', speed: 4+Math.random()*3, sign: Math.random()<0.5?1:-1});
}
setLoadProgress(35, 'Building the city…');
for(var i=0;i<7;i++) spawnNpcCar();
// ---------- NPC pedestrians ----------
var peds = [];
function makePed(){
var g = new THREE.Group();
var skinMat = new THREE.MeshStandardMaterial({color:0xffcc99, roughness:0.8});
var shirtColors = [0x3355aa,0xaa3355,0x33aa55,0xaaaa33,0x555555,0x996633];
var shirtMat = new THREE.MeshStandardMaterial({color:shirtColors[Math.floor(Math.random()*shirtColors.length)], roughness:0.8});
var pantsMat = new THREE.MeshStandardMaterial({color:0x222233, roughness:0.8});
var body = new THREE.Mesh(new THREE.CylinderGeometry(0.18,0.18,0.7,8), shirtMat);
body.position.y = 0.9;
body.castShadow = false;
g.add(body);
var head = new THREE.Mesh(new THREE.SphereGeometry(0.16,10,10), skinMat);
head.position.y = 1.42;
head.castShadow = false;
g.add(head);
var legs = new THREE.Mesh(new THREE.CylinderGeometry(0.15,0.15,0.6,8), pantsMat);
legs.position.y = 0.35;
g.add(legs);
return g;
}
function spawnPed(){
var p = makePed();
var angle = Math.random()*Math.PI*2;
var dist = 10+Math.random()*60;
p.position.set(Math.cos(angle)*dist, 0, Math.sin(angle)*dist);
var heading = Math.random()*Math.PI*2;
scene.add(p);
peds.push({mesh:p, heading:heading, speed:1+Math.random()*0.8, timer:2+Math.random()*3});
}
setLoadProgress(60, 'Spawning citizens…');
for(var i=0;i<10;i++) spawnPed();
// ---------- Cops ----------
var cops = [];
function spawnCop(){
var c = makeCar(0x1133cc, false);
var lightbar = new THREE.Mesh(new THREE.BoxGeometry(0.9,0.15,0.4), new THREE.MeshStandardMaterial({color:0x222222}));
lightbar.position.set(0,1.4,-0.15);
c.add(lightbar);
var redLight = new THREE.Mesh(new THREE.BoxGeometry(0.4,0.12,0.35), new THREE.MeshStandardMaterial({color:0xff0000, emissive:0xff0000, emissiveIntensity:1}));
redLight.position.set(-0.25,1.4,-0.15);
c.add(redLight);
var blueLight = new THREE.Mesh(new THREE.BoxGeometry(0.4,0.12,0.35), new THREE.MeshStandardMaterial({color:0x0000ff, emissive:0x0000ff, emissiveIntensity:1}));
blueLight.position.set(0.25,1.4,-0.15);
c.add(blueLight);
c.userData.lights = [redLight, blueLight];
var angle = Math.random()*Math.PI*2;
c.position.set(player.position.x + Math.cos(angle)*40, 0, player.position.z + Math.sin(angle)*40);
scene.add(c);
cops.push({mesh:c, speed:0});
}
// ---------- Controls ----------
var input = {left:false, right:false, gas:false, brake:false};
function bindBtn(id, key){
var el = document.getElementById(id);
el.addEventListener('touchstart', function(e){e.preventDefault(); input[key]=true;});
el.addEventListener('touchend', function(e){e.preventDefault(); input[key]=false;});
el.addEventListener('mousedown', function(){input[key]=true;});
el.addEventListener('mouseup', function(){input[key]=false;});
el.addEventListener('mouseleave', function(){input[key]=false;});
}
bindBtn('left','left'); bindBtn('right','right'); bindBtn('gas','gas'); bindBtn('brake','brake');
window.addEventListener('keydown', function(e){
if(e.key==='ArrowLeft'||e.key==='a') input.left=true;
if(e.key==='ArrowRight'||e.key==='d') input.right=true;
if(e.key==='ArrowUp'||e.key==='w') input.gas=true;
if(e.key==='ArrowDown'||e.key==='s') input.brake=true;
});
window.addEventListener('keyup', function(e){
if(e.key==='ArrowLeft'||e.key==='a') input.left=false;
if(e.key==='ArrowRight'||e.key==='d') input.right=false;
if(e.key==='ArrowUp'||e.key==='w') input.gas=false;
if(e.key==='ArrowDown'||e.key==='s') input.brake=false;
});
// ---------- Cash pickups ----------
var cashDrops = [];
function spawnCashDrop(){
var geo = new THREE.OctahedronGeometry(0.6);
var mat = new THREE.MeshBasicMaterial({color:0xffd700});
var m = new THREE.Mesh(geo, mat);
var angle = Math.random()*Math.PI*2;
var dist = 15 + Math.random()*30;
m.position.set(player.position.x + Math.cos(angle)*dist, 1, player.position.z + Math.sin(angle)*dist);
scene.add(m);
cashDrops.push(m);
}
setLoadProgress(85, 'Placing cash drops…');
for(var i=0;i<6;i++) spawnCashDrop();
function showMsg(text){
var el = document.getElementById('msg');
el.textContent = text;
el.style.display = 'block';
setTimeout(function(){ el.style.display='none'; }, 1500);
}
function applyCarDamage(car){
if(!car.userData.damage) car.userData.damage = 0;
car.userData.damage = Math.min(1, car.userData.damage + 0.25);
var dmg = car.userData.damage;
// darken and dent body meshes
car.children.forEach(function(part){
if(part.material && part.material.color && part.geometry && part.geometry.type==='BoxGeometry'){
part.material.color.multiplyScalar(1 - dmg*0.15);
part.scale.x = 1 - dmg*0.08*Math.random();
part.scale.y = 1 - dmg*0.08*Math.random();
}
});
spawnSmoke(car.position);
}
var smokes = [];
function spawnSmoke(pos){
var geo = new THREE.SphereGeometry(0.3,6,6);
var mat = new THREE.MeshBasicMaterial({color:0x555555, transparent:true, opacity:0.6});
var m = new THREE.Mesh(geo, mat);
m.position.set(pos.x, 1.2, pos.z);
scene.add(m);
smokes.push({mesh:m, life:1.0});
}
function carCollision(a, b, aIsNpcObj, bIsNpcObj){
var dx = a.position.x - b.position.x;
var dz = a.position.z - b.position.z;
var dist = Math.sqrt(dx*dx+dz*dz);
if(dist < 2.6 && dist > 0.001){
var push = (2.6-dist)/2;
var nx = dx/dist, nz = dz/dist;
a.position.x += nx*push; a.position.z += nz*push;
b.position.x -= nx*push; b.position.z -= nz*push;
applyCarDamage(a);
applyCarDamage(b);
return true;
}
return false;
}
function collidesBuilding(x,z){
for(var i=0;i<buildings.length;i++){
var b = buildings[i].mesh;
if(Math.abs(x-b.position.x) < buildings[i].w/2+1.2 && Math.abs(z-b.position.z) < buildings[i].d/2+1.2){
return true;
}
}
return false;
}
// ---------- Game loop ----------
var clock = new THREE.Clock();
function spinWheels(car, dist){
if(!car.wheels) return;
car.wheels.forEach(function(w){ w.children[0].rotation.x -= dist*2; w.children[1].rotation.x -= dist*2; });
}
function update(dt){
// player physics
var accel = 12, maxSpeed = 26, turnSpeed = 1.8, friction = 6;
if(input.gas) playerState.speed += accel*dt;
else if(input.brake) playerState.speed -= accel*1.5*dt;
else {
if(playerState.speed>0) playerState.speed -= friction*dt;
if(playerState.speed<0) playerState.speed += friction*dt;
}
playerState.speed = Math.max(-maxSpeed/2, Math.min(maxSpeed, playerState.speed));
var turnFactor = Math.min(1, Math.abs(playerState.speed)/6);
if(input.left) playerState.heading += turnSpeed*dt*turnFactor*(playerState.speed>=0?1:-1);
if(input.right) playerState.heading -= turnSpeed*dt*turnFactor*(playerState.speed>=0?1:-1);
var nx = player.position.x + Math.sin(playerState.heading)*playerState.speed*dt;
var nz = player.position.z + Math.cos(playerState.heading)*playerState.speed*dt;
if(!collidesBuilding(nx,nz)){
player.position.x = nx;
player.position.z = nz;
spinWheels(player, playerState.speed*dt);
} else {
playerState.speed *= -0.3;
}
player.rotation.y = playerState.heading;
// camera follow
var camDist = 12, camHeight = 6;
var camX = player.position.x - Math.sin(playerState.heading)*camDist;
var camZ = player.position.z - Math.cos(playerState.heading)*camDist;
camera.position.lerp(new THREE.Vector3(camX, camHeight, camZ), 0.1);
camera.lookAt(player.position.x, 1, player.position.z);
// NPC cars drive along their lane back and forth
npcCars.forEach(function(npc){
var m = npc.mesh;
var moveDist = npc.speed*npc.sign*dt;
if(npc.dir==='h'){
m.position.x += moveDist;
m.rotation.y = npc.sign>0 ? Math.PI/2 : -Math.PI/2;
} else {
m.position.z += moveDist;
m.rotation.y = npc.sign>0 ? 0 : Math.PI;
}
spinWheels(m, moveDist);
// bounce within city bounds
if(Math.abs(m.position.x) > CITY_SIZE/2 || Math.abs(m.position.z) > CITY_SIZE/2){
npc.sign *= -1;
}
// collision with player
var hitPlayer = carCollision(player, m);
if(hitPlayer){
playerState.speed *= -0.4;
npc.speed = Math.max(1, npc.speed*0.5);
showMsg('Crash!');
}
// simple avoid-player nudge (soft)
var d = m.position.distanceTo(player.position);
if(d < 3.5 && !hitPlayer){
npc.sign *= (Math.random()<0.02 ? -1 : 1);
}
});
// NPC vs NPC collisions
for(var i=0;i<npcCars.length;i++){
for(var j=i+1;j<npcCars.length;j++){
if(carCollision(npcCars[i].mesh, npcCars[j].mesh)){
npcCars[i].sign *= -1;
npcCars[j].sign *= -1;
}
}
}
// smoke particles
for(var i=smokes.length-1;i>=0;i--){
var s = smokes[i];
s.life -= dt*0.7;
s.mesh.position.y += dt*0.5;
s.mesh.material.opacity = Math.max(0, s.life*0.6);
s.mesh.scale.setScalar(1 + (1-s.life)*1.5);
if(s.life<=0){
scene.remove(s.mesh);
smokes.splice(i,1);
}
}
// pedestrians wander
peds.forEach(function(p){
if(p.down){
p.downTimer -= dt;
p.mesh.position.x += p.knockX*dt;
p.mesh.position.z += p.knockZ*dt;
p.knockX *= 0.9; p.knockZ *= 0.9;
p.mesh.rotation.x = Math.min(Math.PI/2, p.mesh.rotation.x + dt*8);
if(p.downTimer<=0){
p.down = false;
p.mesh.rotation.x = 0;
}
return;
}
p.timer -= dt;
if(p.timer<=0){
p.heading = Math.random()*Math.PI*2;
p.timer = 2+Math.random()*3;
}
var nx2 = p.mesh.position.x + Math.sin(p.heading)*p.speed*dt;
var nz2 = p.mesh.position.z + Math.cos(p.heading)*p.speed*dt;
if(!collidesBuilding(nx2,nz2)){
p.mesh.position.x = nx2;
p.mesh.position.z = nz2;
p.mesh.rotation.y = p.heading;
} else {
p.heading += Math.PI/2;
}
// hit by player
var pd = p.mesh.position.distanceTo(player.position);
if(pd < 1.6 && Math.abs(playerState.speed) > 3){
p.down = true;
p.downTimer = 3+Math.random()*2;
var dx3 = p.mesh.position.x - player.position.x;
var dz3 = p.mesh.position.z - player.position.z;
var len = Math.max(0.1, Math.sqrt(dx3*dx3+dz3*dz3));
p.knockX = (dx3/len) * Math.abs(playerState.speed) * 1.2;
p.knockZ = (dz3/len) * Math.abs(playerState.speed) * 1.2;
showMsg('Yikes!');
}
});
// cash pickups
for(var i=cashDrops.length-1;i>=0;i--){
var d2 = cashDrops[i];
d2.rotation.y += dt*3;
if(player.position.distanceTo(d2.position) < 2.5){
scene.remove(d2);
cashDrops.splice(i,1);
playerState.cash += 100;
document.getElementById('cash').textContent = playerState.cash;
showMsg('+$100');
spawnCashDrop();
if(Math.random()<0.4){
playerState.wanted = Math.min(3, playerState.wanted+1);
spawnCop();
}
}
}
// cops chase and flash lights
cops.forEach(function(cop){
var dx = player.position.x - cop.mesh.position.x;
var dz = player.position.z - cop.mesh.position.z;
var dist = Math.sqrt(dx*dx+dz*dz);
var targetHeading = Math.atan2(dx,dz);
cop.mesh.rotation.y += (targetHeading - cop.mesh.rotation.y)*0.05;
var spd = 14*dt;
if(dist>3){
cop.mesh.position.x += Math.sin(cop.mesh.rotation.y)*spd;
cop.mesh.position.z += Math.cos(cop.mesh.rotation.y)*spd;
spinWheels(cop.mesh, spd);
}
if(cop.mesh.userData.lights){
var t = Math.floor(clock.elapsedTime*6)%2;
cop.mesh.userData.lights[0].material.emissiveIntensity = t? 1:0.1;
cop.mesh.userData.lights[1].material.emissiveIntensity = t? 0.1:1;
}
if(dist < 2.5 && playerState.wanted>0){
playerState.wanted = Math.max(0, playerState.wanted-1);
playerState.cash = Math.max(0, playerState.cash-50);
document.getElementById('cash').textContent = playerState.cash;
showMsg('Busted! -$50');
cop.mesh.position.set(player.position.x+30, 0, player.position.z+30);
}
});
document.getElementById('wanted').textContent = playerState.wanted>0 ? '★'.repeat(playerState.wanted) : '';
updateBuyPrompt();
// speed-based vignette/motion-blur feel
var speedRatio = Math.min(1, Math.abs(playerState.speed)/26);
document.getElementById('speedFx').style.opacity = (speedRatio*0.7).toFixed(2);
}
function animate(){
requestAnimationFrame(animate);
var dt = Math.min(0.05, clock.getDelta());
try{
update(dt);
// update reflection env map from player's position every few frames for perf
if(!window._envFrameCounter) window._envFrameCounter = 0;
window._envFrameCounter++;
if(window._envFrameCounter % 90 === 0){
player.visible = false;
envCubeCamera.position.copy(player.position);
envCubeCamera.position.y += 1.5;
envCubeCamera.update(renderer, scene);
player.visible = true;
}
renderer.render(scene, camera);
}catch(e){
window.onerror(e.message);
}
}
setLoadProgress(100, 'Ready');
animate();
setTimeout(function(){
var ls = document.getElementById('loadScreen');
if(ls){
ls.classList.add('hidden');
setTimeout(function(){ ls.style.display = 'none'; }, 700);
}
}, 300);
} catch(e){
var e2=document.getElementById('err');
e2.style.display='block';
e2.textContent = 'Error at stage [' + window.CITY_HEIST_STAGE + ']: ' + (e && e.message ? e.message : e) + (e && e.stack ? ('\n'+e.stack) : '');
var ls2 = document.getElementById('loadScreen');
if(ls2) ls2.style.display = 'none';
}
}
</script>
</body>
</html>
Game Source: City Heist
Creator: LunarGalaxy87
Libraries: three
Complexity: complex (779 lines, 30.4 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: city-heist-lunargalaxy87" to link back to the original. Then publish at arcadelab.ai/publish.