@guinetik/gcanvas 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/.github/workflows/release.yaml +70 -0
  2. package/.jshintrc +4 -0
  3. package/.vscode/settings.json +22 -0
  4. package/CLAUDE.md +310 -0
  5. package/blackhole.jpg +0 -0
  6. package/demo.png +0 -0
  7. package/demos/CNAME +1 -0
  8. package/demos/animations.html +31 -0
  9. package/demos/basic.html +38 -0
  10. package/demos/baskara.html +31 -0
  11. package/demos/bezier.html +35 -0
  12. package/demos/beziersignature.html +29 -0
  13. package/demos/blackhole.html +28 -0
  14. package/demos/blob.html +35 -0
  15. package/demos/demos.css +289 -0
  16. package/demos/easing.html +28 -0
  17. package/demos/events.html +195 -0
  18. package/demos/fluent.html +647 -0
  19. package/demos/fractals.html +36 -0
  20. package/demos/genart.html +26 -0
  21. package/demos/gendream.html +26 -0
  22. package/demos/group.html +36 -0
  23. package/demos/home.html +587 -0
  24. package/demos/index.html +364 -0
  25. package/demos/isometric.html +34 -0
  26. package/demos/js/animations.js +452 -0
  27. package/demos/js/basic.js +204 -0
  28. package/demos/js/baskara.js +751 -0
  29. package/demos/js/bezier.js +692 -0
  30. package/demos/js/beziersignature.js +241 -0
  31. package/demos/js/blackhole/accretiondisk.obj.js +379 -0
  32. package/demos/js/blackhole/blackhole.obj.js +318 -0
  33. package/demos/js/blackhole/index.js +409 -0
  34. package/demos/js/blackhole/particle.js +56 -0
  35. package/demos/js/blackhole/starfield.obj.js +218 -0
  36. package/demos/js/blob.js +2263 -0
  37. package/demos/js/easing.js +477 -0
  38. package/demos/js/fluent.js +183 -0
  39. package/demos/js/fractals.js +931 -0
  40. package/demos/js/fractalworker.js +93 -0
  41. package/demos/js/genart.js +268 -0
  42. package/demos/js/gendream.js +209 -0
  43. package/demos/js/group.js +140 -0
  44. package/demos/js/info-toggle.js +25 -0
  45. package/demos/js/isometric.js +863 -0
  46. package/demos/js/kerr.js +1556 -0
  47. package/demos/js/lavalamp.js +590 -0
  48. package/demos/js/layout.js +354 -0
  49. package/demos/js/mondrian.js +285 -0
  50. package/demos/js/opacity.js +275 -0
  51. package/demos/js/painter.js +484 -0
  52. package/demos/js/particles-showcase.js +514 -0
  53. package/demos/js/particles.js +299 -0
  54. package/demos/js/patterns.js +397 -0
  55. package/demos/js/penrose/artifact.js +69 -0
  56. package/demos/js/penrose/blackhole.js +121 -0
  57. package/demos/js/penrose/constants.js +73 -0
  58. package/demos/js/penrose/game.js +943 -0
  59. package/demos/js/penrose/lore.js +278 -0
  60. package/demos/js/penrose/penrosescene.js +892 -0
  61. package/demos/js/penrose/ship.js +216 -0
  62. package/demos/js/penrose/sounds.js +211 -0
  63. package/demos/js/penrose/voidparticle.js +55 -0
  64. package/demos/js/penrose/voidscene.js +258 -0
  65. package/demos/js/penrose/voidship.js +144 -0
  66. package/demos/js/penrose/wormhole.js +46 -0
  67. package/demos/js/pipeline.js +555 -0
  68. package/demos/js/scene.js +304 -0
  69. package/demos/js/scenes.js +320 -0
  70. package/demos/js/schrodinger.js +410 -0
  71. package/demos/js/schwarzschild.js +1023 -0
  72. package/demos/js/shapes.js +628 -0
  73. package/demos/js/space/alien.js +171 -0
  74. package/demos/js/space/boom.js +98 -0
  75. package/demos/js/space/boss.js +353 -0
  76. package/demos/js/space/buff.js +73 -0
  77. package/demos/js/space/bullet.js +102 -0
  78. package/demos/js/space/constants.js +85 -0
  79. package/demos/js/space/game.js +1884 -0
  80. package/demos/js/space/hud.js +112 -0
  81. package/demos/js/space/laserbeam.js +179 -0
  82. package/demos/js/space/lightning.js +277 -0
  83. package/demos/js/space/minion.js +192 -0
  84. package/demos/js/space/missile.js +212 -0
  85. package/demos/js/space/player.js +430 -0
  86. package/demos/js/space/powerup.js +90 -0
  87. package/demos/js/space/starfield.js +58 -0
  88. package/demos/js/space/starpower.js +90 -0
  89. package/demos/js/spacetime.js +559 -0
  90. package/demos/js/svgtween.js +204 -0
  91. package/demos/js/tde/accretiondisk.js +418 -0
  92. package/demos/js/tde/blackhole.js +219 -0
  93. package/demos/js/tde/blackholescene.js +209 -0
  94. package/demos/js/tde/config.js +59 -0
  95. package/demos/js/tde/index.js +695 -0
  96. package/demos/js/tde/jets.js +290 -0
  97. package/demos/js/tde/lensedstarfield.js +147 -0
  98. package/demos/js/tde/tdestar.js +317 -0
  99. package/demos/js/tde/tidalstream.js +356 -0
  100. package/demos/js/tde_old/blackhole.obj.js +354 -0
  101. package/demos/js/tde_old/debris.obj.js +791 -0
  102. package/demos/js/tde_old/flare.obj.js +239 -0
  103. package/demos/js/tde_old/index.js +448 -0
  104. package/demos/js/tde_old/star.obj.js +812 -0
  105. package/demos/js/tiles.js +312 -0
  106. package/demos/js/tweendemo.js +79 -0
  107. package/demos/js/visibility.js +102 -0
  108. package/demos/kerr.html +28 -0
  109. package/demos/lavalamp.html +27 -0
  110. package/demos/layouts.html +37 -0
  111. package/demos/logo.svg +4 -0
  112. package/demos/loop.html +84 -0
  113. package/demos/mondrian.html +32 -0
  114. package/demos/og_image.png +0 -0
  115. package/demos/opacity.html +36 -0
  116. package/demos/painter.html +39 -0
  117. package/demos/particles-showcase.html +28 -0
  118. package/demos/particles.html +24 -0
  119. package/demos/patterns.html +33 -0
  120. package/demos/penrose-game.html +31 -0
  121. package/demos/pipeline.html +737 -0
  122. package/demos/scene.html +33 -0
  123. package/demos/scenes.html +96 -0
  124. package/demos/schrodinger.html +27 -0
  125. package/demos/schwarzschild.html +27 -0
  126. package/demos/shapes.html +16 -0
  127. package/demos/space.html +85 -0
  128. package/demos/spacetime.html +27 -0
  129. package/demos/svgtween.html +29 -0
  130. package/demos/tde.html +28 -0
  131. package/demos/tiles.html +28 -0
  132. package/demos/transforms.html +400 -0
  133. package/demos/tween.html +45 -0
  134. package/demos/visibility.html +33 -0
  135. package/disk_example.png +0 -0
  136. package/docs/README.md +222 -0
  137. package/docs/concepts/architecture-overview.md +204 -0
  138. package/docs/concepts/lifecycle.md +255 -0
  139. package/docs/concepts/rendering-pipeline.md +279 -0
  140. package/docs/concepts/tde-zorder.md +106 -0
  141. package/docs/concepts/two-layer-architecture.md +229 -0
  142. package/docs/getting-started/first-game.md +354 -0
  143. package/docs/getting-started/hello-world.md +269 -0
  144. package/docs/getting-started/installation.md +157 -0
  145. package/docs/modules/collision/README.md +453 -0
  146. package/docs/modules/fluent/README.md +1075 -0
  147. package/docs/modules/game/README.md +303 -0
  148. package/docs/modules/isometric-camera.md +210 -0
  149. package/docs/modules/isometric.md +275 -0
  150. package/docs/modules/painter/README.md +328 -0
  151. package/docs/modules/particle/README.md +559 -0
  152. package/docs/modules/shapes/README.md +221 -0
  153. package/docs/modules/shapes/base/euclidian.md +123 -0
  154. package/docs/modules/shapes/base/geometry2d.md +204 -0
  155. package/docs/modules/shapes/base/renderable.md +215 -0
  156. package/docs/modules/shapes/base/shape.md +262 -0
  157. package/docs/modules/shapes/base/transformable.md +243 -0
  158. package/docs/modules/shapes/hierarchy.md +218 -0
  159. package/docs/modules/state/README.md +577 -0
  160. package/docs/modules/util/README.md +99 -0
  161. package/docs/modules/util/camera3d.md +412 -0
  162. package/docs/modules/util/scene3d.md +395 -0
  163. package/index.html +17 -0
  164. package/jsdoc.json +50 -0
  165. package/package.json +55 -0
  166. package/readme.md +599 -0
  167. package/scripts/build-demo.js +69 -0
  168. package/scripts/bundle4llm.js +276 -0
  169. package/scripts/clearconsole.js +48 -0
  170. package/src/collision/collision-system.js +332 -0
  171. package/src/collision/collision.js +303 -0
  172. package/src/collision/index.js +10 -0
  173. package/src/fluent/fluent-game.js +430 -0
  174. package/src/fluent/fluent-go.js +1060 -0
  175. package/src/fluent/fluent-layer.js +152 -0
  176. package/src/fluent/fluent-scene.js +291 -0
  177. package/src/fluent/index.js +98 -0
  178. package/src/fluent/sketch.js +380 -0
  179. package/src/game/game.js +467 -0
  180. package/src/game/index.js +49 -0
  181. package/src/game/objects/go.js +220 -0
  182. package/src/game/objects/imagego.js +30 -0
  183. package/src/game/objects/index.js +54 -0
  184. package/src/game/objects/isometric-scene.js +260 -0
  185. package/src/game/objects/layoutscene.js +549 -0
  186. package/src/game/objects/scene.js +175 -0
  187. package/src/game/objects/scene3d.js +118 -0
  188. package/src/game/objects/text.js +221 -0
  189. package/src/game/objects/wrapper.js +232 -0
  190. package/src/game/pipeline.js +243 -0
  191. package/src/game/ui/button.js +396 -0
  192. package/src/game/ui/cursor.js +93 -0
  193. package/src/game/ui/fps.js +91 -0
  194. package/src/game/ui/index.js +5 -0
  195. package/src/game/ui/togglebutton.js +93 -0
  196. package/src/game/ui/tooltip.js +249 -0
  197. package/src/index.js +25 -0
  198. package/src/io/events.js +20 -0
  199. package/src/io/index.js +86 -0
  200. package/src/io/input.js +70 -0
  201. package/src/io/keys.js +152 -0
  202. package/src/io/mouse.js +61 -0
  203. package/src/io/touch.js +39 -0
  204. package/src/logger/debugtab.js +138 -0
  205. package/src/logger/index.js +3 -0
  206. package/src/logger/loggable.js +47 -0
  207. package/src/logger/logger.js +113 -0
  208. package/src/math/complex.js +37 -0
  209. package/src/math/constants.js +1 -0
  210. package/src/math/fractal.js +1271 -0
  211. package/src/math/gr.js +201 -0
  212. package/src/math/heat.js +202 -0
  213. package/src/math/index.js +12 -0
  214. package/src/math/noise.js +433 -0
  215. package/src/math/orbital.js +191 -0
  216. package/src/math/patterns.js +1339 -0
  217. package/src/math/penrose.js +259 -0
  218. package/src/math/quantum.js +115 -0
  219. package/src/math/random.js +195 -0
  220. package/src/math/tensor.js +1009 -0
  221. package/src/mixins/anchor.js +131 -0
  222. package/src/mixins/draggable.js +72 -0
  223. package/src/mixins/index.js +2 -0
  224. package/src/motion/bezier.js +132 -0
  225. package/src/motion/bounce.js +58 -0
  226. package/src/motion/easing.js +349 -0
  227. package/src/motion/float.js +130 -0
  228. package/src/motion/follow.js +125 -0
  229. package/src/motion/hop.js +52 -0
  230. package/src/motion/index.js +82 -0
  231. package/src/motion/motion.js +1124 -0
  232. package/src/motion/orbit.js +49 -0
  233. package/src/motion/oscillate.js +39 -0
  234. package/src/motion/parabolic.js +141 -0
  235. package/src/motion/patrol.js +147 -0
  236. package/src/motion/pendulum.js +48 -0
  237. package/src/motion/pulse.js +88 -0
  238. package/src/motion/shake.js +83 -0
  239. package/src/motion/spiral.js +144 -0
  240. package/src/motion/spring.js +150 -0
  241. package/src/motion/swing.js +47 -0
  242. package/src/motion/tween.js +92 -0
  243. package/src/motion/tweenetik.js +139 -0
  244. package/src/motion/waypoint.js +210 -0
  245. package/src/painter/index.js +8 -0
  246. package/src/painter/painter.colors.js +331 -0
  247. package/src/painter/painter.effects.js +230 -0
  248. package/src/painter/painter.img.js +229 -0
  249. package/src/painter/painter.js +295 -0
  250. package/src/painter/painter.lines.js +189 -0
  251. package/src/painter/painter.opacity.js +41 -0
  252. package/src/painter/painter.shapes.js +277 -0
  253. package/src/painter/painter.text.js +273 -0
  254. package/src/particle/emitter.js +124 -0
  255. package/src/particle/index.js +11 -0
  256. package/src/particle/particle-system.js +322 -0
  257. package/src/particle/particle.js +71 -0
  258. package/src/particle/updaters.js +170 -0
  259. package/src/shapes/arc.js +43 -0
  260. package/src/shapes/arrow.js +33 -0
  261. package/src/shapes/bezier.js +42 -0
  262. package/src/shapes/circle.js +62 -0
  263. package/src/shapes/clouds.js +56 -0
  264. package/src/shapes/cone.js +219 -0
  265. package/src/shapes/cross.js +70 -0
  266. package/src/shapes/cube.js +244 -0
  267. package/src/shapes/cylinder.js +254 -0
  268. package/src/shapes/diamond.js +48 -0
  269. package/src/shapes/euclidian.js +111 -0
  270. package/src/shapes/figure.js +115 -0
  271. package/src/shapes/geometry.js +220 -0
  272. package/src/shapes/group.js +375 -0
  273. package/src/shapes/heart.js +42 -0
  274. package/src/shapes/hexagon.js +26 -0
  275. package/src/shapes/image.js +192 -0
  276. package/src/shapes/index.js +111 -0
  277. package/src/shapes/line.js +29 -0
  278. package/src/shapes/pattern.js +90 -0
  279. package/src/shapes/pin.js +44 -0
  280. package/src/shapes/poly.js +31 -0
  281. package/src/shapes/prism.js +226 -0
  282. package/src/shapes/rect.js +35 -0
  283. package/src/shapes/renderable.js +333 -0
  284. package/src/shapes/ring.js +26 -0
  285. package/src/shapes/roundrect.js +95 -0
  286. package/src/shapes/shape.js +117 -0
  287. package/src/shapes/slice.js +26 -0
  288. package/src/shapes/sphere.js +314 -0
  289. package/src/shapes/sphere3d.js +537 -0
  290. package/src/shapes/square.js +15 -0
  291. package/src/shapes/star.js +99 -0
  292. package/src/shapes/svg.js +408 -0
  293. package/src/shapes/text.js +553 -0
  294. package/src/shapes/traceable.js +83 -0
  295. package/src/shapes/transform.js +357 -0
  296. package/src/shapes/transformable.js +172 -0
  297. package/src/shapes/triangle.js +26 -0
  298. package/src/sound/index.js +17 -0
  299. package/src/sound/sound.js +473 -0
  300. package/src/sound/synth.analyzer.js +149 -0
  301. package/src/sound/synth.effects.js +207 -0
  302. package/src/sound/synth.envelope.js +59 -0
  303. package/src/sound/synth.js +229 -0
  304. package/src/sound/synth.musical.js +160 -0
  305. package/src/sound/synth.noise.js +85 -0
  306. package/src/sound/synth.oscillators.js +293 -0
  307. package/src/state/index.js +10 -0
  308. package/src/state/state-machine.js +371 -0
  309. package/src/util/camera3d.js +438 -0
  310. package/src/util/index.js +6 -0
  311. package/src/util/isometric-camera.js +235 -0
  312. package/src/util/layout.js +317 -0
  313. package/src/util/position.js +147 -0
  314. package/src/util/tasks.js +47 -0
  315. package/src/util/zindex.js +287 -0
  316. package/src/webgl/index.js +9 -0
  317. package/src/webgl/shaders/sphere-shaders.js +994 -0
  318. package/src/webgl/webgl-renderer.js +388 -0
  319. package/tde.png +0 -0
  320. package/test/math/orbital.test.js +61 -0
  321. package/test/math/tensor.test.js +114 -0
  322. package/test/particle/emitter.test.js +204 -0
  323. package/test/particle/particle-system.test.js +310 -0
  324. package/test/particle/particle.test.js +116 -0
  325. package/test/particle/updaters.test.js +386 -0
  326. package/test/setup.js +120 -0
  327. package/test/shapes/euclidian.test.js +44 -0
  328. package/test/shapes/geometry.test.js +86 -0
  329. package/test/shapes/group.test.js +86 -0
  330. package/test/shapes/rectangle.test.js +64 -0
  331. package/test/shapes/transform.test.js +379 -0
  332. package/test/util/camera3d.test.js +428 -0
  333. package/test/util/scene3d.test.js +352 -0
  334. package/types/collision.d.ts +249 -0
  335. package/types/common.d.ts +155 -0
  336. package/types/game.d.ts +497 -0
  337. package/types/index.d.ts +309 -0
  338. package/types/io.d.ts +188 -0
  339. package/types/logger.d.ts +127 -0
  340. package/types/math.d.ts +268 -0
  341. package/types/mixins.d.ts +92 -0
  342. package/types/motion.d.ts +678 -0
  343. package/types/painter.d.ts +378 -0
  344. package/types/shapes.d.ts +864 -0
  345. package/types/sound.d.ts +672 -0
  346. package/types/state.d.ts +251 -0
  347. package/types/util.d.ts +253 -0
  348. package/vite.config.js +50 -0
  349. package/vitest.config.js +13 -0
