Browse Source

player footsteps, player movement wasd, bbox fix, item interact gui, sfx for interaction

master
Cailean Finn 3 weeks ago
parent
commit
6e341306dc
  1. 2
      data/drawings.json
  2. 8
      js/Content.js
  3. 15
      js/Item.js
  4. 13
      js/ModelLoader.js
  5. 190
      js/Player.js
  6. 85
      js/main.js
  7. BIN
      public/sounds/hover-2.wav
  8. BIN
      public/sounds/hover.wav
  9. BIN
      public/sounds/item-int-close-2.wav
  10. BIN
      public/sounds/item-int-close.wav
  11. BIN
      public/sounds/item-int-open-2.wav
  12. BIN
      public/sounds/item-int-open.wav

2
data/drawings.json

File diff suppressed because one or more lines are too long

8
js/Content.js

@ -164,10 +164,10 @@ export class ImageContent extends Content {
(texture) => {
this.texture = texture;
const aspect = texture.image.width / texture.image.height;
const height = 5;
const height = 4;
const width = height * aspect;
const geometry = new THREE.PlaneGeometry(width, height);
const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true });
const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true, opacity: 0.8 });
this.displayMesh = new THREE.Mesh(geometry, material);
resolve(this.displayMesh);
},
@ -204,7 +204,7 @@ export class VideoContent extends Content {
return new Promise((resolve, reject) => {
this.video.addEventListener('loadedmetadata', () => {
const aspect = this.video.videoWidth / this.video.videoHeight;
const height = 5; // or any desired height
const height = 4; // or any desired height
const width = height * aspect;
this.texture = new THREE.VideoTexture(this.video);
@ -213,7 +213,7 @@ export class VideoContent extends Content {
this.texture.format = THREE.RGBAFormat;
const geometry = new THREE.PlaneGeometry(width, height);
const material = new THREE.MeshBasicMaterial({ map: this.texture, side: THREE.DoubleSide });
const material = new THREE.MeshBasicMaterial({ map: this.texture, side: THREE.DoubleSide, transparent: true, opacity: 0.8 });
this.displayMesh = new THREE.Mesh(geometry, material);
this.video.play();

15
js/Item.js

@ -66,6 +66,8 @@ export class Item {
this.spawnedObjects.push( { mesh: child, body: null} );
}
});
this.spawnRotation.copy(this.object.rotation);
this._addObject();
@ -92,6 +94,14 @@ export class Item {
}
rbDesc.setTranslation(this.object.position.x, this.object.position.y, this.object.position.z);
rbDesc.setRotation({
x: this.object.quaternion.x,
y: this.object.quaternion.y,
z: this.object.quaternion.z,
w: this.object.quaternion.w
});
this.rb = this.rapierWorld.createRigidBody(rbDesc);
const colDesc = ColliderDesc.cuboid(size.x / 2, size.y / 2, size.z / 2)
@ -145,6 +155,7 @@ export class Item {
} else {
this.object.position.copy(this.spawnPosition);
}
this.object.rotation.copy(this.spawnRotation);
this.scene.add(this.object);
}
}
@ -181,12 +192,12 @@ export class Item {
new THREE.Vector3(-w, h, 0)
];
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ linewidth: 10, color: 'red' });
const material = new THREE.LineBasicMaterial({ linewidth: 2, color: 'red' });
this.debugBox = new THREE.LineLoop(geometry, material);
// 4. Position and orient the new box
this.debugBox.position.copy(center);
this.debugBox.quaternion.copy(this.player.camera.quaternion);
this.debugBox.quaternion.copy(this.player.playerRig.quaternion);
this.scene.add(this.debugBox);
}
}

13
js/ModelLoader.js

