🎮ArcadeLab

HAMILTON CITY: NIGERIA

by StealthRider49
671 lines18.2 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>HAMILTON CITY: NIGERIA</title>

<style>
*{box-sizing:border-box}
body{margin:0;overflow:hidden;background:#080b10;font-family:Arial;color:white}
canvas{display:block}
#logo{
 position:fixed;top:16px;left:18px;z-index:5;
 color:#ffd45a;font-size:25px;font-weight:1000;
 text-shadow:0 0 15px #ff9d00;
 letter-spacing:2px
}
#logo small{display:block;font-size:10px;color:white;letter-spacing:4px}
#hud{
 position:fixed;top:18px;right:18px;z-index:5;
 background:#080b10dd;border:1px solid #bd902f;
 padding:14px 18px;border-radius:12px;min-width:175px
}
#hud strong{color:#ffd45a}
#mission{
 position:fixed;bottom:25px;left:50%;transform:translateX(-50%);
 width:min(440px,90%);padding:15px;text-align:center;
 background:#080b10e8;border:1px solid #bd902f;border-radius:12px
}
#mission h3{margin:0 0 7px;color:#ffd45a}
#message{
 position:fixed;top:27%;left:50%;transform:translateX(-50%);
 background:#080b10dd;padding:12px 20px;border-radius:10px;
 display:none;text-align:center;z-index:10
}
#help{
 position:fixed;bottom:20px;left:18px;color:#ddd;
 background:#080b10bb;padding:12px;border-radius:10px;
 font-size:12px;line-height:1.8
}
#start{
 position:fixed;inset:0;z-index:20;background:#070a10;
 display:flex;align-items:center;justify-content:center;text-align:center
}
#start button{
 padding:16px 35px;border:0;border-radius:8px;
 background:linear-gradient(90deg,#a56c0a,#ffd45a);
 font-size:17px;font-weight:bold;cursor:pointer
}
#start h1{font-size:clamp(30px,6vw,60px);color:#ffd45a}
#start p{color:#ddd;max-width:500px;line-height:1.6;padding:0 15px}
#loading{color:#ffd45a;font-size:13px;margin-top:15px}
</style>
</head>

<body>
<div id="logo">HAMILTON CITY<small>NIGERIA • OPEN WORLD</small></div>

<div id="hud">
 <div>WALLET: <strong id="cash">H$ 2,500</strong></div>
 <div>HEALTH: <strong id="health">100%</strong></div>
 <div>VEHICLE: <strong id="vehicle">ON FOOT</strong></div>
 <div>WANTED: <strong id="wanted">☆☆☆☆☆</strong></div>
</div>

<div id="mission">
 <h3 id="missionTitle">MISSION: THE LAGOS CONNECTION</h3>
 <div id="missionText">Find the gold mission marker.</div>
</div>

<div id="message"></div>

<div id="help">
 <b>CONTROLS</b><br>
 W A S D — Move / Drive<br>
 SHIFT — Run<br>
 SPACE — Jump<br>
 E — Enter / Exit car<br>
 F — Start mission / Interact<br>
 Mouse — Look around<br>
 ESC — Release mouse
</div>

<div id="start">
 <div>
  <h1>HAMILTON CITY</h1>
  <p>Welcome to Nigeria. Explore the city, drive vehicles, complete missions and build your fortune in Hamilton Dollars.</p>
  <button id="play">ENTER THE CITY</button>
  <div id="loading">Loading 3D game...</div>
 </div>
</div>

<script type="importmap">
{
 "imports":{
  "three":"https://cdn.jsdelivr.net/npm/three@0.164.1/build/three.module.js",
  "three/addons/":"https://cdn.jsdelivr.net/npm/three@0.164.1/examples/jsm/"
 }
}
</script>

<script type="module">
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

/* ---------------- BASIC GAME SETUP ---------------- */

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87b8d5);
scene.fog = new THREE.Fog(0x87b8d5, 70, 260);

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

