From ed7620ee7be402e996dfc47b384213967a333089 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Sun, 6 Sep 2026 17:44:19 -0600 Subject: [PATCH] Replace chevron player ship with detailed asymmetric interceptor silhoue - Introduce SHIP_HULL polygon and ENGINE_COLOR for a sharper, cyberpunk-style ship - Swap strokeChevron for drawShip, adding fuselage spine, canted canopy, dorsal-fin greebles, and a speed-reactive neon engine flare - Update lives readout to render mini ship glyphs instead of chevrons --- src/games/defender/DefenderGame.js | 75 ++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/src/games/defender/DefenderGame.js b/src/games/defender/DefenderGame.js index a73f857..c9989d3 100644 --- a/src/games/defender/DefenderGame.js +++ b/src/games/defender/DefenderGame.js @@ -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)