@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,112 @@
1
+ import { GameObject, TextShape } from "../../../src/index.js";
2
+
3
+ export class HUD extends GameObject {
4
+ constructor(game, options = {}) {
5
+ super(game, options);
6
+
7
+ // Title - centered, below info bar
8
+ this.titleText = new TextShape("SPACE INVADERS", {
9
+ font: "bold 32px monospace",
10
+ color: "#ffff00",
11
+ align: "center",
12
+ baseline: "top",
13
+ });
14
+
15
+ // Score - top left
16
+ this.scoreText = new TextShape("SCORE: 0", {
17
+ font: "20px monospace",
18
+ color: "#ffffff",
19
+ align: "left",
20
+ baseline: "top",
21
+ });
22
+
23
+ // Level - top right
24
+ this.levelText = new TextShape("LEVEL: 1", {
25
+ font: "20px monospace",
26
+ color: "#00ffff",
27
+ align: "right",
28
+ baseline: "top",
29
+ });
30
+
31
+ // Lives - bottom left (above FPS)
32
+ this.livesText = new TextShape("LIVES: 3", {
33
+ font: "18px monospace",
34
+ color: "#00ff00",
35
+ align: "left",
36
+ baseline: "bottom",
37
+ });
38
+
39
+ // Center message (font size set dynamically based on screen width)
40
+ this.messageText = new TextShape("", {
41
+ font: "20px monospace",
42
+ color: "#ffff00",
43
+ align: "center",
44
+ baseline: "middle",
45
+ });
46
+
47
+ // Message auto-hide timer
48
+ this.messageTimer = 0;
49
+ this.messageDuration = 0;
50
+ }
51
+
52
+ update(dt) {
53
+ super.update(dt);
54
+ this.scoreText.text = `SCORE: ${this.game.score}`;
55
+ this.levelText.text = `LEVEL: ${this.game.level}`;
56
+ this.livesText.text = `LIVES: ${this.game.lives}`;
57
+
58
+ // Auto-hide message after duration
59
+ if (this.messageDuration > 0 && this.messageText.text) {
60
+ this.messageTimer += dt;
61
+ if (this.messageTimer >= this.messageDuration) {
62
+ this.hideMessage();
63
+ }
64
+ }
65
+ }
66
+
67
+ draw() {
68
+ super.draw();
69
+
70
+ // Title (centered, 100px from top to account for info bar)
71
+ this.titleText.x = this.game.width / 2;
72
+ this.titleText.y = 100;
73
+ this.titleText.render();
74
+
75
+ // Score (top left, below title area)
76
+ this.scoreText.x = 20;
77
+ this.scoreText.y = 140;
78
+ this.scoreText.render();
79
+
80
+ // Level (top right, below title area)
81
+ this.levelText.x = this.game.width - 20;
82
+ this.levelText.y = 140;
83
+ this.levelText.render();
84
+
85
+ // Lives (bottom left, above FPS counter)
86
+ this.livesText.x = 20;
87
+ this.livesText.y = this.game.height - 40;
88
+ this.livesText.render();
89
+
90
+ // Center message (scale font based on screen width)
91
+ if (this.messageText.text) {
92
+ // Scale font: 20px at 800px width, scales proportionally (min 14px, max 24px)
93
+ const fontSize = Math.max(14, Math.min(24, Math.floor(this.game.width / 40)));
94
+ this.messageText.font = `${fontSize}px monospace`;
95
+ this.messageText.x = this.game.width / 2;
96
+ this.messageText.y = this.game.height / 2;
97
+ this.messageText.render();
98
+ }
99
+ }
100
+
101
+ showMessage(text, duration = 0) {
102
+ this.messageText.text = text;
103
+ this.messageTimer = 0;
104
+ this.messageDuration = duration; // 0 = permanent until hideMessage called
105
+ }
106
+
107
+ hideMessage() {
108
+ this.messageText.text = "";
109
+ this.messageTimer = 0;
110
+ this.messageDuration = 0;
111
+ }
112
+ }
@@ -0,0 +1,179 @@
1
+ import { GameObject, Painter } from "../../../src/index.js";
2
+
3
+ /**
4
+ * LaserBeam - Area denial obstacle
5
+ *
6
+ * Phases:
7
+ * 1. Warning (0.3s): Thin green line appears
8
+ * 2. Charging (0.2s): Line grows wider, turns white
9
+ * 3. Active (0.15s): Full width, damages player
10
+ * 4. Fade (0.2s): Fades out
11
+ */
12
+ export class LaserBeam extends GameObject {
13
+ constructor(game, options = {}) {
14
+ super(game, {
15
+ width: 1,
16
+ height: game.height,
17
+ ...options,
18
+ });
19
+
20
+ // Position - random X across screen
21
+ this.x = options.x ?? (50 + Math.random() * (game.width - 100));
22
+ this.y = game.height / 2;
23
+
24
+ // Timing
25
+ this.warningDuration = 0.3;
26
+ this.chargeDuration = 0.2;
27
+ this.activeDuration = 0.4; // Longer damage window
28
+ this.fadeDuration = 0.2;
29
+ this.totalDuration = this.warningDuration + this.chargeDuration + this.activeDuration + this.fadeDuration;
30
+
31
+ this.elapsedTime = 0;
32
+ this.phase = "warning"; // warning, charging, active, fade
33
+
34
+ // Visual properties
35
+ this.maxWidth = 60; // Wider beam during active phase
36
+ this.currentWidth = 1;
37
+ this.opacity = 1;
38
+ this.canDamage = false; // Only damages during active phase
39
+ }
40
+
41
+ update(dt) {
42
+ super.update(dt);
43
+
44
+ this.elapsedTime += dt;
45
+
46
+ // Determine phase and properties
47
+ if (this.elapsedTime < this.warningDuration) {
48
+ // Warning phase - thin green line
49
+ this.phase = "warning";
50
+ this.currentWidth = 1;
51
+ this.canDamage = false;
52
+ } else if (this.elapsedTime < this.warningDuration + this.chargeDuration) {
53
+ // Charging phase - grows wider, turns white
54
+ this.phase = "charging";
55
+ const chargeProgress = (this.elapsedTime - this.warningDuration) / this.chargeDuration;
56
+ this.currentWidth = 1 + (this.maxWidth - 1) * chargeProgress;
57
+ this.canDamage = false;
58
+ } else if (this.elapsedTime < this.warningDuration + this.chargeDuration + this.activeDuration) {
59
+ // Active phase - full width, damages
60
+ this.phase = "active";
61
+ this.currentWidth = this.maxWidth;
62
+ this.canDamage = true;
63
+ } else if (this.elapsedTime < this.totalDuration) {
64
+ // Fade phase
65
+ this.phase = "fade";
66
+ const fadeProgress = (this.elapsedTime - this.warningDuration - this.chargeDuration - this.activeDuration) / this.fadeDuration;
67
+ this.opacity = 1 - fadeProgress;
68
+ this.currentWidth = this.maxWidth * (1 - fadeProgress * 0.5); // Shrink slightly
69
+ this.canDamage = false;
70
+ } else {
71
+ // Done
72
+ this.destroy();
73
+ }
74
+ }
75
+
76
+ draw() {
77
+ if (!this.visible) return;
78
+ super.draw();
79
+
80
+ const ctx = Painter.ctx;
81
+ ctx.save();
82
+
83
+ // Reset transform since we're drawing in screen space
84
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
85
+
86
+ const halfWidth = this.currentWidth / 2;
87
+
88
+ if (this.phase === "warning") {
89
+ // Thin green warning line
90
+ ctx.strokeStyle = `rgba(0, 255, 0, 0.8)`;
91
+ ctx.lineWidth = 1;
92
+ ctx.beginPath();
93
+ ctx.moveTo(this.x, 0);
94
+ ctx.lineTo(this.x, this.game.height);
95
+ ctx.stroke();
96
+
97
+ // Flickering effect
98
+ if (Math.sin(this.elapsedTime * 30) > 0) {
99
+ ctx.strokeStyle = `rgba(100, 255, 100, 0.4)`;
100
+ ctx.lineWidth = 3;
101
+ ctx.stroke();
102
+ }
103
+ } else if (this.phase === "charging") {
104
+ // Growing white beam with green core
105
+ const chargeProgress = (this.elapsedTime - this.warningDuration) / this.chargeDuration;
106
+
107
+ // Outer glow
108
+ const gradient = ctx.createLinearGradient(this.x - halfWidth, 0, this.x + halfWidth, 0);
109
+ gradient.addColorStop(0, `rgba(255, 255, 255, 0)`);
110
+ gradient.addColorStop(0.3, `rgba(200, 255, 200, ${0.3 * chargeProgress})`);
111
+ gradient.addColorStop(0.5, `rgba(255, 255, 255, ${0.6 * chargeProgress})`);
112
+ gradient.addColorStop(0.7, `rgba(200, 255, 200, ${0.3 * chargeProgress})`);
113
+ gradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
114
+
115
+ ctx.fillStyle = gradient;
116
+ ctx.fillRect(this.x - halfWidth, 0, this.currentWidth, this.game.height);
117
+
118
+ // Core line
119
+ ctx.strokeStyle = `rgba(150, 255, 150, ${0.5 + chargeProgress * 0.5})`;
120
+ ctx.lineWidth = 2;
121
+ ctx.beginPath();
122
+ ctx.moveTo(this.x, 0);
123
+ ctx.lineTo(this.x, this.game.height);
124
+ ctx.stroke();
125
+ } else if (this.phase === "active") {
126
+ // Full deadly beam - bright white with slight transparency
127
+ const gradient = ctx.createLinearGradient(this.x - halfWidth, 0, this.x + halfWidth, 0);
128
+ gradient.addColorStop(0, `rgba(255, 255, 255, 0)`);
129
+ gradient.addColorStop(0.2, `rgba(255, 255, 255, 0.3)`);
130
+ gradient.addColorStop(0.4, `rgba(255, 255, 255, 0.7)`);
131
+ gradient.addColorStop(0.5, `rgba(255, 255, 255, 0.9)`);
132
+ gradient.addColorStop(0.6, `rgba(255, 255, 255, 0.7)`);
133
+ gradient.addColorStop(0.8, `rgba(255, 255, 255, 0.3)`);
134
+ gradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
135
+
136
+ ctx.fillStyle = gradient;
137
+ ctx.fillRect(this.x - halfWidth, 0, this.currentWidth, this.game.height);
138
+
139
+ // Bright core
140
+ ctx.strokeStyle = `rgba(255, 255, 255, 1)`;
141
+ ctx.lineWidth = 3;
142
+ ctx.beginPath();
143
+ ctx.moveTo(this.x, 0);
144
+ ctx.lineTo(this.x, this.game.height);
145
+ ctx.stroke();
146
+ } else if (this.phase === "fade") {
147
+ // Fading out
148
+ const gradient = ctx.createLinearGradient(this.x - halfWidth, 0, this.x + halfWidth, 0);
149
+ gradient.addColorStop(0, `rgba(255, 255, 255, 0)`);
150
+ gradient.addColorStop(0.3, `rgba(200, 255, 200, ${0.2 * this.opacity})`);
151
+ gradient.addColorStop(0.5, `rgba(255, 255, 255, ${0.5 * this.opacity})`);
152
+ gradient.addColorStop(0.7, `rgba(200, 255, 200, ${0.2 * this.opacity})`);
153
+ gradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
154
+
155
+ ctx.fillStyle = gradient;
156
+ ctx.fillRect(this.x - halfWidth, 0, this.currentWidth, this.game.height);
157
+ }
158
+
159
+ ctx.restore();
160
+ }
161
+
162
+ destroy() {
163
+ this.active = false;
164
+ this.visible = false;
165
+ }
166
+
167
+ getBounds() {
168
+ // Only return bounds during active phase (when it can damage)
169
+ if (!this.canDamage) {
170
+ return { x: -1000, y: -1000, width: 0, height: 0 }; // Off-screen, no collision
171
+ }
172
+ return {
173
+ x: this.x - this.currentWidth / 2,
174
+ y: 0,
175
+ width: this.currentWidth,
176
+ height: this.game.height,
177
+ };
178
+ }
179
+ }
@@ -0,0 +1,277 @@
1
+ import { GameObject, Painter } from "../../../src/index.js";
2
+
3
+ /**
4
+ * Lightning - Animated branching lightning strike obstacle
5
+ *
6
+ * Phases:
7
+ * 1. Tracing (0.4s): Bolt draws itself downward, branches split as trace reaches them
8
+ * 2. Active (0.3s): Full bolt visible, bright flash, damages player
9
+ * 3. Fade (0.2s): Opacity fades out
10
+ *
11
+ * Unlocked after defeating boss 2 (level 7+)
12
+ */
13
+ export class Lightning extends GameObject {
14
+ constructor(game, options = {}) {
15
+ super(game, {
16
+ width: game.width,
17
+ height: game.height,
18
+ ...options,
19
+ });
20
+
21
+ // Start at top center with slight variance
22
+ this.startX = options.x ?? game.width / 2 + (Math.random() - 0.5) * 100;
23
+
24
+ // Generate full lightning tree at spawn (2-4 branches total)
25
+ this.maxBranches = 2 + Math.floor(Math.random() * 3);
26
+ this.segments = []; // All line segments: [{x1, y1, x2, y2, branch}]
27
+ this.generateLightning();
28
+
29
+ // Animation
30
+ this.progress = 0;
31
+ this.traceSpeed = 2.5; // Complete trace in ~0.4s
32
+ this.phase = "tracing"; // tracing -> active -> fade
33
+
34
+ this.activeDuration = 0.3;
35
+ this.fadeDuration = 0.2;
36
+ this.activeTimer = 0;
37
+ this.fadeTimer = 0;
38
+ this.opacity = 1;
39
+
40
+ this.canDamage = false;
41
+ this.hasHitPlayer = false;
42
+ }
43
+
44
+ generateLightning() {
45
+ // Build main trunk + branches as flat array of segments
46
+ let x = this.startX;
47
+ let y = 0;
48
+ const segmentHeight = 50;
49
+ let branchCount = 0;
50
+
51
+ // Main trunk - jagged path from top to bottom
52
+ while (y < this.game.height) {
53
+ const nextY = Math.min(y + segmentHeight, this.game.height);
54
+ const jitter = (Math.random() - 0.5) * 50;
55
+ const nextX = Math.max(30, Math.min(this.game.width - 30, x + jitter));
56
+
57
+ this.segments.push({ x1: x, y1: y, x2: nextX, y2: nextY, branch: 0 });
58
+
59
+ // Chance to spawn a branch (not too early, not too late)
60
+ if (y > 100 && y < this.game.height - 200 && Math.random() < 0.35 && branchCount < this.maxBranches - 1) {
61
+ branchCount++;
62
+ this.generateBranch(nextX, nextY, branchCount);
63
+ }
64
+
65
+ x = nextX;
66
+ y = nextY;
67
+ }
68
+ }
69
+
70
+ generateBranch(startX, startY, branchId) {
71
+ // Branch goes diagonally outward
72
+ const direction = Math.random() > 0.5 ? 1 : -1;
73
+ let x = startX;
74
+ let y = startY;
75
+ const segmentHeight = 50;
76
+
77
+ // Branch is shorter than main trunk (3-6 segments)
78
+ const branchLength = 3 + Math.floor(Math.random() * 4);
79
+
80
+ for (let i = 0; i < branchLength && y < this.game.height; i++) {
81
+ const nextY = Math.min(y + segmentHeight, this.game.height);
82
+ const drift = direction * (20 + Math.random() * 30);
83
+ const jitter = (Math.random() - 0.5) * 30;
84
+ const nextX = Math.max(30, Math.min(this.game.width - 30, x + drift + jitter));
85
+
86
+ this.segments.push({ x1: x, y1: y, x2: nextX, y2: nextY, branch: branchId });
87
+
88
+ x = nextX;
89
+ y = nextY;
90
+ }
91
+ }
92
+
93
+ update(dt) {
94
+ super.update(dt);
95
+
96
+ if (this.phase === "tracing") {
97
+ this.progress += dt * this.traceSpeed;
98
+ if (this.progress >= 1) {
99
+ this.progress = 1;
100
+ this.phase = "active";
101
+ this.canDamage = true;
102
+ }
103
+ } else if (this.phase === "active") {
104
+ this.activeTimer += dt;
105
+ if (this.activeTimer >= this.activeDuration) {
106
+ this.phase = "fade";
107
+ this.canDamage = false;
108
+ }
109
+ } else if (this.phase === "fade") {
110
+ this.fadeTimer += dt;
111
+ this.opacity = 1 - this.fadeTimer / this.fadeDuration;
112
+ if (this.fadeTimer >= this.fadeDuration) {
113
+ this.destroy();
114
+ }
115
+ }
116
+ }
117
+
118
+ draw() {
119
+ if (!this.visible) return;
120
+ super.draw();
121
+
122
+ const ctx = Painter.ctx;
123
+ ctx.save();
124
+
125
+ // Reset transform since we're drawing in screen space
126
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
127
+
128
+ // How far down has the trace reached?
129
+ const currentTraceY = this.progress * this.game.height;
130
+
131
+ for (const seg of this.segments) {
132
+ // Skip segments not yet reached by the trace
133
+ if (seg.y1 > currentTraceY) continue;
134
+
135
+ // Calculate how much of this segment to draw
136
+ let drawX2 = seg.x2;
137
+ let drawY2 = seg.y2;
138
+
139
+ if (seg.y2 > currentTraceY) {
140
+ // Partial segment - interpolate to current trace position
141
+ const t = (currentTraceY - seg.y1) / (seg.y2 - seg.y1);
142
+ drawX2 = seg.x1 + (seg.x2 - seg.x1) * t;
143
+ drawY2 = currentTraceY;
144
+ }
145
+
146
+ this.drawSegment(ctx, seg.x1, seg.y1, drawX2, drawY2, seg.branch);
147
+ }
148
+
149
+ ctx.restore();
150
+ }
151
+
152
+ drawSegment(ctx, x1, y1, x2, y2, branch) {
153
+ const isBranch = branch > 0;
154
+
155
+ if (this.phase === "tracing") {
156
+ // Tracing phase - cyan/purple electric glow
157
+ // Outer glow
158
+ ctx.strokeStyle = `rgba(100, 150, 255, ${0.4 * this.opacity})`;
159
+ ctx.lineWidth = isBranch ? 8 : 12;
160
+ ctx.lineCap = "round";
161
+ ctx.beginPath();
162
+ ctx.moveTo(x1, y1);
163
+ ctx.lineTo(x2, y2);
164
+ ctx.stroke();
165
+
166
+ // Inner bright line
167
+ ctx.strokeStyle = `rgba(200, 220, 255, ${0.9 * this.opacity})`;
168
+ ctx.lineWidth = isBranch ? 2 : 3;
169
+ ctx.beginPath();
170
+ ctx.moveTo(x1, y1);
171
+ ctx.lineTo(x2, y2);
172
+ ctx.stroke();
173
+
174
+ } else if (this.phase === "active") {
175
+ // Active phase - bright white flash
176
+ // Wide outer glow
177
+ ctx.strokeStyle = `rgba(150, 180, 255, ${0.6 * this.opacity})`;
178
+ ctx.lineWidth = isBranch ? 16 : 24;
179
+ ctx.lineCap = "round";
180
+ ctx.beginPath();
181
+ ctx.moveTo(x1, y1);
182
+ ctx.lineTo(x2, y2);
183
+ ctx.stroke();
184
+
185
+ // Medium glow
186
+ ctx.strokeStyle = `rgba(200, 220, 255, ${0.8 * this.opacity})`;
187
+ ctx.lineWidth = isBranch ? 8 : 12;
188
+ ctx.beginPath();
189
+ ctx.moveTo(x1, y1);
190
+ ctx.lineTo(x2, y2);
191
+ ctx.stroke();
192
+
193
+ // Bright core
194
+ ctx.strokeStyle = `rgba(255, 255, 255, ${this.opacity})`;
195
+ ctx.lineWidth = isBranch ? 3 : 4;
196
+ ctx.beginPath();
197
+ ctx.moveTo(x1, y1);
198
+ ctx.lineTo(x2, y2);
199
+ ctx.stroke();
200
+
201
+ } else if (this.phase === "fade") {
202
+ // Fade phase - decreasing opacity
203
+ ctx.strokeStyle = `rgba(150, 180, 255, ${0.4 * this.opacity})`;
204
+ ctx.lineWidth = isBranch ? 10 : 16;
205
+ ctx.lineCap = "round";
206
+ ctx.beginPath();
207
+ ctx.moveTo(x1, y1);
208
+ ctx.lineTo(x2, y2);
209
+ ctx.stroke();
210
+
211
+ ctx.strokeStyle = `rgba(255, 255, 255, ${0.7 * this.opacity})`;
212
+ ctx.lineWidth = isBranch ? 2 : 3;
213
+ ctx.beginPath();
214
+ ctx.moveTo(x1, y1);
215
+ ctx.lineTo(x2, y2);
216
+ ctx.stroke();
217
+ }
218
+ }
219
+
220
+ destroy() {
221
+ this.active = false;
222
+ this.visible = false;
223
+ }
224
+
225
+ /**
226
+ * Get bounding box for collision detection
227
+ * Returns off-screen bounds when not in damage phase
228
+ */
229
+ getBounds() {
230
+ if (!this.canDamage) {
231
+ return { x: -1000, y: -1000, width: 0, height: 0 };
232
+ }
233
+
234
+ // Return bounding box of entire lightning
235
+ const xs = this.segments.flatMap((s) => [s.x1, s.x2]);
236
+ const minX = Math.min(...xs);
237
+ const maxX = Math.max(...xs);
238
+
239
+ return {
240
+ x: minX - 15,
241
+ y: 0,
242
+ width: maxX - minX + 30,
243
+ height: this.game.height,
244
+ };
245
+ }
246
+
247
+ /**
248
+ * More precise collision check - tests player against each segment
249
+ */
250
+ checkCollision(playerBounds) {
251
+ if (!this.canDamage) return false;
252
+
253
+ const px = playerBounds.x;
254
+ const py = playerBounds.y;
255
+ const pw = playerBounds.width;
256
+ const ph = playerBounds.height;
257
+
258
+ for (const seg of this.segments) {
259
+ // Simple line-rect collision using bounding box of segment
260
+ const segMinX = Math.min(seg.x1, seg.x2) - 10;
261
+ const segMaxX = Math.max(seg.x1, seg.x2) + 10;
262
+ const segMinY = Math.min(seg.y1, seg.y2);
263
+ const segMaxY = Math.max(seg.y1, seg.y2);
264
+
265
+ // Check if player rect overlaps segment bounding box
266
+ if (
267
+ px < segMaxX &&
268
+ px + pw > segMinX &&
269
+ py < segMaxY &&
270
+ py + ph > segMinY
271
+ ) {
272
+ return true;
273
+ }
274
+ }
275
+ return false;
276
+ }
277
+ }