🎮ArcadeLab

Vice Coast — Realistic Open World

by ChromeBuilder44
1258 lines18.8 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Vice Coast — Realistic Open World</title>

<style>
*{box-sizing:border-box}
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#05070a}
body{font-family:Arial,sans-serif}
canvas{display:block}

#hud{
 position:fixed;
 left:20px;
 top:20px;
 color:white;
 z-index:10;
 pointer-events:none;
 text-shadow:0 2px 5px #000;
}
#title{font-size:22px;font-weight:bold;letter-spacing:3px}
#status{margin-top:8px;font-size:14px}
#mission{
 position:fixed;
 top:20px;
 right:20px;
 color:white;
 text-align:right;
 z-index:10;
 text-shadow:0 2px 5px #000;
}
#controls{
 position:fixed;
 bottom:18px;
 left:20px;
 color:#ddd;
 z-index:10;
 font-size:13px;
 text-shadow:0 2px 5px #000;
}
#vignette{
 position:fixed;
 inset:0;
 pointer-events:none;
 z-index:8;
 box-shadow:inset 0 0 180px rgba(0,0,0,.65);
}
#rain{
 position:fixed;
 inset:0;
 pointer-events:none;
 z-index:9;
 opacity:0;
}
</style>
</head>

<body>

<div id="hud">
 <div id="title">VICE COAST</div>
 <div id="status">Loading realistic world...</div>
</div>

<div id="mission">
 <b>FREE ROAM</b><br>
 Reach the glowing checkpoint
</div>

<div id="controls">
 WASD — Move &nbsp; | &nbsp; Shift — Sprint &nbsp; | &nbsp;
 N — Night &nbsp; | &nbsp; R — Rain &nbsp; | &nbsp; E — Cinematic Camera
</div>

<div id="vignette"></div>
<div id="rain"></div>

<script type="module">

import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.186.0/build/three.module.js";
import { OrbitControls } from
"https://cdn.jsdelivr.net/npm/three@0.186.0/examples/jsm/controls/OrbitControls.js";

/* =========================================================
   BASIC SETUP
========================================================= */

const scene = new THREE.Scene();

scene.background = new THREE.Color(0x8fb7d9);
scene.fog = new THREE.FogExp2(0x9bbbd1,0.0018);

const camera = new THREE.PerspectiveCamera(
 65,
 innerWidth/innerHeight,
 0.1,
 2500
);

camera.position.set(0,8,14);

const renderer = new THREE.WebGLRenderer({
 antialias:true,
 powerPreference:"high-performance"
});

renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,2));

renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;

renderer.outputColorSpace=THREE.SRGBColorSpace;
renderer.toneMapping=THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure=1.15;

document.body.appendChild(renderer.domElement);


/* =========================================================
   LIGHTING
========================================================= */

const sun = new THREE.DirectionalLight(0xffffff,4.0);

sun.position.set(
 180,
 260,
 100
);

sun.castShadow=true;

sun.shadow.mapSize.width=4096;
sun.shadow.mapSize.height=4096;

sun.shadow.camera.left=-500;
sun.shadow.camera.right=500;
sun.shadow.camera.top=500;
sun.shadow.camera.bottom=-500;

sun.shadow.bias=-0.0002;

scene.add(sun);

const hemi = new THREE.HemisphereLight(
 0xb9d8ff,
 0x26301f,
 1.6
);

scene.add(hemi);

const streetAmbient = new THREE.AmbientLight(
 0xffffff,
 0.35
);

scene.add(streetAmbient);


/* =========================================================
   WORLD
========================================================= */

const world = new THREE.Group();
scene.add(world);

const CITY_SIZE=900;


/* =========================================================
   GROUND
========================================================= */

const groundMat = new THREE.MeshStandardMaterial({
 color:0x59685c,
 roughness:0.95,
 metalness:0
});

const ground = new THREE.Mesh(
 new THREE.PlaneGeometry(
  CITY_SIZE,
  CITY_SIZE,
  80,
  80
 ),
 groundMat
);

ground.rotation.x=-Math.PI/2;
ground.receiveShadow=true;

world.add(ground);


/* =========================================================
   ROAD MATERIALS
========================================================= */

const roadMat = new THREE.MeshStandardMaterial({
 color:0x181b1d,
 roughness:0.38,
 metalness:0.05
});

