├── .gitignore ├── package.json ├── README.md ├── index.html ├── index.js ├── pnpm-lock.yaml └── index.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "space-invaders", 3 | "private": true, 4 | "repository": "pi0/space-invaders", 5 | "scripts": { 6 | "push": "pnpm build && git commit -am $1", 7 | "build": "esbuild ./index.ts --outdir=. --minify", 8 | "dev": "esbuild ./index.ts --outfile=index.js --serve --servedir=." 9 | }, 10 | "devDependencies": { 11 | "esbuild": "^0.19.2", 12 | "prettier": "^3.0.3", 13 | "terser": "^5.19.3", 14 | "typescript": "^5.2.2" 15 | }, 16 | "packageManager": "pnpm@8.7.1" 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 👾 Space Invaders 2 | 3 | [Space Invaders](https://en.wikipedia.org/wiki/Space_Invaders) game in single (~4KB Gzipped) JavaScript file! 4 | 5 | [Play Online](https://pi0.github.io/space-invaders/) 6 | 7 | ## Using in your pages 8 | 9 | Checkout [`index.html`](./index.html) for additional styles. 10 | 11 | ```html 12 |
13 | 17 | ``` 18 | 19 | ## License 20 | 21 | MIT - Pooya Parsa 22 | 23 | Based on [a codepen](https://codepen.io/adelciotto/pen/WNzRYy) by Anthony Del Ciotto ([@adelciotto](https://github.com/adelciotto)) 24 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 36 | 37 |
38 | 39 | 43 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export function startGame(p={}){const le=/Chrome/.test(navigator.userAgent)&&/Google Inc/.test(navigator.vendor),a=p.width||640,r=p.height||640,oe="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAEACAYAAAADRnAGAAACGUlEQVR42u3aSQ7CMBAEQIsn8P+/hiviAAK8zFIt5QbELiTHmfEYE3L9mZE9AAAAqAVwBQ8AAAD6THY5CgAAAKbfbPX3AQAAYBEEAADAuZrC6UUyfMEEAIBiAN8OePXnAQAAsLcmmKFPAQAAgHMbm+gbr3Sdo/LtcAAAANR6GywPAgBAM4D2JXAAABoBzBjA7AmlOx8AAEAzAOcDAADovTc4vQim6wUCABAYQG8QAADd4dPd2fRVYQAAANQG0B4HAABAawDnAwAA6AXgfAAAALpA2uMAAABwPgAAgPoAM9Ci/R4AAAD2dmqcEQIAIC/AiQGuAAYAAECcRS/a/cJXkUf2AAAAoBaA3iAAALrD+gIAAADY9baX/nwAAADNADwFAADo9YK0e5FMX/UFACA5QPSNEAAAAHKtCekmDAAAAADvBljtfgAAAGgMMGOrunvCy2uCAAAACFU6BwAAwF6AGQPa/XsAAADYB+B8AAAAtU+ItD4OAwAAAFVhAACaA0T7B44/BQAAANALwGMQAAAAADYO8If2+P31AgAAQN0SWbhFDwCAZlXgaO1xAAAA1FngnA8AACAeQPSNEAAAAM4CnC64AAAA4GzN4N9NSfgKEAAAAACszO26X8/X6BYAAAD0Anid8KcLAAAAAAAAAJBnwNEvAAAA9Jns1ygAAAAAAAAAAAAAAAAAAABAQ4COCENERERERERERBrnAa1sJuUVr3rsAAAAAElFTkSuQmCC",w=37,x=39,ae=32,re=500,ce={x:0,y:204,w:62,h:32},ue=[{x:0,y:0,w:51,h:34},{x:0,y:102,w:51,h:34}],he=[{x:0,y:137,w:50,h:33},{x:0,y:170,w:50,h:34}],Ae=[{x:0,y:68,w:50,h:32},{x:0,y:34,w:50,h:32}],I=40,me=11*I;function de(i,e){return Math.random()*(e-i)+i}function pe(i,e,t){return Math.min(Math.max(i,e),t)}function E(i,e,t){return i<=t&&i>=e}function Q(i,e){const t=E(i.x,e.x,e.x+e.w)||E(e.x,i.x,i.x+i.w),n=E(i.y,e.y,e.y+e.h)||E(e.y,i.y,i.y+i.h);return t&&n}class O{x;y;constructor(e,t){this.x=typeof e>"u"?0:e,this.y=typeof t>"u"?0:t}set(e,t){this.x=e,this.y=t}}class be{x;y;w;h;constructor(e,t,n,s){this.x=typeof e>"u"?0:e,this.y=typeof t>"u"?0:t,this.w=typeof n>"u"?0:n,this.h=typeof s>"u"?0:s}set(e,t,n,s){this.x=e,this.y=t,this.w=n,this.h=s}}let A,l,f,S,u=[],M=[],N=0,o,h=[],v,R=!1,g=-1,P=0,y=0,H=1,m=!1;class G{img;position;scale;bounds;doLogic;constructor(e,t,n){this.img=e,this.position=new O(t,n),this.scale=new O(1,1),this.bounds=new be(t,n,this.img.width,this.img.height),this.doLogic=!0}update(e){}_updateBounds(){this.bounds.set(this.position.x,this.position.y,~~(.5+this.img.width*this.scale.x),~~(.5+this.img.height*this.scale.y))}_drawImage(){l.drawImage(this.img,this.position.x,this.position.y)}draw(e){this._updateBounds(),this._drawImage()}}class Y extends G{clipRect;constructor(e,t,n,s){super(e,n,s),this.clipRect=t,this.bounds.set(n,s,this.clipRect.w,this.clipRect.h)}update(e){}_updateBounds(){const e=~~(.5+this.clipRect.w*this.scale.x),t=~~(.5+this.clipRect.h*this.scale.y);this.bounds.set(this.position.x-e/2,this.position.y-t/2,e,t)}_drawImage(){l.save(),l.transform(this.scale.x,0,0,this.scale.y,this.position.x,this.position.y),l.drawImage(this.img,this.clipRect.x,this.clipRect.y,this.clipRect.w,this.clipRect.h,~~(.5+-this.clipRect.w*.5),~~(.5+-this.clipRect.h*.5),this.clipRect.w,this.clipRect.h),l.restore()}draw(e){super.draw(e)}}class fe extends Y{lives;xVel;bullets;bulletDelayAccumulator;score;constructor(){super(f,ce,a/2,r-70),this.scale.set(.85,.85),this.lives=3,this.xVel=0,this.bullets=[],this.bulletDelayAccumulator=0,this.score=0}reset(){this.lives=3,this.score=0,this.position.set(a/2,r-70)}shoot(){const e=new K(this.position.x,this.position.y-this.bounds.h/2,1,1e3);this.bullets.push(e),C("shoot")}handleInput(){X(w)?this.xVel=-175:X(x)?this.xVel=175:this.xVel=0,U(ae)&&this.bulletDelayAccumulator>.5&&(this.shoot(),this.bulletDelayAccumulator=0)}updateBullets(e){for(let t=this.bullets.length-1;t>=0;t--){let n=this.bullets[t];n.alive?n.update(e):(this.bullets.splice(t,1),n=void 0)}}update(e){this.bulletDelayAccumulator+=e,this.position.x+=this.xVel*e,this.position.x=pe(this.position.x,this.bounds.w/2,a-this.bounds.w/2),this.updateBullets(e)}draw(e){super.draw(e);for(let t=0,n=this.bullets.length;t=this.stepDelay){this.position.xa-this.bounds.w/2-20&&(R=!0),this.position.y>a-50&&Ee();const t=Math.floor(Math.random()*(this.stepDelay+1));de(0,1e3)<=5*(this.stepDelay+1)&&(this.doShoot=!0),this.position.x+=10*g,this.toggleFrame(),this.stepAccumulator=0}this.position.y+=P,this.bullet&&this.bullet.alive?this.bullet.update(e):this.bullet=void 0}draw(e){super.draw(e),this.bullet!==void 0&&this.bullet.alive&&this.bullet.draw(e)}}class ye{particlePool;particles;constructor(){this.particlePool=[],this.particles=[]}draw(){for(let e=this.particles.length-1;e>=0;e--){const t=this.particles[e];t.moves++,t.x+=t.xunits,t.y+=t.yunits+t.gravity*t.moves,t.life--,t.life<=0?this.particlePool.length<100?this.particlePool.push(this.particles.splice(e,1)):this.particles.splice(e,1):(l.globalAlpha=t.life/t.maxLife,l.fillStyle=t.color,l.fillRect(t.x,t.y,t.width,t.height),l.globalAlpha=1)}}createExplosion(e,t,n,s,d,b,j,Z,$){for(let ee=0;ee0){const c=this.particlePool.pop();c.x=e,c.y=t,c.xunits=ne,c.yunits=se,c.life=D,c.color=n,c.width=d,c.height=b,c.gravity=Z,c.moves=0,c.alpha=1,c.maxLife=D,this.particles.push(c)}else this.particles.push({x:e,y:t,xunits:ne,yunits:se,life:D,color:n,width:d,height:b,gravity:Z,moves:0,alpha:1,maxLife:D})}}}function we(){if(p.canvas)A=p.canvas;else{const i=p.selector||"#invaders",e=document.querySelector(i)||document.body;A=document.createElement("canvas"),e.appendChild(A)}A.width=a,A.height=r,l=A.getContext("2d"),k(!1),f=new Image,f.src=oe,xe(),window.addEventListener("resize",J),document.addEventListener("keydown",Fe),document.addEventListener("keyup",Be)}function xe(){const i=Me(2,8,e=>{e.fillStyle="white",e.fillRect(0,0,e.canvas.width,e.canvas.height)});S=new Image,S.src=i.toDataURL()}function k(i){l.imageSmoothingEnabled=i,l.mozImageSmoothingEnabled=i,l.oImageSmoothingEnabled=i,l.webkitImageSmoothingEnabled=i,l.msImageSmoothingEnabled=i}function L(){h=[],o=new fe,v=new ye,T(),W()}function T(){y=0;for(let i=0,e=5*11;i=0;e--){let t=h[e];if(!t.alive){h.splice(e,1),t=void 0,y--,y<1&&(H++,T());return}if(t.stepDelay=(y*20-H*10)/1e3,t.stepDelay<=.05&&(t.stepDelay=.05),t.update(i),t.doShoot){t.doShoot=!1,t.shoot();const n=String(Math.round(Math.random()*3+1));C(`fastinvader${n}`)}}P=0}function Ce(){const i=o.bullets;for(let e=0,t=i.length;e100&&(e=100),U(13)&&!m&&(L(),m=!0),m&&Se(e/1e3),l.fillStyle="black",l.fillRect(0,0,a,r),m?Le(!1):Te(),N=i,requestAnimationFrame(z)}function J(){const i=window.innerWidth,e=window.innerHeight,t=Math.min(i/a,e/r);le?(A.width=a*t,A.height=r*t,k(!1),l.transform(t,0,0,t,0,0)):(A.style.width=a*t+"px",A.style.height=r*t+"px")}function Fe(i){i.preventDefault(),u[i.keyCode]=!0}function Be(i){i.preventDefault(),u[i.keyCode]=!1}let q;document.addEventListener("touchstart",i=>{q={x:i.touches[0].clientX,y:i.touches[0].clientY},m?o.shoot():(L(),m=!0)}),document.addEventListener("touchmove",i=>{const t={x:i.touches[0].clientX,y:i.touches[0].clientY}.x-q.x;t>0?(u[x]=!0,u[w]=!1):t<0&&(u[w]=!0,u[x]=!1)}),document.addEventListener("touchend",i=>{u[w]=!1,u[x]=!1});const _=document.createElement("link");_.rel="stylesheet",_.href="https://fonts.googleapis.com/css?family=Play:400,700",document.head.appendChild(_),ve(),z(),p.autoPlay&&(L(),m=!0);async function C(i){}} 2 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | devDependencies: 4 | esbuild: 5 | specifier: ^0.19.2 6 | version: 0.19.2 7 | prettier: 8 | specifier: ^3.0.3 9 | version: 3.0.3 10 | terser: 11 | specifier: ^5.19.3 12 | version: 5.19.3 13 | typescript: 14 | specifier: ^5.2.2 15 | version: 5.2.2 16 | 17 | packages: 18 | 19 | /@esbuild/android-arm64@0.19.2: 20 | resolution: {integrity: sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw==} 21 | engines: {node: '>=12'} 22 | cpu: [arm64] 23 | os: [android] 24 | requiresBuild: true 25 | dev: true 26 | optional: true 27 | 28 | /@esbuild/android-arm@0.19.2: 29 | resolution: {integrity: sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q==} 30 | engines: {node: '>=12'} 31 | cpu: [arm] 32 | os: [android] 33 | requiresBuild: true 34 | dev: true 35 | optional: true 36 | 37 | /@esbuild/android-x64@0.19.2: 38 | resolution: {integrity: sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w==} 39 | engines: {node: '>=12'} 40 | cpu: [x64] 41 | os: [android] 42 | requiresBuild: true 43 | dev: true 44 | optional: true 45 | 46 | /@esbuild/darwin-arm64@0.19.2: 47 | resolution: {integrity: sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA==} 48 | engines: {node: '>=12'} 49 | cpu: [arm64] 50 | os: [darwin] 51 | requiresBuild: true 52 | dev: true 53 | optional: true 54 | 55 | /@esbuild/darwin-x64@0.19.2: 56 | resolution: {integrity: sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw==} 57 | engines: {node: '>=12'} 58 | cpu: [x64] 59 | os: [darwin] 60 | requiresBuild: true 61 | dev: true 62 | optional: true 63 | 64 | /@esbuild/freebsd-arm64@0.19.2: 65 | resolution: {integrity: sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ==} 66 | engines: {node: '>=12'} 67 | cpu: [arm64] 68 | os: [freebsd] 69 | requiresBuild: true 70 | dev: true 71 | optional: true 72 | 73 | /@esbuild/freebsd-x64@0.19.2: 74 | resolution: {integrity: sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw==} 75 | engines: {node: '>=12'} 76 | cpu: [x64] 77 | os: [freebsd] 78 | requiresBuild: true 79 | dev: true 80 | optional: true 81 | 82 | /@esbuild/linux-arm64@0.19.2: 83 | resolution: {integrity: sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg==} 84 | engines: {node: '>=12'} 85 | cpu: [arm64] 86 | os: [linux] 87 | requiresBuild: true 88 | dev: true 89 | optional: true 90 | 91 | /@esbuild/linux-arm@0.19.2: 92 | resolution: {integrity: sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg==} 93 | engines: {node: '>=12'} 94 | cpu: [arm] 95 | os: [linux] 96 | requiresBuild: true 97 | dev: true 98 | optional: true 99 | 100 | /@esbuild/linux-ia32@0.19.2: 101 | resolution: {integrity: sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ==} 102 | engines: {node: '>=12'} 103 | cpu: [ia32] 104 | os: [linux] 105 | requiresBuild: true 106 | dev: true 107 | optional: true 108 | 109 | /@esbuild/linux-loong64@0.19.2: 110 | resolution: {integrity: sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw==} 111 | engines: {node: '>=12'} 112 | cpu: [loong64] 113 | os: [linux] 114 | requiresBuild: true 115 | dev: true 116 | optional: true 117 | 118 | /@esbuild/linux-mips64el@0.19.2: 119 | resolution: {integrity: sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg==} 120 | engines: {node: '>=12'} 121 | cpu: [mips64el] 122 | os: [linux] 123 | requiresBuild: true 124 | dev: true 125 | optional: true 126 | 127 | /@esbuild/linux-ppc64@0.19.2: 128 | resolution: {integrity: sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw==} 129 | engines: {node: '>=12'} 130 | cpu: [ppc64] 131 | os: [linux] 132 | requiresBuild: true 133 | dev: true 134 | optional: true 135 | 136 | /@esbuild/linux-riscv64@0.19.2: 137 | resolution: {integrity: sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw==} 138 | engines: {node: '>=12'} 139 | cpu: [riscv64] 140 | os: [linux] 141 | requiresBuild: true 142 | dev: true 143 | optional: true 144 | 145 | /@esbuild/linux-s390x@0.19.2: 146 | resolution: {integrity: sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g==} 147 | engines: {node: '>=12'} 148 | cpu: [s390x] 149 | os: [linux] 150 | requiresBuild: true 151 | dev: true 152 | optional: true 153 | 154 | /@esbuild/linux-x64@0.19.2: 155 | resolution: {integrity: sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ==} 156 | engines: {node: '>=12'} 157 | cpu: [x64] 158 | os: [linux] 159 | requiresBuild: true 160 | dev: true 161 | optional: true 162 | 163 | /@esbuild/netbsd-x64@0.19.2: 164 | resolution: {integrity: sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ==} 165 | engines: {node: '>=12'} 166 | cpu: [x64] 167 | os: [netbsd] 168 | requiresBuild: true 169 | dev: true 170 | optional: true 171 | 172 | /@esbuild/openbsd-x64@0.19.2: 173 | resolution: {integrity: sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw==} 174 | engines: {node: '>=12'} 175 | cpu: [x64] 176 | os: [openbsd] 177 | requiresBuild: true 178 | dev: true 179 | optional: true 180 | 181 | /@esbuild/sunos-x64@0.19.2: 182 | resolution: {integrity: sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw==} 183 | engines: {node: '>=12'} 184 | cpu: [x64] 185 | os: [sunos] 186 | requiresBuild: true 187 | dev: true 188 | optional: true 189 | 190 | /@esbuild/win32-arm64@0.19.2: 191 | resolution: {integrity: sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg==} 192 | engines: {node: '>=12'} 193 | cpu: [arm64] 194 | os: [win32] 195 | requiresBuild: true 196 | dev: true 197 | optional: true 198 | 199 | /@esbuild/win32-ia32@0.19.2: 200 | resolution: {integrity: sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA==} 201 | engines: {node: '>=12'} 202 | cpu: [ia32] 203 | os: [win32] 204 | requiresBuild: true 205 | dev: true 206 | optional: true 207 | 208 | /@esbuild/win32-x64@0.19.2: 209 | resolution: {integrity: sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw==} 210 | engines: {node: '>=12'} 211 | cpu: [x64] 212 | os: [win32] 213 | requiresBuild: true 214 | dev: true 215 | optional: true 216 | 217 | /@jridgewell/gen-mapping@0.3.3: 218 | resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} 219 | engines: {node: '>=6.0.0'} 220 | dependencies: 221 | '@jridgewell/set-array': 1.1.2 222 | '@jridgewell/sourcemap-codec': 1.4.15 223 | '@jridgewell/trace-mapping': 0.3.19 224 | dev: true 225 | 226 | /@jridgewell/resolve-uri@3.1.1: 227 | resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} 228 | engines: {node: '>=6.0.0'} 229 | dev: true 230 | 231 | /@jridgewell/set-array@1.1.2: 232 | resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} 233 | engines: {node: '>=6.0.0'} 234 | dev: true 235 | 236 | /@jridgewell/source-map@0.3.5: 237 | resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} 238 | dependencies: 239 | '@jridgewell/gen-mapping': 0.3.3 240 | '@jridgewell/trace-mapping': 0.3.19 241 | dev: true 242 | 243 | /@jridgewell/sourcemap-codec@1.4.15: 244 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 245 | dev: true 246 | 247 | /@jridgewell/trace-mapping@0.3.19: 248 | resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} 249 | dependencies: 250 | '@jridgewell/resolve-uri': 3.1.1 251 | '@jridgewell/sourcemap-codec': 1.4.15 252 | dev: true 253 | 254 | /acorn@8.10.0: 255 | resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} 256 | engines: {node: '>=0.4.0'} 257 | hasBin: true 258 | dev: true 259 | 260 | /buffer-from@1.1.2: 261 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 262 | dev: true 263 | 264 | /commander@2.20.3: 265 | resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} 266 | dev: true 267 | 268 | /esbuild@0.19.2: 269 | resolution: {integrity: sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg==} 270 | engines: {node: '>=12'} 271 | hasBin: true 272 | requiresBuild: true 273 | optionalDependencies: 274 | '@esbuild/android-arm': 0.19.2 275 | '@esbuild/android-arm64': 0.19.2 276 | '@esbuild/android-x64': 0.19.2 277 | '@esbuild/darwin-arm64': 0.19.2 278 | '@esbuild/darwin-x64': 0.19.2 279 | '@esbuild/freebsd-arm64': 0.19.2 280 | '@esbuild/freebsd-x64': 0.19.2 281 | '@esbuild/linux-arm': 0.19.2 282 | '@esbuild/linux-arm64': 0.19.2 283 | '@esbuild/linux-ia32': 0.19.2 284 | '@esbuild/linux-loong64': 0.19.2 285 | '@esbuild/linux-mips64el': 0.19.2 286 | '@esbuild/linux-ppc64': 0.19.2 287 | '@esbuild/linux-riscv64': 0.19.2 288 | '@esbuild/linux-s390x': 0.19.2 289 | '@esbuild/linux-x64': 0.19.2 290 | '@esbuild/netbsd-x64': 0.19.2 291 | '@esbuild/openbsd-x64': 0.19.2 292 | '@esbuild/sunos-x64': 0.19.2 293 | '@esbuild/win32-arm64': 0.19.2 294 | '@esbuild/win32-ia32': 0.19.2 295 | '@esbuild/win32-x64': 0.19.2 296 | dev: true 297 | 298 | /prettier@3.0.3: 299 | resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==} 300 | engines: {node: '>=14'} 301 | hasBin: true 302 | dev: true 303 | 304 | /source-map-support@0.5.21: 305 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 306 | dependencies: 307 | buffer-from: 1.1.2 308 | source-map: 0.6.1 309 | dev: true 310 | 311 | /source-map@0.6.1: 312 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 313 | engines: {node: '>=0.10.0'} 314 | dev: true 315 | 316 | /terser@5.19.3: 317 | resolution: {integrity: sha512-pQzJ9UJzM0IgmT4FAtYI6+VqFf0lj/to58AV0Xfgg0Up37RyPG7Al+1cepC6/BVuAxR9oNb41/DL4DEoHJvTdg==} 318 | engines: {node: '>=10'} 319 | hasBin: true 320 | dependencies: 321 | '@jridgewell/source-map': 0.3.5 322 | acorn: 8.10.0 323 | commander: 2.20.3 324 | source-map-support: 0.5.21 325 | dev: true 326 | 327 | /typescript@5.2.2: 328 | resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} 329 | engines: {node: '>=14.17'} 330 | hasBin: true 331 | dev: true 332 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | // Based on original source from https://codepen.io/adelciotto/pen/WNzRYy 2 | // By Anthony Del Ciotto 3 | 4 | export interface InvadersOptions { 5 | selector?: string; 6 | canvas?: HTMLCanvasElement; 7 | width?: number; 8 | height?: number; 9 | autoPlay?: boolean; 10 | title?: string; 11 | } 12 | 13 | export function startGame(options: InvadersOptions = {}) { 14 | // ################################################################### 15 | // Constants 16 | // ################################################################### 17 | const IS_CHROME = 18 | /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); 19 | const CANVAS_WIDTH = options.width || 640; 20 | const CANVAS_HEIGHT = options.height || 640; 21 | const SPRITE_SHEET_SRC = 22 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAEACAYAAAADRnAGAAACGUlEQVR42u3aSQ7CMBAEQIsn8P+/hiviAAK8zFIt5QbELiTHmfEYE3L9mZE9AAAAqAVwBQ8AAAD6THY5CgAAAKbfbPX3AQAAYBEEAADAuZrC6UUyfMEEAIBiAN8OePXnAQAAsLcmmKFPAQAAgHMbm+gbr3Sdo/LtcAAAANR6GywPAgBAM4D2JXAAABoBzBjA7AmlOx8AAEAzAOcDAADovTc4vQim6wUCABAYQG8QAADd4dPd2fRVYQAAANQG0B4HAABAawDnAwAA6AXgfAAAALpA2uMAAABwPgAAgPoAM9Ci/R4AAAD2dmqcEQIAIC/AiQGuAAYAAECcRS/a/cJXkUf2AAAAoBaA3iAAALrD+gIAAADY9baX/nwAAADNADwFAADo9YK0e5FMX/UFACA5QPSNEAAAAHKtCekmDAAAAADvBljtfgAAAGgMMGOrunvCy2uCAAAACFU6BwAAwF6AGQPa/XsAAADYB+B8AAAAtU+ItD4OAwAAAFVhAACaA0T7B44/BQAAANALwGMQAAAAADYO8If2+P31AgAAQN0SWbhFDwCAZlXgaO1xAAAA1FngnA8AACAeQPSNEAAAAM4CnC64AAAA4GzN4N9NSfgKEAAAAACszO26X8/X6BYAAAD0Anid8KcLAAAAAAAAAJBnwNEvAAAA9Jns1ygAAAAAAAAAAAAAAAAAAABAQ4COCENERERERERERBrnAa1sJuUVr3rsAAAAAElFTkSuQmCC"; 23 | const LEFT_KEY = 37; 24 | const RIGHT_KEY = 39; 25 | const SHOOT_KEY = 32; /* space */ 26 | const TEXT_BLINK_FREQ = 500; 27 | const PLAYER_CLIP_RECT = { x: 0, y: 204, w: 62, h: 32 }; 28 | const ALIEN_BOTTOM_ROW = [ 29 | { x: 0, y: 0, w: 51, h: 34 }, 30 | { x: 0, y: 102, w: 51, h: 34 }, 31 | ]; 32 | const ALIEN_MIDDLE_ROW = [ 33 | { x: 0, y: 137, w: 50, h: 33 }, 34 | { x: 0, y: 170, w: 50, h: 34 }, 35 | ]; 36 | const ALIEN_TOP_ROW = [ 37 | { x: 0, y: 68, w: 50, h: 32 }, 38 | { x: 0, y: 34, w: 50, h: 32 }, 39 | ]; 40 | const ALIEN_X_MARGIN = 40; 41 | const ALIEN_SQUAD_WIDTH = 11 * ALIEN_X_MARGIN; 42 | 43 | type ClipRect = { x: number; y: number; w: number; h: number }; 44 | 45 | // ################################################################### 46 | // Utility functions & classes 47 | // 48 | // ################################################################### 49 | function getRandomArbitrary(min: number, max: number) { 50 | return Math.random() * (max - min) + min; 51 | } 52 | 53 | function clamp(num: number, min: number, max: number) { 54 | return Math.min(Math.max(num, min), max); 55 | } 56 | 57 | function valueInRange(value: number, min: number, max: number) { 58 | return value <= max && value >= min; 59 | } 60 | 61 | function checkRectCollision(A: ClipRect, B: ClipRect) { 62 | const xOverlap = 63 | valueInRange(A.x, B.x, B.x + B.w) || valueInRange(B.x, A.x, A.x + A.w); 64 | 65 | const yOverlap = 66 | valueInRange(A.y, B.y, B.y + B.h) || valueInRange(B.y, A.y, A.y + A.h); 67 | return xOverlap && yOverlap; 68 | } 69 | 70 | class Point2D { 71 | x: number; 72 | y: number; 73 | 74 | constructor(x: number, y: number) { 75 | this.x = typeof x === "undefined" ? 0 : x; 76 | this.y = typeof y === "undefined" ? 0 : y; 77 | } 78 | 79 | set(x: number, y: number) { 80 | this.x = x; 81 | this.y = y; 82 | } 83 | } 84 | 85 | class Rect { 86 | x: number; 87 | y: number; 88 | w: number; 89 | h: number; 90 | 91 | constructor(x: number, y: number, w: number, h: number) { 92 | this.x = typeof x === "undefined" ? 0 : x; 93 | this.y = typeof y === "undefined" ? 0 : y; 94 | this.w = typeof w === "undefined" ? 0 : w; 95 | this.h = typeof h === "undefined" ? 0 : h; 96 | } 97 | 98 | set(x: number, y: number, w: number, h: number) { 99 | this.x = x; 100 | this.y = y; 101 | this.w = w; 102 | this.h = h; 103 | } 104 | } 105 | 106 | // ################################################################### 107 | // Globals 108 | // ################################################################### 109 | let canvas: HTMLCanvasElement; 110 | let ctx: CanvasRenderingContext2D; 111 | let spriteSheetImg: HTMLImageElement; 112 | let bulletImg: HTMLImageElement; 113 | let keyStates: boolean[] = []; 114 | let prevKeyStates: boolean[] = []; 115 | let lastTime = 0; 116 | let player: Player; 117 | let aliens: Enemy[] = []; 118 | let particleManager: ParticleExplosion; 119 | let updateAlienLogic = false; 120 | let alienDirection = -1; 121 | let alienYDown = 0; 122 | let alienCount = 0; 123 | let wave = 1; 124 | let hasGameStarted = false; 125 | 126 | // ################################################################### 127 | // Entities 128 | // ################################################################### 129 | class BaseSprite { 130 | img: HTMLImageElement; 131 | position: Point2D; 132 | scale: Point2D; 133 | bounds: Rect; 134 | doLogic: boolean; 135 | constructor(img: HTMLImageElement, x: number, y: number) { 136 | this.img = img; 137 | this.position = new Point2D(x, y); 138 | this.scale = new Point2D(1, 1); 139 | this.bounds = new Rect(x, y, this.img.width, this.img.height); 140 | this.doLogic = true; 141 | } 142 | 143 | update(dt: number) {} 144 | 145 | _updateBounds() { 146 | this.bounds.set( 147 | this.position.x, 148 | this.position.y, 149 | ~~(0.5 + this.img.width * this.scale.x), 150 | ~~(0.5 + this.img.height * this.scale.y), 151 | ); 152 | } 153 | 154 | _drawImage() { 155 | ctx.drawImage(this.img, this.position.x, this.position.y); 156 | } 157 | 158 | draw(resized: boolean) { 159 | this._updateBounds(); 160 | this._drawImage(); 161 | } 162 | } 163 | 164 | class SheetSprite extends BaseSprite { 165 | clipRect: ClipRect; 166 | 167 | constructor( 168 | sheetImg: HTMLImageElement, 169 | clipRect: ClipRect, 170 | x: number, 171 | y: number, 172 | ) { 173 | super(sheetImg, x, y); 174 | this.clipRect = clipRect; 175 | this.bounds.set(x, y, this.clipRect.w, this.clipRect.h); 176 | } 177 | 178 | update(dt: any) {} 179 | 180 | _updateBounds() { 181 | const w = ~~(0.5 + this.clipRect.w * this.scale.x); 182 | const h = ~~(0.5 + this.clipRect.h * this.scale.y); 183 | this.bounds.set(this.position.x - w / 2, this.position.y - h / 2, w, h); 184 | } 185 | 186 | _drawImage() { 187 | ctx.save(); 188 | ctx.transform( 189 | this.scale.x, 190 | 0, 191 | 0, 192 | this.scale.y, 193 | this.position.x, 194 | this.position.y, 195 | ); 196 | ctx.drawImage( 197 | this.img, 198 | this.clipRect.x, 199 | this.clipRect.y, 200 | this.clipRect.w, 201 | this.clipRect.h, 202 | ~~(0.5 + -this.clipRect.w * 0.5), 203 | ~~(0.5 + -this.clipRect.h * 0.5), 204 | this.clipRect.w, 205 | this.clipRect.h, 206 | ); 207 | ctx.restore(); 208 | } 209 | 210 | draw(resized: boolean) { 211 | super.draw(resized); 212 | } 213 | } 214 | 215 | class Player extends SheetSprite { 216 | lives: number; 217 | xVel: number; 218 | bullets: any[]; 219 | bulletDelayAccumulator: number; 220 | score: number; 221 | constructor() { 222 | super( 223 | spriteSheetImg, 224 | PLAYER_CLIP_RECT, 225 | CANVAS_WIDTH / 2, 226 | CANVAS_HEIGHT - 70, 227 | ); 228 | this.scale.set(0.85, 0.85); 229 | this.lives = 3; 230 | this.xVel = 0; 231 | this.bullets = []; 232 | this.bulletDelayAccumulator = 0; 233 | this.score = 0; 234 | } 235 | 236 | reset() { 237 | this.lives = 3; 238 | this.score = 0; 239 | this.position.set(CANVAS_WIDTH / 2, CANVAS_HEIGHT - 70); 240 | } 241 | 242 | shoot() { 243 | const bullet = new Bullet( 244 | this.position.x, 245 | this.position.y - this.bounds.h / 2, 246 | 1, 247 | 1000, 248 | ); 249 | this.bullets.push(bullet); 250 | playSound("shoot"); 251 | } 252 | 253 | handleInput() { 254 | if (isKeyDown(LEFT_KEY)) { 255 | this.xVel = -175; 256 | } else if (isKeyDown(RIGHT_KEY)) { 257 | this.xVel = 175; 258 | } else this.xVel = 0; 259 | 260 | if (wasKeyPressed(SHOOT_KEY)) { 261 | if (this.bulletDelayAccumulator > 0.5) { 262 | this.shoot(); 263 | this.bulletDelayAccumulator = 0; 264 | } 265 | } 266 | } 267 | 268 | updateBullets(dt: number) { 269 | for (let i = this.bullets.length - 1; i >= 0; i--) { 270 | let bullet = this.bullets[i]; 271 | if (bullet.alive) { 272 | bullet.update(dt); 273 | } else { 274 | this.bullets.splice(i, 1); 275 | bullet = undefined; 276 | } 277 | } 278 | } 279 | 280 | update(dt: number) { 281 | // update time passed between shots 282 | this.bulletDelayAccumulator += dt; 283 | 284 | // apply x vel 285 | this.position.x += this.xVel * dt; 286 | 287 | // cap player position in screen bounds 288 | this.position.x = clamp( 289 | this.position.x, 290 | this.bounds.w / 2, 291 | CANVAS_WIDTH - this.bounds.w / 2, 292 | ); 293 | this.updateBullets(dt); 294 | } 295 | 296 | draw(resized: boolean) { 297 | super.draw(resized); 298 | 299 | // draw bullets 300 | for (let i = 0, len = this.bullets.length; i < len; i++) { 301 | const bullet = this.bullets[i]; 302 | if (bullet.alive) { 303 | bullet.draw(resized); 304 | } 305 | } 306 | } 307 | } 308 | 309 | class Bullet extends BaseSprite { 310 | direction: number; 311 | speed: number; 312 | alive: boolean; 313 | 314 | constructor(x: number, y: number, direction: number, speed: number) { 315 | super(bulletImg, x, y); 316 | this.direction = direction; 317 | this.speed = speed; 318 | this.alive = true; 319 | } 320 | 321 | update(dt: number) { 322 | this.position.y -= this.speed * this.direction * dt; 323 | 324 | if (this.position.y < 0) { 325 | this.alive = false; 326 | } 327 | } 328 | 329 | draw(resized: boolean) { 330 | super.draw(resized); 331 | } 332 | } 333 | 334 | class Enemy extends SheetSprite { 335 | clipRects: ClipRect[]; 336 | onFirstState: boolean; 337 | stepDelay: number; 338 | stepAccumulator: number; 339 | doShoot: boolean; 340 | bullet?: Bullet; 341 | alive: boolean; 342 | 343 | constructor(clipRects: ClipRect[], x: number, y: number) { 344 | super(spriteSheetImg, clipRects[0], x, y); 345 | this.clipRects = clipRects; 346 | this.scale.set(0.5, 0.5); 347 | this.alive = true; 348 | this.onFirstState = true; 349 | this.stepDelay = 1; // try 2 secs to start with... 350 | this.stepAccumulator = 0; 351 | this.doShoot = false; 352 | this.bullet = undefined; 353 | } 354 | 355 | toggleFrame() { 356 | this.onFirstState = !this.onFirstState; 357 | this.clipRect = this.onFirstState ? this.clipRects[0] : this.clipRects[1]; 358 | } 359 | 360 | shoot() { 361 | this.bullet = new Bullet( 362 | this.position.x, 363 | this.position.y + this.bounds.w / 2, 364 | -1, 365 | 500, 366 | ); 367 | } 368 | 369 | update(dt: number) { 370 | this.stepAccumulator += dt; 371 | 372 | if (this.stepAccumulator >= this.stepDelay) { 373 | if (this.position.x < this.bounds.w / 2 + 20 && alienDirection < 0) { 374 | updateAlienLogic = true; 375 | } 376 | if ( 377 | alienDirection === 1 && 378 | this.position.x > CANVAS_WIDTH - this.bounds.w / 2 - 20 379 | ) { 380 | updateAlienLogic = true; 381 | } 382 | if (this.position.y > CANVAS_WIDTH - 50) { 383 | reset(); 384 | } 385 | 386 | const fireTest = Math.floor(Math.random() * (this.stepDelay + 1)); 387 | if (getRandomArbitrary(0, 1000) <= 5 * (this.stepDelay + 1)) { 388 | this.doShoot = true; 389 | } 390 | this.position.x += 10 * alienDirection; 391 | this.toggleFrame(); 392 | this.stepAccumulator = 0; 393 | } 394 | this.position.y += alienYDown; 395 | 396 | if (this.bullet && this.bullet.alive) { 397 | this.bullet.update(dt); 398 | } else { 399 | this.bullet = undefined; 400 | } 401 | } 402 | 403 | draw(resized: boolean) { 404 | super.draw(resized); 405 | if (this.bullet !== undefined && this.bullet.alive) { 406 | this.bullet.draw(resized); 407 | } 408 | } 409 | } 410 | 411 | class ParticleExplosion { 412 | particlePool: any[]; 413 | particles: any[]; 414 | 415 | constructor() { 416 | this.particlePool = []; 417 | this.particles = []; 418 | } 419 | 420 | draw() { 421 | for (let i = this.particles.length - 1; i >= 0; i--) { 422 | const particle = this.particles[i]; 423 | particle.moves++; 424 | particle.x += particle.xunits; 425 | particle.y += particle.yunits + particle.gravity * particle.moves; 426 | particle.life--; 427 | 428 | if (particle.life <= 0) { 429 | if (this.particlePool.length < 100) { 430 | this.particlePool.push(this.particles.splice(i, 1)); 431 | } else { 432 | this.particles.splice(i, 1); 433 | } 434 | } else { 435 | ctx.globalAlpha = particle.life / particle.maxLife; 436 | ctx.fillStyle = particle.color; 437 | ctx.fillRect(particle.x, particle.y, particle.width, particle.height); 438 | ctx.globalAlpha = 1; 439 | } 440 | } 441 | } 442 | 443 | createExplosion( 444 | x: number, 445 | y: number, 446 | color: string, 447 | number: number, 448 | width: number, 449 | height: number, 450 | spd: number, 451 | grav: number, 452 | lif: number, 453 | ) { 454 | for (let i = 0; i < number; i++) { 455 | const angle = Math.floor(Math.random() * 360); 456 | const speed = Math.floor((Math.random() * spd) / 2) + spd; 457 | const life = Math.floor(Math.random() * lif) + lif / 2; 458 | const radians = (angle * Math.PI) / 180; 459 | const xunits = Math.cos(radians) * speed; 460 | const yunits = Math.sin(radians) * speed; 461 | 462 | if (this.particlePool.length > 0) { 463 | const tempParticle = this.particlePool.pop(); 464 | tempParticle.x = x; 465 | tempParticle.y = y; 466 | tempParticle.xunits = xunits; 467 | tempParticle.yunits = yunits; 468 | tempParticle.life = life; 469 | tempParticle.color = color; 470 | tempParticle.width = width; 471 | tempParticle.height = height; 472 | tempParticle.gravity = grav; 473 | tempParticle.moves = 0; 474 | tempParticle.alpha = 1; 475 | tempParticle.maxLife = life; 476 | this.particles.push(tempParticle); 477 | } else { 478 | this.particles.push({ 479 | x: x, 480 | y: y, 481 | xunits: xunits, 482 | yunits: yunits, 483 | life: life, 484 | color: color, 485 | width: width, 486 | height: height, 487 | gravity: grav, 488 | moves: 0, 489 | alpha: 1, 490 | maxLife: life, 491 | }); 492 | } 493 | } 494 | } 495 | } 496 | 497 | // ################################################################### 498 | // Initialization functions 499 | // ################################################################### 500 | function initCanvas() { 501 | if (options.canvas) { 502 | canvas = options.canvas; 503 | } else { 504 | const selector = options.selector || "#invaders"; 505 | const el = document.querySelector(selector) || document.body; 506 | canvas = document.createElement("canvas"); 507 | el.appendChild(canvas); 508 | } 509 | 510 | // Set canvas properties 511 | canvas.width = CANVAS_WIDTH; 512 | canvas.height = CANVAS_HEIGHT; 513 | 514 | // Get Context 515 | ctx = canvas.getContext("2d") as CanvasRenderingContext2D; 516 | 517 | // turn off image smoothing 518 | setImageSmoothing(false); 519 | 520 | // create our main sprite sheet img 521 | spriteSheetImg = new Image(); 522 | spriteSheetImg.src = SPRITE_SHEET_SRC; 523 | preDrawImages(); 524 | 525 | // add event listeners and initially resize 526 | window.addEventListener("resize", resize); 527 | document.addEventListener("keydown", onKeyDown); 528 | document.addEventListener("keyup", onKeyUp); 529 | } 530 | 531 | function preDrawImages() { 532 | const canvas = drawIntoCanvas(2, 8, (ctx) => { 533 | ctx.fillStyle = "white"; 534 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); 535 | }); 536 | bulletImg = new Image(); 537 | bulletImg.src = canvas.toDataURL(); 538 | } 539 | 540 | function setImageSmoothing(value: boolean) { 541 | ctx["imageSmoothingEnabled"] = value; 542 | // @ts-ignore 543 | ctx["mozImageSmoothingEnabled"] = value; 544 | // @ts-ignore 545 | ctx["oImageSmoothingEnabled"] = value; 546 | // @ts-ignore 547 | ctx["webkitImageSmoothingEnabled"] = value; 548 | // @ts-ignore 549 | ctx["msImageSmoothingEnabled"] = value; 550 | } 551 | 552 | function initGame() { 553 | aliens = []; 554 | player = new Player(); 555 | particleManager = new ParticleExplosion(); 556 | setupAlienFormation(); 557 | drawBottomHud(); 558 | } 559 | 560 | function setupAlienFormation() { 561 | alienCount = 0; 562 | for (let i = 0, len = 5 * 11; i < len; i++) { 563 | const gridX = i % 11; 564 | const gridY = Math.floor(i / 11); 565 | let clipRects: ClipRect[] = []; 566 | switch (gridY) { 567 | case 0: 568 | case 1: 569 | clipRects = ALIEN_BOTTOM_ROW; 570 | break; 571 | case 2: 572 | case 3: 573 | clipRects = ALIEN_MIDDLE_ROW; 574 | break; 575 | case 4: 576 | clipRects = ALIEN_TOP_ROW; 577 | break; 578 | } 579 | aliens.push( 580 | new Enemy( 581 | clipRects, 582 | CANVAS_WIDTH / 2 - 583 | ALIEN_SQUAD_WIDTH / 2 + 584 | ALIEN_X_MARGIN / 2 + 585 | gridX * ALIEN_X_MARGIN, 586 | CANVAS_HEIGHT / 3.25 - gridY * 40, 587 | ), 588 | ); 589 | alienCount++; 590 | } 591 | } 592 | 593 | function reset() { 594 | aliens = []; 595 | setupAlienFormation(); 596 | player.reset(); 597 | } 598 | 599 | function init() { 600 | initCanvas(); 601 | keyStates = []; 602 | prevKeyStates = []; 603 | resize(); 604 | } 605 | 606 | // ################################################################### 607 | // Helpful input functions 608 | // ################################################################### 609 | function isKeyDown(key: number) { 610 | return keyStates[key]; 611 | } 612 | 613 | function wasKeyPressed(key: number) { 614 | return !prevKeyStates[key] && keyStates[key]; 615 | } 616 | 617 | // ################################################################### 618 | // Drawing & Update functions 619 | // ################################################################### 620 | function updateAliens(dt: number) { 621 | if (updateAlienLogic) { 622 | updateAlienLogic = false; 623 | alienDirection = -alienDirection; 624 | alienYDown = 25; 625 | } 626 | 627 | for (let i = aliens.length - 1; i >= 0; i--) { 628 | let alien: Enemy | undefined = aliens[i]; 629 | if (!alien.alive) { 630 | aliens.splice(i, 1); 631 | alien = undefined; 632 | alienCount--; 633 | if (alienCount < 1) { 634 | wave++; 635 | setupAlienFormation(); 636 | } 637 | return; 638 | } 639 | 640 | alien.stepDelay = (alienCount * 20 - wave * 10) / 1000; 641 | if (alien.stepDelay <= 0.05) { 642 | alien.stepDelay = 0.05; 643 | } 644 | alien.update(dt); 645 | 646 | if (alien.doShoot) { 647 | alien.doShoot = false; 648 | alien.shoot(); 649 | const rand = String(Math.round(Math.random() * 3 + 1)) as 650 | | "1" 651 | | "2" 652 | | "3"; 653 | playSound(`fastinvader${rand}`); 654 | } 655 | } 656 | alienYDown = 0; 657 | } 658 | 659 | function resolveBulletEnemyCollisions() { 660 | const bullets = player.bullets; 661 | 662 | for (let i = 0, len = bullets.length; i < len; i++) { 663 | const bullet = bullets[i]; 664 | for (let j = 0, alen = aliens.length; j < alen; j++) { 665 | const alien = aliens[j]; 666 | if (checkRectCollision(bullet.bounds, alien.bounds)) { 667 | alien.alive = bullet.alive = false; 668 | playSound("invaderkilled"); 669 | particleManager.createExplosion( 670 | alien.position.x, 671 | alien.position.y, 672 | "white", 673 | 70, 674 | 5, 675 | 5, 676 | 3, 677 | 0.15, 678 | 50, 679 | ); 680 | player.score += 25; 681 | } 682 | } 683 | } 684 | } 685 | 686 | function resolveBulletPlayerCollisions() { 687 | for (let i = 0, len = aliens.length; i < len; i++) { 688 | const alien = aliens[i]; 689 | if ( 690 | alien.bullet && 691 | checkRectCollision(alien.bullet.bounds, player.bounds) 692 | ) { 693 | if (player.lives === 0) { 694 | hasGameStarted = false; 695 | } else { 696 | playSound("explosion"); 697 | alien.bullet.alive = false; 698 | particleManager.createExplosion( 699 | player.position.x, 700 | player.position.y, 701 | "green", 702 | 100, 703 | 8, 704 | 8, 705 | 6, 706 | 0.001, 707 | 40, 708 | ); 709 | player.position.set(CANVAS_WIDTH / 2, CANVAS_HEIGHT - 70); 710 | player.lives--; 711 | break; 712 | } 713 | } 714 | } 715 | } 716 | 717 | function resolveCollisions() { 718 | resolveBulletEnemyCollisions(); 719 | resolveBulletPlayerCollisions(); 720 | } 721 | 722 | function updateGame(dt: number) { 723 | player.handleInput(); 724 | prevKeyStates = keyStates.slice(); 725 | player.update(dt); 726 | updateAliens(dt); 727 | resolveCollisions(); 728 | } 729 | 730 | function drawIntoCanvas( 731 | width: number, 732 | height: number, 733 | drawFunc: (ctx: CanvasRenderingContext2D) => void, 734 | ) { 735 | const canvas = document.createElement("canvas"); 736 | canvas.width = width; 737 | canvas.height = height; 738 | const ctx = canvas.getContext("2d") as CanvasRenderingContext2D; 739 | drawFunc(ctx); 740 | return canvas; 741 | } 742 | 743 | function fillText( 744 | text: string, 745 | x: number, 746 | y: number, 747 | color?: string, 748 | fontSize?: number, 749 | ) { 750 | if (typeof color !== "undefined") ctx.fillStyle = color; 751 | if (typeof fontSize !== "undefined") ctx.font = fontSize + "px Play"; 752 | ctx.fillText(text, x, y); 753 | } 754 | 755 | function fillCenteredText( 756 | text: string, 757 | x: number, 758 | y: number, 759 | color?: string, 760 | fontSize?: number, 761 | ) { 762 | const metrics = ctx.measureText(text); 763 | fillText(text, x - metrics.width / 2, y, color, fontSize); 764 | } 765 | 766 | function fillBlinkingText( 767 | text: string, 768 | x: number, 769 | y: number, 770 | blinkFreq: number, 771 | color?: string, 772 | fontSize?: number, 773 | ) { 774 | if (~~(0.5 + Date.now() / blinkFreq) % 2) { 775 | fillCenteredText(text, x, y, color, fontSize); 776 | } 777 | } 778 | 779 | function drawBottomHud() { 780 | ctx.fillStyle = "#02ff12"; 781 | ctx.fillRect(0, CANVAS_HEIGHT - 30, CANVAS_WIDTH, 2); 782 | fillText(player.lives + " x ", 10, CANVAS_HEIGHT - 7.5, "white", 20); 783 | ctx.drawImage( 784 | spriteSheetImg, 785 | player.clipRect.x, 786 | player.clipRect.y, 787 | player.clipRect.w, 788 | player.clipRect.h, 789 | 45, 790 | CANVAS_HEIGHT - 23, 791 | player.clipRect.w * 0.5, 792 | player.clipRect.h * 0.5, 793 | ); 794 | fillText("CREDIT: ", CANVAS_WIDTH - 115, CANVAS_HEIGHT - 7.5); 795 | fillCenteredText("SCORE: " + player.score, CANVAS_WIDTH / 2, 20); 796 | fillBlinkingText( 797 | "00", 798 | CANVAS_WIDTH - 25, 799 | CANVAS_HEIGHT - 7.5, 800 | TEXT_BLINK_FREQ, 801 | ); 802 | } 803 | 804 | function drawAliens(resized: boolean) { 805 | for (let i = 0; i < aliens.length; i++) { 806 | const alien = aliens[i]; 807 | alien.draw(resized); 808 | } 809 | } 810 | 811 | function drawGame(resized: boolean) { 812 | player.draw(resized); 813 | drawAliens(resized); 814 | particleManager.draw(); 815 | drawBottomHud(); 816 | } 817 | 818 | function drawStartScreen() { 819 | fillCenteredText( 820 | options.title || "Space Invaders", 821 | CANVAS_WIDTH / 2, 822 | CANVAS_HEIGHT / 2.75, 823 | "#FFFFFF", 824 | 36, 825 | ); 826 | fillBlinkingText( 827 | "Press enter to play!", 828 | CANVAS_WIDTH / 2, 829 | CANVAS_HEIGHT / 2, 830 | 500, 831 | "#FFFFFF", 832 | 36, 833 | ); 834 | } 835 | 836 | function animate() { 837 | const now = window.performance.now(); 838 | let dt = now - lastTime; 839 | if (dt > 100) dt = 100; 840 | if (wasKeyPressed(13) && !hasGameStarted) { 841 | initGame(); 842 | hasGameStarted = true; 843 | } 844 | 845 | if (hasGameStarted) { 846 | updateGame(dt / 1000); 847 | } 848 | 849 | ctx.fillStyle = "black"; 850 | ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 851 | if (hasGameStarted) { 852 | drawGame(false); 853 | } else { 854 | drawStartScreen(); 855 | } 856 | lastTime = now; 857 | requestAnimationFrame(animate); 858 | } 859 | 860 | // ################################################################### 861 | // Event Listener functions 862 | // ################################################################### 863 | function resize() { 864 | const w = window.innerWidth; 865 | const h = window.innerHeight; 866 | 867 | // calculate the scale factor to keep a correct aspect ratio 868 | const scaleFactor = Math.min(w / CANVAS_WIDTH, h / CANVAS_HEIGHT); 869 | 870 | if (IS_CHROME) { 871 | canvas.width = CANVAS_WIDTH * scaleFactor; 872 | canvas.height = CANVAS_HEIGHT * scaleFactor; 873 | setImageSmoothing(false); 874 | ctx.transform(scaleFactor, 0, 0, scaleFactor, 0, 0); 875 | } else { 876 | // resize the canvas css properties 877 | canvas.style.width = CANVAS_WIDTH * scaleFactor + "px"; 878 | canvas.style.height = CANVAS_HEIGHT * scaleFactor + "px"; 879 | } 880 | } 881 | 882 | function onKeyDown(e: KeyboardEvent) { 883 | e.preventDefault(); 884 | keyStates[e.keyCode] = true; 885 | } 886 | 887 | function onKeyUp(e: KeyboardEvent) { 888 | e.preventDefault(); 889 | keyStates[e.keyCode] = false; 890 | } 891 | 892 | // ################################################################### 893 | // Touch Support 894 | // ################################################################### 895 | type TouchPos = { x: number; y: number }; 896 | let touchStart: TouchPos; 897 | 898 | document.addEventListener("touchstart", (e) => { 899 | touchStart = { 900 | x: e.touches[0].clientX, 901 | y: e.touches[0].clientY, 902 | }; 903 | if (hasGameStarted) { 904 | player.shoot(); 905 | } else { 906 | initGame(); 907 | hasGameStarted = true; 908 | } 909 | }); 910 | 911 | document.addEventListener("touchmove", (e) => { 912 | const touchCurrent = { 913 | x: e.touches[0].clientX, 914 | y: e.touches[0].clientY, 915 | }; 916 | const deltaX = touchCurrent.x - touchStart.x; 917 | if (deltaX > 0) { 918 | keyStates[RIGHT_KEY] = true; 919 | keyStates[LEFT_KEY] = false; 920 | } else if (deltaX < 0) { 921 | keyStates[LEFT_KEY] = true; 922 | keyStates[RIGHT_KEY] = false; 923 | } 924 | }); 925 | 926 | document.addEventListener("touchend", (e) => { 927 | keyStates[LEFT_KEY] = false; 928 | keyStates[RIGHT_KEY] = false; 929 | }); 930 | 931 | // ################################################################### 932 | // Start game! 933 | // ################################################################### 934 | const styleEl = document.createElement("link"); 935 | styleEl.rel = "stylesheet"; 936 | styleEl.href = "https://fonts.googleapis.com/css?family=Play:400,700"; 937 | 938 | document.head.appendChild(styleEl); 939 | 940 | init(); 941 | animate(); 942 | 943 | if (options.autoPlay) { 944 | initGame(); 945 | hasGameStarted = true; 946 | } 947 | 948 | // ################################################################### 949 | // Sounds 950 | // ################################################################### 951 | // const soundSprite = { 952 | // shoot: [0, 350], 953 | // explosion: [400, 775], 954 | // invaderkilled: [1150, 350], 955 | // fastinvader1: [1550, 100], 956 | // fastinvader2: [1650, 100], 957 | // fastinvader3: [1750, 100], 958 | // } as const; 959 | 960 | // const audio = new Audio(getSoundData()); 961 | // let audioStopTime: number | undefined; 962 | // let audioStopTimeout: ReturnType | undefined; 963 | // audio.addEventListener("timeupdate", () => { 964 | // if (audioStopTime && audio.currentTime >= audioStopTime) { 965 | // audio.pause(); 966 | // } 967 | // }); 968 | 969 | async function playSound(name: string /* keyof typeof soundSprite */) { 970 | // if (audioStopTimeout) { 971 | // clearTimeout(audioStopTimeout); 972 | // } 973 | // audio.pause(); 974 | // const [start, len] = soundSprite[name]; 975 | // audio.currentTime = start / 1000; 976 | // audioStopTime = (start + len) / 1000; 977 | // audio.play(); 978 | // audioStopTimeout = setTimeout(() => { 979 | // audio.pause(); 980 | // }, len); 981 | } 982 | } 983 | 984 | // function getSoundData() { 985 | // return "data:audio/ogg;base64,T2dnUwACAAAAAAAAAAA79qUjAAAAAF92vQgBHgF2b3JiaXMAAAAAARErAAAAAAAAuIgAAAAAAACZAU9nZ1MAAAAAAAAAAAAAO/alIwEAAABJpEEXCy3///////////+1A3ZvcmJpcx0AAABYaXBoLk9yZyBsaWJWb3JiaXMgSSAyMDA3MDYyMgAAAAABBXZvcmJpcxJCQ1YBAAABAAxSFCElGVNKYwiVUlIpBR1jUFtHHWPUOUYhZBBTiEkZpXtPKpVYSsgRUlgpRR1TTFNJlVKWKUUdYxRTSCFT1jFloXMUS4ZJCSVsTa50FkvomWOWMUYdY85aSp1j1jFFHWNSUkmhcxg6ZiVkFDpGxehifDA6laJCKL7H3lLpLYWKW4q91xpT6y2EGEtpwQhhc+211dxKasUYY4wxxsXiUyiC0JBVAAABAABABAFCQ1YBAAoAAMJQDEVRgNCQVQBABgCAABRFcRTHcRxHkiTLAkJDVgEAQAAAAgAAKI7hKJIjSZJkWZZlWZameZaouaov+64u667t6roOhIasBADIAAAYhiGH3knMkFOQSSYpVcw5CKH1DjnlFGTSUsaYYoxRzpBTDDEFMYbQKYUQ1E45pQwiCENInWTOIEs96OBi5zgQGrIiAIgCAACMQYwhxpBzDEoGIXKOScggRM45KZ2UTEoorbSWSQktldYi55yUTkompbQWUsuklNZCKwUAAAQ4AAAEWAiFhqwIAKIAABCDkFJIKcSUYk4xh5RSjinHkFLMOcWYcowx6CBUzDHIHIRIKcUYc0455iBkDCrmHIQMMgEAAAEOAAABFkKhISsCgDgBAIMkaZqlaaJoaZooeqaoqqIoqqrleabpmaaqeqKpqqaquq6pqq5seZ5peqaoqp4pqqqpqq5rqqrriqpqy6ar2rbpqrbsyrJuu7Ks256qyrapurJuqq5tu7Js664s27rkearqmabreqbpuqrr2rLqurLtmabriqor26bryrLryratyrKua6bpuqKr2q6purLtyq5tu7Ks+6br6rbqyrquyrLu27au+7KtC7vourauyq6uq7Ks67It67Zs20LJ81TVM03X9UzTdVXXtW3VdW1bM03XNV1XlkXVdWXVlXVddWVb90zTdU1XlWXTVWVZlWXddmVXl0XXtW1Vln1ddWVfl23d92VZ133TdXVblWXbV2VZ92Vd94VZt33dU1VbN11X103X1X1b131htm3fF11X11XZ1oVVlnXf1n1lmHWdMLqurqu27OuqLOu+ruvGMOu6MKy6bfyurQvDq+vGseu+rty+j2rbvvDqtjG8um4cu7Abv+37xrGpqm2brqvrpivrumzrvm/runGMrqvrqiz7uurKvm/ruvDrvi8Mo+vquirLurDasq/Lui4Mu64bw2rbwu7aunDMsi4Mt+8rx68LQ9W2heHVdaOr28ZvC8PSN3a+AACAAQcAgAATykChISsCgDgBAAYhCBVjECrGIIQQUgohpFQxBiFjDkrGHJQQSkkhlNIqxiBkjknIHJMQSmiplNBKKKWlUEpLoZTWUmotptRaDKG0FEpprZTSWmopttRSbBVjEDLnpGSOSSiltFZKaSlzTErGoKQOQiqlpNJKSa1lzknJoKPSOUippNJSSam1UEproZTWSkqxpdJKba3FGkppLaTSWkmptdRSba21WiPGIGSMQcmck1JKSamU0lrmnJQOOiqZg5JKKamVklKsmJPSQSglg4xKSaW1kkoroZTWSkqxhVJaa63VmFJLNZSSWkmpxVBKa621GlMrNYVQUgultBZKaa21VmtqLbZQQmuhpBZLKjG1FmNtrcUYSmmtpBJbKanFFluNrbVYU0s1lpJibK3V2EotOdZaa0ot1tJSjK21mFtMucVYaw0ltBZKaa2U0lpKrcXWWq2hlNZKKrGVklpsrdXYWow1lNJiKSm1kEpsrbVYW2w1ppZibLHVWFKLMcZYc0u11ZRai621WEsrNcYYa2415VIAAMCAAwBAgAlloNCQlQBAFAAAYAxjjEFoFHLMOSmNUs45JyVzDkIIKWXOQQghpc45CKW01DkHoZSUQikppRRbKCWl1losAACgwAEAIMAGTYnFAQoNWQkARAEAIMYoxRiExiClGIPQGKMUYxAqpRhzDkKlFGPOQcgYc85BKRljzkEnJYQQQimlhBBCKKWUAgAAChwAAAJs0JRYHKDQkBUBQBQAAGAMYgwxhiB0UjopEYRMSielkRJaCylllkqKJcbMWomtxNhICa2F1jJrJcbSYkatxFhiKgAA7MABAOzAQig0ZCUAkAcAQBijFGPOOWcQYsw5CCE0CDHmHIQQKsaccw5CCBVjzjkHIYTOOecghBBC55xzEEIIoYMQQgillNJBCCGEUkrpIIQQQimldBBCCKGUUgoAACpwAAAIsFFkc4KRoEJDVgIAeQAAgDFKOSclpUYpxiCkFFujFGMQUmqtYgxCSq3FWDEGIaXWYuwgpNRajLV2EFJqLcZaQ0qtxVhrziGl1mKsNdfUWoy15tx7ai3GWnPOuQAA3AUHALADG0U2JxgJKjRkJQCQBwBAIKQUY4w5h5RijDHnnENKMcaYc84pxhhzzjnnFGOMOeecc4wx55xzzjnGmHPOOeecc84556CDkDnnnHPQQeicc845CCF0zjnnHIQQCgAAKnAAAAiwUWRzgpGgQkNWAgDhAACAMZRSSimllFJKqKOUUkoppZRSAiGllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimVUkoppZRSSimllFJKKaUAIN8KBwD/BxtnWEk6KxwNLjRkJQAQDgAAGMMYhIw5JyWlhjEIpXROSkklNYxBKKVzElJKKYPQWmqlpNJSShmElGILIZWUWgqltFZrKam1lFIoKcUaS0qppdYy5ySkklpLrbaYOQelpNZaaq3FEEJKsbXWUmuxdVJSSa211lptLaSUWmstxtZibCWlllprqcXWWkyptRZbSy3G1mJLrcXYYosxxhoLAOBucACASLBxhpWks8LR4EJDVgIAIQEABDJKOeecgxBCCCFSijHnoIMQQgghREox5pyDEEIIIYSMMecghBBCCKGUkDHmHIQQQgghhFI65yCEUEoJpZRSSucchBBCCKWUUkoJIYQQQiillFJKKSGEEEoppZRSSiklhBBCKKWUUkoppYQQQiillFJKKaWUEEIopZRSSimllBJCCKGUUkoppZRSQgillFJKKaWUUkooIYRSSimllFJKCSWUUkoppZRSSikhlFJKKaWUUkoppQAAgAMHAIAAI+gko8oibDThwgMQAAAAAgACTACBAYKCUQgChBEIAAAAAAAIAPgAAEgKgIiIaOYMDhASFBYYGhweICIkAAAAAAAAAAAAAAAABE9nZ1MAAAAmAAAAAAAAO/alIwIAAACNn16AJ2p4cnF0fXRzd3RsaXNscW1wZWhmY2pqamhoZWlmZWVoZGNmZ2dkatqsV9/ryZFxWDrAwiAkXGj8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"; 986 | // } 987 | 988 | --------------------------------------------------------------------------------