@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,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Player - The Arwing-style player ship for StarFaux
|
|
3
|
+
*
|
|
4
|
+
* The ship is viewed from BEHIND (3rd person), positioned in the
|
|
5
|
+
* lower portion of the screen. Player moves left/right/up/down only.
|
|
6
|
+
* Crosshair is locked to ship position - lasers fire straight ahead.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Keys, Painter } from "/gcanvas.es.min.js";
|
|
10
|
+
import { CONFIG } from "./config.js";
|
|
11
|
+
|
|
12
|
+
export class Player {
|
|
13
|
+
constructor(game, camera) {
|
|
14
|
+
this.game = game;
|
|
15
|
+
this.camera = camera;
|
|
16
|
+
|
|
17
|
+
// Ship position offset from center (keyboard controlled)
|
|
18
|
+
this.offsetX = 0;
|
|
19
|
+
this.offsetY = 0;
|
|
20
|
+
|
|
21
|
+
// Velocity for physics-based movement
|
|
22
|
+
this.velocityX = 0;
|
|
23
|
+
this.velocityY = 0;
|
|
24
|
+
|
|
25
|
+
// Visual banking (rotation based on horizontal movement)
|
|
26
|
+
this.bankAngle = 0;
|
|
27
|
+
this.targetBank = 0;
|
|
28
|
+
|
|
29
|
+
// Pitch angle (tilt up/down based on vertical movement)
|
|
30
|
+
this.pitchAngle = 0;
|
|
31
|
+
|
|
32
|
+
// Shooting
|
|
33
|
+
this.fireTimer = 0;
|
|
34
|
+
|
|
35
|
+
// Health and invincibility
|
|
36
|
+
this.health = CONFIG.player.maxHealth;
|
|
37
|
+
this.isInvincible = false;
|
|
38
|
+
this.invincibilityTimer = 0;
|
|
39
|
+
|
|
40
|
+
// Visual flash when invincible
|
|
41
|
+
this.flashTimer = 0;
|
|
42
|
+
this.visible = true;
|
|
43
|
+
|
|
44
|
+
// Calculate scaled bounds
|
|
45
|
+
this.updateBounds();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Update movement bounds based on current screen size
|
|
50
|
+
*/
|
|
51
|
+
updateBounds() {
|
|
52
|
+
const scale = this.game.scaleFactor || 1;
|
|
53
|
+
this.boundsX = CONFIG.player.bounds.x * scale;
|
|
54
|
+
this.boundsY = CONFIG.player.bounds.y * scale;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
update(dt) {
|
|
58
|
+
const cfg = CONFIG.player;
|
|
59
|
+
const scale = this.game.scaleFactor || 1;
|
|
60
|
+
const accel = cfg.acceleration * scale;
|
|
61
|
+
const maxSpeed = cfg.moveSpeed * scale;
|
|
62
|
+
|
|
63
|
+
// Track input for banking
|
|
64
|
+
this.targetBank = 0;
|
|
65
|
+
let inputX = 0;
|
|
66
|
+
let inputY = 0;
|
|
67
|
+
|
|
68
|
+
// Handle movement input - builds acceleration, not direct position
|
|
69
|
+
if (Keys.isDown(Keys.LEFT) || Keys.isDown(Keys.A)) {
|
|
70
|
+
inputX = -1;
|
|
71
|
+
this.targetBank = -cfg.bankAngle;
|
|
72
|
+
}
|
|
73
|
+
if (Keys.isDown(Keys.RIGHT) || Keys.isDown(Keys.D)) {
|
|
74
|
+
inputX = 1;
|
|
75
|
+
this.targetBank = cfg.bankAngle;
|
|
76
|
+
}
|
|
77
|
+
if (Keys.isDown(Keys.UP) || Keys.isDown(Keys.W)) {
|
|
78
|
+
inputY = -1; // Up = negative Y
|
|
79
|
+
}
|
|
80
|
+
if (Keys.isDown(Keys.DOWN) || Keys.isDown(Keys.S)) {
|
|
81
|
+
inputY = 1; // Down = positive Y
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Apply acceleration based on input
|
|
85
|
+
if (inputX !== 0) {
|
|
86
|
+
this.velocityX += inputX * accel * dt;
|
|
87
|
+
}
|
|
88
|
+
if (inputY !== 0) {
|
|
89
|
+
// Climbing (going up, inputY = -1) is harder than falling
|
|
90
|
+
const resistance = inputY < 0 ? cfg.climbResistance : 1.0;
|
|
91
|
+
this.velocityY += inputY * accel * resistance * dt;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Apply gravity (always pulls down = positive Y)
|
|
95
|
+
this.velocityY += cfg.gravity * scale * dt;
|
|
96
|
+
|
|
97
|
+
// Apply damping (drag) - velocity decays when no input
|
|
98
|
+
if (inputX === 0) {
|
|
99
|
+
this.velocityX *= cfg.damping;
|
|
100
|
+
}
|
|
101
|
+
if (inputY === 0) {
|
|
102
|
+
this.velocityY *= cfg.damping;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Clamp velocity to max speed
|
|
106
|
+
this.velocityX = Math.max(-maxSpeed, Math.min(maxSpeed, this.velocityX));
|
|
107
|
+
this.velocityY = Math.max(-maxSpeed, Math.min(maxSpeed, this.velocityY));
|
|
108
|
+
|
|
109
|
+
// Apply velocity to position
|
|
110
|
+
this.offsetX += this.velocityX * dt;
|
|
111
|
+
this.offsetY += this.velocityY * dt;
|
|
112
|
+
|
|
113
|
+
// Clamp position to bounds (with velocity bounce-back)
|
|
114
|
+
if (this.offsetX < -this.boundsX) {
|
|
115
|
+
this.offsetX = -this.boundsX;
|
|
116
|
+
this.velocityX *= -0.3; // Soft bounce
|
|
117
|
+
} else if (this.offsetX > this.boundsX) {
|
|
118
|
+
this.offsetX = this.boundsX;
|
|
119
|
+
this.velocityX *= -0.3;
|
|
120
|
+
}
|
|
121
|
+
if (this.offsetY < -this.boundsY) {
|
|
122
|
+
this.offsetY = -this.boundsY;
|
|
123
|
+
this.velocityY *= -0.3;
|
|
124
|
+
} else if (this.offsetY > this.boundsY) {
|
|
125
|
+
this.offsetY = this.boundsY;
|
|
126
|
+
this.velocityY *= -0.3;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Smooth banking interpolation (also affected by velocity for more natural feel)
|
|
130
|
+
const velocityBank = (this.velocityX / maxSpeed) * cfg.bankAngle * 0.5;
|
|
131
|
+
this.bankAngle += (this.targetBank + velocityBank - this.bankAngle) * cfg.bankSpeed * dt;
|
|
132
|
+
|
|
133
|
+
// Pitch based on INPUT, not velocity - returns to neutral when keys released
|
|
134
|
+
const maxPitch = 0.35; // Max pitch angle in radians
|
|
135
|
+
const targetPitch = -inputY * maxPitch; // -1 (up key) = positive pitch (nose up), 1 (down key) = negative pitch (nose down)
|
|
136
|
+
this.pitchAngle += (targetPitch - this.pitchAngle) * cfg.bankSpeed * dt;
|
|
137
|
+
|
|
138
|
+
// Handle shooting
|
|
139
|
+
this.fireTimer -= dt;
|
|
140
|
+
if (Keys.isDown(Keys.SPACE) && this.fireTimer <= 0) {
|
|
141
|
+
this.shoot();
|
|
142
|
+
this.fireTimer = cfg.fireRate;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle invincibility
|
|
146
|
+
if (this.isInvincible) {
|
|
147
|
+
this.invincibilityTimer -= dt;
|
|
148
|
+
this.flashTimer += dt;
|
|
149
|
+
|
|
150
|
+
// Flash visibility based on config blink rate
|
|
151
|
+
const blinkRate = CONFIG.player.blinkRate || 0.1;
|
|
152
|
+
this.visible = Math.floor(this.flashTimer / blinkRate) % 2 === 0;
|
|
153
|
+
|
|
154
|
+
if (this.invincibilityTimer <= 0) {
|
|
155
|
+
this.isInvincible = false;
|
|
156
|
+
this.visible = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
shoot() {
|
|
162
|
+
// Fire from ship position straight ahead
|
|
163
|
+
const cfg = CONFIG.player;
|
|
164
|
+
const resScale = this.game.scaleFactor || 1;
|
|
165
|
+
const shipY = this.offsetY + cfg.screenY * resScale;
|
|
166
|
+
this.game.fireLaser(this.offsetX, shipY);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
takeDamage() {
|
|
170
|
+
if (this.isInvincible) return;
|
|
171
|
+
|
|
172
|
+
this.health--;
|
|
173
|
+
this.isInvincible = true;
|
|
174
|
+
this.invincibilityTimer = CONFIG.player.invincibilityTime || 1.5;
|
|
175
|
+
this.flashTimer = 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
reset() {
|
|
179
|
+
this.offsetX = 0;
|
|
180
|
+
this.offsetY = 0;
|
|
181
|
+
this.velocityX = 0;
|
|
182
|
+
this.velocityY = 0;
|
|
183
|
+
this.bankAngle = 0;
|
|
184
|
+
this.pitchAngle = 0;
|
|
185
|
+
this.health = CONFIG.player.maxHealth;
|
|
186
|
+
this.isInvincible = false;
|
|
187
|
+
this.visible = true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
render() {
|
|
191
|
+
if (!this.visible) return;
|
|
192
|
+
|
|
193
|
+
const ctx = Painter.ctx;
|
|
194
|
+
const cfg = CONFIG.player;
|
|
195
|
+
|
|
196
|
+
// First render the shadow (on ground plane)
|
|
197
|
+
this.renderShadow(ctx);
|
|
198
|
+
|
|
199
|
+
// Then render the crosshair (ahead of ship, where we're aiming)
|
|
200
|
+
this.renderCrosshair(ctx);
|
|
201
|
+
|
|
202
|
+
// Then render the ship (behind crosshair, lower on screen)
|
|
203
|
+
this.renderShip(ctx);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Render shadow on the ground plane
|
|
208
|
+
*/
|
|
209
|
+
renderShadow(ctx) {
|
|
210
|
+
const camera = this.camera;
|
|
211
|
+
const cfg = CONFIG.player;
|
|
212
|
+
const resScale = this.game.scaleFactor || 1;
|
|
213
|
+
|
|
214
|
+
// Shadow is projected onto ground (y = 0)
|
|
215
|
+
const groundY = 0;
|
|
216
|
+
const shadowZ = cfg.shipZ;
|
|
217
|
+
|
|
218
|
+
// Project shadow position
|
|
219
|
+
const projected = camera.project(this.offsetX, groundY, shadowZ);
|
|
220
|
+
|
|
221
|
+
if (projected.scale <= 0) return;
|
|
222
|
+
|
|
223
|
+
// Shadow size scales with ship height above ground
|
|
224
|
+
// Higher ship = smaller, more diffuse shadow
|
|
225
|
+
const shipY = this.offsetY + cfg.screenY * resScale;
|
|
226
|
+
const heightAboveGround = Math.abs(shipY - groundY);
|
|
227
|
+
const shadowScale = Math.max(0.3, 1 - heightAboveGround / 400);
|
|
228
|
+
const shadowSize = cfg.size * projected.scale * resScale * shadowScale;
|
|
229
|
+
|
|
230
|
+
// Shadow opacity also fades with height
|
|
231
|
+
const shadowAlpha = Math.max(0.1, 0.5 * shadowScale);
|
|
232
|
+
|
|
233
|
+
ctx.save();
|
|
234
|
+
ctx.globalAlpha = shadowAlpha;
|
|
235
|
+
ctx.fillStyle = "#333333"; // Gray shadow for better visibility
|
|
236
|
+
|
|
237
|
+
// Draw ellipse shadow
|
|
238
|
+
ctx.beginPath();
|
|
239
|
+
ctx.ellipse(
|
|
240
|
+
projected.x,
|
|
241
|
+
projected.y,
|
|
242
|
+
shadowSize * 1.5, // Wider
|
|
243
|
+
shadowSize * 0.4, // Flatter (perspective)
|
|
244
|
+
0, 0, Math.PI * 2
|
|
245
|
+
);
|
|
246
|
+
ctx.fill();
|
|
247
|
+
ctx.restore();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Render the targeting crosshair (locked to ship position)
|
|
252
|
+
*/
|
|
253
|
+
renderCrosshair(ctx) {
|
|
254
|
+
const camera = this.camera;
|
|
255
|
+
const cfg = CONFIG.player;
|
|
256
|
+
const scale = this.game.scaleFactor || 1;
|
|
257
|
+
|
|
258
|
+
// Crosshair is at same X/Y as ship but further into the screen
|
|
259
|
+
const projected = camera.project(this.offsetX, this.offsetY, cfg.crosshairZ);
|
|
260
|
+
|
|
261
|
+
if (projected.scale <= 0) return;
|
|
262
|
+
|
|
263
|
+
// Scale crosshair size with resolution
|
|
264
|
+
const size = 25 * projected.scale * scale;
|
|
265
|
+
|
|
266
|
+
ctx.save();
|
|
267
|
+
ctx.translate(projected.x, projected.y);
|
|
268
|
+
|
|
269
|
+
ctx.strokeStyle = "rgba(255, 50, 50, 0.8)"; // Red crosshair
|
|
270
|
+
ctx.lineWidth = 2 * scale;
|
|
271
|
+
|
|
272
|
+
// Square bracket crosshair like StarFox
|
|
273
|
+
const half = size;
|
|
274
|
+
const corner = size * 0.3;
|
|
275
|
+
|
|
276
|
+
ctx.beginPath();
|
|
277
|
+
// Top-left corner
|
|
278
|
+
ctx.moveTo(-half, -half + corner);
|
|
279
|
+
ctx.lineTo(-half, -half);
|
|
280
|
+
ctx.lineTo(-half + corner, -half);
|
|
281
|
+
// Top-right corner
|
|
282
|
+
ctx.moveTo(half - corner, -half);
|
|
283
|
+
ctx.lineTo(half, -half);
|
|
284
|
+
ctx.lineTo(half, -half + corner);
|
|
285
|
+
// Bottom-right corner
|
|
286
|
+
ctx.moveTo(half, half - corner);
|
|
287
|
+
ctx.lineTo(half, half);
|
|
288
|
+
ctx.lineTo(half - corner, half);
|
|
289
|
+
// Bottom-left corner
|
|
290
|
+
ctx.moveTo(-half + corner, half);
|
|
291
|
+
ctx.lineTo(-half, half);
|
|
292
|
+
ctx.lineTo(-half, half - corner);
|
|
293
|
+
ctx.stroke();
|
|
294
|
+
|
|
295
|
+
ctx.restore();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Render the ship from behind (3rd person view)
|
|
300
|
+
*/
|
|
301
|
+
renderShip(ctx) {
|
|
302
|
+
const camera = this.camera;
|
|
303
|
+
const cfg = CONFIG.player;
|
|
304
|
+
const resScale = this.game.scaleFactor || 1;
|
|
305
|
+
|
|
306
|
+
// Ship is closer to camera than crosshair, and offset down on screen
|
|
307
|
+
const shipY = this.offsetY + cfg.screenY * resScale; // Push ship down visually (scaled)
|
|
308
|
+
const projected = camera.project(this.offsetX, shipY, cfg.shipZ);
|
|
309
|
+
|
|
310
|
+
if (projected.scale <= 0) return;
|
|
311
|
+
|
|
312
|
+
ctx.save();
|
|
313
|
+
ctx.translate(projected.x, projected.y);
|
|
314
|
+
|
|
315
|
+
// Apply bank rotation (left/right tilt)
|
|
316
|
+
ctx.rotate(this.bankAngle);
|
|
317
|
+
|
|
318
|
+
// Apply both perspective scale AND resolution scale
|
|
319
|
+
const baseScale = projected.scale * resScale;
|
|
320
|
+
ctx.scale(baseScale, baseScale);
|
|
321
|
+
|
|
322
|
+
// Apply pitch by skewing the Y axis (simulates 3D tilt toward/away from camera)
|
|
323
|
+
// At neutral (pitchAngle = 0): no transform applied
|
|
324
|
+
// Pitching up (positive): ship leans back, appears compressed vertically
|
|
325
|
+
// Pitching down (negative): ship leans forward
|
|
326
|
+
if (Math.abs(this.pitchAngle) > 0.01) {
|
|
327
|
+
const pitchSkew = Math.sin(this.pitchAngle) * 0.4;
|
|
328
|
+
const pitchScale = 1 - Math.abs(this.pitchAngle) * 0.3; // Compress when pitched
|
|
329
|
+
ctx.transform(1, pitchSkew, 0, pitchScale, 0, 0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Draw ship from BEHIND (we see the back of it)
|
|
333
|
+
this.drawShipFromBehind(ctx);
|
|
334
|
+
|
|
335
|
+
ctx.restore();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Draw the ship as seen from behind
|
|
340
|
+
* We see the engines, back of wings, tail
|
|
341
|
+
*/
|
|
342
|
+
drawShipFromBehind(ctx) {
|
|
343
|
+
const colors = CONFIG.colors;
|
|
344
|
+
const size = CONFIG.player.size;
|
|
345
|
+
|
|
346
|
+
// Main fuselage (back view - wider at back)
|
|
347
|
+
ctx.fillStyle = colors.player;
|
|
348
|
+
ctx.beginPath();
|
|
349
|
+
ctx.moveTo(0, -size * 0.8); // Top (nose pointing away)
|
|
350
|
+
ctx.lineTo(-size * 0.3, -size * 0.4); // Upper left
|
|
351
|
+
ctx.lineTo(-size * 0.4, size * 0.4); // Lower left
|
|
352
|
+
ctx.lineTo(0, size * 0.2); // Bottom center
|
|
353
|
+
ctx.lineTo(size * 0.4, size * 0.4); // Lower right
|
|
354
|
+
ctx.lineTo(size * 0.3, -size * 0.4); // Upper right
|
|
355
|
+
ctx.closePath();
|
|
356
|
+
ctx.fill();
|
|
357
|
+
|
|
358
|
+
// Left wing (seen from above/behind)
|
|
359
|
+
ctx.fillStyle = colors.playerAccent;
|
|
360
|
+
ctx.beginPath();
|
|
361
|
+
ctx.moveTo(-size * 0.35, -size * 0.2);
|
|
362
|
+
ctx.lineTo(-size * 1.3, size * 0.1);
|
|
363
|
+
ctx.lineTo(-size * 1.1, size * 0.3);
|
|
364
|
+
ctx.lineTo(-size * 0.4, size * 0.2);
|
|
365
|
+
ctx.closePath();
|
|
366
|
+
ctx.fill();
|
|
367
|
+
|
|
368
|
+
// Right wing
|
|
369
|
+
ctx.beginPath();
|
|
370
|
+
ctx.moveTo(size * 0.35, -size * 0.2);
|
|
371
|
+
ctx.lineTo(size * 1.3, size * 0.1);
|
|
372
|
+
ctx.lineTo(size * 1.1, size * 0.3);
|
|
373
|
+
ctx.lineTo(size * 0.4, size * 0.2);
|
|
374
|
+
ctx.closePath();
|
|
375
|
+
ctx.fill();
|
|
376
|
+
|
|
377
|
+
// Engine glows (3 engines like Arwing)
|
|
378
|
+
// Thruster intensity based on vertical movement
|
|
379
|
+
// Going UP (negative velocityY) = MORE thrust (climbing)
|
|
380
|
+
// Going DOWN (positive velocityY) = LESS thrust (falling/gliding)
|
|
381
|
+
const maxSpeed = CONFIG.player.moveSpeed * (this.game.scaleFactor || 1);
|
|
382
|
+
const thrustFactor = 1 - (this.velocityY / maxSpeed); // 0.5 to 1.5 range roughly
|
|
383
|
+
const thrustIntensity = Math.max(0.3, Math.min(2.0, thrustFactor));
|
|
384
|
+
|
|
385
|
+
// Flame length extends when thrusting hard
|
|
386
|
+
const flameLength = size * 0.15 * thrustIntensity;
|
|
387
|
+
const flameFlicker = 1 + Math.sin(Date.now() * 0.02) * 0.15; // Subtle flicker
|
|
388
|
+
|
|
389
|
+
// Color shifts from orange (idle) to bright yellow-white (full thrust)
|
|
390
|
+
const r = Math.min(255, Math.floor(255));
|
|
391
|
+
const g = Math.min(255, Math.floor(100 + thrustIntensity * 80));
|
|
392
|
+
const b = Math.min(255, Math.floor(thrustIntensity * 60));
|
|
393
|
+
const thrustColor = `rgb(${r}, ${g}, ${b})`;
|
|
394
|
+
|
|
395
|
+
ctx.fillStyle = thrustColor;
|
|
396
|
+
ctx.shadowColor = thrustIntensity > 1 ? "#ffff00" : "#ff8800";
|
|
397
|
+
ctx.shadowBlur = 10 + thrustIntensity * 10;
|
|
398
|
+
|
|
399
|
+
// Center engine (main thruster)
|
|
400
|
+
ctx.beginPath();
|
|
401
|
+
ctx.ellipse(0, size * 0.3, size * 0.15, size * 0.1 * flameFlicker, 0, 0, Math.PI * 2);
|
|
402
|
+
ctx.fill();
|
|
403
|
+
|
|
404
|
+
// Center engine flame trail (extends when climbing)
|
|
405
|
+
if (thrustIntensity > 0.8) {
|
|
406
|
+
const trailLength = flameLength * flameFlicker;
|
|
407
|
+
ctx.beginPath();
|
|
408
|
+
ctx.moveTo(-size * 0.1, size * 0.35);
|
|
409
|
+
ctx.lineTo(0, size * 0.35 + trailLength);
|
|
410
|
+
ctx.lineTo(size * 0.1, size * 0.35);
|
|
411
|
+
ctx.closePath();
|
|
412
|
+
ctx.fill();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Left engine
|
|
416
|
+
ctx.beginPath();
|
|
417
|
+
ctx.ellipse(-size * 0.5, size * 0.25, size * 0.1, size * 0.07 * flameFlicker, 0, 0, Math.PI * 2);
|
|
418
|
+
ctx.fill();
|
|
419
|
+
|
|
420
|
+
// Left engine flame trail
|
|
421
|
+
if (thrustIntensity > 0.8) {
|
|
422
|
+
const trailLength = flameLength * 0.7 * flameFlicker;
|
|
423
|
+
ctx.beginPath();
|
|
424
|
+
ctx.moveTo(-size * 0.55, size * 0.28);
|
|
425
|
+
ctx.lineTo(-size * 0.5, size * 0.28 + trailLength);
|
|
426
|
+
ctx.lineTo(-size * 0.45, size * 0.28);
|
|
427
|
+
ctx.closePath();
|
|
428
|
+
ctx.fill();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Right engine
|
|
432
|
+
ctx.beginPath();
|
|
433
|
+
ctx.ellipse(size * 0.5, size * 0.25, size * 0.1, size * 0.07 * flameFlicker, 0, 0, Math.PI * 2);
|
|
434
|
+
ctx.fill();
|
|
435
|
+
|
|
436
|
+
// Right engine flame trail
|
|
437
|
+
if (thrustIntensity > 0.8) {
|
|
438
|
+
const trailLength = flameLength * 0.7 * flameFlicker;
|
|
439
|
+
ctx.beginPath();
|
|
440
|
+
ctx.moveTo(size * 0.45, size * 0.28);
|
|
441
|
+
ctx.lineTo(size * 0.5, size * 0.28 + trailLength);
|
|
442
|
+
ctx.lineTo(size * 0.55, size * 0.28);
|
|
443
|
+
ctx.closePath();
|
|
444
|
+
ctx.fill();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
ctx.shadowBlur = 0;
|
|
448
|
+
|
|
449
|
+
// Tail fin
|
|
450
|
+
ctx.fillStyle = colors.player;
|
|
451
|
+
ctx.beginPath();
|
|
452
|
+
ctx.moveTo(0, -size * 0.8);
|
|
453
|
+
ctx.lineTo(-size * 0.08, -size * 0.3);
|
|
454
|
+
ctx.lineTo(size * 0.08, -size * 0.3);
|
|
455
|
+
ctx.closePath();
|
|
456
|
+
ctx.fill();
|
|
457
|
+
|
|
458
|
+
// Wing detail lines
|
|
459
|
+
ctx.strokeStyle = colors.player;
|
|
460
|
+
ctx.lineWidth = 2;
|
|
461
|
+
ctx.beginPath();
|
|
462
|
+
ctx.moveTo(-size * 0.5, 0);
|
|
463
|
+
ctx.lineTo(-size * 1.1, size * 0.2);
|
|
464
|
+
ctx.moveTo(size * 0.5, 0);
|
|
465
|
+
ctx.lineTo(size * 1.1, size * 0.2);
|
|
466
|
+
ctx.stroke();
|
|
467
|
+
}
|
|
468
|
+
}
|