// PROCITY Lane B — player.js // PointerLockControls first-person walk (WASD + shift-run), eye at 1.7m, with push-out collision // against the oriented-rectangle colliders of the nearby chunks (buildings + private yards). // No navmesh — the town is rectangles, so a couple of push-out passes is plenty. All scratch // vectors are reused; the update loop allocates nothing. import * as THREE from 'three'; import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; import { pushOutRect } from './planutil.js'; const EYE = 1.7; const RADIUS = 0.38; // player collision radius const WALK = 4.6, RUN = 8.8; export function createPlayer(camera, domElement, { getColliders }) { camera.position.y = EYE; const controls = new PointerLockControls(camera, domElement); const _dir = new THREE.Vector3(); const _fwd = new THREE.Vector3(); const _right = new THREE.Vector3(); const _move = new THREE.Vector3(); const _out = [0, 0]; let enabled = true; // keys: a live Set of held key codes (index.html fills it) function update(dt, keys) { if (!enabled) return; // horizontal forward/right from camera yaw camera.getWorldDirection(_dir); _fwd.set(_dir.x, 0, _dir.z); if (_fwd.lengthSq() < 1e-6) _fwd.set(0, 0, -1); _fwd.normalize(); _right.set(-_fwd.z, 0, _fwd.x); // camera-right = normalize(cross(forward, up)) on XZ let f = 0, s = 0; if (keys.has('KeyW') || keys.has('ArrowUp')) f += 1; if (keys.has('KeyS') || keys.has('ArrowDown')) f -= 1; if (keys.has('KeyD') || keys.has('ArrowRight')) s += 1; if (keys.has('KeyA') || keys.has('ArrowLeft')) s -= 1; _move.set(0, 0, 0); if (f || s) { _move.addScaledVector(_fwd, f).addScaledVector(_right, s); if (_move.lengthSq() > 1e-6) _move.normalize(); const speed = (keys.has('ShiftLeft') || keys.has('ShiftRight')) ? RUN : WALK; _move.multiplyScalar(speed * dt); } let px = camera.position.x + _move.x; let pz = camera.position.z + _move.z; // collision: two push-out passes so inner corners resolve const cols = getColliders ? getColliders(px, pz) : null; if (cols && cols.length) { for (let pass = 0; pass < 2; pass++) { for (let i = 0; i < cols.length; i++) { if (pushOutRect(px, pz, RADIUS, cols[i], _out)) { px = _out[0]; pz = _out[1]; } } } } camera.position.x = px; camera.position.z = pz; camera.position.y = EYE; } function teleport(x, z, yaw = 0) { camera.position.set(x, EYE, z); camera.rotation.set(0, yaw, 0, 'YXZ'); } return { controls, update, teleport, get position() { return camera.position; }, setEnabled: (v) => { enabled = v; }, lock: () => controls.lock(), unlock: () => controls.unlock(), get isLocked() { return controls.isLocked; }, }; }