@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.
- package/.github/workflows/release.yaml +70 -0
- package/.jshintrc +4 -0
- package/.vscode/settings.json +22 -0
- package/CLAUDE.md +310 -0
- package/blackhole.jpg +0 -0
- package/demo.png +0 -0
- package/demos/CNAME +1 -0
- package/demos/animations.html +31 -0
- package/demos/basic.html +38 -0
- package/demos/baskara.html +31 -0
- package/demos/bezier.html +35 -0
- package/demos/beziersignature.html +29 -0
- package/demos/blackhole.html +28 -0
- package/demos/blob.html +35 -0
- package/demos/demos.css +289 -0
- package/demos/easing.html +28 -0
- package/demos/events.html +195 -0
- package/demos/fluent.html +647 -0
- package/demos/fractals.html +36 -0
- package/demos/genart.html +26 -0
- package/demos/gendream.html +26 -0
- package/demos/group.html +36 -0
- package/demos/home.html +587 -0
- package/demos/index.html +364 -0
- package/demos/isometric.html +34 -0
- package/demos/js/animations.js +452 -0
- package/demos/js/basic.js +204 -0
- package/demos/js/baskara.js +751 -0
- package/demos/js/bezier.js +692 -0
- package/demos/js/beziersignature.js +241 -0
- package/demos/js/blackhole/accretiondisk.obj.js +379 -0
- package/demos/js/blackhole/blackhole.obj.js +318 -0
- package/demos/js/blackhole/index.js +409 -0
- package/demos/js/blackhole/particle.js +56 -0
- package/demos/js/blackhole/starfield.obj.js +218 -0
- package/demos/js/blob.js +2263 -0
- package/demos/js/easing.js +477 -0
- package/demos/js/fluent.js +183 -0
- package/demos/js/fractals.js +931 -0
- package/demos/js/fractalworker.js +93 -0
- package/demos/js/genart.js +268 -0
- package/demos/js/gendream.js +209 -0
- package/demos/js/group.js +140 -0
- package/demos/js/info-toggle.js +25 -0
- package/demos/js/isometric.js +863 -0
- package/demos/js/kerr.js +1556 -0
- package/demos/js/lavalamp.js +590 -0
- package/demos/js/layout.js +354 -0
- package/demos/js/mondrian.js +285 -0
- package/demos/js/opacity.js +275 -0
- package/demos/js/painter.js +484 -0
- package/demos/js/particles-showcase.js +514 -0
- package/demos/js/particles.js +299 -0
- package/demos/js/patterns.js +397 -0
- package/demos/js/penrose/artifact.js +69 -0
- package/demos/js/penrose/blackhole.js +121 -0
- package/demos/js/penrose/constants.js +73 -0
- package/demos/js/penrose/game.js +943 -0
- package/demos/js/penrose/lore.js +278 -0
- package/demos/js/penrose/penrosescene.js +892 -0
- package/demos/js/penrose/ship.js +216 -0
- package/demos/js/penrose/sounds.js +211 -0
- package/demos/js/penrose/voidparticle.js +55 -0
- package/demos/js/penrose/voidscene.js +258 -0
- package/demos/js/penrose/voidship.js +144 -0
- package/demos/js/penrose/wormhole.js +46 -0
- package/demos/js/pipeline.js +555 -0
- package/demos/js/scene.js +304 -0
- package/demos/js/scenes.js +320 -0
- package/demos/js/schrodinger.js +410 -0
- package/demos/js/schwarzschild.js +1023 -0
- package/demos/js/shapes.js +628 -0
- package/demos/js/space/alien.js +171 -0
- package/demos/js/space/boom.js +98 -0
- package/demos/js/space/boss.js +353 -0
- package/demos/js/space/buff.js +73 -0
- package/demos/js/space/bullet.js +102 -0
- package/demos/js/space/constants.js +85 -0
- package/demos/js/space/game.js +1884 -0
- package/demos/js/space/hud.js +112 -0
- package/demos/js/space/laserbeam.js +179 -0
- package/demos/js/space/lightning.js +277 -0
- package/demos/js/space/minion.js +192 -0
- package/demos/js/space/missile.js +212 -0
- package/demos/js/space/player.js +430 -0
- package/demos/js/space/powerup.js +90 -0
- package/demos/js/space/starfield.js +58 -0
- package/demos/js/space/starpower.js +90 -0
- package/demos/js/spacetime.js +559 -0
- package/demos/js/svgtween.js +204 -0
- package/demos/js/tde/accretiondisk.js +418 -0
- package/demos/js/tde/blackhole.js +219 -0
- package/demos/js/tde/blackholescene.js +209 -0
- package/demos/js/tde/config.js +59 -0
- package/demos/js/tde/index.js +695 -0
- package/demos/js/tde/jets.js +290 -0
- package/demos/js/tde/lensedstarfield.js +147 -0
- package/demos/js/tde/tdestar.js +317 -0
- package/demos/js/tde/tidalstream.js +356 -0
- package/demos/js/tde_old/blackhole.obj.js +354 -0
- package/demos/js/tde_old/debris.obj.js +791 -0
- package/demos/js/tde_old/flare.obj.js +239 -0
- package/demos/js/tde_old/index.js +448 -0
- package/demos/js/tde_old/star.obj.js +812 -0
- package/demos/js/tiles.js +312 -0
- package/demos/js/tweendemo.js +79 -0
- package/demos/js/visibility.js +102 -0
- package/demos/kerr.html +28 -0
- package/demos/lavalamp.html +27 -0
- package/demos/layouts.html +37 -0
- package/demos/logo.svg +4 -0
- package/demos/loop.html +84 -0
- package/demos/mondrian.html +32 -0
- package/demos/og_image.png +0 -0
- package/demos/opacity.html +36 -0
- package/demos/painter.html +39 -0
- package/demos/particles-showcase.html +28 -0
- package/demos/particles.html +24 -0
- package/demos/patterns.html +33 -0
- package/demos/penrose-game.html +31 -0
- package/demos/pipeline.html +737 -0
- package/demos/scene.html +33 -0
- package/demos/scenes.html +96 -0
- package/demos/schrodinger.html +27 -0
- package/demos/schwarzschild.html +27 -0
- package/demos/shapes.html +16 -0
- package/demos/space.html +85 -0
- package/demos/spacetime.html +27 -0
- package/demos/svgtween.html +29 -0
- package/demos/tde.html +28 -0
- package/demos/tiles.html +28 -0
- package/demos/transforms.html +400 -0
- package/demos/tween.html +45 -0
- package/demos/visibility.html +33 -0
- package/disk_example.png +0 -0
- package/docs/README.md +222 -0
- package/docs/concepts/architecture-overview.md +204 -0
- package/docs/concepts/lifecycle.md +255 -0
- package/docs/concepts/rendering-pipeline.md +279 -0
- package/docs/concepts/tde-zorder.md +106 -0
- package/docs/concepts/two-layer-architecture.md +229 -0
- package/docs/getting-started/first-game.md +354 -0
- package/docs/getting-started/hello-world.md +269 -0
- package/docs/getting-started/installation.md +157 -0
- package/docs/modules/collision/README.md +453 -0
- package/docs/modules/fluent/README.md +1075 -0
- package/docs/modules/game/README.md +303 -0
- package/docs/modules/isometric-camera.md +210 -0
- package/docs/modules/isometric.md +275 -0
- package/docs/modules/painter/README.md +328 -0
- package/docs/modules/particle/README.md +559 -0
- package/docs/modules/shapes/README.md +221 -0
- package/docs/modules/shapes/base/euclidian.md +123 -0
- package/docs/modules/shapes/base/geometry2d.md +204 -0
- package/docs/modules/shapes/base/renderable.md +215 -0
- package/docs/modules/shapes/base/shape.md +262 -0
- package/docs/modules/shapes/base/transformable.md +243 -0
- package/docs/modules/shapes/hierarchy.md +218 -0
- package/docs/modules/state/README.md +577 -0
- package/docs/modules/util/README.md +99 -0
- package/docs/modules/util/camera3d.md +412 -0
- package/docs/modules/util/scene3d.md +395 -0
- package/index.html +17 -0
- package/jsdoc.json +50 -0
- package/package.json +55 -0
- package/readme.md +599 -0
- package/scripts/build-demo.js +69 -0
- package/scripts/bundle4llm.js +276 -0
- package/scripts/clearconsole.js +48 -0
- package/src/collision/collision-system.js +332 -0
- package/src/collision/collision.js +303 -0
- package/src/collision/index.js +10 -0
- package/src/fluent/fluent-game.js +430 -0
- package/src/fluent/fluent-go.js +1060 -0
- package/src/fluent/fluent-layer.js +152 -0
- package/src/fluent/fluent-scene.js +291 -0
- package/src/fluent/index.js +98 -0
- package/src/fluent/sketch.js +380 -0
- package/src/game/game.js +467 -0
- package/src/game/index.js +49 -0
- package/src/game/objects/go.js +220 -0
- package/src/game/objects/imagego.js +30 -0
- package/src/game/objects/index.js +54 -0
- package/src/game/objects/isometric-scene.js +260 -0
- package/src/game/objects/layoutscene.js +549 -0
- package/src/game/objects/scene.js +175 -0
- package/src/game/objects/scene3d.js +118 -0
- package/src/game/objects/text.js +221 -0
- package/src/game/objects/wrapper.js +232 -0
- package/src/game/pipeline.js +243 -0
- package/src/game/ui/button.js +396 -0
- package/src/game/ui/cursor.js +93 -0
- package/src/game/ui/fps.js +91 -0
- package/src/game/ui/index.js +5 -0
- package/src/game/ui/togglebutton.js +93 -0
- package/src/game/ui/tooltip.js +249 -0
- package/src/index.js +25 -0
- package/src/io/events.js +20 -0
- package/src/io/index.js +86 -0
- package/src/io/input.js +70 -0
- package/src/io/keys.js +152 -0
- package/src/io/mouse.js +61 -0
- package/src/io/touch.js +39 -0
- package/src/logger/debugtab.js +138 -0
- package/src/logger/index.js +3 -0
- package/src/logger/loggable.js +47 -0
- package/src/logger/logger.js +113 -0
- package/src/math/complex.js +37 -0
- package/src/math/constants.js +1 -0
- package/src/math/fractal.js +1271 -0
- package/src/math/gr.js +201 -0
- package/src/math/heat.js +202 -0
- package/src/math/index.js +12 -0
- package/src/math/noise.js +433 -0
- package/src/math/orbital.js +191 -0
- package/src/math/patterns.js +1339 -0
- package/src/math/penrose.js +259 -0
- package/src/math/quantum.js +115 -0
- package/src/math/random.js +195 -0
- package/src/math/tensor.js +1009 -0
- package/src/mixins/anchor.js +131 -0
- package/src/mixins/draggable.js +72 -0
- package/src/mixins/index.js +2 -0
- package/src/motion/bezier.js +132 -0
- package/src/motion/bounce.js +58 -0
- package/src/motion/easing.js +349 -0
- package/src/motion/float.js +130 -0
- package/src/motion/follow.js +125 -0
- package/src/motion/hop.js +52 -0
- package/src/motion/index.js +82 -0
- package/src/motion/motion.js +1124 -0
- package/src/motion/orbit.js +49 -0
- package/src/motion/oscillate.js +39 -0
- package/src/motion/parabolic.js +141 -0
- package/src/motion/patrol.js +147 -0
- package/src/motion/pendulum.js +48 -0
- package/src/motion/pulse.js +88 -0
- package/src/motion/shake.js +83 -0
- package/src/motion/spiral.js +144 -0
- package/src/motion/spring.js +150 -0
- package/src/motion/swing.js +47 -0
- package/src/motion/tween.js +92 -0
- package/src/motion/tweenetik.js +139 -0
- package/src/motion/waypoint.js +210 -0
- package/src/painter/index.js +8 -0
- package/src/painter/painter.colors.js +331 -0
- package/src/painter/painter.effects.js +230 -0
- package/src/painter/painter.img.js +229 -0
- package/src/painter/painter.js +295 -0
- package/src/painter/painter.lines.js +189 -0
- package/src/painter/painter.opacity.js +41 -0
- package/src/painter/painter.shapes.js +277 -0
- package/src/painter/painter.text.js +273 -0
- package/src/particle/emitter.js +124 -0
- package/src/particle/index.js +11 -0
- package/src/particle/particle-system.js +322 -0
- package/src/particle/particle.js +71 -0
- package/src/particle/updaters.js +170 -0
- package/src/shapes/arc.js +43 -0
- package/src/shapes/arrow.js +33 -0
- package/src/shapes/bezier.js +42 -0
- package/src/shapes/circle.js +62 -0
- package/src/shapes/clouds.js +56 -0
- package/src/shapes/cone.js +219 -0
- package/src/shapes/cross.js +70 -0
- package/src/shapes/cube.js +244 -0
- package/src/shapes/cylinder.js +254 -0
- package/src/shapes/diamond.js +48 -0
- package/src/shapes/euclidian.js +111 -0
- package/src/shapes/figure.js +115 -0
- package/src/shapes/geometry.js +220 -0
- package/src/shapes/group.js +375 -0
- package/src/shapes/heart.js +42 -0
- package/src/shapes/hexagon.js +26 -0
- package/src/shapes/image.js +192 -0
- package/src/shapes/index.js +111 -0
- package/src/shapes/line.js +29 -0
- package/src/shapes/pattern.js +90 -0
- package/src/shapes/pin.js +44 -0
- package/src/shapes/poly.js +31 -0
- package/src/shapes/prism.js +226 -0
- package/src/shapes/rect.js +35 -0
- package/src/shapes/renderable.js +333 -0
- package/src/shapes/ring.js +26 -0
- package/src/shapes/roundrect.js +95 -0
- package/src/shapes/shape.js +117 -0
- package/src/shapes/slice.js +26 -0
- package/src/shapes/sphere.js +314 -0
- package/src/shapes/sphere3d.js +537 -0
- package/src/shapes/square.js +15 -0
- package/src/shapes/star.js +99 -0
- package/src/shapes/svg.js +408 -0
- package/src/shapes/text.js +553 -0
- package/src/shapes/traceable.js +83 -0
- package/src/shapes/transform.js +357 -0
- package/src/shapes/transformable.js +172 -0
- package/src/shapes/triangle.js +26 -0
- package/src/sound/index.js +17 -0
- package/src/sound/sound.js +473 -0
- package/src/sound/synth.analyzer.js +149 -0
- package/src/sound/synth.effects.js +207 -0
- package/src/sound/synth.envelope.js +59 -0
- package/src/sound/synth.js +229 -0
- package/src/sound/synth.musical.js +160 -0
- package/src/sound/synth.noise.js +85 -0
- package/src/sound/synth.oscillators.js +293 -0
- package/src/state/index.js +10 -0
- package/src/state/state-machine.js +371 -0
- package/src/util/camera3d.js +438 -0
- package/src/util/index.js +6 -0
- package/src/util/isometric-camera.js +235 -0
- package/src/util/layout.js +317 -0
- package/src/util/position.js +147 -0
- package/src/util/tasks.js +47 -0
- package/src/util/zindex.js +287 -0
- package/src/webgl/index.js +9 -0
- package/src/webgl/shaders/sphere-shaders.js +994 -0
- package/src/webgl/webgl-renderer.js +388 -0
- package/tde.png +0 -0
- package/test/math/orbital.test.js +61 -0
- package/test/math/tensor.test.js +114 -0
- package/test/particle/emitter.test.js +204 -0
- package/test/particle/particle-system.test.js +310 -0
- package/test/particle/particle.test.js +116 -0
- package/test/particle/updaters.test.js +386 -0
- package/test/setup.js +120 -0
- package/test/shapes/euclidian.test.js +44 -0
- package/test/shapes/geometry.test.js +86 -0
- package/test/shapes/group.test.js +86 -0
- package/test/shapes/rectangle.test.js +64 -0
- package/test/shapes/transform.test.js +379 -0
- package/test/util/camera3d.test.js +428 -0
- package/test/util/scene3d.test.js +352 -0
- package/types/collision.d.ts +249 -0
- package/types/common.d.ts +155 -0
- package/types/game.d.ts +497 -0
- package/types/index.d.ts +309 -0
- package/types/io.d.ts +188 -0
- package/types/logger.d.ts +127 -0
- package/types/math.d.ts +268 -0
- package/types/mixins.d.ts +92 -0
- package/types/motion.d.ts +678 -0
- package/types/painter.d.ts +378 -0
- package/types/shapes.d.ts +864 -0
- package/types/sound.d.ts +672 -0
- package/types/state.d.ts +251 -0
- package/types/util.d.ts +253 -0
- package/vite.config.js +50 -0
- 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
|
+
}
|