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