Back to Supersonic RC Revive
SOURCE / PINNED RELEASE

Made of little things.

Supersonic RC Revive

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

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

tools/assets/level.ts
// "Bedroom Grand Prix": the custom level. An original layout (not traced from
// the 2004 game) sized for the ported car: top speed ≈ 460 in/s, checkpoint
// pass radius 300, ≤ 10 s between checkpoints in Checkpoint Chase.
//
// Room: x −4500…4500, y −6500…6500, floor z = 0, walls 1600 high. One lap
// runs anticlockwise from the start line on the east side:
//   1 toy-block chicane → 2 up the ramp onto the toy chest → down to 3 (NE)
//   → 4 over the book-gap jump → 5 (NW, quarter pipe nearby) → 6 → 7 on the
//   bridge (or under it) → 8 (SW) → 9 through the shoebox tunnel → 10 cone
//   slalom → back past the start.
import { box, cylinder, piece, type Piece, quad, quarterPipe, ramp, type Vec3 } from './mesh.ts';
import { rng } from './textures.ts';

export interface Marker {
  name: string;
  position: Vec3;
  /** Heading in degrees about +Z (0 = facing +Y). Only the start uses it. */
  heading?: number;
}

export interface LevelMaterial {
  /** sRGB hex. */
  color: string;
  texture?: string;
}

export const LEVEL_MATERIALS: Record<string, LevelMaterial> = {
  floor: { color: '#ffffff', texture: 'planks' },
  wall: { color: '#a9c8e8' },
  ceiling: { color: '#f3f0e8' },
  skirting: { color: '#f4f1ea' },
  rug: { color: '#ffffff', texture: 'rug' },
  checker: { color: '#ffffff', texture: 'checker' },
  stripes: { color: '#ffffff', texture: 'stripes' },
  chest: { color: '#c0503c' },
  chestTop: { color: '#e8b04a' },
  rampTop: { color: '#3f86d8' },
  rampSide: { color: '#2a5a9c' },
  blockRed: { color: '#e04848' },
  blockBlue: { color: '#3c7ce0' },
  blockYellow: { color: '#f2c230' },
  blockGreen: { color: '#46b45a' },
  bookRed: { color: '#b8343c' },
  bookGreen: { color: '#2f7d54' },
  bookNavy: { color: '#2c3c78' },
  bookCream: { color: '#eadcb8' },
  pipe: { color: '#40b8b0' },
  bridge: { color: '#8e5cc8' },
  pillar: { color: '#6a3fa0' },
  shoebox: { color: '#e6dccb' },
  shoeboxLid: { color: '#d0573f' },
  cone: { color: '#ff7a1c' },
  shelf: { color: '#9a6a44' },
};

const BLOCKS = ['blockRed', 'blockBlue', 'blockYellow', 'blockGreen'];
const BOOKS = ['bookRed', 'bookGreen', 'bookNavy', 'bookCream'];

export interface Level {
  pieces: Piece[];
  markers: Marker[];
}

