PROCITY/web/js/world/player.js
m3ultra 5313402e43 Lane B (Streetscape): chunk-streamed walkable town shell
First-person shell: chunk streaming, instanced buildings + facade texture atlas
(22 facade materials -> 1; 5 skin materials city-wide), signs, awnings, furniture,
day/night lighting, minimap, pixel-accurate collision (incl. arbitrary-angle lots),
door raycast -> procity:enterShop, DBG harness, all-fallback mode. Renders Lane A's
full generated town (seed 20261990 'Boolarra Heads', 493 shops); worst continuous-
walk view ~261 draws (<=300 gate). 6 adversarial-review bugs fixed (collision sign,
house doors, awning skin, sign atlas, furniture seed hash, shot-mode restore).

index.html and skins.js include Lane F's inline integration seam edits (marked
'[Lane F integration]'): interior/keeper/citizen wiring + facade filename fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 14:28:47 +10:00

83 lines
2.8 KiB
JavaScript

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