|
|
@ -3,7 +3,7 @@ import { RigidBodyDesc, ColliderDesc } from '@dimforge/rapier3d-compat'; |
|
|
|
import { AudioContent, VideoContent } from './Content'; |
|
|
|
|
|
|
|
export class Player { |
|
|
|
constructor(rapierWorld, spawnPosition = new THREE.Vector3(0, 1, 0), itemList) { |
|
|
|
constructor(rapierWorld, renderer, scene, spawnPosition = new THREE.Vector3(0, 1, 0), itemList) { |
|
|
|
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); |
|
|
|
this.position = spawnPosition.clone(); |
|
|
|
this.rotation = new THREE.Euler(0, 0, 0, 'YXZ'); |
|
|
@ -13,6 +13,9 @@ export class Player { |
|
|
|
this.name = null; |
|
|
|
this.audioListener = new THREE.AudioListener(); |
|
|
|
|
|
|
|
this.renderer = renderer; |
|
|
|
this.scene = scene; |
|
|
|
|
|
|
|
this.rapierWorld = rapierWorld; |
|
|
|
this.rigibody = null; |
|
|
|
this.collider = null; |
|
|
@ -41,6 +44,19 @@ export class Player { |
|
|
|
|
|
|
|
this.enableInput = false; |
|
|
|
|
|
|
|
/* VR Stuff */ |
|
|
|
this.vrControllers = []; |
|
|
|
this.vrGamepads = [null, null]; |
|
|
|
this.teleArc = null; |
|
|
|
this.teleMarker = null; |
|
|
|
this.teleporting = false; |
|
|
|
this.teleportTarget = new THREE.Vector3(); |
|
|
|
this.playerRig = new THREE.Group(); |
|
|
|
|
|
|
|
this.teleportDistanceFactor = 1.0; // Default distance multiplier
|
|
|
|
this.minTeleportDistanceFactor = 0.1; // Minimum distance multiplier
|
|
|
|
this.maxTeleportDistanceFactor = 1.5; // Maximum distance multiplier
|
|
|
|
|
|
|
|
this._init(); |
|
|
|
this._setupInput(); |
|
|
|
this._bindEvents(); |
|
|
@ -113,6 +129,8 @@ export class Player { |
|
|
|
|
|
|
|
// Attach audio listener to the camera/player
|
|
|
|
this.camera.add(this.audioListener); |
|
|
|
|
|
|
|
this.playerRig.add(this.camera); |
|
|
|
} |
|
|
|
|
|
|
|
_setupInput() { |
|
|
@ -155,6 +173,205 @@ export class Player { |
|
|
|
this.isDrawing = false; |
|
|
|
} |
|
|
|
|
|
|
|
_setupVR() { |
|
|
|
const markerGeometry = new THREE.CircleGeometry(0.5, 32); |
|
|
|
markerGeometry.rotateX(-Math.PI / 2); |
|
|
|
const markerMat = new THREE.MeshBasicMaterial({color: 0x00ff00, transparent: false, opacity: 0.5}); |
|
|
|
this.teleMarker = new THREE.Mesh(markerGeometry, markerMat); |
|
|
|
this.teleMarker.visible = false; |
|
|
|
this.playerRig.add(this.teleMarker); |
|
|
|
|
|
|
|
// Setup teleport arc
|
|
|
|
const arcMaterial = new THREE.LineBasicMaterial({ color: 0x00ff00, linewidth: 2 }); |
|
|
|
const arcGeometry = new THREE.BufferGeometry(); |
|
|
|
this.teleArc = new THREE.Line(arcGeometry, arcMaterial); |
|
|
|
this.teleArc.visible = false; |
|
|
|
this.playerRig.add(this.teleArc); |
|
|
|
|
|
|
|
// Controller Setup
|
|
|
|
for (let i = 0; i < 2; i++) { |
|
|
|
const controller = this.renderer.xr.getController(i); |
|
|
|
this.playerRig.add(controller); |
|
|
|
this.vrControllers.push(controller); |
|
|
|
|
|
|
|
// --- NEW: Add the 'connected' listener to get the Gamepad object ---
|
|
|
|
controller.addEventListener('connected', (event) => { |
|
|
|
// The Gamepad object is in event.data.gamepad
|
|
|
|
this.vrGamepads[i] = event.data.gamepad; |
|
|
|
console.log(`Controller ${i} connected, Gamepad stored.`); |
|
|
|
}); |
|
|
|
|
|
|
|
// --- OPTIONAL: Handle disconnection ---
|
|
|
|
controller.addEventListener('disconnected', () => { |
|
|
|
this.vrGamepads[i] = null; |
|
|
|
console.log(`Controller ${i} disconnected.`); |
|
|
|
}); |
|
|
|
|
|
|
|
// Add a debug sphere to the controller
|
|
|
|
const sphereGeometry = new THREE.SphereGeometry(0.05, 8, 8); |
|
|
|
const sphereMaterial = new THREE.MeshBasicMaterial({ color: (i === 0 ? 0xff0000 : 0x0000ff) }); // Red for left, Blue for right
|
|
|
|
const debugSphere = new THREE.Mesh(sphereGeometry, sphereMaterial); |
|
|
|
controller.add(debugSphere); |
|
|
|
|
|
|
|
const lineGeometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1)]); |
|
|
|
const line = new THREE.Line(lineGeometry); |
|
|
|
line.scale.z = 5; |
|
|
|
controller.add(line); |
|
|
|
|
|
|
|
controller.addEventListener('selectstart', () => this._OnVRSelectStart(i)); |
|
|
|
controller.addEventListener('selectend', () => this._OnVRSelectEnd(i)); |
|
|
|
controller.addEventListener('squeezestart', () => this._OnVRSqueezeStart(i)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
_OnVRSelectStart(controllerIndex) { |
|
|
|
const controller = this.vrControllers[controllerIndex]; |
|
|
|
// Right controller (index 1) for drawing/interaction
|
|
|
|
console.log(`Select Started: ${controllerIndex}`); |
|
|
|
if (controllerIndex === 0) { |
|
|
|
if (this.currentIntItem && !this.attachedItem) { |
|
|
|
this.attachedItem = this.currentIntItem; |
|
|
|
this.attachedItem.isActive = true; |
|
|
|
} else { |
|
|
|
this.isDrawing = true; |
|
|
|
} |
|
|
|
} |
|
|
|
// Left controller (index 0) for teleporting
|
|
|
|
if (controllerIndex === 1) { |
|
|
|
this.teleporting = true; |
|
|
|
this.teleArc.visible = true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
_OnVRSelectEnd(controllerIndex) { |
|
|
|
// Right controller
|
|
|
|
console.log(`Select End: ${controllerIndex}`); |
|
|
|
if (controllerIndex === 0) { |
|
|
|
this.isDrawing = false; |
|
|
|
} |
|
|
|
// Left controller
|
|
|
|
if (controllerIndex === 1) { |
|
|
|
this.teleporting = false; |
|
|
|
this.teleArc.visible = false; |
|
|
|
if (this.teleMarker.visible) { |
|
|
|
const newPosition = this.teleportTarget.clone(); |
|
|
|
newPosition.y = 10; // Maintain height
|
|
|
|
this.playerRig.position.copy(newPosition); |
|
|
|
|
|
|
|
this.rigibody.setNextKinematicTranslation({ x: newPosition.x, y: newPosition.y, z: newPosition.z }); |
|
|
|
this.position.copy(newPosition); |
|
|
|
} |
|
|
|
this.teleMarker.visible = false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
_OnVRSqueezeStart(controllerIndex) { |
|
|
|
console.log(`Squeeze Started: ${controllerIndex}`); |
|
|
|
// Use squeeze on right controller to detach item
|
|
|
|
if (controllerIndex === 0 && this.attachedItem) { |
|
|
|
this.attachedItem.isActive = false; |
|
|
|
this.attachedItem._removeContentDisplay(); |
|
|
|
this.attachedItem = null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
_handleVRJoystick() { |
|
|
|
// Get the gamepad for the left controller (index 1 in your setup)
|
|
|
|
const gamepad = this.vrGamepads[1]; |
|
|
|
|
|
|
|
// You no longer need to check this.vrControllers[1] since the gamepad is null if disconnected.
|
|
|
|
if (!gamepad) return; |
|
|
|
|
|
|
|
// The axes array for a thumbstick is often at index 2 (X) and 3 (Y) for the primary stick.
|
|
|
|
// If the left stick is the primary for movement/teleport, these are the typical indices.
|
|
|
|
// Always check for undefined or use a safe index, just in case.
|
|
|
|
const joystickVertical = gamepad.axes[3]; |
|
|
|
|
|
|
|
if (joystickVertical !== undefined) { |
|
|
|
// joystickVertical is -1 (forward) to 1 (backward). We want forward to be max distance.
|
|
|
|
// We'll map the [-1, 1] range to our [min, max] distance factor range.
|
|
|
|
|
|
|
|
// 1. Convert [-1, 1] to [0, 1] (Mapping: -1 -> 1, 0 -> 0.5, 1 -> 0)
|
|
|
|
// Since Y is typically -1 forward, using (-Y + 1) / 2 makes full forward (Y=-1) equal to 1.
|
|
|
|
const normalizedValue = (-joystickVertical + 1) / 2; |
|
|
|
|
|
|
|
// 2. Linearly interpolate between min and max factors
|
|
|
|
this.teleportDistanceFactor = this.minTeleportDistanceFactor + normalizedValue * (this.maxTeleportDistanceFactor - this.minTeleportDistanceFactor); |
|
|
|
|
|
|
|
// Optional: Apply a small deadzone to prevent accidental changes when the stick is centered
|
|
|
|
const deadzone = 0.05; |
|
|
|
if (Math.abs(joystickVertical) < deadzone) { |
|
|
|
// If centered, reset to the default factor (e.g., the midpoint of your min/max range)
|
|
|
|
this.teleportDistanceFactor = (this.minTeleportDistanceFactor + this.maxTeleportDistanceFactor) / 2; |
|
|
|
} |
|
|
|
|
|
|
|
// console.log(`Normalized: ${normalizedValue.toFixed(2)}, Factor: ${this.teleportDistanceFactor.toFixed(2)}`);
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
_handleVRTeleport(floorObjects) { |
|
|
|
if (!this.teleporting) { |
|
|
|
this.teleMarker.visible = false; |
|
|
|
this.teleArc.visible = false; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const controller = this.vrControllers[1]; // Left controller for teleporting
|
|
|
|
const controllerMatrix = controller.matrixWorld; |
|
|
|
|
|
|
|
const initialVelocity = 50 * this.teleportDistanceFactor;; |
|
|
|
const gravity = -9.8; |
|
|
|
const timeStep = 0.03; |
|
|
|
const numSegments = 100; |
|
|
|
|
|
|
|
const points = []; |
|
|
|
const startPoint = new THREE.Vector3().setFromMatrixPosition(controllerMatrix); |
|
|
|
points.push(startPoint.clone()); |
|
|
|
const launchDirection = new THREE.Vector3(0, 0, -1).applyMatrix4(new THREE.Matrix4().extractRotation(controllerMatrix)); |
|
|
|
|
|
|
|
let lastPoint = startPoint.clone(); |
|
|
|
let hit = false; |
|
|
|
|
|
|
|
for (let i = 1; i < numSegments; i++) { |
|
|
|
const t = i * timeStep; |
|
|
|
const currentPoint = new THREE.Vector3( |
|
|
|
startPoint.x + launchDirection.x * initialVelocity * t, |
|
|
|
startPoint.y + launchDirection.y * initialVelocity * t + 0.5 * gravity * t * t, |
|
|
|
startPoint.z + launchDirection.z * initialVelocity * t |
|
|
|
); |
|
|
|
|
|
|
|
const ray = new THREE.Raycaster(lastPoint, currentPoint.clone().sub(lastPoint).normalize()); |
|
|
|
ray.far = lastPoint.distanceTo(currentPoint); |
|
|
|
const floorMeshes = floorObjects.map(obj => obj.mesh); |
|
|
|
const intersects = ray.intersectObjects(floorMeshes); |
|
|
|
|
|
|
|
if (intersects.length > 0) { |
|
|
|
const intersectPoint = intersects[0].point; |
|
|
|
points.push(intersectPoint); |
|
|
|
this.teleportTarget.copy(intersectPoint); |
|
|
|
this.teleMarker.position.copy(this.playerRig.worldToLocal(this.teleportTarget.clone())); |
|
|
|
this.teleMarker.position.y += 0.01; // Avoid z-fighting
|
|
|
|
this.teleMarker.visible = true; |
|
|
|
hit = true; |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
points.push(currentPoint); |
|
|
|
lastPoint = currentPoint; |
|
|
|
} |
|
|
|
|
|
|
|
if (!hit) { |
|
|
|
this.teleMarker.visible = false; |
|
|
|
} |
|
|
|
|
|
|
|
// Convert world-space points to local-space for the rig
|
|
|
|
const localPoints = points.map(p => this.playerRig.worldToLocal(p.clone())); |
|
|
|
|
|
|
|
this.teleArc.geometry.setFromPoints(localPoints); |
|
|
|
this.teleArc.geometry.computeBoundingSphere(); // Important for visibility
|
|
|
|
this.teleArc.visible = true; |
|
|
|
} |
|
|
|
|
|
|
|
_drawOnTexture(intersect, color = 'red') { |
|
|
|
const object = intersect.object; |
|
|
|
const uv = intersect.uv; |
|
|
@ -199,20 +416,36 @@ export class Player { |
|
|
|
} |
|
|
|
|
|
|
|
draw(drawableObjects) { |
|
|
|
if (!this.isDrawing) return; |
|
|
|
|
|
|
|
this.pointer.x = 0; |
|
|
|
this.pointer.y = 0; |
|
|
|
|
|
|
|
this.raycast.setFromCamera(this.pointer, this.camera); |
|
|
|
|
|
|
|
const meshesToIntersect = drawableObjects.map(obj => obj.mesh); |
|
|
|
const intersections = this.raycast.intersectObjects(meshesToIntersect); |
|
|
|
|
|
|
|
if( intersections.length > 0) { |
|
|
|
const intersect = intersections[0]; |
|
|
|
if( intersect.object.material.map && intersect.object.material.map.isCanvasTexture ) { |
|
|
|
this._drawOnTexture(intersect); |
|
|
|
// Desktop drawing
|
|
|
|
if (this.isDrawing && !this.vrControllers[0]?.userData.isDrawing) { |
|
|
|
this.pointer.x = 0; |
|
|
|
this.pointer.y = 0; |
|
|
|
this.raycast.setFromCamera(this.pointer, this.camera); |
|
|
|
const intersections = this.raycast.intersectObjects(meshesToIntersect); |
|
|
|
if (intersections.length > 0) { |
|
|
|
const intersect = intersections[0]; |
|
|
|
if (intersect.object.material.map && intersect.object.material.map.isCanvasTexture) { |
|
|
|
this._drawOnTexture(intersect); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// VR drawing (right controller)
|
|
|
|
const vrController = this.vrControllers[0]; |
|
|
|
if (this.isDrawing && vrController) { |
|
|
|
const controllerMatrix = vrController.matrixWorld; |
|
|
|
const ray = new THREE.Raycaster(); |
|
|
|
ray.ray.origin.setFromMatrixPosition(controllerMatrix); |
|
|
|
ray.ray.direction.set(0, 0, -1).applyMatrix4(new THREE.Matrix4().extractRotation(controllerMatrix)); |
|
|
|
|
|
|
|
const intersections = ray.intersectObjects(meshesToIntersect); |
|
|
|
if (intersections.length > 0) { |
|
|
|
const intersect = intersections[0]; |
|
|
|
if (intersect.object.material.map && intersect.object.material.map.isCanvasTexture) { |
|
|
|
this._drawOnTexture(intersect); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -220,7 +453,18 @@ export class Player { |
|
|
|
_checkForInteractableItems() { |
|
|
|
|
|
|
|
const ray = new THREE.Raycaster(); |
|
|
|
ray.set(this.camera.position, this.camera.getWorldDirection(new THREE.Vector3())); |
|
|
|
let isVR = this.vrControllers[0] && this.vrControllers[0].visible; |
|
|
|
|
|
|
|
if (isVR) { |
|
|
|
// Use right controller for interaction ray
|
|
|
|
const controller = this.vrControllers[0]; |
|
|
|
const controllerMatrix = controller.matrixWorld; |
|
|
|
ray.ray.origin.setFromMatrixPosition(controllerMatrix); |
|
|
|
ray.ray.direction.set(0, 0, -1).applyMatrix4(new THREE.Matrix4().extractRotation(controllerMatrix)); |
|
|
|
} else { |
|
|
|
// Use camera for desktop interaction ray
|
|
|
|
ray.set(this.camera.position, this.camera.getWorldDirection(new THREE.Vector3())); |
|
|
|
} |
|
|
|
|
|
|
|
const nearbyItems = this.itemList.filter(item => item.object && this.position.distanceTo(item.object.position) < this.maxInteractionDistance); |
|
|
|
|
|
|
@ -295,11 +539,17 @@ export class Player { |
|
|
|
this.rigibody.setNextKinematicTranslation({ x: newPosition.x, y: newPosition.y, z: newPosition.z }); |
|
|
|
} |
|
|
|
|
|
|
|
update(delta) { |
|
|
|
update(delta, spawnedObjects) { |
|
|
|
//console.log(`Number of Controllers: ${this.vrControllers.length}`);
|
|
|
|
if (this.renderer.xr.isPresenting) { |
|
|
|
this._handleVRJoystick(); |
|
|
|
this._handleVRTeleport(spawnedObjects); |
|
|
|
} |
|
|
|
|
|
|
|
if (this.enableInput) { |
|
|
|
if (this.attachedItem) { |
|
|
|
this._lockCameraForAttachedItem(); |
|
|
|
} else { |
|
|
|
this._lockCameraForAttachedItem(); |
|
|
|
} else if (!this.renderer.xr.isPresenting) { // Only update movement if not in VR
|
|
|
|
this._updatePlayerMovement(delta); |
|
|
|
} |
|
|
|
this._checkForInteractableItems(); |
|
|
|