Created
September 1, 2026 17:51
-
-
Save EncodeTheCode/683807d9f4ba0abcace762abc4ff07ef 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 Style Wanted System</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} | |
| #info{position:absolute;left:12px;top:10px;text-shadow:1px 1px #000} | |
| #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} | |
| </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=>{ | |
| let k=e.key.toLowerCase(); | |
| keys[k]=1; | |
| if(k==="r")reload(player); | |
| if(k==="q"&&!e.repeat)nextWeapon(); | |
| }); | |
| 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; | |
| /* ---------------- WORLD ---------------- */ | |
| const WORLD={w:1800,h:1200,grid:40}; | |
| 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} | |
| ]; | |
| /* | |
| Every weapon keeps its own: | |
| clip = bullets currently loaded | |
| reserve = spare ammunition | |
| */ | |
| 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 weaponOrder=["pistol","smg","rifle","shotgun"]; | |
| /* ---------------- UTIL ---------------- */ | |
| function clamp(v,a,b){return Math.max(a,Math.min(b,v))} | |
| function len(a,b){return Math.hypot(a,b)} | |
| function norm(a,b){ | |
| let l=Math.hypot(a,b)||1; | |
| return [a/l,b/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); | |
| let 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 visible(a,b){ | |
| let dx=b.x-a.x,dy=b.y-a.y; | |
| let d=Math.hypot(dx,dy); | |
| let n=Math.ceil(d/20); | |
| for(let i=1;i<n;i++){ | |
| let t=i/n; | |
| let 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; | |
| } | |
| /* ---------------- A* ---------------- */ | |
| const GW=Math.ceil(WORLD.w/WORLD.grid); | |
| const GH=Math.ceil(WORLD.h/WORLD.grid); | |
| function gridOK(gx,gy){ | |
| let px=gx*40+20,py=gy*40+20; | |
| return px>=20&&py>=20&&px<WORLD.w-20&&py<WORLD.h-20&&!blocked(px,py,14); | |
| } | |
| function AStar(a,b){ | |
| let sx=clamp(Math.floor(a.x/40),0,GW-1); | |
| let sy=clamp(Math.floor(a.y/40),0,GH-1); | |
| let ex=clamp(Math.floor(b.x/40),0,GW-1); | |
| let ey=clamp(Math.floor(b.y/40),0,GH-1); | |
| const key=(x,y)=>x+","+y; | |
| const h=(x,y)=>Math.abs(x-ex)+Math.abs(y-ey); | |
| let open=[{x:sx,y:sy}]; | |
| let g={[key(sx,sy)]:0}; | |
| let f={[key(sx,sy)]:h(sx,sy)}; | |
| let came={}; | |
| let guard=0; | |
| while(open.length&&guard++<1400){ | |
| 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 p=k.split(",").map(Number); | |
| out.push({x:p[0]*40+20,y:p[1]*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; | |
| if(nx<0||ny<0||nx>=GW||ny>=GH||!gridOK(nx,ny))continue; | |
| let k=key(nx,ny),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 []; | |
| } | |
| /* ---------------- ACTORS ---------------- */ | |
| 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, | |
| weapon:"pistol", | |
| reloadStart:0, | |
| /* important: each actor stores ammo for every weapon */ | |
| ammo:{} | |
| }; | |
| } | |
| function initAmmo(){ | |
| let a={}; | |
| for(let w in weapons){ | |
| a[w]={ | |
| clip:weapons[w].mag, | |
| reserve:weapons[w].ammo | |
| }; | |
| } | |
| return a; | |
| } | |
| /* ---------------- PLAYER ---------------- */ | |
| let player=actor(90,700,14,145); | |
| player.ammo=initAmmo(); | |
| player.weapon="pistol"; | |
| player.sprint=1.8; | |
| player.lastShot=0; | |
| function currentAmmo(o){ | |
| return o.ammo[o.weapon]; | |
| } | |
| function switchWeapon(w){ | |
| if(!weapons[w])return; | |
| /* don't switch away from a reload */ | |
| if(player.reloading)return; | |
| player.weapon=w; | |
| player.lastShot=0; | |
| } | |
| function nextWeapon(){ | |
| let i=weaponOrder.indexOf(player.weapon); | |
| switchWeapon(weaponOrder[(i+1)%weaponOrder.length]); | |
| } | |
| /* ---------------- WANTED SYSTEM ---------------- */ | |
| let wanted=0; | |
| let crimeTime=0; | |
| let spawnTime=0; | |
| function addWanted(v){ | |
| wanted=clamp(wanted+v,0,5); | |
| crimeTime=performance.now(); | |
| } | |
| function wantedSystem(){ | |
| if(performance.now()-crimeTime>12000) | |
| wanted=Math.max(0,wanted-.01); | |
| 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; | |
| let d=500; | |
| let 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; | |
| } | |
| } | |
| /* ---------------- PATROLS ---------------- */ | |
| 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} | |
| ] | |
| ]; | |
| let enemies=[]; | |
| function makeEnemy(x,y,weapon="pistol",police=false){ | |
| let e=actor(x,y,14,police?105:85); | |
| e.police=police; | |
| e.weapon=weapon; | |
| e.ammo=initAmmo(); | |
| e.lastShot=0; | |
| /* More natural NPC ammunition */ | |
| e.ammo[weapon].clip=weapons[weapon].mag; | |
| e.ammo[weapon].reserve=weapons[weapon].ammo; | |
| 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; | |
| e.alert=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] | |
| ); | |
| } | |
| /* ---------------- RELOADING ---------------- */ | |
| function reload(o){ | |
| let a=currentAmmo(o),w=weapons[o.weapon]; | |
| if(o.reloading||a.clip>=w.mag||a.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]; | |
| let 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 a=currentAmmo(o); | |
| let take=Math.min(w.mag-a.clip,a.reserve); | |
| a.clip+=take; | |
| a.reserve-=take; | |
| o.reloading=0; | |
| } | |
| } | |
| /* ---------------- SHOOTING ---------------- */ | |
| let bullets=[]; | |
| function fire(o,enemy=false){ | |
| let w=weapons[o.weapon]; | |
| let a=currentAmmo(o); | |
| let now=performance.now(); | |
| if(o.reloading||now-o.lastShot<w.rate)return; | |
| if(a.clip<=0){ | |
| reload(o); | |
| return; | |
| } | |
| a.clip--; | |
| o.lastShot=now; | |
| if(!enemy) | |
| addWanted(.12); | |
| /* | |
| Shotgun = 6 pellets. | |
| Each pellet receives a random angle inside | |
| the narrow spread cone. | |
| */ | |
| for(let i=0;i<w.pellets;i++){ | |
| let ang=o.angle+(Math.random()-.5)*w.spread; | |
| bullets.push({ | |
| x:o.x+Math.cos(ang)*o.r, | |
| y:o.y+Math.sin(ang)*o.r, | |
| vx:Math.cos(ang)*w.speed, | |
| vy:Math.sin(ang)*w.speed, | |
| damage:w.damage, | |
| enemy, | |
| life:1000, | |
| rad:3 | |
| }); | |
| } | |
| } | |
| /* ---------------- ENEMY VISION ---------------- */ | |
| function enemyCanSee(e){ | |
| let dx=player.x-e.x; | |
| let dy=player.y-e.y; | |
| let 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); | |
| } | |
| /* ---------------- ENEMY DAMAGE REACTION ---------------- */ | |
| function enemyHit(e){ | |
| /* | |
| Getting hit immediately overrides patrol behavior. | |
| The enemy now faces the player and enters combat. | |
| */ | |
| e.state="chase"; | |
| e.alert=performance.now(); | |
| e.lastSeen=performance.now(); | |
| e.angle=Math.atan2( | |
| player.y-e.y, | |
| player.x-e.x | |
| ); | |
| /* give enemies a chance to shoot immediately */ | |
| e.lastShot=0; | |
| } | |
| /* ---------------- ENEMY AI ---------------- */ | |
| function enemyAI(e,dt){ | |
| updateReload(e); | |
| let now=performance.now(); | |
| let sees=enemyCanSee(e); | |
| if(sees){ | |
| e.state="chase"; | |
| e.lastSeen=now; | |
| } | |
| /* | |
| When hit recently, stay alert even if the player | |
| briefly moves behind a wall. | |
| */ | |
| if(e.alert&&now-e.alert<2500){ | |
| e.state="chase"; | |
| } | |
| /* | |
| Lose player -> after a short delay return to patrol. | |
| */ | |
| if( | |
| e.state==="chase" && | |
| now-e.lastSeen>4000 && | |
| now-e.alert>2500 | |
| ){ | |
| e.state="patrol"; | |
| e.path=[]; | |
| e.alert=0; | |
| } | |
| let tx,ty; | |
| if(e.state==="chase"){ | |
| tx=player.x; | |
| ty=player.y; | |
| e.angle=Math.atan2( | |
| ty-e.y, | |
| tx-e.x | |
| ); | |
| /* | |
| Enemies can fire as soon as: | |
| - player is in their cone and visible | |
| - OR they were recently hit | |
| */ | |
| if(sees||now-e.alert<1200) | |
| 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(dist(e,p2)<18)e.pathI++; | |
| e.angle=Math.atan2( | |
| p2.y-e.y, | |
| p2.x-e.x | |
| ); | |
| } | |
| } | |
| let targetSpeed=e.speed; | |
| if(e.state==="chase"&&dist(e,player)<180) | |
| targetSpeed*=.9; | |
| let ax=Math.cos(e.angle)*targetSpeed; | |
| let ay=Math.sin(e.angle)*targetSpeed; | |
| e.vx+=(ax-e.vx)*Math.min(1,dt*5); | |
| e.vy+=(ay-e.vy)*Math.min(1,dt*5); | |
| let sp=Math.hypot(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(currentAmmo(e).clip<=0) | |
| reload(e); | |
| } | |
| /* ---------------- DAMAGE ---------------- */ | |
| let loot=[]; | |
| let particles=[]; | |
| function hurtEnemy(e,d){ | |
| e.hp-=d; | |
| /* | |
| Every hit makes the NPC turn around and attack. | |
| */ | |
| enemyHit(e); | |
| if(e.hp<=0){ | |
| addWanted(1); | |
| const dropAmmo={ | |
| pistol:18, | |
| smg:45, | |
| rifle:30, | |
| shotgun:12 | |
| }; | |
| loot.push({ | |
| x:e.x, | |
| y:e.y, | |
| weapon:e.weapon, | |
| ammo:dropAmmo[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; | |
| } | |
| } | |
| /* ---------------- PLAYER ---------------- */ | |
| 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 playerAim(){ | |
| cam(); | |
| player.angle=Math.atan2( | |
| mouse.y+camera.y-player.y, | |
| mouse.x+camera.x-player.x | |
| ); | |
| } | |
| 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; | |
| let tx=ix*player.speed*sprint; | |
| let ty=iy*player.speed*sprint; | |
| let rate= | |
| ix||iy? | |
| player.acc: | |
| player.decel; | |
| player.vx+= | |
| (tx-player.vx)* | |
| Math.min( | |
| 1, | |
| rate*dt/Math.max(player.speed,1) | |
| ); | |
| player.vy+= | |
| (ty-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(currentAmmo(player).clip<=0) | |
| reload(player); | |
| /* | |
| Loot automatically with E. | |
| */ | |
| if(keys.e){ | |
| for(let l of loot){ | |
| if(dist(player,l)<30){ | |
| player.weapon=l.weapon; | |
| currentAmmo(player).clip= | |
| weapons[l.weapon].mag; | |
| currentAmmo(player).reserve+=l.ammo; | |
| l.dead=1; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| /* ---------------- BULLETS ---------------- */ | |
| 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(dist(b,player)<player.r+b.rad){ | |
| player.hp-=b.damage; | |
| b.life=0; | |
| } | |
| }else{ | |
| for(let e of enemies){ | |
| if(e.dead)continue; | |
| if(dist(b,e)<e.r+b.rad){ | |
| hurtEnemy(e,b.damage); | |
| b.life=0; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| bullets=bullets.filter(b=>b.life>0); | |
| } | |
| /* ---------------- DRAWING ---------------- */ | |
| function drawActor(o,fill){ | |
| let px=o.x-camera.x; | |
| let py=o.y-camera.y; | |
| x.fillStyle=fill; | |
| x.beginPath(); | |
| x.arc(px,py,o.r,0,Math.PI*2); | |
| x.fill(); | |
| x.strokeStyle="#000"; | |
| x.stroke(); | |
| /* pistol/SMG/rifle/shotgun represented by direction line */ | |
| x.strokeStyle="#fff"; | |
| x.lineWidth=4; | |
| x.beginPath(); | |
| x.moveTo(px,py); | |
| x.lineTo( | |
| px+Math.cos(o.angle)*20, | |
| py+Math.sin(o.angle)*20 | |
| ); | |
| x.stroke(); | |
| x.lineWidth=1; | |
| } | |
| function drawFOV(e){ | |
| if(!e.police)return; | |
| let px=e.x-camera.x; | |
| let py=e.y-camera.y; | |
| x.fillStyle="rgba(255,255,80,.07)"; | |
| x.beginPath(); | |
| x.moveTo(px,py); | |
| x.arc( | |
| px, | |
| py, | |
| 360, | |
| e.angle-Math.PI*.31, | |
| e.angle+Math.PI*.31 | |
| ); | |
| x.closePath(); | |
| x.fill(); | |
| } | |
| function draw(){ | |
| x.clearRect(0,0,W,H); | |
| cam(); | |
| /* background */ | |
| x.fillStyle="#24292f"; | |
| x.fillRect( | |
| -camera.x, | |
| -camera.y, | |
| WORLD.w, | |
| WORLD.h | |
| ); | |
| /* roads / background grid */ | |
| x.strokeStyle="#333"; | |
| for(let i=0;i<WORLD.w;i+=80){ | |
| x.beginPath(); | |
| x.moveTo(i-camera.x,-camera.y); | |
| x.lineTo(i-camera.x,WORLD.h-camera.y); | |
| x.stroke(); | |
| } | |
| for(let i=0;i<WORLD.h;i+=80){ | |
| x.beginPath(); | |
| x.moveTo(-camera.x,i-camera.y); | |
| x.lineTo(WORLD.w-camera.x,i-camera.y); | |
| x.stroke(); | |
| } | |
| /* walls */ | |
| x.fillStyle="#454b52"; | |
| for(let w of walls) | |
| x.fillRect( | |
| w.x-camera.x, | |
| w.y-camera.y, | |
| w.w, | |
| w.h | |
| ); | |
| /* weapon loot */ | |
| for(let l of loot){ | |
| let w=weapons[l.weapon]; | |
| x.fillStyle=w.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( | |
| w.name, | |
| l.x-camera.x, | |
| l.y-camera.y+3 | |
| ); | |
| } | |
| /* enemies */ | |
| for(let e of enemies){ | |
| if(e.dead)continue; | |
| drawFOV(e); | |
| drawActor( | |
| e, | |
| e.police? | |
| "#3f82ff": | |
| "#d94b4b" | |
| ); | |
| } | |
| /* player */ | |
| drawActor(player,"#57d97c"); | |
| /* bullets */ | |
| 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(); | |
| } | |
| /* hit particles */ | |
| 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 a=currentAmmo(player); | |
| let w=weapons[player.weapon]; | |
| document.getElementById("info").innerHTML= | |
| `♥ ${Math.max(0,Math.ceil(player.hp))} | |
| | ${w.name} | |
| | ${a.clip}/${a.reserve} | |
| | Wanted: | |
| ${"★".repeat(Math.ceil(wanted))} | |
| ${"☆".repeat(5-Math.ceil(wanted))} | |
| | Q: switch | |
| | R: reload | |
| | E: loot`; | |
| } | |
| /* ---------------- GAME UPDATE ---------------- */ | |
| function update(dt){ | |
| control(dt); | |
| for(let e of enemies) | |
| if(!e.dead) | |
| enemyAI(e,dt); | |
| /* player/enemy collision */ | |
| for(let e of enemies){ | |
| if(e.dead)continue; | |
| let dx=player.x-e.x; | |
| let dy=player.y-e.y; | |
| let 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); | |
| loot=loot.filter(l=>!l.dead); | |
| enemies=enemies.filter(e=>!e.dead); | |
| /* respawn */ | |
| if(player.hp<=0){ | |
| player.x=90; | |
| player.y=700; | |
| player.vx=player.vy=0; | |
| player.hp=100; | |
| wanted=0; | |
| } | |
| } | |
| /* ---------------- LOOP ---------------- */ | |
| 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