Back to Supersonic RC Revive
SOURCE / PINNED RELEASE

Made of little things.

Supersonic RC Revive

Release
1ba42f1ca1d6…
Author-recorded commit
baecad10b1cd…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-143146b0d6f

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

tools/assets/props.ts
// The custom car and props, built in the frames src/game/render/world.ts
// expects of car.glb, star.glb, prize.glb and arrow2.glb:
//  - car: node "Car" (body mesh) with children Wheel1..Wheel4. The body is drawn
//    at the rigid body × Car.visualOffset() (lift 2, turned 180° about Z,
//    scale 0.17), so the mesh's front is −Y and 1 game unit is 1/0.17 mesh units.
//    Wheels are placed by the physics; each mesh is centred, axle along X.
//  - star: in the XZ plane (stars spin about world Z), drawn at scale 3.
//  - prize: centred on its marker.
//  - arrow: flat in XY, pointing +Y, drawn at scale 0.05 with no scene lights,
//    so only its emissive colour shows.
//  - ui/carshadow.png: the blob shadow under the car.
import { writePng } from './files.ts';
import { box, cylinder, piece, type Piece, quad, tri, transformPiece, type Vec3 } from './mesh.ts';

export interface PropMaterial {
  /** sRGB hex. */
  diffuse: string;
  ambient?: string;
  emissive?: string;
}

export const CAR_MATERIALS: Record<string, PropMaterial> = {
  chassis: { diffuse: '#2a2c33' },
  body: { diffuse: '#1fa3ff' },
  bodyDark: { diffuse: '#0f5fa8' },
  stripe: { diffuse: '#ffffff' },
  canopy: { diffuse: '#14171f', emissive: '#10141c' },
  wing: { diffuse: '#ffcc1a' },
  bumper: { diffuse: '#3a3d46' },
  light: { diffuse: '#fff6c8', emissive: '#d8cf98' },
  tire: { diffuse: '#1c1c20' },
  hub: { diffuse: '#ffcc1a' },
};

export const STAR_MATERIALS: Record<string, PropMaterial> = {
  star: { diffuse: '#ffd23a', emissive: '#6a4a00' },
  starEdge: { diffuse: '#ff9a1a', emissive: '#4a2400' },
};

export const PRIZE_MATERIALS: Record<string, PropMaterial> = {
  giftBox: { diffuse: '#e8407a', emissive: '#300818' },
  ribbon: { diffuse: '#ffe14a', emissive: '#403000' },
};

export const ARROW_MATERIALS: Record<string, PropMaterial> = {
  arrowTop: { diffuse: '#000000', emissive: '#ffd02a' },
  arrowSide: { diffuse: '#000000', emissive: '#c25a00' },
};

const S = 1 / 0.17;

export function buildCar(): { body: Piece; wheel: Piece; wheelPositions: Vec3[] } {
  const body = piece('Car', 'none');
  // Game-unit sketch (front = +Y), converted to mesh space at the end.
  box(body, [-6.6, -21, -9], [6.6, 21, -7], 'chassis');
  box(body, [-5.6, -15, -7], [5.6, 12, -2.2], 'body');
  // Nose: sloping wedge from the body to the bumper.
  const nose = (x: number): Vec3[] => [[x, 12, -2.2], [x, 20, -6], [x, 20, -7], [x, 12, -7]];
  const L = nose(-5.6);
  const R = nose(5.6);
  quad(body, 'body', L[0], R[0], R[1], L[1]);
  quad(body, 'bodyDark', L[1], R[1], R[2], L[2]);
  quad(body, 'bodyDark', R[3], R[0], R[1], R[2]);
  quad(body, 'bodyDark', L[0], L[3], L[2], L[1]);
  box(body, [-1.2, -15, -2.2], [1.2, 18, -2.0], 'stripe');
  // Side pods over the wheels' inner edge.
  box(body, [-6.8, -6, -7], [-5.6, 7, -4], 'bodyDark');
  box(body, [5.6, -6, -7], [6.8, 7, -4], 'bodyDark');
  // Canopy and roll hoop.
  box(body, [-3.4, -6, -2.2], [3.4, 5, 1.6], 'canopy');
  box(body, [-3.6, -8, -2.2], [3.6, -6, 2.6], 'bumper');
  // Rear wing on two posts.
  box(body, [-3.5, -18.5, -2.2], [-2.5, -17.5, 3.5], 'bumper');
  box(body, [2.5, -18.5, -2.2], [3.5, -17.5, 3.5], 'bumper');
  box(body, [-8, -21, 3.5], [8, -16.5, 4.5], 'wing');
  box(body, [-8.2, -21.2, 1.5], [-7.8, -16.3, 5.2], 'wing');
  box(body, [7.8, -21.2, 1.5], [8.2, -16.3, 5.2], 'wing');
  // Bumpers and headlights.
  box(body, [-6.8, 20, -8.5], [6.8, 21.5, -5.5], 'bumper');
  box(body, [-6.8, -21.5, -8.5], [6.8, -20, -5.5], 'bumper');
  box(body, [-4.8, 19.9, -5.8], [-2.8, 20.2, -4.2], 'light');
  box(body, [2.8, 19.9, -5.8], [4.8, 20.2, -4.2], 'light');
  // To mesh space: turn 180° about Z, then scale up.
  transformPiece(body, ([x, y, z]) => [-x * S, -y * S, z * S], ([x, y, z]) => [-x, -y, z]);

  const wheel = piece('Wheel', 'none');
  const R0 = 5 * S;
  const W = 2.4 * S;
  cylinder(wheel, 0, 0, R0, -W, W, { sides: 'tire', top: 'hub' }, 18);
  cylinder(wheel, 0, 0, R0 * 0.55, -W - 0.4 * S, W + 0.4 * S, { sides: 'hub', top: 'hub' }, 10);
  // The other cap of each cylinder (cylinder() has no bottom).
  for (const [r, z] of [[R0, -W], [R0 * 0.55, -W - 0.4 * S]] as const) {
    for (let i = 0; i < 18; i++) {
      const a = (i / 18) * Math.PI * 2;
      const b = ((i + 1) / 18) * Math.PI * 2;
      tri(wheel, 'hub', [0, 0, z], [Math.cos(b) * r, Math.sin(b) * r, z], [Math.cos(a) * r, Math.sin(a) * r, z]);
    }
  }
  // Axle along X: (x, y, z) → (z, x, y).
  transformPiece(wheel, ([x, y, z]) => [z, x, y], ([x, y, z]) => [z, x, y]);
  const wheelPositions: Vec3[] = [
    [56, -100, -53],
    [-56, -100, -53],
    [-56, 100, -53],
    [56, 100, -53],
  ];
  return { body, wheel, wheelPositions };
}