@@ -0,0 +1,1884 @@
1
+ import {
2
+ Game,
3
+ TextShape,
4
+ Keys,
5
+ FPSCounter,
6
+ Synth,
7
+ Sound,
8
+ Button,
9
+ ToggleButton,
10
+ } from "../../../src/index.js";
11
+
12
+ // Import constants
13
+ import {
14
+ PLAYER_WIDTH,
15
+ PLAYER_HEIGHT,
16
+ BULLET_SPEED,
17
+ ALIEN_BASE_ROWS,
18
+ ALIEN_COLS,
19
+ MAX_ALIEN_ROWS,
20
+ ALIEN_WIDTH,
21
+ ALIEN_HEIGHT,
22
+ ALIEN_SPACING_X,
23
+ ALIEN_SPACING_Y,
24
+ ALIEN_MOVE_SPEED,
25
+ ALIEN_DROP_DISTANCE,
26
+ ALIEN_SHOOT_CHANCE,
27
+ ALIEN_BULLET_SPEED,
28
+ POWERUP_SPAWN_CHANCE,
29
+ STARPOWER_SPAWN_CHANCE,
30
+ POWERUP_SIZE,
31
+ MISSILE_SPAWN_CHANCE,
32
+ MISSILE_HEIGHT,
33
+ BOSS_LEVELS,
34
+ LASER_SPAWN_CHANCE,
35
+ LIGHTNING_SPAWN_CHANCE,
36
+ } from "./constants.js";
37
+
38
+ // Import game components
39
+ import { Player } from "./player.js";
40
+ import { Alien } from "./alien.js";
41
+ import { Bullet } from "./bullet.js";
42
+ import { Explosion } from "./boom.js";
43
+ import { AbsorbEffect } from "./buff.js";
44
+ import { PowerUp } from "./powerup.js";
45
+ import { StarPowerUp } from "./starpower.js";
46
+ import { Missile } from "./missile.js";
47
+ import { HUD } from "./hud.js";
48
+ import { Starfield } from "./starfield.js";
49
+ import { Boss } from "./boss.js";
50
+ import { BossMinion } from "./minion.js";
51
+ import { LaserBeam } from "./laserbeam.js";
52
+ import { Lightning } from "./lightning.js";
53
+
54
+ export class SpaceGame extends Game {
55
+ constructor(canvas) {
56
+ super(canvas);
57
+ // Enable fluid sizing for fullscreen display
58
+ this.enableFluidSize();
59
+ this.backgroundColor = "#000011";
60
+
61
+ // Handle window resize - reset game to ready state
62
+ this._resizeHandler = () => this.handleResize();
63
+ window.addEventListener("resize", this._resizeHandler);
64
+ }
65
+
66
+ handleResize() {
67
+ // Only reset if game has been initialized
68
+ if (!this._spaceGameInitialized) return;
69
+
70
+ // Clear all game objects and reset to ready state
71
+ this.resetToReady();
72
+ }
73
+
74
+ stop() {
75
+ // Clean up resize listener
76
+ if (this._resizeHandler) {
77
+ window.removeEventListener("resize", this._resizeHandler);
78
+ this._resizeHandler = null;
79
+ }
80
+ super.stop();
81
+ }
82
+
83
+ resetToReady() {
84
+ // Clear all collections
85
+ for (const bullet of this.bullets) {
86
+ this.pipeline.remove(bullet);
87
+ }
88
+ this.bullets = [];
89
+
90
+ for (const alien of this.aliens) {
91
+ this.pipeline.remove(alien);
92
+ }
93
+ this.aliens = [];
94
+
95
+ for (const explosion of this.explosions) {
96
+ this.pipeline.remove(explosion);
97
+ }
98
+ this.explosions = [];
99
+
100
+ for (const powerup of this.powerups) {
101
+ this.pipeline.remove(powerup);
102
+ }
103
+ this.powerups = [];
104
+
105
+ for (const missile of this.missiles) {
106
+ this.pipeline.remove(missile);
107
+ }
108
+ this.missiles = [];
109
+
110
+ for (const laser of this.laserBeams) {
111
+ this.pipeline.remove(laser);
112
+ }
113
+ this.laserBeams = [];
114
+
115
+ for (const lightning of this.lightnings) {
116
+ this.pipeline.remove(lightning);
117
+ }
118
+ this.lightnings = [];
119
+
120
+ for (const minion of this.minions) {
121
+ this.pipeline.remove(minion);
122
+ }
123
+ this.minions = [];
124
+
125
+ if (this.boss) {
126
+ this.pipeline.remove(this.boss);
127
+ this.boss = null;
128
+ }
129
+
130
+ // Remove play button if exists
131
+ if (this.playButton) {
132
+ this.pipeline.remove(this.playButton);
133
+ this.playButton = null;
134
+ }
135
+
136
+ // Reset game state
137
+ this.score = 0;
138
+ this.lives = 3;
139
+ this.level = 1;
140
+ this.gameState = "ready";
141
+ this.alienDirection = 1;
142
+ this.alienMoveTimer = 0;
143
+ this.alienMoveInterval = this.baseMoveInterval;
144
+ this.countdownText.text = "";
145
+
146
+ // Reset gauntlet state
147
+ this.isGauntletMode = false;
148
+ this.gauntletPhase = 0;
149
+
150
+ // Reposition player
151
+ this.player.x = this.width / 2;
152
+ this.player.y = this.height - 90;
153
+ this.player.visible = true;
154
+ this.player.opacity = 1;
155
+ this.player.canShoot = true;
156
+ this.player.starPower = false;
157
+ this.player.starPowerTimer = 0;
158
+ // Reset all upgrades and ship colors
159
+ this.player.resetUpgrades();
160
+
161
+ // Reposition HUD elements
162
+ this.hud.hideMessage();
163
+
164
+ // Reposition countdown text
165
+ this.countdownText.x = this.width / 2;
166
+ this.countdownText.y = this.height / 2;
167
+
168
+ // Reposition sound button
169
+ this.soundButton.x = this.width - 50;
170
+ this.soundButton.y = this.height - 25;
171
+
172
+ // Respawn aliens for new screen size
173
+ this.spawnAliens();
174
+
175
+ // Create new play button centered
176
+ this.playButton = new Button(this, {
177
+ x: this.width / 2,
178
+ y: this.height / 2,
179
+ width: 200,
180
+ height: 60,
181
+ text: "PLAY",
182
+ font: "bold 24px monospace",
183
+ colorDefaultBg: "#003300",
184
+ colorDefaultStroke: "#00ff00",
185
+ colorDefaultText: "#00ff00",
186
+ colorHoverBg: "#004400",
187
+ colorHoverStroke: "#44ff44",
188
+ colorHoverText: "#44ff44",
189
+ colorPressedBg: "#002200",
190
+ colorPressedStroke: "#00aa00",
191
+ colorPressedText: "#00aa00",
192
+ onClick: () => this.startPlaying(),
193
+ });
194
+ this.pipeline.add(this.playButton);
195
+ }
196
+
197
+ init() {
198
+ // Prevent re-initialization on resume from alt-tab
199
+ if (this._spaceGameInitialized) {
200
+ return;
201
+ }
202
+ this._spaceGameInitialized = true;
203
+
204
+ super.init();
205
+ this.initKeyboard();
206
+ this.initAudio();
207
+
208
+ // Game state
209
+ this.score = 0;
210
+ this.lives = 3;
211
+ this.level = 1;
212
+ this.gameState = "ready"; // ready, countdown, playing, gameover, win, levelcomplete, flyoff, flyin
213
+ this.countdownValue = 3; // 3, 2, 1, then "GO!"
214
+ this.countdownTimer = 0;
215
+ this.alienDirection = 1; // 1 = right, -1 = left
216
+ this.alienMoveTimer = 0;
217
+ this.alienMoveInterval = 1; // seconds between moves
218
+ this.levelStartY = 80;
219
+ this.audioResumed = false;
220
+ this.baseMoveInterval = 1; // Base seconds between moves (decreases with level)
221
+ this.alienMoveInterval = this.baseMoveInterval;
222
+ this.levelStartY = 170; // Below title and score display
223
+ this.levelTransitionTimer = 0;
224
+ this.shipAnimationTimer = 0;
225
+ this.shipStartY = 0; // For fly-in animation
226
+ this.levelPlayTime = 0; // Time spent in current level (for escalating difficulty)
227
+
228
+ // Gauntlet mode state (Level 10 - final challenge)
229
+ // Phases: wave1 → boss1 → wave2 → boss2 → wave3 → boss3 → victory
230
+ this.gauntletPhase = 0; // 0-5 (0,2,4 = waves, 1,3,5 = bosses)
231
+ this.isGauntletMode = false;
232
+
233
+ // Collections
234
+ this.bullets = [];
235
+ this.aliens = [];
236
+ this.explosions = [];
237
+ this.powerups = [];
238
+ this.missiles = [];
239
+ this.laserBeams = [];
240
+ this.lightnings = [];
241
+ this.minions = [];
242
+ this.boss = null;
243
+
244
+ // Create starfield background
245
+ this.starfield = new Starfield(this);
246
+ this.pipeline.add(this.starfield);
247
+
248
+ // Create player at bottom center (use actual canvas dimensions)
249
+ this.player = new Player(this, {
250
+ x: this.width / 2,
251
+ y: this.height - 90,
252
+ });
253
+ this.pipeline.add(this.player);
254
+
255
+ // Create aliens
256
+ this.spawnAliens();
257
+
258
+ // Create HUD
259
+ this.hud = new HUD(this);
260
+ this.pipeline.add(this.hud);
261
+
262
+ // Create countdown text (hidden initially)
263
+ this.countdownText = new TextShape("", {
264
+ font: "bold 120px monospace",
265
+ color: "#00ff00",
266
+ align: "center",
267
+ baseline: "middle",
268
+ zIndex: 1000,
269
+ });
270
+ this.countdownText.x = this.width / 2;
271
+ this.countdownText.y = this.height / 2;
272
+ this.pipeline.add(this.countdownText);
273
+
274
+ // Create play button for start screen
275
+ this.playButton = new Button(this, {
276
+ x: this.width / 2,
277
+ y: this.height / 2,
278
+ width: 200,
279
+ height: 60,
280
+ text: "PLAY",
281
+ font: "bold 24px monospace",
282
+ colorDefaultBg: "#003300",
283
+ colorDefaultStroke: "#00ff00",
284
+ colorDefaultText: "#00ff00",
285
+ colorHoverBg: "#004400",
286
+ colorHoverStroke: "#44ff44",
287
+ colorHoverText: "#44ff44",
288
+ colorPressedBg: "#002200",
289
+ colorPressedStroke: "#00aa00",
290
+ colorPressedText: "#00aa00",
291
+ onClick: () => this.startPlaying(),
292
+ });
293
+ this.pipeline.add(this.playButton);
294
+
295
+ // FPS counter
296
+ this.fpsCounter = new FPSCounter(this, {
297
+ color: "#666666",
298
+ anchor: "bottom-left",
299
+ });
300
+ this.pipeline.add(this.fpsCounter);
301
+
302
+ // Sound toggle button (bottom right)
303
+ this.soundEnabled = true;
304
+ this.soundButton = new ToggleButton(this, {
305
+ x: this.width - 50,
306
+ y: this.height - 25,
307
+ width: 80,
308
+ height: 30,
309
+ text: "🔊 ON",
310
+ font: "12px monospace",
311
+ startToggled: true,
312
+ colorDefaultBg: "#222",
313
+ colorDefaultStroke: "#444",
314
+ colorDefaultText: "#666",
315
+ colorActiveBg: "#222",
316
+ colorActiveStroke: "#0f0",
317
+ colorActiveText: "#0f0",
318
+ colorHoverBg: "#333",
319
+ colorHoverStroke: "#666",
320
+ colorHoverText: "#0f0",
321
+ onToggle: (isOn) => {
322
+ this.soundEnabled = isOn;
323
+ this.soundButton.text = isOn ? "🔊 ON" : "🔇 OFF";
324
+ },
325
+ });
326
+ this.pipeline.add(this.soundButton);
327
+
328
+ // Debug commands for testing
329
+ this.setupDebugCommands();
330
+ }
331
+
332
+ /**
333
+ * Setup window debug commands for testing
334
+ */
335
+ setupDebugCommands() {
336
+ const game = this;
337
+
338
+ // Skip to gauntlet boss (0, 1, or 2)
339
+ window.skipToBoss = (bossIndex = 0) => {
340
+ // Clear everything
341
+ game.clearAllEntities();
342
+
343
+ // Set up gauntlet state
344
+ game.level = 10;
345
+ game.isGauntletMode = true;
346
+ game.gauntletPhase = bossIndex * 2; // Phase before boss (0, 2, or 4)
347
+ game.gameState = "bossfight";
348
+
349
+ // Give player all upgrades
350
+ game.player.applyUpgrade("speed1");
351
+ game.player.applyUpgrade("firerate1");
352
+ game.player.applyUpgrade("speed2");
353
+ game.player.applyUpgrade("tripleshot");
354
+ game.player.applyUpgrade("shield");
355
+
356
+ // Position player
357
+ game.player.x = game.width / 2;
358
+ game.player.y = game.height - 90;
359
+ game.player.visible = true;
360
+ game.player.opacity = 1;
361
+ game.player.canShoot = true;
362
+
363
+ // Spawn the boss
364
+ game.spawnGauntletBoss(bossIndex);
365
+ game.hud.hideMessage();
366
+
367
+ console.log(`Skipped to gauntlet boss ${bossIndex + 1}`);
368
+ };
369
+
370
+ // Skip to specific level
371
+ window.skipToLevel = (level) => {
372
+ game.clearAllEntities();
373
+
374
+ game.level = level;
375
+ game.isGauntletMode = false;
376
+ game.gauntletPhase = 0;
377
+ game.gameState = "playing";
378
+
379
+ // Apply upgrades based on level
380
+ if (level > 3) game.player.applyUpgrade("speed1");
381
+ if (level > 4) game.player.applyUpgrade("firerate1");
382
+ if (level > 5) game.player.applyUpgrade("speed2");
383
+ if (level > 6) game.player.applyUpgrade("tripleshot");
384
+ if (level > 9) game.player.applyUpgrade("shield");
385
+
386
+ // Position player
387
+ game.player.x = game.width / 2;
388
+ game.player.y = game.height - 90;
389
+ game.player.visible = true;
390
+ game.player.opacity = 1;
391
+ game.player.canShoot = true;
392
+
393
+ game.spawnAliens();
394
+ game.hud.hideMessage();
395
+
396
+ console.log(`Skipped to level ${level}`);
397
+ };
398
+
399
+ // Skip to gauntlet mode (start of level 10)
400
+ window.skipToGauntlet = () => {
401
+ game.clearAllEntities();
402
+
403
+ game.level = 10;
404
+ game.isGauntletMode = true;
405
+ game.gauntletPhase = 0;
406
+ game.gameState = "playing";
407
+
408
+ // Give all upgrades
409
+ game.player.applyUpgrade("speed1");
410
+ game.player.applyUpgrade("firerate1");
411
+ game.player.applyUpgrade("speed2");
412
+ game.player.applyUpgrade("tripleshot");
413
+ game.player.applyUpgrade("shield");
414
+
415
+ // Position player
416
+ game.player.x = game.width / 2;
417
+ game.player.y = game.height - 90;
418
+ game.player.visible = true;
419
+ game.player.opacity = 1;
420
+ game.player.canShoot = true;
421
+
422
+ game.spawnGauntletWave();
423
+ game.hud.hideMessage();
424
+
425
+ console.log("Skipped to gauntlet (level 10)");
426
+ };
427
+
428
+ console.log("Debug commands available: skipToBoss(0-2), skipToLevel(1-9), skipToGauntlet()");
429
+ }
430
+
431
+ /**
432
+ * Clear all game entities (used by debug commands)
433
+ */
434
+ clearAllEntities() {
435
+ for (const bullet of this.bullets) this.pipeline.remove(bullet);
436
+ this.bullets = [];
437
+
438
+ for (const alien of this.aliens) this.pipeline.remove(alien);
439
+ this.aliens = [];
440
+
441
+ for (const explosion of this.explosions) this.pipeline.remove(explosion);
442
+ this.explosions = [];
443
+
444
+ for (const powerup of this.powerups) this.pipeline.remove(powerup);
445
+ this.powerups = [];
446
+
447
+ for (const missile of this.missiles) this.pipeline.remove(missile);
448
+ this.missiles = [];
449
+
450
+ for (const laser of this.laserBeams) this.pipeline.remove(laser);
451
+ this.laserBeams = [];
452
+
453
+ for (const lightning of this.lightnings) this.pipeline.remove(lightning);
454
+ this.lightnings = [];
455
+
456
+ for (const minion of this.minions) this.pipeline.remove(minion);
457
+ this.minions = [];
458
+
459
+ if (this.boss) {
460
+ this.pipeline.remove(this.boss);
461
+ this.boss = null;
462
+ }
463
+
464
+ if (this.playButton) {
465
+ this.pipeline.remove(this.playButton);
466
+ this.playButton = null;
467
+ }
468
+ }
469
+
470
+ playSound(soundFn, ...args) {
471
+ if (this.soundEnabled) {
472
+ soundFn(...args);
473
+ }
474
+ }
475
+
476
+ initAudio() {
477
+ // Initialize the Synth audio system
478
+ Synth.init({ masterVolume: 0.4 });
479
+ this.logger.log("[SpaceGame] Audio system initialized");
480
+ }
481
+
482
+ async resumeAudio() {
483
+ if (!this.audioResumed) {
484
+ await Synth.resume();
485
+ // Play a silent warmup sound to prime the audio pipeline
486
+ Sound.beep(1, 0.01, { volume: 0.001 });
487
+ // Small delay to let the audio pipeline fully initialize
488
+ await new Promise(resolve => setTimeout(resolve, 50));
489
+ this.audioResumed = true;
490
+ this.logger.log("[SpaceGame] Audio context resumed and warmed up");
491
+ }
492
+ }
493
+
494
+ spawnAliens() {
495
+ // Clear existing aliens
496
+ for (const alien of this.aliens) {
497
+ this.pipeline.remove(alien);
498
+ }
499
+ this.aliens = [];
500
+
501
+ // Calculate rows based on level (starts at 4, increases every 2 levels, max 8)
502
+ const alienRows = Math.min(MAX_ALIEN_ROWS, ALIEN_BASE_ROWS + Math.floor((this.level - 1) / 2));
503
+
504
+ // Calculate starting position to center the alien grid
505
+ const gridWidth = ALIEN_COLS * ALIEN_SPACING_X;
506
+ const startX = (this.width - gridWidth) / 2 + ALIEN_SPACING_X / 2;
507
+
508
+ // Adjust starting Y based on level - aliens start lower on higher levels
509
+ const levelStartOffset = Math.min(50, (this.level - 1) * 10);
510
+ const startY = this.levelStartY + levelStartOffset;
511
+
512
+ for (let row = 0; row < alienRows; row++) {
513
+ for (let col = 0; col < ALIEN_COLS; col++) {
514
+ // Assign row type - distribute all types across rows
515
+ // row=0 → squid, row=1-2 → crab, row=3+ → octopus (matches Alien.createShape logic)
516
+ let rowType;
517
+ if (row < 2) {
518
+ rowType = 0; // Top 2 rows = squid (30pts)
519
+ } else if (row < Math.max(3, Math.floor(alienRows * 0.5))) {
520
+ rowType = 1; // Middle rows = crab (20pts) - at least row 2
521
+ } else {
522
+ rowType = 3; // Bottom rows = octopus (10pts)
523
+ }
524
+
525
+ const alien = new Alien(this, {
526
+ x: startX + col * ALIEN_SPACING_X,
527
+ y: startY + row * ALIEN_SPACING_Y,
528
+ row: rowType,
529
+ col: col,
530
+ });
531
+ this.aliens.push(alien);
532
+ this.pipeline.add(alien);
533
+ }
534
+ }
535
+
536
+ // Calculate level-based speed multiplier
537
+ // Each level is 15% faster, compounding
538
+ const levelSpeedMultiplier = Math.pow(1.15, this.level - 1);
539
+ this.baseMoveInterval = Math.max(0.3, 1 / levelSpeedMultiplier);
540
+ this.alienMoveInterval = this.baseMoveInterval;
541
+ }
542
+
543
+ spawnPlayerBullet(x, y, angle = 0) {
544
+ const bullet = new Bullet(this, {
545
+ x: x,
546
+ y: y,
547
+ speed: BULLET_SPEED,
548
+ direction: -1,
549
+ angle: angle, // Angle in degrees (0 = straight up, negative = left, positive = right)
550
+ isPlayerBullet: true,
551
+ });
552
+ this.bullets.push(bullet);
553
+ this.pipeline.add(bullet);
554
+
555
+ // Play laser sound (only for center bullet to avoid triple sound)
556
+ if (angle === 0) {
557
+ this.resumeAudio();
558
+ if (this.soundEnabled) Sound.laser({ startFreq: 1500, endFreq: 300, duration: 0.1, volume: 0.2 });
559
+ }
560
+ }
561
+
562
+ spawnAlienBullet(x, y) {
563
+ const bullet = new Bullet(this, {
564
+ x: x,
565
+ y: y,
566
+ speed: ALIEN_BULLET_SPEED,
567
+ direction: 1,
568
+ isPlayerBullet: false,
569
+ });
570
+ this.bullets.push(bullet);
571
+ this.pipeline.add(bullet);
572
+
573
+ // Play alien laser sound (lower, more menacing)
574
+ if (this.soundEnabled) Sound.laser({ startFreq: 600, endFreq: 200, duration: 0.12, volume: 0.15, type: "square" });
575
+ }
576
+
577
+ spawnExplosion(x, y, color, isPlayer = false) {
578
+ const explosion = new Explosion(this, {
579
+ x: x,
580
+ y: y,
581
+ color: color,
582
+ });
583
+ this.explosions.push(explosion);
584
+ this.pipeline.add(explosion);
585
+
586
+ // Play explosion sound - different for player vs alien
587
+ if (this.soundEnabled) {
588
+ if (isPlayer) {
589
+ Sound.explosion(0.8);
590
+ } else {
591
+ // Alien explosion - higher pitched, shorter
592
+ Sound.impact(0.6);
593
+ Sound.beep(200 + Math.random() * 100, 0.08, { volume: 0.15, type: "square" });
594
+ }
595
+ }
596
+ }
597
+
598
+ spawnPowerUp() {
599
+ // Spawn at random X position at top of screen
600
+ const powerup = new PowerUp(this, {
601
+ x: POWERUP_SIZE + Math.random() * (this.width - POWERUP_SIZE * 2),
602
+ y: -POWERUP_SIZE,
603
+ });
604
+ this.powerups.push(powerup);
605
+ this.pipeline.add(powerup);
606
+ }
607
+
608
+ spawnStarPower() {
609
+ // Spawn golden star power-up at random X position
610
+ const starpower = new StarPowerUp(this, {
611
+ x: POWERUP_SIZE + Math.random() * (this.width - POWERUP_SIZE * 2),
612
+ y: -POWERUP_SIZE,
613
+ });
614
+ this.powerups.push(starpower);
615
+ this.pipeline.add(starpower);
616
+ }
617
+
618
+ spawnMissile() {
619
+ // Always spawn from the top - left, middle, or right
620
+ const spawnZone = Math.random();
621
+ let startX, startY;
622
+
623
+ startY = -MISSILE_HEIGHT; // Always from top
624
+
625
+ if (spawnZone < 0.33) {
626
+ // Top left
627
+ startX = 30 + Math.random() * (this.width / 3 - 60);
628
+ } else if (spawnZone < 0.66) {
629
+ // Top middle
630
+ startX = this.width / 3 + Math.random() * (this.width / 3);
631
+ } else {
632
+ // Top right
633
+ startX = (this.width * 2 / 3) + Math.random() * (this.width / 3 - 30);
634
+ }
635
+
636
+ const missile = new Missile(this, {
637
+ x: startX,
638
+ y: startY,
639
+ targetX: this.player.x,
640
+ targetY: this.player.y,
641
+ });
642
+ this.missiles.push(missile);
643
+ this.pipeline.add(missile);
644
+
645
+ // Warning sound
646
+ if (this.soundEnabled) {
647
+ Sound.beep(400, 0.1, { volume: 0.2, type: "sawtooth" });
648
+ }
649
+ }
650
+
651
+ spawnLaserBeam(targetX = null) {
652
+ // Use provided X or random position
653
+ const x = targetX !== null
654
+ ? Math.max(50, Math.min(this.width - 50, targetX)) // Clamp to screen
655
+ : 80 + Math.random() * (this.width - 160); // Random, avoiding edges
656
+
657
+ const laser = new LaserBeam(this, { x });
658
+ this.laserBeams.push(laser);
659
+ this.pipeline.add(laser);
660
+
661
+ // Warning sound - high pitch zap
662
+ if (this.soundEnabled) {
663
+ Sound.beep(1200, 0.15, { volume: 0.25, type: "sine" });
664
+ }
665
+ }
666
+
667
+ spawnLightning() {
668
+ const lightning = new Lightning(this, {});
669
+ this.lightnings.push(lightning);
670
+ this.pipeline.add(lightning);
671
+
672
+ // Thunder crack sound
673
+ if (this.soundEnabled) {
674
+ // Start with high frequency crack
675
+ Sound.beep(2000, 0.08, { volume: 0.3, type: "sawtooth" });
676
+ // Follow with rumble
677
+ setTimeout(() => {
678
+ Sound.beep(80, 0.3, { volume: 0.25, type: "sawtooth" });
679
+ }, 80);
680
+ }
681
+ }
682
+
683
+ spawnAbsorbEffect(x, y, color) {
684
+ const effect = new AbsorbEffect(this, {
685
+ x: x,
686
+ y: y,
687
+ color: color,
688
+ targetX: this.player.x,
689
+ targetY: this.player.y,
690
+ });
691
+ this.explosions.push(effect); // Reuse explosions array for effects
692
+ this.pipeline.add(effect);
693
+ }
694
+
695
+ /**
696
+ * Spawn a boss based on the current level
697
+ * Level 3 = Squid boss (type 0)
698
+ * Level 6 = Crab boss (type 1)
699
+ * Level 9 = Octopus boss (type 2)
700
+ */
701
+ spawnBoss() {
702
+ // Determine boss type based on which boss level this is
703
+ const bossIndex = BOSS_LEVELS.indexOf(this.level);
704
+ const bossType = bossIndex >= 0 ? bossIndex : 0;
705
+
706
+ this.boss = new Boss(this, {
707
+ x: this.width / 2,
708
+ y: -100, // Start above screen
709
+ bossType: bossType,
710
+ targetY: 320, // Where boss will stop after entry animation (below game title)
711
+ });
712
+ this.pipeline.add(this.boss);
713
+
714
+ // Warning sound for boss entrance
715
+ if (this.soundEnabled) {
716
+ Sound.beep(200, 0.3, { volume: 0.3, type: "sawtooth" });
717
+ setTimeout(() => Sound.beep(150, 0.3, { volume: 0.3, type: "sawtooth" }), 300);
718
+ setTimeout(() => Sound.beep(100, 0.5, { volume: 0.4, type: "sawtooth" }), 600);
719
+ }
720
+ }
721
+
722
+ /**
723
+ * Spawn a missile from boss position toward player
724
+ */
725
+ spawnBossMissile(x, y) {
726
+ const missile = new Missile(this, {
727
+ x: x,
728
+ y: y,
729
+ targetX: this.player.x,
730
+ targetY: this.player.y,
731
+ });
732
+ this.missiles.push(missile);
733
+ this.pipeline.add(missile);
734
+
735
+ // Warning sound
736
+ if (this.soundEnabled) {
737
+ Sound.beep(300, 0.1, { volume: 0.2, type: "sawtooth" });
738
+ }
739
+ }
740
+
741
+ /**
742
+ * Spawn a boss minion that floats to a position near the boss
743
+ */
744
+ spawnBossMinion(startX, startY, targetX, targetY, minionType) {
745
+ // Spawn minions closer to the boss - within 150px horizontally, 50-120px below
746
+ const bossX = this.boss ? this.boss.x : this.width / 2;
747
+ const bossY = this.boss ? this.boss.y : 170;
748
+ const closeTargetX = bossX + (Math.random() - 0.5) * 300; // ±150px from boss
749
+ const closeTargetY = bossY + 50 + Math.random() * 70; // 50-120px below boss
750
+
751
+ const minion = new BossMinion(this, {
752
+ x: startX,
753
+ y: startY,
754
+ startX: startX,
755
+ startY: startY,
756
+ targetX: Math.max(50, Math.min(this.width - 50, closeTargetX)),
757
+ targetY: closeTargetY,
758
+ minionType: minionType,
759
+ boss: this.boss,
760
+ });
761
+ this.minions.push(minion);
762
+ this.pipeline.add(minion);
763
+
764
+ // Spawn sound
765
+ if (this.soundEnabled) {
766
+ Sound.beep(600, 0.1, { volume: 0.15, type: "square" });
767
+ }
768
+ }
769
+
770
+ update(dt) {
771
+ super.update(dt);
772
+
773
+ // Resume audio on any key press (browser autoplay policy)
774
+ if (!this.audioResumed && (Keys.isDown(Keys.SPACE) || Keys.isDown(Keys.LEFT) || Keys.isDown(Keys.RIGHT))) {
775
+ this.resumeAudio();
776
+ }
777
+
778
+ // Handle ready state - waiting for player to click play button
779
+ if (this.gameState === "ready") {
780
+ // Button handles the click, just wait
781
+ return;
782
+ }
783
+
784
+ // Handle ship flying off screen after level complete
785
+ if (this.gameState === "flyoff") {
786
+ this.shipAnimationTimer += dt;
787
+ const flyOffDuration = 0.8; // 0.8 seconds to fly off
788
+ const progress = Math.min(1, this.shipAnimationTimer / flyOffDuration);
789
+
790
+ // Ease in cubic for acceleration
791
+ const eased = Math.pow(progress, 2);
792
+
793
+ // Move from current position to off-screen
794
+ const startY = this.height - 90;
795
+ const targetY = -80;
796
+ this.player.y = startY + (targetY - startY) * eased;
797
+
798
+ // When ship is off screen, start next level with fly-in
799
+ if (progress >= 1) {
800
+ this.prepareNextLevel();
801
+ this.gameState = "flyin";
802
+ this.shipAnimationTimer = 0;
803
+ this.player.y = this.height + 50; // Start below screen
804
+ this.shipStartY = this.height - 90; // Target position
805
+ this.hud.hideMessage();
806
+ }
807
+ return;
808
+ }
809
+
810
+ // Handle ship flying in from bottom
811
+ if (this.gameState === "flyin") {
812
+ this.shipAnimationTimer += dt;
813
+ const flyInDuration = 1.0; // 1 second to fly in
814
+ const progress = Math.min(1, this.shipAnimationTimer / flyInDuration);
815
+
816
+ // Ease out cubic for smooth deceleration
817
+ const eased = 1 - Math.pow(1 - progress, 3);
818
+
819
+ // Interpolate from bottom to target position
820
+ const startY = this.height + 50;
821
+ const targetY = this.shipStartY;
822
+ this.player.y = startY + (targetY - startY) * eased;
823
+
824
+ // When animation complete, start playing
825
+ if (progress >= 1) {
826
+ this.player.y = targetY;
827
+ this.gameState = "playing";
828
+ }
829
+ return;
830
+ }
831
+
832
+ // Handle countdown before gameplay starts
833
+ if (this.gameState === "countdown") {
834
+ this.countdownTimer += dt;
835
+ if (this.countdownTimer >= 1) {
836
+ this.countdownTimer = 0;
837
+ this.countdownValue--;
838
+
839
+ if (this.countdownValue > 0) {
840
+ // Show next number
841
+ this.countdownText.text = String(this.countdownValue);
842
+ if (this.soundEnabled) Sound.beep(880, 0.15, { volume: 0.4 });
843
+ } else if (this.countdownValue === 0) {
844
+ // Show "GO!"
845
+ this.countdownText.text = "GO!";
846
+ this.countdownText.color = "#ffff00"; // Yellow for GO!
847
+ if (this.soundEnabled) Sound.beep(1320, 0.2, { volume: 0.5 });
848
+ } else {
849
+ // Countdown finished, start gameplay
850
+ this.countdownText.color = "#00ff00"; // Reset color for next time
851
+ this.beginGameplay();
852
+ }
853
+ }
854
+ return;
855
+ }
856
+
857
+ // Handle level complete - show message then fly off
858
+ if (this.gameState === "levelcomplete") {
859
+ this.levelTransitionTimer += dt;
860
+ if (this.levelTransitionTimer >= 1.5) {
861
+ this.gameState = "flyoff";
862
+ this.shipAnimationTimer = 0;
863
+ }
864
+ return;
865
+ }
866
+
867
+ // Handle boss fight state
868
+ if (this.gameState === "bossfight") {
869
+ this.updateBossFight(dt);
870
+ return;
871
+ }
872
+
873
+ if (this.gameState !== "playing") {
874
+ // Buttons handle gameover/win states
875
+ return;
876
+ }
877
+
878
+ // Update aliens movement
879
+ this.updateAliens(dt);
880
+
881
+ // Alien shooting
882
+ this.alienShooting();
883
+
884
+ // Random chance to spawn power-ups (max 1 of each type on screen)
885
+ const has1Up = this.powerups.some(p => p.active && p instanceof PowerUp);
886
+ const hasStarPower = this.powerups.some(p => p.active && p instanceof StarPowerUp);
887
+
888
+ if (!has1Up && Math.random() < POWERUP_SPAWN_CHANCE) {
889
+ this.spawnPowerUp();
890
+ }
891
+ if (!hasStarPower && Math.random() < STARPOWER_SPAWN_CHANCE) {
892
+ this.spawnStarPower();
893
+ }
894
+
895
+ // Track time in level for escalating missile danger
896
+ this.levelPlayTime += dt;
897
+
898
+ // Random chance to spawn homing missiles - ESCALATES over time!
899
+ // Base chance increases with level, PLUS increases every 10 seconds within level
900
+ const levelMultiplier = 1 + (this.level - 1) * 0.5; // +50% per level
901
+ const timeMultiplier = 1 + (this.levelPlayTime / 10) * 0.3; // +30% every 10 seconds
902
+ const missileChance = MISSILE_SPAWN_CHANCE * levelMultiplier * timeMultiplier;
903
+ const maxMissiles = Math.min(5, 2 + Math.floor(this.level / 2)); // +1 max every 2 levels, cap at 5
904
+ if (this.missiles.length < maxMissiles && Math.random() < missileChance) {
905
+ this.spawnMissile();
906
+ }
907
+
908
+ // Laser beams start spawning after defeating first boss (level 4+)
909
+ if (this.level > 3) {
910
+ const laserMultiplier = 1 + (this.level - 4) * 0.3; // +30% per level after 4
911
+ const laserChance = LASER_SPAWN_CHANCE * laserMultiplier * timeMultiplier;
912
+ const maxLasers = Math.min(2, 1 + Math.floor((this.level - 3) / 3)); // Max 1-2 lasers
913
+ if (this.laserBeams.filter(l => l.active).length < maxLasers && Math.random() < laserChance) {
914
+ this.spawnLaserBeam();
915
+ }
916
+ }
917
+
918
+ // Lightning starts spawning after defeating second boss (level 7+)
919
+ if (this.level > 6) {
920
+ const lightningMultiplier = 1 + (this.level - 7) * 0.25; // +25% per level after 7
921
+ const lightningChance = LIGHTNING_SPAWN_CHANCE * lightningMultiplier * timeMultiplier;
922
+ // Only one lightning at a time - it's dramatic!
923
+ if (this.lightnings.filter(l => l.active).length < 1 && Math.random() < lightningChance) {
924
+ this.spawnLightning();
925
+ }
926
+ }
927
+
928
+ // Check collisions
929
+ this.checkCollisions();
930
+
931
+ // Check power-up collection
932
+ this.checkPowerUpCollection();
933
+
934
+ // Clean up dead objects
935
+ this.cleanup();
936
+
937
+ // Check win condition - advance to next level or start boss fight
938
+ if (this.getAliveAliens().length === 0) {
939
+ if (this.isGauntletMode) {
940
+ // In gauntlet mode - wave cleared, spawn boss immediately (no interruption)
941
+ this.advanceGauntlet();
942
+ } else if (BOSS_LEVELS.includes(this.level)) {
943
+ // Normal boss level
944
+ this.startBossFight();
945
+ } else {
946
+ this.levelComplete();
947
+ }
948
+ }
949
+ }
950
+
951
+ /**
952
+ * Start a boss fight after clearing all aliens on a boss level
953
+ */
954
+ startBossFight() {
955
+ this.gameState = "bossfight";
956
+ this.levelPlayTime = 0;
957
+
958
+ // Clear any remaining bullets
959
+ for (const bullet of this.bullets) {
960
+ this.pipeline.remove(bullet);
961
+ }
962
+ this.bullets = [];
963
+
964
+ // Show boss warning
965
+ this.hud.showMessage("WARNING!\nBOSS INCOMING!");
966
+
967
+ // Spawn boss after a brief delay
968
+ setTimeout(() => {
969
+ if (this.gameState === "bossfight") {
970
+ this.hud.hideMessage();
971
+ this.spawnBoss();
972
+ }
973
+ }, 1500);
974
+ }
975
+
976
+ /**
977
+ * Update boss fight - handles boss, minions, and all combat
978
+ */
979
+ updateBossFight(dt) {
980
+ // Track time for power-ups and missiles
981
+ this.levelPlayTime += dt;
982
+
983
+ // Power-ups still spawn during boss fight
984
+ const has1Up = this.powerups.some(p => p.active && p instanceof PowerUp);
985
+ const hasStarPower = this.powerups.some(p => p.active && p instanceof StarPowerUp);
986
+
987
+ if (!has1Up && Math.random() < POWERUP_SPAWN_CHANCE) {
988
+ this.spawnPowerUp();
989
+ }
990
+ if (!hasStarPower && Math.random() < STARPOWER_SPAWN_CHANCE) {
991
+ this.spawnStarPower();
992
+ }
993
+
994
+ // Regular missiles also spawn during boss fight - scales with boss difficulty
995
+ const bossType = this.boss ? this.boss.bossType : 0;
996
+ const maxMissiles = 2 + bossType; // 2, 3, 4 max missiles
997
+ const missileMultiplier = 1 + bossType * 0.5; // 1x, 1.5x, 2x spawn rate
998
+ if (this.missiles.length < maxMissiles && Math.random() < MISSILE_SPAWN_CHANCE * missileMultiplier) {
999
+ this.spawnMissile();
1000
+ }
1001
+
1002
+ // Check bullet collisions with boss and minions
1003
+ this.checkBossCollisions();
1004
+
1005
+ // Check power-up collection
1006
+ this.checkPowerUpCollection();
1007
+
1008
+ // Clean up dead objects
1009
+ this.cleanup();
1010
+ this.cleanupMinions();
1011
+
1012
+ // Check if boss is defeated
1013
+ if (this.boss && !this.boss.active) {
1014
+ this.bossDefeated();
1015
+ }
1016
+ }
1017
+
1018
+ /**
1019
+ * Check collisions during boss fight
1020
+ */
1021
+ checkBossCollisions() {
1022
+ const playerBounds = {
1023
+ x: this.player.x - PLAYER_WIDTH / 2,
1024
+ y: this.player.y - PLAYER_HEIGHT / 2,
1025
+ width: PLAYER_WIDTH,
1026
+ height: PLAYER_HEIGHT,
1027
+ };
1028
+
1029
+ for (const bullet of this.bullets) {
1030
+ if (!bullet.active) continue;
1031
+
1032
+ const bulletBounds = bullet.getBounds();
1033
+
1034
+ if (bullet.isPlayerBullet) {
1035
+ // Check against boss
1036
+ if (this.boss && this.boss.active) {
1037
+ const bossBounds = this.boss.getBounds();
1038
+ if (this.intersects(bulletBounds, bossBounds)) {
1039
+ bullet.destroy();
1040
+ const defeated = this.boss.takeDamage();
1041
+ // Small hit effect
1042
+ if (!defeated) {
1043
+ if (this.soundEnabled) Sound.beep(150, 0.05, { volume: 0.2, type: "square" });
1044
+ }
1045
+ continue;
1046
+ }
1047
+ }
1048
+
1049
+ // Check against minions
1050
+ for (const minion of this.minions) {
1051
+ if (!minion.active) continue;
1052
+
1053
+ const minionBounds = minion.getBounds();
1054
+ if (this.intersects(bulletBounds, minionBounds)) {
1055
+ bullet.destroy();
1056
+ const defeated = minion.takeDamage();
1057
+ if (defeated) {
1058
+ this.addScore(minion.points);
1059
+ this.spawnExplosion(minion.x, minion.y, "#ffff00");
1060
+ } else {
1061
+ if (this.soundEnabled) Sound.beep(200, 0.05, { volume: 0.15, type: "square" });
1062
+ }
1063
+ break;
1064
+ }
1065
+ }
1066
+ } else {
1067
+ // Enemy bullet - check against player
1068
+ if (this.intersects(bulletBounds, playerBounds)) {
1069
+ bullet.destroy();
1070
+ this.playerHit();
1071
+ }
1072
+ }
1073
+ }
1074
+
1075
+ // Check missile collisions with player
1076
+ for (const missile of this.missiles) {
1077
+ if (!missile.active) continue;
1078
+
1079
+ const missileBounds = missile.getBounds();
1080
+ if (this.intersects(missileBounds, playerBounds)) {
1081
+ missile.destroy();
1082
+ this.spawnExplosion(missile.x, missile.y, "#ff6600");
1083
+ this.playerHit();
1084
+ }
1085
+ }
1086
+
1087
+ // Check laser beam collisions with player
1088
+ for (const laser of this.laserBeams) {
1089
+ if (!laser.active || !laser.canDamage) continue;
1090
+
1091
+ const laserBounds = laser.getBounds();
1092
+ if (this.intersects(laserBounds, playerBounds)) {
1093
+ // Laser damages player once per activation
1094
+ if (!laser.hasHitPlayer) {
1095
+ laser.hasHitPlayer = true;
1096
+ this.spawnExplosion(this.player.x, this.player.y, "#ffffff");
1097
+ this.playerHit();
1098
+ }
1099
+ }
1100
+ }
1101
+
1102
+ // Check lightning collisions with player
1103
+ for (const lightning of this.lightnings) {
1104
+ if (!lightning.active || !lightning.canDamage) continue;
1105
+
1106
+ // Use precise segment-based collision
1107
+ if (lightning.checkCollision(playerBounds)) {
1108
+ // Lightning damages player once per strike
1109
+ if (!lightning.hasHitPlayer) {
1110
+ lightning.hasHitPlayer = true;
1111
+ this.spawnExplosion(this.player.x, this.player.y, "#8888ff");
1112
+ this.playerHit();
1113
+ }
1114
+ }
1115
+ }
1116
+ }
1117
+
1118
+ /**
1119
+ * Boss has been defeated
1120
+ */
1121
+ bossDefeated() {
1122
+ // Big explosion at boss position
1123
+ const bossX = this.boss.x;
1124
+ const bossY = this.boss.y;
1125
+
1126
+ // Multiple explosions for dramatic effect
1127
+ for (let i = 0; i < 5; i++) {
1128
+ setTimeout(() => {
1129
+ const offsetX = (Math.random() - 0.5) * 80;
1130
+ const offsetY = (Math.random() - 0.5) * 80;
1131
+ this.spawnExplosion(bossX + offsetX, bossY + offsetY, "#ff8800");
1132
+ }, i * 100);
1133
+ }
1134
+
1135
+ // Award boss points
1136
+ this.addScore(this.boss.points);
1137
+
1138
+ // Award extra life for defeating boss
1139
+ this.lives++;
1140
+ this.hud.showMessage("BOSS DEFEATED!\n+1 LIFE", 2.0);
1141
+
1142
+ // Clean up boss
1143
+ this.pipeline.remove(this.boss);
1144
+ this.boss = null;
1145
+
1146
+ // Clear all minions
1147
+ for (const minion of this.minions) {
1148
+ if (minion.active) {
1149
+ this.spawnExplosion(minion.x, minion.y, "#ffff00");
1150
+ }
1151
+ this.pipeline.remove(minion);
1152
+ }
1153
+ this.minions = [];
1154
+
1155
+ // Play victory sound
1156
+ if (this.soundEnabled) {
1157
+ Sound.win();
1158
+ }
1159
+
1160
+ // In gauntlet mode, advance seamlessly to next wave
1161
+ if (this.isGauntletMode) {
1162
+ this.advanceGauntlet();
1163
+ } else {
1164
+ // Normal level complete
1165
+ this.levelComplete();
1166
+ }
1167
+ }
1168
+
1169
+ /**
1170
+ * Clean up inactive minions
1171
+ */
1172
+ cleanupMinions() {
1173
+ this.minions = this.minions.filter((m) => {
1174
+ if (!m.active) {
1175
+ this.pipeline.remove(m);
1176
+ return false;
1177
+ }
1178
+ return true;
1179
+ });
1180
+ }
1181
+
1182
+ updateAliens(dt) {
1183
+ this.alienMoveTimer += dt;
1184
+
1185
+ if (this.alienMoveTimer >= this.alienMoveInterval) {
1186
+ this.alienMoveTimer = 0;
1187
+
1188
+ const aliveAliens = this.getAliveAliens();
1189
+ if (aliveAliens.length === 0) return;
1190
+
1191
+ // Play alien march sound (alternating tones like classic Space Invaders)
1192
+ this.alienMoveNote = (this.alienMoveNote || 0) + 1;
1193
+ const freq = this.alienMoveNote % 2 === 0 ? 100 : 80;
1194
+ if (this.soundEnabled) Sound.beep(freq, 0.05, { volume: 0.15, type: "square" });
1195
+
1196
+ // Check if we need to change direction
1197
+ let shouldDrop = false;
1198
+ let shouldReverse = false;
1199
+
1200
+ for (const alien of aliveAliens) {
1201
+ const nextX = alien.x + ALIEN_MOVE_SPEED * this.alienDirection;
1202
+ if (nextX < ALIEN_WIDTH / 2 || nextX > this.width - ALIEN_WIDTH / 2) {
1203
+ shouldReverse = true;
1204
+ shouldDrop = true;
1205
+ break;
1206
+ }
1207
+ }
1208
+
1209
+ if (shouldReverse) {
1210
+ this.alienDirection *= -1;
1211
+ }
1212
+
1213
+ // Move all aliens
1214
+ for (const alien of aliveAliens) {
1215
+ if (shouldDrop) {
1216
+ alien.y += ALIEN_DROP_DISTANCE;
1217
+ } else {
1218
+ alien.x += ALIEN_MOVE_SPEED * this.alienDirection;
1219
+ }
1220
+
1221
+ // Check if aliens reached the bottom
1222
+ if (alien.y > this.height - 100) {
1223
+ this.gameOver();
1224
+ return;
1225
+ }
1226
+ }
1227
+
1228
+ // Speed up as fewer aliens remain (relative to level's base speed)
1229
+ const totalAliens = this.aliens.length;
1230
+ const destroyedCount = totalAliens - aliveAliens.length;
1231
+ const killSpeedBonus = 1 + destroyedCount * 0.02;
1232
+ this.alienMoveInterval = Math.max(0.1, this.baseMoveInterval / killSpeedBonus);
1233
+ }
1234
+ }
1235
+
1236
+ alienShooting() {
1237
+ const aliveAliens = this.getAliveAliens();
1238
+ if (aliveAliens.length === 0) return;
1239
+
1240
+ // Find bottom-most alien in each column
1241
+ const bottomAliens = new Map();
1242
+ for (const alien of aliveAliens) {
1243
+ const existing = bottomAliens.get(alien.col);
1244
+ if (!existing || alien.y > existing.y) {
1245
+ bottomAliens.set(alien.col, alien);
1246
+ }
1247
+ }
1248
+
1249
+ // Shooting chance increases with level (base + 50% per level, capped)
1250
+ const shootChance = Math.min(0.015, ALIEN_SHOOT_CHANCE * (1 + (this.level - 1) * 0.5));
1251
+
1252
+ // Random chance to shoot from bottom aliens
1253
+ for (const alien of bottomAliens.values()) {
1254
+ if (Math.random() < shootChance) {
1255
+ this.spawnAlienBullet(alien.x, alien.y + ALIEN_HEIGHT / 2);
1256
+ }
1257
+ }
1258
+ }
1259
+
1260
+ checkCollisions() {
1261
+ const playerBounds = {
1262
+ x: this.player.x - PLAYER_WIDTH / 2,
1263
+ y: this.player.y - PLAYER_HEIGHT / 2,
1264
+ width: PLAYER_WIDTH,
1265
+ height: PLAYER_HEIGHT,
1266
+ };
1267
+
1268
+ for (const bullet of this.bullets) {
1269
+ if (!bullet.active) continue;
1270
+
1271
+ const bulletBounds = bullet.getBounds();
1272
+
1273
+ if (bullet.isPlayerBullet) {
1274
+ // Check against aliens
1275
+ for (const alien of this.aliens) {
1276
+ if (!alien.active) continue;
1277
+
1278
+ const alienBounds = alien.getBounds();
1279
+ if (this.intersects(bulletBounds, alienBounds)) {
1280
+ // Hit!
1281
+ bullet.destroy();
1282
+ alien.destroy();
1283
+ this.addScore(alien.points);
1284
+ this.spawnExplosion(alien.x, alien.y, "#ffff00");
1285
+ break;
1286
+ }
1287
+ }
1288
+ } else {
1289
+ // Enemy bullet - check against player
1290
+ if (this.intersects(bulletBounds, playerBounds)) {
1291
+ bullet.destroy();
1292
+ this.playerHit();
1293
+ }
1294
+ }
1295
+ }
1296
+
1297
+ // Check missile collisions with player
1298
+ for (const missile of this.missiles) {
1299
+ if (!missile.active) continue;
1300
+
1301
+ const missileBounds = missile.getBounds();
1302
+ if (this.intersects(missileBounds, playerBounds)) {
1303
+ missile.destroy();
1304
+ this.spawnExplosion(missile.x, missile.y, "#ff6600");
1305
+ this.playerHit();
1306
+ }
1307
+ }
1308
+ }
1309
+
1310
+ intersects(a, b) {
1311
+ return (
1312
+ a.x < b.x + b.width &&
1313
+ a.x + a.width > b.x &&
1314
+ a.y < b.y + b.height &&
1315
+ a.y + a.height > b.y
1316
+ );
1317
+ }
1318
+
1319
+ checkPowerUpCollection() {
1320
+ const playerBounds = {
1321
+ x: this.player.x - PLAYER_WIDTH / 2,
1322
+ y: this.player.y - PLAYER_HEIGHT / 2,
1323
+ width: PLAYER_WIDTH,
1324
+ height: PLAYER_HEIGHT,
1325
+ };
1326
+
1327
+ for (const powerup of this.powerups) {
1328
+ if (!powerup.active) continue;
1329
+
1330
+ const powerupBounds = powerup.getBounds();
1331
+ if (this.intersects(playerBounds, powerupBounds)) {
1332
+ // Collected!
1333
+ powerup.destroy();
1334
+
1335
+ // Award 500 points for any pickup
1336
+ this.addScore(500);
1337
+
1338
+ if (powerup instanceof StarPowerUp) {
1339
+ // Star power - invincibility and fast shooting
1340
+ this.player.activateStarPower();
1341
+ // Golden absorb effect
1342
+ this.spawnAbsorbEffect(powerup.x, powerup.y, "#ffd700");
1343
+ } else {
1344
+ // 1-Up - extra life
1345
+ this.lives++;
1346
+ // Green absorb effect - particles fly toward player
1347
+ this.spawnAbsorbEffect(powerup.x, powerup.y, "#98fb98");
1348
+ }
1349
+ }
1350
+ }
1351
+ }
1352
+
1353
+ /**
1354
+ * Add score with bonus life check every 5000 points
1355
+ */
1356
+ addScore(points) {
1357
+ const oldMilestone = Math.floor(this.score / 5000);
1358
+ this.score += points;
1359
+ const newMilestone = Math.floor(this.score / 5000);
1360
+
1361
+ // Award bonus life for every 5000 point milestone crossed
1362
+ if (newMilestone > oldMilestone) {
1363
+ const livesEarned = newMilestone - oldMilestone;
1364
+ this.lives += livesEarned;
1365
+
1366
+ // Show 1UP notification
1367
+ this.hud.showMessage("BONUS LIFE!", 1.0);
1368
+
1369
+ // Play 1up sound
1370
+ if (this.soundEnabled) {
1371
+ Sound.beep(880, 0.1, { volume: 0.3, type: "square" });
1372
+ setTimeout(() => Sound.beep(1100, 0.15, { volume: 0.3, type: "square" }), 100);
1373
+ }
1374
+ }
1375
+ }
1376
+
1377
+ playerHit() {
1378
+ // Invincible during star power!
1379
+ if (this.player.starPower) {
1380
+ // Still show a small effect to indicate hit was blocked
1381
+ this.spawnExplosion(this.player.x, this.player.y, "#ffd700");
1382
+ return;
1383
+ }
1384
+
1385
+ // Shield blocks damage!
1386
+ if (this.player.isShielded()) {
1387
+ // Show cyan shield impact effect
1388
+ this.spawnExplosion(this.player.x, this.player.y - 10, "#00ddff");
1389
+ if (this.soundEnabled) Sound.beep(800, 0.1, { volume: 0.3, type: "sine" });
1390
+ return;
1391
+ }
1392
+
1393
+ this.lives--;
1394
+ this.spawnExplosion(this.player.x, this.player.y, "#ff0000", true);
1395
+
1396
+ // Play hurt sound
1397
+ if (this.soundEnabled) Sound.hurt(0.8);
1398
+
1399
+ if (this.lives <= 0) {
1400
+ this.gameOver();
1401
+ } else {
1402
+ // Brief invulnerability flash
1403
+ this.player.opacity = 0.5;
1404
+ setTimeout(() => {
1405
+ if (this.player) this.player.opacity = 1;
1406
+ }, 1000);
1407
+ }
1408
+ }
1409
+
1410
+ getAliveAliens() {
1411
+ return this.aliens.filter((a) => a.active);
1412
+ }
1413
+
1414
+ cleanup() {
1415
+ // Remove inactive bullets
1416
+ this.bullets = this.bullets.filter((b) => {
1417
+ if (!b.active) {
1418
+ this.pipeline.remove(b);
1419
+ return false;
1420
+ }
1421
+ return true;
1422
+ });
1423
+
1424
+ // Remove dead aliens (important for performance!)
1425
+ this.aliens = this.aliens.filter((a) => {
1426
+ if (!a.active) {
1427
+ this.pipeline.remove(a);
1428
+ return false;
1429
+ }
1430
+ return true;
1431
+ });
1432
+
1433
+ // Remove finished explosions
1434
+ this.explosions = this.explosions.filter((e) => {
1435
+ if (!e.active) {
1436
+ this.pipeline.remove(e);
1437
+ // Clear particles to free memory
1438
+ if (e.particles) e.particles = [];
1439
+ return false;
1440
+ }
1441
+ return true;
1442
+ });
1443
+
1444
+ // Remove collected/missed power-ups
1445
+ this.powerups = this.powerups.filter((p) => {
1446
+ if (!p.active) {
1447
+ this.pipeline.remove(p);
1448
+ return false;
1449
+ }
1450
+ return true;
1451
+ });
1452
+
1453
+ // Remove finished/destroyed missiles
1454
+ this.missiles = this.missiles.filter((m) => {
1455
+ if (!m.active) {
1456
+ this.pipeline.remove(m);
1457
+ return false;
1458
+ }
1459
+ return true;
1460
+ });
1461
+
1462
+ // Remove finished laser beams
1463
+ this.laserBeams = this.laserBeams.filter((l) => {
1464
+ if (!l.active) {
1465
+ this.pipeline.remove(l);
1466
+ return false;
1467
+ }
1468
+ return true;
1469
+ });
1470
+
1471
+ // Remove finished lightning
1472
+ this.lightnings = this.lightnings.filter((l) => {
1473
+ if (!l.active) {
1474
+ this.pipeline.remove(l);
1475
+ return false;
1476
+ }
1477
+ return true;
1478
+ });
1479
+ }
1480
+
1481
+ async startPlaying() {
1482
+ // Resume audio context on user interaction (required by browser autoplay policy)
1483
+ await this.resumeAudio();
1484
+
1485
+ // Hide play button and start countdown
1486
+ if (this.playButton) {
1487
+ this.pipeline.remove(this.playButton);
1488
+ this.playButton = null;
1489
+ }
1490
+ this.hud.hideMessage();
1491
+
1492
+ // Start countdown
1493
+ this.countdownValue = 3;
1494
+ this.countdownTimer = 0;
1495
+ this.countdownText.text = "3";
1496
+ this.gameState = "countdown";
1497
+
1498
+ // Play first countdown beep
1499
+ if (this.soundEnabled) Sound.beep(880, 0.15, { volume: 0.4 });
1500
+ }
1501
+
1502
+ /**
1503
+ * Called when countdown finishes - actually start gameplay
1504
+ */
1505
+ beginGameplay() {
1506
+ this.countdownText.text = "";
1507
+ this.gameState = "playing";
1508
+ this.levelPlayTime = 0; // Reset level timer for missile escalation
1509
+
1510
+ // Play start sound (higher pitch for "GO!")
1511
+ if (this.soundEnabled) Sound.beep(1320, 0.3, { volume: 0.5 });
1512
+ }
1513
+
1514
+ gameOver() {
1515
+ this.gameState = "gameover";
1516
+ this.hud.showMessage(`GAME OVER\n\nScore: ${this.score}\nLevel: ${this.level}`);
1517
+ this.player.visible = false;
1518
+
1519
+ // Play game over sound
1520
+ if (this.soundEnabled) Sound.lose();
1521
+ // Show play again button
1522
+ this.playButton = new Button(this, {
1523
+ x: this.width / 2,
1524
+ y: this.height / 2 + 100,
1525
+ width: 200,
1526
+ height: 60,
1527
+ text: "PLAY AGAIN",
1528
+ font: "bold 20px monospace",
1529
+ colorDefaultBg: "#330000",
1530
+ colorDefaultStroke: "#ff0000",
1531
+ colorDefaultText: "#ff0000",
1532
+ colorHoverBg: "#440000",
1533
+ colorHoverStroke: "#ff4444",
1534
+ colorHoverText: "#ff4444",
1535
+ colorPressedBg: "#220000",
1536
+ colorPressedStroke: "#aa0000",
1537
+ colorPressedText: "#aa0000",
1538
+ onClick: () => this.restart(),
1539
+ });
1540
+ this.pipeline.add(this.playButton);
1541
+ }
1542
+
1543
+ levelComplete() {
1544
+ this.gameState = "levelcomplete";
1545
+ this.levelTransitionTimer = 0;
1546
+
1547
+ // Bonus points for completing level
1548
+ const levelBonus = this.level * 100;
1549
+ this.addScore(levelBonus);
1550
+
1551
+ // Apply upgrades after specific levels
1552
+ let upgradeMessage = "";
1553
+ switch (this.level) {
1554
+ case 3: // After first boss
1555
+ this.player.applyUpgrade("speed1");
1556
+ upgradeMessage = "\nSPEED UP!";
1557
+ break;
1558
+ case 4:
1559
+ this.player.applyUpgrade("firerate1");
1560
+ upgradeMessage = "\nRAPID FIRE!";
1561
+ break;
1562
+ case 5:
1563
+ this.player.applyUpgrade("speed2");
1564
+ upgradeMessage = "\nHYPER SPEED!";
1565
+ break;
1566
+ case 6: // After second boss
1567
+ this.player.applyUpgrade("tripleshot");
1568
+ upgradeMessage = "\nTRIPLE SHOT!";
1569
+ break;
1570
+ case 9: // After third boss - shield for gauntlet
1571
+ this.player.applyUpgrade("shield");
1572
+ upgradeMessage = "\nSHIELD! [SHIFT]";
1573
+ break;
1574
+ }
1575
+
1576
+ // Show level complete message (not used for gauntlet - that uses advanceGauntlet)
1577
+ this.hud.showMessage(`LEVEL ${this.level} COMPLETE!\n+${levelBonus}${upgradeMessage}`);
1578
+ }
1579
+
1580
+ prepareNextLevel() {
1581
+ // Called when ship flies off - set up the next level
1582
+ this.alienDirection = 1;
1583
+ this.alienMoveTimer = 0;
1584
+ this.levelPlayTime = 0; // Reset level timer for missile escalation
1585
+
1586
+ // Clear any remaining bullets
1587
+ for (const bullet of this.bullets) {
1588
+ this.pipeline.remove(bullet);
1589
+ }
1590
+ this.bullets = [];
1591
+
1592
+ // Clear explosions
1593
+ for (const explosion of this.explosions) {
1594
+ this.pipeline.remove(explosion);
1595
+ }
1596
+ this.explosions = [];
1597
+
1598
+ // Clear power-ups
1599
+ for (const powerup of this.powerups) {
1600
+ this.pipeline.remove(powerup);
1601
+ }
1602
+ this.powerups = [];
1603
+
1604
+ // Clear missiles
1605
+ for (const missile of this.missiles) {
1606
+ this.pipeline.remove(missile);
1607
+ }
1608
+ this.missiles = [];
1609
+
1610
+ // Clear laser beams
1611
+ for (const laser of this.laserBeams) {
1612
+ this.pipeline.remove(laser);
1613
+ }
1614
+ this.laserBeams = [];
1615
+
1616
+ // Clear lightning
1617
+ for (const lightning of this.lightnings) {
1618
+ this.pipeline.remove(lightning);
1619
+ }
1620
+ this.lightnings = [];
1621
+
1622
+ // Clear minions
1623
+ for (const minion of this.minions) {
1624
+ this.pipeline.remove(minion);
1625
+ }
1626
+ this.minions = [];
1627
+
1628
+ // Clear boss if present
1629
+ if (this.boss) {
1630
+ this.pipeline.remove(this.boss);
1631
+ this.boss = null;
1632
+ }
1633
+
1634
+ // Reset player horizontal position (y is animated)
1635
+ this.player.x = this.width / 2;
1636
+ this.player.canShoot = true;
1637
+ // Keep star power through level transitions (reward!)
1638
+
1639
+ // Check if entering gauntlet mode from level 9
1640
+ if (this.level === 9 && !this.isGauntletMode) {
1641
+ // Entering gauntlet mode (level 10) - seamless entry
1642
+ this.level = 10;
1643
+ this.isGauntletMode = true;
1644
+ this.gauntletPhase = 0;
1645
+ this.spawnGauntletWave();
1646
+ // Override gameState since we're now in gauntlet playing mode
1647
+ // (the flyin state will be set after this, which is fine for first wave)
1648
+ } else {
1649
+ // Normal level progression
1650
+ this.level++;
1651
+ this.spawnAliens();
1652
+ }
1653
+ }
1654
+
1655
+ /**
1656
+ * Spawn a gauntlet wave - powered up aliens at level 10 difficulty
1657
+ */
1658
+ spawnGauntletWave() {
1659
+ // Clear existing aliens
1660
+ for (const alien of this.aliens) {
1661
+ this.pipeline.remove(alien);
1662
+ }
1663
+ this.aliens = [];
1664
+
1665
+ // Gauntlet uses max rows (8) with level 10 speed
1666
+ const alienRows = MAX_ALIEN_ROWS;
1667
+
1668
+ // Calculate starting position to center the alien grid
1669
+ const gridWidth = ALIEN_COLS * ALIEN_SPACING_X;
1670
+ const startX = (this.width - gridWidth) / 2 + ALIEN_SPACING_X / 2;
1671
+ const startY = this.levelStartY + 50; // Start a bit lower
1672
+
1673
+ for (let row = 0; row < alienRows; row++) {
1674
+ for (let col = 0; col < ALIEN_COLS; col++) {
1675
+ // Assign row type - distribute all types across rows
1676
+ let rowType;
1677
+ if (row < 2) {
1678
+ rowType = 0; // Top 2 rows = squid (30pts)
1679
+ } else if (row < 5) {
1680
+ rowType = 1; // Middle rows = crab (20pts)
1681
+ } else {
1682
+ rowType = 3; // Bottom rows = octopus (10pts)
1683
+ }
1684
+
1685
+ const alien = new Alien(this, {
1686
+ x: startX + col * ALIEN_SPACING_X,
1687
+ y: startY + row * ALIEN_SPACING_Y,
1688
+ row: rowType,
1689
+ col: col,
1690
+ });
1691
+ this.aliens.push(alien);
1692
+ this.pipeline.add(alien);
1693
+ }
1694
+ }
1695
+
1696
+ // Level 10 speed - very fast
1697
+ const levelSpeedMultiplier = Math.pow(1.15, 9); // Same as level 10
1698
+ this.baseMoveInterval = Math.max(0.3, 1 / levelSpeedMultiplier);
1699
+ this.alienMoveInterval = this.baseMoveInterval;
1700
+ }
1701
+
1702
+ /**
1703
+ * Spawn a gauntlet boss - larger and different colored
1704
+ */
1705
+ spawnGauntletBoss(bossIndex) {
1706
+ this.boss = new Boss(this, {
1707
+ x: this.width / 2,
1708
+ y: -150, // Start above screen (larger boss needs more space)
1709
+ bossType: bossIndex,
1710
+ targetY: 200, // Slightly lower for larger boss
1711
+ isGauntlet: true, // Enable gauntlet mode (larger, different colors)
1712
+ });
1713
+ this.pipeline.add(this.boss);
1714
+
1715
+ // Epic warning sound for gauntlet boss
1716
+ if (this.soundEnabled) {
1717
+ Sound.beep(150, 0.4, { volume: 0.4, type: "sawtooth" });
1718
+ setTimeout(() => Sound.beep(120, 0.4, { volume: 0.4, type: "sawtooth" }), 400);
1719
+ setTimeout(() => Sound.beep(80, 0.6, { volume: 0.5, type: "sawtooth" }), 800);
1720
+ }
1721
+ }
1722
+
1723
+ /**
1724
+ * Advance gauntlet to next phase seamlessly (no interruptions)
1725
+ * Called when a wave or boss is defeated in gauntlet mode
1726
+ */
1727
+ advanceGauntlet() {
1728
+ // Award bonus points for clearing the phase
1729
+ const phaseBonus = 500 + (this.gauntletPhase * 200); // 500, 700, 900, 1100, 1300, 1500
1730
+ this.addScore(phaseBonus);
1731
+
1732
+ // Advance to next phase
1733
+ this.gauntletPhase++;
1734
+ this.levelPlayTime = 0;
1735
+
1736
+ // Clear bullets, missiles, powerups between phases
1737
+ for (const bullet of this.bullets) {
1738
+ this.pipeline.remove(bullet);
1739
+ }
1740
+ this.bullets = [];
1741
+
1742
+ for (const missile of this.missiles) {
1743
+ this.pipeline.remove(missile);
1744
+ }
1745
+ this.missiles = [];
1746
+
1747
+ // Check if gauntlet complete (phase 6 = after all 3 waves and 3 bosses)
1748
+ if (this.gauntletPhase >= 6) {
1749
+ this.win();
1750
+ return;
1751
+ }
1752
+
1753
+ // Determine what's next based on phase
1754
+ // Phases: 0=wave1, 1=boss1, 2=wave2, 3=boss2, 4=wave3, 5=boss3
1755
+ if (this.gauntletPhase % 2 === 0) {
1756
+ // Even phases (0, 2, 4) = waves - spawn immediately
1757
+ this.gameState = "playing";
1758
+ this.spawnGauntletWave();
1759
+ } else {
1760
+ // Odd phases (1, 3, 5) = bosses - spawn immediately
1761
+ this.gameState = "bossfight";
1762
+ const bossIndex = Math.floor(this.gauntletPhase / 2);
1763
+ this.spawnGauntletBoss(bossIndex);
1764
+ }
1765
+ }
1766
+
1767
+ win() {
1768
+ // Victory! Player beat the gauntlet
1769
+ this.gameState = "win";
1770
+
1771
+ // Play victory fanfare
1772
+ if (this.soundEnabled) Sound.win();
1773
+
1774
+ // Victory message (keep it short for small screens)
1775
+ this.hud.showMessage(`VICTORY!\n\nGAUNTLET COMPLETE!\n\nScore: ${this.score}`);
1776
+
1777
+ // Show play again button
1778
+ this.playButton = new Button(this, {
1779
+ x: this.width / 2,
1780
+ y: this.height / 2 + 120,
1781
+ width: 200,
1782
+ height: 60,
1783
+ text: "PLAY AGAIN",
1784
+ font: "bold 20px monospace",
1785
+ colorDefaultBg: "#333300",
1786
+ colorDefaultStroke: "#ffff00",
1787
+ colorDefaultText: "#ffff00",
1788
+ colorHoverBg: "#444400",
1789
+ colorHoverStroke: "#ffff44",
1790
+ colorHoverText: "#ffff44",
1791
+ colorPressedBg: "#222200",
1792
+ colorPressedStroke: "#aaaa00",
1793
+ colorPressedText: "#aaaa00",
1794
+ onClick: () => this.restart(),
1795
+ });
1796
+ this.pipeline.add(this.playButton);
1797
+ }
1798
+
1799
+ async restart() {
1800
+ // Resume audio context on user interaction
1801
+ await this.resumeAudio();
1802
+
1803
+ // Remove play again button if present
1804
+ if (this.playButton) {
1805
+ this.pipeline.remove(this.playButton);
1806
+ this.playButton = null;
1807
+ }
1808
+
1809
+ // Reset game state
1810
+ this.score = 0;
1811
+ this.lives = 3;
1812
+ this.level = 1;
1813
+ this.alienDirection = 1;
1814
+ this.alienMoveTimer = 0;
1815
+ this.alienMoveInterval = 1;
1816
+ this.alienMoveNote = 0;
1817
+ this.baseMoveInterval = 1;
1818
+ this.alienMoveInterval = this.baseMoveInterval;
1819
+ this.hud.hideMessage();
1820
+
1821
+ // Reset gauntlet state
1822
+ this.isGauntletMode = false;
1823
+ this.gauntletPhase = 0;
1824
+
1825
+ // Start countdown
1826
+ this.countdownValue = 3;
1827
+ this.countdownTimer = 0;
1828
+ this.countdownText.text = "3";
1829
+ this.gameState = "countdown";
1830
+
1831
+ // Play first countdown beep
1832
+ if (this.soundEnabled) Sound.beep(880, 0.15, { volume: 0.4 });
1833
+
1834
+ // Clear bullets and explosions
1835
+ for (const bullet of this.bullets) {
1836
+ this.pipeline.remove(bullet);
1837
+ }
1838
+ this.bullets = [];
1839
+
1840
+ for (const explosion of this.explosions) {
1841
+ this.pipeline.remove(explosion);
1842
+ }
1843
+ this.explosions = [];
1844
+
1845
+ // Clear power-ups
1846
+ for (const powerup of this.powerups) {
1847
+ this.pipeline.remove(powerup);
1848
+ }
1849
+ this.powerups = [];
1850
+
1851
+ // Clear missiles
1852
+ for (const missile of this.missiles) {
1853
+ this.pipeline.remove(missile);
1854
+ }
1855
+ this.missiles = [];
1856
+
1857
+ // Clear minions
1858
+ for (const minion of this.minions) {
1859
+ this.pipeline.remove(minion);
1860
+ }
1861
+ this.minions = [];
1862
+
1863
+ // Clear boss if present
1864
+ if (this.boss) {
1865
+ this.pipeline.remove(this.boss);
1866
+ this.boss = null;
1867
+ }
1868
+
1869
+ // Reset player position and state
1870
+ this.player.x = this.width / 2;
1871
+ this.player.y = this.height - 90;
1872
+ this.player.visible = true;
1873
+ this.player.opacity = 1;
1874
+ this.player.canShoot = true;
1875
+ // Reset star power on full restart
1876
+ this.player.starPower = false;
1877
+ this.player.starPowerTimer = 0;
1878
+ // Reset all upgrades and ship colors
1879
+ this.player.resetUpgrades();
1880
+
1881
+ // Respawn aliens
1882
+ this.spawnAliens();
1883
+ }
1884
+ }