🎮ArcadeLab

Liberty City 3D

by ApexPanther55
538 lines17.5 KB🛠️ Three.js (3D graphics)
▶ Play


<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Liberty City 3D</title>
<style>
*{box-sizing:border-box}
body{margin:0;overflow:hidden;background:#111;font-family:Arial}
canvas{display:block}
#hud{position:fixed;top:12px;right:15px;color:white;font-size:17px;line-height:1.8;text-shadow:0 2px 5px black;pointer-events:none}
#mission{position:fixed;top:12px;left:15px;background:#07151ccc;color:white;padding:10px;border-radius:10px}
#hint{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);color:white;background:#111c;padding:10px 18px;border-radius:20px;white-space:nowrap}
#map{position:fixed;bottom:15px;right:15px;border:2px solid white;border-radius:50%;background:#222;width:120px;height:120px}
#menu{position:fixed;inset:0;background:#07121deF;display:flex;align-items:center;justify-content:center;z-index:10;color:white}
.panel{background:#152b3a;padding:25px;border-radius:20px;width:min(90%,450px);text-align:center;max-height:90vh;overflow:auto}
button{border:0;border-radius:12px;padding:12px 18px;margin:5px;background:#16a085;color:white;font-weight:bold;font-size:15px}
input,select{accent-color:#16a085}
#mobile{display:none}
.pad{position:fixed;bottom:20px;display:grid;gap:7px;z-index:4}
#move{left:15px;grid-template-columns:48px 48px 48px}
#acts{right:15px;grid-template-columns:60px 60px}
.pad button{width:48px;height:48px;padding:0;background:#152d3bcc;border:1px solid #fff8}
#acts button{width:60px;height:52px;font-size:12px}
#rotate{display:none;position:fixed;inset:0;background:#07121d;color:white;z-index:20;align-items:center;justify-content:center;text-align:center;font-size:24px}
@media(orientation:portrait) and (max-width:800px){
 #rotate{display:flex}
 #map{width:85px;height:85px}
}
</style>
<script type="importmap">
{"imports":{"three":"https://unpkg.com/three@0.160.0/build/three.module.js"}}
</script>
</head>
<body>
<div id="rotate">↻<br>أدر هاتفك إلى الوضع الأفقي لتلعب</div>
<div id="hud">💰 $0<br>❤️ 100<br>⭐ 0</div>
<div id="mission">المهمة: استكشف المدينة</div>
<div id="hint">WASD للحركة · F للمركبة · Shift للجري</div>
<canvas id="map" width="160" height="160"></canvas>

<div id="mobile">
 <div class="pad" id="move">
  <button data-key="KeyW" style="grid-column:2">▲</button>
  <button data-key="KeyA">◀</button>
  <button data-key="KeyS">▼</button>
  <button data-key="KeyD">▶</button>
 </div>
 <div class="pad" id="acts">
  <button data-key="ShiftLeft">جري</button>
  <button id="enter">ركوب</button>
  <button data-key="Space">قفز</button>
  <button id="horn">بوق</button>
 </div>
</div>

<div id="menu">
 <div class="panel">
  <h1 id="title">LIBERTY CITY</h1>
  <p>عالم مفتوح ثلاثي الأبعاد. استكشف المدينة وقُد المركبات واكتشف المهام.</p>
  <div id="settings" style="display:none">
   <p>جودة الرسومات</p>
   <select id="quality">
    <option value="1">عالية</option>
    <option value=".7">متوسطة</option>
    <option value=".45">منخفضة</option>
   </select>
   <p>حساسية الكاميرا</p>
   <input id="sensitivity" type="range" min=".5" max="2" step=".1" value="1">
   <p>مستوى الصوت</p>
   <input id="volume" type="range" min="0" max="1" step=".1" value=".5">
  </div>
  <button id="start">ابدأ اللعب</button>
  <button id="settingsBtn">الإعدادات</button>
  <button id="resume" style="display:none">متابعة</button>
 </div>
</div>

<script type="module">
import * as THREE from 'three';

// إعداد العالم
const scene=new THREE.Scene();
scene.background=new THREE.Color(0x86b9df);
scene.fog=new THREE.Fog(0x86b9df,100,500);

const camera=new THREE.PerspectiveCamera(70,innerWidth/innerHeight,.1,1000);
const renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

scene.add(new THREE.HemisphereLight(0xffffff,0x52604a,1.6));
const sun=new THREE.DirectionalLight(0xffe4bd,2);
sun.position.set(100,150,50);
sun.castShadow=true;
sun.shadow.mapSize.set(1024,1024);
scene.add(sun);

const N=8,B=70,W=N*B;
const mat=c=>new THREE.MeshStandardMaterial({color:c,roughness:.8});
const box=(w,h,d,c,x,y,z)=>{
 const m=new THREE.Mesh(new THREE.BoxGeometry(w,h,d),mat(c));
 m.position.set(x,y,z);m.castShadow=true;m.receiveShadow=true;
 return m;
};

// الأرض والطرق
const ground=new THREE.Mesh(
 new THREE.PlaneGeometry(W+200,W+200),mat(0x497b43)
);
ground.rotation.x=-Math.PI/2;
ground.position.set(W/2,-.1,W/2);
scene.add(ground);

for(let i=0;i<=N;i++){
 const r1=box(23,.12,W+23,0x303438,i*B,0,W/2);
 const r2=box(W+23,.12,23,0x303438,W/2,0,i*B);
 for(let j=0;j<N;j++){
  box(.2,.03,7,0xe8d27d,i*B,.1,j*B+35);
  box(7,.03,.2,0xe8d27d,j*B+35,.1,i*B);
 }
}

// المباني
const buildings=[];
const colors=[0x9ba8ac,0xb8a58d,0x788e99,0xb2b5b0,0x8d8078];
for(let i=0;i<N;i++)for(let j=0;j<N;j++){
 for(let sx of [-1,1])for(let sz of [-1,1]){
  if(Math.random()<.12)continue;
  let w=14+Math.random()*5;
  let d=14+Math.random()*5;
  let h=12+Math.random()*45;
  let x=(i+.5)*B+sx*12;
  let z=(j+.5)*B+sz*12;
  let b=box(w,h,d,colors[Math.floor(Math.random()*colors.length)],x,h/2,z);
  buildings.push({x,z,w,d});
  scene.add(b);
  // نوافذ المباني
  for(let y=3;y<h-2;y+=4){
   let win=box(w*.65,.8,.12,0x5794ae,x,y,z+d/2+.08);
   scene.add(win);
  }
 }
}

// الأشجار
for(let i=0;i<100;i++){
 let x=Math.random()*W,z=Math.random()*W;
 let trunk=new THREE.Mesh(new THREE.CylinderGeometry(.2,.3,2,8),mat(0x755033));
 trunk.position.set(x,1,z);scene.add(trunk);
 let leaves=new THREE.Mesh(new THREE.SphereGeometry(1.7,8,8),mat(0x28703d));
 leaves.position.set(x,3,z);leaves.castShadow=true;scene.add(leaves);
}

// الشخصيات المتحركة
function makePerson(color=0x2255aa){
 const g=new THREE.Group();
 const torso=box(.65,1,.35,color,0,1.25,0);
 const head=new THREE.Mesh(new THREE.SphereGeometry(.24,12,12),mat(0xd8a27e));
 head.position.y=2;g.add(torso,head);
 const limbs=[];
 for(let side of [-1,1]){
  let arm=new THREE.Group();
  arm.position.set(side*.43,1.65,0);
  arm.add(box(.19,.65,.2,0xb78c70,0,-.3,0));
  g.add(arm);limbs.push(arm);
  let leg=new THREE.Group();
  leg.position.set(side*.18,.85,0);
  leg.add(box(.23,.8,.25,0x202a36,0,-.35,0));
  g.add(leg);limbs.push(leg);
 }
 return {mesh:g,limbs,phase:Math.random()*6};
}

const people=[];
for(let i=0;i<32;i++){
 const person=makePerson([0x2255aa,0x993333,0x228855,0xaaaa44,0xeeeeee][i%5]);
 person.mesh.position.set(Math.random()*W,0,Math.random()*W);
 scene.add(person.mesh);
 people.push({
  ...person,
  direction:Math.random()*Math.PI*2,
  speed:1+Math.random()*1.2,
  change:Math.random()*5
 });
}

// المركبات
function makeVehicle(type,color){
 const g=new THREE.Group();
 if(type==="car"){
  g.add(box(2,.8,4,color,0,.8,0));
  g.add(box(1.65,.7,2,0x85b7d1,0,1.5,-.2));
  for(let x of [-1,1])for(let z of [-1.3,1.3]){
   let w=new THREE.Mesh(new THREE.CylinderGeometry(.4,.4,.25,16),mat(0x111111));
   w.rotation.z=Math.PI/2;w.position.set(x,.4,z);g.add(w);
  }
 }else if(type==="bike"){
  g.add(box(.35,.55,1.4,color,0,.8,0));
  for(let z of [-.7,.7]){
   let w=new THREE.Mesh(new THREE.TorusGeometry(.36,.08,8,16),mat(0x111111));
   w.position.set(0,.4,z);g.add(w);
  }
 }else{
  g.add(box(.08,.08,1.4,color,0,.7,0));
  for(let z of [-.65,.65]){
   let w=new THREE.Mesh(new THREE.TorusGeometry(.34,.045,8,16),mat(0x111111));
   w.position.set(0,.4,z);g.add(w);
  }
  g.add(box(.5,.08,.08,0x222222,0,1.1,.5));
 }
 return g;
}

const vehicles=[];
function spawnVehicle(type,x,z){
 const mesh=makeVehicle(type,[0xcc2222,0x2255cc,0xeeeeee,0x22aa55,0xe6bd35][Math.floor(Math.random()*5)]);
 mesh.position.set(x,0,z);scene.add(mesh);
 vehicles.push({mesh,type,speed:0,heading:Math.random()*6.28});
}
spawnVehicle("car",B+5,B);
spawnVehicle("car",B*2+8,B*2);
spawnVehicle("bike",B+10,B+12);
spawnVehicle("bicycle",B-8,B+15);
for(let i=0;i<18;i++){
 spawnVehicle(Math.random()<.7?"car":Math.random()<.5?"bike":"bicycle",
 Math.random()*W,Math.random()*W);
}

// هليكوبتر متحركة
const heli=new THREE.Group();
heli.add(box(2,.7,4,0x334b5d,0,0,0));
heli.add(box(1.6,.6,1.5,0x78aaca,0,.3,-.2));
heli.add(box(.12,.12,5,0x222222,0,.6,0));
const rotor=new THREE.Group();
rotor.add(box(7,.07,.16,0x222222,0,0,0));
rotor.add(box(.16,.07,7,0x222222,0,0,0));
rotor.position.y=.8;heli.add(rotor);
heli.position.set(200,55,200);scene.add(heli);

// اللاعب
const player=makePerson(0x2255aa);
scene.add(player.mesh);
player.mesh.position.set(B,0,B);
let activeVehicle=null;
let money=0,missionCount=0;
let yaw=0,pitch=.3,paused=true;
let time=0,velocityY=0;
const keys={};

addEventListener('keydown',e=>{
 keys[e.code]=true;
 if(e.code==="KeyF")toggleVehicle();
 if(e.code==="Escape")openMenu();
});
addEventListener('keyup',e=>keys[e.code]=false);

renderer.domElement.addEventListener('click',()=>{
 if(!paused)renderer.domElement.requestPointerLock?.();
});
addEventListener('mousemove',e=>{
 if(document.pointerLockElement&&!paused){
  let s=+$("sensitivity").value;
  yaw-=e.movementX*.002*s;
  pitch=Math.max(.05,Math.min(1.1,pitch+e.movementY*.001*s));
 }
});

function $(id){return document.getElementById(id)}
function nearestVehicle(){
 let best=null,distance=7;
 for(let v of vehicles){
  if(v===activeVehicle)continue;
  let d=v.mesh.position.distanceTo(player.mesh.position);
  if(d<distance){distance=d;best=v}
 }
 return best;
}
function toggleVehicle(){
 if(activeVehicle){
  player.mesh.position.copy(activeVehicle.mesh.position);
  player.mesh.position.x+=3;
  player.mesh.visible=true;
  activeVehicle.speed=0;
  activeVehicle=null;
 }else{
  let v=nearestVehicle();
  if(v){activeVehicle=v;player.mesh.visible=false;yaw=v.heading}
 }
}

function animatePerson(p,dt,moving){
 p.phase+=dt*(moving?9:2);
 let a=Math.sin(p.phase)*.55*(moving?1:.08);
 p.limbs[0].rotation.x=a;
 p.limbs[2].rotation.x=-a;
 p.limbs[1].rotation.x=-a;
 p.limbs[3].rotation.x=a;
 p.mesh.position.y=moving?Math.abs(Math.sin(p.phase*2))*.06:0;
}

function walk(dt){
 let f=(keys.KeyW?1:0)-(keys.KeyS?1:0);
 let s=(keys.KeyD?1:0)-(keys.KeyA?1:0);
 let x=Math.sin(yaw)*f-Math.cos(yaw)*s;
 let z=Math.cos(yaw)*f+Math.sin(yaw)*s;
 let len=Math.hypot(x,z);
 if(len){
  x/=len;z/=len;
  let speed=keys.ShiftLeft?9:5;
  player.mesh.position.x+=x*speed*dt;
  player.mesh.position.z+=z*speed*dt;
  player.mesh.rotation.y=Math.atan2(x,z);
 }
 if(keys.Space&&player.mesh.position.y<=.01)velocityY=7;
 velocityY-=18*dt;
 player.mesh.position.y+=velocityY*dt;
 if(player.mesh.position.y<0){player.mesh.position.y=0;velocityY=0}
 animatePerson(player,dt,!!len);
}

function drive(v,dt){
 let acceleration=(keys.KeyW?1:0)-(keys.KeyS?1:0);
 let steering=(keys.KeyA?1:0)-(keys.KeyD?1:0);
 let max=v.type==="car"?35:v.type==="bike"?48:12;
 v.speed+=acceleration*(v.speed<0?25:17)*dt;
 if(!acceleration)v.speed*=Math.max(0,1-1.7*dt);
 if(keys.Space)v.speed*=Math.max(0,1-5*dt);
 v.speed=Math.max(-10,Math.min(max,v.speed));
 v.heading+=steering*2*dt*Math.sign(v.speed);
 v.mesh.position.x+=Math.sin(v.heading)*v.speed*dt;
 v.mesh.position.z+=Math.cos(v.heading)*v.speed*dt;
 v.mesh.rotation.y=v.heading;
}

// مؤثرات الجسيمات
const particles=[];
function burst(position,color=0xffcc66,count=10){
 for(let i=0;i<count;i++){
  let m=new THREE.Mesh(new THREE.SphereGeometry(.08,5,5),new THREE.MeshBasicMaterial({color,transparent:true}));
  m.position.copy(position);
  scene.add(m);
  particles.push({mesh:m,life:1,
   velocity:new THREE.Vector3((Math.random()-.5)*4,Math.random()*4,(Math.random()-.5)*4)});
 }
}
function updateParticles(dt){
 for(let i=particles.length-1;i>=0;i--){
  let p=particles[i];
  p.life-=dt;
  p.mesh.position.addScaledVector(p.velocity,dt);
  p.velocity.y-=5*dt;
  p.mesh.material.opacity=Math.max(0,p.life);
  p.mesh.scale.setScalar(Math.max(.01,p.life));
  if(p.life<=0){scene.remove(p.mesh);particles.splice(i,1)}
 }
}

// أصوات مولدة دون ملفات خارجية
let audio=null,engineOsc=null,engineGain=null;
function startAudio(){
 if(audio)return;
 audio=new (window.AudioContext||window.webkitAudioContext)();
 engineOsc=audio.createOscillator();
 engineGain=audio.createGain();
 engineOsc.type="sawtooth";
 engineOsc.frequency.value=55;
 engineGain.gain.value=0;
 engineOsc.connect(engineGain);
 engineGain.connect(audio.destination);
 engineOsc.start();
}
function sound(freq=440,duration=.12){
 if(!audio)return;
 let o=audio.createOscillator(),g=audio.createGain();
 o.frequency.value=freq;
 g.gain.setValueAtTime(+$("volume").value*.12,audio.currentTime);
 g.gain.exponentialRampToValueAtTime(.001,audio.currentTime+duration);
 o.connect(g);g.connect(audio.destination);
 o.start();o.stop(audio.currentTime+duration);
}

// عناصر قابلة للجمع
const coins=[];
for(let i=0;i<35;i++){
 let c=new THREE.Mesh(new THREE.TorusGeometry(.65,.16,8,18),new THREE.MeshStandardMaterial({color:0xffd32a,metalness:.7,emissive:0x553300}));
 c.position.set(Math.random()*W,1.5,Math.random()*W);
 scene.add(c);coins.push(c);
}

const map=$("map").getContext("2d");
function update(dt){
 time+=dt;
 if(paused)return;

 if(activeVehicle){
  drive(activeVehicle,dt);
  player.mesh.position.copy(activeVehicle.mesh.position);
  yaw+=(activeVehicle.heading-yaw)*Math.min(1,dt*2);
  if(engineGain){
   engineGain.gain.setTargetAtTime(Math.abs(activeVehicle.speed)*.0007, audio.currentTime,.1);
   engineOsc.frequency.setTargetAtTime(45+Math.abs(activeVehicle.speed)*3,audio.currentTime,.1);
  }
 }else{
  walk(dt);
  if(engineGain)engineGain.gain.setTargetAtTime(0,audio.currentTime,.1);
 }

 // الشخصيات تتجول وتتفاعل مع محيطها
 for(let p of people){
  p.change-=dt;
  if(p.change<=0){p.direction+=Math.random()*2-1;p.change=2+Math.random()*5}
  let dx=Math.sin(p.direction),dz=Math.cos(p.direction);
  p.mesh.position.x+=dx*p.speed*dt;
  p.mesh.position.z+=dz*p.speed*dt;
  p.mesh.rotation.y=Math.atan2(dx,dz);
  if(p.mesh.position.x<2||p.mesh.position.x>W-2)p.direction=Math.PI-p.direction;
  if(p.mesh.position.z<2||p.mesh.position.z>W-2)p.direction=-p.direction;
  animatePerson(p,dt,true);
  // تغيير اتجاه الشخصيات عند الاقتراب من اللاعب
  let distance=p.mesh.position.distanceTo(player.mesh.position);
  if(distance<3)p.direction=Math.atan2(p.mesh.position.x-player.mesh.position.x,p.mesh.position.z-player.mesh.position.z);
 }

 // الهليكوبتر والمروحة
 heli.position.x=200+Math.sin(time*.12)*110;
 heli.position.z=200+Math.cos(time*.12)*110;
 heli.position.y=55+Math.sin(time*.8)*2;
 heli.rotation.y=time*.15;
 rotor.rotation.y+=dt*18;

 // العملات
 for(let c of coins){
  c.rotation.y+=dt*2;
  c.position.y=1.5+Math.sin(time*3+c.position.x)*.25;
  if(c.visible&&c.position.distanceTo(player.mesh.position)<2.5){
   c.visible=false;money+=100;missionCount++;
   burst(c.position,0xffd32a,16);sound(850,.2);
   $("mission").textContent="الرموز المجمعة: "+missionCount;
  }
 }

 // الجسيمات
 updateParticles(dt);

 // الكاميرا
 let target=player.mesh.position.clone();
 target.y+=1.5;
 let dist=activeVehicle?10:6;
 camera.position.set(
  target.x-Math.sin(yaw)*Math.cos(pitch)*dist,
  target.y+Math.sin(pitch)*dist+1,
  target.z-Math.cos(yaw)*Math.cos(pitch)*dist
 );
 camera.lookAt(target);

 // واجهة اللعبة
 $("hud").innerHTML=`💰 $${money}<br>❤️ 100<br>⭐ 0`;
 $("hint").textContent=activeVehicle?"F للنزول · WASD للقيادة":
 nearestVehicle()?"F لركوب المركبة القريبة":"WASD للحركة · Shift للجري · Space للقفز";

 // الخريطة
 map.fillStyle="#263b37";map.fillRect(0,0,160,160);
 map.fillStyle="#65716d";
 for(let i=0;i<=N;i++){
  map.fillRect(i*160/N-2,0,4,160);
  map.fillRect(0,i*160/N-2,160,4);
 }
 for(let v of vehicles){
  map.fillStyle="#48b9ed";
  map.fillRect(v.mesh.position.x/W*160-2,v.mesh.position.z/W*160-2,4,4);
 }
 map.fillStyle="white";
 map.beginPath();
 map.arc(player.mesh.position.x/W*160,player.mesh.position.z/W*160,4,0,Math.PI*2);
 map.fill();
}

let last=performance.now();
function loop(now){
 requestAnimationFrame(loop);
 let dt=Math.min((now-last)/1000,.04);
 last=now;
 update(dt);
 renderer.render(scene,camera);
}
requestAnimationFrame(loop);

// القائمة والإعدادات
function openMenu(){
 paused=true;
 document.exitPointerLock?.();
 $("menu").style.display="flex";
 $("start").style.display="none";
 $("resume").style.display="inline-block";
}
$("start").onclick=()=>{
 startAudio();
 paused=false;
 $("menu").style.display="none";
};
$("resume").onclick=()=>{
 paused=false;
 $("menu").style.display="none";
};
$("settingsBtn").onclick=()=>{
 $("settings").style.display=$("settings").style.display==="none"?"block":"none";
};
$("quality").onchange=()=>{
 renderer.setPixelRatio(devicePixelRatio*+$("quality").value);
 renderer.setSize(innerWidth,innerHeight);
};
$("volume").oninput=()=>{};
$("enter").onclick=toggleVehicle;
$("horn").onclick=()=>sound(180,.4);

document.querySelectorAll("[data-key]").forEach(b=>{
 const k=b.dataset.key;
 b.addEventListener("pointerdown",e=>{e.preventDefault();keys[k]=true});
 ["pointerup","pointerleave","pointercancel"].forEach(ev=>
  b.addEventListener(ev,()=>keys[k]=false));
});

if("ontouchstart" in window)$("mobile").style.display="block";
addEventListener("resize",()=>{
 camera.aspect=innerWidth/innerHeight;
 camera.updateProjectionMatrix();
 renderer.setSize(innerWidth,innerHeight);
});
openMenu();
</script>
</body>
</html>

Game Source: Liberty City 3D

Creator: ApexPanther55

Libraries: three

Complexity: complex (538 lines, 17.5 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: liberty-city-3d-apexpanther55" to link back to the original. Then publish at arcadelab.ai/publish.