const wetRoadMat = new THREE.MeshStandardMaterial({
 color:0x111417,
 roughness:0.12,
 metalness:0.45
});


/* =========================================================
   ROADS
========================================================= */

const roads=[];

function createRoad(x,z,w,d){

 const road=new THREE.Mesh(
  new THREE.BoxGeometry(w,0.08,d),
  roadMat
 );

 road.position.set(x,0.04,z);
 road.receiveShadow=true;

 world.add(road);
 roads.push(road);

}


/* Main road grid */

for(let x=-400;x<=400;x+=100){
 createRoad(x,0,18,900);
}

for(let z=-400;z<=400;z+=100){
 createRoad(0,z,900,18);
}


/* =========================================================
   ROAD MARKINGS
========================================================= */

const markingMat=new THREE.MeshBasicMaterial({
 color:0xd8c98a
});

function marking(x,z,vertical){

 const m=new THREE.Mesh(
  new THREE.BoxGeometry(
   vertical?0.25:5,
   0.025,
   vertical?5:0.25
  ),
  markingMat
 );

 m.position.set(x,0.095,z);
 world.add(m);
}

for(let x=-400;x<=400;x+=100){
 for(let z=-420;z<=420;z+=20){
  marking(x,z,true);
 }
}

for(let z=-400;z<=400;z+=100){
 for(let x=-420;x<=420;x+=20){
  marking(x,z,false);
 }
}


/* =========================================================
   BUILDINGS
========================================================= */

const buildingMats=[];

const buildingColors=[
 0xaaa49a,
 0x7d8790,
 0x927c69,
 0x626b73,
 0xb5ad9b,
 0x77715f,
 0x4c5961
];

for(const c of buildingColors){

 buildingMats.push(
  new THREE.MeshStandardMaterial({
   color:c,
   roughness:.72,
   metalness:.08
  })
 );

}


function createBuilding(x,z,w,d,h){

 const mat=
  buildingMats[
   Math.floor(Math.random()*buildingMats.length)
  ];

 const building=new THREE.Mesh(
  new THREE.BoxGeometry(w,h,d),
  mat
 );

 building.position.set(
  x,
  h/2,
  z
 );

 building.castShadow=true;
 building.receiveShadow=true;

 world.add(building);


 /* windows */

 const windowMat=new THREE.MeshStandardMaterial({
  color:0x86a9bb,
  emissive:0x172c36,
  emissiveIntensity:.25,
  roughness:.25,
  metalness:.35
 });

 const rows=Math.max(2,Math.floor(h/4));
 const cols=Math.max(2,Math.floor(w/4));

 for(let r=1;r<rows;r++){

  for(let c=0;c<cols;c++){

   if(Math.random()<.2) continue;

   const windowMesh=new THREE.Mesh(
    new THREE.BoxGeometry(.8,.65,.05),
    windowMat
   );

   windowMesh.position.set(
    x-w/2+2+c*3,
    r*3,
    z-d/2-.03
   );

   world.add(windowMesh);
  }
 }

}


/* city blocks */

for(let x=-400;x<=400;x+=100){

 for(let z=-400;z<=400;z+=100){

  /* leave roads clear */

  const bx=x+30+(Math.random()*15);
  const bz=z+30+(Math.random()*15);

  const w=45+Math.random()*28;
  const d=45+Math.random()*28;
  const h=12+Math.random()*75;

  createBuilding(
   bx,
   bz,
   w,
   d,
   h
  );

 }

}


/* =========================================================
   PALM TREES
========================================================= */

function createPalm(x,z,scale=1){

 const palm=new THREE.Group();

 const trunkMat=new THREE.MeshStandardMaterial({
  color:0x79553b,
  roughness:.9
 });

 const leafMat=new THREE.MeshStandardMaterial({
  color:0x1f683c,
  roughness:.75
 });

 const trunk=new THREE.Mesh(
  new THREE.CylinderGeometry(
   .35,
   .55,
   7,
   10
  ),
  trunkMat
 );

 trunk.position.y=3.5;
 trunk.castShadow=true;

 palm.add(trunk);


 for(let i=0;i<9;i++){

  const leaf=new THREE.Mesh(
   new THREE.ConeGeometry(
    .38,
    5,
    7
   ),
   leafMat
  );

  leaf.position.y=7;
  leaf.rotation.z=Math.PI/2;
  leaf.rotation.y=
   i*Math.PI*2/9;

  leaf.translateX(2.2);

  leaf.castShadow=true;

  palm.add(leaf);

 }

 palm.position.set(x,0,z);
 palm.scale.setScalar(scale);

 world.add(palm);

}


