@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,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
|
+
}
|