export function buildLevel(): Level {
  const pieces: Piece[] = [];
  const add = (p: Piece) => (pieces.push(p), p);
  const r = rng(2026);

  // --- room shell: floor and inward-facing walls (one static trimesh) -----------
  const X = 4500;
  const Y = 6500;
  const H = 1600;
  const room = add(piece('room', 'mesh'));
  quad(room, 'floor', [-X, -Y, 0], [X, -Y, 0], [X, Y, 0], [-X, Y, 0], { cell: 125, uvScale: 512 });
  const wall = (a: Vec3, b: Vec3) => {
    quad(room, 'skirting', a, b, [b[0], b[1], 40], [a[0], a[1], 40], { cell: 250 });
    quad(room, 'wall', [a[0], a[1], 40], [b[0], b[1], 40], [b[0], b[1], H], [a[0], a[1], H], { cell: 250, visualOnly: false });
  };
  wall([X, -Y, 0], [-X, -Y, 0]);
  wall([-X, -Y, 0], [-X, Y, 0]);
  wall([-X, Y, 0], [X, Y, 0]);
  wall([X, Y, 0], [X, -Y, 0]);
  // A ceiling so the chase camera never sees past the walls.
  const ceiling = add(piece('ceiling', 'none', false));
  ceiling.castsShadow = false;
  quad(ceiling, 'ceiling', [-X, Y, H], [X, Y, H], [X, -Y, H], [-X, -Y, H], { cell: 500 });

  // Rugs and the start line: flat, visual and ray-visible only.
  const decal = (name: string, mat: string, x0: number, y0: number, x1: number, y1: number, z: number) => {
    const p = add(piece(name, 'none'));
    quad(p, mat, [x0, y0, z], [x1, y0, z], [x1, y1, z], [x0, y1, z], { cell: 125 });
    return p;
  };
  decal('rug_west', 'rug', -3900, -900, -2300, 2900, 0.5);
  decal('rug_middle', 'rug', -900, -1800, 900, 1800, 0.5);
  const start = add(piece('start_line', 'none', false));
  quad(start, 'checker', [2500, -4720, 0.6], [3500, -4720, 0.6], [3500, -4600, 0.6], [2500, -4600, 0.6], { uvScale: 60 });

  // --- 1: toy-block chicane between the start and checkpoint 1 ---------------------
  const block = (name: string, x: number, y: number, size: number, z = 0) => {
    const p = add(piece(name));
    const m = BLOCKS[Math.floor(r() * BLOCKS.length)];
    box(p, [x - size / 2, y - size / 2, z], [x + size / 2, y + size / 2, z + size], m, { cell: 100, skipBottom: z === 0 });
    return p;
  };
  block('block_c1', 2650, -3900, 220);
  block('block_c2', 2650, -3900, 160, 220);
  block('block_c3', 3400, -3300, 220);
  block('block_c4', 2300, -2700, 200);
  block('block_c5', 3700, -2700, 180);

  // --- 2: toy chest with an up ramp (south) and a down ramp (north) --------------------
  const CHEST_H = 360;
  const chest = add(piece('toy_chest'));
  box(chest, [1800, -500, 0], [4200, 2500, CHEST_H], { top: 'chestTop', sides: 'chest' }, { cell: 120, skipBottom: true });
  const up = add(piece('ramp_chest_up'));
  ramp(up, 2500, 3500, -2300, -500, '+y', 0, CHEST_H, { top: 'rampTop', sides: 'rampSide' }, 0, { cell: 120 });
  const down = add(piece('ramp_chest_down'));
  ramp(down, 2500, 3500, 2500, 4300, '-y', 0, CHEST_H, { top: 'rampTop', sides: 'rampSide' }, 0, { cell: 120 });
  // A little kicker on the chest lid for air before the down ramp.
  const lid = add(piece('chest_kicker'));
  ramp(lid, 2700, 3300, 1700, 2000, '+y', CHEST_H, CHEST_H + 40, { top: 'stripes', sides: 'chest' }, CHEST_H, { cell: 100, uvScale: 80 });

  // Bookshelf on the east wall north of the chest.
  const shelf = add(piece('bookshelf'));
  box(shelf, [4150, 3000, 0], [4500, 5800, 900], 'shelf', { cell: 150, skipBottom: true });
  for (let i = 0; i < 14; i++) {
    const b = add(piece(`shelf_books_${i}`));
    const y = 3100 + i * 190;
    box(b, [4020, y, 0], [4150, y + 150 + r() * 30, 120 + r() * 120], BOOKS[i % 4], { cell: 100, skipBottom: true });
  }

  // --- 4: the book-gap jump along the north wall (heading −X) ---------------------------
  const JUMP_H = 260;
  const launch = add(piece('ramp_jump_launch'));
  ramp(launch, 900, 1550, 4900, 5900, '-x', 0, JUMP_H, { top: 'rampTop', sides: 'rampSide' }, 0, { cell: 110 });
  const lip = add(piece('ramp_jump_lip', 'none'));
  quad(lip, 'stripes', [900, 4900, JUMP_H + 0.5], [960, 4900, JUMP_H + 0.5 - 24], [960, 5900, JUMP_H + 0.5 - 24], [900, 5900, JUMP_H + 0.5], { uvScale: 80 });
  const landing = add(piece('ramp_jump_landing'));
  ramp(landing, -400, 550, 4900, 5900, '+x', 0, JUMP_H, { top: 'rampTop', sides: 'rampSide' }, 0, { cell: 110 });
  // Books lying in the gap: falling short is a bump, not a wall.
  for (let i = 0; i < 4; i++) {
    const b = add(piece(`gap_books_${i}`));
    const y = 4920 + i * 245;
    box(b, [580 + r() * 20, y, 0], [880 - r() * 20, y + 230, 50 + i * 8], BOOKS[(i + 1) % 4], { cell: 100, skipBottom: true });
  }

  // --- 5: quarter pipe in the NW corner ----------------------------------------
  const pipe = add(piece('quarter_pipe', 'mesh'));
  quarterPipe(pipe, -4400, -2700, 5900, 560, 1, 'pipe', 12, { cell: 110 });

  // West side: a scatter of blocks on the rug.
  block('block_w1', -3700, 2500, 160);
  block('block_w2', -2700, 2100, 200);
  block('block_w3', -3300, 300, 180);
  block('block_w4', -2500, -400, 140);

  // --- 7: the bridge, two pillars and a deck; drive over it or under it -----------------
  const BX0 = -2400;
  const BX1 = -1400;
  const DECK = 220;
  const bridgeUp = add(piece('bridge_ramp_north'));
  ramp(bridgeUp, BX0, BX1, -300, 700, '-y', 0, DECK, { top: 'bridge', sides: 'pillar' }, 0, { cell: 110 });
  const deck = add(piece('bridge_deck'));
  box(deck, [BX0, -2000, DECK - 24], [BX1, -300, DECK], { top: 'bridge', sides: 'pillar', bottom: 'pillar' }, { cell: 110 });
  for (const [name, y0, y1] of [['bridge_pillar_n', -520, -300], ['bridge_pillar_s', -2000, -1780]] as const) {
    for (const [x0, x1] of [[BX0, BX0 + 220], [BX1 - 220, BX1]]) {
      const p = add(piece(`${name}_${x0}`));
      box(p, [x0, y0, 0], [x1, y1, DECK - 24], 'pillar', { cell: 80, skipBottom: true });
    }
  }
  const bridgeDown = add(piece('bridge_ramp_south'));
  ramp(bridgeDown, BX0, BX1, -3000, -2000, '+y', 0, DECK, { top: 'bridge', sides: 'pillar' }, 0, { cell: 110 });

  // --- middle: a fun box (ramp up, flat, ramp down) for stunts -----------------------
  const funUp = add(piece('funbox_up'));
  ramp(funUp, -500, 500, -1500, -900, '+y', 0, 160, { top: 'rampTop', sides: 'rampSide' }, 0, { cell: 110 });
  const funTop = add(piece('funbox_top'));
  box(funTop, [-500, -900, 0], [500, 300, 160], { top: 'stripes', sides: 'rampSide' }, { cell: 110, skipBottom: true, uvScale: 160 });
  const funDown = add(piece('funbox_down'));
  ramp(funDown, -500, 500, 300, 900, '-y', 0, 160, { top: 'rampTop', sides: 'rampSide' }, 0, { cell: 110 });

  // --- 9: shoebox tunnel along the south wall (heading +X) ------------------------------
  const TX0 = -1700;
  const TX1 = -300;
  const TUNNEL_H = 420;
  for (const [name, y0, y1] of [['tunnel_wall_s', -6260, -6140], ['tunnel_wall_n', -5260, -5140]] as const) {
    const p = add(piece(name));
    box(p, [TX0, y0, 0], [TX1, y1, TUNNEL_H], 'shoebox', { cell: 110, skipBottom: true });
  }
  const roof = add(piece('tunnel_roof'));
  box(roof, [TX0 - 40, -6300, TUNNEL_H], [TX1 + 40, -5100, TUNNEL_H + 50], 'shoeboxLid', { cell: 110 });

  // --- 10: cone slalom, then back past the start ----------------------------------
  for (let i = 0; i < 6; i++) {
    const p = add(piece(`cone_${i}`));
    const x = 300 + i * 330;
    const y = -5000 + (i % 2 ? 260 : -260);
    cylinder(p, x, y, 45, 0, 130, { sides: 'cone', top: 'stripes' }, 12, { cell: 60 });
  }
  // A heap of blocks in the SE corner to tuck prizes behind.
  block('block_se1', 4100, -6100, 300);
  block('block_se2', 3750, -6150, 200);
  block('block_se3', 4150, -5750, 180);

  // --- markers: checkpoint node z is star height + 50 (checkpoint_gametype subtracts 50)
  const cp = (i: number, x: number, y: number, surface: number) => ({ name: `checkpoint_${i}`, position: [x, y, surface + 150] as Vec3 });
  const prize = (i: number, x: number, y: number, surface = 0) => ({ name: `prizes_${i}`, position: [x, y, surface + 30] as Vec3 });
  const markers: Marker[] = [
    { name: 'ssrc_start', position: [3000, -5100, 30], heading: 0 },
    cp(1, 3000, -3150, 0),
    cp(2, 3000, 800, CHEST_H),
    cp(3, 2400, 5400, 0),
    cp(4, -1100, 5400, 0),
    cp(5, -3500, 4300, 0),
    cp(6, -1900, 1500, 0),
    cp(7, -1900, -1150, DECK),
    cp(8, -3400, -4400, 0),
    cp(9, -1000, -5700, 0),
    cp(10, 1700, -5000, 0),
    // Prizes: off the racing line, in corners, behind and on top of things.
    prize(1, 4250, -6350),
    prize(2, 3900, -5900, 0),
    prize(3, 2000, -300, CHEST_H),
    prize(4, 4000, 2300, CHEST_H),
    prize(5, 2000, 2300, CHEST_H),
    prize(6, 3950, 3100),
    prize(7, 3950, 5700),
    prize(8, 4300, 6300),
    prize(9, 1200, 6300),
    prize(10, 730, 5530, 66),
    prize(11, -300, 6300),
    prize(12, -2600, 6300),
    prize(13, -3550, 6470, 490),
    prize(14, -4300, 6300),
    prize(15, -4300, 3800),
    prize(16, -3700, 2500, 160),
    prize(17, -2700, 2100, 200),
    prize(18, -4300, 900),
    prize(19, -1900, -1150, 0),
    prize(20, -1900, -700, DECK),
    prize(21, -2600, -3200),
    prize(22, -4300, -2500),
    prize(23, -4300, -6300),
    prize(24, -3000, -6300),
    prize(25, -1000, -5900),
    prize(26, 0, -6300),
    prize(27, 1500, -6300),
    prize(28, 2650, -3900, 380),
    prize(29, 3400, -3300, 220),
    prize(30, 1000, -4000),
    prize(31, 0, -300, 160),
    prize(32, 0, 1400),
    prize(33, -1500, 3200),
    prize(34, 1300, 3000),
    prize(35, 1500, -2200),
    prize(36, 1600, 900),
    prize(37, 4300, -1500),
    prize(38, 4350, 600),
    prize(39, -900, 0),
    prize(40, 4100, -6100, 300),
  ];
  return { pieces, markers };
}