for(let i=0;i<180;i++){

 const x=(Math.random()-.5)*850;
 const z=(Math.random()-.5)*850;

 /* avoid center roads */

 if(
  Math.abs(x%100)<14 ||
  Math.abs(z%100)<14
 ) continue;

 createPalm(
  x,
  z,
  .8+Math.random()*.7
 );

}


/* =========================================================
   BEACH + OCEAN
========================================================= */

const beachMat=new THREE.MeshStandardMaterial({
 color:0xd5bd8c,
 roughness:.95
});

const beach=new THREE.Mesh(
 new THREE.PlaneGeometry(900,90),
 beachMat
);

beach.rotation.x=-Math.PI/2;
beach.position.set(0,.02,-485);

world.add(beach);


const oceanMat=new THREE.MeshPhysicalMaterial({
 color:0x167da3,
 roughness:.08,
 metalness:.05,
 transmission:.05,
 transparent:true,
 opacity:.92
});

const ocean=new THREE.Mesh(
 new THREE.PlaneGeometry(
 1200,
 300,
 100,
 40
 ),
 oceanMat
);

ocean.rotation.x=-Math.PI/2;
ocean.position.set(0,-.1,-620);

world.add(ocean);


/* =========================================================
   PLAYER
========================================================= */

const player=new THREE.Group();

const skinMat=new THREE.MeshStandardMaterial({
 color:0xb98262,
 roughness:.7
});

const shirtMat=new THREE.MeshStandardMaterial({
 color:0x252c35,
 roughness:.7
});

const body=new THREE.Mesh(
 new THREE.CapsuleGeometry(
  .55,
  1.4,
  8,
  16
 ),
 shirtMat
);

body.position.y=1.35;
body.castShadow=true;

player.add(body);

const head=new THREE.Mesh(
 new THREE.SphereGeometry(.43,24,24),
 skinMat
);

head.position.y=2.55;
head.castShadow=true;

player.add(head);

player.position.set(0,0,30);

world.add(player);


/* =========================================================
   VEHICLES
========================================================= */

const cars=[];

const carColors=[
 0xb52d2d,
 0x22262b,
 0xd6d6d2,
 0x315d88,
 0xb8892f,
 0x4c7654
];


function createCar(x,z,angle=0){

 const car=new THREE.Group();

 const bodyMat=new THREE.MeshPhysicalMaterial({
  color:
   carColors[
    Math.floor(Math.random()*carColors.length)
   ],
  roughness:.2,
  metalness:.75,
  clearcoat:1,
  clearcoatRoughness:.08
 });

 const body=new THREE.Mesh(
  new THREE.BoxGeometry(4.4,1.1,2),
  bodyMat
 );

 body.position.y=.9;
 body.castShadow=true;

 car.add(body);


 const cabinMat=new THREE.MeshPhysicalMaterial({
  color:0x172b38,
  roughness:.08,
  metalness:.15,
  transmission:.05,
  transparent:true,
  opacity:.9
 });

 const cabin=new THREE.Mesh(
  new THREE.BoxGeometry(2.2,.85,1.65),
  cabinMat
 );

 cabin.position.set(-.1,1.65,0);
 cabin.castShadow=true;

 car.add(cabin);


 const wheelMat=new THREE.MeshStandardMaterial({
  color:0x101010,
  roughness:.75
 });

 for(const wx of [-1.5,1.5]){
  for(const wz of [-1.05,1.05]){

   const wheel=new THREE.Mesh(
    new THREE.CylinderGeometry(
     .42,.42,.3,20
    ),
    wheelMat
   );

   wheel.rotation.z=Math.PI/2;

   wheel.position.set(
    wx,
    .48,
    wz
   );

   wheel.castShadow=true;

   car.add(wheel);
  }
 }


 car.position.set(x,.1,z);
 car.rotation.y=angle;

 world.add(car);
 cars.push({
  mesh:car,
  speed:.15+Math.random()*.25
 });

}


/* traffic */

for(let i=0;i<70;i++){

 const vertical=Math.random()<.5;

 if(vertical){

  createCar(
   Math.round(
    (Math.random()*8-4)
   )*100+8,
   (Math.random()-.5)*800,
   Math.PI/2
  );

 }else{

  createCar(
   (Math.random()-.5)*800,
   Math.round(
    (Math.random()*8-4)
   )*100-8,
   0
  );

 }

}


