@guinetik/gcanvas 1.0.2 → 1.0.4

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 (217) hide show
  1. package/dist/gcanvas.es.js +25656 -0
  2. package/dist/gcanvas.es.min.js +1 -0
  3. package/dist/gcanvas.umd.js +1 -0
  4. package/dist/gcanvas.umd.min.js +1 -0
  5. package/package.json +23 -6
  6. package/src/game/objects/index.js +1 -0
  7. package/src/game/objects/spritesheet.js +260 -0
  8. package/src/game/ui/theme.js +6 -0
  9. package/src/io/keys.js +9 -1
  10. package/src/math/boolean.js +481 -0
  11. package/src/math/index.js +1 -0
  12. package/.github/workflows/release.yaml +0 -70
  13. package/.jshintrc +0 -4
  14. package/.vscode/settings.json +0 -22
  15. package/CLAUDE.md +0 -310
  16. package/blackhole.jpg +0 -0
  17. package/demo.png +0 -0
  18. package/demos/CNAME +0 -1
  19. package/demos/animations.html +0 -31
  20. package/demos/basic.html +0 -38
  21. package/demos/baskara.html +0 -31
  22. package/demos/bezier.html +0 -35
  23. package/demos/beziersignature.html +0 -29
  24. package/demos/blackhole.html +0 -28
  25. package/demos/blob.html +0 -35
  26. package/demos/coordinates.html +0 -698
  27. package/demos/cube3d.html +0 -23
  28. package/demos/demos.css +0 -303
  29. package/demos/dino.html +0 -42
  30. package/demos/easing.html +0 -28
  31. package/demos/events.html +0 -195
  32. package/demos/fluent.html +0 -647
  33. package/demos/fluid-simple.html +0 -22
  34. package/demos/fluid.html +0 -37
  35. package/demos/fractals.html +0 -36
  36. package/demos/gameobjects.html +0 -626
  37. package/demos/genart.html +0 -26
  38. package/demos/gendream.html +0 -26
  39. package/demos/group.html +0 -36
  40. package/demos/home.html +0 -587
  41. package/demos/index.html +0 -376
  42. package/demos/isometric.html +0 -34
  43. package/demos/js/animations.js +0 -452
  44. package/demos/js/basic.js +0 -204
  45. package/demos/js/baskara.js +0 -751
  46. package/demos/js/bezier.js +0 -692
  47. package/demos/js/beziersignature.js +0 -241
  48. package/demos/js/blackhole/accretiondisk.obj.js +0 -379
  49. package/demos/js/blackhole/blackhole.obj.js +0 -318
  50. package/demos/js/blackhole/index.js +0 -409
  51. package/demos/js/blackhole/particle.js +0 -56
  52. package/demos/js/blackhole/starfield.obj.js +0 -218
  53. package/demos/js/blob.js +0 -2276
  54. package/demos/js/coordinates.js +0 -840
  55. package/demos/js/cube3d.js +0 -789
  56. package/demos/js/dino.js +0 -1420
  57. package/demos/js/easing.js +0 -477
  58. package/demos/js/fluent.js +0 -183
  59. package/demos/js/fluid-simple.js +0 -253
  60. package/demos/js/fluid.js +0 -527
  61. package/demos/js/fractals.js +0 -931
  62. package/demos/js/fractalworker.js +0 -93
  63. package/demos/js/gameobjects.js +0 -176
  64. package/demos/js/genart.js +0 -268
  65. package/demos/js/gendream.js +0 -209
  66. package/demos/js/group.js +0 -140
  67. package/demos/js/info-toggle.js +0 -25
  68. package/demos/js/isometric.js +0 -863
  69. package/demos/js/kerr.js +0 -1556
  70. package/demos/js/lavalamp.js +0 -590
  71. package/demos/js/layout.js +0 -354
  72. package/demos/js/mondrian.js +0 -285
  73. package/demos/js/opacity.js +0 -275
  74. package/demos/js/painter.js +0 -484
  75. package/demos/js/particles-showcase.js +0 -514
  76. package/demos/js/particles.js +0 -299
  77. package/demos/js/patterns.js +0 -397
  78. package/demos/js/penrose/artifact.js +0 -69
  79. package/demos/js/penrose/blackhole.js +0 -121
  80. package/demos/js/penrose/constants.js +0 -73
  81. package/demos/js/penrose/game.js +0 -943
  82. package/demos/js/penrose/lore.js +0 -278
  83. package/demos/js/penrose/penrosescene.js +0 -892
  84. package/demos/js/penrose/ship.js +0 -216
  85. package/demos/js/penrose/sounds.js +0 -211
  86. package/demos/js/penrose/voidparticle.js +0 -55
  87. package/demos/js/penrose/voidscene.js +0 -258
  88. package/demos/js/penrose/voidship.js +0 -144
  89. package/demos/js/penrose/wormhole.js +0 -46
  90. package/demos/js/pipeline.js +0 -555
  91. package/demos/js/plane3d.js +0 -256
  92. package/demos/js/platformer.js +0 -1579
  93. package/demos/js/scene.js +0 -304
  94. package/demos/js/scenes.js +0 -320
  95. package/demos/js/schrodinger.js +0 -410
  96. package/demos/js/schwarzschild.js +0 -1023
  97. package/demos/js/shapes.js +0 -628
  98. package/demos/js/space/alien.js +0 -171
  99. package/demos/js/space/boom.js +0 -98
  100. package/demos/js/space/boss.js +0 -353
  101. package/demos/js/space/buff.js +0 -73
  102. package/demos/js/space/bullet.js +0 -102
  103. package/demos/js/space/constants.js +0 -85
  104. package/demos/js/space/game.js +0 -1884
  105. package/demos/js/space/hud.js +0 -112
  106. package/demos/js/space/laserbeam.js +0 -179
  107. package/demos/js/space/lightning.js +0 -277
  108. package/demos/js/space/minion.js +0 -192
  109. package/demos/js/space/missile.js +0 -212
  110. package/demos/js/space/player.js +0 -430
  111. package/demos/js/space/powerup.js +0 -90
  112. package/demos/js/space/starfield.js +0 -58
  113. package/demos/js/space/starpower.js +0 -90
  114. package/demos/js/spacetime.js +0 -559
  115. package/demos/js/sphere3d.js +0 -229
  116. package/demos/js/sprite.js +0 -473
  117. package/demos/js/svgtween.js +0 -204
  118. package/demos/js/tde/accretiondisk.js +0 -471
  119. package/demos/js/tde/blackhole.js +0 -219
  120. package/demos/js/tde/blackholescene.js +0 -209
  121. package/demos/js/tde/config.js +0 -59
  122. package/demos/js/tde/index.js +0 -820
  123. package/demos/js/tde/jets.js +0 -290
  124. package/demos/js/tde/lensedstarfield.js +0 -154
  125. package/demos/js/tde/tdestar.js +0 -297
  126. package/demos/js/tde/tidalstream.js +0 -372
  127. package/demos/js/tde_old/blackhole.obj.js +0 -354
  128. package/demos/js/tde_old/debris.obj.js +0 -791
  129. package/demos/js/tde_old/flare.obj.js +0 -239
  130. package/demos/js/tde_old/index.js +0 -448
  131. package/demos/js/tde_old/star.obj.js +0 -812
  132. package/demos/js/tiles.js +0 -312
  133. package/demos/js/tweendemo.js +0 -79
  134. package/demos/js/visibility.js +0 -102
  135. package/demos/kerr.html +0 -28
  136. package/demos/lavalamp.html +0 -27
  137. package/demos/layouts.html +0 -37
  138. package/demos/logo.svg +0 -4
  139. package/demos/loop.html +0 -84
  140. package/demos/mondrian.html +0 -32
  141. package/demos/og_image.png +0 -0
  142. package/demos/opacity.html +0 -36
  143. package/demos/painter.html +0 -39
  144. package/demos/particles-showcase.html +0 -28
  145. package/demos/particles.html +0 -24
  146. package/demos/patterns.html +0 -33
  147. package/demos/penrose-game.html +0 -31
  148. package/demos/pipeline.html +0 -737
  149. package/demos/plane3d.html +0 -24
  150. package/demos/platformer.html +0 -43
  151. package/demos/scene.html +0 -33
  152. package/demos/scenes.html +0 -96
  153. package/demos/schrodinger.html +0 -27
  154. package/demos/schwarzschild.html +0 -27
  155. package/demos/shapes.html +0 -16
  156. package/demos/space.html +0 -85
  157. package/demos/spacetime.html +0 -27
  158. package/demos/sphere3d.html +0 -24
  159. package/demos/sprite.html +0 -18
  160. package/demos/svgtween.html +0 -29
  161. package/demos/tde.html +0 -28
  162. package/demos/tiles.html +0 -28
  163. package/demos/transforms.html +0 -400
  164. package/demos/tween.html +0 -45
  165. package/demos/visibility.html +0 -33
  166. package/docs/README.md +0 -230
  167. package/docs/api/FluidSystem.md +0 -173
  168. package/docs/concepts/architecture-overview.md +0 -204
  169. package/docs/concepts/coordinate-system.md +0 -384
  170. package/docs/concepts/lifecycle.md +0 -255
  171. package/docs/concepts/rendering-pipeline.md +0 -279
  172. package/docs/concepts/shapes-vs-gameobjects.md +0 -187
  173. package/docs/concepts/tde-zorder.md +0 -106
  174. package/docs/concepts/two-layer-architecture.md +0 -229
  175. package/docs/fluid-dynamics.md +0 -99
  176. package/docs/getting-started/first-game.md +0 -354
  177. package/docs/getting-started/hello-world.md +0 -269
  178. package/docs/getting-started/installation.md +0 -175
  179. package/docs/modules/collision/README.md +0 -453
  180. package/docs/modules/fluent/README.md +0 -1075
  181. package/docs/modules/game/README.md +0 -303
  182. package/docs/modules/isometric-camera.md +0 -210
  183. package/docs/modules/isometric.md +0 -275
  184. package/docs/modules/painter/README.md +0 -328
  185. package/docs/modules/particle/README.md +0 -559
  186. package/docs/modules/shapes/README.md +0 -221
  187. package/docs/modules/shapes/base/euclidian.md +0 -123
  188. package/docs/modules/shapes/base/geometry2d.md +0 -204
  189. package/docs/modules/shapes/base/renderable.md +0 -215
  190. package/docs/modules/shapes/base/shape.md +0 -262
  191. package/docs/modules/shapes/base/transformable.md +0 -243
  192. package/docs/modules/shapes/hierarchy.md +0 -218
  193. package/docs/modules/state/README.md +0 -577
  194. package/docs/modules/util/README.md +0 -99
  195. package/docs/modules/util/camera3d.md +0 -412
  196. package/docs/modules/util/scene3d.md +0 -395
  197. package/index.html +0 -17
  198. package/jsdoc.json +0 -50
  199. package/scripts/build-demo.js +0 -69
  200. package/scripts/bundle4llm.js +0 -276
  201. package/scripts/clearconsole.js +0 -48
  202. package/test/math/orbital.test.js +0 -61
  203. package/test/math/tensor.test.js +0 -114
  204. package/test/particle/emitter.test.js +0 -204
  205. package/test/particle/particle-system.test.js +0 -310
  206. package/test/particle/particle.test.js +0 -116
  207. package/test/particle/updaters.test.js +0 -386
  208. package/test/setup.js +0 -120
  209. package/test/shapes/euclidian.test.js +0 -44
  210. package/test/shapes/geometry.test.js +0 -86
  211. package/test/shapes/group.test.js +0 -86
  212. package/test/shapes/rectangle.test.js +0 -64
  213. package/test/shapes/transform.test.js +0 -379
  214. package/test/util/camera3d.test.js +0 -428
  215. package/test/util/scene3d.test.js +0 -352
  216. package/vite.config.js +0 -50
  217. package/vitest.config.js +0 -13
@@ -1,112 +0,0 @@
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
- }
@@ -1,179 +0,0 @@
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
- }
@@ -1,277 +0,0 @@
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
- }