SOURCE / PINNED RELEASE
Made of little things.
Mini Samurai
- Release
- a7beb798adbc…
- Author-recorded commit
- 4f97be1515b8…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-3b28f0efc5b
Archive hash verified: 401ec9347ab8f8cd…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import * as THREE from 'three';
import type { RoomId, ThrowDef } from './types';
/*
* Throwable household objects. Scale: 1 unit = 10 cm, real objects are scaled
* down ~0.45x so a 1-unit samurai can lift them over its head with both hands.
*
* Mesh convention (see types.ts): origin at the centre of the object's BOTTOM
* face, upright along +Y up to y = height, footprint within `radius`.
* Exception: the big wrench lies along X and its mesh extends to about ±0.8.
*/
// ---------------------------------------------------------------------------
// Materials and primitive helpers (local copies of the items.ts style)
// ---------------------------------------------------------------------------
interface MatOpts {
roughness?: number;
metalness?: number;
double?: boolean;
/** < 1 makes the material transparent (glass). */
opacity?: number;
}
const METAL: MatOpts = { metalness: 0.6, roughness: 0.35 };
const PLASTIC: MatOpts = { metalness: 0, roughness: 0.55 };
const GLOSSY: MatOpts = { metalness: 0, roughness: 0.3 };
const WOOD: MatOpts = { metalness: 0, roughness: 0.85 };
const SOFT: MatOpts = { metalness: 0, roughness: 0.95 };
const GLASS: MatOpts = { metalness: 0.1, roughness: 0.15, opacity: 0.55 };
const CLEAR: MatOpts = { metalness: 0.1, roughness: 0.1, opacity: 0.3 };
const matCache = new Map<string, THREE.MeshStandardMaterial>();
function mat(color: number, opts: MatOpts = PLASTIC): THREE.MeshStandardMaterial {
const roughness = opts.roughness ?? 0.6;
const metalness = opts.metalness ?? 0;
const double = opts.double ?? false;
const opacity = opts.opacity ?? 1;
const key = `${color}|${roughness}|${metalness}|${double}|${opacity}`;
let m = matCache.get(key);
if (!m) {
m = new THREE.MeshStandardMaterial({
color,
flatShading: true,
roughness,
metalness,
side: double ? THREE.DoubleSide : THREE.FrontSide,
transparent: opacity < 1,
opacity,
depthWrite: opacity >= 1,
});
matCache.set(key, m);
}
return m;
}
function put(
group: THREE.Group,
geo: THREE.BufferGeometry,
material: THREE.Material,
x = 0,
y = 0,
z = 0,
rx = 0,
ry = 0,
rz = 0,
): THREE.Mesh {
const m = new THREE.Mesh(geo, material);
m.position.set(x, y, z);
m.rotation.set(rx, ry, rz);
m.castShadow = true;
m.receiveShadow = true;
group.add(m);
return m;
}
/** Box centred at (x, y, z). */
function box(
g: THREE.Group,
w: number,
h: number,
d: number,
material: THREE.Material,
x = 0,
y = 0,
z = 0,
rx = 0,
ry = 0,
rz = 0,
): THREE.Mesh {
return put(g, new THREE.BoxGeometry(w, h, d), material, x, y, z, rx, ry, rz);
}
/** Vertical cylinder spanning y0..y1. */
function rod(
g: THREE.Group,
rBottom: number,
rTop: number,
y0: number,
y1: number,
material: THREE.Material,
seg = 8,
x = 0,
z = 0,
): THREE.Mesh {
const h = y1 - y0;
return put(g, new THREE.CylinderGeometry(rTop, rBottom, h, seg), material, x, (y0 + y1) / 2, z);
}
/** Vertical cone with its base at y0 and its point at y1 (y1 > y0 points up). */
function cone(
g: THREE.Group,
r: number,
y0: number,
y1: number,
material: THREE.Material,
seg = 8,
x = 0,
z = 0,
): THREE.Mesh {
const h = Math.abs(y1 - y0);
const m = put(g, new THREE.ConeGeometry(r, h, seg), material, x, (y0 + y1) / 2, z);
if (y1 < y0) m.rotation.x = Math.PI;
return m;
}
// ---------------------------------------------------------------------------
// Builders
// ---------------------------------------------------------------------------
function buildMug(): THREE.Group {
const g = new THREE.Group();
const glaze = mat(0x3f7fc4, GLOSSY);
rod(g, 0.16, 0.17, 0, 0.45, glaze, 10);
rod(g, 0.172, 0.172, 0.3, 0.36, mat(0xf4efe4, GLOSSY), 10); // cream stripe
rod(g, 0.15, 0.15, 0.43, 0.445, mat(0x4a2a17, GLOSSY), 10); // coffee
// handle: half torus bulging to +X
const handle = new THREE.TorusGeometry(0.09, 0.028, 5, 8, Math.PI);
put(g, handle, glaze, 0.165, 0.23, 0, 0, 0, -Math.PI / 2);
return g;
}
function buildBottle(): THREE.Group {
const g = new THREE.Group();
const glass = mat(0x2f9a4a, GLASS);
rod(g, 0.15, 0.15, 0, 0.5, glass, 10);
rod(g, 0.15, 0.065, 0.5, 0.66, glass, 10);
rod(g, 0.065, 0.055, 0.66, 0.83, glass, 10);
rod(g, 0.07, 0.07, 0.82, 0.85, glass, 10); // lip
rod(g, 0.07, 0.065, 0.85, 0.9, mat(0xd8342c, METAL), 10); // crown cap
rod(g, 0.157, 0.157, 0.18, 0.38, mat(0xf6e7b8, PLASTIC), 10); // label
rod(g, 0.159, 0.159, 0.25, 0.31, mat(0xd8342c, PLASTIC), 10); // label stripe
rod(g, 0.12, 0.12, 0.02, 0.3, mat(0x7a3b1a, GLOSSY), 8); // soda inside
return g;
}
function buildCan(): THREE.Group {
const g = new THREE.Group();
const tin = mat(0xc9ccd1, METAL);
rod(g, 0.15, 0.15, 0.02, 0.48, tin, 10);
rod(g, 0.157, 0.157, 0, 0.035, tin, 10); // bottom rim
rod(g, 0.157, 0.157, 0.465, 0.5, tin, 10); // top rim
rod(g, 0.153, 0.153, 0.06, 0.44, mat(0xc8262b, PLASTIC), 10); // label
rod(g, 0.155, 0.155, 0.13, 0.25, mat(0xf5f0e2, PLASTIC), 10); // white band
rod(g, 0.156, 0.156, 0.17, 0.21, mat(0xd9a520, METAL), 10); // gold medallion strip
rod(g, 0.12, 0.12, 0.5, 0.505, tin, 10); // lid
return g;
}
function buildApple(): THREE.Group {
const g = new THREE.Group();
const skin = put(g, new THREE.SphereGeometry(0.19, 9, 7), mat(0xc8202a, GLOSSY), 0, 0.175, 0);
skin.scale.set(1, 0.9, 1);
put(g, new THREE.SphereGeometry(0.1, 7, 5), mat(0xe8503a, GLOSSY), 0.06, 0.22, 0.1).scale.set(1, 0.9, 0.7); // blush
rod(g, 0.05, 0.02, 0.31, 0.335, mat(0x6a1218, SOFT), 7); // dimple
rod(g, 0.014, 0.012, 0.3, 0.42, mat(0x5a3a1c, WOOD), 5); // stem
const leaf = put(g, new THREE.SphereGeometry(0.06, 6, 4), mat(0x4caf3a, SOFT), 0.06, 0.39, 0, 0, 0, -0.5);
leaf.scale.set(1, 0.18, 0.5);
return g;
}
function buildSalt(): THREE.Group {
const g = new THREE.Group();
rod(g, 0.13, 0.115, 0, 0.36, mat(0xdff2f5, GLASS), 8);
rod(g, 0.105, 0.1, 0.02, 0.24, mat(0xffffff, SOFT), 8); // salt
const cap = mat(0xb9bec6, METAL);
rod(g, 0.122, 0.122, 0.34, 0.44, cap, 10);
const dome = put(g, new THREE.SphereGeometry(0.122, 10, 4, 0, Math.PI * 2, 0, Math.PI / 2), cap, 0, 0.44, 0);
dome.scale.set(1, 0.4, 1);
// holes suggested by dark dots on the dome
const hole = new THREE.CylinderGeometry(0.014, 0.014, 0.012, 6);
const dark = mat(0x22252a, SOFT);
put(g, hole, dark, 0, 0.489, 0);
for (let i = 0; i < 5; i++) {
const a = (i / 5) * Math.PI * 2;
put(g, hole, dark, Math.cos(a) * 0.055, 0.482, Math.sin(a) * 0.055);
}
return g;
}
function buildSoap(): THREE.Group {
const g = new THREE.Group();
const pink = mat(0xf6b3c8, SOFT);
// stadium-shaped bar: centre box + two rounded ends
box(g, 0.24, 0.2, 0.3, pink, 0, 0.1, 0);
const end = new THREE.CylinderGeometry(0.15, 0.15, 0.2, 10);
put(g, end, pink, 0.12, 0.1, 0);
put(g, end, pink, -0.12, 0.1, 0);
// softer top bevel
box(g, 0.24, 0.04, 0.24, pink, 0, 0.22, 0);
const topEnd = new THREE.CylinderGeometry(0.12, 0.15, 0.04, 10);
put(g, topEnd, pink, 0.12, 0.22, 0);
put(g, topEnd, pink, -0.12, 0.22, 0);
// stamped indent
box(g, 0.22, 0.012, 0.1, mat(0xe48aa6, SOFT), 0, 0.238, 0);
return g;
}
function buildShampoo(): THREE.Group {
const g = new THREE.Group();
const plastic = mat(0x6ec6d8, GLOSSY);
rod(g, 0.17, 0.17, 0, 0.55, plastic, 10).scale.z = 0.6;
rod(g, 0.17, 0.08, 0.55, 0.63, plastic, 10).scale.z = 0.6;
rod(g, 0.172, 0.172, 0.14, 0.42, mat(0xffffff, PLASTIC), 10).scale.z = 0.62; // label
rod(g, 0.174, 0.174, 0.24, 0.3, mat(0xf27aa0, PLASTIC), 10).scale.z = 0.63; // label stripe
const capMat = mat(0xf7f3ea, PLASTIC);
rod(g, 0.08, 0.075, 0.63, 0.72, capMat, 10);
box(g, 0.13, 0.025, 0.08, capMat, -0.02, 0.735, 0, 0, 0, 0.12); // flip lid, slightly open
rod(g, 0.015, 0.015, 0.72, 0.74, mat(0x3a7f8c, PLASTIC), 6, 0.04); // nozzle
return g;
}
function buildCup(): THREE.Group {
const g = new THREE.Group();
rod(g, 0.13, 0.16, 0, 0.42, mat(0x7ad3b5, GLOSSY), 10);
rod(g, 0.165, 0.165, 0.4, 0.43, mat(0xa9e8d2, GLOSSY), 10); // rim
rod(g, 0.145, 0.145, 0.405, 0.41, mat(0x2d5a4d, SOFT), 10); // dark inside
// toothbrush leaning out of the cup
const tb = new THREE.Group();
box(tb, 0.035, 0.42, 0.025, mat(0xf2a33a, GLOSSY), 0, 0.21, 0);
box(tb, 0.045, 0.1, 0.03, mat(0xf2a33a, GLOSSY), 0, 0.45, 0);
box(tb, 0.04, 0.08, 0.035, mat(0xffffff, SOFT), 0, 0.45, 0.03); // bristles
tb.position.set(-0.04, 0.12, 0);
tb.rotation.z = -0.25;
g.add(tb);
return g;
}
function buildSponge(): THREE.Group {
const g = new THREE.Group();
box(g, 0.4, 0.19, 0.26, mat(0xf5d33f, SOFT), 0, 0.095, 0);
box(g, 0.4, 0.07, 0.26, mat(0x3f9a4a, SOFT), 0, 0.225, 0);
// pores
const pore = new THREE.BoxGeometry(0.03, 0.03, 0.01);
const dark = mat(0xc9a226, SOFT);
const spots: [number, number][] = [
[-0.13, 0.06],
[-0.04, 0.13],
[0.06, 0.07],
[0.14, 0.14],
];
for (const [x, y] of spots) {
put(g, pore, dark, x, y, 0.131);
put(g, pore, dark, -x, y + 0.01, -0.131);
}
return g;
}
function buildAlarmClock(): THREE.Group {
const g = new THREE.Group();
const red = mat(0xd8332c, GLOSSY);
const brass = mat(0xd4a83a, METAL);
// round body facing +Z
put(g, new THREE.CylinderGeometry(0.22, 0.22, 0.14, 12), red, 0, 0.32, 0, Math.PI / 2);
put(g, new THREE.CylinderGeometry(0.18, 0.18, 0.02, 12), mat(0xfbf6e8, PLASTIC), 0, 0.32, 0.07, Math.PI / 2);
const black = mat(0x1d1d22, PLASTIC);
box(g, 0.02, 0.13, 0.01, black, 0, 0.375, 0.085); // minute hand
box(g, 0.09, 0.022, 0.01, black, 0.04, 0.32, 0.087); // hour hand
put(g, new THREE.CylinderGeometry(0.02, 0.02, 0.02, 6), brass, 0, 0.32, 0.09, Math.PI / 2);
// twin bells
const bell = new THREE.SphereGeometry(0.1, 10, 4, 0, Math.PI * 2, 0, Math.PI / 2);
put(g, bell, brass, 0.14, 0.5, 0, 0, 0, -0.5);
put(g, bell, brass, -0.14, 0.5, 0, 0, 0, 0.5);
// hammer
rod(g, 0.012, 0.012, 0.52, 0.64, brass, 5);
put(g, new THREE.SphereGeometry(0.03, 6, 4), brass, 0, 0.66, 0);
// little legs
const leg = new THREE.CylinderGeometry(0.035, 0.02, 0.13, 6);
put(g, leg, brass, 0.13, 0.06, 0, 0, 0, 0.3);
put(g, leg, brass, -0.13, 0.06, 0, 0, 0, -0.3);
return g;
}
function buildToyCar(): THREE.Group {
const g = new THREE.Group();
const paint = mat(0x2f6fd8, GLOSSY);
box(g, 0.6, 0.14, 0.3, paint, 0, 0.14, 0);
box(g, 0.32, 0.13, 0.26, paint, -0.03, 0.275, 0); // cabin
box(g, 0.33, 0.08, 0.27, mat(0x9fd8ee, GLASS), -0.03, 0.285, 0); // windows
const wheel = new THREE.CylinderGeometry(0.08, 0.08, 0.06, 8);
const tyre = mat(0x222226, SOFT);
for (const x of [-0.19, 0.19]) {
for (const z of [-0.14, 0.14]) put(g, wheel, tyre, x, 0.08, z, Math.PI / 2);
}
const lamp = new THREE.BoxGeometry(0.02, 0.04, 0.06);
const yellow = mat(0xffe066, GLOSSY);
put(g, lamp, yellow, 0.3, 0.17, 0.09);
put(g, lamp, yellow, 0.3, 0.17, -0.09);
box(g, 0.08, 0.02, 0.31, mat(0xffffff, GLOSSY), 0.15, 0.212, 0); // racing stripe
return g;
}
function buildDice(): THREE.Group {
const g = new THREE.Group();
const s = 0.4;
const h = s / 2;
box(g, s, s, s, mat(0xd9a86a, WOOD), 0, h, 0);
const pip = new THREE.CylinderGeometry(0.035, 0.035, 0.012, 8);
const ink = mat(0x3a2210, SOFT);
const o = 0.1;
const e = h + 0.003;
// top: 1
put(g, pip, ink, 0, s + 0.003, 0);
// front (+Z): 2
put(g, pip, ink, -o, h + o, e, Math.PI / 2);
put(g, pip, ink, o, h - o, e, Math.PI / 2);
// right (+X): 3
put(g, pip, ink, e, h + o, -o, 0, 0, Math.PI / 2);
put(g, pip, ink, e, h, 0, 0, 0, Math.PI / 2);
put(g, pip, ink, e, h - o, o, 0, 0, Math.PI / 2);
// back (-Z): 5
for (const [x, y] of [
[-o, o],
[o, o],
[0, 0],
[-o, -o],
[o, -o],
] as [number, number][]) {
put(g, pip, ink, x, h + y, -e, Math.PI / 2);
}
// left (-X): 4
for (const [y, z] of [
[o, -o],
[o, o],
[-o, -o],
[-o, o],
] as [number, number][]) {
put(g, pip, ink, -e, h + y, z, 0, 0, Math.PI / 2);
}
// bottom: 6
for (const [x, z] of [
[-o, -o],
[-o, 0],
[-o, o],
[o, -o],
[o, 0],
[o, o],
] as [number, number][]) {
put(g, pip, ink, x, -0.003, z);
}
return g;
}
function buildSnowGlobe(): THREE.Group {
const g = new THREE.Group();
rod(g, 0.24, 0.2, 0, 0.15, mat(0x7a4a26, WOOD), 10);
rod(g, 0.205, 0.205, 0.15, 0.18, mat(0xd4a83a, METAL), 10);
const snow = mat(0xffffff, SOFT);
put(g, new THREE.SphereGeometry(0.16, 10, 4, 0, Math.PI * 2, 0, Math.PI / 2), snow, 0, 0.18, 0).scale.set(1, 0.35, 1);
rod(g, 0.02, 0.02, 0.2, 0.25, mat(0x5a3a1c, WOOD), 5); // trunk
cone(g, 0.085, 0.24, 0.36, mat(0x2f7d3a, SOFT), 7);
cone(g, 0.06, 0.32, 0.43, mat(0x3a9447, SOFT), 7);
put(g, new THREE.OctahedronGeometry(0.025), mat(0xffd84a, GLOSSY), 0, 0.445, 0); // star
const flake = new THREE.OctahedronGeometry(0.012);
put(g, flake, snow, 0.09, 0.42, 0.05);
put(g, flake, snow, -0.1, 0.36, -0.04);
put(g, flake, snow, 0.03, 0.5, -0.09);
put(g, new THREE.SphereGeometry(0.2, 12, 9), mat(0xd8f0ff, CLEAR), 0, 0.36, 0); // glass dome
return g;
}
function buildBigWrench(): THREE.Group {
const g = new THREE.Group();
const steel = mat(0xaeb5bd, METAL);
const y = 0.125;
// handle (stands on its edge, flat faces toward ±Z)
box(g, 1.1, 0.12, 0.07, steel, 0, y, 0);
box(g, 0.8, 0.06, 0.075, mat(0x8d949c, METAL), 0, y, 0); // recessed panel
box(g, 0.24, 0.07, 0.078, mat(0xc8312b, PLASTIC), 0, y, 0); // red size stamp
// open-end jaw at +X (C shape)
box(g, 0.1, 0.25, 0.08, steel, 0.6, y, 0);
box(g, 0.18, 0.065, 0.08, steel, 0.72, y + 0.09, 0, 0, 0, 0.12);
box(g, 0.18, 0.065, 0.08, steel, 0.72, y - 0.09, 0, 0, 0, -0.12);
// ring end at -X
put(g, new THREE.TorusGeometry(0.085, 0.04, 6, 10), steel, -0.66, y, 0);
return g;
}
function buildRemote(): THREE.Group {
const g = new THREE.Group();
box(g, 0.18, 0.08, 0.54, mat(0x26272c, PLASTIC), 0, 0.04, 0);
box(g, 0.15, 0.02, 0.5, mat(0x34363d, PLASTIC), 0, 0.09, 0);
box(g, 0.08, 0.03, 0.02, mat(0x8a1a1a, GLOSSY), 0, 0.05, -0.27); // IR window
rod(g, 0.025, 0.025, 0.095, 0.11, mat(0xe0352c, GLOSSY), 8, 0, -0.2); // power
rod(g, 0.05, 0.05, 0.095, 0.11, mat(0x8b9099, PLASTIC), 8, 0, -0.08); // d-pad
const btn = new THREE.BoxGeometry(0.035, 0.015, 0.03);
const grey = mat(0xc4c8cf, PLASTIC);
for (let r = 0; r < 3; r++) {
for (const x of [-0.035, 0.035]) put(g, btn, grey, x, 0.105, 0.04 + r * 0.06);
}
return g;
}
// ---------------------------------------------------------------------------
// Marble, rubber band, crayon box, water balloon
// ---------------------------------------------------------------------------
function buildMarble(): THREE.Group {
const g = new THREE.Group();
put(g, new THREE.IcosahedronGeometry(0.2, 2), mat(0x7fc8f0, GLASS), 0, 0.2, 0);
// The coloured swirl inside.
put(g, new THREE.TorusGeometry(0.1, 0.035, 5, 10), mat(0xf2a33a, GLOSSY), 0, 0.2, 0, 0.6, 0.3, 0);
put(g, new THREE.TorusGeometry(0.08, 0.03, 5, 10), mat(0xd8443a, GLOSSY), 0, 0.2, 0, -0.5, 1.2, 0);
return g;
}
function buildRubberband(): THREE.Group {
const g = new THREE.Group();
const band = put(g, new THREE.TorusGeometry(0.26, 0.035, 5, 20), mat(0xe86f4a, SOFT), 0, 0.04, 0, Math.PI / 2, 0, 0);
band.scale.set(1, 0.7, 1);
return g;
}
function buildCrayonbox(): THREE.Group {
const g = new THREE.Group();
box(g, 0.5, 0.55, 0.2, mat(0xf2c14e, PLASTIC), 0, 0.275, 0);
box(g, 0.52, 0.14, 0.21, mat(0x2f8f4a, PLASTIC), 0, 0.42, 0);
// Crayon tips poking out of the top.
const colors = [0xd8443a, 0x3e6ea8, 0x5aa04a, 0x8e4fb0, 0xf28a2e];
colors.forEach((c, i) => {
const x = -0.2 + i * 0.1;
rod(g, 0.035, 0.035, 0.55, 0.66, mat(c, SOFT), 6, x, 0);
put(g, new THREE.ConeGeometry(0.035, 0.07, 6), mat(c, SOFT), x, 0.695, 0);
});
return g;
}
function buildWaterballoon(): THREE.Group {
const g = new THREE.Group();
const water = put(g, new THREE.IcosahedronGeometry(0.3, 2), mat(0x4aa8e8, GLASS), 0, 0.3, 0);
water.scale.set(1, 1.08, 1);
put(g, new THREE.ConeGeometry(0.06, 0.1, 6), mat(0x2f86c8, PLASTIC), 0, 0.64, 0);
put(g, new THREE.TorusGeometry(0.035, 0.015, 4, 8), mat(0x2f86c8, PLASTIC), 0, 0.7, 0, Math.PI / 2, 0, 0);
return g;
}
// ---------------------------------------------------------------------------
// Pick-ups: sugar cube, thimble, button
// ---------------------------------------------------------------------------
function buildSugar(): THREE.Group {
const g = new THREE.Group();
box(g, 0.32, 0.32, 0.32, mat(0xfbfbf6, { metalness: 0, roughness: 0.4 }), 0, 0.16, 0);
// Glinting crystals.
for (const [x, y, z] of [
[0.1, 0.33, 0.05],
[-0.08, 0.2, 0.165],
[0.165, 0.12, -0.06],
])
put(g, new THREE.OctahedronGeometry(0.03), mat(0xffffff, GLOSSY), x, y, z);
return g;
}
/** A metal thimble: worn as a helmet, it stops one hit. */
export function buildThimble(): THREE.Group {
const g = new THREE.Group();
const steel = mat(0xc9d1d6, METAL);
rod(g, 0.15, 0.13, 0, 0.3, steel, 12);
put(g, new THREE.SphereGeometry(0.13, 12, 6, 0, Math.PI * 2, 0, Math.PI / 2), steel, 0, 0.3, 0);
rod(g, 0.158, 0.158, 0, 0.04, mat(0xd4a83a, METAL), 12);
// Dimples.
const dim = mat(0x8d949c, METAL);
for (let i = 0; i < 6; i++) {
const a = (i / 6) * Math.PI * 2;
put(g, new THREE.SphereGeometry(0.018, 4, 3), dim, Math.cos(a) * 0.14, 0.18, Math.sin(a) * 0.14);
}
return g;
}
/** A big coat button: strapped to the arm, it turns hits from the front. */
export function buildButton(): THREE.Group {
const g = new THREE.Group();
rod(g, 0.3, 0.3, 0, 0.08, mat(0xc8323c, GLOSSY), 18);
rod(g, 0.24, 0.24, 0.08, 0.1, mat(0xa82830, GLOSSY), 18);
const hole = mat(0x3a1518, PLASTIC);
for (const [x, z] of [
[-0.06, -0.06],
[0.06, -0.06],
[-0.06, 0.06],
[0.06, 0.06],
])
rod(g, 0.03, 0.03, 0.095, 0.105, hole, 6, x, z);
return g;
}
// ---------------------------------------------------------------------------
// Definitions
// ---------------------------------------------------------------------------
function def(d: ThrowDef): [string, ThrowDef] {
return [d.id, d];
}
export const THROWABLES: Record<string, ThrowDef> = Object.fromEntries([
def({ id: 'mug', name: 'Coffee mug', damage: 12, knock: 4, weight: 0.9, breaks: true, height: 0.45, radius: 0.28, chips: [0x3f7fc4, 0xf4efe4, 0x4a2a17], build: buildMug }),
def({ id: 'bottle', name: 'Soda bottle', damage: 14, knock: 4, weight: 1.1, breaks: true, height: 0.9, radius: 0.16, chips: [0x2f9a4a, 0x8fd49c, 0x7a3b1a], build: buildBottle }),
def({ id: 'can', name: 'Soup can', damage: 10, knock: 3.5, weight: 0.9, breaks: false, height: 0.5, radius: 0.16, chips: [0xc9ccd1, 0xc8262b, 0xf5f0e2], build: buildCan }),
def({ id: 'apple', name: 'Apple', damage: 7, knock: 2.5, weight: 0.7, breaks: true, height: 0.42, radius: 0.19, chips: [0xc8202a, 0xf3e6b0, 0x4caf3a], build: buildApple }),
def({ id: 'salt', name: 'Salt shaker', damage: 8, knock: 3, weight: 0.7, breaks: true, height: 0.5, radius: 0.14, chips: [0xdff2f5, 0xffffff, 0xb9bec6], build: buildSalt }),
def({ id: 'soap', name: 'Bar of soap', damage: 8, knock: 3, weight: 0.6, breaks: false, height: 0.25, radius: 0.28, chips: [0xf6b3c8, 0xffffff], build: buildSoap }),
def({ id: 'shampoo', name: 'Shampoo bottle', damage: 9, knock: 3.5, weight: 0.9, breaks: false, height: 0.75, radius: 0.18, chips: [0x6ec6d8, 0xffffff, 0xf27aa0], build: buildShampoo }),
def({ id: 'cup', name: 'Toothbrush cup', damage: 6, knock: 2.5, weight: 0.5, breaks: false, height: 0.6, radius: 0.17, chips: [0x7ad3b5, 0xf2a33a], build: buildCup }),
def({ id: 'sponge', name: 'Sponge', damage: 3, knock: 1.5, weight: 0.5, breaks: false, height: 0.26, radius: 0.24, chips: [0xf5d33f, 0x3f9a4a], build: buildSponge }),
def({ id: 'alarmclock', name: 'Alarm clock', damage: 13, knock: 4, weight: 1.0, breaks: false, height: 0.69, radius: 0.26, chips: [0xd8332c, 0xd4a83a, 0xfbf6e8], build: buildAlarmClock }),
def({ id: 'toycar', name: 'Toy car', damage: 10, knock: 4, weight: 0.9, breaks: false, height: 0.34, radius: 0.34, chips: [0x2f6fd8, 0x9fd8ee, 0x222226], build: buildToyCar }),
def({ id: 'dice', name: 'Wooden die', damage: 11, knock: 4, weight: 1.0, breaks: false, anyFace: true, height: 0.4, radius: 0.29, chips: [0xd9a86a, 0x3a2210], build: buildDice }),
def({ id: 'snowglobe', name: 'Snow globe', damage: 15, knock: 4.5, weight: 1.2, breaks: true, height: 0.56, radius: 0.24, chips: [0xd8f0ff, 0xffffff, 0x2f7d3a], build: buildSnowGlobe }),
// Lies along X: the mesh extends to about ±0.8 in X despite radius 0.35.
def({ id: 'bigwrench', name: 'Big wrench', damage: 20, knock: 6, weight: 1.5, breaks: false, height: 0.25, radius: 0.35, chips: [0xaeb5bd, 0x8d949c], build: buildBigWrench }),
def({ id: 'remote', name: 'TV remote', damage: 8, knock: 3, weight: 0.7, breaks: false, height: 0.11, radius: 0.28, chips: [0x26272c, 0xe0352c, 0xc4c8cf], build: buildRemote }),
def({ id: 'marble', name: 'Marble', damage: 6, knock: 3, weight: 0.5, breaks: false, rolls: true, height: 0.4, radius: 0.2, chips: [0x7fc8f0, 0xf2a33a], build: buildMarble }),
def({ id: 'rubberband', name: 'Rubber band', damage: 5, knock: 2.5, weight: 0.5, breaks: false, flick: 22, height: 0.08, radius: 0.28, chips: [0xe86f4a], build: buildRubberband }),
def({ id: 'crayonbox', name: 'Box of crayons', damage: 9, knock: 3.5, weight: 0.8, breaks: true, burst: ['crayon', 'crayon', 'crayon'], height: 0.7, radius: 0.26, chips: [0xf2c14e, 0x2f8f4a, 0xd8443a, 0x3e6ea8], build: buildCrayonbox }),
def({ id: 'waterballoon', name: 'Water balloon', damage: 6, knock: 3, weight: 0.9, breaks: true, splash: 1.9, height: 0.74, radius: 0.3, chips: [0x4aa8e8, 0xbfe6ff, 0xffffff], build: buildWaterballoon }),
]);
/** Pick-ups: grabbing one does something right away. */
export const POWERUPS: ThrowDef[] = [
{ id: 'sugar', name: 'Sugar cube', power: 'sugar', damage: 4, knock: 2, weight: 0.5, breaks: true, height: 0.32, radius: 0.2, chips: [0xffffff], build: buildSugar },
{ id: 'thimble', name: 'Thimble', power: 'thimble', damage: 6, knock: 2, weight: 0.6, breaks: false, height: 0.4, radius: 0.16, chips: [0xc9d1d6], build: buildThimble },
{ id: 'button', name: 'Button', power: 'button', damage: 5, knock: 2, weight: 0.6, breaks: false, height: 0.1, radius: 0.3, chips: [0xc8323c], build: buildButton },
];
export function throwById(id: string): ThrowDef {
return THROWABLES[id] ?? THROWABLES.soap;
}
/** Throwable ids that appear in each room. */
export const THROWS_BY_ROOM: Record<RoomId, string[]> = {
kitchen: ['mug', 'bottle', 'can', 'apple', 'salt', 'waterballoon'],
bathroom: ['soap', 'shampoo', 'cup', 'sponge', 'waterballoon'],
bedroom: ['alarmclock', 'toycar', 'dice', 'snowglobe', 'marble', 'crayonbox', 'rubberband'],
living: ['bigwrench', 'remote', 'mug', 'can', 'toycar', 'marble', 'rubberband'],
dining: ['mug', 'bottle', 'apple', 'salt', 'can', 'waterballoon'],
garage: ['bigwrench', 'can', 'rubberband', 'marble', 'waterballoon'],
house: ['bigwrench', 'remote', 'toycar', 'mug', 'dice', 'alarmclock', 'marble', 'rubberband', 'crayonbox', 'waterballoon'],
};
