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