Files
tilt/src/physics/net.js
T
2026-08-03 06:43:21 -05:00

125 lines
4.6 KiB
JavaScript

import * as THREE from 'three';
import { NET, goalLineX } from '../../shared/net.js';
import { CAT, KIND, makeTag, xyz } from './bridge.js';
/**
* The goal frame: posts, crossbar, and a mesh back that stops the puck.
*
* Static bodies, because a net that moves is a rule (it comes off its moorings)
* rather than a feature, and not one worth having before there is a game.
*
* The back and sides are solid boxes rather than a real mesh. A puck that goes
* in should stay in and settle, and modelling twine is a lot of work to make a
* puck stop moving.
*/
export function createNet(physics, end) {
const { api, world } = physics;
const line = goalLineX(end);
const halfW = NET.width / 2;
const r = NET.postRadius;
const sd = api.b3DefaultShapeDef();
sd.baseMaterial.friction = 0.4;
// Posts ring; the back eats everything so the puck settles in the net.
sd.baseMaterial.restitution = 0.35;
sd.baseMaterial.userMaterialId = makeTag(KIND.RINK, 0xff, end > 0 ? 10 : 11);
sd.filter.categoryBits = CAT.RINK;
sd.filter.maskBits = 0xffffffffffffffffn;
const bodies = [];
const box = (x, y, z, hx, hy, hz, restitution = null) => {
const bd = api.b3DefaultBodyDef();
bd.position = xyz(x, y, z);
const b = api.b3CreateBody(world, bd);
if (restitution !== null) sd.baseMaterial.restitution = restitution;
api.b3CreateBoxShape(b, sd, hx, hy, hz);
sd.baseMaterial.restitution = 0.35;
bodies.push(b);
return b;
};
// Posts, on the line.
box(line, NET.height / 2, halfW, r, NET.height / 2, r);
box(line, NET.height / 2, -halfW, r, NET.height / 2, r);
// Crossbar.
box(line, NET.height, 0, r, r, halfW);
// Back and sides, deadened so the puck does not fire back out.
//
// The net extends *away* from centre ice, `line + end * depth`. Getting this
// sign backwards put the back panel a metre in front of the goal line — a
// solid wall across the mouth — and every shot in the game bounced off it
// before it could cross. Nothing ever scored, and the symptom looked like a
// goalie problem.
box(line + end * NET.depth, NET.height / 2, 0, 0.04, NET.height / 2, halfW, 0.02);
box(line + end * NET.depth * 0.5, NET.height / 2, halfW, NET.depth / 2, NET.height / 2, 0.03, 0.05);
box(line + end * NET.depth * 0.5, NET.height / 2, -halfW, NET.depth / 2, NET.height / 2, 0.03, 0.05);
return {
end,
bodies,
destroy() {
for (const b of bodies) api.b3DestroyBody(b);
},
};
}
/** The rendered net: frame tubes plus a translucent mesh bag. */
export function buildNetMesh(scene, end) {
const line = goalLineX(end);
const halfW = NET.width / 2;
const group = new THREE.Group();
group.name = 'net:' + end;
const frame = new THREE.MeshStandardMaterial({ color: 0xc0332c, roughness: 0.45, metalness: 0.25 });
const mesh = new THREE.MeshStandardMaterial({
color: 0xf2f4f8,
roughness: 0.9,
transparent: true,
opacity: 0.28,
side: THREE.DoubleSide,
depthWrite: false,
});
const tube = (len, x, y, z, axis) => {
const g = new THREE.CylinderGeometry(NET.postRadius, NET.postRadius, len, 10);
const m = new THREE.Mesh(g, frame);
if (axis === 'z') m.rotation.x = Math.PI / 2;
if (axis === 'x') m.rotation.z = Math.PI / 2;
m.position.set(x, y, z);
m.castShadow = true;
group.add(m);
};
tube(NET.height, line, NET.height / 2, halfW, 'y');
tube(NET.height, line, NET.height / 2, -halfW, 'y');
tube(NET.width, line, NET.height, 0, 'z');
// Back frame, so the net reads as a box rather than as a doorway.
tube(NET.depth, line + end * NET.depth / 2, 0.06, halfW, 'x');
tube(NET.depth, line + end * NET.depth / 2, 0.06, -halfW, 'x');
const back = new THREE.Mesh(new THREE.PlaneGeometry(NET.width, NET.height), mesh);
back.position.set(line + end * NET.depth, NET.height / 2, 0);
back.rotation.y = Math.PI / 2;
group.add(back);
for (const s of [1, -1]) {
const side = new THREE.Mesh(new THREE.PlaneGeometry(NET.depth, NET.height), mesh);
side.position.set(line + end * NET.depth / 2, NET.height / 2, s * halfW);
group.add(side);
}
const top = new THREE.Mesh(new THREE.PlaneGeometry(NET.depth, NET.width), mesh);
top.rotation.x = -Math.PI / 2;
top.position.set(line + end * NET.depth / 2, NET.height, 0);
group.add(top);
// Crease paint.
const crease = new THREE.Mesh(
new THREE.CircleGeometry(NET.creaseRadius, 24, end > 0 ? -Math.PI / 2 : Math.PI / 2, Math.PI),
new THREE.MeshBasicMaterial({ color: 0x77b3e0, transparent: true, opacity: 0.45, depthWrite: false }),
);
crease.rotation.x = -Math.PI / 2;
crease.position.set(line, 0.004, 0);
group.add(crease);
scene.add(group);
return group;
}