|
|
@ -12,6 +12,13 @@ export class Player { |
|
|
this.id = null; |
|
|
this.id = null; |
|
|
this.name = null; |
|
|
this.name = null; |
|
|
this.audioListener = new THREE.AudioListener(); |
|
|
this.audioListener = new THREE.AudioListener(); |
|
|
|
|
|
this.attachSound = null; |
|
|
|
|
|
this.detachSound = null; |
|
|
|
|
|
this.hoverSound = null; |
|
|
|
|
|
|
|
|
|
|
|
this.interactPrompt = null; |
|
|
|
|
|
this.interactPromptTime = 0; |
|
|
|
|
|
this.lastPromptItem = null; |
|
|
|
|
|
|
|
|
this.renderer = renderer; |
|
|
this.renderer = renderer; |
|
|
this.scene = scene; |
|
|
this.scene = scene; |
|
|
@ -60,6 +67,7 @@ export class Player { |
|
|
|
|
|
|
|
|
this.socket = socket; |
|
|
this.socket = socket; |
|
|
|
|
|
|
|
|
|
|
|
this._loadSounds(); |
|
|
this._init(); |
|
|
this._init(); |
|
|
this._setupInput(); |
|
|
this._setupInput(); |
|
|
this._bindEvents(); |
|
|
this._bindEvents(); |
|
|
@ -90,10 +98,20 @@ export class Player { |
|
|
// Update player's position and rotation to match the camera's current state
|
|
|
// Update player's position and rotation to match the camera's current state
|
|
|
this.position.copy(this.playerRig.position); |
|
|
this.position.copy(this.playerRig.position); |
|
|
this.rotation.setFromQuaternion(this.playerRig.quaternion, 'YXZ'); |
|
|
this.rotation.setFromQuaternion(this.playerRig.quaternion, 'YXZ'); |
|
|
|
|
|
if (this.detachSound && this.detachSound.isPlaying) this.detachSound.stop(); |
|
|
|
|
|
if (this.detachSound) { |
|
|
|
|
|
this.detachSound.playbackRate = 0.5 + Math.random() * 0.5; |
|
|
|
|
|
this.detachSound.play(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
} else if(e.code == 'KeyF' && this.currentIntItem && !this.attachedItem){ |
|
|
} else if(e.code == 'KeyF' && this.currentIntItem && !this.attachedItem){ |
|
|
this.attachedItem = this.currentIntItem; |
|
|
this.attachedItem = this.currentIntItem; |
|
|
this.attachedItem.isActive = true; |
|
|
this.attachedItem.isActive = true; |
|
|
|
|
|
if (this.attachSound && this.attachSound.isPlaying) this.attachSound.stop(); |
|
|
|
|
|
if (this.attachSound){ |
|
|
|
|
|
this.attachSound.playbackRate = 0.5 + Math.random() * 0.5; |
|
|
|
|
|
this.attachSound.play(); |
|
|
|
|
|
} |
|
|
console.log("Attached item to player: ", this.attachedItem.object.name); |
|
|
console.log("Attached item to player: ", this.attachedItem.object.name); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -136,6 +154,26 @@ export class Player { |
|
|
this.playerRig.add(this.camera); |
|
|
this.playerRig.add(this.camera); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
_loadSounds() { |
|
|
|
|
|
const audioLoader = new THREE.AudioLoader(); |
|
|
|
|
|
this.attachSound = new THREE.Audio(this.audioListener); |
|
|
|
|
|
this.detachSound = new THREE.Audio(this.audioListener); |
|
|
|
|
|
this.hoverSound = new THREE.Audio(this.audioListener); |
|
|
|
|
|
|
|
|
|
|
|
audioLoader.load('sounds/item-int-open.wav', (buffer) => { |
|
|
|
|
|
this.attachSound.setBuffer(buffer); |
|
|
|
|
|
this.attachSound.setVolume(0.05); |
|
|
|
|
|
}); |
|
|
|
|
|
audioLoader.load('sounds/item-int-close.wav', (buffer) => { |
|
|
|
|
|
this.detachSound.setBuffer(buffer); |
|
|
|
|
|
this.detachSound.setVolume(0.05); |
|
|
|
|
|
}); |
|
|
|
|
|
audioLoader.load('sounds/hover-2.wav', (buffer) => { |
|
|
|
|
|
this.hoverSound.setBuffer(buffer); |
|
|
|
|
|
this.hoverSound.setVolume(0.05); |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
_setupInput() { |
|
|
_setupInput() { |
|
|
window.addEventListener('keydown', (e) => { |
|
|
window.addEventListener('keydown', (e) => { |
|
|
switch (e.code) { |
|
|
switch (e.code) { |
|
|
@ -166,6 +204,73 @@ export class Player { |
|
|
document.addEventListener('pointerup', this.onPointerUp.bind(this)); |
|
|
document.addEventListener('pointerup', this.onPointerUp.bind(this)); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ...existing code...
|
|
|
|
|
|
|
|
|
|
|
|
_createInteractPrompt() { |
|
|
|
|
|
function getRandomColor() { |
|
|
|
|
|
const hexChars = '0123456789ABCDEF'; |
|
|
|
|
|
let color = '#'; |
|
|
|
|
|
for (let i = 0; i < 6; i++) { |
|
|
|
|
|
color += hexChars[Math.floor(Math.random() * 16)]; |
|
|
|
|
|
} |
|
|
|
|
|
return color; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const text = '(F) Interact'; |
|
|
|
|
|
const charMeshes = []; |
|
|
|
|
|
const group = new THREE.Group(); |
|
|
|
|
|
const fontSize = 0.25; // Adjust as needed
|
|
|
|
|
|
|
|
|
|
|
|
// First, calculate total width
|
|
|
|
|
|
let totalWidth = 0; |
|
|
|
|
|
const charWidths = []; |
|
|
|
|
|
for (let i = 0; i < text.length; i++) { |
|
|
|
|
|
const charWidth = fontSize * (text[i] === ' ' ? 0.2 : 0.3); //Changes gap size in text
|
|
|
|
|
|
charWidths.push(charWidth); |
|
|
|
|
|
totalWidth += charWidth; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Now, create meshes and position them centered
|
|
|
|
|
|
let offsetX = -totalWidth / 2; |
|
|
|
|
|
for (let i = 0; i < text.length; i++) { |
|
|
|
|
|
const char = text[i]; |
|
|
|
|
|
|
|
|
|
|
|
// Create a canvas for each character
|
|
|
|
|
|
const canvas = document.createElement('canvas'); |
|
|
|
|
|
canvas.width = 128; |
|
|
|
|
|
canvas.height = 128; |
|
|
|
|
|
const ctx = canvas.getContext('2d'); |
|
|
|
|
|
ctx.font = '100px Redacted70'; |
|
|
|
|
|
ctx.textAlign = 'center'; |
|
|
|
|
|
ctx.textBaseline = 'middle'; |
|
|
|
|
|
ctx.fillStyle = getRandomColor(); |
|
|
|
|
|
ctx.fillText(char, canvas.width / 2, canvas.height / 2); |
|
|
|
|
|
|
|
|
|
|
|
const texture = new THREE.CanvasTexture(canvas); |
|
|
|
|
|
texture.minFilter = THREE.LinearFilter; |
|
|
|
|
|
texture.magFilter = THREE.LinearFilter; |
|
|
|
|
|
texture.needsUpdate = true; |
|
|
|
|
|
|
|
|
|
|
|
const geometry = new THREE.PlaneGeometry(fontSize, fontSize); |
|
|
|
|
|
const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true }); |
|
|
|
|
|
const mesh = new THREE.Mesh(geometry, material); |
|
|
|
|
|
|
|
|
|
|
|
mesh.position.x = offsetX + charWidths[i] / 2; |
|
|
|
|
|
group.add(mesh); |
|
|
|
|
|
charMeshes.push(mesh); |
|
|
|
|
|
|
|
|
|
|
|
// --- SNAP ROTATION STATE ---
|
|
|
|
|
|
mesh.userData.snapRotation = 0; |
|
|
|
|
|
mesh.userData.nextSnap = performance.now() + Math.random() * 600 + 300; // 100-700ms
|
|
|
|
|
|
|
|
|
|
|
|
offsetX += charWidths[i]; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
group.userData.charMeshes = charMeshes; // Store for animation
|
|
|
|
|
|
|
|
|
|
|
|
return group; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
onPointerDown() { |
|
|
onPointerDown() { |
|
|
if (document.pointerLockElement) { |
|
|
if (document.pointerLockElement) { |
|
|
this.isDrawing = true; |
|
|
this.isDrawing = true; |
|
|
@ -455,6 +560,7 @@ export class Player { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
draw(drawableObjects) { |
|
|
draw(drawableObjects) { |
|
|
const meshesToIntersect = drawableObjects.map(obj => obj.mesh); |
|
|
const meshesToIntersect = drawableObjects.map(obj => obj.mesh); |
|
|
|
|
|
|
|
|
@ -533,6 +639,11 @@ export class Player { |
|
|
if (this.currentIntItem !== foundItem) { |
|
|
if (this.currentIntItem !== foundItem) { |
|
|
// Optional: Add some visual feedback for the newly highlighted item
|
|
|
// Optional: Add some visual feedback for the newly highlighted item
|
|
|
console.log("Hovering over:", foundItem.object.name); |
|
|
console.log("Hovering over:", foundItem.object.name); |
|
|
|
|
|
if (this.hoverSound && this.hoverSound.isPlaying) this.hoverSound.stop(); |
|
|
|
|
|
if (this.hoverSound) { |
|
|
|
|
|
this.hoverSound.playbackRate = 0.9 + Math.random() * 0.1; |
|
|
|
|
|
this.hoverSound.play(); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
this.currentIntItem = foundItem; |
|
|
this.currentIntItem = foundItem; |
|
|
} else { |
|
|
} else { |
|
|
@ -583,14 +694,21 @@ export class Player { |
|
|
if (this.input.backward) direction.z += 1; |
|
|
if (this.input.backward) direction.z += 1; |
|
|
if (this.input.left) direction.x -= 1; |
|
|
if (this.input.left) direction.x -= 1; |
|
|
if (this.input.right) 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(); |
|
|
// Only apply yaw (Y axis) rotation for WASD movement
|
|
|
const move = new THREE.Vector3(direction.x, direction.y, direction.z); |
|
|
const yaw = this.rotation.y; |
|
|
move.applyEuler(this.rotation); |
|
|
const move = new THREE.Vector3(direction.x, 0, direction.z) |
|
|
move.multiplyScalar(this.moveSpeed * delta); |
|
|
.applyAxisAngle(new THREE.Vector3(0, 1, 0), yaw) |
|
|
|
|
|
.normalize(); |
|
|
|
|
|
|
|
|
|
|
|
// Add vertical movement from Q/E
|
|
|
|
|
|
let vertical = 0; |
|
|
|
|
|
if (this.input.up) vertical += 1; |
|
|
|
|
|
if (this.input.down) vertical -= 1; |
|
|
|
|
|
|
|
|
|
|
|
move.y = vertical; |
|
|
|
|
|
|
|
|
|
|
|
move.multiplyScalar(this.moveSpeed * delta); |
|
|
// Updating the position of the RB based on the position calcuted by Rapier
|
|
|
// Updating the position of the RB based on the position calcuted by Rapier
|
|
|
const newPosition = this.position.clone().add(move); |
|
|
const newPosition = this.position.clone().add(move); |
|
|
|
|
|
|
|
|
@ -617,6 +735,66 @@ export class Player { |
|
|
|
|
|
|
|
|
this._checkForInteractableItems(); |
|
|
this._checkForInteractableItems(); |
|
|
|
|
|
|
|
|
|
|
|
// --- Interact Prompt Logic ---
|
|
|
|
|
|
if (!this.interactPrompt) { |
|
|
|
|
|
this.interactPrompt = this._createInteractPrompt(); |
|
|
|
|
|
this.camera.add(this.interactPrompt); |
|
|
|
|
|
this.interactPrompt.position.set(0, -0.5, -2); |
|
|
|
|
|
this.interactPrompt.visible = false; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Show prompt if hovering over item and not holding one
|
|
|
|
|
|
if (this.currentIntItem && !this.attachedItem) { |
|
|
|
|
|
// Regenerate prompt if hovered item changed
|
|
|
|
|
|
if (this.lastPromptItem !== this.currentIntItem) { |
|
|
|
|
|
// Remove old prompt mesh
|
|
|
|
|
|
this.camera.remove(this.interactPrompt); |
|
|
|
|
|
|
|
|
|
|
|
// Dispose all character meshes in the group
|
|
|
|
|
|
if (this.interactPrompt.userData.charMeshes) { |
|
|
|
|
|
this.interactPrompt.userData.charMeshes.forEach(mesh => { |
|
|
|
|
|
if (mesh.geometry) mesh.geometry.dispose(); |
|
|
|
|
|
if (mesh.material) { |
|
|
|
|
|
if (mesh.material.map) mesh.material.map.dispose(); |
|
|
|
|
|
mesh.material.dispose(); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Create new prompt with new random colors
|
|
|
|
|
|
this.interactPrompt = this._createInteractPrompt(); |
|
|
|
|
|
this.camera.add(this.interactPrompt); |
|
|
|
|
|
this.interactPrompt.position.set(0, -0.5, -2); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.interactPrompt.visible = true; |
|
|
|
|
|
this.interactPromptTime += delta; |
|
|
|
|
|
const bob = Math.sin(this.interactPromptTime * 2) * 0.01; |
|
|
|
|
|
this.interactPrompt.position.y = -0.5 + bob; |
|
|
|
|
|
this.lastPromptItem = this.currentIntItem; |
|
|
|
|
|
} else { |
|
|
|
|
|
this.interactPrompt.visible = false; |
|
|
|
|
|
this.interactPromptTime = 0; |
|
|
|
|
|
this.lastPromptItem = null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (this.interactPrompt && this.interactPrompt.userData.charMeshes) { |
|
|
|
|
|
const now = performance.now(); |
|
|
|
|
|
this.interactPrompt.userData.charMeshes.forEach((mesh, i) => { |
|
|
|
|
|
// Don't animate spaces
|
|
|
|
|
|
if (mesh.material.map.image) { |
|
|
|
|
|
const ctx = mesh.material.map.image.getContext && mesh.material.map.image.getContext('2d'); |
|
|
|
|
|
if (ctx && ctx.measureText && ctx.measureText(' ').width === 0) return; |
|
|
|
|
|
} |
|
|
|
|
|
// Snap rotation at random intervals
|
|
|
|
|
|
if (now > mesh.userData.nextSnap) { |
|
|
|
|
|
mesh.userData.snapRotation = (Math.random() * 40 - 20) * Math.PI / 180; // -20 to +20 deg in radians
|
|
|
|
|
|
mesh.userData.nextSnap = now + Math.random() * 600 + 300; // 100-700ms
|
|
|
|
|
|
} |
|
|
|
|
|
mesh.rotation.z = mesh.userData.snapRotation; |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
this.input.mouseDelta.x = 0; |
|
|
this.input.mouseDelta.x = 0; |
|
|
this.input.mouseDelta.y = 0; |
|
|
this.input.mouseDelta.y = 0; |
|
|
} |
|
|
} |
|
|
|