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.
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)};
}
