Created
September 1, 2026 17:51
-
-
Save EncodeTheCode/0de7f35d0397e92fbdf02bd918bd278c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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))} | |
| | ${w.name} | |
| | ${player.clip}/${player.reserve} | |
| | Wanted: ${"★".repeat(Math.ceil(wanted))}${"☆".repeat(5-Math.ceil(wanted))} | |
| | 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