Back to Limb Race
SOURCE / PINNED RELEASE

Made of little things.

Limb Race

Release
6a8ae4fc626e…
Author-recorded commit
008c01ee6454…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-c2869f12ad8

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

src/level-model.ts
import {PHYSICS_VERSION, type Track} from './physics.js';

export const PIECE_KINDS = ['flat', 'uphill', 'downhill', 'step', 'drop', 'trench'] as const;
export type PieceKind = typeof PIECE_KINDS[number];
export type LevelPiece = {kind: PieceKind; width: number; height: number};
export type LevelData = {physics: number; pieces: LevelPiece[]};
export type LevelSnapshot = {key: string; title: string; data: LevelData};
export const PIECE_NAMES: Record<PieceKind, string> = {flat:'Flat', uphill:'Uphill', downhill:'Downhill', step:'Step up', drop:'Drop', trench:'Trench'};
export function starterLevel(): LevelData {return {physics: PHYSICS_VERSION, pieces: [{kind:'uphill',width:300,height:60},{kind:'downhill',width:300,height:60},{kind:'flat',width:200,height:0}]};}
export function validateLevel(value: unknown): LevelData {
  const v = value as Partial<LevelData> | null;
  if (!v || v.physics !== PHYSICS_VERSION || !Array.isArray(v.pieces) || !v.pieces.length || v.pieces.length > 24) throw new Error('A level needs 1–24 terrain pieces and the current physics rules.');
  let y = 300, width = 0;
  const pieces = v.pieces.map(piece => {
    if (!piece || !PIECE_KINDS.includes(piece.kind) || !Number.isSafeInteger(piece.width) || piece.width < 40 || piece.width > 600 || !Number.isSafeInteger(piece.height) || piece.height < 0 || piece.height > 130 || (piece.kind !== 'flat' && piece.height < 5)) throw new Error('Use widths of 40–600 and heights of 5–130.');
    width += piece.width;
    const height = piece.kind === 'flat' ? 0 : piece.height;
    if (piece.kind === 'uphill' || piece.kind === 'step') y -= height;
    if (piece.kind === 'downhill' || piece.kind === 'drop') y += height;
    if (y < 60 || y > 460 || (piece.kind === 'trench' && y + height > 460)) throw new Error('Terrain is too high or too low. Balance climbs with descents.');
    return {kind: piece.kind, width: piece.width, height};
  });
  if (width > 8000) throw new Error('Keep the terrain below 8,000 units long.');
  return {physics: PHYSICS_VERSION, pieces};
}
export function levelTrack(snapshot: LevelSnapshot): Track {
  const data = validateLevel(snapshot.data);
  let x = 400, y = 300;
  const ground = [{x:-600,y},{x,y}];
  for (const p of data.pieces) {
    if (p.kind === 'step' || p.kind === 'drop') {y += p.kind === 'step' ? -p.height : p.height; ground.push({x,y});}
    if (p.kind === 'trench') ground.push({x,y:y+p.height});
    x += p.width;
    if (p.kind === 'uphill' || p.kind === 'downhill') y += p.kind === 'uphill' ? -p.height : p.height;
    if (p.kind === 'trench') ground.push({x,y:y+p.height});
    ground.push({x,y});
  }
  const length = x + 250;
  ground.push({x:length+500,y});
  return {id:snapshot.key, name:snapshot.title, difficulty:'Custom', description:'A player-made course. Find your own way through.',color:'#a6ced0',length,ground};
}
export function validateSnapshot(value: unknown): LevelSnapshot {
  const v=value as Partial<LevelSnapshot>|null;
  if (!v || typeof v.key !== 'string' || !/^(?:draft|custom):[a-zA-Z0-9:_-]{1,150}$/.test(v.key) || typeof v.title !== 'string' || !v.title.trim() || v.title.length > 160 || /[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/.test(v.title)) throw new Error('Invalid saved level.');
  return {key:v.key,title:v.title,data:validateLevel(v.data)};
}