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