City Drive Open World
by GoldenKnight361480 lines20.8 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0">
<title>City Drive Open World</title>
<style>
*{
margin:0;
padding:0;
box-sizing:border-box;
}
html,body{
width:100%;
height:100%;
overflow:hidden;
background:#79a9c7;
font-family:Arial,sans-serif;
user-select:none;
}
canvas{
display:block;
}
#ui{
position:fixed;
inset:0;
pointer-events:none;
z-index:20;
}
#title{
position:absolute;
top:18px;
left:20px;
color:white;
font-size:20px;
font-weight:bold;
text-shadow:0 2px 5px #000;
}
#stats{
position:absolute;
top:18px;
right:20px;
text-align:right;
color:white;
text-shadow:0 2px 5px #000;
}
#speed{
font-size:32px;
font-weight:bold;
}
#mode{
font-size:14px;
margin-top:3px;
opacity:.9;
}
#help{
position:absolute;
left:20px;
bottom:20px;
padding:10px 14px;
background:rgba(0,0,0,.55);
color:white;
border-radius:9px;
font-size:13px;
line-height:1.6;
}
#message{
position:absolute;
left:50%;
bottom:100px;
transform:translateX(-50%);
color:white;
background:rgba(0,0,0,.65);
padding:10px 18px;
border-radius:20px;
opacity:0;
transition:.2s;
font-weight:bold;
}
#cross{
position:absolute;
left:50%;
top:50%;
transform:translate(-50%,-50%);
color:white;
font-size:20px;
text-shadow:0 1px 3px black;
}
#mini{
position:absolute;
left:20px;
top:65px;
width:130px;
height:130px;
border-radius:12px;
background:rgba(0,0,0,.7);
border:2px solid rgba(255,255,255,.7);
}
#mobile{
display:none;
}
@media(max-width:700px){
#help{
display:none;
}
#mobile{
display:block;
position:absolute;
inset:0;
}
.mb{
position:absolute;
width:62px;
height:62px;
border-radius:50%;
border:1px solid rgba(255,255,255,.4);
background:rgba(0,0,0,.45);
color:white;
font-size:25px;
pointer-events:auto;
touch-action:none;
}
#up{
bottom:135px;
left:85px;
}
#down{
bottom:35px;
left:85px;
}
#left{
bottom:85px;
left:20px;
}
#right{
bottom:85px;
left:150px;
}
#enter{
right:25px;
bottom:75px;
width:78px;
height:78px;
font-size:14px;
}
#boost{
right:25px;
bottom:170px;
width:65px;
height:65px;
font-size:13px;
}
}
</style>
</head>
<body>
<div id="ui">
<div id="title">
CITY DRIVE
</div>
<div id="stats">
<div id="speed">0 KM/H</div>
<div id="mode">ON FOOT</div>
</div>
<canvas id="mini" width="130" height="130"></canvas>
<div id="cross">+</div>
<div id="help">
WASD = Move / Drive<br>
SHIFT = Boost<br>
E = Enter / Exit Car<br>
Mouse = Look Around
</div>
<div id="message"></div>
<div id="mobile">
<button class="mb" id="up">▲</button>
<button class="mb" id="down">▼</button>
<button class="mb" id="left">◀</button>
<button class="mb" id="right">▶</button>
<button class="mb" id="boost">BOOST</button>
<button class="mb" id="enter">ENTER</button>
</div>
</div>
<script>
(function(){
"use strict";
const THREE = window.THREE;
if(!THREE){
document.body.innerHTML =
'<div style="color:white;background:#111;height:100vh;display:flex;align-items:center;justify-content:center;font:20px Arial;text-align:center;padding:30px">Three.js did not load. Please refresh the ArcadeLab preview.</div>';
return;
}
/* =========================
BASIC SETUP
========================= */
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x79a9c7);
scene.fog = new THREE.Fog(
0x79a9c7,
180,
850
);
const camera = new THREE.PerspectiveCamera(
65,
innerWidth/innerHeight,
0.1,
1200
);
const renderer = new THREE.WebGLRenderer({
antialias:true,
powerPreference:"high-performance"
});
renderer.setPixelRatio(
Math.min(devicePixelRatio,1.5)
);
renderer.setSize(
innerWidth,
innerHeight
);
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
/* =========================
LIGHTING
========================= */
const hemi = new THREE.HemisphereLight(
0xbfe4ff,
0x59634e,
2.1
);
scene.add(hemi);
const sun = new THREE.DirectionalLight(
0xffffff,
2.2
);
sun.position.set(
150,
260,
100
);
sun.castShadow=true;
sun.shadow.mapSize.width=1024;
sun.shadow.mapSize.height=1024;
sun.shadow.camera.left=-300;
sun.shadow.camera.right=300;
sun.shadow.camera.top=300;
sun.shadow.camera.bottom=-300;
scene.add(sun);
/* =========================
WORLD
========================= */
const WORLD=2400;
const ROAD=240;
const groundMat =
new THREE.MeshStandardMaterial({
color:0x55724d,
roughness:1
});
const ground =
new THREE.Mesh(
new THREE.PlaneGeometry(WORLD,WORLD),
groundMat
);
ground.rotation.x=-Math.PI/2;
ground.receiveShadow=true;
scene.add(ground);
/* =========================
MATERIAL HELPERS
========================= */
function mat(color){
return new THREE.MeshStandardMaterial({
color:color,
roughness:.82
});
}
const roadMat=mat(0x25282b);
const sidewalkMat=mat(0x777878);
const whiteMat=mat(0xe9e9e9);
const darkMat=mat(0x151719);
const glassMat=
new THREE.MeshStandardMaterial({
color:0x355468,
roughness:.25,
metalness:.25
});
/* =========================
ROADS
========================= */
function makeRoads(){
const roadSize=WORLD;
for(
let x=-WORLD/2;
x<=WORLD/2;
x+=ROAD
){
const r=new THREE.Mesh(
new THREE.BoxGeometry(
82,
.08,
roadSize
),
roadMat
);
r.position.set(x,.04,0);
r.receiveShadow=true;
scene.add(r);
const s1=new THREE.Mesh(
new THREE.BoxGeometry(
7,
.1,
roadSize
),
sidewalkMat
);
s1.position.set(x-48,.07,0);
scene.add(s1);
const s2=s1.clone();
s2.position.x=x+48;
scene.add(s2);
for(
let z=-WORLD/2;
z<WORLD/2;
z+=35
){
const line=new THREE.Mesh(
new THREE.BoxGeometry(
2,
.11,
18
),
whiteMat
);
line.position.set(
x,
.11,
z
);
scene.add(line);
}
}
for(
let z=-WORLD/2;
z<=WORLD/2;
z+=ROAD
){
const r=new THREE.Mesh(
new THREE.BoxGeometry(
roadSize,
.08,
82
),
roadMat
);
r.position.set(0,.05,z);
r.receiveShadow=true;
scene.add(r);
const s1=new THREE.Mesh(
new THREE.BoxGeometry(
roadSize,
.1,
7
),
sidewalkMat
);
s1.position.set(0,.08,z-48);
scene.add(s1);
const s2=s1.clone();
s2.position.z=z+48;
scene.add(s2);
for(
let x=-WORLD/2;
x<WORLD/2;
x+=35
){
const line=new THREE.Mesh(
new THREE.BoxGeometry(
18,
.11,
2
),
whiteMat
);
line.position.set(
x,
.11,
z
);
scene.add(line);
}
}
}
makeRoads();
/* =========================
BUILDINGS
========================= */
const buildingColors=[
0x59636b,
0x746e67,
0x656b70,
0x80786b,
0x4f5962,
0x6d7072
];
const buildings=[];
function makeBuilding(x,z){
const w=55+Math.random()*35;
const d=55+Math.random()*35;
const h=35+Math.random()*100;
const g=new THREE.BoxGeometry(
w,h,d
);
const b=new THREE.Mesh(
g,
mat(
buildingColors[
Math.floor(
Math.random()*buildingColors.length
)
]
)
);
b.position.set(
x,
h/2,
z
);
b.castShadow=true;
b.receiveShadow=true;
scene.add(b);
buildings.push(b);
/* roof */
if(Math.random()<.45){
const roof=new THREE.Mesh(
new THREE.BoxGeometry(
w*.5,
4,
d*.5
),
darkMat
);
roof.position.set(
x,
h+2,
z
);
roof.castShadow=true;
scene.add(roof);
}
}
/* City blocks */
for(
let x=-WORLD/2+120;
x<WORLD/2-120;
x+=ROAD
){
for(
let z=-WORLD/2+120;
z<WORLD/2-120;
z+=ROAD
){
const spots=[
[-65,-65],
[65,-65],
[-65,65],
[65,65]
];
for(const p of spots){
if(Math.random()<.78){
makeBuilding(
x+p[0],
z+p[1]
);
}
}
}
}
/* =========================
TREES
========================= */
function makeTree(x,z){
const trunk=new THREE.Mesh(
new THREE.CylinderGeometry(
2.5,3,14,8
),
mat(0x68482d)
);
trunk.position.set(
x,
7,
z
);
trunk.castShadow=true;
scene.add(trunk);
const crown=new THREE.Mesh(
new THREE.SphereGeometry(
11,
8,
7
),
mat(
Math.random()<.5
?0x285b32
:0x34733b
)
);
crown.position.set(
x,
19,
z
);
crown.castShadow=true;
scene.add(crown);
}
for(let i=0;i<110;i++){
const x=
Math.random()*WORLD-WORLD/2;
const z=
Math.random()*WORLD-WORLD/2;
const rx=Math.abs(
((x+WORLD/2)%ROAD)-ROAD/2
);
const rz=Math.abs(
((z+WORLD/2)%ROAD)-ROAD/2
);
if(rx>55 && rz>55){
makeTree(x,z);
}
}
/* =========================
CAR CREATION
========================= */
const carColors=[
0xc52b2b,
0x1e4d9c,
0xe4e4e4,
0x17191b,
0xd59b27,
0x2e7440
];
function createCar(color){
const group=new THREE.Group();
const body=new THREE.Mesh(
new THREE.BoxGeometry(
12,
4,
24
),
mat(color)
);
body.position.y=5;
body.castShadow=true;
group.add(body);
const cabin=new THREE.Mesh(
new THREE.BoxGeometry(
9,
4.5,
11
),
glassMat
);
cabin.position.set(
0,
8,
-1
);
cabin.castShadow=true;
group.add(cabin);
const front=new THREE.Mesh(
new THREE.BoxGeometry(
9.5,
1.5,
3
),
mat(0x222222)
);
front.position.set(
0,
7,
-8
);
group.add(front);
const wheelGeo=
new THREE.CylinderGeometry(
2.7,
2.7,
2,
12
);
const wheelMat=mat(0x111111);
const wheels=[];
for(const x of [-7,7]){
for(const z of [-7.5,7.5]){
const w=new THREE.Mesh(
wheelGeo,
wheelMat
);
w.rotation.z=Math.PI/2;
w.position.set(
x,
3,
z
);
w.castShadow=true;
group.add(w);
wheels.push(w);
}
}
group.userData.wheels=wheels;
return group;
}
/* =========================
PLAYER CAR
========================= */
const player={
x:0,
z:20,
angle:0,
speed:0,
inCar:false,
car:null
};
const playerCar=createCar(
0xb92c2c
);
playerCar.position.set(
player.x,
0,
player.z
);
scene.add(playerCar);
player.car=playerCar;
/* =========================
TRAFFIC
========================= */
const traffic=[];
function addTraffic(x,z,angle){
const c=createCar(
carColors[
Math.floor(
Math.random()*carColors.length
)
]
);
c.position.set(
x,0,z
);
c.rotation.y=angle;
c.userData.speed=
12+Math.random()*10;
c.userData.angle=angle;
scene.add(c);
traffic.push(c);
}
for(let i=0;i<28;i++){
const horizontal=
Math.random()<.5;
const roadIndex=
Math.floor(
Math.random()*8
)-4;
if(horizontal){
addTraffic(
Math.random()*1800-900,
roadIndex*ROAD,
Math.random()<.5?0:Math.PI
);
}else{
addTraffic(
roadIndex*ROAD,
Math.random()*1800-900,
Math.PI/2
);
}
}
/* =========================
PLAYER CHARACTER
========================= */
const person=new THREE.Group();
const body=new THREE.Mesh(
new THREE.CapsuleGeometry(
2.2,
6,
4,
8
),
mat(0x263b58)
);
body.position.y=7;
person.add(body);
const head=new THREE.Mesh(
new THREE.SphereGeometry(
2.3,
12,
8
),
mat(0xc98d67)
);
head.position.y=14;
person.add(head);
person.position.set(
player.x,
0,
player.z+25
);
scene.add(person);
/* =========================
INPUT
========================= */
const keys={};
addEventListener(
"keydown",
function(e){
keys[e.key.toLowerCase()]=true;
if(e.key.toLowerCase()==="e"){
enterExitCar();
}
}
);
addEventListener(
"keyup",
function(e){
keys[e.key.toLowerCase()]=false;
}
);
/* =========================
MOBILE BUTTONS
========================= */
function bindButton(id,key){
const b=document.getElementById(id);
if(!b)return;
b.addEventListener(
"pointerdown",
e=>{
e.preventDefault();
keys[key]=true;
}
);
b.addEventListener(
"pointerup",
e=>{
e.preventDefault();
keys[key]=false;
}
);
b.addEventListener(
"pointercancel",
()=>{
keys[key]=false;
}
);
b.addEventListener(
"pointerleave",
()=>{
keys[key]=false;
}
);
}
bindButton("up","w");
bindButton("down","s");
bindButton("left","a");
bindButton("right","d");
bindButton("boost","shift");
document.getElementById(
"enter"
).addEventListener(
"pointerdown",
e=>{
e.preventDefault();
enterExitCar();
}
);
/* =========================
MOUSE CAMERA
========================= */
let dragging=false;
let lastX=0;
let cameraYaw=0;
let cameraPitch=.42;
renderer.domElement.addEventListener(
"pointerdown",
e=>{
dragging=true;
lastX=e.clientX;
}
);
addEventListener(
"pointerup",
()=>{
dragging=false;
}
);
addEventListener(
"pointermove",
e=>{
if(!dragging)return;
const dx=e.clientX-lastX;
lastX=e.clientX;
cameraYaw-=dx*.006;
cameraPitch=
Math.max(
.2,
Math.min(
.8,
cameraPitch
)
);
}
);
/* =========================
ENTER / EXIT
========================= */
function enterExitCar(){
if(player.inCar){
player.inCar=false;
player.speed=0;
person.visible=true;
person.position.set(
player.x+12,
0,
player.z
);
showMessage(
"Exited vehicle"
);
return;
}
let closest=null;
let closestDist=40;
for(const c of traffic){
const d=
c.position.distanceTo(
new THREE.Vector3(
player.x,
0,
player.z
)
);
if(d<closestDist){
closest=c;
closestDist=d;
}
}
if(
player.car.position.distanceTo(
new THREE.Vector3(
player.x,
0,
player.z
)
)<40
){
closest=player.car;
}
if(closest){
if(closest!==player.car){
player.car.position.copy(
closest.position
);
player.car.rotation.copy(
closest.rotation
);
scene.remove(closest);
traffic.splice(
traffic.indexOf(closest),
1
);
}
player.x=
player.car.position.x;
player.z=
player.car.position.z;
player.angle=
player.car.rotation.y;
player.inCar=true;
person.visible=false;
showMessage(
"Vehicle entered"
);
}
}
/* =========================
MESSAGE
========================= */
let messageTimer=0;
function showMessage(t){
const el=
document.getElementById(
"message"
);
el.textContent=t;
el.style.opacity=1;
messageTimer=2;
}
/* =========================
MOVEMENT
========================= */
const clock=new THREE.Clock();
function update(dt){
if(player.inCar){
let acceleration=0;
if(keys["w"])
acceleration+=55;
if(keys["s"])
acceleration-=45;
player.speed+=
acceleration*dt;
if(!keys["w"] && !keys["s"]){
player.speed*=
Math.pow(.12,dt);
}
const maxSpeed=
keys["shift"]?115:82;
player.speed=
Math.max(
-35,
Math.min(
maxSpeed,
player.speed
)
);
const steering=
(keys["a"]?-1:0)+
(keys["d"]?1:0);
if(Math.abs(player.speed)>2){
player.angle+=
steering*
dt*
1.8*
Math.min(
Math.abs(player.speed)/45,
1.5
)*
(player.speed>=0?1:-1);
}
player.x+=
Math.sin(player.angle)*
player.speed*
dt;
player.z+=
Math.cos(player.angle)*
player.speed*
dt;
player.x=
THREE.MathUtils.clamp(
player.x,
-WORLD/2+20,
WORLD/2-20
);
player.z=
THREE.MathUtils.clamp(
player.z,
-WORLD/2+20,
WORLD/2-20
);
player.car.position.set(
player.x,
0,
player.z
);
player.car.rotation.y=
player.angle;
for(
const w of player.car.userData.wheels
){
w.rotation.x-=
player.speed*dt*.8;
}
}else{
let dx=0;
let dz=0;
if(keys["w"])dz-=1;
if(keys["s"])dz+=1;
if(keys["a"])dx-=1;
if(keys["d"])dx+=1;
if(dx||dz){
const len=
Math.hypot(dx,dz);
dx/=len;
dz/=len;
const sp=
keys["shift"]?13:7;
player.x+=dx*sp*dt;
player.z+=dz*sp*dt;
player.x=
THREE.MathUtils.clamp(
player.x,
-WORLD/2+20,
WORLD/2-20
);
player.z=
THREE.MathUtils.clamp(
player.z,
-WORLD/2+20,
WORLD/2-20
);
person.position.set(
player.x,
0,
player.z
);
person.rotation.y=
Math.atan2(
dx,
dz
);
}
player.speed=0;
}
/* traffic */
for(const c of traffic){
const s=c.userData.speed;
c.position.x+=
Math.sin(
c.userData.angle
)*s*dt;
c.position.z+=
Math.cos(
c.userData.angle
)*s*dt;
if(c.position.x>WORLD/2)
c.position.x=-WORLD/2;
if(c.position.x<-WORLD/2)
c.position.x=WORLD/2;
if(c.position.z>WORLD/2)
c.position.z=-WORLD/2;
if(c.position.z<-WORLD/2)
c.position.z=WORLD/2;
}
/* message timer */
if(messageTimer>0){
messageTimer-=dt;
if(messageTimer<=0){
document.getElementById(
"message"
).style.opacity=0;
}
}
/* HUD */
document.getElementById(
"speed"
).textContent=
Math.round(
Math.abs(player.speed)*1.25
)+" KM/H";
document.getElementById(
"mode"
).textContent=
player.inCar
?"DRIVING"
:"ON FOOT";
}
/* =========================
CAMERA
========================= */
function updateCamera(){
let targetX=
player.inCar
?player.x
:person.position.x;
let targetZ=
player.inCar
?player.z
:person.position.z;
const distance=
player.inCar?70:48;
const height=
player.inCar?34:25;
const desiredX=
targetX+
Math.sin(cameraYaw)*
distance;
const desiredZ=
targetZ+
Math.cos(cameraYaw)*
distance;
camera.position.x+=
(desiredX-camera.position.x)*.08;
camera.position.y+=
(height-camera.position.y)*.08;
camera.position.z+=
(desiredZ-camera.position.z)*.08;
camera.lookAt(
targetX,
5,
targetZ
);
}
/* =========================
MINIMAP
========================= */
const mini=
document.getElementById("mini");
const mctx=
mini.getContext("2d");
function drawMini(){
const w=130;
mctx.clearRect(
0,0,w,w
);
mctx.fillStyle="#24352b";
mctx.fillRect(
0,0,w,w
);
const scale=
w/WORLD;
mctx.fillStyle="#444";
for(
let x=-WORLD/2;
x<=WORLD/2;
x+=ROAGame Source: City Drive Open World
Creator: GoldenKnight36
Libraries: none
Complexity: complex (1480 lines, 20.8 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-drive-open-world-goldenknight36" to link back to the original. Then publish at arcadelab.ai/publish.