@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.
- package/dist/CNAME +1 -0
- package/dist/animations.html +31 -0
- package/dist/basic.html +38 -0
- package/dist/baskara.html +31 -0
- package/dist/bezier.html +35 -0
- package/dist/beziersignature.html +29 -0
- package/dist/blackhole.html +28 -0
- package/dist/blob.html +35 -0
- package/dist/coordinates.html +698 -0
- package/dist/cube3d.html +23 -0
- package/dist/demos.css +303 -0
- package/dist/dino.html +42 -0
- package/dist/easing.html +28 -0
- package/dist/events.html +195 -0
- package/dist/fluent.html +647 -0
- package/dist/fluid-simple.html +22 -0
- package/dist/fluid.html +37 -0
- package/dist/fractals.html +36 -0
- package/dist/gameobjects.html +626 -0
- package/dist/gcanvas.es.js +517 -0
- package/dist/gcanvas.es.min.js +1 -1
- package/dist/gcanvas.umd.js +1 -1
- package/dist/gcanvas.umd.min.js +1 -1
- package/dist/genart.html +26 -0
- package/dist/gendream.html +26 -0
- package/dist/group.html +36 -0
- package/dist/home.html +587 -0
- package/dist/hyperbolic001.html +23 -0
- package/dist/hyperbolic002.html +23 -0
- package/dist/hyperbolic003.html +23 -0
- package/dist/hyperbolic004.html +23 -0
- package/dist/hyperbolic005.html +22 -0
- package/dist/index.html +398 -0
- package/dist/isometric.html +34 -0
- package/dist/js/animations.js +452 -0
- package/dist/js/basic.js +204 -0
- package/dist/js/baskara.js +751 -0
- package/dist/js/bezier.js +692 -0
- package/dist/js/beziersignature.js +241 -0
- package/dist/js/blackhole/accretiondisk.obj.js +379 -0
- package/dist/js/blackhole/blackhole.obj.js +318 -0
- package/dist/js/blackhole/index.js +409 -0
- package/dist/js/blackhole/particle.js +56 -0
- package/dist/js/blackhole/starfield.obj.js +218 -0
- package/dist/js/blob.js +2276 -0
- package/dist/js/coordinates.js +840 -0
- package/dist/js/cube3d.js +789 -0
- package/dist/js/dino.js +1420 -0
- package/dist/js/easing.js +477 -0
- package/dist/js/fluent.js +183 -0
- package/dist/js/fluid-simple.js +253 -0
- package/dist/js/fluid.js +527 -0
- package/dist/js/fractals.js +932 -0
- package/dist/js/fractalworker.js +93 -0
- package/dist/js/gameobjects.js +176 -0
- package/dist/js/genart.js +268 -0
- package/dist/js/gendream.js +209 -0
- package/dist/js/group.js +140 -0
- package/dist/js/hyperbolic001.js +310 -0
- package/dist/js/hyperbolic002.js +388 -0
- package/dist/js/hyperbolic003.js +319 -0
- package/dist/js/hyperbolic004.js +345 -0
- package/dist/js/hyperbolic005.js +340 -0
- package/dist/js/info-toggle.js +25 -0
- package/dist/js/isometric.js +863 -0
- package/dist/js/kerr.js +1547 -0
- package/dist/js/lavalamp.js +590 -0
- package/dist/js/layout.js +354 -0
- package/dist/js/mondrian.js +285 -0
- package/dist/js/opacity.js +275 -0
- package/dist/js/painter.js +484 -0
- package/dist/js/particles-showcase.js +514 -0
- package/dist/js/particles.js +299 -0
- package/dist/js/patterns.js +397 -0
- package/dist/js/penrose/artifact.js +69 -0
- package/dist/js/penrose/blackhole.js +121 -0
- package/dist/js/penrose/constants.js +73 -0
- package/dist/js/penrose/game.js +943 -0
- package/dist/js/penrose/lore.js +278 -0
- package/dist/js/penrose/penrosescene.js +892 -0
- package/dist/js/penrose/ship.js +216 -0
- package/dist/js/penrose/sounds.js +211 -0
- package/dist/js/penrose/voidparticle.js +55 -0
- package/dist/js/penrose/voidscene.js +258 -0
- package/dist/js/penrose/voidship.js +144 -0
- package/dist/js/penrose/wormhole.js +46 -0
- package/dist/js/pipeline.js +555 -0
- package/dist/js/plane3d.js +256 -0
- package/dist/js/platformer.js +1579 -0
- package/dist/js/scene.js +304 -0
- package/dist/js/scenes.js +320 -0
- package/dist/js/schrodinger.js +410 -0
- package/dist/js/schwarzschild.js +1015 -0
- package/dist/js/shapes.js +628 -0
- package/dist/js/space/alien.js +171 -0
- package/dist/js/space/boom.js +98 -0
- package/dist/js/space/boss.js +353 -0
- package/dist/js/space/buff.js +73 -0
- package/dist/js/space/bullet.js +102 -0
- package/dist/js/space/constants.js +85 -0
- package/dist/js/space/game.js +1884 -0
- package/dist/js/space/hud.js +112 -0
- package/dist/js/space/laserbeam.js +179 -0
- package/dist/js/space/lightning.js +277 -0
- package/dist/js/space/minion.js +192 -0
- package/dist/js/space/missile.js +212 -0
- package/dist/js/space/player.js +430 -0
- package/dist/js/space/powerup.js +90 -0
- package/dist/js/space/starfield.js +58 -0
- package/dist/js/space/starpower.js +90 -0
- package/dist/js/spacetime.js +559 -0
- package/dist/js/sphere3d.js +229 -0
- package/dist/js/sprite.js +473 -0
- package/dist/js/starfaux/config.js +118 -0
- package/dist/js/starfaux/enemy.js +353 -0
- package/dist/js/starfaux/hud.js +78 -0
- package/dist/js/starfaux/index.js +482 -0
- package/dist/js/starfaux/laser.js +182 -0
- package/dist/js/starfaux/player.js +468 -0
- package/dist/js/starfaux/terrain.js +560 -0
- package/dist/js/study001.js +275 -0
- package/dist/js/study002.js +366 -0
- package/dist/js/study003.js +331 -0
- package/dist/js/study004.js +389 -0
- package/dist/js/study005.js +209 -0
- package/dist/js/study006.js +194 -0
- package/dist/js/study007.js +192 -0
- package/dist/js/study008.js +413 -0
- package/dist/js/svgtween.js +204 -0
- package/dist/js/tde/accretiondisk.js +471 -0
- package/dist/js/tde/blackhole.js +219 -0
- package/dist/js/tde/blackholescene.js +209 -0
- package/dist/js/tde/config.js +59 -0
- package/dist/js/tde/index.js +820 -0
- package/dist/js/tde/jets.js +290 -0
- package/dist/js/tde/lensedstarfield.js +154 -0
- package/dist/js/tde/tdestar.js +297 -0
- package/dist/js/tde/tidalstream.js +372 -0
- package/dist/js/tde_old/blackhole.obj.js +354 -0
- package/dist/js/tde_old/debris.obj.js +791 -0
- package/dist/js/tde_old/flare.obj.js +239 -0
- package/dist/js/tde_old/index.js +448 -0
- package/dist/js/tde_old/star.obj.js +812 -0
- package/dist/js/tetris/config.js +157 -0
- package/dist/js/tetris/grid.js +286 -0
- package/dist/js/tetris/index.js +1195 -0
- package/dist/js/tetris/renderer.js +634 -0
- package/dist/js/tetris/tetrominos.js +280 -0
- package/dist/js/tiles.js +312 -0
- package/dist/js/tweendemo.js +79 -0
- package/dist/js/visibility.js +102 -0
- package/dist/kerr.html +28 -0
- package/dist/lavalamp.html +27 -0
- package/dist/layouts.html +37 -0
- package/dist/logo.svg +4 -0
- package/dist/loop.html +84 -0
- package/dist/mondrian.html +32 -0
- package/dist/og_image.png +0 -0
- package/dist/opacity.html +36 -0
- package/dist/painter.html +39 -0
- package/dist/particles-showcase.html +28 -0
- package/dist/particles.html +24 -0
- package/dist/patterns.html +33 -0
- package/dist/penrose-game.html +31 -0
- package/dist/pipeline.html +737 -0
- package/dist/plane3d.html +24 -0
- package/dist/platformer.html +43 -0
- package/dist/scene.html +33 -0
- package/dist/scenes.html +96 -0
- package/dist/schrodinger.html +27 -0
- package/dist/schwarzschild.html +27 -0
- package/dist/shapes.html +16 -0
- package/dist/space.html +85 -0
- package/dist/spacetime.html +27 -0
- package/dist/sphere3d.html +24 -0
- package/dist/sprite.html +18 -0
- package/dist/starfaux.html +22 -0
- package/dist/study001.html +23 -0
- package/dist/study002.html +23 -0
- package/dist/study003.html +23 -0
- package/dist/study004.html +23 -0
- package/dist/study005.html +22 -0
- package/dist/study006.html +24 -0
- package/dist/study007.html +24 -0
- package/dist/study008.html +22 -0
- package/dist/svgtween.html +29 -0
- package/dist/tde.html +28 -0
- package/dist/tetris3d.html +25 -0
- package/dist/tiles.html +28 -0
- package/dist/transforms.html +400 -0
- package/dist/tween.html +45 -0
- package/dist/visibility.html +33 -0
- 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
|
+
}
|