/* =========================================================
   CHECKPOINT
========================================================= */

const checkpoint=new THREE.Group();

const ringMat=new THREE.MeshBasicMaterial({
 color:0x00ffff,
 transparent:true,
 opacity:.8
});

const ring=new THREE.Mesh(
 new THREE.TorusGeometry(5,.25,12,48),
 ringMat
);

ring.rotation.x=Math.PI/2;

checkpoint.add(ring);

const beamMat=new THREE.MeshBasicMaterial({
 color:0x00ffff,
 transparent:true,
 opacity:.12,
 side:THREE.DoubleSide
});

const beam=new THREE.Mesh(
 new THREE.CylinderGeometry(
  4.8,4.8,20,32,1,true
 ),
 beamMat
);

beam.position.y=10;

checkpoint.add(beam);

checkpoint.position.set(
 300,
 0,
 -300
);

world.add(checkpoint);


/* =========================================================
   STREET LIGHTS
========================================================= */

function createStreetLight(x,z){

 const poleMat=new THREE.MeshStandardMaterial({
  color:0x25282b,
  metalness:.7,
  roughness:.35
 });

 const pole=new THREE.Mesh(
  new THREE.CylinderGeometry(
   .12,.16,8,10
  ),
  poleMat
 );

 pole.position.set(x,4,z);
 pole.castShadow=true;

 world.add(pole);


 const bulb=new THREE.PointLight(
  0xffc982,
  0,
  35,
  2
 );

 bulb.position.set(x,7.7,z);

 world.add(bulb);

 return bulb;

}

const streetLights=[];

for(let x=-400;x<=400;x+=100){

 for(let z=-400;z<=400;z+=100){

  streetLights.push(
   createStreetLight(
    x+8,
    z+8
   )
  );

 }

}


/* =========================================================
   WEATHER
========================================================= */

let raining=false;

const rainCount=2500;

const rainGeo=new THREE.BufferGeometry();

const rainPositions=new Float32Array(
 rainCount*3
);

for(let i=0;i<rainCount;i++){

 rainPositions[i*3]=
  (Math.random()-.5)*900;

 rainPositions[i*3+1]=
  Math.random()*200;

 rainPositions[i*3+2]=
  (Math.random()-.5)*900;

}

rainGeo.setAttribute(
 "position",
 new THREE.BufferAttribute(
  rainPositions,
  3
 )
);

const rainMat=new THREE.PointsMaterial({
 color:0xb9dfff,
 size:.16,
 transparent:true,
 opacity:.7
});

const rain=new THREE.Points(
 rainGeo,
 rainMat
);

rain.visible=false;

scene.add(rain);


/* =========================================================
   NIGHT MODE
========================================================= */

let night=false;

function setNight(value){

 night=value;

 if(night){

  scene.background.set(0x071426);
  scene.fog.color.set(0x071426);

  sun.intensity=.35;
  hemi.intensity=.45;
  streetAmbient.intensity=.12;

  for(const light of streetLights)
   light.intensity=5;

 }else{

  scene.background.set(0x8fb7d9);
  scene.fog.color.set(0x9bbbd1);

  sun.intensity=4;
  hemi.intensity=1.6;
  streetAmbient.intensity=.35;

  for(const light of streetLights)
   light.intensity=0;

 }

}


/* =========================================================
   INPUT
========================================================= */

const keys={};

addEventListener(
 "keydown",
 e=>{
  keys[e.key.toLowerCase()]=true;

  if(e.key.toLowerCase()==="n")
   setNight(!night);

  if(e.key.toLowerCase()==="r")
   setRain(!raining);

  if(e.key.toLowerCase()==="e")
   cinematic=!cinematic;
 }
);

addEventListener(
 "keyup",
 e=>{
  keys[e.key.toLowerCase()]=false;
 }
);


/* =========================================================
   RAIN
========================================================= */

function setRain(value){

 raining=value;

 rain.visible=raining;

 if(raining){

  rainGeo.attributes.position.needsUpdate=true;

  for(const road of roads)
   road.material=wetRoadMat;

 }else{

  for(const road of roads)
   road.material=roadMat;

 }

}


/* =========================================================
   PLAYER MOVEMENT
========================================================= */

const clock=new THREE.Clock();

