You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
561 lines
22 KiB
561 lines
22 KiB
import * as THREE from 'three';
|
|
import { RigidBodyDesc, ColliderDesc } from '@dimforge/rapier3d-compat';
|
|
import { AudioContent, VideoContent } from './Content';
|
|
|
|
export class Player {
|
|
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');
|
|
this.velocity = new THREE.Vector3();
|
|
this.acceleration = new THREE.Vector3();
|
|
this.id = null;
|
|
this.name = null;
|
|
this.audioListener = new THREE.AudioListener();
|
|
|
|
this.renderer = renderer;
|
|
this.scene = scene;
|
|
|
|
this.rapierWorld = rapierWorld;
|
|
this.rigibody = null;
|
|
this.collider = null;
|
|
|
|
this.moveSpeed = 20;
|
|
this.mouseSensitivity = 0.002;
|
|
this.maxInteractionDistance = 200.0;
|
|
|
|
this.isDrawing = false;
|
|
this.raycast = new THREE.Raycaster();
|
|
this.pointer = new THREE.Vector2();
|
|
|
|
this.itemList = itemList;
|
|
this.currentIntItem = null;
|
|
this.attachedItem = null;
|
|
|
|
this.input = {
|
|
forward: false,
|
|
backwards: false,
|
|
left: false,
|
|
right:false,
|
|
up: false,
|
|
down: false,
|
|
mouseDelta: { x: 0, y: 0}
|
|
};
|
|
|
|
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();
|
|
}
|
|
|
|
_bindEvents() {
|
|
window.addEventListener('keydown', (e) => this._onKeyDown(e));
|
|
window.addEventListener('wheel', (e) => this._onWheel(e));
|
|
}
|
|
|
|
_onWheel(e) {
|
|
if (this.attachedItem && this.attachedItem.content) {
|
|
if (e.deltaY < 0 && typeof this.attachedItem.content.scrollUp === 'function') {
|
|
this.attachedItem.content.scrollUp();
|
|
} else if (e.deltaY > 0 && typeof this.attachedItem.content.scrollDown === 'function') {
|
|
this.attachedItem.content.scrollDown();
|
|
}
|
|
}
|
|
}
|
|
|
|
_onKeyDown(e) {
|
|
if (e.code == 'KeyF' && this.attachedItem) {
|
|
console.log("Dettached item to player: ", this.attachedItem.object.name);
|
|
this.attachedItem.isActive = false;
|
|
this.attachedItem._removeContentDisplay();
|
|
this.attachedItem = null;
|
|
|
|
// Update player's position and rotation to match the camera's current state
|
|
this.position.copy(this.camera.position);
|
|
this.rotation.setFromQuaternion(this.camera.quaternion, 'YXZ');
|
|
|
|
} else if(e.code == 'KeyF' && this.currentIntItem && !this.attachedItem){
|
|
this.attachedItem = this.currentIntItem;
|
|
this.attachedItem.isActive = true;
|
|
//console.log("Attached item to player: ", this.attachedItem.object.name);
|
|
}
|
|
|
|
if (e.code === 'Space' && this.attachedItem) {
|
|
if (this.attachedItem.content instanceof VideoContent) {
|
|
const video = this.attachedItem.content.video;
|
|
if (video.paused) {
|
|
video.play();
|
|
} else {
|
|
video.pause();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (e.code === 'KeyM' && this.attachedItem) {
|
|
if (this.attachedItem.content instanceof VideoContent) {
|
|
const video = this.attachedItem.content.video;
|
|
if (video) {
|
|
video.muted = !video.muted;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_init() {
|
|
// Create rapier rb & coll
|
|
this.position.y = 10;
|
|
|
|
const rbDesc = RigidBodyDesc.kinematicPositionBased().setTranslation(this.position.x, this.position.y, this.position.z);
|
|
this.rigibody = this.rapierWorld.createRigidBody(rbDesc);
|
|
const colliderDesc = ColliderDesc.capsule(7.5, 1);
|
|
this.collider = this.rapierWorld.createCollider(colliderDesc, this.rigibody);
|
|
|
|
// Offset from ground
|
|
this.camera.position.copy(this.position);
|
|
|
|
// Attach audio listener to the camera/player
|
|
this.camera.add(this.audioListener);
|
|
|
|
this.playerRig.add(this.camera);
|
|
}
|
|
|
|
_setupInput() {
|
|
window.addEventListener('keydown', (e) => {
|
|
switch (e.code) {
|
|
case 'KeyW': this.input.forward = true; break;
|
|
case 'KeyS': this.input.backward = true; break;
|
|
case 'KeyA': this.input.left = true; break;
|
|
case 'KeyD': this.input.right = true; break;
|
|
case 'KeyQ': this.input.down = true; break;
|
|
case 'KeyE': this.input.up = true; break;
|
|
}
|
|
});
|
|
window.addEventListener('keyup', (e) => {
|
|
switch (e.code) {
|
|
case 'KeyW': this.input.forward = false; break;
|
|
case 'KeyS': this.input.backward = false; break;
|
|
case 'KeyA': this.input.left = false; break;
|
|
case 'KeyD': this.input.right = false; break;
|
|
case 'KeyQ': this.input.down = false; break;
|
|
case 'KeyE': this.input.up = false; break;
|
|
}
|
|
});
|
|
window.addEventListener('mousemove', (e) => {
|
|
this.input.mouseDelta.x += e.movementX;
|
|
this.input.mouseDelta.y += e.movementY;
|
|
});
|
|
|
|
document.addEventListener('pointerdown', this.onPointerDown.bind(this));
|
|
document.addEventListener('pointerup', this.onPointerUp.bind(this));
|
|
}
|
|
|
|
onPointerDown() {
|
|
if (document.pointerLockElement) {
|
|
this.isDrawing = true;
|
|
}
|
|
}
|
|
|
|
onPointerUp() {
|
|
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;
|
|
const texture = object.material.map;
|
|
const canvas = texture.image;
|
|
const context = canvas.getContext('2d');
|
|
|
|
// --- Dynamic Brush Size Calculation ---
|
|
const worldBrushRadius = 0.1;
|
|
const face = intersect.face;
|
|
const geometry = object.geometry;
|
|
const positionAttribute = geometry.attributes.position;
|
|
const uvAttribute = geometry.attributes.uv;
|
|
|
|
const vA = new THREE.Vector3().fromBufferAttribute(positionAttribute, face.a);
|
|
const vB = new THREE.Vector3().fromBufferAttribute(positionAttribute, face.b);
|
|
const vC = new THREE.Vector3().fromBufferAttribute(positionAttribute, face.c);
|
|
|
|
object.localToWorld(vA);
|
|
object.localToWorld(vB);
|
|
object.localToWorld(vC);
|
|
|
|
const uvA = new THREE.Vector2().fromBufferAttribute(uvAttribute, face.a);
|
|
const uvB = new THREE.Vector2().fromBufferAttribute(uvAttribute, face.b);
|
|
const uvC = new THREE.Vector2().fromBufferAttribute(uvAttribute, face.c);
|
|
|
|
const worldDistAB = vA.distanceTo(vB);
|
|
const uvDistAB = uvA.distanceTo(uvB) * canvas.width;
|
|
const texelsPerWorldUnit = uvDistAB / worldDistAB;
|
|
const pixelBrushRadius = worldBrushRadius * texelsPerWorldUnit;
|
|
// --- End Dynamic Calculation ---
|
|
|
|
const x = uv.x * canvas.width;
|
|
const y = uv.y * canvas.height;
|
|
|
|
context.fillStyle = color;
|
|
context.beginPath();
|
|
context.arc(x, y, Math.max(1, pixelBrushRadius), 0, 2 * Math.PI);
|
|
context.fill();
|
|
|
|
texture.needsUpdate = true;
|
|
}
|
|
|
|
draw(drawableObjects) {
|
|
const meshesToIntersect = drawableObjects.map(obj => obj.mesh);
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_checkForInteractableItems() {
|
|
|
|
const ray = new THREE.Raycaster();
|
|
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);
|
|
|
|
const itemObj = nearbyItems.map(item => item.object);
|
|
|
|
const intersects = ray.intersectObjects(itemObj, true);
|
|
|
|
if (intersects.length > 0) {
|
|
const intersected = intersects[0].object;
|
|
// Find the item whose object contains the intersected mesh
|
|
const foundItem = nearbyItems.find(item => {
|
|
let found = false;
|
|
item.object.traverse(child => {
|
|
if (child === intersected) found = true;
|
|
});
|
|
return found;
|
|
});
|
|
|
|
if (foundItem) {
|
|
this.currentIntItem = foundItem;
|
|
} else {
|
|
this.currentIntItem = null;
|
|
}
|
|
} else {
|
|
this.currentIntItem = null;
|
|
}
|
|
}
|
|
|
|
_lockCameraForAttachedItem() {
|
|
const itemCenter = new THREE.Vector3();
|
|
new THREE.Box3().setFromObject(this.attachedItem.object).getCenter(itemCenter);
|
|
|
|
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(this.camera.quaternion);
|
|
const targetPosition = itemCenter.clone().add(forward.multiplyScalar(2));
|
|
|
|
this.camera.position.lerp(targetPosition, 0.1);
|
|
|
|
const targetRotation = new THREE.Quaternion().setFromRotationMatrix(
|
|
new THREE.Matrix4().lookAt(this.camera.position, itemCenter, this.camera.up)
|
|
);
|
|
|
|
this.camera.quaternion.slerp(targetRotation, 0.1);
|
|
}
|
|
|
|
_updatePlayerMovement(delta) {
|
|
// Normal movement and camera logic
|
|
this.rotation.y -= this.input.mouseDelta.x * this.mouseSensitivity;
|
|
this.rotation.x -= this.input.mouseDelta.y * this.mouseSensitivity;
|
|
this.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.rotation.x));
|
|
|
|
// Only update rotation here. Position will be updated in the main loop after the physics step.
|
|
this.camera.rotation.copy(this.rotation);
|
|
|
|
let direction = new THREE.Vector3();
|
|
if (this.input.forward) direction.z -= 1;
|
|
if (this.input.backward) direction.z += 1;
|
|
if (this.input.left) direction.x -= 1;
|
|
if (this.input.right) direction.x += 1;
|
|
if (this.input.up) direction.y += 1;
|
|
if (this.input.down) direction.y -= 1;
|
|
|
|
direction.normalize();
|
|
const move = new THREE.Vector3(direction.x, direction.y, direction.z);
|
|
move.applyEuler(this.rotation);
|
|
move.multiplyScalar(this.moveSpeed * delta);
|
|
|
|
const newPosition = this.position.clone().add(move);
|
|
|
|
if( newPosition.y <= 10 ) newPosition.y = 10;
|
|
|
|
// Tell the physics engine where we want to go in the next step.
|
|
this.rigibody.setNextKinematicTranslation({ x: newPosition.x, y: newPosition.y, z: newPosition.z });
|
|
}
|
|
|
|
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 if (!this.renderer.xr.isPresenting) { // Only update movement if not in VR
|
|
this._updatePlayerMovement(delta);
|
|
}
|
|
this._checkForInteractableItems();
|
|
}
|
|
|
|
this.input.mouseDelta.x = 0;
|
|
this.input.mouseDelta.y = 0;
|
|
}
|
|
}
|