const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(window.innerWidth,window.innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.outputColorSpace=THREE.SRGBColorSpace;
renderer.toneMapping=THREE.ACESFilmicToneMapping;
document.body.appendChild(renderer.domElement);

scene.add(new THREE.HemisphereLight(0xffffff,0x52604b,2));

const sun = new THREE.DirectionalLight(0xfff0d0,3);
sun.position.set(60,100,40);
sun.castShadow=true;
sun.shadow.mapSize.set(2048,2048);
scene.add(sun);

/* ---------------- CITY ENVIRONMENT ---------------- */

const ground = new THREE.Mesh(
 new THREE.PlaneGeometry(500,500),
 new THREE.MeshStandardMaterial({color:0x777c70})
);
ground.rotation.x=-Math.PI/2;
ground.receiveShadow=true;
scene.add(ground);

function box(x,y,z,w,h,d,color){
 const mesh=new THREE.Mesh(
  new THREE.BoxGeometry(w,h,d),
  new THREE.MeshStandardMaterial({color})
 );
 mesh.position.set(x,y,z);
 mesh.castShadow=true;
 mesh.receiveShadow=true;
 scene.add(mesh);
 return mesh;
}

// Roads
const roadMat=new THREE.MeshStandardMaterial({color:0x25282c});
function road(x,z,w,d){
 const r=new THREE.Mesh(new THREE.BoxGeometry(w,0.08,d),roadMat);
 r.position.set(x,0.045,z);
 scene.add(r);
}
for(let i=-200;i<=200;i+=55){
 road(i,0,15,450);
 road(0,i,450,15);
}

// Road lane markings
const lineMat=new THREE.MeshBasicMaterial({color:0xf5d46b});
for(let i=-200;i<=200;i+=12){
 for(let j=-200;j<=200;j+=55){
  const a=new THREE.Mesh(new THREE.BoxGeometry(0.25,0.1,5),lineMat);
  a.position.set(i,0.11,j);
  scene.add(a);
  const b=new THREE.Mesh(new THREE.BoxGeometry(5,0.1,0.25),lineMat);
  b.position.set(j,0.11,i);
  scene.add(b);
 }
}

// City buildings
const buildingColors=[0xd1b18c,0xb7a18d,0x9eaaa9,0xc6c4b6,0xb1a08d];
const buildings=[];
for(let x=-180;x<=180;x+=55){
 for(let z=-180;z<=180;z+=55){
  if(Math.abs(x)<35 || Math.abs(z)<35) continue;
  const w=20+Math.random()*12;
  const d=20+Math.random()*12;
  const h=12+Math.random()*45;
  const b=box(x,h/2,z,w,h,d,
   buildingColors[Math.floor(Math.random()*buildingColors.length)]);
  buildings.push(b);

  // Windows
  for(let y=3;y<h-2;y+=5){
   for(let side=-1;side<=1;side+=2){
    const win=new THREE.Mesh(
     new THREE.BoxGeometry(0.12,1.7,2),
     new THREE.MeshStandardMaterial({
      color:0x253e50,metalness:0.3,roughness:0.3
     })
    );
    win.position.set(x+side*(w/2+0.07),y,z);
    scene.add(win);
   }
  }
 }
}

// Nigerian market stalls
for(let i=0;i<15;i++){
 const x=-28+Math.random()*56;
 const z=42+i*5;
 box(x,1.2,z,5,2.4,4,0x8d4c20);
 box(x,2.6,z,6,0.25,5,0xd3a12f);
}

// Street signs
function sign(x,z,label){
 const pole=box(x,2,z,0.2,4,0.2,0x333333);
 const board=box(x,4,z,6,1.3,0.2,0x087044);
}
sign(18,30,"LAGOS");
sign(-18,-30,"HAMILTON");

/* ---------------- PLAYER AND HUMAN ANIMATION ---------------- */

const player=new THREE.Group();
player.position.set(0,0,0);
scene.add(player);

let character=null;
let mixer=null;
let actions={};
let activeAction=null;
let characterReady=false;

const loader=new GLTFLoader();

document.getElementById("loading").textContent="Loading animated 3D human...";

loader.load(
 "https://threejs.org/examples/models/gltf/Soldier.glb",
 gltf=>{
  character=gltf.scene;
  character.scale.setScalar(1);
  character.traverse(o=>{
   if(o.isMesh){
    o.castShadow=true;
    o.receiveShadow=true;
   }
  });
  player.add(character);

  mixer=new THREE.AnimationMixer(character);
  gltf.animations.forEach(clip=>{
   actions[clip.name]=mixer.clipAction(clip);
  });

  characterReady=true;
  playAnimation("Idle");
  document.getElementById("loading").textContent="Character ready!";
 },
 undefined,
 error=>{
  document.getElementById("loading").textContent=
   "Character model failed to load. Check your internet connection.";
  console.error(error);
 }
);

function playAnimation(name){
 if(!mixer || !actions[name]) return;
 const next=actions[name];
 if(activeAction===next) return;
 next.reset().fadeIn(0.2).play();
 if(activeAction) activeAction.fadeOut(0.2);
 activeAction=next;
}

/* ---------------- VEHICLES ---------------- */

const cars=[];
let currentCar=null;

function createCar(x,z,color,name){
 const car=new THREE.Group();

 const body=new THREE.Mesh(
  new THREE.BoxGeometry(2.3,0.8,4.5),
  new THREE.MeshStandardMaterial({color,metalness:0.45,roughness:0.35})
 );
 body.position.y=0.85;
 body.castShadow=true;
 car.add(body);

 const roof=new THREE.Mesh(
  new THREE.BoxGeometry(1.8,0.8,2.2),
  new THREE.MeshStandardMaterial({color:0x303840,metalness:0.3})
 );
 roof.position.set(0,1.55,-0.15);
 roof.castShadow=true;
 car.add(roof);

 for(const sx of [-1,1]){
  for(const sz of [-1.45,1.45]){
   const wheel=new THREE.Mesh(
    new THREE.CylinderGeometry(0.42,0.42,0.25,16),
    new THREE.MeshStandardMaterial({color:0x101010})
   );
   wheel.rotation.z=Math.PI/2;
   wheel.position.set(sx*1.17,0.48,sz);
   car.add(wheel);
  }
 }

 car.position.set(x,0,z);
 scene.add(car);
 const data={group:car,name,speed:0,angle:0};
 cars.push(data);
 return data;
}

createCar(8,4,0xd9aa26,"Lagos Danfo");
createCar(-10,5,0x151b27,"Black SUV");
createCar(12,-12,0xc8c8c8,"Toyota Taxi");
createCar(-15,-10,0x9d1717,"Mercedes-Benz");

/* ---------------- NPC PEDESTRIANS ---------------- */

const pedestrians=[];
for(let i=0;i<12;i++){
 const npc=new THREE.Group();
 const skin=new THREE.MeshStandardMaterial({color:0x75452d});
 const shirt=new THREE.MeshStandardMaterial({
  color:[0x15804b,0xc72b2b,0x244f9c,0xd2a52d][i%4]
 });
 const body=new THREE.Mesh(new THREE.CapsuleGeometry(0.35,0.7,4,8),shirt);
 body.position.y=1.1;
 npc.add(body);
 const head=new THREE.Mesh(new THREE.SphereGeometry(0.28,12,12),skin);
 head.position.y=1.85;
 npc.add(head);
 npc.position.set(-25+Math.random()*50,0,40+Math.random()*30);
 scene.add(npc);
 pedestrians.push(npc);
}

/* ---------------- MISSIONS ---------------- */

let cash=2500;
let health=100;
let wanted=0;
let missionIndex=0;

const missions=[
 {title:"THE LAGOS CONNECTION",text:"Go to the market and meet your contact.",x:20,z:45,reward:1500},
 {title:"DANFO DELIVERY",text:"Drive to the yellow delivery checkpoint.",x:-70,z:0,reward:2500},
 {title:"THE GETAWAY",text:"Reach the safe house before the police catch you.",x:100,z:-100,reward:5000}
];

let marker;
function createMarker(){
 if(marker) scene.remove(marker);
 const m=missions[missionIndex];
 if(!m) return;
 marker=new THREE.Group();
 const ring=new THREE.Mesh(
  new THREE.TorusGeometry(3,0.3,12,48),
  new THREE.MeshBasicMaterial({color:0xffc42b})
 );
 ring.rotation.x=Math.PI/2;
 marker.add(ring);
 const beam=new THREE.Mesh(
  new THREE.CylinderGeometry(0.3,0.8,8,16,1,true),
  new THREE.MeshBasicMaterial({
   color:0xffc42b,transparent:true,opacity:0.25
  })
 );
 beam.position.y=4;
 marker.add(beam);
 marker.position.set(m.x,0.2,m.z);
 scene.add(marker);
}

createMarker();

function completeMission(){
 const m=missions[missionIndex];
 if(!m) return;
 cash+=m.reward;
 showMessage("MISSION COMPLETE! You earned H$"+m.reward);
 missionIndex++;
 if(missionIndex<missions.length){
  updateMission();
  createMarker();
 }else{
  document.getElementById("missionTitle").textContent="STORY CHAPTER COMPLETE";
  document.getElementById("missionText").textContent=
   "More Hamilton City missions coming soon.";
  scene.remove(marker);
 }
}

function updateMission(){
 const m=missions[missionIndex];
 if(!m) return;
 document.getElementById("missionTitle").textContent="MISSION: "+m.title;
 document.getElementById("missionText").textContent=m.text;
}

function showMessage(text){
 const el=document.getElementById("message");
 el.textContent=text;
 el.style.display="block";
 setTimeout(()=>el.style.display="none",2600);
}

/* ---------------- INPUT ---------------- */

const keys={};
let mouseLocked=false;
let cameraYaw=0;
let cameraPitch=0.22;

window.addEventListener("keydown",e=>{
 keys[e.code]=true;
 if(e.code==="Space") e.preventDefault();
 if(e.code==="KeyE") enterExitCar();
 if(e.code==="KeyF") interact();
});
window.addEventListener("keyup",e=>keys[e.code]=false);

renderer.domElement.addEventListener("click",()=>{
 if(document.getElementById("start").style.display==="none"){
  renderer.domElement.requestPointerLock?.();
 }
});

document.addEventListener("pointerlockchange",()=>{
 mouseLocked=document.pointerLockElement===renderer.domElement;
});

document.addEventListener("mousemove",e=>{
 if(!mouseLocked) return;
 cameraYaw-=e.movementX*0.0025;
 cameraPitch=Math.max(-0.1,Math.min(0.65,cameraPitch+e.movementY*0.0015));
});

/* ---------------- VEHICLE INTERACTION ---------------- */

function enterExitCar(){
 if(currentCar){
  player.position.copy(currentCar.group.position);
  player.position.x+=3;
  currentCar.speed=0;
  currentCar=null;
  showMessage("You left the vehicle.");
  return;
 }

 let closest=null;
 let distance=5;
 for(const car of cars){
  const d=player.position.distanceTo(car.group.position);
  if(d<distance){distance=d;closest=car;}
 }
 if(closest){
  currentCar=closest;
  showMessage("Entered "+closest.name);
 }else{
  showMessage("No vehicle nearby.");
 }
}

function interact(){
 if(currentCar){
  showMessage("You are driving. Reach the mission marker.");
  return;
 }
 const m=missions[missionIndex];
 if(m && player.position.distanceTo(new THREE.Vector3(m.x,0,m.z))<9){
  if(missionIndex===0) completeMission();
  else if(missionIndex===1 && currentCar===null){
   showMessage("Get into a vehicle to complete this delivery.");
  }else if(missionIndex===2){
   completeMission();
  }
 }else{
  showMessage("Explore the city and find your mission marker.");
 }
}

/* ---------------- POLICE SYSTEM ---------------- */

const policeCars=[];
for(let i=0;i<2;i++){
 const p=createCar(35+i*8,35,0x152c79,"Police");
 policeCars.push(p);
}

function addWanted(){
 wanted=Math.min(5,wanted+1);
}

function updatePolice(dt){
 if(wanted<=0) return;
 for(const police of policeCars){
  const target=currentCar?currentCar.group:player;
  const dx=target.position.x-police.group.position.x;
  const dz=target.position.z-police.group.position.z;
  const distance=Math.hypot(dx,dz);
  if(distance>2){
   police.angle=Math.atan2(dx,dz);
   police.group.rotation.y=police.angle;
   police.group.position.x+=Math.sin(police.angle)*dt*5;
   police.group.position.z+=Math.cos(police.angle)*dt*5;
  }
  if(distance<4){
   health=Math.max(0,health-dt*3);
   if(health===0){
    health=100;
    cash=Math.max(0,cash-500);
    wanted=0;
    if(currentCar) currentCar=null;
    player.position.set(0,0,0);
    showMessage("BUSTED! You lost H$500.");
   }
  }
 }
}

/* ---------------- GAME MOVEMENT ---------------- */

const clock=new THREE.Clock();
const velocity=new THREE.Vector3();
let jumpVelocity=0;
let grounded=true;
let gameStarted=false;

function updatePlayer(dt){
 if(currentCar){
  const car=currentCar;
  const forward=(keys.KeyW?1:0)-(keys.KeyS?1:0);
  const steering=(keys.KeyA?1:0)-(keys.KeyD?1:0);
  car.speed+=forward*dt*12;
  car.speed*=Math.pow(0.97,dt*60);
  car.speed=THREE.MathUtils.clamp(car.speed,-8,25);
  car.angle+=steering*dt*1.8*(car.speed>=0?1:-1);
  car.group.rotation.y=car.angle;
  car.group.position.x+=Math.sin(car.angle)*car.speed*dt;
  car.group.position.z+=Math.cos(car.angle)*car.speed*dt;
  player.position.copy(car.group.position);
  player.rotation.y=car.angle;
  return;
 }

 const forward=(keys.KeyW?1:0)-(keys.KeyS?1:0);
 const sideways=(keys.KeyD?1:0)-(keys.KeyA?1:0);
 const running=keys.ShiftLeft||keys.ShiftRight;
 const speed=running?10:5;

 const direction=new THREE.Vector3(
  sideways,0,forward
 );
 if(direction.lengthSq()>0){
  direction.normalize();
  direction.applyAxisAngle(new THREE.Vector3(0,1,0),cameraYaw);
  player.position.addScaledVector(direction,speed*dt);
  player.rotation.y=Math.atan2(direction.x,direction.z);
  playAnimation(running?"Run":"Walk");
 }else{
  playAnimation("Idle");
 }

 if(keys.Space && grounded){
  jumpVelocity=7;
  grounded=false;
 }
 player.position.y+=jumpVelocity*dt;
 jumpVelocity-=20*dt;
 if(player.position.y<0){
  player.position.y=0;
  jumpVelocity=0;
  grounded=true;
 }

 // Keep the player inside the playable city.
 player.position.x=THREE.MathUtils.clamp(player.position.x,-220,220);
 player.position.z=THREE.MathUtils.clamp(player.position.z,-220,220);
}

function updateCamera(){
 const target=currentCar?currentCar.group:player;
 const distance=currentCar?11:8;
 const height=currentCar?5:4;
 const offset=new THREE.Vector3(
  Math.sin(cameraYaw)*distance,
  height+cameraPitch*5,
  -Math.cos(cameraYaw)*distance
 );
 camera.position.copy(target.position).add(offset);
 camera.lookAt(target.position.x,target.position.y+1.5,target.position.z);
}

/* ---------------- GAME LOOP ---------------- */

function updateHUD(){
 document.getElementById("cash").textContent="H$ "+cash.toLocaleString();
 document.getElementById("health").textContent=Math.round(health)+"%";
 document.getElementById("vehicle").textContent=currentCar?currentCar.name:"ON FOOT";
 document.getElementById("wanted").textContent=
  "★".repeat(wanted)+"☆".repeat(5-wanted);
}

function animate(){
 requestAnimationFrame(animate);
 const dt=Math.min(clock.getDelta(),0.04);

 if(gameStarted){
  if(mixer) mixer.update(dt);
  updatePlayer(dt);
  updatePolice(dt);
  updateCamera();

  if(marker){
   marker.rotation.y+=dt*0.8;
   marker.children[1].scale.y=1+Math.sin(clock.elapsedTime*3)*0.1;
  }

  pedestrians.forEach((npc,i)=>{
   npc.position.x+=Math.sin(clock.elapsedTime+i)*dt*0.5;
  });

  const m=missions[missionIndex];
  if(m){
   const target=currentCar?currentCar.group.position:player.position;
   if(target.distanceTo(new THREE.Vector3(m.x,0,m.z))<7){
    if(missionIndex===1 && !currentCar){
     showMessage("Get into a car to complete this mission.");
    }else{
     completeMission();
    }
   }
  }
  updateHUD();
 }
 renderer.render(scene,camera);
}
animate();

/* ---------------- START GAME ---------------- */

document.getElementById("play").addEventListener("click",()=>{
 gameStarted=true;
 document.getElementById("start").style.display="none";
 showMessage("Welcome to Hamilton City, Nigeria!");
 updateCamera();
 renderer.domElement.requestPointerLock?.();
});

window.addEventListener("resize",()=>{
 camera.aspect=window.innerWidth/window.innerHeight;
 camera.updateProjectionMatrix();
 renderer.setSize(window.innerWidth,window.innerHeight);
});

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

Game Source: HAMILTON CITY: NIGERIA

Creator: StealthRider49

Libraries: three

Complexity: complex (671 lines, 18.2 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: hamilton-city-nigeria-stealthrider49" to link back to the original. Then publish at arcadelab.ai/publish.