Back to Impact Yard remix
SOURCE / PINNED RELEASE

Made of little things.

Impact Yard remix

Release
5b31a37bbd07
Author-recorded commit
7762f7efb018…
License
LICENSE
Author’s source reference
nostr://npub1n8ga89w8h6tvwamxusfyzexw8gjy84yxu9rxgnmk955cxtml4ujswzxydd/wss%3A%2F%2Fgit.napplet.soy%2F/n-c1d8c061b02

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

src/simulation.ts
import { Body, Box, ContactMaterial, ConvexPolyhedron, Cylinder, GSSolver, Material, Plane, Quaternion, RaycastVehicle, SAPBroadphase, Vec3, World, type ContactEquation } from 'cannon-es';

export type PropKind = 'crate' | 'brick' | 'barrel' | 'timber' | 'domino' | 'column' | 'deck';
export type Prop = { body: Body; kind: PropKind; size: [number, number, number]; start: Vec3; startRotation: Quaternion; group: number; variant: number };
export type Impact = { x: number; y: number; z: number; strength: number; group: number };
export type DriveInput = { throttle: number; steer: number; brake: boolean };
export const FIXED_STEP = 1 / 120;
export const ARENA = { halfSize: 180, wallHeight: 3 };
export const BOOST = { duration: 1.2, recharge: 2.4, kick: 28, force: 70000, maxSpeed: 76 };
export const GROUPS = [
  { name: 'Crate stacks', short: 'CRATES', x: 0, z: -9, width: 9, depth: 8 },
  { name: 'Masonry wall', short: 'MASONRY', x: -17, z: -15, width: 12, depth: 8 },
  { name: 'Barrel pyramid', short: 'BARRELS', x: 15, z: -22, width: 9, depth: 8 },
  { name: 'Crate tower', short: 'TOWER', x: -19, z: 10, width: 9, depth: 8 },
  { name: 'Domino run', short: 'DOMINO RUN', x: -30, z: 12.2, width: 5, depth: 19 },
  { name: 'Timber tower', short: 'TIMBER', x: 26, z: 16, width: 7, depth: 8 },
  { name: 'Smash arch', short: 'SMASH ARCH', x: -8, z: -28, width: 10, depth: 7 },
  { name: 'Concrete colossus', short: '17 M WALL', x: 0, z: -60, width: 32, depth: 9 },
  { name: 'Mega crate pyramid', short: '16 M CRATES', x: 51, z: -48, width: 22, depth: 13 },
  { name: 'Timber skyscraper', short: '14 M TIMBER', x: 54, z: 25, width: 15, depth: 15 },
  { name: 'Titan dominoes', short: '10 M DOMINOES', x: -57, z: 0, width: 11, depth: 54 },
  { name: 'Goliath gate', short: '19 M GATE', x: -14, z: 57, width: 26, depth: 12 },
  { name: 'Skybridge twins', short: '42 M SKYBRIDGE', x: -113, z: -100, width: 44, depth: 15 },
  { name: 'Freight avalanche', short: '40 M FREIGHT', x: 112, z: -110, width: 22, depth: 17 },
  { name: 'Cascade viaduct', short: '29 M VIADUCT', x: 110, z: 60, width: 23, depth: 78 },
  { name: 'Helix megatower', short: '50 M HELIX', x: -116, z: 40, width: 22, depth: 22 },
  { name: 'Cathedral cascade', short: '34 M CATHEDRAL', x: 0, z: 125, width: 24, depth: 50 },
];
export const SITE_APPROACHES: [number, number][] = [
  [0, 17], [-17, 3], [13, -5], [-19, 25], [-30, 27], [26, 29], [-10.8, -19],
  [0, -38], [51, -30], [54, 45], [-57, 36], [-21.8, 75],
  [-129.8, -77], [106.5, -87], [104.7, 114], [-116, 63], [-5, 163],
];
// Shared by the collider and its rendered mesh. The entrance top is at y=0;
// all its thickness is underground, so the bumper cannot strike a raised lip.
export const RAMP = {
  x: 18, z: -4, width: 6, length: 14, height: 2.8,
  vertices: [[-3, -0.15, -7], [3, -0.15, -7], [3, -0.15, 7], [-3, -0.15, 7],
    [-3, 2.8, -7], [3, 2.8, -7], [3, 0, 7], [-3, 0, 7]],
  faces: [[0, 1, 2, 3], [4, 7, 6, 5], [0, 4, 5, 1], [3, 2, 6, 7], [0, 3, 7, 4], [1, 5, 6, 2]],
};

