Download js/engine/PlayerController.js from timfromhcs/3d-outdoor-map-explorer: direct link, hf CLI and curl.
- Browser
- Download file 7.52 kB
-
https://huggingface.co/spaces/timfromhcs/3d-outdoor-map-explorer/resolve/main/js/engine/PlayerController.js
- Command line
-
hf download hf://spaces/timfromhcs/3d-outdoor-map-explorer/js/engine/PlayerController.js
-
curl -L -o PlayerController.js https://huggingface.co/spaces/timfromhcs/3d-outdoor-map-explorer/resolve/main/js/engine/PlayerController.js
7.52 kB
| /** | |
| * PlayerController.js | |
| * First-person kinematic character controller with PointerLockControls, | |
| * sprint, jump, gravity, and collision integration. | |
| */ | |
| class PlayerController { | |
| constructor(camera, domElement, collisionSystem) { | |
| this.camera = camera; | |
| this.domElement = domElement; | |
| this.collision = collisionSystem; | |
| // Player Physical Dimensions (calibrated to map scale) | |
| this.height = 0.18; | |
| this.eyeHeight = 0.16; | |
| this.radius = 0.035; | |
| this.stepHeight = 0.03; | |
| // Kinematic state | |
| this.position = new THREE.Vector3(0, 0.5, 0); | |
| this.velocity = new THREE.Vector3(); | |
| this.grounded = false; | |
| this.movementState = "IDLE"; // "IDLE", "WALKING", "SPRINTING", "IN_AIR" | |
| // Speeds | |
| this.baseWalkSpeed = 0.35; | |
| this.baseSprintSpeed = 0.70; | |
| this.jumpVelocity = 0.85; | |
| this.gravity = 2.8; | |
| // PointerLock | |
| this.controls = new THREE.PointerLockControls(this.camera, this.domElement); | |
| this.isLocked = false; | |
| // Input state | |
| this.inputs = { | |
| forward: false, | |
| backward: false, | |
| left: false, | |
| right: false, | |
| sprint: false, | |
| jump: false | |
| }; | |
| this.spawnPoint = { position: [0, 0.5, 0], lookYaw: 0 }; | |
| this._setupInputListeners(); | |
| } | |
| _setupInputListeners() { | |
| this.controls.addEventListener('lock', () => { | |
| this.isLocked = true; | |
| if (this.onLockChange) this.onLockChange(true); | |
| }); | |
| this.controls.addEventListener('unlock', () => { | |
| this.isLocked = false; | |
| if (this.onLockChange) this.onLockChange(false); | |
| }); | |
| const onKeyDown = (e) => { | |
| switch (e.code) { | |
| case 'KeyW': case 'ArrowUp': this.inputs.forward = true; break; | |
| case 'KeyS': case 'ArrowDown': this.inputs.backward = true; break; | |
| case 'KeyA': case 'ArrowLeft': this.inputs.left = true; break; | |
| case 'KeyD': case 'ArrowRight': this.inputs.right = true; break; | |
| case 'ShiftLeft': case 'ShiftRight': this.inputs.sprint = true; break; | |
| case 'Space': | |
| if (this.grounded) { | |
| this.velocity.y = this.jumpVelocity; | |
| this.grounded = false; | |
| } | |
| break; | |
| } | |
| }; | |
| const onKeyUp = (e) => { | |
| switch (e.code) { | |
| case 'KeyW': case 'ArrowUp': this.inputs.forward = false; break; | |
| case 'KeyS': case 'ArrowDown': this.inputs.backward = false; break; | |
| case 'KeyA': case 'ArrowLeft': this.inputs.left = false; break; | |
| case 'KeyD': case 'ArrowRight': this.inputs.right = false; break; | |
| case 'ShiftLeft': case 'ShiftRight': this.inputs.sprint = false; break; | |
| } | |
| }; | |
| window.addEventListener('keydown', onKeyDown); | |
| window.addEventListener('keyup', onKeyUp); | |
| } | |
| /** | |
| * Resets player position and view to current map's safe spawn point. | |
| */ | |
| respawn(mode = null) { | |
| if (!mode) mode = (window.mapApp && window.mapApp.engine) ? window.mapApp.engine.currentMode : "POV"; | |
| this.position.set(this.spawnPoint.position[0], this.spawnPoint.position[1], this.spawnPoint.position[2]); | |
| this.velocity.set(0, 0, 0); | |
| this.grounded = false; | |
| if (mode === "POV") { | |
| this.camera.position.set(this.position.x, this.position.y + this.eyeHeight, this.position.z); | |
| if (this.spawnPoint.lookYaw !== undefined) { | |
| this.camera.rotation.set(0, THREE.MathUtils.degToRad(this.spawnPoint.lookYaw), 0); | |
| } | |
| } | |
| } | |
| /** | |
| * Configures the safe spawn point and calibrates controller parameters to map extents. | |
| */ | |
| setSpawn(spawnData, mapExtents, mode = null) { | |
| this.spawnPoint = spawnData; | |
| // Calibrate scale based on map vertical extent | |
| const extentY = mapExtents ? mapExtents[1] : 2.0; | |
| if (extentY > 2.0) { | |
| // Realistic outdoor metric world scale (1 unit = 1 meter) | |
| this.height = 1.75; | |
| this.eyeHeight = 1.65; | |
| this.radius = 0.35; | |
| this.stepHeight = 0.40; | |
| this.baseWalkSpeed = 4.5; | |
| this.baseSprintSpeed = 8.5; | |
| this.jumpVelocity = 5.5; | |
| this.gravity = 14.0; | |
| } else { | |
| // Miniature scale fallback | |
| const scale = Math.max(0.4, Math.min(2.5, extentY / 0.35)); | |
| this.height = 0.16 * scale; | |
| this.eyeHeight = 0.14 * scale; | |
| this.radius = 0.03 * scale; | |
| this.stepHeight = 0.025 * scale; | |
| this.baseWalkSpeed = 0.35 * scale; | |
| this.baseSprintSpeed = 0.70 * scale; | |
| this.jumpVelocity = 0.85 * Math.sqrt(scale); | |
| this.gravity = 3.2 * scale; | |
| } | |
| this.respawn(mode); | |
| } | |
| update(delta) { | |
| if (delta > 0.1) delta = 0.1; // Clamp large time steps | |
| // 1. Calculate horizontal intent vector in camera space | |
| const inputVec = new THREE.Vector3(); | |
| if (this.inputs.forward) inputVec.z -= 1; | |
| if (this.inputs.backward) inputVec.z += 1; | |
| if (this.inputs.left) inputVec.x -= 1; | |
| if (this.inputs.right) inputVec.x += 1; | |
| const isMoving = inputVec.lengthSq() > 0; | |
| if (isMoving) inputVec.normalize(); | |
| const currentSpeed = this.inputs.sprint ? this.baseSprintSpeed : this.baseWalkSpeed; | |
| // Transform input vector to world space horizontal heading | |
| const yawEuler = new THREE.Euler(0, this.camera.rotation.y, 0, 'YXZ'); | |
| const worldMove = inputVec.applyEuler(yawEuler).multiplyScalar(currentSpeed); | |
| // 2. Wall collision & sliding | |
| const horizontalVelocity = this.collision.collideAndSlide( | |
| this.position, | |
| worldMove, | |
| this.radius, | |
| this.height | |
| ); | |
| // Apply horizontal movement | |
| this.position.x += horizontalVelocity.x * delta; | |
| this.position.z += horizontalVelocity.z * delta; | |
| // 3. Ground check & Gravity | |
| const groundHit = this.collision.checkGround(this.position, this.stepHeight); | |
| if (groundHit && this.velocity.y <= 0) { | |
| // Player is touching ground | |
| this.grounded = true; | |
| this.velocity.y = 0; | |
| // Smooth snap to ground | |
| this.position.y = groundHit.groundY; | |
| if (isMoving) { | |
| this.movementState = this.inputs.sprint ? "SPRINTING" : "WALKING"; | |
| } else { | |
| this.movementState = "IDLE"; | |
| } | |
| } else { | |
| // In air | |
| this.grounded = false; | |
| this.movementState = "IN_AIR"; | |
| this.velocity.y -= this.gravity * delta; | |
| this.position.y += this.velocity.y * delta; | |
| } | |
| // 4. Out of bounds check & auto recovery | |
| if (this.collision.isOutOfBounds(this.position)) { | |
| console.warn("Player fell out of world, resetting to safe spawn."); | |
| this.respawn(); | |
| } | |
| // 5. Sync camera position to eye level | |
| this.camera.position.set( | |
| this.position.x, | |
| this.position.y + this.eyeHeight, | |
| this.position.z | |
| ); | |
| } | |
| } | |
| window.PlayerController = PlayerController; | |