import * as THREE from 'three' ; import * as CANNON from 'cannon' ; import * as _ from 'lodash' ; import * as Utils from '../core/FunctionLibrary' ; import { KeyBinding } from '../core/KeyBinding' ; import { VectorSpringSimulator } from '../physics/spring_simulation/VectorSpringSimulator' ; import { RelativeSpringSimulator } from '../physics/spring_simulation/RelativeSpringSimulator' ; import { Idle } from './character_states/Idle' ; import { EnteringVehicle } from './character_states/vehicles/EnteringVehicle' ; import { ExitingVehicle } from './character_states/vehicles/ExitingVehicle' ; import { OpenVehicleDoor as OpenVehicleDoor } from './character_states/vehicles/OpenVehicleDoor' ; import { Driving } from './character_states/vehicles/Driving' ; import { ExitingAirplane } from './character_states/vehicles/ExitingAirplane' ; import { ICharacterAI } from '../interfaces/ICharacterAI' ; import { World } from '../world/World' ; import { IControllable } from '../interfaces/IControllable' ; import { ICharacterState } from '../interfaces/ICharacterState' ; import { IWorldEntity } from '../interfaces/IWorldEntity' ; import { VehicleSeat } from '../vehicles/VehicleSeat' ; import { Vehicle } from '../vehicles/Vehicle' ; import { CollisionGroups } from '../enums/CollisionGroups' ; import { CapsuleCollider } from '../physics/colliders/CapsuleCollider' ; import { VehicleEntryInstance } from './VehicleEntryInstance' ; import { SeatType } from '../enums/SeatType' ; import { GroundImpactData } from './GroundImpactData' ; import { ClosestObjectFinder } from '../core/ClosestObjectFinder' ; import { Object3D } from 'three' ; import { EntityType } from '../enums/EntityType' ; export class Character extends THREE . Object3D implements IWorldEntity { public updateOrder : number = 1 ; public entityType : EntityType = EntityType . Character ; public height : number = 0 ; public tiltContainer : THREE . Group ; public modelContainer : THREE . Group ; public materials : THREE . Material [ ] = [ ] ; public mixer : THREE . AnimationMixer ; public animations : any [ ] ; public acceleration : THREE . Vector3 = new THREE . Vector3 ( ) ; public velocity : THREE . Vector3 = new THREE . Vector3 ( ) ; public arcadeVelocityInfluence : THREE . Vector3 = new THREE . Vector3 ( ) ; public velocityTarget : THREE . Vector3 = new THREE . Vector3 ( ) ; public arcadeVelocityIsAdditive : boolean = false ; public defaultVelocitySimulatorDamping : number = 0.8 ; public defaultVelocitySimulatorMass : number = 50 ; public velocitySimulator : VectorSpringSimulator ; public moveSpeed : number = 4 ; public angularVelocity : number = 0 ; public orientation : THREE . Vector3 = new THREE . Vector3 ( 0 , 0 , 1 ) ; public orientationTarget : THREE . Vector3 = new THREE . Vector3 ( 0 , 0 , 1 ) ; public defaultRotationSimulatorDamping : number = 0.5 ; public defaultRotationSimulatorMass : number = 10 ; public rotationSimulator : RelativeSpringSimulator ; public viewVector : THREE . Vector3 ; public actions : { [ action ] : KeyBinding ; } ; public characterCapsule : CapsuleCollider ; public rayResult : CANNON . RaycastResult = new CANNON . RaycastResult ( ) ; public rayHasHit : boolean = false ; public rayCastLength : number = 0.57 ; public raySafeOffset : number = 0.03 ; public wantsToJump : boolean = false ; public initJumpSpeed : number = - 1 ; public groundImpactData : GroundImpactData = new GroundImpactData ( ) ; public raycastBox : THREE . Mesh ; public world : World ; public charState : ICharacterState ; public behaviour : ICharacterAI ; public controlledObject : IControllable ; public occupyingSeat : VehicleSeat = null ; public vehicleEntryInstance : VehicleEntryInstance = null ; private physicsEnabled : boolean = true ; constructor ( gltf ) { super ( ) ; this . readCharacterData ( gltf ) ; this . setAnimations ( gltf . animations ) ; this . tiltContainer = new THREE . Group ( ) ; this . add ( this . tiltContainer ) ; this . modelContainer = new THREE . Group ( ) ; this . modelContainer . position . y = - 0.57 ; this . tiltContainer . add ( this . modelContainer ) ; this . modelContainer . add ( gltf . scene ) ; this . mixer = new THREE . AnimationMixer ( gltf . scene ) ; this . velocitySimulator = new VectorSpringSimulator ( 60 , this . defaultVelocitySimulatorMass , this . defaultVelocitySimulatorDamping ) ; this . rotationSimulator = new RelativeSpringSimulator ( 60 , this . defaultRotationSimulatorMass , this . defaultRotationSimulatorDamping ) ; this . viewVector = new THREE . Vector3 ( ) ; this . actions = { 'up' : new KeyBinding ( 'KeyW' ) , 'down' : new KeyBinding ( 'KeyS' ) , 'left' : new KeyBinding ( 'KeyA' ) , 'right' : new KeyBinding ( 'KeyD' ) , 'run' : new KeyBinding ( 'ShiftLeft' ) , 'jump' : new KeyBinding ( 'Space' ) , 'use' : new KeyBinding ( 'KeyE' ) , 'enter' : new KeyBinding ( 'KeyF' ) , 'enter_passenger' : new KeyBinding ( 'KeyG' ) , 'seat_switch' : new KeyBinding ( 'KeyX' ) , 'primary' : new KeyBinding ( 'Mouse0' ) , 'secondary' : new KeyBinding ( 'Mouse1' ) , } ; this . characterCapsule = new CapsuleCollider ( { mass : 1 , position : new CANNON . Vec3 ( ) , height : 0.5 , radius : 0.25 , segments : 8 , friction : 0.0 } ) ; this . characterCapsule . body . shapes . forEach ( ( shape ) => { shape . collisionFilterMask = ~ CollisionGroups . TrimeshColliders ; } ) ; this . characterCapsule . body . allowSleep = false ; this . characterCapsule . body . collisionFilterGroup = 2 ; this . characterCapsule . body . fixedRotation = true ; this . characterCapsule . body . updateMassProperties ( ) ; const boxGeo = new THREE . BoxGeometry ( 0.1 , 0.1 , 0.1 ) ; const boxMat = new THREE . MeshLambertMaterial ( { color : 0xff0000 } ) ; this . raycastBox = new THREE . Mesh ( boxGeo , boxMat ) ; this . raycastBox . visible = false ; this . characterCapsule . body . preStep = ( body ) => { this . physicsPreStep ( body , this ) ; } ; this . characterCapsule . body . postStep = ( body ) => { this . physicsPostStep ( body , this ) ; } ; this . setState ( new Idle ( this ) ) ; } public setAnimations ( animations ) { this . animations = animations ; } public setArcadeVelocityInfluence ( x , y = x , z = x ) { this . arcadeVelocityInfluence . set ( x , y , z ) ; } public setViewVector ( vector ) { this . viewVector . copy ( vector ) . normalize ( ) ; } param { function } State public setState ( state ) { this . charState = state ; this . charState . onInputChange ( ) ; } public setPosition ( x , y , z ) { if ( this . physicsEnabled ) { this . characterCapsule . body . previousPosition = new CANNON . Vec3 ( x , y , z ) ; this . characterCapsule . body . position = new CANNON . Vec3 ( x , y , z ) ; this . characterCapsule . body . interpolatedPosition = new CANNON . Vec3 ( x , y , z ) ; } else { this . position . x = x ; this . position . y = y ; this . position . z = z ; } } public resetVelocity ( ) { this . velocity . x = 0 ; this . velocity . y = 0 ; this . velocity . z = 0 ; this . characterCapsule . body . velocity . x = 0 ; this . characterCapsule . body . velocity . y = 0 ; this . characterCapsule . body . velocity . z = 0 ; this . velocitySimulator . init ( ) ; } public setArcadeVelocityTarget ( velZ , velX = 0 , velY = 0 ) { this . velocityTarget . z = velZ ; this . velocityTarget . x = velX ; this . velocityTarget . y = velY ; } public setOrientation ( vector , instantly = false ) { let lookVector = new THREE . Vector3 ( ) . copy ( vector ) . setY ( 0 ) . normalize ( ) ; this . orientationTarget . copy ( lookVector ) ; if ( instantly ) { this . orientation . copy ( lookVector ) ; } } public resetOrientation ( ) { const forward = Utils . getForward ( this ) ; this . setOrientation ( forward , true ) ; } public setBehaviour ( behaviour ) { behaviour . character = this ; this . behaviour = behaviour ; } public setPhysicsEnabled ( value ) { this . physicsEnabled = value ; if ( value === true ) { this . world . physicsWorld . addBody ( this . characterCapsule . body ) ; } else { this . world . physicsWorld . remove ( this . characterCapsule . body ) ; } } public readCharacterData ( gltf ) { gltf . scene . traverse ( ( child ) => { if ( child . isMesh ) { Utils . setupMeshProperties ( child ) ; if ( child . material !== undefined ) { this . materials . push ( child . material ) ; } } } ) ; } public handleKeyboardEvent ( event , code , pressed ) { if ( this . controlledObject !== undefined ) { this . controlledObject . handleKeyboardEvent ( event , code , pressed ) ; } else { if ( code === 'KeyC' && pressed === true && event . shiftKey === true ) { this . resetControls ( ) ; this . world . cameraOperator . characterCaller = this ; this . world . inputManager . setInputReceiver ( this . world . cameraOperator ) ; } else if ( code === 'KeyR' && pressed === true && event . shiftKey === true ) { this . world . restartScenario ( ) ; } else { for ( const action in this . actions ) { if ( this . actions . hasOwnProperty ( action ) ) { const binding = this . actions [ action ] ; if ( _ . includes ( binding . eventCodes , code ) ) { this . triggerAction ( action , pressed ) ; } } } } } } public handleMouseButton ( event , code , pressed ) { if ( this . controlledObject !== undefined ) { this . controlledObject . handleMouseButton ( event , code , pressed ) ; } else { for ( const action in this . actions ) { if ( this . actions . hasOwnProperty ( action ) ) { const binding = this . actions [ action ] ; if ( _ . includes ( binding . eventCodes , code ) ) { this . triggerAction ( action , pressed ) ; } } } } } public handleMouseMove ( event , deltaX , deltaY ) { if ( this . controlledObject !== undefined ) { this . controlledObject . handleMouseMove ( event , deltaX , deltaY ) ; } else { this . world . cameraOperator . move ( deltaX , deltaY ) ; } } public handleMouseWheel ( event , value ) { if ( this . controlledObject !== undefined ) { this . controlledObject . handleMouseWheel ( event , value ) ; } else { this . world . scrollTheTimeScale ( value ) ; } } public triggerAction ( actionName , value ) { let action = this . actions [ actionName ] ; if ( action . isPressed !== value ) { action . isPressed = value ; action . justPressed = false ; action . justReleased = false ; if ( value ) action . justPressed = true ; else action . justReleased = true ; this . charState . onInputChange ( ) ; action . justPressed = false ; action . justReleased = false ; } } public takeControl ( ) { if ( this . world !== undefined ) { this . world . inputManager . setInputReceiver ( this ) ; } else { console . warn ( 'Attempting to take control of a character that doesn\'t belong to a world.' ) ; } } public resetControls ( ) { for ( const action in this . actions ) { if ( this . actions . hasOwnProperty ( action ) ) { this . triggerAction ( action , false ) ; } } } public update ( timeStep ) { this . behaviour ?. update ( timeStep ) ; this . vehicleEntryInstance ?. update ( timeStep ) ; this . charState ?. update ( timeStep ) ; if ( this . physicsEnabled ) this . springMovement ( timeStep ) ; if ( this . physicsEnabled ) this . springRotation ( timeStep ) ; if ( this . physicsEnabled ) this . rotateModel ( ) ; if ( this . mixer !== undefined ) this . mixer . update ( timeStep ) ; if ( this . physicsEnabled ) { this . position . set ( this . characterCapsule . body . interpolatedPosition . x , this . characterCapsule . body . interpolatedPosition . y , this . characterCapsule . body . interpolatedPosition . z ) ; } else { let newPos = new THREE . Vector3 ( ) ; this . getWorldPosition ( newPos ) ; this . characterCapsule . body . position . copy ( Utils . cannonVector ( newPos ) ) ; this . characterCapsule . body . interpolatedPosition . copy ( Utils . cannonVector ( newPos ) ) ; } this . updateMatrixWorld ( ) ; } public inputReceiverInit ( ) { if ( this . controlledObject !== undefined ) { this . controlledObject . inputReceiverInit ( ) ; return ; } this . world . cameraOperator . setRadius ( 1.6 , true ) ; this . world . cameraOperator . followMode = false ; this . displayControls ( ) ; } public displayControls ( ) { this . world . updateControls ( [ { keys : [ 'W' , 'A' , 'S' , 'D' ] , desc : 'Movement' } , { keys : [ 'Shift' ] , desc : 'Sprint' } , { keys : [ 'Space' ] , desc : 'Jump' } , { keys : [ 'F' , 'or' , 'G' ] , desc : 'Enter vehicle' } , { keys : [ 'Shift' , '+' , 'R' ] , desc : 'Respawn' } , { keys : [ 'Shift' , '+' , 'C' ] , desc : 'Free camera' } , ] ) ; } public inputReceiverUpdate ( timeStep ) { if ( this . controlledObject !== undefined ) { this . controlledObject . inputReceiverUpdate ( timeStep ) ; } else { this . viewVector = new THREE . Vector3 ( ) . subVectors ( this . position , this . world . camera . position ) ; this . getWorldPosition ( this . world . cameraOperator . target ) ; } } public setAnimation ( clipName , fadeIn ) { if ( this . mixer !== undefined ) { let clip = THREE . AnimationClip . findByName ( this . animations , clipName ) ; let action = this . mixer . clipAction ( clip ) ; if ( action === null ) { console . error ( `Animation ${ clipName } not found!` ) ; return 0 ; } this . mixer . stopAllAction ( ) ; action . fadeIn ( fadeIn ) ; action . play ( ) ; return action . getClip ( ) . duration ; } } public springMovement ( timeStep ) { this . velocitySimulator . target . copy ( this . velocityTarget ) ; this . velocitySimulator . simulate ( timeStep ) ; this . velocity . copy ( this . velocitySimulator . position ) ; this . acceleration . copy ( this . velocitySimulator . velocity ) ; } public springRotation ( timeStep ) { let angle = Utils . getSignedAngleBetweenVectors ( this . orientation , this . orientationTarget ) ; this . rotationSimulator . target = angle ; this . rotationSimulator . simulate ( timeStep ) ; let rot = this . rotationSimulator . position ; this . orientation . applyAxisAngle ( new THREE . Vector3 ( 0 , 1 , 0 ) , rot ) ; this . angularVelocity = this . rotationSimulator . velocity ; } public getLocalMovementDirection ( ) { const positiveX = this . actions . right . isPressed ? - 1 : 0 ; const negativeX = this . actions . left . isPressed ? 1 : 0 ; const positiveZ = this . actions . up . isPressed ? 1 : 0 ; const negativeZ = this . actions . down . isPressed ? - 1 : 0 ; return new THREE . Vector3 ( positiveX + negativeX , 0 , positiveZ + negativeZ ) . normalize ( ) ; } public getCameraRelativeMovementVector ( ) { const localDirection = this . getLocalMovementDirection ( ) ; const flatViewVector = new THREE . Vector3 ( this . viewVector . x , 0 , this . viewVector . z ) . normalize ( ) ; return Utils . appplyVectorMatrixXZ ( flatViewVector , localDirection ) ; } public setCameraRelativeOrientationTarget ( ) { if ( this . vehicleEntryInstance === null ) { let moveVector = this . getCameraRelativeMovementVector ( ) ; if ( moveVector . x === 0 && moveVector . y === 0 && moveVector . z === 0 ) { this . setOrientation ( this . orientation ) ; } else { this . setOrientation ( moveVector ) ; } } } public rotateModel ( ) { this . lookAt ( this . position . x + this . orientation . x , this . position . y + this . orientation . y , this . position . z + this . orientation . z ) ; this . tiltContainer . rotation . z = ( - this . angularVelocity * 2.3 * this . velocity . length ( ) ) ; this . tiltContainer . position . setY ( ( Math . cos ( Math . abs ( this . angularVelocity * 2.3 * this . velocity . length ( ) ) ) / 2 ) - 0.5 ) ; } public jump ( initJumpSpeed = - 1 ) { this . wantsToJump = true ; this . initJumpSpeed = initJumpSpeed ; } public findVehicleToEnter ( wantsToDrive ) { let worldPos = new THREE . Vector3 ( ) ; let vehicleFinder = new ClosestObjectFinder < Vehicle > ( this . position , 10 ) ; this . world . vehicles . forEach ( ( vehicle ) => { vehicleFinder . consider ( vehicle , vehicle . position ) ; } ) ; if ( vehicleFinder . closestObject !== undefined ) { let vehicle = vehicleFinder . closestObject ; let vehicleEntryInstance = new VehicleEntryInstance ( this ) ; vehicleEntryInstance . wantsToDrive = wantsToDrive ; let seatFinder = new ClosestObjectFinder < VehicleSeat > ( this . position ) ; for ( const seat of vehicle . seats ) { if ( wantsToDrive ) { if ( seat . type === SeatType . Driver ) { seat . seatPointObject . getWorldPosition ( worldPos ) ; seatFinder . consider ( seat , worldPos ) ; } else if ( seat . type === SeatType . Passenger ) { for ( const connSeat of seat . connectedSeats ) { if ( connSeat . type === SeatType . Driver ) { seat . seatPointObject . getWorldPosition ( worldPos ) ; seatFinder . consider ( seat , worldPos ) ; break ; } } } } else { if ( seat . type === SeatType . Passenger ) { seat . seatPointObject . getWorldPosition ( worldPos ) ; seatFinder . consider ( seat , worldPos ) ; } } } if ( seatFinder . closestObject !== undefined ) { let targetSeat = seatFinder . closestObject ; vehicleEntryInstance . targetSeat = targetSeat ; let entryPointFinder = new ClosestObjectFinder < Object3D > ( this . position ) ; for ( const point of targetSeat . entryPoints ) { point . getWorldPosition ( worldPos ) ; entryPointFinder . consider ( point , worldPos ) ; } if ( entryPointFinder . closestObject !== undefined ) { vehicleEntryInstance . entryPoint = entryPointFinder . closestObject ; this . triggerAction ( 'up' , true ) ; this . vehicleEntryInstance = vehicleEntryInstance ; } } } } public enterVehicle ( seat , entryPoint ) { this . resetControls ( ) ; if ( seat . door ?. rotation < 0.5 ) { this . setState ( new OpenVehicleDoor ( this , seat , entryPoint ) ) ; } else { this . setState ( new EnteringVehicle ( this , seat , entryPoint ) ) ; } } public teleportToVehicle ( vehicle , seat ) { this . resetVelocity ( ) ; this . rotateModel ( ) ; this . setPhysicsEnabled ( false ) ; ( vehicle as unknown as THREE . Object3D ) . attach ( this ) ; this . setPosition ( seat . seatPointObject . position . x , seat . seatPointObject . position . y + 0.6 , seat . seatPointObject . position . z ) ; this . quaternion . copy ( seat . seatPointObject . quaternion ) ; this . occupySeat ( seat ) ; this . setState ( new Driving ( this , seat ) ) ; this . startControllingVehicle ( vehicle , seat ) ; } public startControllingVehicle ( vehicle , seat ) { if ( this . controlledObject !== vehicle ) { this . transferControls ( vehicle ) ; this . resetControls ( ) ; this . controlledObject = vehicle ; this . controlledObject . allowSleep ( false ) ; vehicle . inputReceiverInit ( ) ; vehicle . controllingCharacter = this ; } } public transferControls ( entity ) { for ( const action1 in this . actions ) { if ( this . actions . hasOwnProperty ( action1 ) ) { for ( const action2 in entity . actions ) { if ( entity . actions . hasOwnProperty ( action2 ) ) { let a1 = this . actions [ action1 ] ; let a2 = entity . actions [ action2 ] ; a1 . eventCodes . forEach ( ( code1 ) => { a2 . eventCodes . forEach ( ( code2 ) => { if ( code1 === code2 ) { entity . triggerAction ( action2 , a1 . isPressed ) ; } } ) ; } ) ; } } } } } public stopControllingVehicle ( ) { if ( this . controlledObject ?. controllingCharacter === this ) { this . controlledObject . allowSleep ( true ) ; this . controlledObject . controllingCharacter = undefined ; this . controlledObject . resetControls ( ) ; this . controlledObject = undefined ; this . inputReceiverInit ( ) ; } } public exitVehicle ( ) { if ( this . occupyingSeat !== null ) { if ( this . occupyingSeat . vehicle . entityType === EntityType . Airplane ) { this . setState ( new ExitingAirplane ( this , this . occupyingSeat ) ) ; } else { this . setState ( new ExitingVehicle ( this , this . occupyingSeat ) ) ; } this . stopControllingVehicle ( ) ; } } public occupySeat ( seat ) { this . occupyingSeat = seat ; seat . occupiedBy = this ; } public leaveSeat ( ) { if ( this . occupyingSeat !== null ) { this . occupyingSeat . occupiedBy = null ; this . occupyingSeat = null ; } } public physicsPreStep ( body , character ) { character . feetRaycast ( ) ; if ( character . rayHasHit ) { if ( character . raycastBox . visible ) { character . raycastBox . position . x = character . rayResult . hitPointWorld . x ; character . raycastBox . position . y = character . rayResult . hitPointWorld . y ; character . raycastBox . position . z = character . rayResult . hitPointWorld . z ; } } else { if ( character . raycastBox . visible ) { character . raycastBox . position . set ( body . position . x , body . position . y - character . rayCastLength - character . raySafeOffset , body . position . z ) ; } } } public feetRaycast ( ) { let body = this . characterCapsule . body ; const start = new CANNON . Vec3 ( body . position . x , body . position . y , body . position . z ) ; const end = new CANNON . Vec3 ( body . position . x , body . position . y - this . rayCastLength - this . raySafeOffset , body . position . z ) ; const rayCastOptions = { collisionFilterMask : CollisionGroups . Default , skipBackfaces : true } ; this . rayHasHit = this . world . physicsWorld . raycastClosest ( start , end , rayCastOptions , this . rayResult ) ; } public physicsPostStep ( body , character ) { let simulatedVelocity = new THREE . Vector3 ( body . velocity . x , body . velocity . y , body . velocity . z ) ; let arcadeVelocity = new THREE . Vector3 ( ) . copy ( character . velocity ) . multiplyScalar ( character . moveSpeed ) ; arcadeVelocity = Utils . appplyVectorMatrixXZ ( character . orientation , arcadeVelocity ) ; let newVelocity = new THREE . Vector3 ( ) ; if ( character . arcadeVelocityIsAdditive ) { newVelocity . copy ( simulatedVelocity ) ; let globalVelocityTarget = Utils . appplyVectorMatrixXZ ( character . orientation , character . velocityTarget ) ; let add = new THREE . Vector3 ( ) . copy ( arcadeVelocity ) . multiply ( character . arcadeVelocityInfluence ) ; if ( Math . abs ( simulatedVelocity . x ) < Math . abs ( globalVelocityTarget . x * character . moveSpeed ) || Utils . haveDifferentSigns ( simulatedVelocity . x , arcadeVelocity . x ) ) { newVelocity . x += add . x ; } if ( Math . abs ( simulatedVelocity . y ) < Math . abs ( globalVelocityTarget . y * character . moveSpeed ) || Utils . haveDifferentSigns ( simulatedVelocity . y , arcadeVelocity . y ) ) { newVelocity . y += add . y ; } if ( Math . abs ( simulatedVelocity . z ) < Math . abs ( globalVelocityTarget . z * character . moveSpeed ) || Utils . haveDifferentSigns ( simulatedVelocity . z , arcadeVelocity . z ) ) { newVelocity . z += add . z ; } } else { newVelocity = new THREE . Vector3 ( THREE . MathUtils . lerp ( simulatedVelocity . x , arcadeVelocity . x , character . arcadeVelocityInfluence . x ) , THREE . MathUtils . lerp ( simulatedVelocity . y , arcadeVelocity . y , character . arcadeVelocityInfluence . y ) , THREE . MathUtils . lerp ( simulatedVelocity . z , arcadeVelocity . z , character . arcadeVelocityInfluence . z ) ) ; } if ( character . rayHasHit ) { newVelocity . y = 0 ; if ( character . rayResult . body . mass > 0 ) { let pointVelocity = new CANNON . Vec3 ( ) ; character . rayResult . body . getVelocityAtWorldPoint ( character . rayResult . hitPointWorld , pointVelocity ) ; newVelocity . add ( Utils . threeVector ( pointVelocity ) ) ; } let up = new THREE . Vector3 ( 0 , 1 , 0 ) ; let normal = new THREE . Vector3 ( character . rayResult . hitNormalWorld . x , character . rayResult . hitNormalWorld . y , character . rayResult . hitNormalWorld . z ) ; let q = new THREE . Quaternion ( ) . setFromUnitVectors ( up , normal ) ; let m = new THREE . Matrix4 ( ) . makeRotationFromQuaternion ( q ) ; newVelocity . applyMatrix4 ( m ) ; body . velocity . x = newVelocity . x ; body . velocity . y = newVelocity . y ; body . velocity . z = newVelocity . z ; body . position . y = character . rayResult . hitPointWorld . y + character . rayCastLength + ( newVelocity . y / character . world . physicsFrameRate ) ; } else { body . velocity . x = newVelocity . x ; body . velocity . y = newVelocity . y ; body . velocity . z = newVelocity . z ; character . groundImpactData . velocity . x = body . velocity . x ; character . groundImpactData . velocity . y = body . velocity . y ; character . groundImpactData . velocity . z = body . velocity . z ; } if ( character . wantsToJump ) { if ( character . initJumpSpeed > - 1 ) { body . velocity . y = 0 ; let speed = Math . max ( character . velocitySimulator . position . length ( ) * 4 , character . initJumpSpeed ) ; body . velocity = Utils . cannonVector ( character . orientation . clone ( ) . multiplyScalar ( speed ) ) ; } else { let add = new CANNON . Vec3 ( ) ; character . rayResult . body . getVelocityAtWorldPoint ( character . rayResult . hitPointWorld , add ) ; body . velocity . vsub ( add , body . velocity ) ; } body . velocity . y += 4 ; body . position . y += character . raySafeOffset * 2 ; character . wantsToJump = false ; } } public addToWorld ( world ) { if ( _ . includes ( world . characters , this ) ) { console . warn ( 'Adding character to a world in which it already exists.' ) ; } else { this . world = world ; world . characters . push ( this ) ; world . physicsWorld . addBody ( this . characterCapsule . body ) ; world . graphicsWorld . add ( this ) ; world . graphicsWorld . add ( this . raycastBox ) ; this . materials . forEach ( ( mat ) => { world . sky . csm . setupMaterial ( mat ) ; } ) ; } } public removeFromWorld ( world ) { if ( ! _ . includes ( world . characters , this ) ) { console . warn ( 'Removing character from a world in which it isn\'t present.' ) ; } else { if ( world . inputManager . inputReceiver === this ) { world . inputManager . inputReceiver = undefined ; } this . world = undefined ; _ . pull ( world . characters , this ) ; world . physicsWorld . remove ( this . characterCapsule . body ) ; world . graphicsWorld . remove ( this ) ; world . graphicsWorld . remove ( this . raycastBox ) ; } } }