├── .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 |
--------------------------------------------------------------------------------