export function createSimulation() {
  const world = new World({ gravity: new Vec3(0, -9.81, 0), allowSleep: true });
  world.broadphase = new SAPBroadphase(world);
  (world.solver as GSSolver).iterations = 18;
  (world.solver as GSSolver).tolerance = 0.001;
  world.defaultContactMaterial.friction = 0.55;
  world.defaultContactMaterial.restitution = 0.03;
  world.defaultContactMaterial.contactEquationStiffness = 1e8;
  world.defaultContactMaterial.contactEquationRelaxation = 3;
  const groundMaterial = new Material('concrete');
  const propMaterial = new Material('props');
  const carMaterial = new Material('car');
  const dominoMaterial = new Material('smooth panels with rubber feet');
  world.addContactMaterial(new ContactMaterial(groundMaterial, propMaterial, { friction: 0.58, restitution: 0.06 }));
  world.addContactMaterial(new ContactMaterial(propMaterial, propMaterial, { friction: 0.55, restitution: 0.04 }));
  world.addContactMaterial(new ContactMaterial(carMaterial, propMaterial, { friction: 0.35, restitution: 0.08 }));
  world.addContactMaterial(new ContactMaterial(carMaterial, groundMaterial, { friction: 0.3, restitution: 0.01 }));
  world.addContactMaterial(new ContactMaterial(dominoMaterial, groundMaterial, { friction: 1.1, restitution: 0.02 }));
  world.addContactMaterial(new ContactMaterial(dominoMaterial, dominoMaterial, { friction: 0.15, restitution: 0.15 }));
  const ground = new Body({ mass: 0, material: groundMaterial, shape: new Plane() });
  ground.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
  ground.aabbNeedsUpdate = true;
  world.addBody(ground);
  const barriers: Body[] = [];
  const edge = ARENA.halfSize;
  for (const [x, z, sx, sz] of [[0, -edge, edge, 0.8], [0, edge, edge, 0.8], [-edge, 0, 0.8, edge], [edge, 0, 0.8, edge]]) {
    const body = new Body({ mass: 0, material: groundMaterial, shape: new Box(new Vec3(sx, ARENA.wallHeight / 2, sz)), position: new Vec3(x, ARENA.wallHeight / 2, z) });
    world.addBody(body);
    barriers.push(body);
  }
  const ramp = new Body({ mass: 0, material: groundMaterial,
    shape: new ConvexPolyhedron({ vertices: RAMP.vertices.map(([x, y, z]) => new Vec3(x, y, z)), faces: RAMP.faces.map(face => [...face]) }),
    position: new Vec3(RAMP.x, 0, RAMP.z) });
  world.addBody(ramp);
  const props: Prop[] = [];
  const impacts: Impact[] = [];
  function add(kind: PropKind, x: number, y: number, z: number, size: [number, number, number], mass: number, group: number) {
    const shape = kind === 'barrel' ? new Cylinder(size[0] / 2, size[0] / 2, size[1], 16) : new Box(new Vec3(size[0] / 2, size[1] / 2, size[2] / 2));
    const body = new Body({ mass, material: kind === 'domino' ? dominoMaterial : propMaterial, shape, position: new Vec3(x, y, z), linearDamping: 0.015, angularDamping: 0.025, sleepSpeedLimit: kind === 'domino' ? 0.015 : 0.15, sleepTimeLimit: 0.8 });
    world.addBody(body);
    const prop = { body, kind, size, start: body.position.clone(), startRotation: body.quaternion.clone(), group, variant: props.length % 4 };
    props.push(prop);
    let lastImpact = -1;
    body.addEventListener('collide', (event: { contact: ContactEquation }) => {
      const speed = Math.abs(event.contact.getImpactVelocityAlongNormal());
      if (speed < 4 || world.time - lastImpact < 0.18 || impacts.length >= 64) return;
      lastImpact = world.time;
      const point = event.contact.bi.position.vadd(event.contact.ri);
      impacts.push({ x: point.x, y: point.y, z: point.z, strength: Math.min(3, speed / 12), group });
    });
    return prop;
  }
  function rotate(prop: Prop, angle: number) {
    prop.body.quaternion.setFromEuler(0, angle, 0);
    prop.startRotation.copy(prop.body.quaternion); prop.body.aabbNeedsUpdate = true;
  }
  for (let level = 0; level < 4; level++) {
    const width = 5 - level;
    for (let x = 0; x < width; x++) for (let depth = 0; depth < 2; depth++) {
      add('crate', (x - (width - 1) / 2) * 1.44, 0.7 + level * 1.405, -9 + depth * 1.43, [1.4, 1.4, 1.4], 32, 0);
    }
  }
  for (let level = 0; level < 5; level++) for (let x = 0; x < 7; x++) {
    add('brick', -17 + (x - 3) * 1.43 + (level % 2) * 0.36, 0.36 + level * 0.725, -15, [1.4, 0.72, 0.9], 75, 1);
  }
  for (let level = 0; level < 3; level++) for (let x = 0; x < 4 - level; x++) for (let z = 0; z < 2; z++) {
    add('barrel', 15 + (x - (3 - level) / 2) * 1.16, 0.74 + level * 1.485, -22 + z * 1.16, [1.12, 1.48, 1.12], 38, 2);
  }
  for (let level = 0; level < 6; level++) for (let x = 0; x < 2; x++) for (let z = 0; z < 2; z++) {
    add('crate', -19 + (x - 0.5) * 1.44, 0.7 + level * 1.405, 10 + (z - 0.5) * 1.44, [1.4, 1.4, 1.4], 32, 3);
  }
  // A small gap relative to panel height lets one falling slab tip the next.
  for (let i = 0; i < 14; i++) {
    add('domino', -30, 1.7, 20 - i * 1.2, [2.8, 3.4, 0.4], 52, 4);
  }
  // Alternating solid timbers rest on each other without invisible anchors.
  for (let level = 0; level < 9; level++) for (let lane = 0; lane < 3; lane++) {
    const crosswise = level % 2 === 0;
    add('timber', 26 + (crosswise ? 0 : (lane - 1) * 1.22), 0.275 + level * 0.555,
      16 + (crosswise ? (lane - 1) * 1.22 : 0), crosswise ? [3.6, 0.55, 1.18] : [1.18, 0.55, 3.6], 24, 5);
  }
  // Take out either pier and the lintel and its loose cargo fall with it.
  for (const side of [-1, 1]) for (let level = 0; level < 3; level++) {
    add('brick', -8 + side * 2.8, 0.5 + level * 1.005, -28, [1.4, 1, 1.8], 90, 6);
  }
  add('timber', -8, 3.32, -28, [7.4, 0.6, 1.8], 90, 6);
  for (let i = 0; i < 4; i++) add('crate', -8 + (i - 1.5) * 1.45, 4.325, -28, [1.4, 1.4, 1.4], 32, 6);
  // Large pieces make the outer yard monumental without thousands of bodies.
  for (let level = 0; level < 6; level++) for (let lane = 0; lane < 7; lane++) {
    add('brick', (lane - 3) * 4.04 + (level % 2) * 0.5, 1.4 + level * 2.805, -60, [4, 2.8, 2.2], 220, 7);
  }
  for (let level = 0; level < 5; level++) {
    const width = 5 - level;
    for (let lane = 0; lane < width; lane++) for (let row = 0; row < 2; row++) {
      add('crate', 51 + (lane - (width - 1) / 2) * 3.24, 1.6 + level * 3.205, -48 + (row - 0.5) * 3.24, [3.2, 3.2, 3.2], 100, 8);
    }
  }
  for (let level = 0; level < 15; level++) for (let lane = 0; lane < 3; lane++) {
    const crosswise = level % 2 === 0;
    add('timber', 54 + (crosswise ? 0 : (lane - 1) * 1.82), 0.45 + level * 0.905,
      25 + (crosswise ? (lane - 1) * 1.82 : 0), crosswise ? [5.4, 0.9, 1.78] : [1.78, 0.9, 5.4], 65, 9);
  }
  for (let i = 0; i < 12; i++) add('domino', -57, 5, 22 - i * 4, [5, 10, 1.2], 320, 10);
  for (const side of [-1, 1]) for (let level = 0; level < 5; level++) {
    add('brick', -14 + side * 7.8, 1.4 + level * 2.805, 57, [3.4, 2.8, 4], 350, 11);
  }
  add('timber', -14, 15.03, 57, [20, 2, 4], 1000, 11);
  for (let i = 0; i < 4; i++) add('crate', -14 + (i - 1.5) * 4, 17.635, 57, [3.2, 3.2, 3.2], 100, 11);
  // Two independent eight-storey frames share a loaded, unsupported middle span.
  for (const side of [-1, 1]) for (let level = 0; level < 8; level++) {
    const base = level * 4.81, x = -113 + side * 14;
    for (const pier of [-1, 1]) add('column', x + pier * 2.8, base + 2, -100, [1.4, 4, 3], 100, 12);
    add('deck', x, base + 4.405, -100, [8.4, 0.8, 7], 190, 12);
  }
  add('deck', -113, 39.08, -100, [24, 1.2, 5], 450, 12);
  for (let i = 0; i < 6; i++) add('crate', -113 + (i - 2.5) * 3.5, 41.285, -100, [3.2, 3.2, 3.2], 90, 12);
  // Tall freight rack: losing one pier sheds seven decks and a rooftop avalanche.
  for (let level = 0; level < 7; level++) {
    const base = level * 4.46;
    for (const side of [-1, 1]) add('column', 112 + side * 5.5, base + 1.9, -110, [1.8, 3.8, 4], 95, 13);
    add('deck', 112, base + 4.13, -110, [14, 0.65, 10], 220, 13);
  }
  for (let level = 0; level < 3; level++) for (let x = 0; x < 5 - level; x++) for (let z = 0; z < 3 - level; z++) {
    add('crate', 112 + (x - (4 - level) / 2) * 2.44, 32.425 + level * 2.405,
      -110 + (z - (2 - level) / 2) * 2.44, [2.4, 2.4, 2.4], 65, 13);
  }
  for (let i = 0; i < 3; i++) add('barrel', 112 + (i - 1) * 2.44, 39.185, -110, [1.12, 1.48, 1.12], 38, 13);
  // Decks rest across five tall portals; a falling span loads the next portal.
  for (let pier = 0; pier < 5; pier++) {
    const z = 30 + pier * 15;
    for (let level = 0; level < 6; level++) for (const side of [-1, 1]) {
      add('column', 110 + side * 5.3, 2.15 + level * 4.305, z, [1.6, 4.3, 3.5], 100, 14);
    }
    add('deck', 110, 26.23, z, [14, 0.8, 3.5], 230, 14);
    if (pier < 4) {
      add('deck', 110, 27.085, z + 7.5, [12, 0.9, 15.2], 360, 14);
      for (let i = 0; i < 3; i++) add('barrel', 110 + (i - 1) * 3, 28.28, z + 7.5, [1.12, 1.48, 1.12], 38, 14);
    }
  }
  // Rotating ledges and balanced cargo surround a vulnerable central spine.
  for (let level = 0; level < 12; level++) {
    const base = level * 4.11, angle = level * Math.PI / 8;
    rotate(add('column', -116, base + 1.7, 40, [3.2, 3.4, 3.2], 80, 15), angle);
    rotate(add('deck', -116, base + 3.755, 40, [10, 0.7, 6], 160, 15), angle);
    if (level % 3 === 2) for (const side of [-1, 1]) for (const end of [-1, 1]) {
      const x = side * 3.6, z = end * 1.8;
      rotate(add('crate', -116 + x * Math.cos(angle) + z * Math.sin(angle), base + 4.91,
        40 - x * Math.sin(angle) + z * Math.cos(angle), [1.6, 1.6, 1.6], 35, 15), angle);
    }
  }
  // Tall segmented arches fall along the passage into the next portal.
  for (let arch = 0; arch < 5; arch++) {
    const z = 141 - arch * 8;
    for (const side of [-1, 1]) for (let level = 0; level < 3; level++) {
      add('brick', side * 5, 5 + level * 10.005, z, [2, 10, 2.2], 250, 16);
    }
    add('deck', 0, 30.62, z, [14, 1.2, 4], 280, 16);
    for (let i = 0; i < 4; i++) add('crate', (i - 1.5) * 2.4, 32.425, z, [2.4, 2.4, 2.4], 55, 16);
    if (arch < 4) for (const side of [-1, 1]) add('deck', side * 6, 31.625, z - 4, [1.2, 0.8, 7.8], 100, 16);
  }
  const chassis = new Body({ mass: 950, material: carMaterial, linearDamping: 0.025, angularDamping: 0.35, allowSleep: false });
  chassis.addShape(new Box(new Vec3(0.88, 0.3, 1.85)));
  chassis.addShape(new Box(new Vec3(0.69, 0.28, 0.8)), new Vec3(0, 0.52, 0.2));
  const vehicle = new RaycastVehicle({ chassisBody: chassis, indexRightAxis: 0, indexUpAxis: 1, indexForwardAxis: 2 });
  for (const [x, z] of [[-0.95, -1.22], [0.95, -1.22], [-0.95, 1.22], [0.95, 1.22]]) {
    vehicle.addWheel({
      radius: 0.4, directionLocal: new Vec3(0, -1, 0), axleLocal: new Vec3(-1, 0, 0),
      chassisConnectionPointLocal: new Vec3(x, 0.04, z), suspensionStiffness: 34,
      suspensionRestLength: 0.34, frictionSlip: 2.8, dampingRelaxation: 2.8,
      dampingCompression: 4.5, maxSuspensionForce: 50000, rollInfluence: 0.08,
      maxSuspensionTravel: 0.24, customSlidingRotationalSpeed: -25, useCustomSlidingRotationalSpeed: true,
    });
  }
  vehicle.addToWorld(world);
  let steering = 0;
  let peakImpact = 0;
  let elapsed = 0;
  let accumulator = 0;
  let currentSite = -1;
  let boostRemaining = 0;
  let boostCooldown = 0;
  const thrustDirection = new Vec3();
  const thrust = new Vec3();
  function boost() {
    if (boostCooldown > 0) return false;
    chassis.vectorToWorldFrame(new Vec3(0, 0, -1), thrustDirection);
    const kick = Math.max(0, Math.min(BOOST.kick, BOOST.maxSpeed - chassis.velocity.dot(thrustDirection)));
    chassis.applyImpulse(thrustDirection.scale(chassis.mass * kick, thrust));
    boostRemaining = BOOST.duration;
    boostCooldown = BOOST.duration + BOOST.recharge;
    return true;
  }
  function cancelBoost() { boostRemaining = 0; }
  function rocketState() {
    return { active: boostRemaining > 0, cooldown: boostCooldown,
      charge: boostRemaining > 0 ? boostRemaining / BOOST.duration : Math.max(0, 1 - boostCooldown / BOOST.recharge) };
  }
  chassis.addEventListener('collide', (event: { body: Body; contact: { getImpactVelocityAlongNormal(): number } }) => {
    if (event.body.mass > 0) peakImpact = Math.max(peakImpact, Math.abs(event.contact.getImpactVelocityAlongNormal()));
  });
  function resetBody(body: Body, position: Vec3, rotation = new Quaternion()) {
    body.position.copy(position);
    body.previousPosition.copy(position);
    body.interpolatedPosition.copy(position);
    body.quaternion.copy(rotation);
    body.previousQuaternion.copy(body.quaternion);
    body.interpolatedQuaternion.copy(body.quaternion);
    body.velocity.setZero(); body.angularVelocity.setZero(); body.force.setZero(); body.torque.setZero();
    body.aabbNeedsUpdate = true;
    body.wakeUp();
  }
  function recover() {
    // Recovery is an explicit user action; impacts never reposition bodies.
    const [x, z] = currentSite < 0 ? SITE_APPROACHES[0] : SITE_APPROACHES[currentSite];
    resetBody(chassis, new Vec3(x, 1.1, z));
    steering = 0;
    boostRemaining = 0; boostCooldown = 0;
    vehicle.wheelInfos.forEach((_, i) => { vehicle.setSteeringValue(0, i); vehicle.applyEngineForce(0, i); vehicle.setBrake(0, i); });
    vehicle.updateVehicle(FIXED_STEP);
  }
  function rebuild() {
    for (const prop of props) resetBody(prop.body, prop.start, prop.startRotation);
    currentSite = -1; impacts.length = 0;
    recover(); peakImpact = 0; elapsed = 0; accumulator = 0;
  }
  function visit(index: number) {
    if (!Number.isInteger(index) || index < 0 || index >= GROUPS.length) return;
    currentSite = index; recover();
  }
  function drainImpacts() { return impacts.splice(0); }
  function step(dt: number, input: DriveInput) {
    if (input.brake) cancelBoost();
    accumulator += Math.min(dt, 0.05);
    while (accumulator >= FIXED_STEP) {
      const speed = chassis.velocity.length();
      const targetSteering = input.steer * (0.48 / (1 + speed * 0.055));
      steering += (targetSteering - steering) * 0.065;
      vehicle.setSteeringValue(steering, 0); vehicle.setSteeringValue(steering, 1);
      const engine = input.throttle * 3100 * Math.max(0, 1 - speed / (input.throttle < 0 ? 12 : 29));
      for (let i = 0; i < 4; i++) {
        vehicle.applyEngineForce(input.brake ? 0 : engine, i);
        vehicle.setBrake(input.brake ? 70 : input.throttle === 0 && boostRemaining === 0 ? 0.7 : 0, i);
      }
      if (boostRemaining > 0) {
        chassis.vectorToWorldFrame(new Vec3(0, 0, -1), thrustDirection);
        const forwardSpeed = chassis.velocity.dot(thrustDirection);
        chassis.applyForce(thrustDirection.scale(BOOST.force * Math.max(0, 1 - forwardSpeed / BOOST.maxSpeed), thrust));
        boostRemaining = Math.max(0, boostRemaining - FIXED_STEP);
      }
      boostCooldown = Math.max(0, boostCooldown - FIXED_STEP);
      world.step(FIXED_STEP);
      elapsed += FIXED_STEP;
      accumulator -= FIXED_STEP;
    }
  }
  function stats() {
    const displaced = props.filter(p => p.body.position.distanceTo(p.start) > 0.85);
    return { speed: chassis.velocity.length() * 3.6, displaced: displaced.length, total: props.length, groups: GROUPS.map((_, i) => displaced.filter(p => p.group === i).length), peakImpact: peakImpact * 3.6, elapsed };
  }
  recover();
  return { world, chassis, vehicle, props, barriers, ramp, step, stats, recover, rebuild, boost, cancelBoost, rocketState, visit, drainImpacts };
}
export type Simulation = ReturnType<typeof createSimulation>;