@ -91,6 +91,8 @@ export class ModelLoader {
const subgroups = [...dynamicGroup.children];
subgroups.forEach(subgroup => {
if (subgroup.isGroup || subgroup.isObject3D) { // Treat empties as groups
subgroup.scale.multiplyScalar(worldScale);
const groupData = {
name: subgroup.name,
objects: []
@ -100,12 +102,17 @@ export class ModelLoader {
const objects = [...subgroup.children];
objects.forEach(object => {
if (object.isMesh) {
// 1. Apply world scale
object.position.multiplyScalar(worldScale);
object.scale.multiplyScalar(worldScale);
// --- Preserve rotation before re-parenting ---
const savedQuaternion = object.quaternion.clone();
console.log(savedQuaternion);
// 2. Detach from original parent and apply material
gltf.scene.attach(object);
// --- Restore rotation after re-parenting ---
object.quaternion.copy(savedQuaternion);
this._applyCustomMaterial(object);
groupData.objects.push(object);

190
js/Player.js

@ -12,6 +12,13 @@ export class Player {
this.id = null;
this.name = null;
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.scene = scene;
@ -60,6 +67,7 @@ export class Player {
this.socket = socket;
this._loadSounds();
this._init();
this._setupInput();
this._bindEvents();
@ -90,10 +98,20 @@ export class Player {
// Update player's position and rotation to match the camera's current state
this.position.copy(this.playerRig.position);
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){
this.attachedItem = this.currentIntItem;
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);
}
@ -136,6 +154,26 @@ export class Player {
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() {
window.addEventListener('keydown', (e) => {
switch (e.code) {
@ -166,6 +204,73 @@ export class Player {
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() {
if (document.pointerLockElement) {
this.isDrawing = true;
@ -454,6 +559,7 @@ export class Player {
});
}
}
draw(drawableObjects) {
const meshesToIntersect = drawableObjects.map(obj => obj.mesh);
@ -533,6 +639,11 @@ export class Player {
if (this.currentIntItem !== foundItem) {
// Optional: Add some visual feedback for the newly highlighted item
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;
} else {
@ -583,14 +694,21 @@ export class Player {
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);
// Only apply yaw (Y axis) rotation for WASD movement
const yaw = this.rotation.y;
const move = new THREE.Vector3(direction.x, 0, direction.z)
.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
const newPosition = this.position.clone().add(move);
@ -617,6 +735,66 @@ export class Player {
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.y = 0;
}

85
js/main.js

@ -44,6 +44,12 @@ const vertex = new THREE.Vector3();
const color = new THREE.Color();
const stats = new Stats();
const footstepMeshes = [];
const footsteps = {};
const footStepInterval = 2.0;
const footstepFade = 5000; //ms
init();
async function init() {
@ -165,6 +171,19 @@ async function animate() {
player.draw(spawnedObjects);
// (A) Update footsteps for main player
updateFootstepsForPlayer(socket.id, player.playerRig.position);
// (B) Update footsteps for other players
for (const [id, obj] of Object.entries(currentPlayers)) {
if (obj.mesh) {
updateFootstepsForPlayer(id, obj.mesh.position);
}
}
// (C) Draw and fade footsteps
drawAndFadeFootsteps();
sendPlayerDataToServer();
renderer.render(scene, player.camera); // No post-processing in XR
@ -353,7 +372,7 @@ function applyDrawings(drawings) {
function addOtherPlayer(playerData) {
const geometry = new THREE.BoxGeometry(1, 2, 1);
const material = new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff });
const material = new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff, transparent: true, opacity: 0 });
const playerMesh = new THREE.Mesh(geometry, material);
playerMesh.position.set(playerData.position.x, playerData.position.y, playerData.position.z);
scene.add(playerMesh);
@ -510,4 +529,68 @@ async function loadWorldModel(modelUrl) {
} catch (error) {
console.error("Failed to load world:", error);
}
}
function updateFootstepsForPlayer(playerId, playerPosition) {
if (!footsteps[playerId]) footsteps[playerId] = [];
const steps = footsteps[playerId];
const last = steps[steps.length - 1];
if (!last || playerPosition.distanceTo(new THREE.Vector3(last.x, playerPosition.y, last.z)) > footStepInterval) {
// Determine step index for left/right alternation
const stepIndex = steps.length;
const offsetAmount = (Math.random() - 0.5) * 2; // Adjust for wider/narrower steps
// Determine facing direction (Y rotation)
let rotationY = 0;
if (playerId === socket.id) {
rotationY = player.playerRig.rotation.y;
} else if (currentPlayers[playerId] && currentPlayers[playerId].mesh) {
rotationY = currentPlayers[playerId].mesh.rotation.y;
}
// Calculate left/right offset vector
const direction = new THREE.Vector3(Math.sin(rotationY), 0, Math.cos(rotationY));
const left = new THREE.Vector3().crossVectors(direction, new THREE.Vector3(0, 1, 0)).normalize();
const offset = left.multiplyScalar((stepIndex % 2 === 0 ? 1 : -1) * offsetAmount);
// Create a flat circle mesh for the footstep
const geometry = new THREE.CircleGeometry(0.3, 16);
const material = new THREE.MeshBasicMaterial({
color: 0xFFFFFF,
transparent: true,
opacity: 1
});
const footstep = new THREE.Mesh(geometry, material);
footstep.rotation.x = -Math.PI / 2;
footstep.position.set(
playerPosition.x + offset.x,
1.05,
playerPosition.z + offset.z
);
footstep.userData = { time: performance.now() };
scene.add(footstep);
footstepMeshes.push(footstep);
steps.push({
x: playerPosition.x,
z: playerPosition.z,
time: performance.now()
});
}
}
function drawAndFadeFootsteps() {
const now = performance.now();
for (let i = footstepMeshes.length - 1; i >= 0; i--) {
const mesh = footstepMeshes[i];
const age = now - mesh.userData.time;
const alpha = 1 - age / footstepFade;
mesh.material.opacity = Math.max(0, alpha);
if (age > footstepFade) {
scene.remove(mesh);
mesh.geometry.dispose();
mesh.material.dispose();
footstepMeshes.splice(i, 1);
}
}
}

BIN
public/sounds/hover-2.wav

Binary file not shown.

BIN
public/sounds/hover.wav

Binary file not shown.

BIN
public/sounds/item-int-close-2.wav

Binary file not shown.

BIN
public/sounds/item-int-close.wav

Binary file not shown.

BIN
public/sounds/item-int-open-2.wav

Binary file not shown.

BIN
public/sounds/item-int-open.wav

Binary file not shown.
Loading…
Cancel
Save