Back to City Breaker
SOURCE / PINNED RELEASE

Made of little things.

City Breaker

Release
94d2b8664b10…
Author-recorded commit
df23103cbcc1…
License
LICENSE
Author’s source reference
nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-cf8b858653e

Archive hash verified: eeeed725668c1ece…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/ui/hudart.ts
// Pictograms for the cockpit HUD: show, don't tell.
type C = CanvasRenderingContext2D;

/** A keyboard key cap with its letter. */
export function keycap(ctx: C, x: number, y: number, label: string, hot = false): number {
  ctx.font = '700 12px ui-monospace, monospace';
  const w = Math.max(24, ctx.measureText(label).width + 12);
  const h = 22;
  ctx.fillStyle = hot ? '#ffd36a' : '#e6eef0';
  ctx.beginPath();
  ctx.roundRect(x, y, w, h, 4);
  ctx.fill();
  ctx.fillStyle = hot ? '#9a6b14' : '#8b9799';
  ctx.fillRect(x + 2, y + h - 4, w - 4, 3);
  ctx.fillStyle = '#10181a';
  ctx.textBaseline = 'middle';
  ctx.textAlign = 'center';
  ctx.fillText(label, x + w / 2, y + h / 2 - 1);
  ctx.textAlign = 'left';
  ctx.textBaseline = 'top';
  return w;
}

export function flame(ctx: C, x: number, y: number, s: number, color = '#ff7a2a'): void {
  ctx.fillStyle = color;
  ctx.beginPath();
  ctx.moveTo(x, y - s);
  ctx.bezierCurveTo(x + s * 0.8, y - s * 0.2, x + s * 0.7, y + s * 0.8, x, y + s * 0.8);
  ctx.bezierCurveTo(x - s * 0.7, y + s * 0.8, x - s * 0.8, y - s * 0.1, x - s * 0.15, y - s * 0.45);
  ctx.closePath();
  ctx.fill();
  ctx.fillStyle = '#ffe08a';
  ctx.beginPath();
  ctx.ellipse(x, y + s * 0.35, s * 0.28, s * 0.38, 0, 0, Math.PI * 2);
  ctx.fill();
}

export function drop(ctx: C, x: number, y: number, s: number, color: string): void {
  ctx.fillStyle = color;
  ctx.beginPath();
  ctx.moveTo(x, y - s);
  ctx.bezierCurveTo(x + s * 0.9, y, x + s * 0.7, y + s * 0.85, x, y + s * 0.85);
  ctx.bezierCurveTo(x - s * 0.7, y + s * 0.85, x - s * 0.9, y, x, y - s);
  ctx.fill();
}

export function fuelCan(ctx: C, x: number, y: number, s: number, color: string): void {
  ctx.strokeStyle = color;
  ctx.fillStyle = color;
  ctx.lineWidth = 2;
  ctx.strokeRect(x - s * 0.55, y - s * 0.6, s * 0.8, s * 1.3);
  ctx.fillRect(x - s * 0.4, y - s * 0.45, s * 0.5, s * 0.35);
  ctx.beginPath();
  ctx.moveTo(x + s * 0.25, y - s * 0.3);
  ctx.lineTo(x + s * 0.6, y - s * 0.1);
  ctx.lineTo(x + s * 0.6, y + s * 0.5);
  ctx.stroke();
}

/** A bomb seen from the side (one per stick remaining). */
export function bomb(ctx: C, x: number, y: number, s: number, color: string): void {
  ctx.fillStyle = color;
  ctx.beginPath();
  ctx.ellipse(x, y, s * 0.28, s * 0.62, 0, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillRect(x - s * 0.3, y - s * 0.95, s * 0.6, s * 0.18);
}

/** Bomb-bay doors seen from below: two flaps that swing open. */
export function bayDoors(ctx: C, x: number, y: number, w: number, open: number): void {
  const h = 18;
  ctx.fillStyle = '#0a0d0e';
  ctx.fillRect(x, y, w, h);
  ctx.fillStyle = open >= 1 ? '#7dffa0' : open > 0 ? '#ffd36a' : '#8fa3a8';
  const gap = (w / 2) * open;
  ctx.fillRect(x, y, w / 2 - gap * 0.85, h);
  ctx.fillRect(x + w / 2 + gap * 0.85, y, w / 2 - gap * 0.85, h);
  if (open > 0.3) {
    ctx.fillStyle = '#c9c2a3';
    for (let i = 0; i < 3; i++) bomb(ctx, x + w / 2 - 8 + i * 8, y + h / 2 + 1, 9, '#b9b29a');
  }
  ctx.strokeStyle = 'rgba(170,240,255,0.5)';
  ctx.lineWidth = 1;
  ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
}

/** Release mode: dotted train, solid salvo, single dot. */
export function releaseMode(ctx: C, x: number, y: number, mode: string): void {
  ctx.fillStyle = '#cfe9f0';
  const n = mode === 'train' ? 5 : mode === 'salvo' ? 1 : 1;
  if (mode === 'salvo') {
    ctx.fillRect(x, y + 4, 34, 10);
    return;
  }
  for (let i = 0; i < n; i++) {
    ctx.beginPath();
    ctx.arc(x + 4 + i * (mode === 'train' ? 7 : 0), y + 9, mode === 'single' ? 5 : 3, 0, Math.PI * 2);
    ctx.fill();
  }
}

/** Formation picture: three dots close or spread. */
export function formation(ctx: C, x: number, y: number, kind: string): void {
  const k = kind === 'tight' ? 4 : kind === 'loose' ? 11 : 7;
  ctx.fillStyle = '#cfe9f0';
  for (const [dx, dy] of [[0, 0], [-k, k], [k, k]]) {
    ctx.beginPath();
    ctx.moveTo(x + 18 + dx + 4, y + 4 + dy);
    ctx.lineTo(x + 18 + dx - 3, y + 1 + dy);
    ctx.lineTo(x + 18 + dx - 3, y + 7 + dy);
    ctx.closePath();
    ctx.fill();
  }
}

/** Vertical gauge with a fill level and a danger zone at the bottom. */
export function gauge(ctx: C, x: number, y: number, w: number, h: number, v: number, color: string, danger = 0): void {
  ctx.fillStyle = 'rgba(0,0,0,0.6)';
  ctx.fillRect(x, y, w, h);
  if (danger > 0) {
    ctx.fillStyle = 'rgba(255,60,40,0.35)';
    ctx.fillRect(x, y + h * (1 - danger), w, h * danger);
  }
  const f = Math.max(0, Math.min(1, v));
  ctx.fillStyle = color;
  ctx.fillRect(x + 2, y + h * (1 - f) + 1, w - 4, h * f - 2);
  ctx.strokeStyle = 'rgba(170,240,255,0.45)';
  ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
}