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.
// Low-poly models built from a few flat-shaded primitives. Local forward is +Z.
import * as THREE from 'three';
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
export const palette = {
horse: 0x8b5a2b,
horseDark: 0x4a2c17,
shirt: 0xc0392b,
jeans: 0x34558b,
skin: 0xe0ac69,
hat: 0xc8a165,
hatBand: 0x5b3a1e,
leather: 0x6b3e1f,
steel: 0x2b2b2b,
pickle: 0x5a9e2f,
pickleDark: 0x3d6e1e,
gold: 0xf2c230,
onion: 0xa4508f,
onionLight: 0xe7a4d4,
sprout: 0x6fae3c,
tank: 0xc9a96e,
tankDark: 0x8c7448,
enemyTank: 0x6c3483,
enemyTankDark: 0x40204f,
track: 0x333333,
rope: 0xd1a660,
sauce: 0xd62828,
cream: 0xf3e3c3,
glass: 0xcdeff0,
brine: 0xa6c94a,
lid: 0xd4a017,
};
const materials = new Map<string, THREE.MeshLambertMaterial>();
export function mat(color: number, extra: Partial<THREE.MeshLambertMaterialParameters> = {}): THREE.MeshLambertMaterial {
const key = `${color}:${JSON.stringify(extra)}`;
let m = materials.get(key);
if (!m) {
m = new THREE.MeshLambertMaterial({ color, flatShading: true, ...extra });
materials.set(key, m);
}
return m;
}
function part(
geo: THREE.BufferGeometry,
color: number | THREE.Material,
x = 0,
y = 0,
z = 0,
parent?: THREE.Object3D,
): THREE.Mesh {
const m = new THREE.Mesh(geo, typeof color === 'number' ? mat(color) : color);
m.position.set(x, y, z);
m.castShadow = true;
parent?.add(m);
return m;
}
const box = (w: number, h: number, d: number) => new THREE.BoxGeometry(w, h, d);
const cyl = (rt: number, rb: number, h: number, seg = 7) => new THREE.CylinderGeometry(rt, rb, h, seg);
export function cowboyHat(parent: THREE.Object3D, y: number, scale = 1, color = palette.hat): THREE.Group {
const hat = new THREE.Group();
hat.position.y = y;
hat.scale.setScalar(scale);
const brim = part(cyl(0.6, 0.6, 0.06, 9), color, 0, 0, 0, hat);
brim.scale.z = 1.15;
part(cyl(0.27, 0.33, 0.38, 7), color, 0, 0.2, 0, hat);
part(cyl(0.335, 0.335, 0.08, 7), palette.hatBand, 0, 0.06, 0, hat);
parent.add(hat);
return hat;
}
export interface RiderModel {
root: THREE.Group;
body: THREE.Group;
legs: THREE.Object3D[];
head: THREE.Object3D;
tail: THREE.Object3D;
rider: THREE.Group;
gun: THREE.Group;
flash: THREE.Mesh;
ropeAnchor: THREE.Object3D;
}
function createHorseAndRider(): RiderModel {
const root = new THREE.Group();
const body = new THREE.Group();
root.add(body);
part(box(1.0, 1.0, 2.3), palette.horse, 0, 1.75, 0, body);
const neck = part(box(0.5, 1.2, 0.55), palette.horse, 0, 2.35, 1.15, body);
neck.rotation.x = 0.55;
const head = new THREE.Group();
head.position.set(0, 2.9, 1.55);
body.add(head);
part(box(0.45, 0.45, 0.95), palette.horse, 0, 0, 0.25, head);
part(box(0.4, 0.3, 0.3), palette.horseDark, 0, -0.08, 0.7, head);
part(new THREE.ConeGeometry(0.08, 0.25, 4), palette.horseDark, 0.15, 0.32, -0.05, head);
part(new THREE.ConeGeometry(0.08, 0.25, 4), palette.horseDark, -0.15, 0.32, -0.05, head);
part(box(0.1, 0.06, 0.06), 0x111111, 0.23, 0.08, 0.2, head);
part(box(0.1, 0.06, 0.06), 0x111111, -0.23, 0.08, 0.2, head);
const mane = part(box(0.14, 1.1, 0.3), palette.horseDark, 0, 2.55, 0.95, body);
mane.rotation.x = 0.55;
const tail = new THREE.Group();
tail.position.set(0, 2.05, -1.15);
body.add(tail);
const tailMesh = part(box(0.18, 1.1, 0.22), palette.horseDark, 0, -0.5, -0.15, tail);
tailMesh.rotation.x = -0.3;
const legs: THREE.Object3D[] = [];
for (const [x, z] of [
[0.32, 0.85],
[-0.32, 0.85],
[0.32, -0.85],
[-0.32, -0.85],
]) {
const leg = new THREE.Group();
leg.position.set(x, 1.3, z);
part(box(0.24, 1.25, 0.26), palette.horse, 0, -0.62, 0, leg);
part(box(0.27, 0.2, 0.3), palette.horseDark, 0, -1.2, 0.02, leg);
body.add(leg);
legs.push(leg);
}
// Saddle and rope coil.
part(box(1.1, 0.15, 0.9), palette.leather, 0, 2.3, -0.1, body);
const coil = part(new THREE.TorusGeometry(0.28, 0.07, 4, 8), palette.rope, 0.6, 2.05, -0.3, body);
coil.rotation.y = Math.PI / 2;
const rider = new THREE.Group();
rider.position.set(0, 2.35, -0.1);
body.add(rider);
part(box(0.25, 0.25, 0.7), palette.jeans, 0.3, 0.05, 0.15, rider);
part(box(0.25, 0.25, 0.7), palette.jeans, -0.3, 0.05, 0.15, rider);
part(box(0.22, 0.7, 0.25), palette.jeans, 0.42, -0.3, 0.45, rider);
part(box(0.22, 0.7, 0.25), palette.jeans, -0.42, -0.3, 0.45, rider);
part(box(0.7, 0.85, 0.42), palette.shirt, 0, 0.6, 0, rider);
part(box(0.72, 0.5, 0.44), palette.leather, 0, 0.75, -0.01, rider).scale.set(1, 1, 1);
const bandana = part(new THREE.ConeGeometry(0.25, 0.3, 3), 0xe63946, 0, 1.0, 0.18, rider);
bandana.rotation.x = Math.PI;
const riderHead = new THREE.Group();
riderHead.position.y = 1.3;
rider.add(riderHead);
part(new THREE.IcosahedronGeometry(0.3, 0), palette.skin, 0, 0, 0, riderHead);
part(box(0.36, 0.08, 0.1), 0x3b2412, 0, -0.08, 0.25, riderHead); // mustache
cowboyHat(riderHead, 0.22, 1);
part(box(0.2, 0.6, 0.2), palette.shirt, 0.42, 0.6, 0.2, rider).rotation.x = -0.9;
part(box(0.2, 0.6, 0.2), palette.shirt, -0.42, 0.6, 0.2, rider).rotation.x = -0.9;
const gun = new THREE.Group();
gun.position.set(0.22, 0.75, 0.55);
rider.add(gun);
part(cyl(0.06, 0.06, 1.3, 5), palette.steel, 0.05, 0, 0.4, gun).rotation.x = Math.PI / 2;
part(cyl(0.06, 0.06, 1.3, 5), palette.steel, -0.07, 0, 0.4, gun).rotation.x = Math.PI / 2;
part(box(0.16, 0.22, 0.5), palette.leather, 0, -0.05, -0.35, gun);
const flash = part(new THREE.OctahedronGeometry(0.45, 0), new THREE.MeshBasicMaterial({ color: 0xffd166 }), 0, 0, 1.3, gun);
flash.visible = false;
flash.castShadow = false;
const ropeAnchor = new THREE.Object3D();
ropeAnchor.position.set(0, 2.2, -1.0);
body.add(ropeAnchor);
return { root, body, legs, head, tail, rider, gun, flash, ropeAnchor };
}
export interface PickleModel {
root: THREE.Group;
body: THREE.Group;
}
function createPickle(golden: boolean): PickleModel {
const root = new THREE.Group();
const body = new THREE.Group();
root.add(body);
const color = golden ? palette.gold : palette.pickle;
const dark = golden ? 0xb8860b : palette.pickleDark;
const skin = golden ? mat(color, { emissive: 0x6b4f00 }) : mat(color);
part(new THREE.CapsuleGeometry(0.45, 1.0, 2, 7), skin, 0, 0.95, 0, body);
const bumps: [number, number, number][] = [
[0.4, 1.3, 0.15],
[-0.38, 0.7, 0.2],
[0.2, 0.5, -0.38],
[-0.25, 1.5, -0.3],
[0.35, 0.9, -0.25],
];
for (const [x, y, z] of bumps) part(new THREE.IcosahedronGeometry(0.1, 0), dark, x, y, z, body);
for (const x of [0.17, -0.17]) {
part(new THREE.IcosahedronGeometry(0.13, 0), 0xffffff, x, 1.35, 0.38, body);
part(new THREE.IcosahedronGeometry(0.065, 0), 0x111111, x, 1.36, 0.5, body);
}
part(box(0.25, 0.06, 0.06), 0x2b1a0e, 0, 1.08, 0.44, body); // little mouth
return { root, body };
}
export interface OnionModel {
root: THREE.Group;
body: THREE.Group;
hat: THREE.Group;
}
function createOnion(): OnionModel {
const root = new THREE.Group();
const body = new THREE.Group();
root.add(body);
const bulb = part(new THREE.IcosahedronGeometry(0.85, 1), palette.onion, 0, 0.9, 0, body);
bulb.scale.set(1, 0.95, 1);
const tip = part(new THREE.ConeGeometry(0.35, 0.7, 6), palette.onion, 0, 1.85, 0, body);
tip.scale.set(1, 1, 1);
part(new THREE.ConeGeometry(0.08, 0.6, 4), palette.sprout, 0.08, 2.35, 0, body).rotation.z = -0.3;
part(new THREE.ConeGeometry(0.08, 0.5, 4), palette.sprout, -0.1, 2.3, 0.05, body).rotation.z = 0.4;
for (const [x, z] of [
[0.1, 0],
[-0.1, 0.05],
[0, -0.1],
]) {
part(box(0.04, 0.3, 0.04), palette.cream, x, 0.05, z, body);
}
// Light streaks so it reads as an onion, not a plum.
for (const a of [0.4, -0.4]) {
const streak = part(box(0.06, 1.2, 0.06), palette.onionLight, Math.sin(a) * 0.8, 0.95, Math.cos(a) * 0.8, body);
streak.rotation.z = -a * 0.6;
}
for (const x of [0.25, -0.25]) {
part(new THREE.IcosahedronGeometry(0.16, 0), 0xffffff, x, 1.1, 0.72, body);
part(new THREE.IcosahedronGeometry(0.08, 0), 0x111111, x, 1.1, 0.86, body);
const brow = part(box(0.32, 0.08, 0.08), 0x2b0f22, x, 1.32, 0.78, body);
brow.rotation.z = x > 0 ? 0.45 : -0.45;
}
const bandana = part(new THREE.ConeGeometry(0.55, 0.45, 3), 0x1d3557, 0, 0.65, 0.55, body);
bandana.rotation.x = Math.PI + 0.3;
const hat = cowboyHat(body, 1.75, 1.4, 0x3b2412);
hat.rotation.z = 0.15;
return { root, body, hat };
}
export interface TankModel {
root: THREE.Group;
turret: THREE.Group;
barrel: THREE.Group;
muzzle: THREE.Object3D;
wheels: THREE.Mesh[];
}
function createTank(enemy: boolean): TankModel {
const hullColor = enemy ? palette.enemyTank : palette.tank;
const darkColor = enemy ? palette.enemyTankDark : palette.tankDark;
const root = new THREE.Group();
part(box(3.0, 1.0, 6.4), hullColor, 0, 1.15, 0, root);
const glacis = part(box(3.0, 0.5, 1.5), hullColor, 0, 1.3, 3.55, root);
glacis.rotation.x = 0.5;
const rear = part(box(3.0, 0.8, 0.6), darkColor, 0, 1.2, -3.3, root);
rear.rotation.x = -0.2;
const wheels: THREE.Mesh[] = [];
for (const side of [1, -1]) {
part(box(0.9, 1.0, 7.2), palette.track, side * 1.85, 0.55, 0, root);
part(box(0.12, 0.65, 6.4), hullColor, side * 2.35, 1.15, 0.2, root);
for (let i = 0; i < 6; i++) {
const w = part(cyl(0.4, 0.4, 0.2, 7), 0x555555, side * 2.32, 0.5, -2.6 + i * 1.05, root);
w.rotation.z = Math.PI / 2;
wheels.push(w);
}
}
const turret = new THREE.Group();
turret.position.set(0, 1.65, -0.3);
root.add(turret);
const outline = new THREE.Shape();
const pts: [number, number][] = [
[-1.5, -1.9],
[1.5, -1.9],
[1.65, 0.6],
[0.75, 2.1],
[-0.75, 2.1],
[-1.65, 0.6],
];
outline.moveTo(pts[0][0], pts[0][1]);
for (const [x, y] of pts.slice(1)) outline.lineTo(x, y);
const turretGeo = new THREE.ExtrudeGeometry(outline, { depth: 0.85, bevelEnabled: false });
turretGeo.rotateX(Math.PI / 2);
turretGeo.translate(0, 0.85, 0);
part(turretGeo, hullColor, 0, 0, 0, turret);
part(box(3.4, 0.5, 1.2), darkColor, 0, 0.45, -2.3, turret); // bustle rack
part(cyl(0.38, 0.4, 0.3, 7), darkColor, 0.65, 1.0, -0.5, turret); // hatch
part(cyl(0.05, 0.05, 1.0, 4), palette.steel, 0.65, 1.3, 0.1, turret).rotation.x = Math.PI / 2;
const barrel = new THREE.Group();
barrel.position.set(0, 0.45, 2.0);
turret.add(barrel);
part(box(0.7, 0.6, 0.6), darkColor, 0, 0, 0.1, barrel);
part(cyl(0.13, 0.15, 5.0, 6), darkColor, 0, 0, 2.6, barrel).rotation.x = Math.PI / 2;
part(cyl(0.22, 0.22, 0.7, 6), darkColor, 0, 0, 2.2, barrel).rotation.x = Math.PI / 2;
const muzzle = new THREE.Object3D();
muzzle.position.set(0, 0, 5.2);
barrel.add(muzzle);
if (enemy) {
// The onion commander pokes out of the hatch.
const commander = buildOnion();
commander.root.scale.setScalar(0.5);
commander.root.position.set(-0.6, 0.75, -0.4);
turret.add(commander.root);
}
return { root, turret, barrel, muzzle, wheels };
}
export interface JarModel {
root: THREE.Group;
brine: THREE.Mesh;
floaters: THREE.Group;
}
export const JAR_RADIUS = 6;
export function buildJar(): JarModel {
const root = new THREE.Group();
const floaters = new THREE.Group();
root.add(floaters);
const brine = new THREE.Mesh(
new THREE.CylinderGeometry(JAR_RADIUS - 0.3, JAR_RADIUS - 0.3, 1, 12),
new THREE.MeshLambertMaterial({ color: palette.brine, transparent: true, opacity: 0.6, flatShading: true }),
);
brine.position.y = 0.5;
root.add(brine);
const glass = new THREE.Mesh(
new THREE.CylinderGeometry(JAR_RADIUS, JAR_RADIUS, 10, 12, 1, true),
new THREE.MeshLambertMaterial({
color: palette.glass,
transparent: true,
opacity: 0.28,
side: THREE.DoubleSide,
depthWrite: false,
flatShading: true,
}),
);
glass.position.y = 5;
glass.renderOrder = 2;
root.add(glass);
part(new THREE.CylinderGeometry(JAR_RADIUS + 0.1, JAR_RADIUS + 0.1, 0.3, 12), palette.glass, 0, 0.15, 0, root);
const rim = part(new THREE.TorusGeometry(JAR_RADIUS, 0.3, 4, 12), palette.lid, 0, 10, 0, root);
rim.rotation.x = Math.PI / 2;
// The lid fell off when the jar tipped. It lies propped against the jar.
const lid = part(new THREE.CylinderGeometry(JAR_RADIUS + 0.4, JAR_RADIUS + 0.4, 0.8, 12), palette.lid, JAR_RADIUS + 2.5, 0.9, -6, root);
lid.rotation.set(0.12, 0.4, 0.18);
lid.receiveShadow = true;
// Wooden sign.
part(box(0.25, 3, 0.25), palette.leather, -7.5, 1.5, 4, root);
const sign = part(box(3.2, 1.3, 0.2), 0x9c6b3c, -7.5, 3, 4, root);
sign.rotation.y = -0.6;
part(box(2.6, 0.25, 0.22), palette.pickle, -7.5, 3.2, 4.02, root).rotation.y = -0.6;
part(box(1.8, 0.2, 0.22), palette.cream, -7.5, 2.75, 4.02, root).rotation.y = -0.6;
return { root, brine, floaters };
}
function createSauce(): THREE.Group {
const root = new THREE.Group();
const bottle = new THREE.Group();
bottle.position.y = 1.2;
root.add(bottle);
part(cyl(0.38, 0.4, 1.1, 7), palette.sauce, 0, 0, 0, bottle);
part(cyl(0.41, 0.41, 0.45, 7), palette.cream, 0, 0.05, 0, bottle);
part(cyl(0.14, 0.32, 0.5, 6), palette.sauce, 0, 0.75, 0, bottle);
part(cyl(0.16, 0.16, 0.25, 6), 0x2d6a4f, 0, 1.1, 0, bottle);
const ring = new THREE.Mesh(
new THREE.TorusGeometry(1.1, 0.08, 4, 14),
new THREE.MeshBasicMaterial({ color: 0xff7b00, transparent: true, opacity: 0.8 }),
);
ring.rotation.x = Math.PI / 2;
ring.position.y = 0.15;
root.add(ring);
return root;
}
export function buildCactus(rng: () => number): THREE.Group {
const root = new THREE.Group();
const h = 3.5 + rng() * 3;
const green = rng() > 0.5 ? 0x4f7942 : 0x5e8c4a;
part(cyl(0.45, 0.5, h, 7), green, 0, h / 2, 0, root);
part(new THREE.IcosahedronGeometry(0.45, 0), green, 0, h, 0, root);
const arms = 1 + Math.floor(rng() * 2);
for (let i = 0; i < arms; i++) {
const side = i === 0 ? 1 : -1;
const y = h * (0.35 + rng() * 0.3);
const up = 0.8 + rng() * 1.2;
part(cyl(0.3, 0.3, 0.9, 6), green, side * 0.75, y, 0, root).rotation.z = Math.PI / 2;
part(cyl(0.3, 0.3, up, 6), green, side * 1.15, y + up / 2, 0, root);
part(new THREE.IcosahedronGeometry(0.3, 0), green, side * 1.15, y + up, 0, root);
}
root.rotation.y = rng() * Math.PI * 2;
optimizeModel(root);
return root;
}
export function buildRock(rng: () => number): THREE.Mesh {
const r = 0.8 + rng() * 1.6;
const rock = part(new THREE.DodecahedronGeometry(r, 0), rng() > 0.5 ? 0xa0522d : 0x8a6a4f);
rock.scale.y = 0.6 + rng() * 0.4;
rock.rotation.set(rng(), rng() * 6, rng());
rock.receiveShadow = true;
return rock;
}
export function buildMesa(rng: () => number): THREE.Group {
const root = new THREE.Group();
const layers = [0xc1440e, 0xd2691e, 0xe2725b, 0xb5541c];
const r = 9 + rng() * 10;
const h = 14 + rng() * 18;
const seg = 6 + Math.floor(rng() * 3);
let y = 0;
const bands = 3;
for (let i = 0; i < bands; i++) {
const bh = h / bands;
const top = r * (1 - (i + 1) * 0.08);
const bottom = r * (1 - i * 0.08) + (i === 0 ? 3 : 0);
const m = part(new THREE.CylinderGeometry(top, bottom, bh, seg), layers[(i + Math.floor(rng() * 4)) % 4], 0, y + bh / 2, 0, root);
m.receiveShadow = true;
y += bh;
}
root.rotation.y = rng() * 6;
optimizeModel(root);
return root;
}
export function buildTumbleweed(): THREE.Mesh {
const m = new THREE.Mesh(
new THREE.IcosahedronGeometry(0.9, 1),
new THREE.MeshBasicMaterial({ color: 0x9c7a3c, wireframe: true }),
);
return m;
}
/** The Onion King: a big onion outlaw with a gold crown on his ten-gallon hat and a red cape. */
function createKing(): OnionModel {
const king = buildOnion();
const crown = new THREE.Group();
crown.position.y = 0.42;
king.hat.add(crown);
part(cyl(0.3, 0.3, 0.16, 8), palette.gold, 0, 0, 0, crown);
for (let i = 0; i < 5; i++) {
const a = (i / 5) * Math.PI * 2;
part(new THREE.ConeGeometry(0.08, 0.24, 4), palette.gold, Math.cos(a) * 0.26, 0.18, Math.sin(a) * 0.26, crown);
}
part(new THREE.OctahedronGeometry(0.07, 0), palette.sauce, 0, 0.05, 0.3, crown);
const cape = part(box(1.5, 1.3, 0.08), palette.sauce, 0, 0.95, -0.72, king.body);
cape.rotation.x = 0.25;
part(box(1.55, 0.12, 0.12), palette.cream, 0, 1.58, -0.6, king.body); // ermine collar
return king;
}
/** A floating arrow that points toward the jar while you tow pickles. */
export function buildGuideArrow(): THREE.Group {
const root = new THREE.Group();
const material = new THREE.MeshBasicMaterial({ color: 0x9be564 });
const head = new THREE.Mesh(new THREE.ConeGeometry(0.5, 0.9, 4), material);
head.rotation.x = Math.PI / 2;
head.position.z = 0.6;
const shaft = new THREE.Mesh(new THREE.BoxGeometry(0.22, 0.22, 0.9), material);
shaft.position.z = -0.1;
root.add(head, shaft);
return root;
}
/** Ground ring that marks the pickle your lasso would grab. */
export function buildTargetRing(): THREE.Mesh {
const ring = new THREE.Mesh(
new THREE.RingGeometry(1.0, 1.35, 12),
new THREE.MeshBasicMaterial({ color: 0xfff1a8, transparent: true, opacity: 0.85, side: THREE.DoubleSide, depthWrite: false }),
);
ring.rotation.x = -Math.PI / 2;
return ring;
}
/** Merge only rigid siblings; animation pivots and explicitly animated meshes survive. */
function optimizeModel(root: THREE.Object3D, excluded: THREE.Object3D[] = []): void {
for (const child of [...root.children]) optimizeModel(child, excluded);
const buckets = new Map<THREE.Material, THREE.Mesh[]>();
for (const child of root.children) {
if (!(child instanceof THREE.Mesh) || excluded.includes(child) || Array.isArray(child.material)
|| child.material.transparent || !child.visible) continue;
const list = buckets.get(child.material) ?? [];
list.push(child);
buckets.set(child.material, list);
}
for (const [material, meshes] of buckets) {
if (meshes.length < 2) continue;
// Separate shadow behaviors even when the material is shared.
for (const flags of [0, 1, 2, 3]) {
const parts = meshes.filter(m => Number(m.castShadow) + Number(m.receiveShadow) * 2 === flags);
if (parts.length < 2) continue;
const geometries = parts.map(m => {
m.updateMatrix();
const geometry = m.geometry.clone().applyMatrix4(m.matrix);
if (!geometry.index) return geometry;
const expanded = geometry.toNonIndexed();
geometry.dispose();
return expanded;
});
const geometry = mergeGeometries(geometries);
for (const g of geometries) g.dispose();
if (!geometry) throw new Error('Incompatible rigid model geometry');
const merged = new THREE.Mesh(geometry, material);
merged.castShadow = Boolean(flags & 1);
merged.receiveShadow = Boolean(flags & 2);
root.remove(...parts);
root.add(merged);
}
}
// Local transforms of fixed mesh parts never change; animated ancestors still propagate.
for (const child of root.children) {
if (child instanceof THREE.Mesh && !excluded.includes(child)) {
child.updateMatrix();
child.matrixAutoUpdate = false;
}
}
}
function cloneModel<T extends { root: THREE.Group }>(template: T): T {
const root = template.root.clone(true);
const originals: THREE.Object3D[] = [];
const copies: THREE.Object3D[] = [];
template.root.traverse(o => originals.push(o));
root.traverse(o => copies.push(o));
const mapping = new Map(originals.map((o, i) => [o, copies[i]]));
return Object.fromEntries(Object.entries(template as Record<string, unknown>).map(([key, value]) => [key,
value instanceof THREE.Object3D ? mapping.get(value) : Array.isArray(value) ? value.map(o => mapping.get(o)) : value,
])) as T;
}
let riderTemplate: RiderModel | undefined;
export function buildHorseAndRider(): RiderModel {
if (!riderTemplate) {
riderTemplate = createHorseAndRider();
optimizeModel(riderTemplate.root, [riderTemplate.flash]);
}
return cloneModel(riderTemplate);
}
const pickleTemplates = new Map<boolean, PickleModel>();
export function buildPickle(golden: boolean): PickleModel {
let template = pickleTemplates.get(golden);
if (!template) {
template = createPickle(golden);
optimizeModel(template.root);
pickleTemplates.set(golden, template);
}
return cloneModel(template);
}
let onionTemplate: OnionModel | undefined;
export function buildOnion(): OnionModel {
if (!onionTemplate) {
onionTemplate = createOnion();
optimizeModel(onionTemplate.root);
}
return cloneModel(onionTemplate);
}
const tankTemplates = new Map<boolean, TankModel>();
export function buildTank(enemy: boolean): TankModel {
let template = tankTemplates.get(enemy);
if (!template) {
template = createTank(enemy);
optimizeModel(template.root, template.wheels);
tankTemplates.set(enemy, template);
}
return cloneModel(template);
}
let sauceTemplate: THREE.Group | undefined;
export function buildSauce(): THREE.Group {
if (!sauceTemplate) {
sauceTemplate = createSauce();
optimizeModel(sauceTemplate);
}
return sauceTemplate.clone(true);
}
let kingTemplate: OnionModel | undefined;
export function buildKing(): OnionModel {
if (!kingTemplate) {
kingTemplate = createKing();
optimizeModel(kingTemplate.root);
}
return cloneModel(kingTemplate);
}
