Defender #12

Merged
brianfertig merged 3 commits from Defender into main 2026-09-06 23:45:43 +00:00
1 changed files with 66 additions and 9 deletions
Showing only changes of commit ed7620ee7b - Show all commits

View File

@ -44,6 +44,20 @@ const ALERT_PINK = 0xff17c4;
// the red enemy shots, so incoming vs. outgoing fire is unmistakable at a glance.
const PLAYER_SHOT_COLOR = 0xffe135;
// Fixed neon engine-glow accent for the player ship — independent of the level
// palette, like PLAYER_SHOT_COLOR, so the thruster always reads as "engine".
const ENGINE_COLOR = 0x00e5ff;
// Player ship silhouette, in unit space (nose at +x; mirrored by `facing`,
// scaled by radius) — an asymmetric interceptor: a long swept dorsal fin
// against a short ventral one, with concave "blade" notches at both wing
// roots and a spiked tail, for a sharper silhouette than a plain dart.
const SHIP_HULL = [
[1.35, 0], [0.75, -0.16], [0.30, -0.40], [-0.10, -0.30], [-1.15, -0.92],
[-0.50, -0.26], [-0.80, -0.14], [-1.05, 0], [-0.80, 0.14], [-0.50, 0.26],
[-0.95, 0.68], [-0.10, 0.30], [0.30, 0.40], [0.75, 0.16],
];
// Mini-map panel — a squashed side-view of the whole wrapped world, tucked
// under MusicPlayer's buttons + track-name readout (src/ui/MusicPlayer.js:
// PAD=12, BTN=32 buttons at y=12-44, track info text at y=58, so the music
@ -213,10 +227,10 @@ export default class DefenderGame extends Phaser.Scene {
g.clear();
const pal = paletteFor(sim.level);
// Lives as little chevron glyphs under the score.
// Lives as tiny ship glyphs under the score, matching the real thing.
for (let i = 0; i < Math.min(sim.lives, 8); i += 1) {
const x = 46 + i * 34; const y = 108;
this.strokeChevron(g, x, y, 12, 1, pal.accent, [[5, 0.18], [2, 1]]);
const x = 50 + i * 38; const y = 108;
this.drawShip(g, x, y, 12, 1, pal.accent, ENGINE_COLOR, 0, [[5, 0.18], [2, 1]]);
}
// Overdrive meter bar.
@ -339,17 +353,58 @@ export default class DefenderGame extends Phaser.Scene {
this.camX = wrap(this.camX + d * 0.14);
}
strokeChevron(g, x, y, r, facing, color, passes) {
// A sleek, asymmetric interceptor silhouette in place of the old plain dart:
// a long swept dorsal fin against a shorter ventral one, concave "blade" cuts
// at each wing root, a canted canopy, and a speed-reactive engine flare —
// aiming for sharp/edgy cyberpunk rather than a generic arcade ship.
drawShip(g, x, y, r, facing, color, engineColor, thrust, passes) {
const P = (ux, uy) => ({ x: x + facing * ux * r, y: y + uy * r });
const hull = SHIP_HULL.map(([ux, uy]) => P(ux, uy));
for (const [lw, a] of passes) {
g.lineStyle(lw, color, a);
g.beginPath();
g.moveTo(x - facing * r * 0.8, y - r * 0.7);
g.lineTo(x + facing * r, y);
g.lineTo(x - facing * r * 0.8, y + r * 0.7);
g.lineTo(x - facing * r * 0.3, y);
hull.forEach((p, i) => { if (i === 0) g.moveTo(p.x, p.y); else g.lineTo(p.x, p.y); });
g.closePath();
g.strokePath();
}
// Fuselage spine.
const spineA = P(0.9, 0); const spineB = P(-0.8, 0);
g.lineStyle(passes[passes.length - 1][0] * 0.7, color, 0.3);
g.lineBetween(spineA.x, spineA.y, spineB.x, spineB.y);
// Canted canopy — a filled, glassy diamond just aft of the nose.
const canopy = [[0.63, 0], [0.45, -0.11], [0.27, 0], [0.45, 0.11]].map(([ux, uy]) => P(ux, uy));
g.beginPath();
canopy.forEach((p, i) => { if (i === 0) g.moveTo(p.x, p.y); else g.lineTo(p.x, p.y); });
g.closePath();
g.fillStyle(color, 0.3);
g.fillPath();
g.lineStyle(1.6, 0xffffff, 0.85);
g.strokePath();
// Two greeble ticks along the long dorsal fin for mechanical detail.
const wingA = P(-0.1, -0.3); const wingB = P(-1.15, -0.92);
const wdx = wingB.x - wingA.x; const wdy = wingB.y - wingA.y;
const wlen = Math.hypot(wdx, wdy) || 1;
const nx = -wdy / wlen; const ny = wdx / wlen;
for (const t of [0.35, 0.7]) {
const mx = wingA.x + wdx * t; const my = wingA.y + wdy * t;
g.lineStyle(1.4, color, 0.6);
g.lineBetween(mx - nx * r * 0.12, my - ny * r * 0.12, mx + nx * r * 0.12, my + ny * r * 0.12);
}
// Engine flare — a fixed neon accent (not the level palette) so the
// thruster always reads as "engine", and it stretches with current speed.
const tail = P(-1.05, 0);
const flareLen = r * (0.5 + thrust * 1.5);
g.lineStyle(7, engineColor, 0.16);
g.lineBetween(tail.x, tail.y, tail.x - facing * flareLen, tail.y);
g.lineStyle(2.4, engineColor, 0.9);
g.lineBetween(tail.x, tail.y, tail.x - facing * flareLen * 0.65, tail.y);
g.fillStyle(engineColor, 0.95);
g.fillCircle(tail.x, tail.y, r * 0.14);
}
strokeNgon(g, x, y, r, sides, rot = 0) {
@ -509,7 +564,9 @@ export default class DefenderGame extends Phaser.Scene {
if (p.alive && (Math.floor(p.invulnMs / 90) % 2 === 0 || p.invulnMs <= 0)) {
const sx = this.screenX(p.x);
const color = this.sim.overdriveActive ? 0xffffff : pal.accent;
this.strokeChevron(g, sx, p.y, TUNE.PLAYER_RADIUS, p.facing, color, [[6, 0.2], [2.2, 1]]);
const engineColor = this.sim.overdriveActive ? 0xffffff : ENGINE_COLOR;
const thrust = Math.min(1, Math.hypot(p.vx, p.vy) / TUNE.PLAYER_MAX_SPEED_X);
this.drawShip(g, sx, p.y, TUNE.PLAYER_RADIUS, p.facing, color, engineColor, thrust, [[6, 0.2], [2.2, 1]]);
if (p.carrying != null) {
// (drawn in drawHumanoids since the humanoid entity itself already
// tracks the carry offset each tick)