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 { WeaponDef } from './types';
/*
* Weapons and household items. Scale: 1 unit = 10 cm, real objects are scaled
* down ~0.65x so a 1-unit samurai can carry them.
*
* Mesh convention (see types.ts): origin = grip point, striking part along +Y
* up to y = length, handle end may extend into -Y. Flat blades are thin along X
* with the cutting edge facing +Z.
*/
// ---------------------------------------------------------------------------
// Materials and primitive helpers
// ---------------------------------------------------------------------------
interface MatOpts {
roughness?: number;
metalness?: number;
double?: boolean;
}
const METAL: MatOpts = { metalness: 0.6, roughness: 0.35 };
const PLASTIC: MatOpts = { metalness: 0, roughness: 0.55 };
const WOOD: MatOpts = { metalness: 0, roughness: 0.85 };
const SOFT: MatOpts = { metalness: 0, roughness: 0.95 };
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 key = `${color}|${roughness}|${metalness}|${double}`;
let m = matCache.get(key);
if (!m) {
m = new THREE.MeshStandardMaterial({
color,
flatShading: true,
roughness,
metalness,
side: double ? THREE.DoubleSide : THREE.FrontSide,
});
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;
}
/**
* Flat plate from a 2D profile drawn in the (z, y) plane, extruded along X with
* the given thickness and centred on x = 0.
*/
function plate(
g: THREE.Group,
pts: [number, number][],
thickness: number,
material: THREE.Material,
x = 0,
): THREE.Mesh {
const shape = new THREE.Shape(pts.map(([z, y]) => new THREE.Vector2(z, y)));
const geo = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: false, curveSegments: 4 });
geo.translate(0, 0, -thickness / 2);
// shape x -> world z, extrusion z -> world -x
geo.rotateY(-Math.PI / 2);
geo.computeVertexNormals();
return put(g, geo, material, x);
}
// ---------------------------------------------------------------------------
// Katana
// ---------------------------------------------------------------------------
function buildKatana(): THREE.Group {
const g = new THREE.Group();
const steel = mat(0x9aa4ad, { metalness: 0.7, roughness: 0.3 });
const edge = mat(0xe9eef2, { metalness: 0.75, roughness: 0.18 });
const gold = mat(0xc89b3c, { metalness: 0.7, roughness: 0.35 });
const iron = mat(0x2b2a2e, { metalness: 0.55, roughness: 0.5 });
const ito = mat(0x1d2340, SOFT);
const same = mat(0xe8e0c8, { roughness: 0.8 });
// Blade: curved profile, spine (-Z) convex, tip bending back toward -Z.
const y0 = 0.11;
const yTip = 1.05;
const curve = (y: number): number => {
const t = Math.max(0, (y - y0) / (yTip - y0));
return -0.06 * t * t;
};
const halfW = (y: number): number => {
const t = Math.max(0, (y - y0) / (yTip - y0));
return 0.036 - 0.01 * t;
};
const EDGE_BAND = 0.017;
const steps = 7;
const yShoulder = 0.95; // where the kissaki (tip) begins
const spine: [number, number][] = [];
const edgeOuter: [number, number][] = [];
const edgeInner: [number, number][] = [];
for (let i = 0; i <= steps; i++) {
const y = y0 + ((yShoulder - y0) * i) / steps;
const c = curve(y);
const w = halfW(y);
spine.push([c - w, y]);
edgeOuter.push([c + w, y]);
edgeInner.push([c + w - EDGE_BAND, y]);
}
const tipZ = curve(yTip) - halfW(yTip) * 0.6;
const tip: [number, number] = [tipZ, yTip];
// Kissaki: edge sweeps up to meet the spine at the tip.
const cs = curve(yShoulder);
const ws = halfW(yShoulder);
const kissakiOuter: [number, number] = [cs + ws * 0.55, yShoulder + 0.07];
const kissakiInner: [number, number] = [cs + ws * 0.55 - EDGE_BAND * 0.8, yShoulder + 0.055];
// Body (spine side up to the hamon line).
plate(
g,
[...edgeInner, kissakiInner, tip, ...spine.slice().reverse()],
0.026,
steel,
);
// Bright edge band along +Z (thinner, like a tapered edge).
plate(
g,
[...edgeOuter, kissakiOuter, tip, kissakiInner, ...edgeInner.slice().reverse()],
0.014,
edge,
);
// Shinogi ridge line hint: a thin dark strip along the body near the spine.
for (let i = 0; i < steps; i++) {
const ya = y0 + 0.04 + ((yShoulder - y0 - 0.04) * i) / steps;
const yb = y0 + 0.04 + ((yShoulder - y0 - 0.04) * (i + 1)) / steps;
const za = curve(ya) - halfW(ya) * 0.25;
const zb = curve(yb) - halfW(yb) * 0.25;
const len = Math.hypot(yb - ya, zb - za);
const ang = Math.atan2(zb - za, yb - ya);
for (const sx of [-1, 1]) {
box(g, 0.003, len, 0.004, iron, sx * 0.0135, (ya + yb) / 2, (za + zb) / 2, ang, 0, 0);
}
}
// Habaki (blade collar).
box(g, 0.036, 0.05, 0.085, gold, 0, 0.115, 0);
// Seppa washers + round tsuba.
rod(g, 0.05, 0.05, 0.075, 0.085, gold, 8);
put(g, new THREE.CylinderGeometry(0.092, 0.092, 0.018, 10), iron, 0, 0.066, 0);
rod(g, 0.05, 0.05, 0.048, 0.057, gold, 8);
// Tsuba rim detail.
put(g, new THREE.TorusGeometry(0.088, 0.008, 4, 10), gold, 0, 0.066, 0, Math.PI / 2);
// Fuchi collar.
const fuchi = rod(g, 0.034, 0.034, 0.02, 0.048, gold, 8);
fuchi.scale.set(0.8, 1, 1.1);
// Tsuka: oval wrapped handle from y = -0.2 to 0.02.
const tsuka = rod(g, 0.03, 0.032, -0.2, 0.02, same, 8);
tsuka.scale.set(0.8, 1, 1.1);
// Ito wrap: crossing bands leaving samegawa diamonds visible.
const wraps = 6;
for (let i = 0; i < wraps; i++) {
const y = -0.185 + (0.2 / wraps) * (i + 0.5);
const ring = rod(g, 0.033, 0.033, y - 0.012, y + 0.012, ito, 8);
ring.scale.set(0.82, 1, 1.12);
// Diamond peeks on both flat sides between wraps.
if (i < wraps - 1) {
for (const sx of [-1, 1]) {
box(g, 0.006, 0.017, 0.017, same, sx * 0.026, y + 0.0165, 0, Math.PI / 4, 0, 0);
}
}
}
// Menuki ornaments.
box(g, 0.008, 0.03, 0.014, gold, 0.029, -0.06, 0);
box(g, 0.008, 0.03, 0.014, gold, -0.029, -0.1, 0);
// Kashira pommel cap.
const kashira = rod(g, 0.03, 0.035, -0.22, -0.198, iron, 8);
kashira.scale.set(0.8, 1, 1.1);
return g;
}
// ---------------------------------------------------------------------------
// Kitchen
// ---------------------------------------------------------------------------
function buildFork(): THREE.Group {
const g = new THREE.Group();
const m = mat(0xc4c9cf, METAL);
// Handle: flat, wider near the end.
plate(g, [[-0.035, -0.15], [0.035, -0.15], [0.03, 0.2], [0.014, 0.55], [-0.014, 0.55], [-0.03, 0.2]], 0.02, m);
// Neck widening into the head.
plate(g, [[-0.014, 0.55], [0.014, 0.55], [0.07, 0.72], [-0.07, 0.72]], 0.02, m);
// Four tines.
for (let i = 0; i < 4; i++) {
const z = -0.055 + i * 0.0367;
box(g, 0.018, 0.3, 0.016, m, 0, 0.87, z);
cone(g, 0.009, 1.02, 1.05, m, 4, 0, z);
}
return g;
}
function buildSpoon(): THREE.Group {
const g = new THREE.Group();
const m = mat(0xc9ced4, METAL);
plate(g, [[-0.03, -0.12], [0.03, -0.12], [0.022, 0.3], [0.01, 0.6], [-0.01, 0.6], [-0.022, 0.3]], 0.02, m);
const bowl = put(g, new THREE.SphereGeometry(0.1, 8, 6), m, 0, 0.75, 0);
bowl.scale.set(0.35, 1.5, 1);
return g;
}
function buildButterknife(): THREE.Group {
const g = new THREE.Group();
const steel = mat(0xc7ccd2, METAL);
const edge = mat(0xe4e8ec, { metalness: 0.65, roughness: 0.25 });
// Rounded handle.
const h = rod(g, 0.032, 0.028, -0.14, 0.24, steel, 8);
h.scale.set(0.7, 1, 1);
// Bolster.
box(g, 0.03, 0.03, 0.05, steel, 0, 0.255, 0);
// Blade with a round tip, edge toward +Z.
plate(
g,
[[-0.02, 0.27], [0.035, 0.27], [0.04, 0.6], [0.035, 0.78], [0.015, 0.84], [-0.01, 0.85], [-0.02, 0.82]],
0.014,
steel,
);
plate(g, [[0.035, 0.29], [0.045, 0.29], [0.048, 0.6], [0.04, 0.78], [0.033, 0.78], [0.038, 0.6]], 0.008, edge);
return g;
}
function buildWhisk(): THREE.Group {
const g = new THREE.Group();
const handle = mat(0xd84a3a, PLASTIC);
const wire = mat(0xd0d5da, METAL);
rod(g, 0.04, 0.035, -0.15, 0.32, handle, 8);
rod(g, 0.03, 0.03, 0.32, 0.38, wire, 8);
// Wire loops: tall tori rotated around Y.
const loops = 4;
for (let i = 0; i < loops; i++) {
const t = put(g, new THREE.TorusGeometry(0.15, 0.008, 4, 12), wire, 0, 0.66, 0, 0, (Math.PI / loops) * i, 0);
t.scale.set(1, 1.9, 1);
}
return g;
}
function buildSpatula(): THREE.Group {
const g = new THREE.Group();
const handle = mat(0x2b2b2b, PLASTIC);
const head = mat(0x3a3f45, { metalness: 0.3, roughness: 0.5 });
rod(g, 0.038, 0.032, -0.18, 0.45, handle, 8);
rod(g, 0.016, 0.016, 0.45, 0.62, mat(0xbfc4ca, METAL), 6);
// Flat wide head with slots.
box(g, 0.02, 0.48, 0.34, head, 0, 0.86, 0);
const slot = mat(0x1a1c1f, PLASTIC);
for (let i = 0; i < 3; i++) {
box(g, 0.024, 0.28, 0.025, slot, 0, 0.86, -0.08 + i * 0.08);
}
return g;
}
function buildChopstick(): THREE.Group {
const g = new THREE.Group();
const lacquer = mat(0x8a2420, WOOD);
const tipM = mat(0xd8b98a, WOOD);
rod(g, 0.028, 0.02, -0.15, 0.75, lacquer, 6);
rod(g, 0.02, 0.012, 0.75, 1.08, tipM, 6);
cone(g, 0.012, 1.08, 1.1, tipM, 6);
// Decorative bands near the top end.
rod(g, 0.03, 0.03, -0.1, -0.08, mat(0xd6a83c, { metalness: 0.4, roughness: 0.5 }), 6);
return g;
}
function buildLadle(): THREE.Group {
const g = new THREE.Group();
const m = mat(0xbfc5cb, METAL);
plate(g, [[-0.035, -0.18], [0.035, -0.18], [0.024, 0.3], [0.014, 0.95], [-0.014, 0.95], [-0.024, 0.3]], 0.025, m);
// Hook at the handle end.
put(g, new THREE.TorusGeometry(0.03, 0.01, 4, 8, Math.PI), m, 0, -0.18, -0.03, 0, Math.PI / 2, Math.PI);
// Deep bowl, opening facing +Z.
const bowl = put(
g,
new THREE.SphereGeometry(0.15, 8, 5, 0, Math.PI * 2, 0, Math.PI / 2),
mat(0xbfc5cb, { ...METAL, double: true }),
0,
1.04,
0.02,
-Math.PI / 2,
);
bowl.castShadow = true;
put(g, new THREE.TorusGeometry(0.15, 0.012, 4, 10), m, 0, 1.04, 0.02);
return g;
}
function buildPizzacutter(): THREE.Group {
const g = new THREE.Group();
const handle = mat(0x1f6fb2, PLASTIC);
const steel = mat(0xc4cad0, METAL);
const edge = mat(0xeef1f4, { metalness: 0.7, roughness: 0.2 });
rod(g, 0.04, 0.036, -0.18, 0.3, handle, 8);
rod(g, 0.042, 0.042, 0.3, 0.34, handle, 8);
// Fork arms holding the wheel axle.
box(g, 0.012, 0.42, 0.04, steel, 0.03, 0.52, 0);
box(g, 0.012, 0.42, 0.04, steel, -0.03, 0.52, 0);
// Wheel (axis along X).
put(g, new THREE.CylinderGeometry(0.14, 0.14, 0.012, 12), steel, 0, 0.71, 0, 0, 0, Math.PI / 2);
put(g, new THREE.TorusGeometry(0.14, 0.006, 3, 12), edge, 0, 0.71, 0, 0, Math.PI / 2, 0);
put(g, new THREE.CylinderGeometry(0.025, 0.025, 0.08, 6), steel, 0, 0.71, 0, 0, 0, Math.PI / 2);
return g;
}
// ---------------------------------------------------------------------------
// Bathroom
// ---------------------------------------------------------------------------
function buildToothbrush(): THREE.Group {
const g = new THREE.Group();
const body = mat(0x3fb7c9, PLASTIC);
const grip = mat(0xffffff, PLASTIC);
const bristle = mat(0xf2f2f2, SOFT);
const bristle2 = mat(0x6ec3ff, SOFT);
plate(g, [[-0.035, -0.15], [0.035, -0.15], [0.03, 0.4], [0.014, 0.75], [-0.014, 0.75], [-0.03, 0.4]], 0.035, body);
box(g, 0.04, 0.2, 0.05, grip, 0, 0.05, 0);
// Head and bristles facing +Z.
box(g, 0.03, 0.22, 0.04, body, 0, 0.88, 0);
for (let i = 0; i < 5; i++) {
box(g, 0.026, 0.03, 0.07, i % 2 ? bristle2 : bristle, 0, 0.79 + i * 0.04, 0.05);
}
return g;
}
function buildComb(): THREE.Group {
const g = new THREE.Group();
const m = mat(0x30302f, { roughness: 0.4 });
// Spine on -Z, teeth pointing +Z (the "edge").
plate(g, [[-0.05, -0.12], [0.0, -0.12], [0.0, 0.75], [-0.05, 0.75]], 0.018, m);
for (let i = 0; i < 16; i++) {
const y = 0.05 + i * 0.044;
box(g, 0.012, 0.016, 0.07, m, 0, y, 0.035);
}
return g;
}
function buildRazor(): THREE.Group {
const g = new THREE.Group();
const handle = mat(0xf07a22, PLASTIC);
const grip = mat(0x2f3a4a, SOFT);
const steel = mat(0xd6dbe0, { metalness: 0.75, roughness: 0.2 });
const h = rod(g, 0.032, 0.026, -0.12, 0.42, handle, 8);
h.scale.set(0.8, 1, 1);
box(g, 0.045, 0.14, 0.03, grip, 0, 0.02, 0.0);
// Head with blades, edge toward +Z.
box(g, 0.05, 0.16, 0.06, handle, 0, 0.5, 0, 0.2, 0, 0);
for (let i = 0; i < 3; i++) {
box(g, 0.054, 0.006, 0.05, steel, 0, 0.45 + i * 0.045, 0.02, 0.2, 0, 0);
}
// Wide guard bar.
box(g, 0.13, 0.02, 0.03, mat(0x9de3d7, PLASTIC), 0, 0.59, 0.02);
box(g, 0.13, 0.12, 0.012, steel, 0, 0.53, 0.04);
return g;
}
function buildCottonswab(): THREE.Group {
const g = new THREE.Group();
const stick = mat(0xf2f2ee, PLASTIC);
const cotton = mat(0xffffff, SOFT);
rod(g, 0.014, 0.014, -0.12, 0.6, stick, 6);
const top = put(g, new THREE.IcosahedronGeometry(0.045, 0), cotton, 0, 0.64, 0);
top.scale.set(1, 1.7, 1);
const bottom = put(g, new THREE.IcosahedronGeometry(0.045, 0), cotton, 0, -0.16, 0);
bottom.scale.set(1, 1.7, 1);
return g;
}
function buildNailfile(): THREE.Group {
const g = new THREE.Group();
const handle = mat(0xe86aa0, PLASTIC);
const grit = mat(0x9b8f86, { roughness: 1 });
box(g, 0.022, 0.26, 0.06, handle, 0, -0.03, 0);
plate(g, [[-0.035, 0.1], [0.035, 0.1], [0.032, 0.7], [0.0, 0.8], [-0.032, 0.7]], 0.014, mat(0xc8ccd2, METAL));
box(g, 0.018, 0.56, 0.05, grit, 0, 0.4, 0);
return g;
}
function buildTweezers(): THREE.Group {
const g = new THREE.Group();
const m = mat(0xc8cdd3, METAL);
// Joined end at the bottom, two arms spreading slightly toward +Y.
box(g, 0.03, 0.08, 0.05, m, 0, -0.08, 0);
for (const sx of [-1, 1]) {
box(g, 0.012, 0.66, 0.045, m, sx * 0.022, 0.25, 0, 0, 0, -sx * 0.035);
// Pointed tips.
box(g, 0.01, 0.08, 0.03, m, sx * 0.03, 0.6, 0, 0, 0, sx * 0.08);
}
// Grip ridges.
const ridge = mat(0x8c939b, METAL);
for (let i = 0; i < 3; i++) {
for (const sx of [-1, 1]) {
box(g, 0.006, 0.01, 0.047, ridge, sx * (0.03 + i * 0.001), 0.04 + i * 0.03, 0);
}
}
return g;
}
// ---------------------------------------------------------------------------
// Bedroom
// ---------------------------------------------------------------------------
function buildPencil(): THREE.Group {
const g = new THREE.Group();
const paint = mat(0xf2c12e, PLASTIC);
const wood = mat(0xe3c08c, WOOD);
const graphite = mat(0x2d2d2f, { metalness: 0.3, roughness: 0.4 });
const ferrule = mat(0xc9ccd0, METAL);
const eraser = mat(0xf29bab, SOFT);
// Eraser and ferrule at the bottom.
rod(g, 0.04, 0.04, -0.17, -0.1, eraser, 8);
rod(g, 0.043, 0.043, -0.1, -0.03, ferrule, 8);
rod(g, 0.046, 0.046, -0.08, -0.07, mat(0xa9adb2, METAL), 8);
rod(g, 0.046, 0.046, -0.06, -0.05, mat(0xa9adb2, METAL), 8);
// Hex body.
rod(g, 0.045, 0.045, -0.03, 0.93, paint, 6);
// Sharpened wood cone + graphite point.
cone(g, 0.045, 0.93, 1.1, wood, 6);
cone(g, 0.0125, 1.1, 1.15, graphite, 6);
return g;
}
function buildRuler(): THREE.Group {
const g = new THREE.Group();
const wood = mat(0xd9b77a, WOOD);
const ink = mat(0x2a2522, SOFT);
box(g, 0.02, 1.85, 0.16, wood, 0, 0.675, 0);
// Tick marks along the +Z edge on both faces.
for (let i = 0; i < 18; i++) {
const y = -0.2 + i * 0.1;
const long = i % 5 === 0;
for (const sx of [-1, 1]) {
box(g, 0.004, 0.008, long ? 0.07 : 0.035, ink, sx * 0.011, y, 0.08 - (long ? 0.035 : 0.0175));
}
}
return g;
}
function buildPaintbrush(): THREE.Group {
const g = new THREE.Group();
const handle = mat(0xc0392b, { roughness: 0.4 });
const ferrule = mat(0xc9ccd0, METAL);
const hair = mat(0x6b4a2b, SOFT);
const paint = mat(0x2e86de, PLASTIC);
cone(g, 0.024, -0.05, -0.18, handle, 8);
rod(g, 0.026, 0.032, -0.05, 0.55, handle, 8);
rod(g, 0.034, 0.038, 0.55, 0.72, ferrule, 8);
const tuft = rod(g, 0.042, 0.04, 0.72, 0.9, hair, 8);
tuft.scale.set(0.6, 1, 1.2);
const tip = cone(g, 0.04, 0.9, 1.0, paint, 8);
tip.scale.set(0.6, 1, 1.2);
return g;
}
function buildKnittingneedle(): THREE.Group {
const g = new THREE.Group();
const m = mat(0x7d4fc1, { metalness: 0.35, roughness: 0.4 });
const knob = mat(0xf2d25c, PLASTIC);
put(g, new THREE.SphereGeometry(0.04, 8, 6), knob, 0, -0.22, 0);
rod(g, 0.03, 0.03, -0.2, -0.17, knob, 8);
rod(g, 0.02, 0.02, -0.17, 1.7, m, 8);
cone(g, 0.02, 1.7, 1.8, m, 8);
return g;
}
function buildCrayon(): THREE.Group {
const g = new THREE.Group();
const wax = mat(0x2f9e44, { roughness: 0.7 });
const label = mat(0xf4efe2, SOFT);
const stripe = mat(0x1d1d1d, SOFT);
rod(g, 0.05, 0.05, -0.12, 0.45, wax, 8);
rod(g, 0.052, 0.052, -0.08, 0.36, label, 8);
rod(g, 0.054, 0.054, -0.06, -0.04, stripe, 8);
rod(g, 0.054, 0.054, 0.32, 0.34, stripe, 8);
rod(g, 0.05, 0.025, 0.45, 0.56, wax, 8);
rod(g, 0.025, 0.02, 0.56, 0.6, wax, 8);
return g;
}
function buildHairpin(): THREE.Group {
const g = new THREE.Group();
const m = mat(0x26221f, { metalness: 0.5, roughness: 0.4 });
const tip = mat(0x5a4e46, PLASTIC);
// Bobby pin: loop at the bottom, one straight and one wavy prong.
put(g, new THREE.TorusGeometry(0.03, 0.01, 4, 8, Math.PI), m, 0, -0.08, 0, 0, Math.PI / 2, Math.PI);
box(g, 0.012, 0.66, 0.018, m, 0, 0.25, 0.03);
for (let i = 0; i < 5; i++) {
const y = -0.05 + i * 0.12;
box(g, 0.012, 0.125, 0.018, m, 0, y + 0.06, -0.025, 0, 0, 0);
box(g, 0.012, 0.03, 0.018, m, 0, y + 0.06, i % 2 ? -0.02 : -0.032);
}
put(g, new THREE.SphereGeometry(0.014, 6, 4), tip, 0, 0.59, 0.03);
put(g, new THREE.SphereGeometry(0.014, 6, 4), tip, 0, 0.56, -0.025);
return g;
}
// ---------------------------------------------------------------------------
// Living room
// ---------------------------------------------------------------------------
function buildScrewdriver(): THREE.Group {
const g = new THREE.Group();
const handle = mat(0xe03131, { roughness: 0.35 });
const band = mat(0xf2d230, PLASTIC);
const steel = mat(0xb8bec5, METAL);
rod(g, 0.05, 0.055, -0.15, 0.25, handle, 6);
rod(g, 0.058, 0.058, 0.0, 0.06, band, 6);
rod(g, 0.05, 0.03, 0.25, 0.32, handle, 6);
rod(g, 0.016, 0.016, 0.32, 0.98, steel, 6);
// Flat-head tip.
box(g, 0.012, 0.07, 0.034, steel, 0, 1.015, 0);
return g;
}
function buildWrench(): THREE.Group {
const g = new THREE.Group();
const m = mat(0xaeb5bd, METAL);
// Box end at the bottom.
put(g, new THREE.TorusGeometry(0.06, 0.025, 4, 8), m, 0, -0.18, 0, 0, Math.PI / 2, 0);
// Shaft (flat, thin along X).
plate(g, [[-0.04, -0.13], [0.04, -0.13], [0.05, 1.05], [-0.05, 1.05]], 0.04, m);
// Open jaw at the top.
plate(
g,
[[-0.05, 1.05], [0.05, 1.05], [0.15, 1.15], [0.14, 1.3], [0.07, 1.35], [0.05, 1.2], [-0.05, 1.2], [-0.07, 1.35], [-0.14, 1.3], [-0.15, 1.15]],
0.045,
m,
);
// Stamped label.
box(g, 0.042, 0.25, 0.03, mat(0x7c848c, METAL), 0, 0.5, 0);
return g;
}
// ---------------------------------------------------------------------------
// Odds and ends: chopstick nunchaku, toothpick, clothes peg, paperclip
/** Two lacquered chopsticks joined by a short string: one is held, the other swings out ahead. */
function buildNunchaku(): THREE.Group {
const g = new THREE.Group();
const lacquer = mat(0x1d1f24, WOOD);
const red = mat(0xc8323c, WOOD);
rod(g, 0.03, 0.026, -0.12, 0.42, lacquer, 6);
rod(g, 0.032, 0.032, -0.08, -0.04, red, 6);
// String, then the swinging stick, kinked a little off the line like it's mid-swing.
rod(g, 0.008, 0.008, 0.42, 0.52, mat(0xe8e0cc, SOFT), 4);
const swing = new THREE.Group();
swing.name = 'flail';
swing.position.set(0, 0.52, 0);
g.add(swing);
rod(swing, 0.026, 0.03, 0, 0.48, lacquer, 6);
rod(swing, 0.032, 0.032, 0.42, 0.46, red, 6);
return g;
}
/** A sharpened wooden toothpick: a light, quick rapier. */
function buildToothpick(): THREE.Group {
const g = new THREE.Group();
const wood = mat(0xe3c48e, WOOD);
cone(g, 0.022, 0.6, 1.0, wood, 6);
rod(g, 0.022, 0.022, -0.12, 0.6, wood, 6);
cone(g, 0.022, -0.12, -0.2, wood, 6);
// A fancy cocktail-stick knot of foil near the grip, as a guard.
put(g, new THREE.IcosahedronGeometry(0.06, 0), mat(0xd8434a, { metalness: 0.5, roughness: 0.3 }), 0, 0.02, 0);
return g;
}
/** A sprung wooden clothes peg, held by the jaws' other end. */
function buildClothespeg(): THREE.Group {
const g = new THREE.Group();
const wood = mat(0xd9b77a, WOOD);
box(g, 0.05, 0.62, 0.07, wood, -0.03, 0.27, 0);
box(g, 0.05, 0.62, 0.07, wood, 0.03, 0.27, 0, 0, 0, -0.04);
put(g, new THREE.TorusGeometry(0.05, 0.012, 4, 10), mat(0xc0c6cc, METAL), 0, 0.2, 0, 0, Math.PI / 2, 0);
return g;
}
/** A paperclip pulled out straight, with a hook left at the tip: a stinging little whip. */
function buildPaperclip(): THREE.Group {
const g = new THREE.Group();
const wire = mat(0xc9d1d6, METAL);
// Handle loop, the long straightened wire (a gentle wave), and the hooked tip.
put(g, new THREE.TorusGeometry(0.07, 0.014, 4, 12), wire, 0, -0.05, 0, 0, Math.PI / 2, 0);
const pts = [0, 0.25, 0.5, 0.75, 1.0].map((y, i) => new THREE.Vector3(0, y + 0.02, Math.sin(i * 1.4) * 0.03));
const curve = new THREE.CatmullRomCurve3(pts);
put(g, new THREE.TubeGeometry(curve, 16, 0.014, 4, false), wire);
put(g, new THREE.TorusGeometry(0.05, 0.014, 4, 10, Math.PI), wire, 0, 1.05, 0.04, 0, Math.PI / 2, Math.PI / 2);
return g;
}
// ---------------------------------------------------------------------------
// Definitions
// ---------------------------------------------------------------------------
export const KATANA: WeaponDef = {
id: 'katana',
name: 'Katana',
style: 'katana',
length: 1.05,
damage: 1,
speed: 1,
knockback: 1,
durability: Infinity,
twoHanded: true,
build: buildKatana,
};
function def(
id: string,
name: string,
style: WeaponDef['style'],
length: number,
damage: number,
speed: number,
knockback: number,
durability: number,
twoHanded: boolean,
build: () => THREE.Group,
): WeaponDef {
return { id, name, style, length, damage, speed, knockback, durability, twoHanded, build };
}
const LIST: WeaponDef[] = [
KATANA,
// kitchen
def('fork', 'Fork', 'stab', 1.05, 0.8, 0.9, 0.8, 10, true, buildFork),
def('spoon', 'Spoon', 'blunt', 0.9, 0.6, 1.05, 1.0, 10, false, buildSpoon),
def('butterknife', 'Butter Knife', 'slash', 0.85, 0.7, 1.1, 0.7, 8, false, buildButterknife),
def('whisk', 'Whisk', 'blunt', 0.95, 0.6, 0.95, 1.2, 5, false, buildWhisk),
def('spatula', 'Spatula', 'blunt', 1.1, 0.7, 0.85, 1.8, 9, true, buildSpatula),
def('chopstick', 'Chopstick', 'stab', 1.1, 0.6, 1.25, 0.5, 5, false, buildChopstick),
def('ladle', 'Ladle', 'blunt', 1.19, 1.1, 0.75, 1.5, 12, true, buildLadle),
def('pizzacutter', 'Pizza Cutter', 'slash', 0.85, 0.95, 1.0, 0.8, 9, false, buildPizzacutter),
// bathroom
def('toothbrush', 'Toothbrush', 'blunt', 1.0, 0.45, 1.2, 0.6, 5, false, buildToothbrush),
def('comb', 'Comb', 'slash', 0.75, 0.55, 1.15, 0.6, 4, false, buildComb),
def('razor', 'Razor', 'slash', 0.6, 1.25, 1.1, 0.6, 6, false, buildRazor),
def('cottonswab', 'Cotton Swab', 'blunt', 0.71, 0.2, 1.25, 0.3, 3, false, buildCottonswab),
def('nailfile', 'Nail File', 'slash', 0.8, 0.6, 1.1, 0.6, 6, false, buildNailfile),
def('tweezers', 'Tweezers', 'stab', 0.64, 0.75, 1.15, 0.5, 8, false, buildTweezers),
// bedroom
def('pencil', 'Pencil', 'stab', 1.15, 0.85, 1.0, 0.7, 6, false, buildPencil),
def('ruler', 'Ruler', 'slash', 1.6, 0.75, 0.8, 1.3, 6, true, buildRuler),
def('paintbrush', 'Paintbrush', 'blunt', 1.0, 0.5, 1.1, 0.8, 5, false, buildPaintbrush),
def('knittingneedle', 'Knitting Needle', 'stab', 1.8, 0.9, 0.85, 0.6, 10, true, buildKnittingneedle),
def('crayon', 'Crayon', 'blunt', 0.6, 0.45, 1.2, 0.7, 3, false, buildCrayon),
def('hairpin', 'Hairpin', 'stab', 0.6, 0.65, 1.25, 0.4, 7, false, buildHairpin),
// living room
def('screwdriver', 'Screwdriver', 'stab', 1.05, 0.95, 0.95, 0.8, 13, false, buildScrewdriver),
def('wrench', 'Wrench', 'blunt', 1.35, 1.3, 0.7, 2.0, 14, true, buildWrench),
// odds and ends
{ ...def('nunchaku', 'Chopstick Nunchaku', 'blunt', 1.0, 0.85, 1.2, 1.3, 9, false, buildNunchaku), flail: true },
def('toothpick', 'Toothpick Rapier', 'stab', 1.0, 0.7, 1.3, 0.5, 6, false, buildToothpick),
def('clothespeg', 'Clothes Peg', 'blunt', 0.6, 0.75, 1.15, 1.1, 8, false, buildClothespeg),
def('paperclip', 'Paperclip Whip', 'slash', 1.1, 0.75, 1.1, 1.4, 7, false, buildPaperclip),
];
export const ITEMS: Record<string, WeaponDef> = Object.fromEntries(LIST.map((w) => [w.id, w]));
export function itemById(id: string): WeaponDef {
return ITEMS[id] ?? KATANA;
}
// ---------------------------------------------------------------------------
// Pickup marker
// ---------------------------------------------------------------------------
let markerGeo: THREE.RingGeometry | null = null;
let markerMat: THREE.MeshBasicMaterial | null = null;
/** Little pickup visuals: an idle bob/spin is done elsewhere; just a helper to build the floating ring marker. */
export function buildPickupMarker(): THREE.Object3D {
if (!markerGeo) {
markerGeo = new THREE.RingGeometry(0.4, 0.48, 32);
markerGeo.rotateX(-Math.PI / 2);
}
if (!markerMat) {
markerMat = new THREE.MeshBasicMaterial({
color: 0xffd08a,
transparent: true,
opacity: 0.6,
depthWrite: false,
side: THREE.DoubleSide,
});
}
const m = new THREE.Mesh(markerGeo, markerMat);
m.castShadow = false;
m.receiveShadow = false;
m.renderOrder = 1;
return m;
}
