@ -1,12 +1,13 @@
import * as THREE from 'three' ;
import * as THREE from 'three' ;
import { RigidBodyDesc , ColliderDesc } from '@dimforge/rapier3d-compat' ;
import { RigidBodyDesc , ColliderDesc } from '@dimforge/rapier3d-compat' ;
import { AudioContent , VideoContent } from './Content' ;
import { AudioContent , VideoContent } from './Content' ;
import { ModelLoader } from './ModelLoader' ;
export class Player {
export class Player {
constructor ( rapierWorld , renderer , scene , spawnPosition = new THREE . Vector3 ( 0 , 1 , 0 ) , itemList , socket ) {
constructor ( rapierWorld , renderer , scene , spawnPosition = new THREE . Vector3 ( 0 , 1 , 0 ) , itemList , socket ) {
this . camera = new THREE . PerspectiveCamera ( 75 , window . innerWidth / window . innerHeight , 0.1 , 1000 ) ;
this . camera = new THREE . PerspectiveCamera ( 75 , window . innerWidth / window . innerHeight , 0.1 , 1000 ) ;
this . position = spawnPosition . clone ( ) ;
this . position = spawnPosition . clone ( ) ;
this . rotation = new THREE . Euler ( 0 , 0 , 0 , 'YXZ' ) ;
this . rotation = new THREE . Euler ( 0 , 89.6 , 0 , 'YXZ' ) ;
this . velocity = new THREE . Vector3 ( ) ;
this . velocity = new THREE . Vector3 ( ) ;
this . acceleration = new THREE . Vector3 ( ) ;
this . acceleration = new THREE . Vector3 ( ) ;
this . id = null ;
this . id = null ;
@ -16,6 +17,35 @@ export class Player {
this . detachSound = null ;
this . detachSound = null ;
this . hoverSound = null ;
this . hoverSound = null ;
this . collectedItems = [ ] ;
this . numCollectedItems = [ ] ;
this . areaList = [ ] ;
this . currentArea = null ;
this . areasVisited = 0 ;
this . itemCountDisplay = null ;
this . itemCountAnim = {
active : false ,
time : 0 ,
duration : 1.2 , // seconds
fadeIn : 0.3 ,
fadeOut : 0.5 ,
startY : - 1 ,
endY : - 0.7
} ;
this . sprayCan = null ;
this . hand = null ;
this . currentSprayColor = '#FF0000' ;
this . modelLoader = new ModelLoader ( ) ;
this . shakeData = {
lastPosition : new THREE . Vector3 ( ) ,
lastVelocity : new THREE . Vector3 ( ) ,
lastShakeTime : 0 ,
shakeThreshold : 1000 , // Acceleration magnitude needed to trigger
shakeCooldown : 0.5 // Seconds between shakes
} ;
this . interactPrompt = null ;
this . interactPrompt = null ;
this . interactPromptTime = 0 ;
this . interactPromptTime = 0 ;
this . lastPromptItem = null ;
this . lastPromptItem = null ;
@ -27,7 +57,7 @@ export class Player {
this . rigibody = null ;
this . rigibody = null ;
this . collider = null ;
this . collider = null ;
this . moveSpeed = 40 ;
this . moveSpeed = 100 ; //40
this . mouseSensitivity = 0.002 ;
this . mouseSensitivity = 0.002 ;
this . maxInteractionDistance = 200.0 ;
this . maxInteractionDistance = 200.0 ;
@ -112,6 +142,26 @@ export class Player {
this . attachSound . playbackRate = 0.5 + Math . random ( ) * 0.5 ;
this . attachSound . playbackRate = 0.5 + Math . random ( ) * 0.5 ;
this . attachSound . play ( ) ;
this . attachSound . play ( ) ;
}
}
if ( ! this . collectedItems . includes ( this . attachedItem . name ) ) {
this . collectedItems . push ( this . attachedItem . name ) ;
this . numCollectedItems ++ ;
console . log ( "Number of items collected: " , this . numCollectedItems ) ;
// Update item count display
this . camera . remove ( this . itemCountDisplay ) ;
this . itemCountDisplay = this . _ createItemCountDisplay ( ) ;
this . camera . add ( this . itemCountDisplay ) ;
this . itemCountAnim . active = true ;
this . itemCountAnim . time = 0 ;
this . itemCountDisplay . position . set ( 0 , this . itemCountAnim . startY , - 2 ) ;
this . itemCountDisplay . visible = true ;
// Set initial opacity
this . itemCountDisplay . traverse ( obj => {
if ( obj . material ) obj . material . opacity = 0 ;
} ) ;
}
console . log ( "Attached item to player: " , this . attachedItem . object . name ) ;
console . log ( "Attached item to player: " , this . attachedItem . object . name ) ;
}
}
@ -140,13 +190,21 @@ export class Player {
// Create rapier rb & coll
// Create rapier rb & coll
this . position . y = 10 ;
this . position . y = 10 ;
const tempPos = new THREE . Vector3 ( 0 , 20 , 0 ) ;
const rbDesc = RigidBodyDesc . kinematicPositionBased ( ) . setTranslation ( this . position . x , this . position . y , this . position . z ) ;
const rbDesc = RigidBodyDesc . kinematicPositionBased ( ) . setTranslation ( this . position . x , this . position . y , this . position . z ) ;
this . rigibody = this . rapierWorld . createRigidBody ( rbDesc ) ;
this . rigibody = this . rapierWorld . createRigidBody ( rbDesc ) ;
const colliderDesc = ColliderDesc . capsule ( 7.5 , 1 ) ;
const colliderDesc = ColliderDesc . capsule ( 3 , 4 ) ;
this . collider = this . rapierWorld . createCollider ( colliderDesc , this . rigibody ) ;
this . collider = this . rapierWorld . createCollider ( colliderDesc , this . rigibody ) ;
// Initialize Character Controller
// offset: small gap to prevent snagging (0.1)
this . characterController = this . rapierWorld . createCharacterController ( 0.01 ) ;
//this.characterController.enableAutostep(1, 1, true); // Handle small steps/stairs
this . characterController . setApplyImpulsesToDynamicBodies ( true ) ; // Allow pushing dynamic objects
// Offset from ground
// Offset from ground
this . camera . position . copy ( this . position ) ;
this . camera . position . copy ( tem pP os) ;
// Attach audio listener to the camera/player
// Attach audio listener to the camera/player
this . camera . add ( this . audioListener ) ;
this . camera . add ( this . audioListener ) ;
@ -154,6 +212,18 @@ export class Player {
this . playerRig . add ( this . camera ) ;
this . playerRig . add ( this . camera ) ;
}
}
_ loadSprayCan ( modelUrl , scale ) {
return this . modelLoader . loadModel ( modelUrl , scale )
. then ( ( model ) => {
console . log ( 'loaded spraycan' ) ;
return model ;
} )
. catch ( ( err ) => {
console . error ( 'Failed to load spray can model:' , err ) ;
return null ;
} ) ;
}
_ loadSounds ( ) {
_ loadSounds ( ) {
const audioLoader = new THREE . AudioLoader ( ) ;
const audioLoader = new THREE . AudioLoader ( ) ;
this . attachSound = new THREE . Audio ( this . audioListener ) ;
this . attachSound = new THREE . Audio ( this . audioListener ) ;
@ -174,6 +244,47 @@ export class Player {
} ) ;
} ) ;
}
}
_ initAreaTriggers ( ) {
this . areaList = [ ] ;
const testLayers = new THREE . Layers ( ) ;
testLayers . set ( 4 ) ;
this . scene . traverse ( ( object ) => {
// Find meshes on the AREA_LAYER
if ( object . isMesh && object . layers . test ( testLayers ) ) {
this . areaList . push ( object ) ;
object . visible = false ; // Optionally make trigger volumes invisible
}
} ) ;
console . log ( ` Initialized ${ this . areaList . length } area triggers. ` ) ;
}
_ checkAreaTriggers ( ) {
if ( this . areaList . length == 0 ) return ;
let inAnyArea = false ;
for ( const trigger of this . areaList ) {
const triggerBox = new THREE . Box3 ( ) . setFromObject ( trigger ) ;
if ( triggerBox . containsPoint ( this . position ) ) {
inAnyArea = true ;
if ( this . currentArea !== trigger ) {
this . currentArea = trigger ;
console . log ( ` Player entered area: ${ trigger . name } ` ) ;
this . areasVisited ++ ;
}
break ;
}
}
// Check if the player has left an area
if ( ! inAnyArea && this . currentArea !== null ) {
console . log ( ` Player left area: ${ this . currentArea . name } ` ) ;
this . currentArea = null ;
}
}
_ setupInput ( ) {
_ setupInput ( ) {
window . addEventListener ( 'keydown' , ( e ) => {
window . addEventListener ( 'keydown' , ( e ) => {
switch ( e . code ) {
switch ( e . code ) {
@ -204,7 +315,47 @@ export class Player {
document . addEventListener ( 'pointerup' , this . onPointerUp . bind ( this ) ) ;
document . addEventListener ( 'pointerup' , this . onPointerUp . bind ( this ) ) ;
}
}
// ...existing code...
_ createItemCountDisplay ( ) {
const text = ` ${ this . numCollectedItems } / 50 ` ;
const group = new THREE . Group ( ) ;
const fontSize = 0.25 ;
let totalWidth = 0 ;
const charWidths = [ ] ;
for ( let i = 0 ; i < text . length ; i ++ ) {
const charWidth = fontSize * ( text [ i ] === ' ' ? 0.3 : 0.4 ) ;
charWidths . push ( charWidth ) ;
totalWidth += charWidth ;
}
let offsetX = - totalWidth / 2 ;
for ( let i = 0 ; i < text . length ; i ++ ) {
const char = text [ i ] ;
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 = '#ffd500ff' ; // Consistent color
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 ) ;
offsetX += charWidths [ i ] ;
}
group . name = 'itemCountDisplay' ;
return group ;
}
_ createInteractPrompt ( ) {
_ createInteractPrompt ( ) {
function getRandomColor ( ) {
function getRandomColor ( ) {
@ -281,7 +432,7 @@ _createInteractPrompt() {
this . isDrawing = false ;
this . isDrawing = false ;
}
}
_ setupVR ( ) {
async _ setupVR ( ) {
const markerGeometry = new THREE . CircleGeometry ( 0.5 , 32 ) ;
const markerGeometry = new THREE . CircleGeometry ( 0.5 , 32 ) ;
markerGeometry . rotateX ( - Math . PI / 2 ) ;
markerGeometry . rotateX ( - Math . PI / 2 ) ;
const markerMat = new THREE . MeshBasicMaterial ( { color : 0x00ff00 , transparent : false , opacity : 0.5 } ) ;
const markerMat = new THREE . MeshBasicMaterial ( { color : 0x00ff00 , transparent : false , opacity : 0.5 } ) ;
@ -316,18 +467,29 @@ _createInteractPrompt() {
} ) ;
} ) ;
// Add a debug sphere to the controller
// Add a debug sphere to the controller
const sphereGeometry = new THREE . SphereGeometry ( 0.05 , 8 , 8 ) ;
// 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 sphereMaterial = new THREE.MeshBasicMaterial({ color: (i === 0 ? 0xff0000 : 0x0000ff) }); // Red for left, Blue for right
const debugSphere = new THREE . Mesh ( sphereGeometry , sphereMaterial ) ;
// const debugSphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
controller . add ( debugSphere ) ;
//controller.add(debugSphere);
const lineGeometry = new THREE . BufferGeometry ( ) . setFromPoints ( [ new THREE . Vector3 ( 0 , 0 , 0 ) , new THREE . Vector3 ( 0 , 0 , - 1 ) ] ) ;
const lineGeometry = new THREE . BufferGeometry ( ) . setFromPoints ( [ new THREE . Vector3 ( 0 , 0 , 0 ) , new THREE . Vector3 ( 0 , 0 , - 1 ) ] ) ;
const lineMaterial = new THREE . LineBasicMaterial ( { color : 0xff0000 } ) ; // White color for the ray
const lineMaterial = new THREE . LineBasicMaterial ( { color : 0xff0000 , opacity : 0.4 , transparent : true } ) ; // White color for the ray
const line = new THREE . Line ( lineGeometry , lineMaterial ) ;
const line = new THREE . Line ( lineGeometry , lineMaterial ) ;
line . name = 'controller-ray' ; // Give the line a name to find it later
line . name = 'controller-ray' ; // Give the line a name to find it later
line . scale . z = this . maxInteractionDistance ;
line . scale . z = this . maxInteractionDistance ;
controller . add ( line ) ;
;
if ( i == 0 ) {
this . _ loadSprayCan ( 'models/spray-can.glb' , 0.04 ) . then ( ( model ) => {
if ( model instanceof THREE . Object3D ) {
controller . add ( model ) ;
this . sprayCan = model ;
controller . add ( line )
} else {
console . warn ( 'Spray can model is not a THREE.Object3D:' , model ) ;
}
} ) ;
}
controller . addEventListener ( 'selectstart' , ( ) => this . _ OnVRSelectStart ( i ) ) ;
controller . addEventListener ( 'selectstart' , ( ) => this . _ OnVRSelectStart ( i ) ) ;
controller . addEventListener ( 'selectend' , ( ) => this . _ OnVRSelectEnd ( i ) ) ;
controller . addEventListener ( 'selectend' , ( ) => this . _ OnVRSelectEnd ( i ) ) ;
@ -439,7 +601,7 @@ _createInteractPrompt() {
}
}
}
}
_ handleVRTeleport ( floorObjects ) {
_ handleVRTeleport ( ) {
if ( ! this . teleporting ) {
if ( ! this . teleporting ) {
this . teleMarker . visible = false ;
this . teleMarker . visible = false ;
this . teleArc . visible = false ;
this . teleArc . visible = false ;
@ -449,7 +611,7 @@ _createInteractPrompt() {
const controller = this . vrControllers [ 1 ] ; // Left controller for teleporting
const controller = this . vrControllers [ 1 ] ; // Left controller for teleporting
const controllerMatrix = controller . matrixWorld ;
const controllerMatrix = controller . matrixWorld ;
const initialVelocity = 50 * this . teleportDistanceFactor ; ;
const initialVelocity = 50 * this . teleportDistanceFactor ;
const gravity = - 9.8 ;
const gravity = - 9.8 ;
const timeStep = 0.03 ;
const timeStep = 0.03 ;
const numSegments = 100 ;
const numSegments = 100 ;
@ -472,9 +634,11 @@ _createInteractPrompt() {
const ray = new THREE . Raycaster ( lastPoint , currentPoint . clone ( ) . sub ( lastPoint ) . normalize ( ) ) ;
const ray = new THREE . Raycaster ( lastPoint , currentPoint . clone ( ) . sub ( lastPoint ) . normalize ( ) ) ;
ray . far = lastPoint . distanceTo ( currentPoint ) ;
ray . far = lastPoint . distanceTo ( currentPoint ) ;
const floorMeshes = floorObjects . map ( obj => obj . mesh ) ;
ray . layers . set ( 3 ) ; // TP_SURFACE Layer
const intersects = ray . intersectObjects ( floorMeshes ) ;
// Intersect the whole scene, but only objects on the TP_OBJECT_LAYER will be hit
const intersects = ray . intersectObjects ( this . scene . children , true ) ;
console . log ( intersects . length ) ;
if ( intersects . length > 0 ) {
if ( intersects . length > 0 ) {
const intersectPoint = intersects [ 0 ] . point ;
const intersectPoint = intersects [ 0 ] . point ;
points . push ( intersectPoint ) ;
points . push ( intersectPoint ) ;
@ -502,6 +666,51 @@ _createInteractPrompt() {
this . teleArc . visible = true ;
this . teleArc . visible = true ;
}
}
_ handleControllerShake ( delta ) {
if ( ! this . renderer . xr . isPresenting || ! this . vrControllers [ 0 ] || delta === 0 ) {
return ;
}
const controller = this . vrControllers [ 0 ] ;
const now = performance . now ( ) / 1000 ;
// Check if cooldown has passed
if ( now - this . shakeData . lastShakeTime < this . shakeData . shakeCooldown ) {
// Update position even during cooldown to avoid a spike when it ends
this . shakeData . lastPosition . copy ( controller . position ) ;
this . shakeData . lastVelocity . set ( 0 , 0 , 0 ) ;
return ;
}
const currentPosition = controller . position . clone ( ) ;
// Velocity = (current_pos - last_pos) / time
const currentVelocity = currentPosition . clone ( ) . sub ( this . shakeData . lastPosition ) . divideScalar ( delta ) ;
// Acceleration = (current_vel - last_vel) / time
const acceleration = currentVelocity . clone ( ) . sub ( this . shakeData . lastVelocity ) . divideScalar ( delta ) ;
// Check if acceleration exceeds the threshold
if ( acceleration . length ( ) > this . shakeData . shakeThreshold ) {
// Shake detected!
this . shakeData . lastShakeTime = now ;
// Change to a new random color
const r = Math . floor ( Math . random ( ) * 256 ) . toString ( 16 ) . padStart ( 2 , '0' ) ;
const g = Math . floor ( Math . random ( ) * 256 ) . toString ( 16 ) . padStart ( 2 , '0' ) ;
const b = Math . floor ( Math . random ( ) * 256 ) . toString ( 16 ) . padStart ( 2 , '0' ) ;
this . currentSprayColor = ` # ${ r } ${ g } ${ b } ` ;
console . log ( ` Shake detected! New color: ${ this . currentSprayColor } ` ) ;
// Optional: Play a sound effect here
}
// Update values for the next frame
this . shakeData . lastPosition . copy ( currentPosition ) ;
this . shakeData . lastVelocity . copy ( currentVelocity ) ;
}
_ drawOnTexture ( intersect , color = 'red' ) {
_ drawOnTexture ( intersect , color = 'red' ) {
const object = intersect . object ;
const object = intersect . object ;
const objectId = object . userData . drawingId ;
const objectId = object . userData . drawingId ;
@ -511,7 +720,7 @@ _createInteractPrompt() {
const context = canvas . getContext ( '2d' ) ;
const context = canvas . getContext ( '2d' ) ;
// --- Dynamic Brush Size Calculation ---
// --- Dynamic Brush Size Calculation ---
const worldBrushRadius = 0. 1;
const worldBrushRadius = 1 ;
const face = intersect . face ;
const face = intersect . face ;
const geometry = object . geometry ;
const geometry = object . geometry ;
const positionAttribute = geometry . attributes . position ;
const positionAttribute = geometry . attributes . position ;
@ -538,10 +747,20 @@ _createInteractPrompt() {
const x = uv . x * canvas . width ;
const x = uv . x * canvas . width ;
const y = uv . y * canvas . height ;
const y = uv . y * canvas . height ;
context . fillStyle = color ;
const gradient = context . createRadialGradient (
x , y , 0 ,
x , y , Math . max ( 4 , pixelBrushRadius * 2 )
) ;
gradient . addColorStop ( 0 , color ) ;
gradient . addColorStop ( 1 , 'rgba(0,0,0,0)' ) ;
context . globalCompositeOperation = 'source-over' ;
context . globalAlpha = 0.6 ; // tweak softness
context . fillStyle = gradient ;
context . beginPath ( ) ;
context . beginPath ( ) ;
context . arc ( x , y , Math . max ( 1 , pixelBrushRadius ) , 0 , 2 * Math . PI ) ;
context . arc ( x , y , Math . max ( 1 , pixelBrushRadius ) , 0 , 2 * Math . PI ) ;
context . fill ( ) ;
context . fill ( ) ;
context . globalAlpha = 0.6 ;
texture . needsUpdate = true ;
texture . needsUpdate = true ;
@ -560,7 +779,6 @@ _createInteractPrompt() {
}
}
}
}
draw ( drawableObjects ) {
draw ( drawableObjects ) {
const meshesToIntersect = drawableObjects . map ( obj => obj . mesh ) ;
const meshesToIntersect = drawableObjects . map ( obj => obj . mesh ) ;
@ -569,28 +787,29 @@ _createInteractPrompt() {
this . pointer . x = 0 ;
this . pointer . x = 0 ;
this . pointer . y = 0 ;
this . pointer . y = 0 ;
this . raycast . setFromCamera ( this . pointer , this . camera ) ;
this . raycast . setFromCamera ( this . pointer , this . camera ) ;
const intersections = this . raycast . intersectObjects ( meshesToIntersect ) ;
this . raycast . layers . set ( 1 ) ;
const intersections = this . raycast . intersectObjects ( meshesToIntersect , true ) ;
if ( intersections . length > 0 ) {
if ( intersections . length > 0 ) {
const intersect = intersections [ 0 ] ;
const intersect = intersections [ 0 ] ;
if ( intersect . object . material . map && intersect . object . material . map . isCanvasTexture ) {
if ( intersect . object . material . map && intersect . object . material . map . isCanvasTexture ) {
this . _ drawOnTexture ( intersect ) ;
this . _ drawOnTexture ( intersect , this . currentSprayColor ) ;
}
}
}
}
}
}
// VR drawing (right controller)
// VR drawing (right controller)
const vrController = this . vrControllers [ 0 ] ;
const vrController = this . vrControllers [ 0 ] ;
if ( this . vr Drawing && vrController && this . renderer . xr . isPresenting ) {
if ( this . is Drawing && vrController && this . renderer . xr . isPresenting ) {
const controllerMatrix = vrController . matrixWorld ;
const controllerMatrix = vrController . matrixWorld ;
const ray = new THREE . Raycaster ( ) ;
const ray = new THREE . Raycaster ( ) ;
ray . ray . origin . setFromMatrixPosition ( controllerMatrix ) ;
ray . ray . origin . setFromMatrixPosition ( controllerMatrix ) ;
ray . ray . direction . set ( 0 , 0 , - 1 ) . applyMatrix4 ( new THREE . Matrix4 ( ) . extractRotation ( controllerMatrix ) ) ;
ray . ray . direction . set ( 0 , 0 , - 1 ) . applyMatrix4 ( new THREE . Matrix4 ( ) . extractRotation ( controllerMatrix ) ) ;
ray . layers . set ( 1 ) ;
const intersections = ray . intersectObjects ( meshesToIntersect ) ;
const intersections = ray . intersectObjects ( meshesToIntersect , true ) ;
if ( intersections . length > 0 ) {
if ( intersections . length > 0 ) {
const intersect = intersections [ 0 ] ;
const intersect = intersections [ 0 ] ;
if ( intersect . object . material . map && intersect . object . material . map . isCanvasTexture ) {
if ( intersect . object . material . map && intersect . object . material . map . isCanvasTexture ) {
this . _ drawOnTexture ( intersect ) ;
this . _ drawOnTexture ( intersect , this . currentSprayColor ) ;
}
}
}
}
}
}
@ -617,6 +836,9 @@ _createInteractPrompt() {
const intersects = ray . intersectObjects ( itemObj , true ) ;
const intersects = ray . intersectObjects ( itemObj , true ) ;
// Keep track of the previously hovered item
const previouslyHoveredItem = this . currentIntItem ;
// Update ray visualization
// Update ray visualization
const controllerRay = this . vrControllers [ 0 ] ? . getObjectByName ( 'controller-ray' ) ;
const controllerRay = this . vrControllers [ 0 ] ? . getObjectByName ( 'controller-ray' ) ;
if ( controllerRay ) {
if ( controllerRay ) {
@ -652,6 +874,16 @@ _createInteractPrompt() {
} else {
} else {
this . currentIntItem = null ;
this . currentIntItem = null ;
}
}
// If the hovered item has changed, update outlines
if ( previouslyHoveredItem !== this . currentIntItem ) {
if ( previouslyHoveredItem ) {
previouslyHoveredItem . hideOutline ( ) ;
}
if ( this . currentIntItem ) {
this . currentIntItem . showOutline ( ) ;
}
}
}
}
_ lockCameraForAttachedItem ( ) {
_ lockCameraForAttachedItem ( ) {
@ -702,27 +934,38 @@ _createInteractPrompt() {
. normalize ( ) ;
. normalize ( ) ;
// Add vertical movement from Q/E
// Add vertical movement from Q/E
let vertical = 0 ;
// let vertical = 0;
if ( this . input . up ) vertical += 1 ;
// if (this.input.up) vertical += 1;
if ( this . input . down ) vertical -= 1 ;
// if (this.input.down) vertical -= 1;
move . y = vertical ;
// move.y = vertical;
move . multiplyScalar ( this . moveSpeed * delta ) ;
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 ) ;
if ( newPosition . y <= 10 ) newPosition . y = 10 ;
// --- COLLISION LOGIC START ---
// Calculate movement with collision detection
this . characterController . computeColliderMovement (
this . collider ,
move
) ;
// Get the corrected movement vector (sliding against walls, stopping, etc.)
const correctedMovement = this . characterController . computedMovement ( ) ;
// Updating the position of the RB based on the corrected movement
const newPosition = this . position . clone ( ) . add ( correctedMovement ) ;
// Tell the physics engine where we want to go in the next step.
// 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 } ) ;
this . rigibody . setNextKinematicTranslation ( { x : newPosition . x , y : newPosition . y , z : newPosition . z } ) ;
// --- COLLISION LOGIC END ---
}
}
update ( delta , spawnedObjects ) {
update ( delta , spawnedObjects ) {
if ( this . renderer . xr . isPresenting ) {
if ( this . renderer . xr . isPresenting ) {
this . _ handleVRJoystick ( ) ;
this . _ handleVRJoystick ( ) ;
this . _ handleVRTeleport ( spawnedObjects ) ;
this . _ handleVRTeleport ( ) ;
this . _ handleControllerShake ( delta ) ;
}
}
if ( this . enableInput ) {
if ( this . enableInput ) {
@ -730,10 +973,16 @@ _createInteractPrompt() {
this . _ lockCameraForAttachedItem ( ) ;
this . _ lockCameraForAttachedItem ( ) ;
} else if ( ! this . renderer . xr . isPresenting ) { // Only update movement if not in VR
} else if ( ! this . renderer . xr . isPresenting ) { // Only update movement if not in VR
this . _ updatePlayerMovement ( delta ) ;
this . _ updatePlayerMovement ( delta ) ;
// --- Head Sway Logic ---
const swayAmplitude = 0.2 ; // How far the head moves up/down
const swaySpeed = 0.7 ; // How fast the sway is (Hz)
const swayOffset = Math . sin ( performance . now ( ) * 0.001 * swaySpeed * Math . PI * 2 ) * swayAmplitude ;
this . camera . position . y = 20 + swayOffset ; // 20 is your default camera Y
}
}
}
}
this . _ checkForInteractableItems ( ) ;
this . _ checkForInteractableItems ( ) ;
this . _ checkAreaTriggers ( ) ;
// --- Interact Prompt Logic ---
// --- Interact Prompt Logic ---
if ( ! this . interactPrompt ) {
if ( ! this . interactPrompt ) {
@ -795,6 +1044,42 @@ _createInteractPrompt() {
} ) ;
} ) ;
}
}
// --- Item Counter Animation ---
if ( this . itemCountDisplay && this . itemCountAnim . active ) {
this . itemCountAnim . time += delta ;
const t = this . itemCountAnim . time ;
const { duration , fadeIn , fadeOut , startY , endY } = this . itemCountAnim ;
// Fade in
let opacity = 1 ;
if ( t < fadeIn ) {
opacity = t / fadeIn ;
}
// Fade out
else if ( t > duration - fadeOut ) {
opacity = Math . max ( 0 , 1 - ( t - ( duration - fadeOut ) ) / fadeOut ) ;
}
// Move up
let y = startY ;
if ( t < duration ) {
y = startY + ( endY - startY ) * ( t / duration ) ;
} else {
y = endY ;
}
this . itemCountDisplay . position . set ( 0 , y , - 2 ) ;
this . itemCountDisplay . traverse ( obj => {
if ( obj . material ) obj . material . opacity = opacity ;
} ) ;
// Hide after animation
if ( t > duration ) {
this . itemCountDisplay . visible = false ;
this . itemCountAnim . active = false ;
}
}
this . input . mouseDelta . x = 0 ;
this . input . mouseDelta . x = 0 ;
this . input . mouseDelta . y = 0 ;
this . input . mouseDelta . y = 0 ;
}
}