let cinematic=false;

function updatePlayer(dt){

 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;

 const length=Math.hypot(dx,dz);

 if(length>0){

  dx/=length;
  dz/=length;

  const speed=
   keys["shift"]?18:9;

  player.position.x+=
   dx*speed*dt;

  player.position.z+=
   dz*speed*dt;

  player.rotation.y=
   Math.atan2(dx,dz);

 }

 player.position.x=
  THREE.MathUtils.clamp(
   player.position.x,
   -440,
   440
  );

 player.position.z=
  THREE.MathUtils.clamp(
   player.position.z,
   -440,
   440
  );

}


/* =========================================================
   TRAFFIC
========================================================= */

function updateTraffic(dt){

 for(const car of cars){

  const obj=car.mesh;

  if(Math.abs(obj.rotation.y)<.1){

   obj.position.x+=
    car.speed*dt*60;

   if(obj.position.x>460)
    obj.position.x=-460;

  }else{

   obj.position.z+=
    car.speed*dt*60;

   if(obj.position.z>460)
    obj.position.z=-460;

  }

 }

}


/* =========================================================
   RAIN UPDATE
========================================================= */

function updateRain(dt){

 if(!raining)return;

 const p=
  rainGeo.attributes.position.array;

 for(let i=0;i<rainCount;i++){

  p[i*3+1]-=
   55*dt;

  if(p[i*3+1]<0)
   p[i*3+1]=200;

 }

 rainGeo.attributes.position.needsUpdate=true;

}


/* =========================================================
   WATER ANIMATION
========================================================= */

let waterTime=0;

function updateOcean(dt){

 waterTime+=dt;

 ocean.position.y=
  -.1+
  Math.sin(waterTime*.7)*.03;

}


/* =========================================================
   CINEMATIC CAMERA
========================================================= */

function updateCamera(dt){

 if(cinematic){

  const t=clock.elapsedTime;

  camera.position.x=
   player.position.x+
   Math.sin(t*.22)*15;

  camera.position.z=
   player.position.z+
   Math.cos(t*.22)*15;

  camera.position.y=
   8+
   Math.sin(t*.5)*1.5;

 }else{

  const desired=
   new THREE.Vector3(
    player.position.x,
    player.position.y+6,
    player.position.z+12
   );

  camera.position.lerp(
   desired,
   1-Math.pow(.001,dt)
  );

 }

 const target=
  new THREE.Vector3(
   player.position.x,
   player.position.y+1.8,
   player.position.z
  );

 camera.lookAt(target);

}


/* =========================================================
   CHECKPOINT
========================================================= */

function updateCheckpoint(){

 const distance=
  player.position.distanceTo(
   checkpoint.position
  );

 ring.rotation.z=
  clock.elapsedTime;

 beam.rotation.y=
  clock.elapsedTime*.3;

 if(distance<7){

  checkpoint.position.set(
   (Math.random()-.5)*700,
   0,
   (Math.random()-.5)*700
  );

 }

}


/* =========================================================
   HUD
========================================================= */

function updateHUD(){

 const distance=
  player.position.distanceTo(
   checkpoint.position
  );

 document.getElementById("status").textContent=
  `${night?"NIGHT":"DAY"} • ${
   raining?"RAIN":"CLEAR"
  } • CHECKPOINT ${
   Math.round(distance)
  }m`;

}


/* =========================================================
   RESIZE
========================================================= */

addEventListener(
 "resize",
 ()=>{
  camera.aspect=
   innerWidth/innerHeight;

  camera.updateProjectionMatrix();

  renderer.setSize(
   innerWidth,
   innerHeight
  );
 }
);


/* =========================================================
   MAIN LOOP
========================================================= */

setNight(false);

function animate(){

 requestAnimationFrame(animate);

 const dt=Math.min(
  clock.getDelta(),
  .033
 );

 updatePlayer(dt);
 updateTraffic(dt);
 updateRain(dt);
 updateOcean(dt);
 updateCamera(dt);
 updateCheckpoint();
 updateHUD();

 renderer.render(
  scene,
  camera
 );

}

animate();

</script>
</body>
</html>

Game Source: Vice Coast — Realistic Open World

Creator: ChromeBuilder44

Libraries: three

Complexity: complex (1258 lines, 18.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: vice-coast-realistic-open-world-chromebuilder44" to link back to the original. Then publish at arcadelab.ai/publish.