export function buildStar(): Piece {
  const star = piece('Star', 'none');
  const outer = 13;
  const inner = 5.5;
  const t = 2.5;
  const pts: Array<[number, number]> = [];
  for (let i = 0; i < 10; i++) {
    const a = Math.PI / 2 + (i / 10) * Math.PI * 2;
    const r = i % 2 ? inner : outer;
    pts.push([Math.cos(a) * r, Math.sin(a) * r]);
  }
  // Faces in the XZ plane: front (−Y) and back (+Y) fans, then the rim.
  for (let i = 0; i < 10; i++) {
    const [ax, az] = pts[i];
    const [bx, bz] = pts[(i + 1) % 10];
    tri(star, 'star', [0, -t, 0], [bx, -t * 0.4, bz], [ax, -t * 0.4, az]);
    tri(star, 'star', [0, t, 0], [ax, t * 0.4, az], [bx, t * 0.4, bz]);
    quad(star, 'starEdge', [ax, -t * 0.4, az], [bx, -t * 0.4, bz], [bx, t * 0.4, bz], [ax, t * 0.4, az]);
  }
  return star;
}

export function buildPrize(): Piece {
  const p = piece('prizesbox', 'none');
  const s = 24;
  box(p, [-s, -s, -s], [s, s, s], 'giftBox');
  box(p, [-s - 1, -5, -s - 1], [s + 1, 5, s + 1], 'ribbon');
  box(p, [-5, -s - 1, -s - 1], [5, s + 1, s + 1], 'ribbon');
  // A bow on top.
  box(p, [-12, -4, s + 1], [-2, 4, s + 8], 'ribbon');
  box(p, [2, -4, s + 1], [12, 4, s + 8], 'ribbon');
  return p;
}

export function buildArrow(): Piece {
  const p = piece('arrow', 'none');
  const z = 6.75;
  const outline: Array<[number, number]> = [[-24, -80], [24, -80], [24, 10], [60, 10], [0, 80.6], [-60, 10], [-24, 10]];
  // Top face: the shaft quad and the head triangle.
  quad(p, 'arrowTop', [-24, -80, z], [24, -80, z], [24, 10, z], [-24, 10, z]);
  tri(p, 'arrowTop', [-60, 10, z], [60, 10, z], [0, 80.6, z]);
  quad(p, 'arrowTop', [-24, 10, -z], [24, 10, -z], [24, -80, -z], [-24, -80, -z]);
  tri(p, 'arrowTop', [60, 10, -z], [-60, 10, -z], [0, 80.6, -z]);
  for (let i = 0; i < outline.length; i++) {
    const [ax, ay] = outline[i];
    const [bx, by] = outline[(i + 1) % outline.length];
    quad(p, 'arrowSide', [ax, ay, -z], [bx, by, -z], [bx, by, z], [ax, ay, z]);
  }
  return p;
}

/** The blob shadow: a soft rounded rectangle in the alpha channel. */
export function carShadow(): Buffer {
  const W = 32;
  const H = 64;
  const out = new Uint8Array(W * H * 4);
  for (let y = 0; y < H; y++) {
    for (let x = 0; x < W; x++) {
      // Distance outside a rounded rectangle, in normalised units.
      const dx = Math.max(0, Math.abs((x + 0.5) / W - 0.5) * 2 - 0.45) / 0.55;
      const dy = Math.max(0, Math.abs((y + 0.5) / H - 0.5) * 2 - 0.7) / 0.3;
      const d = Math.min(1, Math.hypot(dx, dy));
      out[(y * W + x) * 4 + 3] = Math.round(255 * (1 - d * d * (3 - 2 * d)));
    }
  }
  return writePng(W, H, out);
}