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.
 
 
 

311 lines
11 KiB

import * as THREE from 'three';
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) {
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.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;
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);
}
_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;
}
_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) {
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);
}
}
}
_checkForInteractableItems() {
const ray = new THREE.Raycaster();
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) {
if (this.enableInput) {
if (this.attachedItem) {
this._lockCameraForAttachedItem();
} else {
this._updatePlayerMovement(delta);
}
this._checkForInteractableItems();
}
this.input.mouseDelta.x = 0;
this.input.mouseDelta.y = 0;
}
}