@guinetik/gcanvas 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/dist/CNAME +1 -0
  2. package/dist/animations.html +31 -0
  3. package/dist/basic.html +38 -0
  4. package/dist/baskara.html +31 -0
  5. package/dist/bezier.html +35 -0
  6. package/dist/beziersignature.html +29 -0
  7. package/dist/blackhole.html +28 -0
  8. package/dist/blob.html +35 -0
  9. package/dist/coordinates.html +698 -0
  10. package/dist/cube3d.html +23 -0
  11. package/dist/demos.css +303 -0
  12. package/dist/dino.html +42 -0
  13. package/dist/easing.html +28 -0
  14. package/dist/events.html +195 -0
  15. package/dist/fluent.html +647 -0
  16. package/dist/fluid-simple.html +22 -0
  17. package/dist/fluid.html +37 -0
  18. package/dist/fractals.html +36 -0
  19. package/dist/gameobjects.html +626 -0
  20. package/dist/gcanvas.es.js +517 -0
  21. package/dist/gcanvas.es.min.js +1 -1
  22. package/dist/gcanvas.umd.js +1 -1
  23. package/dist/gcanvas.umd.min.js +1 -1
  24. package/dist/genart.html +26 -0
  25. package/dist/gendream.html +26 -0
  26. package/dist/group.html +36 -0
  27. package/dist/home.html +587 -0
  28. package/dist/hyperbolic001.html +23 -0
  29. package/dist/hyperbolic002.html +23 -0
  30. package/dist/hyperbolic003.html +23 -0
  31. package/dist/hyperbolic004.html +23 -0
  32. package/dist/hyperbolic005.html +22 -0
  33. package/dist/index.html +398 -0
  34. package/dist/isometric.html +34 -0
  35. package/dist/js/animations.js +452 -0
  36. package/dist/js/basic.js +204 -0
  37. package/dist/js/baskara.js +751 -0
  38. package/dist/js/bezier.js +692 -0
  39. package/dist/js/beziersignature.js +241 -0
  40. package/dist/js/blackhole/accretiondisk.obj.js +379 -0
  41. package/dist/js/blackhole/blackhole.obj.js +318 -0
  42. package/dist/js/blackhole/index.js +409 -0
  43. package/dist/js/blackhole/particle.js +56 -0
  44. package/dist/js/blackhole/starfield.obj.js +218 -0
  45. package/dist/js/blob.js +2276 -0
  46. package/dist/js/coordinates.js +840 -0
  47. package/dist/js/cube3d.js +789 -0
  48. package/dist/js/dino.js +1420 -0
  49. package/dist/js/easing.js +477 -0
  50. package/dist/js/fluent.js +183 -0
  51. package/dist/js/fluid-simple.js +253 -0
  52. package/dist/js/fluid.js +527 -0
  53. package/dist/js/fractals.js +932 -0
  54. package/dist/js/fractalworker.js +93 -0
  55. package/dist/js/gameobjects.js +176 -0
  56. package/dist/js/genart.js +268 -0
  57. package/dist/js/gendream.js +209 -0
  58. package/dist/js/group.js +140 -0
  59. package/dist/js/hyperbolic001.js +310 -0
  60. package/dist/js/hyperbolic002.js +388 -0
  61. package/dist/js/hyperbolic003.js +319 -0
  62. package/dist/js/hyperbolic004.js +345 -0
  63. package/dist/js/hyperbolic005.js +340 -0
  64. package/dist/js/info-toggle.js +25 -0
  65. package/dist/js/isometric.js +863 -0
  66. package/dist/js/kerr.js +1547 -0
  67. package/dist/js/lavalamp.js +590 -0
  68. package/dist/js/layout.js +354 -0
  69. package/dist/js/mondrian.js +285 -0
  70. package/dist/js/opacity.js +275 -0
  71. package/dist/js/painter.js +484 -0
  72. package/dist/js/particles-showcase.js +514 -0
  73. package/dist/js/particles.js +299 -0
  74. package/dist/js/patterns.js +397 -0
  75. package/dist/js/penrose/artifact.js +69 -0
  76. package/dist/js/penrose/blackhole.js +121 -0
  77. package/dist/js/penrose/constants.js +73 -0
  78. package/dist/js/penrose/game.js +943 -0
  79. package/dist/js/penrose/lore.js +278 -0
  80. package/dist/js/penrose/penrosescene.js +892 -0
  81. package/dist/js/penrose/ship.js +216 -0
  82. package/dist/js/penrose/sounds.js +211 -0
  83. package/dist/js/penrose/voidparticle.js +55 -0
  84. package/dist/js/penrose/voidscene.js +258 -0
  85. package/dist/js/penrose/voidship.js +144 -0
  86. package/dist/js/penrose/wormhole.js +46 -0
  87. package/dist/js/pipeline.js +555 -0
  88. package/dist/js/plane3d.js +256 -0
  89. package/dist/js/platformer.js +1579 -0
  90. package/dist/js/scene.js +304 -0
  91. package/dist/js/scenes.js +320 -0
  92. package/dist/js/schrodinger.js +410 -0
  93. package/dist/js/schwarzschild.js +1015 -0
  94. package/dist/js/shapes.js +628 -0
  95. package/dist/js/space/alien.js +171 -0
  96. package/dist/js/space/boom.js +98 -0
  97. package/dist/js/space/boss.js +353 -0
  98. package/dist/js/space/buff.js +73 -0
  99. package/dist/js/space/bullet.js +102 -0
  100. package/dist/js/space/constants.js +85 -0
  101. package/dist/js/space/game.js +1884 -0
  102. package/dist/js/space/hud.js +112 -0
  103. package/dist/js/space/laserbeam.js +179 -0
  104. package/dist/js/space/lightning.js +277 -0
  105. package/dist/js/space/minion.js +192 -0
  106. package/dist/js/space/missile.js +212 -0
  107. package/dist/js/space/player.js +430 -0
  108. package/dist/js/space/powerup.js +90 -0
  109. package/dist/js/space/starfield.js +58 -0
  110. package/dist/js/space/starpower.js +90 -0
  111. package/dist/js/spacetime.js +559 -0
  112. package/dist/js/sphere3d.js +229 -0
  113. package/dist/js/sprite.js +473 -0
  114. package/dist/js/starfaux/config.js +118 -0
  115. package/dist/js/starfaux/enemy.js +353 -0
  116. package/dist/js/starfaux/hud.js +78 -0
  117. package/dist/js/starfaux/index.js +482 -0
  118. package/dist/js/starfaux/laser.js +182 -0
  119. package/dist/js/starfaux/player.js +468 -0
  120. package/dist/js/starfaux/terrain.js +560 -0
  121. package/dist/js/study001.js +275 -0
  122. package/dist/js/study002.js +366 -0
  123. package/dist/js/study003.js +331 -0
  124. package/dist/js/study004.js +389 -0
  125. package/dist/js/study005.js +209 -0
  126. package/dist/js/study006.js +194 -0
  127. package/dist/js/study007.js +192 -0
  128. package/dist/js/study008.js +413 -0
  129. package/dist/js/svgtween.js +204 -0
  130. package/dist/js/tde/accretiondisk.js +471 -0
  131. package/dist/js/tde/blackhole.js +219 -0
  132. package/dist/js/tde/blackholescene.js +209 -0
  133. package/dist/js/tde/config.js +59 -0
  134. package/dist/js/tde/index.js +820 -0
  135. package/dist/js/tde/jets.js +290 -0
  136. package/dist/js/tde/lensedstarfield.js +154 -0
  137. package/dist/js/tde/tdestar.js +297 -0
  138. package/dist/js/tde/tidalstream.js +372 -0
  139. package/dist/js/tde_old/blackhole.obj.js +354 -0
  140. package/dist/js/tde_old/debris.obj.js +791 -0
  141. package/dist/js/tde_old/flare.obj.js +239 -0
  142. package/dist/js/tde_old/index.js +448 -0
  143. package/dist/js/tde_old/star.obj.js +812 -0
  144. package/dist/js/tetris/config.js +157 -0
  145. package/dist/js/tetris/grid.js +286 -0
  146. package/dist/js/tetris/index.js +1195 -0
  147. package/dist/js/tetris/renderer.js +634 -0
  148. package/dist/js/tetris/tetrominos.js +280 -0
  149. package/dist/js/tiles.js +312 -0
  150. package/dist/js/tweendemo.js +79 -0
  151. package/dist/js/visibility.js +102 -0
  152. package/dist/kerr.html +28 -0
  153. package/dist/lavalamp.html +27 -0
  154. package/dist/layouts.html +37 -0
  155. package/dist/logo.svg +4 -0
  156. package/dist/loop.html +84 -0
  157. package/dist/mondrian.html +32 -0
  158. package/dist/og_image.png +0 -0
  159. package/dist/opacity.html +36 -0
  160. package/dist/painter.html +39 -0
  161. package/dist/particles-showcase.html +28 -0
  162. package/dist/particles.html +24 -0
  163. package/dist/patterns.html +33 -0
  164. package/dist/penrose-game.html +31 -0
  165. package/dist/pipeline.html +737 -0
  166. package/dist/plane3d.html +24 -0
  167. package/dist/platformer.html +43 -0
  168. package/dist/scene.html +33 -0
  169. package/dist/scenes.html +96 -0
  170. package/dist/schrodinger.html +27 -0
  171. package/dist/schwarzschild.html +27 -0
  172. package/dist/shapes.html +16 -0
  173. package/dist/space.html +85 -0
  174. package/dist/spacetime.html +27 -0
  175. package/dist/sphere3d.html +24 -0
  176. package/dist/sprite.html +18 -0
  177. package/dist/starfaux.html +22 -0
  178. package/dist/study001.html +23 -0
  179. package/dist/study002.html +23 -0
  180. package/dist/study003.html +23 -0
  181. package/dist/study004.html +23 -0
  182. package/dist/study005.html +22 -0
  183. package/dist/study006.html +24 -0
  184. package/dist/study007.html +24 -0
  185. package/dist/study008.html +22 -0
  186. package/dist/svgtween.html +29 -0
  187. package/dist/tde.html +28 -0
  188. package/dist/tetris3d.html +25 -0
  189. package/dist/tiles.html +28 -0
  190. package/dist/transforms.html +400 -0
  191. package/dist/tween.html +45 -0
  192. package/dist/visibility.html +33 -0
  193. package/package.json +1 -1
