HAMILTON CITY: NIGERIA
by StealthRider49671 lines18.2 KB🛠️ Three.js (3D graphics)
<!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.