Back to Pickle Rustlers
SOURCE / PINNED RELEASE

Made of little things.

Pickle Rustlers

Release
039e51f55e34…
Author-recorded commit
40608586a8a1…
License
LICENSE
Author’s source reference
nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-bad44e7ab35

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

tests/engine.test.mjs
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { registerHooks } from 'node:module';
import { readFileSync, existsSync } from 'node:fs';
import ts from 'typescript';
import * as THREE from 'three';
registerHooks({ resolve(specifier, context, next) {
  if (specifier.endsWith('.js') && context.parentURL?.includes('/src/game/')) {
    const source = new URL(specifier.replace(/\.js$/, '.ts'), context.parentURL);
    if (existsSync(source)) return next(source.href, context);
  }
  return next(specifier, context);
}, load(url, context, next) {
  if (!url.endsWith('.ts')) return next(url, context);
  return { format: 'module', shortCircuit: true, source: ts.transpileModule(readFileSync(new URL(url), 'utf8'), {
    compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
  }).outputText };
}});
const models = await import('../src/game/models.ts');
const { Effects } = await import('../src/game/fx.ts');

for (const [name, args, meshCount, triangleCount] of [
  ['Pickle', [false], 5, 262], ['Pickle', [true], 5, 262],
  ['Onion', [], 10, 370], ['HorseAndRider', [], 27, 570],
  ['Tank', [false], 19, 556], ['Tank', [true], 29, 926], ['King', [], 13, 474],
]) test(`${name} ${args}: fewer meshes, original triangle count, reusable resources`, () => {
  const a = models[`build${name}`](...args), b = models[`build${name}`](...args);
  const am = [], bm = [];
  a.root.traverse(o => { if (o.isMesh) am.push(o); });
  b.root.traverse(o => { if (o.isMesh) bm.push(o); });
  assert.equal(am.length, meshCount);
  assert.equal(am.reduce((n, m) => n + (m.geometry.index?.count ?? m.geometry.attributes.position.count) / 3, 0), triangleCount);
  for (let i = 0; i < am.length; i++) {
    assert.notEqual(am[i], bm[i]);
    assert.equal(am[i].geometry, bm[i].geometry);
    assert.equal(am[i].material, bm[i].material);
  }
  for (const [key, value] of Object.entries(a)) {
    if (value?.isObject3D) {
      assert.notEqual(value, b[key]);
      assert.ok(value === a.root || a.root.getObjectById(value.id));
      value.rotation.y += 0.5;
      assert.notEqual(value.rotation.y, b[key].rotation.y);
    }
  }
  if (a.wheels) for (const wheel of a.wheels) assert.equal(wheel.matrixAutoUpdate, true);
  if (a.flash) assert.equal(a.flash.matrixAutoUpdate, true);
});

test('particles use two batches, preserve color/transforms, expire and wrap the pool', () => {
  const scene = new THREE.Scene();
  const fx = new Effects(scene);
  const pos = new THREE.Vector3(2, 3, 4), velocity = new THREE.Vector3(1, 2, 3);
  fx.spawn(pos, velocity, 0xff5400, 1, 0.5, 0);
  fx.streak(pos, new THREE.Vector3(0, 0, 1), 10);
  fx.update(0.1);
  fx.syncInstances();
  assert.equal(scene.children.length, 2);
  const [tetra, boxes] = scene.children;
  assert.deepEqual([tetra.count, boxes.count], [1, 1]);
  const matrix = new THREE.Matrix4();
  tetra.getMatrixAt(0, matrix);
  const actual = new THREE.Vector3().setFromMatrixPosition(matrix);
  assert.ok(actual.distanceTo(new THREE.Vector3(2.1, 3.2, 4.3)) < 1e-6);
  const color = new THREE.Color(); tetra.getColorAt(0, color);
  assert.ok(Math.abs(color.g - new THREE.Color(0xff5400).g) < 1e-6);
  assert.equal(tetra.frustumCulled, false);
  fx.update(2); fx.syncInstances();
  assert.deepEqual([tetra.count, boxes.count], [0, 0]);
  for (let i = 0; i < 400; i++) fx.spawn(pos, velocity, 0xffffff);
  fx.syncInstances();
  assert.equal(tetra.count + boxes.count, 360);
});


test('transient meshes release their GPU geometry and materials on retirement', async () => {
  const { Game } = await import('../src/game/game.ts');
  const game = Object.create(Game.prototype);
  game.scene = new THREE.Scene();
  const geometry = new THREE.BoxGeometry();
  const material = new THREE.MeshBasicMaterial();
  const mesh = new THREE.Mesh(geometry, material);
  game.scene.add(mesh);
  let geometriesDisposed = 0, materialsDisposed = 0;
  geometry.addEventListener('dispose', () => geometriesDisposed++);
  material.addEventListener('dispose', () => materialsDisposed++);
  game.retireMesh(mesh);
  assert.equal(mesh.parent, null);
  assert.equal(geometriesDisposed, 1);
  assert.equal(materialsDisposed, 1);
});

test('world freezes scenery while lighting and tumbleweeds still move', async () => {
  const { World } = await import('../src/game/world.ts');
  const scene = new THREE.Scene();
  const unrelated = new THREE.Object3D();
  scene.add(unrelated);
  const world = new World(scene, true);
  assert.equal(unrelated.matrixAutoUpdate, true);
  const ground = scene.children.find(o => o.isMesh && o.geometry.type === 'BufferGeometry');
  assert.ok(ground);
  assert.equal(ground.matrixAutoUpdate, false);
  assert.equal(ground.matrixWorldAutoUpdate, false);
  const matrix = ground.matrixWorld.clone();
  const weed = world.tumbleweeds[0].mesh;
  const position = weed.position.clone();
  world.update(0.1, 0.5, new THREE.Vector3(10, 0, 0));
  scene.updateMatrixWorld(true);
  assert.deepEqual(ground.matrixWorld.elements, matrix.elements);
  assert.notDeepEqual(weed.position.toArray(), position.toArray());
  assert.equal(world.sun.matrixAutoUpdate, true);
});