@@ -0,0 +1,118 @@
1
+ /**
2
+ * StarFaux Configuration
3
+ * All game constants centralized here - NO magic numbers in code
4
+ */
5
+
6
+ export const CONFIG = {
7
+ // Reference resolution for scaling (all sizes are relative to this)
8
+ // On larger screens, everything scales proportionally
9
+ referenceWidth: 1920,
10
+ referenceHeight: 1080,
11
+
12
+ // Rails camera system
13
+ rails: {
14
+ speed: 300, // Forward movement units/sec
15
+ perspective: 500, // Camera3D perspective distance
16
+ tiltX: 0, // NO tilt - camera positioned above ground instead
17
+ cameraY: -500, // Camera is HIGH above the ground plane
18
+ // Camera reaction to player movement (creates world-ship coupling)
19
+ cameraReactX: 0.08, // How much camera rotates Y based on player X (subtle banking)
20
+ cameraReactY: 0.02, // How much camera tilts based on player Y
21
+ cameraLag: 5, // Smoothing factor for camera follow
22
+ },
23
+
24
+ // Player ship
25
+ player: {
26
+ moveSpeed: 500, // X/Y movement speed (fast for wide arena)
27
+ bounds: {
28
+ x: 600, // Max horizontal offset - MUCH wider to explore terrain
29
+ y: 180, // Max vertical offset from center
30
+ },
31
+ size: 30, // Ship size for collision
32
+ fireRate: 0.12, // Seconds between shots
33
+ maxHealth: 3, // Hit points
34
+ bankAngle: 0.5, // Max banking rotation when strafing (radians)
35
+ bankSpeed: 8, // Banking interpolation speed
36
+ shipZ: 100, // Ship Z position (in front of camera)
37
+ crosshairZ: 400, // Crosshair Z position (further ahead, where aiming)
38
+ screenY: -80, // Ship Y offset - NEGATIVE to spawn higher on screen
39
+ invincibilityTime: 1.5, // Seconds of invincibility after taking damage
40
+ blinkRate: 0.1, // Blink interval during invincibility
41
+ // Physics - inertia and damping
42
+ acceleration: 1800, // How fast ship accelerates toward target velocity
43
+ damping: 0.92, // Velocity decay when no input (0-1, lower = more drag)
44
+ gravity: 200, // Downward pull (easier to fall than climb)
45
+ climbResistance: 0.6, // Multiplier for upward movement (< 1 = harder to climb)
46
+ },
47
+
48
+ // Laser projectiles
49
+ laser: {
50
+ speed: 800, // Forward velocity
51
+ lifetime: 1.5, // Seconds before despawn
52
+ width: 6, // Visual width
53
+ height: 20, // Visual length
54
+ color: "#ff3333", // RED laser - distinct from green terrain
55
+ glowColor: "#ff8888", // Red glow
56
+ poolSize: 30, // Pre-allocated laser pool
57
+ collisionSize: 25, // Collision radius (larger than visual for better hit detection)
58
+ },
59
+
60
+ // Enemy configuration
61
+ enemy: {
62
+ spawnDistance: 1500, // How far ahead to spawn
63
+ despawnDistance: -100, // Behind camera threshold
64
+ spawnInterval: 1.2, // Base seconds between spawns
65
+ spawnVariance: 0.5, // Random variance on spawn timing
66
+ types: {
67
+ fighter: {
68
+ size: 40,
69
+ health: 1,
70
+ score: 100,
71
+ color: "#ff4444",
72
+ speed: 50, // Additional approach speed
73
+ },
74
+ },
75
+ },
76
+
77
+ // Terrain grid
78
+ terrain: {
79
+ gridSpacing: 120, // Distance between grid lines
80
+ lineCount: 25, // Number of lines to draw
81
+ color: "#00aa00", // Grid line color
82
+ lineWidth: 1, // Line thickness
83
+ yPosition: 0, // Ground plane at Y=0 (camera is above at Y=-150)
84
+ // Width is now calculated dynamically based on screen width
85
+ // widthMultiplier: how many screen widths the terrain spans
86
+ widthMultiplier: 6, // Terrain width
87
+ numCols: 100, // Many points per line for smooth waveforms
88
+ // Joy Division style - stacked horizontal waveforms with clear gaps
89
+ nearPlaneZ: 20, // Start terrain
90
+ nearSpacing: 80, // WIDER spacing between lines - more black gaps
91
+ totalRows: 35, // More rows to reach horizon
92
+ // Terrain features (hills/obstacles)
93
+ features: {
94
+ spawnInterval: 800, // Distance between terrain features
95
+ maxHeight: 350, // Maximum hill height - TALLER mountains
96
+ width: 200, // Feature width
97
+ color: "#006600", // Darker green for hills
98
+ collisionHeight: 120, // Collide with medium+ terrain (120+ out of 350 max)
99
+ },
100
+ },
101
+
102
+ // Visual settings
103
+ colors: {
104
+ background: "#000011", // Dark blue-black space
105
+ player: "#4488ff", // Player ship blue
106
+ playerAccent: "#88aaff",
107
+ laser: "#00ff00",
108
+ enemyLaser: "#ff4444",
109
+ explosion: "#ffaa00",
110
+ hud: "#00ff00",
111
+ },
112
+
113
+ // HUD
114
+ hud: {
115
+ font: "bold 20px monospace",
116
+ margin: 20,
117
+ },
118
+ };
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Enemy - Enemy fighter system for StarFaux
3
+ *
4
+ * Enemies spawn far away (high Z, at horizon) and move
5
+ * toward the camera (Z decreases). When Z < 0, they've passed.
6
+ */
7
+
8
+ import { Painter, Easing } from "/gcanvas.es.min.js";
9
+ import { CONFIG } from "./config.js";
10
+
11
+ /**
12
+ * Individual enemy fighter
13
+ */
14
+ class Enemy {
15
+ constructor() {
16
+ this.reset();
17
+ }
18
+
19
+ reset() {
20
+ this.worldX = 0;
21
+ this.worldY = 0;
22
+ this.worldZ = 0;
23
+ this.alive = false;
24
+ this.health = 1;
25
+ this.size = CONFIG.enemy.types.fighter.size;
26
+ this.scoreValue = CONFIG.enemy.types.fighter.score;
27
+ this.rotation = 0;
28
+ this.rotationSpeed = 0;
29
+ this.damageFlash = 0; // Flash timer when hit
30
+ // Spawn animation
31
+ this.spawnTime = 0;
32
+ this.spawnDuration = 0.5; // Half second spawn animation
33
+ this.spawnProgress = 0;
34
+ }
35
+
36
+ init(x, y, z) {
37
+ const cfg = CONFIG.enemy.types.fighter;
38
+ this.worldX = x;
39
+ this.worldY = y;
40
+ this.worldZ = z;
41
+ this.alive = true;
42
+ this.health = cfg.health;
43
+ this.size = cfg.size;
44
+ this.scoreValue = cfg.score;
45
+ this.rotation = Math.random() * Math.PI * 2;
46
+ this.rotationSpeed = (Math.random() - 0.5) * 2;
47
+ // Reset spawn animation
48
+ this.spawnTime = 0;
49
+ this.spawnProgress = 0;
50
+ }
51
+
52
+ update(dt) {
53
+ if (!this.alive) return;
54
+
55
+ // Update spawn animation
56
+ if (this.spawnProgress < 1) {
57
+ this.spawnTime += dt;
58
+ this.spawnProgress = Math.min(1, this.spawnTime / this.spawnDuration);
59
+ }
60
+
61
+ // Move TOWARD camera (Z decreases)
62
+ // Also factor in the "rails speed" - world moves toward us
63
+ this.worldZ -= (CONFIG.rails.speed + CONFIG.enemy.types.fighter.speed) * dt;
64
+
65
+ // Slight wobble movement for visual interest
66
+ this.worldX += Math.sin(this.worldZ * 0.02) * 0.3;
67
+
68
+ // Rotation animation
69
+ this.rotation += this.rotationSpeed * dt;
70
+
71
+ // Damage flash decay
72
+ if (this.damageFlash > 0) {
73
+ this.damageFlash -= dt * 5; // Flash fades quickly
74
+ }
75
+
76
+ // Check if passed camera (despawn)
77
+ if (this.worldZ < CONFIG.enemy.despawnDistance) {
78
+ this.alive = false;
79
+ }
80
+ }
81
+
82
+ takeDamage() {
83
+ this.health--;
84
+ this.damageFlash = 1; // Start flash
85
+ if (this.health <= 0) {
86
+ this.alive = false;
87
+ }
88
+ }
89
+
90
+ render(ctx, camera, resScale = 1, groundY = 0) {
91
+ if (!this.alive) return;
92
+
93
+ const projected = camera.project(this.worldX, this.worldY, this.worldZ);
94
+
95
+ // Don't render if behind camera
96
+ if (projected.scale <= 0) return;
97
+
98
+ // Don't render if too small (too far away)
99
+ if (projected.scale < 0.08) return;
100
+
101
+ // Calculate spawn animation values using easing
102
+ const spawnEased = Easing.easeOutBack(this.spawnProgress);
103
+ const spawnScale = spawnEased;
104
+ const spawnAlpha = Easing.easeOutQuad(this.spawnProgress);
105
+
106
+ // Draw shadow on ground (also affected by spawn)
107
+ if (this.spawnProgress > 0.3) {
108
+ this.drawShadow(ctx, camera, resScale * spawnScale, groundY, spawnAlpha);
109
+ }
110
+
111
+ ctx.save();
112
+ ctx.translate(projected.x, projected.y);
113
+ ctx.rotate(this.rotation);
114
+
115
+ // Apply spawn scale animation (easeOutBack gives a nice overshoot pop-in)
116
+ const totalScale = projected.scale * resScale * spawnScale;
117
+ ctx.scale(totalScale, totalScale);
118
+
119
+ // Apply spawn alpha (fade in)
120
+ ctx.globalAlpha = spawnAlpha;
121
+
122
+ // Apply damage flash - make whole ship white
123
+ if (this.damageFlash > 0) {
124
+ ctx.globalAlpha = (0.3 + this.damageFlash * 0.7) * spawnAlpha;
125
+ }
126
+
127
+ this.drawFighter(ctx, this.damageFlash > 0);
128
+
129
+ ctx.restore();
130
+ }
131
+
132
+ /**
133
+ * Draw shadow on ground plane to help judge depth
134
+ */
135
+ drawShadow(ctx, camera, resScale, groundY, spawnAlpha = 1) {
136
+ // Project shadow position (same X/Z but at ground level)
137
+ const shadowProjected = camera.project(this.worldX, groundY, this.worldZ);
138
+ if (shadowProjected.scale <= 0) return;
139
+
140
+ const shadowSize = this.size * 0.6 * shadowProjected.scale * resScale;
141
+ const alpha = Math.min(0.4, shadowProjected.scale * 2) * spawnAlpha;
142
+
143
+ ctx.save();
144
+ ctx.globalAlpha = alpha;
145
+ ctx.fillStyle = "#000000";
146
+
147
+ // Ellipse shadow (wider than tall for ground projection)
148
+ ctx.beginPath();
149
+ ctx.ellipse(shadowProjected.x, shadowProjected.y, shadowSize, shadowSize * 0.3, 0, 0, Math.PI * 2);
150
+ ctx.fill();
151
+
152
+ ctx.restore();
153
+ }
154
+
155
+ /**
156
+ * Draw enemy fighter shape
157
+ */
158
+ drawFighter(ctx, isFlashing = false) {
159
+ const size = this.size;
160
+ const color = isFlashing ? "#ffffff" : CONFIG.enemy.types.fighter.color;
161
+
162
+ // Main body - angular fighter shape
163
+ ctx.fillStyle = color;
164
+ ctx.beginPath();
165
+ ctx.moveTo(0, -size * 0.6); // Top point
166
+ ctx.lineTo(-size * 0.5, size * 0.3); // Bottom left
167
+ ctx.lineTo(-size * 0.2, size * 0.1); // Inner left
168
+ ctx.lineTo(0, size * 0.4); // Bottom center
169
+ ctx.lineTo(size * 0.2, size * 0.1); // Inner right
170
+ ctx.lineTo(size * 0.5, size * 0.3); // Bottom right
171
+ ctx.closePath();
172
+ ctx.fill();
173
+
174
+ // Wings
175
+ ctx.fillStyle = isFlashing ? "#ffffff" : "#aa2222";
176
+ ctx.beginPath();
177
+ ctx.moveTo(-size * 0.3, 0);
178
+ ctx.lineTo(-size * 0.8, size * 0.2);
179
+ ctx.lineTo(-size * 0.5, size * 0.3);
180
+ ctx.closePath();
181
+ ctx.fill();
182
+
183
+ ctx.beginPath();
184
+ ctx.moveTo(size * 0.3, 0);
185
+ ctx.lineTo(size * 0.8, size * 0.2);
186
+ ctx.lineTo(size * 0.5, size * 0.3);
187
+ ctx.closePath();
188
+ ctx.fill();
189
+
190
+ // Cockpit
191
+ ctx.fillStyle = "#ffff00";
192
+ ctx.beginPath();
193
+ ctx.arc(0, -size * 0.1, size * 0.12, 0, Math.PI * 2);
194
+ ctx.fill();
195
+
196
+ // Outline for visibility
197
+ ctx.strokeStyle = "#ff6666";
198
+ ctx.lineWidth = 2;
199
+ ctx.beginPath();
200
+ ctx.moveTo(0, -size * 0.6);
201
+ ctx.lineTo(-size * 0.5, size * 0.3);
202
+ ctx.lineTo(size * 0.5, size * 0.3);
203
+ ctx.closePath();
204
+ ctx.stroke();
205
+ }
206
+ }
207
+
208
+ /**
209
+ * EnemySpawner - Manages enemy spawning and lifecycle
210
+ */
211
+ export class EnemySpawner {
212
+ constructor(game, camera) {
213
+ this.game = game;
214
+ this.camera = camera;
215
+
216
+ // Enemy pool
217
+ this.pool = [];
218
+ this.active = [];
219
+
220
+ // Pre-allocate enemy pool
221
+ for (let i = 0; i < 30; i++) {
222
+ this.pool.push(new Enemy());
223
+ }
224
+
225
+ // Spawn timing
226
+ this.spawnTimer = 0;
227
+ this.nextSpawnTime = CONFIG.enemy.spawnInterval;
228
+ }
229
+
230
+ update(dt) {
231
+ // Spawn new enemies
232
+ this.spawnTimer += dt;
233
+ if (this.spawnTimer >= this.nextSpawnTime) {
234
+ this.spawnWave();
235
+ this.spawnTimer = 0;
236
+ this.nextSpawnTime = CONFIG.enemy.spawnInterval +
237
+ (Math.random() - 0.5) * CONFIG.enemy.spawnVariance * 2;
238
+ }
239
+
240
+ // Update active enemies
241
+ for (let i = this.active.length - 1; i >= 0; i--) {
242
+ const enemy = this.active[i];
243
+ enemy.update(dt);
244
+
245
+ // Return dead enemies to pool
246
+ if (!enemy.alive) {
247
+ enemy.reset();
248
+ this.pool.push(enemy);
249
+ this.active.splice(i, 1);
250
+ }
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Spawn a wave of enemies
256
+ */
257
+ spawnWave() {
258
+ const patterns = ["single", "pair", "vFormation", "line"];
259
+ const pattern = patterns[Math.floor(Math.random() * patterns.length)];
260
+
261
+ // Spawn far away (high Z = at horizon)
262
+ const spawnZ = CONFIG.enemy.spawnDistance;
263
+ // Use scaled bounds for spawn positions
264
+ const scale = this.game.scaleFactor || 1;
265
+ const bounds = {
266
+ x: CONFIG.player.bounds.x * scale,
267
+ y: CONFIG.player.bounds.y * scale
268
+ };
269
+
270
+ switch (pattern) {
271
+ case "single":
272
+ // Spawn anywhere across the wide playfield
273
+ this.spawnEnemy(
274
+ (Math.random() - 0.5) * bounds.x * 1.8,
275
+ (Math.random() - 0.5) * bounds.y,
276
+ spawnZ
277
+ );
278
+ break;
279
+
280
+ case "pair":
281
+ // Wide pair spread across arena
282
+ this.spawnEnemy(-bounds.x * 0.7, 0, spawnZ);
283
+ this.spawnEnemy(bounds.x * 0.7, 0, spawnZ);
284
+ break;
285
+
286
+ case "vFormation":
287
+ // Spread V formation across wider area
288
+ this.spawnEnemy(0, -bounds.y * 0.3, spawnZ);
289
+ this.spawnEnemy(-bounds.x * 0.6, bounds.y * 0.1, spawnZ + 100);
290
+ this.spawnEnemy(bounds.x * 0.6, bounds.y * 0.1, spawnZ + 100);
291
+ break;
292
+
293
+ case "line":
294
+ // Line spread across full width
295
+ for (let i = 0; i < 4; i++) {
296
+ this.spawnEnemy(
297
+ -bounds.x * 0.8 + i * (bounds.x * 0.53),
298
+ 0,
299
+ spawnZ + i * 80
300
+ );
301
+ }
302
+ break;
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Spawn a single enemy at position
308
+ */
309
+ spawnEnemy(x, y, z) {
310
+ let enemy;
311
+
312
+ if (this.pool.length > 0) {
313
+ enemy = this.pool.pop();
314
+ } else {
315
+ enemy = new Enemy();
316
+ }
317
+
318
+ enemy.init(x, y, z);
319
+ this.active.push(enemy);
320
+ }
321
+
322
+ render() {
323
+ const ctx = Painter.ctx;
324
+ const resScale = this.game.scaleFactor || 1;
325
+ const groundY = CONFIG.terrain.yPosition;
326
+
327
+ // Sort by Z for proper depth rendering (furthest first = highest Z first)
328
+ this.active.sort((a, b) => b.worldZ - a.worldZ);
329
+
330
+ for (const enemy of this.active) {
331
+ enemy.render(ctx, this.camera, resScale, groundY);
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Get all active enemies for collision detection
337
+ */
338
+ getActiveEnemies() {
339
+ return this.active;
340
+ }
341
+
342
+ /**
343
+ * Reset all enemies (on game restart)
344
+ */
345
+ reset() {
346
+ for (const enemy of this.active) {
347
+ enemy.reset();
348
+ this.pool.push(enemy);
349
+ }
350
+ this.active = [];
351
+ this.spawnTimer = 0;
352
+ }
353
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * HUD - Heads-Up Display for StarFaux
3
+ *
4
+ * Displays score and health. The crosshair is rendered by
5
+ * the Player class since it follows the ship position.
6
+ */
7
+
8
+ import { GameObject, Painter } from "/gcanvas.es.min.js";
9
+ import { CONFIG } from "./config.js";
10
+
11
+ export class HUD extends GameObject {
12
+ constructor(game) {
13
+ super(game, {});
14
+
15
+ this.score = 0;
16
+ this.health = CONFIG.player.maxHealth;
17
+ this.maxHealth = CONFIG.player.maxHealth;
18
+ }
19
+
20
+ setScore(score) {
21
+ this.score = score;
22
+ }
23
+
24
+ setHealth(health) {
25
+ this.health = health;
26
+ }
27
+
28
+ draw() {
29
+ const ctx = Painter.ctx;
30
+ const scale = this.game.scaleFactor || 1;
31
+ const margin = CONFIG.hud.margin * scale;
32
+
33
+ ctx.save();
34
+
35
+ // Score display (top-left) - scale font size with resolution
36
+ ctx.fillStyle = CONFIG.colors.hud;
37
+ const fontSize = 20 * scale;
38
+ ctx.font = `bold ${fontSize}px monospace`;
39
+ ctx.textAlign = "left";
40
+ ctx.textBaseline = "top";
41
+ ctx.fillText(`SCORE: ${this.score}`, margin, margin);
42
+
43
+ // Health display (bottom-center) - in the UI area below terrain
44
+ this.drawHealth(ctx, this.game.width / 2, this.game.height - margin - 40 * scale, scale);
45
+
46
+ ctx.restore();
47
+ }
48
+
49
+ /**
50
+ * Draw health indicator as shield bars (centered at bottom)
51
+ */
52
+ drawHealth(ctx, x, y, scale = 1) {
53
+ const fontSize = 16 * scale;
54
+ ctx.font = `bold ${fontSize}px monospace`;
55
+ ctx.textAlign = "center";
56
+ ctx.fillStyle = CONFIG.colors.hud;
57
+ ctx.fillText("SHIELD", x, y);
58
+
59
+ const barWidth = 120 * scale;
60
+ const barHeight = 14 * scale;
61
+ const barX = x - barWidth / 2;
62
+ const barY = y + 20 * scale;
63
+
64
+ // Background bar
65
+ ctx.fillStyle = "#222222";
66
+ ctx.fillRect(barX, barY, barWidth, barHeight);
67
+
68
+ // Health bar
69
+ const healthPercent = this.health / this.maxHealth;
70
+ ctx.fillStyle = healthPercent > 0.3 ? CONFIG.colors.hud : "#ff4444";
71
+ ctx.fillRect(barX, barY, barWidth * healthPercent, barHeight);
72
+
73
+ // Border
74
+ ctx.strokeStyle = CONFIG.colors.hud;
75
+ ctx.lineWidth = 2 * scale;
76
+ ctx.strokeRect(barX, barY, barWidth, barHeight);
77
+ }
78
+ }