@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,482 @@
1
+ /**
2
+ * StarFaux - A StarFox64-style rail shooter
3
+ *
4
+ * Classic on-rails shooter with:
5
+ * - Pseudo-3D perspective via Camera3D
6
+ * - Wireframe terrain + flat-shaded ships
7
+ * - Player movement, shooting, enemy waves
8
+ *
9
+ * Coordinate Convention:
10
+ * - Camera stays at Z=0, looking into positive Z (toward horizon)
11
+ * - Objects spawn far away (high positive Z) and move toward camera (Z decreases)
12
+ * - When Z <= 0, objects have passed the camera
13
+ */
14
+
15
+ import { Game, Camera3D, Painter, Keys } from "/gcanvas.es.min.js";
16
+ import { CONFIG } from "./config.js";
17
+ import { Terrain } from "./terrain.js";
18
+ import { Player } from "./player.js";
19
+ import { LaserPool } from "./laser.js";
20
+ import { EnemySpawner } from "./enemy.js";
21
+ import { HUD } from "./hud.js";
22
+
23
+ export class StarfauxGame extends Game {
24
+ constructor(canvas) {
25
+ super(canvas);
26
+ this.enableFluidSize();
27
+ this.backgroundColor = CONFIG.colors.background;
28
+ }
29
+
30
+ init() {
31
+ super.init();
32
+
33
+ // Game state
34
+ this.score = 0;
35
+ this.gameOver = false;
36
+ this.distance = 0; // Total distance "traveled"
37
+ this.explosionParticles = []; // Active explosion particles
38
+
39
+ // Camera shake
40
+ this.shakeIntensity = 0;
41
+ this.shakeDuration = 0;
42
+ this.shakeOffsetX = 0;
43
+ this.shakeOffsetY = 0;
44
+
45
+ // Calculate scale factor based on resolution
46
+ this.updateScaleFactor();
47
+
48
+ // Center of screen (for projection offset)
49
+ this.centerX = this.width / 2;
50
+ this.centerY = this.height / 2;
51
+
52
+ // Initialize Camera3D for 3D projection
53
+ // Camera positioned ABOVE ground plane - terrain fills ~80% of bottom half
54
+ const cameraHeight = -this.height * 0.35; // Less extreme = terrain stops before bottom
55
+ this.camera = new Camera3D({
56
+ perspective: CONFIG.rails.perspective * this.scaleFactor,
57
+ rotationX: CONFIG.rails.tiltX,
58
+ rotationY: 0,
59
+ x: 0,
60
+ y: cameraHeight,
61
+ z: 0,
62
+ });
63
+
64
+ // Camera target values for smooth interpolation (creates ship-world coupling)
65
+ this.cameraTargetRotX = CONFIG.rails.tiltX;
66
+ this.cameraTargetRotY = 0;
67
+
68
+ // Create terrain (wireframe grid)
69
+ this.terrain = new Terrain(this, this.camera);
70
+
71
+ // Create player ship
72
+ this.player = new Player(this, this.camera);
73
+
74
+ // Create laser pool
75
+ this.laserPool = new LaserPool(this, this.camera);
76
+
77
+ // Create enemy spawner
78
+ this.enemySpawner = new EnemySpawner(this, this.camera);
79
+
80
+ // Create HUD
81
+ this.hud = new HUD(this);
82
+ this.pipeline.add(this.hud);
83
+ }
84
+
85
+ update(dt) {
86
+ if (this.gameOver) {
87
+ // Check for restart
88
+ if (Keys.isDown(Keys.SPACE)) {
89
+ this.restart();
90
+ }
91
+ return;
92
+ }
93
+
94
+ super.update(dt);
95
+
96
+ // Track distance traveled (for spawning, terrain scrolling)
97
+ this.distance += CONFIG.rails.speed * dt;
98
+
99
+ // Update game systems
100
+ this.player.update(dt);
101
+
102
+ // Update camera to react to player movement (creates world-ship coupling)
103
+ this.updateCameraReaction(dt);
104
+
105
+ this.terrain.update(dt);
106
+ this.laserPool.update(dt);
107
+ this.enemySpawner.update(dt);
108
+
109
+ // Check collisions
110
+ this.checkCollisions();
111
+
112
+ // Update explosion particles
113
+ this.updateExplosions(dt);
114
+
115
+ // Update camera shake
116
+ this.updateShake(dt);
117
+
118
+ // Update HUD
119
+ this.hud.setScore(this.score);
120
+ this.hud.setHealth(this.player.health);
121
+ }
122
+
123
+ /**
124
+ * Update explosion particles
125
+ */
126
+ updateExplosions(dt) {
127
+ for (let i = this.explosionParticles.length - 1; i >= 0; i--) {
128
+ const p = this.explosionParticles[i];
129
+ p.age += dt;
130
+
131
+ if (p.age >= p.life) {
132
+ this.explosionParticles.splice(i, 1);
133
+ continue;
134
+ }
135
+
136
+ // Move particle
137
+ p.x += p.vx * dt;
138
+ p.y += p.vy * dt;
139
+ p.z += p.vz * dt;
140
+
141
+ // Gravity and drag
142
+ p.vy += 200 * dt;
143
+ p.vx *= 0.98;
144
+ p.vy *= 0.98;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Render explosion particles
150
+ */
151
+ renderExplosions(ctx) {
152
+ const scale = this.scaleFactor || 1;
153
+
154
+ for (const p of this.explosionParticles) {
155
+ const projected = this.camera.project(p.x, p.y, p.z);
156
+ if (projected.scale <= 0) continue;
157
+
158
+ const progress = p.age / p.life;
159
+ const alpha = 1 - progress;
160
+ const size = p.size * projected.scale * scale * (1 + progress);
161
+
162
+ ctx.save();
163
+ ctx.globalAlpha = alpha;
164
+ ctx.fillStyle = p.color;
165
+ ctx.shadowColor = p.color;
166
+ ctx.shadowBlur = 10 * scale;
167
+
168
+ ctx.beginPath();
169
+ ctx.arc(projected.x, projected.y, size, 0, Math.PI * 2);
170
+ ctx.fill();
171
+
172
+ ctx.restore();
173
+ }
174
+ }
175
+
176
+ render() {
177
+ // Clear and set up context (but don't render pipeline yet)
178
+ Painter.setContext(this.ctx);
179
+ if (this.running) {
180
+ this.clear();
181
+ }
182
+
183
+ // Render in depth order: terrain -> enemies -> explosions -> lasers -> player
184
+ Painter.useCtx((ctx) => {
185
+ ctx.save();
186
+ // Apply camera shake offset
187
+ ctx.translate(
188
+ this.centerX + this.shakeOffsetX,
189
+ this.centerY + this.shakeOffsetY
190
+ );
191
+
192
+ this.terrain.render();
193
+ this.enemySpawner.render();
194
+ this.renderExplosions(ctx);
195
+ this.laserPool.render();
196
+ this.player.render();
197
+
198
+ ctx.restore();
199
+ });
200
+
201
+ // HUD renders on top AFTER game world
202
+ this.pipeline.render();
203
+
204
+ // Game over overlay
205
+ if (this.gameOver) {
206
+ this.renderGameOver();
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Update camera rotation to react to player movement
212
+ * Only subtle vertical tilt - no horizontal rotation (looks weird)
213
+ */
214
+ updateCameraReaction(dt) {
215
+ const rails = CONFIG.rails;
216
+ const bounds = CONFIG.player.bounds;
217
+
218
+ // Calculate normalized player Y position (-1 to 1)
219
+ const normalizedY = this.player.offsetY / bounds.y;
220
+
221
+ // When player moves up, camera tilts slightly less (looking more forward)
222
+ // No X movement reaction - horizontal rotation looks weird
223
+ this.cameraTargetRotX = rails.tiltX - normalizedY * rails.cameraReactY;
224
+
225
+ // Smoothly interpolate camera to target (creates inertia feeling)
226
+ const lag = rails.cameraLag * dt;
227
+ this.camera.rotationX += (this.cameraTargetRotX - this.camera.rotationX) * lag;
228
+ // Keep Y rotation at 0
229
+ this.camera.rotationY = 0;
230
+ }
231
+
232
+ /**
233
+ * Fire a laser from the player's position straight ahead
234
+ */
235
+ fireLaser(x, y) {
236
+ // Laser starts at ship position and travels straight forward
237
+ this.laserPool.fire(x, y, CONFIG.player.shipZ);
238
+ }
239
+
240
+ /**
241
+ * Check all collisions between lasers and enemies
242
+ */
243
+ checkCollisions() {
244
+ const lasers = this.laserPool.getActiveLasers();
245
+ const enemies = this.enemySpawner.getActiveEnemies();
246
+
247
+ // Laser vs Enemy
248
+ for (const laser of lasers) {
249
+ if (!laser.alive) continue;
250
+
251
+ for (const enemy of enemies) {
252
+ if (!enemy.alive) continue;
253
+
254
+ if (this.checkCollision3D(laser, enemy)) {
255
+ laser.alive = false;
256
+ enemy.takeDamage();
257
+ if (!enemy.alive) {
258
+ this.score += enemy.scoreValue;
259
+ this.spawnExplosion(enemy.worldX, enemy.worldY, enemy.worldZ);
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ // Player vs Enemy
266
+ if (!this.player.isInvincible) {
267
+ const playerZ = CONFIG.player.shipZ;
268
+ for (const enemy of enemies) {
269
+ if (!enemy.alive) continue;
270
+
271
+ // Only check collision when enemy is near player's Z
272
+ if (Math.abs(enemy.worldZ - playerZ) < 60) {
273
+ if (this.checkPlayerCollision(enemy)) {
274
+ this.player.takeDamage();
275
+ enemy.alive = false;
276
+ this.spawnExplosion(enemy.worldX, enemy.worldY, enemy.worldZ);
277
+
278
+ if (this.player.health <= 0) {
279
+ this.triggerGameOver();
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ // Player vs Terrain - use actual ship Y position (includes screenY offset)
286
+ const shipY = this.player.offsetY + CONFIG.player.screenY * this.scaleFactor;
287
+ if (this.terrain.checkPlayerCollision(this.player.offsetX, shipY)) {
288
+ this.player.takeDamage();
289
+
290
+ // Strong bounce - set position AND velocity to push ship up
291
+ this.player.offsetY = -this.player.boundsY * 0.5; // Move to upper half of play area
292
+ this.player.velocityY = -600 * this.scaleFactor; // Strong upward velocity
293
+
294
+ // Camera shake feedback - stronger and longer
295
+ this.triggerShake(25, 0.4);
296
+
297
+ if (this.player.health <= 0) {
298
+ this.triggerGameOver();
299
+ }
300
+ }
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Trigger camera shake effect
306
+ */
307
+ triggerShake(intensity, duration) {
308
+ this.shakeIntensity = intensity * this.scaleFactor;
309
+ this.shakeDuration = duration;
310
+ this.shakeTime = 0; // Reset shake timer for consistent pattern
311
+ }
312
+
313
+ /**
314
+ * Update camera shake
315
+ */
316
+ updateShake(dt) {
317
+ if (this.shakeDuration > 0) {
318
+ this.shakeDuration -= dt;
319
+ this.shakeTime = (this.shakeTime || 0) + dt;
320
+
321
+ // Sine-based shake for smoother, more consistent rumble
322
+ const decay = Math.max(0, this.shakeDuration / 0.4);
323
+ const intensity = this.shakeIntensity * decay;
324
+ const freq = 30; // Shake frequency
325
+
326
+ this.shakeOffsetX = Math.sin(this.shakeTime * freq) * intensity;
327
+ this.shakeOffsetY = Math.cos(this.shakeTime * freq * 1.3) * intensity * 0.7;
328
+ } else {
329
+ this.shakeOffsetX = 0;
330
+ this.shakeOffsetY = 0;
331
+ }
332
+ }
333
+
334
+ /**
335
+ * 2D collision check - only X and Z matter (like classic rail shooters)
336
+ * Ignores Y height difference since lasers and enemies are on different planes visually
337
+ */
338
+ checkCollision3D(a, b) {
339
+ const dx = a.worldX - b.worldX;
340
+ const dz = a.worldZ - b.worldZ;
341
+ // Ignore Y - if laser is at same X and Z as enemy, it's a hit
342
+ const dist = Math.sqrt(dx * dx + dz * dz);
343
+ const combinedSize = (a.size + b.size);
344
+ return dist < combinedSize;
345
+ }
346
+
347
+ /**
348
+ * Check collision between player and an enemy (X only, Z already checked)
349
+ */
350
+ checkPlayerCollision(enemy) {
351
+ const dx = this.player.offsetX - enemy.worldX;
352
+ // Just check X distance - Z is already filtered before this is called
353
+ const dist = Math.abs(dx);
354
+ const combinedSize = (CONFIG.player.size + enemy.size);
355
+ return dist < combinedSize;
356
+ }
357
+
358
+ /**
359
+ * Spawn explosion effect at position
360
+ */
361
+ spawnExplosion(x, y, z) {
362
+ // Create particles for explosion
363
+ const particleCount = 12;
364
+ for (let i = 0; i < particleCount; i++) {
365
+ const angle = (i / particleCount) * Math.PI * 2 + Math.random() * 0.5;
366
+ const speed = 80 + Math.random() * 120;
367
+ this.explosionParticles.push({
368
+ x, y, z,
369
+ vx: Math.cos(angle) * speed,
370
+ vy: Math.sin(angle) * speed - 50, // Slight upward bias
371
+ vz: (Math.random() - 0.5) * 60,
372
+ life: 0.4 + Math.random() * 0.2,
373
+ age: 0,
374
+ size: 3 + Math.random() * 4,
375
+ color: Math.random() > 0.5 ? "#ffff00" : "#ff6600",
376
+ });
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Trigger game over state
382
+ */
383
+ triggerGameOver() {
384
+ this.gameOver = true;
385
+ }
386
+
387
+ /**
388
+ * Render game over screen
389
+ */
390
+ renderGameOver() {
391
+ const scale = this.scaleFactor || 1;
392
+
393
+ Painter.useCtx((ctx) => {
394
+ // Darken background
395
+ ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
396
+ ctx.fillRect(0, 0, this.width, this.height);
397
+
398
+ // Game Over text - scale fonts with resolution
399
+ ctx.fillStyle = CONFIG.colors.hud;
400
+ ctx.font = `bold ${48 * scale}px monospace`;
401
+ ctx.textAlign = "center";
402
+ ctx.fillText("GAME OVER", this.centerX, this.centerY - 40 * scale);
403
+
404
+ ctx.font = `${24 * scale}px monospace`;
405
+ ctx.fillText(`Final Score: ${this.score}`, this.centerX, this.centerY + 20 * scale);
406
+ ctx.fillText("Press SPACE to restart", this.centerX, this.centerY + 60 * scale);
407
+ });
408
+ }
409
+
410
+ /**
411
+ * Restart the game
412
+ */
413
+ restart() {
414
+ this.score = 0;
415
+ this.gameOver = false;
416
+ this.distance = 0;
417
+ this.player.reset();
418
+ this.laserPool.reset();
419
+ this.enemySpawner.reset();
420
+ }
421
+
422
+ /**
423
+ * Calculate scale factor based on current resolution vs reference
424
+ * This ensures the game looks consistent across different screen sizes
425
+ */
426
+ updateScaleFactor() {
427
+ const refWidth = CONFIG.referenceWidth;
428
+ const refHeight = CONFIG.referenceHeight;
429
+
430
+ // Use the larger dimension's ratio to ensure everything fits
431
+ // For 4K (3840x2160), this gives us ~2.0 scale factor
432
+ this.scaleFactor = Math.max(
433
+ this.width / refWidth,
434
+ this.height / refHeight
435
+ );
436
+
437
+ // Also calculate terrain width based on actual screen width
438
+ this.terrainWidth = this.width * (CONFIG.terrain.widthMultiplier || 6);
439
+ }
440
+
441
+ /**
442
+ * Handle window resize - recalculate all resolution-dependent values
443
+ */
444
+ onResize() {
445
+ this.updateScaleFactor();
446
+
447
+ // Update center coordinates
448
+ this.centerX = this.width / 2;
449
+ this.centerY = this.height / 2;
450
+
451
+ // Guard: these don't exist yet during constructor (enableFluidSize runs before init)
452
+ if (!this.camera) return;
453
+
454
+ // Update camera for new resolution
455
+ this.camera.perspective = CONFIG.rails.perspective * this.scaleFactor;
456
+ this.camera.y = -this.height * 0.35; // Camera height scales with screen
457
+
458
+ // Notify terrain of new dimensions
459
+ if (this.terrain) {
460
+ this.terrain.updateDimensions();
461
+ }
462
+
463
+ // Update player bounds for new resolution
464
+ if (this.player) {
465
+ this.player.updateBounds();
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Get scaled value - multiply by scale factor for resolution independence
471
+ */
472
+ scaled(value) {
473
+ return value * this.scaleFactor;
474
+ }
475
+ }
476
+
477
+ // Bootstrap
478
+ window.addEventListener("load", () => {
479
+ const canvas = document.getElementById("game");
480
+ const game = new StarfauxGame(canvas);
481
+ game.start();
482
+ });
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Laser - Player projectile system for StarFaux
3
+ *
4
+ * Uses object pooling for efficient bullet management.
5
+ * Lasers travel forward INTO the screen (Z increases toward horizon).
6
+ */
7
+
8
+ import { Painter } from "/gcanvas.es.min.js";
9
+ import { CONFIG } from "./config.js";
10
+
11
+ /**
12
+ * Individual laser projectile
13
+ */
14
+ class Laser {
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.age = 0;
25
+ this.size = CONFIG.laser.collisionSize || 25; // Use collision size, not visual width
26
+ }
27
+
28
+ init(x, y, z) {
29
+ this.worldX = x;
30
+ this.worldY = y;
31
+ this.worldZ = z;
32
+ this.alive = true;
33
+ this.age = 0;
34
+ }
35
+
36
+ update(dt) {
37
+ if (!this.alive) return;
38
+
39
+ // Move forward INTO screen (positive Z = toward horizon)
40
+ this.worldZ += CONFIG.laser.speed * dt;
41
+ this.age += dt;
42
+
43
+ // Check lifetime
44
+ if (this.age >= CONFIG.laser.lifetime) {
45
+ this.alive = false;
46
+ }
47
+
48
+ // Check if too far (beyond visible range)
49
+ if (this.worldZ > CONFIG.enemy.spawnDistance + 500) {
50
+ this.alive = false;
51
+ }
52
+ }
53
+
54
+ render(ctx, camera, resScale = 1) {
55
+ if (!this.alive) return;
56
+
57
+ // Project front and back of the laser bolt to get perspective line
58
+ const laserLength = 40; // Length in world units
59
+ const frontZ = this.worldZ;
60
+ const backZ = this.worldZ - laserLength; // Back is closer to camera
61
+
62
+ const front = camera.project(this.worldX, this.worldY, frontZ);
63
+ const back = camera.project(this.worldX, this.worldY, backZ);
64
+
65
+ // Don't render if both points behind camera
66
+ if (front.scale <= 0 && back.scale <= 0) return;
67
+
68
+ // Don't render if too far
69
+ if (front.scale < 0.03) return;
70
+
71
+ ctx.save();
72
+
73
+ // Glow effect - scale glow with resolution
74
+ ctx.shadowColor = CONFIG.laser.glowColor;
75
+ ctx.shadowBlur = 8 * resScale;
76
+
77
+ // Draw laser as a tapered line from back (near/big) to front (far/small)
78
+ // Scale width with resolution
79
+ const backWidth = CONFIG.laser.width * back.scale * resScale;
80
+ const frontWidth = CONFIG.laser.width * front.scale * 0.5 * resScale;
81
+
82
+ // Main laser body - trapezoid shape for perspective
83
+ ctx.fillStyle = CONFIG.laser.color;
84
+ ctx.beginPath();
85
+ ctx.moveTo(back.x - backWidth / 2, back.y); // Back left
86
+ ctx.lineTo(front.x - frontWidth / 2, front.y); // Front left
87
+ ctx.lineTo(front.x + frontWidth / 2, front.y); // Front right
88
+ ctx.lineTo(back.x + backWidth / 2, back.y); // Back right
89
+ ctx.closePath();
90
+ ctx.fill();
91
+
92
+ // Bright core line
93
+ ctx.strokeStyle = "#ffffff";
94
+ ctx.lineWidth = Math.max(1 * resScale, backWidth * 0.3);
95
+ ctx.beginPath();
96
+ ctx.moveTo(back.x, back.y);
97
+ ctx.lineTo(front.x, front.y);
98
+ ctx.stroke();
99
+
100
+ ctx.restore();
101
+ }
102
+ }
103
+
104
+ /**
105
+ * LaserPool - Manages a pool of reusable laser objects
106
+ */
107
+ export class LaserPool {
108
+ constructor(game, camera) {
109
+ this.game = game;
110
+ this.camera = camera;
111
+
112
+ // Pre-allocate laser pool
113
+ this.pool = [];
114
+ this.active = [];
115
+
116
+ for (let i = 0; i < CONFIG.laser.poolSize; i++) {
117
+ this.pool.push(new Laser());
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Fire a new laser from the given position straight ahead
123
+ */
124
+ fire(x, y, z) {
125
+ let laser;
126
+
127
+ if (this.pool.length > 0) {
128
+ laser = this.pool.pop();
129
+ } else {
130
+ // Pool exhausted, create new
131
+ laser = new Laser();
132
+ }
133
+
134
+ laser.init(x, y, z);
135
+ this.active.push(laser);
136
+ }
137
+
138
+ update(dt) {
139
+ // Update all active lasers
140
+ for (let i = this.active.length - 1; i >= 0; i--) {
141
+ const laser = this.active[i];
142
+ laser.update(dt);
143
+
144
+ // Return dead lasers to pool
145
+ if (!laser.alive) {
146
+ laser.reset();
147
+ this.pool.push(laser);
148
+ this.active.splice(i, 1);
149
+ }
150
+ }
151
+ }
152
+
153
+ render() {
154
+ const ctx = Painter.ctx;
155
+ const resScale = this.game.scaleFactor || 1;
156
+
157
+ // Sort by Z for proper depth (furthest first)
158
+ this.active.sort((a, b) => b.worldZ - a.worldZ);
159
+
160
+ for (const laser of this.active) {
161
+ laser.render(ctx, this.camera, resScale);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Get all active lasers for collision detection
167
+ */
168
+ getActiveLasers() {
169
+ return this.active;
170
+ }
171
+
172
+ /**
173
+ * Reset all lasers (on game restart)
174
+ */
175
+ reset() {
176
+ for (const laser of this.active) {
177
+ laser.reset();
178
+ this.pool.push(laser);
179
+ }
180
+ this.active = [];
181
+ }
182
+ }