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/build.ts
#!/usr/bin/env node
// Build the custom (original-art-free) car, props and blob shadow into
// src/game/assets-custom/.
//   pnpm assets:build
// The level is not generated here: its source is src/game/assets-custom/level.blend,
// exported to level.glb next to it with `pnpm level:export`
// (docs/level-editing.md). Everything here is authored from scratch as
// procedural meshes. Sounds are not files; src/game/synth.ts renders them at
// load time.
import { mkdirSync, readdirSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { type GlbMaterialIn, type GlbNodeIn, type GlbPrimitiveIn, writeGlb } from './files.ts';
import type { Piece } from './mesh.ts';
import { ARROW_MATERIALS, buildArrow, buildCar, buildPrize, buildStar, CAR_MATERIALS, carShadow, PRIZE_MATERIALS, type PropMaterial, STAR_MATERIALS } from './props.ts';

const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const out = join(root, 'src/game/assets-custom');

const srgbToLinear = (c: number) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
function linear(hex: string): [number, number, number] {
  const v = parseInt(hex.slice(1), 16);
  return [srgbToLinear(((v >> 16) & 255) / 255), srgbToLinear(((v >> 8) & 255) / 255), srgbToLinear((v & 255) / 255)];
}
const round = (v: number) => Math.round(v * 1000) / 1000;

function primitives(p: Piece, materialIndex: (name: string) => number): GlbPrimitiveIn[] {
  return [...p.prims.values()].map((pr) => ({
    material: materialIndex(pr.material),
    positions: new Float32Array(pr.positions.map(round)),
    normals: new Float32Array(pr.normals),
    indices: new Uint32Array(pr.indices),
  }));
}
/** A props GLB plus its materials.json section (the W3D shader colours the game reads). */
function propGlb(nodes: (index: (name: string) => number) => GlbNodeIn[], table: Record<string, PropMaterial>) {
  const names = Object.keys(table);
  const index = (name: string) => {
    const i = names.indexOf(name);
    if (i < 0) throw new Error(`unknown material ${name}`);
    return i;
  };
  const materials: GlbMaterialIn[] = names.map((name) => ({ name, color: [...linear(table[name].diffuse), 1] }));
  const section = Object.fromEntries(
    names.map((name) => {
      const m = table[name];
      return [name, { ambient: linear(m.ambient ?? m.diffuse), diffuse: linear(m.diffuse), emissive: linear(m.emissive ?? '#000000'), opacity: 1 }];
    }),
  );
  return { glb: writeGlb(nodes(index), materials), section };
}

function main(): void {
  const started = Date.now();
  mkdirSync(join(out, 'ui'), { recursive: true });
  const write = (name: string, data: Buffer | string) => writeFileSync(join(out, name), data);

  // --- car and props -------------------------------------------------------------------
  const car = buildCar();
  const carGlb = propGlb(
    (index) => [
      {
        name: 'Car',
        mesh: { name: 'Car', primitives: primitives(car.body, index) },
        children: car.wheelPositions.map((t, i) => ({ name: `Wheel${i + 1}`, translation: t, mesh: { name: `Wheel${i + 1}`, primitives: primitives(car.wheel, index) } })),
      },
    ],
    CAR_MATERIALS,
  );
  write('car.glb', carGlb.glb);
  const star = propGlb((index) => [{ name: 'Star', mesh: { name: 'Star', primitives: primitives(buildStar(), index) } }], STAR_MATERIALS);
  write('star.glb', star.glb);
  const prize = propGlb((index) => [{ name: 'prizesbox', mesh: { name: 'prizesbox', primitives: primitives(buildPrize(), index) } }], PRIZE_MATERIALS);
  write('prize.glb', prize.glb);
  const arrow = propGlb((index) => [{ name: 'arrow', mesh: { name: 'arrow', primitives: primitives(buildArrow(), index) } }], ARROW_MATERIALS);
  write('arrow2.glb', arrow.glb);
  write('ui/carshadow.png', carShadow());
  write('materials.json', JSON.stringify({ holypolycrap: carGlb.section, star: star.section, prize: prize.section, arrow2: arrow.section }, null, 1) + '\n');

  write(
    'README.md',
    '# Custom asset bundle (generated)\n\n' +
      'The car, props and shadow are built by `pnpm assets:build` (tools/assets/build.ts); level.glb is\n' +
      'exported from level.blend by `pnpm level:export`. Do not edit by hand.\n' +
      'Everything here is original procedural work for this project, under the repository licence.\n' +
      'Sounds are synthesised at load time by src/game/synth.ts.\n',
  );
  const files = readdirSync(out, { recursive: true }).length;
  console.log(`${files} files -> ${out} in ${Date.now() - started} ms`);
}

main();