Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created September 1, 2026 17:51
Show Gist options
  • Select an option

  • Save EncodeTheCode/0de7f35d0397e92fbdf02bd918bd278c to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/0de7f35d0397e92fbdf02bd918bd278c to your computer and use it in GitHub Desktop.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>GTA 2 Wanted System - HTML5 Prototype</title>
<style>
html,body{margin:0;background:#111;color:#fff;font:14px Arial;overflow:hidden}
canvas{display:block;margin:auto;background:#20242a;cursor:crosshair}
#ui{position:fixed;inset:0;pointer-events:none}
#reload{
position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
width:260px;padding:10px;background:#111c;border:2px solid #fff;
text-align:center;visibility:hidden
}
#rb{height:12px;margin-top:8px;background:#333}
#rf{height:100%;width:0;background:#fff}
#info{position:absolute;left:12px;top:10px;text-shadow:1px 1px #000}
</style>
</head>
<body>
<canvas id="c" width="960" height="600"></canvas>
<div id="ui">
<div id="info"></div>
<div id="reload">
<b>RELOADING</b>
<div id="rb"><div id="rf"></div></div>
</div>
</div>
<script>
const c=document.getElementById("c"),x=c.getContext("2d"),W=c.width,H=c.height;
const reloadUI=document.getElementById("reload"),reloadFill=document.getElementById("rf");
const keys={},mouse={x:W/2,y:H/2,down:false};
addEventListener("keydown",e=>{keys[e.key.toLowerCase()]=1;if(e.key.toLowerCase()=="r")reload(player)});
addEventListener("keyup",e=>keys[e.key.toLowerCase()]=0);
c.onmousemove=e=>{let r=c.getBoundingClientRect();mouse.x=e.clientX-r.left;mouse.y=e.clientY-r.top};
c.onmousedown=()=>mouse.down=1;c.onmouseup=()=>mouse.down=0;
const WORLD={w:1800,h:1200,grid:40};
const weapons={
pistol :{name:"Pistol", damage:20, rate:300, reload:900, mag:12, ammo:48, speed:850, spread:.02, pellets:1,color:"#ddd"},
smg :{name:"SMG", damage:10, rate:90, reload:1200,mag:30, ammo:120,speed:950, spread:.07,pellets:1,color:"#ffdf55"},
rifle :{name:"Rifle", damage:28, rate:240, reload:1400,mag:20, ammo:80, speed:1200,spread:.015,pellets:1,color:"#77dfff"},
shotgun:{name:"Shotgun",damage:13, rate:700, reload:1600,mag:6, ammo:30, speed:800,spread:.20,pellets:6,color:"#ff8f62"}
};
const walls=[
{x:120,y:80,w:420,h:40},{x:650,y:80,w:40,h:300},{x:780,y:40,w:500,h:40},
{x:250,y:250,w:40,h:400},{x:420,y:500,w:420,h:40},{x:930,y:250,w:420,h:40},
{x:1040,y:450,w:40,h:400},{x:1400,y:120,w:40,h:500},{x:1320,y:760,w:350,h:40},
{x:700,y:850,w:40,h:250},{x:180,y:850,w:350,h:40}
];
function clamp(v,a,b){return Math.max(a,Math.min(b,v))}
function len(x,y){return Math.hypot(x,y)}
function norm(x,y){let l=len(x,y)||1;return [x/l,y/l]}
function dist(a,b){return Math.hypot(a.x-b.x,a.y-b.y)}
function blocked(cx,cy,r){
return walls.some(w=>{
let qx=clamp(cx,w.x,w.x+w.w),qy=clamp(cy,w.y,w.y+w.h);
return (cx-qx)**2+(cy-qy)**2<r*r;
});
}
function move(o,dx,dy){
let nx=o.x+dx;if(!blocked(nx,o.y,o.r))o.x=clamp(nx,o.r,WORLD.w-o.r);
let ny=o.y+dy;if(!blocked(o.x,ny,o.r))o.y=clamp(ny,o.r,WORLD.h-o.r);
}
function segRect(ax,ay,bx,by,r){
let dx=bx-ax,dy=by-ay;
let t0=0,t1=1;
for(const [p,q] of [[-dx,dx],[ax-r,-Infinity],[dy,-dy],[ay-r,-Infinity]]){
if(p===0)continue;
}
// sampled segment test is cheap enough for this prototype
for(let i=0;i<=12;i++){
let t=i/12,px=ax+dx*t,py=ay+dy*t;
if(px>=r&&px<=WORLD.w-r&&py>=r&&py<=WORLD.h-r){}
for(const w of walls)
if(px>=w.x-r&&px<=w.x+w.w+r&&py>=w.y-r&&py<=w.y+w.h+r)return true;
}
return false;
}
function visible(a,b){
let dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy),n=Math.max(1,Math.ceil(d/20));
for(let i=1;i<n;i++){
let t=i/n,px=a.x+dx*t,py=a.y+dy*t;
if(walls.some(w=>px>w.x&&px<w.x+w.w&&py>w.y&&py<w.y+w.h))return false;
}
return true;
}
function gridOK(gx,gy){
let px=gx*WORLD.grid+WORLD.grid/2,py=gy*WORLD.grid+WORLD.grid/2;
return px>=20&&py>=20&&px<WORLD.w-20&&py<WORLD.h-20&&!blocked(px,py,14);
}
const GW=Math.ceil(WORLD.w/WORLD.grid),GH=Math.ceil(WORLD.h/WORLD.grid);
function AStar(a,b){
let sx=clamp(Math.floor(a.x/WORLD.grid),0,GW-1),sy=clamp(Math.floor(a.y/WORLD.grid),0,GH-1);
let ex=clamp(Math.floor(b.x/WORLD.grid),0,GW-1),ey=clamp(Math.floor(b.y/WORLD.grid),0,GH-1);
const key=(x,y)=>x+","+y;
let open=[{x:sx,y:sy}],g={[key(sx,sy)]:0},f={[key(sx,sy)]:0},came={};
const h=(x,y)=>Math.abs(x-ex)+Math.abs(y-ey);
let guard=0;
while(open.length&&guard++<1200){
open.sort((a,b)=>f[key(a.x,a.y)]-f[key(b.x,b.y)]);
let q=open.shift();
if(q.x==ex&&q.y==ey){
let out=[],k=key(ex,ey);
while(k){let [qx,qy]=k.split(",").map(Number);out.push({x:qx*40+20,y:qy*40+20});k=came[k]}
return out.reverse();
}
for(let [dx,dy] of [[1,0],[-1,0],[0,1],[0,-1]]){
let nx=q.x+dx,ny=q.y+dy,k=key(nx,ny);
if(nx<0||ny<0||nx>=GW||ny>=GH||!gridOK(nx,ny))continue;
let ng=g[key(q.x,q.y)]+1;
if(g[k]===undefined||ng<g[k]){
came[k]=key(q.x,q.y);g[k]=ng;f[k]=ng+h(nx,ny);
if(!open.some(n=>n.x==nx&&n.y==ny))open.push({x:nx,y:ny});
}
}
}
return [];
}
function actor(x,y,r=14,speed=120){
return {x,y,r,speed,vx:0,vy:0,acc:900,decel:1100,angle:0,hp:100};
}
let player=actor(90,700,14,145);
player.weapon="pistol";
player.clip=weapons.pistol.mag;
player.reserve=weapons.pistol.ammo;
player.lastShot=0;
player.reloading=0;
player.sprint=1.8;
function setWeapon(w){
player.weapon=w;
player.clip=Math.min(weapons[w].mag,player.clip||0);
player.reserve=player.reserve||0;
}
function pickupWeapon(w,ammo){
player.weapon=w;
player.clip=weapons[w].mag;
player.reserve=ammo;
}
let enemies=[],bullets=[],loot=[],particles=[];
let wanted=0,crimeTime=0,spawnTime=0;
const patrols=[
[{x:70,y:70},{x:580,y:70},{x:580,y:420},{x:70,y:420}],
[{x:720,y:520},{x:900,y:520},{x:900,y:980},{x:720,y:980}],
[{x:1100,y:100},{x:1550,y:100},{x:1550,y:650},{x:1100,y:650}]
];
function makeEnemy(x,y,weapon="pistol",police=false){
let e=actor(x,y,14,police?105:85);
e.weapon=weapon;e.clip=weapons[weapon].mag;e.reserve=weapons[weapon].ammo;
e.lastShot=0;e.reloading=0;e.police=police;e.state="patrol";
e.route=patrols[(Math.random()*patrols.length)|0].map(p=>({...p}));
e.way=(Math.random()*e.route.length)|0;e.path=[];e.pathI=0;e.lastSeen=0;
enemies.push(e);
}
for(let i=0;i<7;i++)makeEnemy(150+i*170,150+(i%3)*220,["pistol","smg","rifle","shotgun"][i%4]);
function addWanted(v){
wanted=clamp(wanted+v,0,5);crimeTime=performance.now();
}
function fire(o,enemy=false){
let w=weapons[o.weapon],now=performance.now();
if(o.reloading||now-o.lastShot<w.rate)return;
if(o.clip<=0){reload(o);return}
o.clip--;o.lastShot=now;
if(!enemy)addWanted(.12);
let base=o.angle,pellets=w.pellets;
for(let i=0;i<pellets;i++){
let a=base+(Math.random()-.5)*w.spread;
bullets.push({
x:o.x+Math.cos(a)*o.r,y:o.y+Math.sin(a)*o.r,
vx:Math.cos(a)*w.speed,vy:Math.sin(a)*w.speed,
damage:w.damage,life:1000,enemy,rad:3
});
}
}
function reload(o){
let w=weapons[o.weapon];
if(o.reloading||o.clip>=w.mag||o.reserve<=0)return;
o.reloading=performance.now();
if(o===player)reloadUI.style.visibility="visible";
}
function updateReload(o){
if(!o.reloading)return;
let w=weapons[o.weapon],p=clamp((performance.now()-o.reloading)/w.reload,0,1);
if(o===player){
reloadUI.style.visibility=p<1?"visible":"hidden";
reloadFill.style.width=(p*100)+"%";
}
if(p>=1){
let n=Math.min(w.mag-o.clip,o.reserve);
o.clip+=n;o.reserve-=n;o.reloading=0;
}
}
function playerAim(){
cam();
player.angle=Math.atan2(mouse.y+camera.y-player.y,mouse.x+camera.x-player.x);
}
let camera={x:0,y:0};
function cam(){
camera.x=clamp(player.x-W/2,0,WORLD.w-W);
camera.y=clamp(player.y-H/2,0,WORLD.h-H);
}
function enemyCanSee(e){
let dx=player.x-e.x,dy=player.y-e.y,d=Math.hypot(dx,dy);
if(d>360)return false;
let dot=(dx*Math.cos(e.angle)+dy*Math.sin(e.angle))/d;
return dot>Math.cos(Math.PI*.31)&&visible(e,player);
}
function enemyAI(e,dt){
updateReload(e);
let sees=enemyCanSee(e);
if(sees){e.state="chase";e.lastSeen=performance.now();}
else if(e.state==="chase"&&performance.now()-e.lastSeen>3000)e.state="patrol";
let tx,ty;
if(e.state==="chase"){
tx=player.x;ty=player.y;
e.angle=Math.atan2(ty-e.y,tx-e.x);
if(sees)fire(e,true);
}else{
let p=e.route[e.way];
if(dist(e,p)<25){
e.way=(e.way+1)%e.route.length;
e.path=[];
p=e.route[e.way];
}
tx=p.x;ty=p.y;
if(!e.path.length||e.pathI>=e.path.length)e.path=AStar(e,{x:tx,y:ty}),e.pathI=1;
if(e.path[e.pathI]){
let p2=e.path[e.pathI];
if(Math.hypot(p2.x-e.x,p2.y-e.y)<18)e.pathI++;
e.angle=Math.atan2(p2.y-e.y,p2.x-e.x);
}
}
let d=len(tx-e.x,ty-e.y);
if(d){
let target=e.angle,s=e.speed;
if(e.state==="chase"&&dist(e,player)<180)s*=.9;
let ax=Math.cos(target)*s,ay=Math.sin(target)*s;
let dv=(e.vx*ax+e.vy*ay);
e.vx+=(ax-e.vx)*Math.min(1,dt*5);
e.vy+=(ay-e.vy)*Math.min(1,dt*5);
}
let sp=len(e.vx,e.vy);
if(sp>e.speed){e.vx*=e.speed/sp;e.vy*=e.speed/sp}
move(e,e.vx*dt,e.vy*dt);
if(e.clip<=0)reload(e);
}
function hurtEnemy(e,d){
e.hp-=d;
addWanted(.04);
if(e.hp<=0){
addWanted(1);
loot.push({
x:e.x,y:e.y,weapon:e.weapon,
ammo:{pistol:18,smg:45,rifle:30,shotgun:12}[e.weapon],
r:10
});
for(let i=0;i<12;i++)particles.push({x:e.x,y:e.y,vx:(Math.random()-.5)*160,vy:(Math.random()-.5)*160,life:400});
e.dead=1;
}
}
function updateBullets(dt){
for(let b of bullets){
b.x+=b.vx*dt;b.y+=b.vy*dt;b.life-=dt*1000;
if(b.life<=0)continue;
if(b.x<0||b.y<0||b.x>WORLD.w||b.y>WORLD.h){b.life=0;continue}
if(walls.some(w=>b.x>w.x&&b.x<w.x+w.w&&b.y>w.y&&b.y<w.y+w.h)){b.life=0;continue}
if(b.enemy){
if(Math.hypot(b.x-player.x,b.y-player.y)<player.r+b.rad){
player.hp-=b.damage;b.life=0;
}
}else{
for(let e of enemies){
if(e.dead)continue;
if(Math.hypot(b.x-e.x,b.y-e.y)<e.r+b.rad){
hurtEnemy(e,b.damage);b.life=0;break;
}
}
}
}
bullets=bullets.filter(b=>b.life>0);
}
function control(dt){
playerAim();
updateReload(player);
let ix=(keys.d||keys.arrowright?1:0)-(keys.a||keys.arrowleft?1:0);
let iy=(keys.s||keys.arrowdown?1:0)-(keys.w||keys.arrowup?1:0);
if(ix||iy)[ix,iy]=norm(ix,iy);
let sprint=keys.shift?player.sprint:1,targetX=ix*player.speed*sprint,targetY=iy*player.speed*sprint;
let rate=ix||iy?player.acc:player.decel;
player.vx+=(targetX-player.vx)*Math.min(1,rate*dt/Math.max(player.speed,1));
player.vy+=(targetY-player.vy)*Math.min(1,rate*dt/Math.max(player.speed,1));
move(player,player.vx*dt,player.vy*dt);
if(mouse.down)fire(player);
if(player.clip<=0)reload(player);
for(let l of loot){
if(dist(player,l)<28&&keys.e){
pickupWeapon(l.weapon,l.ammo);
l.dead=1;
}
}
loot=loot.filter(l=>!l.dead);
}
function wantedSystem(){
if(performance.now()-crimeTime>12000)wanted=Math.max(0,wanted-.015);
spawnTime-=16;
let target=Math.ceil(wanted);
let cops=enemies.filter(e=>e.police&&!e.dead).length;
if(wanted>0&&cops<target&&spawnTime<=0){
let a=Math.random()*Math.PI*2,d=500,p={
x:clamp(player.x+Math.cos(a)*d,30,WORLD.w-30),
y:clamp(player.y+Math.sin(a)*d,30,WORLD.h-30)
};
if(!blocked(p.x,p.y,20))makeEnemy(p.x,p.y,target>=4?"rifle":"smg",true);
spawnTime=4500;
}
}
function update(dt){
control(dt);
for(let e of enemies)if(!e.dead)enemyAI(e,dt);
// character collision
for(let e of enemies)if(!e.dead){
let dx=player.x-e.x,dy=player.y-e.y,d=Math.hypot(dx,dy);
if(d&&d<player.r+e.r){
let n=(player.r+e.r-d)/d;
move(player,dx*n,dy*n);
}
}
updateBullets(dt);
wantedSystem();
for(let p of particles){p.x+=p.vx*dt;p.y+=p.vy*dt;p.life-=dt*1000}
particles=particles.filter(p=>p.life>0);
enemies=enemies.filter(e=>!e.dead);
if(player.hp<=0){
player.x=90;player.y=700;player.hp=100;player.vx=player.vy=0;wanted=0;
}
}
function drawActor(o,fill){
x.fillStyle=fill;x.beginPath();x.arc(o.x-camera.x,o.y-camera.y,o.r,0,Math.PI*2);x.fill();
x.strokeStyle="#000";x.stroke();
x.strokeStyle="#fff";x.lineWidth=4;
x.beginPath();
x.moveTo(o.x-camera.x,o.y-camera.y);
x.lineTo(
o.x-camera.x+Math.cos(o.angle)*20,
o.y-camera.y+Math.sin(o.angle)*20
);x.stroke();x.lineWidth=1;
}
function drawFOV(e){
if(!e.police)return;
let px=e.x-camera.x,py=e.y-camera.y,r=360;
x.fillStyle="rgba(255,255,80,.07)";
x.beginPath();x.moveTo(px,py);
x.arc(px,py,r,e.angle-Math.PI*.31,e.angle+Math.PI*.31);
x.closePath();x.fill();
}
function draw(){
x.clearRect(0,0,W,H);
cam();
x.fillStyle="#24292f";x.fillRect(-camera.x,-camera.y,WORLD.w,WORLD.h);
x.fillStyle="#454b52";
for(let w of walls)x.fillRect(w.x-camera.x,w.y-camera.y,w.w,w.h);
// simple road markings
x.strokeStyle="#333";
for(let i=0;i<WORLD.w;i+=80){
x.beginPath();x.moveTo(i-camera.x,0-camera.y);x.lineTo(i-camera.x,WORLD.h-camera.y);x.stroke();
}
for(let l of loot){
x.fillStyle=weapons[l.weapon].color;x.beginPath();
x.arc(l.x-camera.x,l.y-camera.y,l.r,0,Math.PI*2);x.fill();
x.fillStyle="#111";x.font="9px Arial";x.textAlign="center";
x.fillText(l.weapon,l.x-camera.x,l.y-camera.y+3);
}
for(let e of enemies)if(!e.dead){drawFOV(e);drawActor(e,e.police?"#3f82ff":"#d94b4b")}
drawActor(player,"#57d97c");
for(let b of bullets){
x.fillStyle=b.enemy?"#ff5d5d":"#fff";
x.beginPath();x.arc(b.x-camera.x,b.y-camera.y,b.rad,0,Math.PI*2);x.fill();
}
for(let p of particles){
x.globalAlpha=clamp(p.life/400,0,1);x.fillStyle="#fff";
x.fillRect(p.x-camera.x,p.y-camera.y,3,3);x.globalAlpha=1;
}
let w=weapons[player.weapon];
document.getElementById("info").innerHTML=
`♥ ${Math.max(0,Math.ceil(player.hp))}
&nbsp; | &nbsp; ${w.name}
&nbsp; | &nbsp; ${player.clip}/${player.reserve}
&nbsp; | &nbsp; Wanted: ${"★".repeat(Math.ceil(wanted))}${"☆".repeat(5-Math.ceil(wanted))}
&nbsp; | &nbsp; E: loot`;
}
let last=performance.now();
function loop(t){
let dt=Math.min(.033,(t-last)/1000);last=t;
update(dt);draw();requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment