"use strict";/*jslint vars:true, plusplus:true, forin:true*//*global Window, Script, Controller, MyAvatar, AvatarList, Entities, Messages, Audio, SoundCache, Account, UserActivityLogger, Vec3, Quat, XMLHttpRequest, location, print*///// makeUserConnection.js// scripts/system//// Created by David Kelly on 3/7/2017.// Copyright 2017 High Fidelity, Inc.//// Distributed under the Apache License, Version 2.0.// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// (function () { // BEGIN LOCAL_SCOPE var request = Script.require('request').request; var controllerStandard = Controller.Standard; var WANT_DEBUG = Settings.getValue('MAKE_USER_CONNECTION_DEBUG', false); var LABEL = "makeUserConnection"; var MAX_AVATAR_DISTANCE = 0.2; // m var GRIP_MIN = 0.75; // goes from 0-1, so 75% pressed is pressed var MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection"; var STATES = { INACTIVE: 0, WAITING: 1, CONNECTING: 2, MAKING_CONNECTION: 3 }; var STATE_STRINGS = ["inactive", "waiting", "connecting", "makingConnection"]; var HAND_STRING_PROPERTY = 'hand'; // Used in our message protocol. IWBNI we changed it to handString, but that would break compatability. var WAITING_INTERVAL = 100; // ms var CONNECTING_INTERVAL = 100; // ms var MAKING_CONNECTION_TIMEOUT = 800; // ms var CONNECTING_TIME = 100; // ms One interval. var PARTICLE_RADIUS = 0.15; // m var PARTICLE_ANGLE_INCREMENT = 360 / 45; // 1hz var HANDSHAKE_SOUND_URL = Script.resolvePath("assets/sounds/4beat_sweep.wav"); var SUCCESSFUL_HANDSHAKE_SOUND_URL = Script.resolvePath("assets/sounds/3rdbeat_success_bell.wav"); var PARTICLE_TEXTURE = Script.resolvePath("assets/images/Bokeh-Particle.png"); var PREFERRER_HAND_JOINT_POSTFIX_ORDER = ['Middle1', 'Index1', '']; var HAPTIC_DATA = { initial: { duration: 20, strength: 0.6 }, // duration is in ms background: { duration: 100, strength: 0.3 }, // duration is in ms success: { duration: 60, strength: 1.0 } // duration is in ms }; var PARTICLE_EFFECT_PROPS = { "alpha": 0.8, "azimuthFinish": Math.PI, "azimuthStart": -Math.PI, "emitRate": 500, "emitterShouldTrail": 1, "isEmitting": 1, "lifespan": 3, "lifetime": 5, "maxParticles": 1000, "particleRadius": 0.003, "polarStart": Math.PI / 2, "polarFinish": Math.PI / 2, "radiusFinish": 0.008, "radiusStart": 0.0025, "emitSpeed": 0.02, "speedSpread": 0.015, "textures": PARTICLE_TEXTURE, "color": {"red": 255, "green": 255, "blue": 255}, "colorFinish": {"red": 0, "green": 164, "blue": 255}, "colorStart": {"red": 255, "green": 255, "blue": 255}, "emitOrientation": {"w": -0.71, "x": 0.0, "y": 0.0, "z": 0.71}, "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, "emitDimensions": { "x": 0.15, "y": 0.15, "z": 0.01 }, "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, "dimensions": {"x": 0.05, "y": 0.05, "z": 0.05}, "type": "ParticleEffect" }; var MAKING_CONNECTION_PARTICLE_PROPS = { "alpha": 0.07, "alphaStart": 0.011, "alphaSpread": 0, "alphaFinish": 0, "azimuthFinish": Math.PI, "azimuthStart": -1 * Math.PI, "emitRate": 2000, "emitSpeed": 0.0, "emitterShouldTrail": 1, "isEmitting": 1, "lifespan": 3.6, "lifetime": 5, "maxParticles": 4000, "particleRadius": 0.048, "polarStart": 0, "polarFinish": 1, "radiusFinish": 0.3, "radiusStart": 0.04, "speedSpread": 0.00, "radiusSpread": 0.0, "textures": PARTICLE_TEXTURE, "color": {"red": 200, "green": 170, "blue": 255}, "colorFinish": {"red": 0, "green": 134, "blue": 255}, "colorStart": {"red": 185, "green": 222, "blue": 255}, "emitOrientation": {"w": -0.71, "x": 0.0, "y": 0.0, "z": 0.71}, "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, "dimensions": {"x": 0.05, "y": 0.05, "z": 0.05}, "type": "ParticleEffect" }; var currentHand; var currentHandJointIndex = -1; var state = STATES.INACTIVE; var connectingInterval; var waitingInterval; var makingConnectionTimeout; var animHandlerId; var connectingId; var connectingHandJointIndex = -1; var waitingList = {}; var particleEffect; var particleEmitRate; var PARTICLE_INITIAL_EMIT_RATE = 250; var PARTICLE_MINIMUM_EMIT_RATE = 50; var PARTICLE_DECAY_RATE = 0.5; var particleEffectUpdateTimer = null; var PARTICLE_EFFECT_UPDATE_INTERVAL = 200; var makingConnectionParticleEffect; var makingConnectionEmitRate; var isMakingConnectionEmitting; var MAKING_CONNECTION_INITIAL_EMIT_RATE = 500; var MAKING_CONNECTION_MINIMUM_EMIT_RATE = 20; var MAKING_CONNECTION_DECAY_RATE = 0.5; var makingConnectionUpdateTimer = null; var MAKING_CONNECTION_UPDATE_INTERVAL = 200; var handshakeInjector; var successfulHandshakeInjector; var handshakeSound; var successfulHandshakeSound; function debug() { if (!WANT_DEBUG) { return; } var stateString = "<" + STATE_STRINGS[state] + ">"; var connecting = "[" + connectingId + "/" + connectingHandJointIndex + "]"; var current = "[" + currentHand + "/" + currentHandJointIndex + "]" print.apply(null, [].concat.apply([LABEL, stateString, current, JSON.stringify(waitingList), connecting], [].map.call(arguments, JSON.stringify))); } function cleanId(guidWithCurlyBraces) { return guidWithCurlyBraces.slice(1, -1); } function handToString(hand) { if (hand === controllerStandard.RightHand) { return "RightHand"; } if (hand === controllerStandard.LeftHand) { return "LeftHand"; } debug("handToString called without valid hand! value: ", hand); return ""; } function handToHaptic(hand) { if (hand === controllerStandard.RightHand) { return 1; } if (hand === controllerStandard.LeftHand) { return 0; } debug("handToHaptic called without a valid hand!"); return -1; } function stopWaiting() { if (waitingInterval) { waitingInterval = Script.clearInterval(waitingInterval); } } function stopConnecting() { if (connectingInterval) { connectingInterval = Script.clearInterval(connectingInterval); } } function stopMakingConnection() { if (makingConnectionTimeout) { makingConnectionTimeout = Script.clearTimeout(makingConnectionTimeout); } } // This returns the ideal hand joint index for the avatar. // [handString]middle1 -> [handString]index1 -> [handString] function getIdealHandJointIndex(avatar, handString) { debug("get hand " + handString + " for avatar " + (avatar && avatar.sessionUUID)); var suffixIndex, jointName, jointIndex; for (suffixIndex = 0; suffixIndex < (avatar ? PREFERRER_HAND_JOINT_POSTFIX_ORDER.length : 0); suffixIndex++) { jointName = handString + PREFERRER_HAND_JOINT_POSTFIX_ORDER[suffixIndex]; jointIndex = avatar.getJointIndex(jointName); if (jointIndex !== -1) { debug('found joint ' + jointName + ' (' + jointIndex + ')'); return jointIndex; } } debug('no hand joint found.'); return -1; } // This returns the preferred hand position. function getHandPosition(avatar, handJointIndex) { if (handJointIndex === -1) { debug("calling getHandPosition with no hand joint index! (returning avatar position but this is a BUG)"); return avatar.position; } return avatar.getJointPosition(handJointIndex); } var animationData = {}; function updateAnimationData(verticalOffset) { // all we are doing here is moving the right hand to a spot // that is in front of and a bit above the hips. Basing how // far in front as scaling with the avatar's height (say hips // to head distance) var headIndex = MyAvatar.getJointIndex("Head"); var offset = 0.5; // default distance of hand in front of you if (headIndex) { offset = 0.8 * MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y; } animationData.rightHandPosition = Vec3.multiply(offset, {x: -0.25, y: 0.8, z: 1.3}); if (verticalOffset) { animationData.rightHandPosition.y += verticalOffset; } animationData.rightHandRotation = Quat.fromPitchYawRollDegrees(90, 0, 90); animationData.rightHandType = 0; // RotationAndPosition, see IKTargets.h // turn on the right hand grip overlay animationData.rightHandOverlayAlpha = 1.0; // make sure the right hand grip animation is the "grasp", not pointing or thumbs up. animationData.isRightHandGrasp = true; animationData.isRightIndexPoint = false; animationData.isRightThumbRaise = false; animationData.isRightIndexPointAndThumbRaise = false; } function shakeHandsAnimation() { return animationData; } function endHandshakeAnimation() { if (animHandlerId) { debug("removing animation"); animHandlerId = MyAvatar.removeAnimationStateHandler(animHandlerId); } } function startHandshakeAnimation() { endHandshakeAnimation(); // just in case order of press/unpress is broken debug("adding animation"); updateAnimationData(); animHandlerId = MyAvatar.addAnimationStateHandler(shakeHandsAnimation, []); } function positionFractionallyTowards(posA, posB, frac) { return Vec3.sum(posA, Vec3.multiply(frac, Vec3.subtract(posB, posA))); } function deleteParticleEffect() { if (particleEffectUpdateTimer) { Script.clearTimeout(particleEffectUpdateTimer); particleEffectUpdateTimer = null; } if (particleEffect) { particleEffect = Entities.deleteEntity(particleEffect); } } function deleteMakeConnectionParticleEffect() { if (makingConnectionUpdateTimer) { Script.clearTimeout(makingConnectionUpdateTimer); makingConnectionUpdateTimer = null; } if (makingConnectionParticleEffect) { makingConnectionParticleEffect = Entities.deleteEntity(makingConnectionParticleEffect); } } function stopHandshakeSound() { if (handshakeInjector) { handshakeInjector.stop(); handshakeInjector = null; } } function updateMakingConnection() { if (!makingConnectionParticleEffect) { particleEffectUpdateTimer = null; return; } makingConnectionEmitRate = Math.max(makingConnectionEmitRate * MAKING_CONNECTION_DECAY_RATE, MAKING_CONNECTION_MINIMUM_EMIT_RATE); isMakingConnectionEmitting = true; Entities.editEntity(makingConnectionParticleEffect, { emitRate: makingConnectionEmitRate, isEmitting: true }); if (makingConnectionEmitRate > MAKING_CONNECTION_MINIMUM_EMIT_RATE) { makingConnectionUpdateTimer = Script.setTimeout(makingConnectionUpdateTimer, MAKING_CONNECTION_UPDATE_INTERVAL); } else { makingConnectionUpdateTimer = null; } } function updateParticleEffect() { if (!particleEffect) { particleEffectUpdateTimer = null; return; } particleEmitRate = Math.max(PARTICLE_MINIMUM_EMIT_RATE, particleEmitRate * PARTICLE_DECAY_RATE); Entities.editEntity(particleEffect, { emitRate: particleEmitRate }); if (particleEmitRate > PARTICLE_MINIMUM_EMIT_RATE) { particleEffectUpdateTimer = Script.setTimeout(updateParticleEffect, PARTICLE_EFFECT_UPDATE_INTERVAL); } else { particleEffectUpdateTimer = null; } } // this is called frequently, but usually does nothing function updateVisualization() { if (state === STATES.INACTIVE) { deleteParticleEffect(); deleteMakeConnectionParticleEffect(); return; } var myHandPosition = getHandPosition(MyAvatar, currentHandJointIndex); var otherHandPosition; var otherOrientation; if (connectingId) { var other = AvatarList.getAvatar(connectingId); if (other) { otherOrientation = other.orientation; otherHandPosition = getHandPosition(other, connectingHandJointIndex); } } switch (state) { case STATES.WAITING: // no visualization while waiting deleteParticleEffect(); deleteMakeConnectionParticleEffect(); stopHandshakeSound(); break; case STATES.CONNECTING: var particleProps = {}; // put the position between the 2 hands, if we have a connectingId. This // helps define the plane in which the particles move. positionFractionallyTowards(myHandPosition, otherHandPosition, 0.5); // now manage the rest of the entity if (!particleEffect) { particleEmitRate = PARTICLE_INITIAL_EMIT_RATE; particleProps = PARTICLE_EFFECT_PROPS; particleProps.position = positionFractionallyTowards(myHandPosition, otherHandPosition, 0.5); particleProps.rotation = Vec3.mix(Quat.getFront(MyAvatar.orientation), Quat.inverse(Quat.getFront(otherOrientation)), 0.5); particleProps.parentID = MyAvatar.sessionUUID; particleEffect = Entities.addEntity(particleProps, true); } if (!makingConnectionParticleEffect) { var props = MAKING_CONNECTION_PARTICLE_PROPS; props.parentID = MyAvatar.sessionUUID; makingConnectionEmitRate = MAKING_CONNECTION_INITIAL_EMIT_RATE; props.emitRate = makingConnectionEmitRate; props.isEmitting = false; props.position = myHandPosition; makingConnectionParticleEffect = Entities.addEntity(props, true); makingConnectionUpdateTimer = Script.setTimeout(updateMakingConnection, MAKING_CONNECTION_UPDATE_INTERVAL); } break; case STATES.MAKING_CONNECTION: if (makingConnectionUpdateTimer) { Script.clearTimeout(makingConnectionUpdateTimer); makingConnectionUpdateTimer = null; } if (isMakingConnectionEmitting) { Entities.editEntity(makingConnectionParticleEffect, { isEmitting: false }); isMakingConnectionEmitting = false; } if (!particleEffectUpdateTimer && particleEmitRate > PARTICLE_MINIMUM_EMIT_RATE) { particleEffectUpdateTimer = Script.setTimeout(updateParticleEffect, PARTICLE_EFFECT_UPDATE_INTERVAL); } break; default: debug("unexpected state", state); break; } } function isNearby() { if (currentHand) { var handPosition = getHandPosition(MyAvatar, currentHandJointIndex); var avatar = AvatarList.getAvatar(connectingId); if (avatar) { var distance = Vec3.distance(getHandPosition(avatar, connectingHandJointIndex), handPosition); return (distance < MAX_AVATAR_DISTANCE); } } return false; } function findNearestAvatar() { // We only look some max distance away (much larger than the handshake distance, but still...) var minDistance = MAX_AVATAR_DISTANCE * 20; var closestAvatar; AvatarList.getAvatarIdentifiers().forEach(function (id) { var avatar = AvatarList.getAvatar(id); if (avatar && avatar.sessionUUID != MyAvatar.sessionUUID) { var currentDistance = Vec3.distance(avatar.position, MyAvatar.position); if (minDistance > currentDistance) { minDistance = currentDistance; closestAvatar = avatar; } } }); return closestAvatar; } function adjustAnimationHeight() { var avatar = findNearestAvatar(); if (avatar) { var myHeadIndex = MyAvatar.getJointIndex("Head"); var otherHeadIndex = avatar.getJointIndex("Head"); var diff = (avatar.getJointPosition(otherHeadIndex).y - MyAvatar.getJointPosition(myHeadIndex).y) / 2; debug("head height difference: " + diff); updateAnimationData(diff); } } function findNearestWaitingAvatar() { var handPosition = getHandPosition(MyAvatar, currentHandJointIndex); var minDistance = MAX_AVATAR_DISTANCE; var nearestAvatar = {}; Object.keys(waitingList).forEach(function (identifier) { var avatar = AvatarList.getAvatar(identifier); if (avatar) { var handJointIndex = waitingList[identifier]; var distance = Vec3.distance(getHandPosition(avatar, handJointIndex), handPosition); if (distance < minDistance) { minDistance = distance; nearestAvatar = {avatarId: identifier, jointIndex: handJointIndex}; } } }); return nearestAvatar; } function messageSend(message) { // we always append whether or not we are logged in... message.isLoggedIn = Account.isLoggedIn(); Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); } function handStringMessageSend(message) { message[HAND_STRING_PROPERTY] = handToString(currentHand); messageSend(message); } function setupCandidate() { // find the closest in-range avatar, send connection request, and return true. (Otherwise falsey) var nearestAvatar = findNearestWaitingAvatar(); if (nearestAvatar.avatarId) { connectingId = nearestAvatar.avatarId; connectingHandJointIndex = nearestAvatar.jointIndex; debug("sending connectionRequest to", connectingId); handStringMessageSend({ key: "connectionRequest", id: connectingId }); return true; } } function clearConnecting() { connectingId = undefined; connectingHandJointIndex = -1; } function lookForWaitingAvatar() { // we started with nobody close enough, but maybe I've moved // or they did. Note that 2 people doing this race, so stop // as soon as you have a connectingId (which means you got their // message before noticing they were in range in this loop) // just in case we re-enter before stopping stopWaiting(); debug("started looking for waiting avatars"); waitingInterval = Script.setInterval(function () { if (state === STATES.WAITING && !connectingId) { setupCandidate(); } else { // something happened, stop looking for avatars to connect stopWaiting(); debug("stopped looking for waiting avatars"); } }, WAITING_INTERVAL);Lines 1–500 of 964. Load more
"use strict"; /*jslint vars:true, plusplus:true, forin:true*/ /*global Window, Script, Controller, MyAvatar, AvatarList, Entities, Messages, Audio, SoundCache, Account, UserActivityLogger, Vec3, Quat, XMLHttpRequest, location, print*/ // // makeUserConnection.js // scripts/system // // Created by David Kelly on 3/7/2017. // Copyright 2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // (function () { // BEGIN LOCAL_SCOPE var request = Script.require('request').request; var controllerStandard = Controller.Standard; var WANT_DEBUG = Settings.getValue('MAKE_USER_CONNECTION_DEBUG', false); var LABEL = "makeUserConnection"; var MAX_AVATAR_DISTANCE = 0.2; // m var GRIP_MIN = 0.75; // goes from 0-1, so 75% pressed is pressed var MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection"; var STATES = { INACTIVE: 0, WAITING: 1, CONNECTING: 2, MAKING_CONNECTION: 3 }; var STATE_STRINGS = ["inactive", "waiting", "connecting", "makingConnection"]; var HAND_STRING_PROPERTY = 'hand'; // Used in our message protocol. IWBNI we changed it to handString, but that would break compatability. var WAITING_INTERVAL = 100; // ms var CONNECTING_INTERVAL = 100; // ms var MAKING_CONNECTION_TIMEOUT = 800; // ms var CONNECTING_TIME = 100; // ms One interval. var PARTICLE_RADIUS = 0.15; // m var PARTICLE_ANGLE_INCREMENT = 360 / 45; // 1hz var HANDSHAKE_SOUND_URL = Script.resolvePath("assets/sounds/4beat_sweep.wav"); var SUCCESSFUL_HANDSHAKE_SOUND_URL = Script.resolvePath("assets/sounds/3rdbeat_success_bell.wav"); var PARTICLE_TEXTURE = Script.resolvePath("assets/images/Bokeh-Particle.png"); var PREFERRER_HAND_JOINT_POSTFIX_ORDER = ['Middle1', 'Index1', '']; var HAPTIC_DATA = { initial: { duration: 20, strength: 0.6 }, // duration is in ms background: { duration: 100, strength: 0.3 }, // duration is in ms success: { duration: 60, strength: 1.0 } // duration is in ms }; var PARTICLE_EFFECT_PROPS = { "alpha": 0.8, "azimuthFinish": Math.PI, "azimuthStart": -Math.PI, "emitRate": 500, "emitterShouldTrail": 1, "isEmitting": 1, "lifespan": 3, "lifetime": 5, "maxParticles": 1000, "particleRadius": 0.003, "polarStart": Math.PI / 2, "polarFinish": Math.PI / 2, "radiusFinish": 0.008, "radiusStart": 0.0025, "emitSpeed": 0.02, "speedSpread": 0.015, "textures": PARTICLE_TEXTURE, "color": {"red": 255, "green": 255, "blue": 255}, "colorFinish": {"red": 0, "green": 164, "blue": 255}, "colorStart": {"red": 255, "green": 255, "blue": 255}, "emitOrientation": {"w": -0.71, "x": 0.0, "y": 0.0, "z": 0.71}, "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, "emitDimensions": { "x": 0.15, "y": 0.15, "z": 0.01 }, "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, "dimensions": {"x": 0.05, "y": 0.05, "z": 0.05}, "type": "ParticleEffect" }; var MAKING_CONNECTION_PARTICLE_PROPS = { "alpha": 0.07, "alphaStart": 0.011, "alphaSpread": 0, "alphaFinish": 0, "azimuthFinish": Math.PI, "azimuthStart": -1 * Math.PI, "emitRate": 2000, "emitSpeed": 0.0, "emitterShouldTrail": 1, "isEmitting": 1, "lifespan": 3.6, "lifetime": 5, "maxParticles": 4000, "particleRadius": 0.048, "polarStart": 0, "polarFinish": 1, "radiusFinish": 0.3, "radiusStart": 0.04, "speedSpread": 0.00, "radiusSpread": 0.0, "textures": PARTICLE_TEXTURE, "color": {"red": 200, "green": 170, "blue": 255}, "colorFinish": {"red": 0, "green": 134, "blue": 255}, "colorStart": {"red": 185, "green": 222, "blue": 255}, "emitOrientation": {"w": -0.71, "x": 0.0, "y": 0.0, "z": 0.71}, "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, "dimensions": {"x": 0.05, "y": 0.05, "z": 0.05}, "type": "ParticleEffect" }; var currentHand; var currentHandJointIndex = -1; var state = STATES.INACTIVE; var connectingInterval; var waitingInterval; var makingConnectionTimeout; var animHandlerId; var connectingId; var connectingHandJointIndex = -1; var waitingList = {}; var particleEffect; var particleEmitRate; var PARTICLE_INITIAL_EMIT_RATE = 250; var PARTICLE_MINIMUM_EMIT_RATE = 50; var PARTICLE_DECAY_RATE = 0.5; var particleEffectUpdateTimer = null; var PARTICLE_EFFECT_UPDATE_INTERVAL = 200; var makingConnectionParticleEffect; var makingConnectionEmitRate; var isMakingConnectionEmitting; var MAKING_CONNECTION_INITIAL_EMIT_RATE = 500; var MAKING_CONNECTION_MINIMUM_EMIT_RATE = 20; var MAKING_CONNECTION_DECAY_RATE = 0.5; var makingConnectionUpdateTimer = null; var MAKING_CONNECTION_UPDATE_INTERVAL = 200; var handshakeInjector; var successfulHandshakeInjector; var handshakeSound; var successfulHandshakeSound; function debug() { if (!WANT_DEBUG) { return; } var stateString = "<" + STATE_STRINGS[state] + ">"; var connecting = "[" + connectingId + "/" + connectingHandJointIndex + "]"; var current = "[" + currentHand + "/" + currentHandJointIndex + "]" print.apply(null, [].concat.apply([LABEL, stateString, current, JSON.stringify(waitingList), connecting], [].map.call(arguments, JSON.stringify))); } function cleanId(guidWithCurlyBraces) { return guidWithCurlyBraces.slice(1, -1); } function handToString(hand) { if (hand === controllerStandard.RightHand) { return "RightHand"; } if (hand === controllerStandard.LeftHand) { return "LeftHand"; } debug("handToString called without valid hand! value: ", hand); return ""; } function handToHaptic(hand) { if (hand === controllerStandard.RightHand) { return 1; } if (hand === controllerStandard.LeftHand) { return 0; } debug("handToHaptic called without a valid hand!"); return -1; } function stopWaiting() { if (waitingInterval) { waitingInterval = Script.clearInterval(waitingInterval); } } function stopConnecting() { if (connectingInterval) { connectingInterval = Script.clearInterval(connectingInterval); } } function stopMakingConnection() { if (makingConnectionTimeout) { makingConnectionTimeout = Script.clearTimeout(makingConnectionTimeout); } } // This returns the ideal hand joint index for the avatar. // [handString]middle1 -> [handString]index1 -> [handString] function getIdealHandJointIndex(avatar, handString) { debug("get hand " + handString + " for avatar " + (avatar && avatar.sessionUUID)); var suffixIndex, jointName, jointIndex; for (suffixIndex = 0; suffixIndex < (avatar ? PREFERRER_HAND_JOINT_POSTFIX_ORDER.length : 0); suffixIndex++) { jointName = handString + PREFERRER_HAND_JOINT_POSTFIX_ORDER[suffixIndex]; jointIndex = avatar.getJointIndex(jointName); if (jointIndex !== -1) { debug('found joint ' + jointName + ' (' + jointIndex + ')'); return jointIndex; } } debug('no hand joint found.'); return -1; } // This returns the preferred hand position. function getHandPosition(avatar, handJointIndex) { if (handJointIndex === -1) { debug("calling getHandPosition with no hand joint index! (returning avatar position but this is a BUG)"); return avatar.position; } return avatar.getJointPosition(handJointIndex); } var animationData = {}; function updateAnimationData(verticalOffset) { // all we are doing here is moving the right hand to a spot // that is in front of and a bit above the hips. Basing how // far in front as scaling with the avatar's height (say hips // to head distance) var headIndex = MyAvatar.getJointIndex("Head"); var offset = 0.5; // default distance of hand in front of you if (headIndex) { offset = 0.8 * MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y; } animationData.rightHandPosition = Vec3.multiply(offset, {x: -0.25, y: 0.8, z: 1.3}); if (verticalOffset) { animationData.rightHandPosition.y += verticalOffset; } animationData.rightHandRotation = Quat.fromPitchYawRollDegrees(90, 0, 90); animationData.rightHandType = 0; // RotationAndPosition, see IKTargets.h // turn on the right hand grip overlay animationData.rightHandOverlayAlpha = 1.0; // make sure the right hand grip animation is the "grasp", not pointing or thumbs up. animationData.isRightHandGrasp = true; animationData.isRightIndexPoint = false; animationData.isRightThumbRaise = false; animationData.isRightIndexPointAndThumbRaise = false; } function shakeHandsAnimation() { return animationData; } function endHandshakeAnimation() { if (animHandlerId) { debug("removing animation"); animHandlerId = MyAvatar.removeAnimationStateHandler(animHandlerId); } } function startHandshakeAnimation() { endHandshakeAnimation(); // just in case order of press/unpress is broken debug("adding animation"); updateAnimationData(); animHandlerId = MyAvatar.addAnimationStateHandler(shakeHandsAnimation, []); } function positionFractionallyTowards(posA, posB, frac) { return Vec3.sum(posA, Vec3.multiply(frac, Vec3.subtract(posB, posA))); } function deleteParticleEffect() { if (particleEffectUpdateTimer) { Script.clearTimeout(particleEffectUpdateTimer); particleEffectUpdateTimer = null; } if (particleEffect) { particleEffect = Entities.deleteEntity(particleEffect); } } function deleteMakeConnectionParticleEffect() { if (makingConnectionUpdateTimer) { Script.clearTimeout(makingConnectionUpdateTimer); makingConnectionUpdateTimer = null; } if (makingConnectionParticleEffect) { makingConnectionParticleEffect = Entities.deleteEntity(makingConnectionParticleEffect); } } function stopHandshakeSound() { if (handshakeInjector) { handshakeInjector.stop(); handshakeInjector = null; } } function updateMakingConnection() { if (!makingConnectionParticleEffect) { particleEffectUpdateTimer = null; return; } makingConnectionEmitRate = Math.max(makingConnectionEmitRate * MAKING_CONNECTION_DECAY_RATE, MAKING_CONNECTION_MINIMUM_EMIT_RATE); isMakingConnectionEmitting = true; Entities.editEntity(makingConnectionParticleEffect, { emitRate: makingConnectionEmitRate, isEmitting: true }); if (makingConnectionEmitRate > MAKING_CONNECTION_MINIMUM_EMIT_RATE) { makingConnectionUpdateTimer = Script.setTimeout(makingConnectionUpdateTimer, MAKING_CONNECTION_UPDATE_INTERVAL); } else { makingConnectionUpdateTimer = null; } } function updateParticleEffect() { if (!particleEffect) { particleEffectUpdateTimer = null; return; } particleEmitRate = Math.max(PARTICLE_MINIMUM_EMIT_RATE, particleEmitRate * PARTICLE_DECAY_RATE); Entities.editEntity(particleEffect, { emitRate: particleEmitRate }); if (particleEmitRate > PARTICLE_MINIMUM_EMIT_RATE) { particleEffectUpdateTimer = Script.setTimeout(updateParticleEffect, PARTICLE_EFFECT_UPDATE_INTERVAL); } else { particleEffectUpdateTimer = null; } } // this is called frequently, but usually does nothing function updateVisualization() { if (state === STATES.INACTIVE) { deleteParticleEffect(); deleteMakeConnectionParticleEffect(); return; } var myHandPosition = getHandPosition(MyAvatar, currentHandJointIndex); var otherHandPosition; var otherOrientation; if (connectingId) { var other = AvatarList.getAvatar(connectingId); if (other) { otherOrientation = other.orientation; otherHandPosition = getHandPosition(other, connectingHandJointIndex); } } switch (state) { case STATES.WAITING: // no visualization while waiting deleteParticleEffect(); deleteMakeConnectionParticleEffect(); stopHandshakeSound(); break; case STATES.CONNECTING: var particleProps = {}; // put the position between the 2 hands, if we have a connectingId. This // helps define the plane in which the particles move. positionFractionallyTowards(myHandPosition, otherHandPosition, 0.5); // now manage the rest of the entity if (!particleEffect) { particleEmitRate = PARTICLE_INITIAL_EMIT_RATE; particleProps = PARTICLE_EFFECT_PROPS; particleProps.position = positionFractionallyTowards(myHandPosition, otherHandPosition, 0.5); particleProps.rotation = Vec3.mix(Quat.getFront(MyAvatar.orientation), Quat.inverse(Quat.getFront(otherOrientation)), 0.5); particleProps.parentID = MyAvatar.sessionUUID; particleEffect = Entities.addEntity(particleProps, true); } if (!makingConnectionParticleEffect) { var props = MAKING_CONNECTION_PARTICLE_PROPS; props.parentID = MyAvatar.sessionUUID; makingConnectionEmitRate = MAKING_CONNECTION_INITIAL_EMIT_RATE; props.emitRate = makingConnectionEmitRate; props.isEmitting = false; props.position = myHandPosition; makingConnectionParticleEffect = Entities.addEntity(props, true); makingConnectionUpdateTimer = Script.setTimeout(updateMakingConnection, MAKING_CONNECTION_UPDATE_INTERVAL); } break; case STATES.MAKING_CONNECTION: if (makingConnectionUpdateTimer) { Script.clearTimeout(makingConnectionUpdateTimer); makingConnectionUpdateTimer = null; } if (isMakingConnectionEmitting) { Entities.editEntity(makingConnectionParticleEffect, { isEmitting: false }); isMakingConnectionEmitting = false; } if (!particleEffectUpdateTimer && particleEmitRate > PARTICLE_MINIMUM_EMIT_RATE) { particleEffectUpdateTimer = Script.setTimeout(updateParticleEffect, PARTICLE_EFFECT_UPDATE_INTERVAL); } break; default: debug("unexpected state", state); break; } } function isNearby() { if (currentHand) { var handPosition = getHandPosition(MyAvatar, currentHandJointIndex); var avatar = AvatarList.getAvatar(connectingId); if (avatar) { var distance = Vec3.distance(getHandPosition(avatar, connectingHandJointIndex), handPosition); return (distance < MAX_AVATAR_DISTANCE); } } return false; } function findNearestAvatar() { // We only look some max distance away (much larger than the handshake distance, but still...) var minDistance = MAX_AVATAR_DISTANCE * 20; var closestAvatar; AvatarList.getAvatarIdentifiers().forEach(function (id) { var avatar = AvatarList.getAvatar(id); if (avatar && avatar.sessionUUID != MyAvatar.sessionUUID) { var currentDistance = Vec3.distance(avatar.position, MyAvatar.position); if (minDistance > currentDistance) { minDistance = currentDistance; closestAvatar = avatar; } } }); return closestAvatar; } function adjustAnimationHeight() { var avatar = findNearestAvatar(); if (avatar) { var myHeadIndex = MyAvatar.getJointIndex("Head"); var otherHeadIndex = avatar.getJointIndex("Head"); var diff = (avatar.getJointPosition(otherHeadIndex).y - MyAvatar.getJointPosition(myHeadIndex).y) / 2; debug("head height difference: " + diff); updateAnimationData(diff); } } function findNearestWaitingAvatar() { var handPosition = getHandPosition(MyAvatar, currentHandJointIndex); var minDistance = MAX_AVATAR_DISTANCE; var nearestAvatar = {}; Object.keys(waitingList).forEach(function (identifier) { var avatar = AvatarList.getAvatar(identifier); if (avatar) { var handJointIndex = waitingList[identifier]; var distance = Vec3.distance(getHandPosition(avatar, handJointIndex), handPosition); if (distance < minDistance) { minDistance = distance; nearestAvatar = {avatarId: identifier, jointIndex: handJointIndex}; } } }); return nearestAvatar; } function messageSend(message) { // we always append whether or not we are logged in... message.isLoggedIn = Account.isLoggedIn(); Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); } function handStringMessageSend(message) { message[HAND_STRING_PROPERTY] = handToString(currentHand); messageSend(message); } function setupCandidate() { // find the closest in-range avatar, send connection request, and return true. (Otherwise falsey) var nearestAvatar = findNearestWaitingAvatar(); if (nearestAvatar.avatarId) { connectingId = nearestAvatar.avatarId; connectingHandJointIndex = nearestAvatar.jointIndex; debug("sending connectionRequest to", connectingId); handStringMessageSend({ key: "connectionRequest", id: connectingId }); return true; } } function clearConnecting() { connectingId = undefined; connectingHandJointIndex = -1; } function lookForWaitingAvatar() { // we started with nobody close enough, but maybe I've moved // or they did. Note that 2 people doing this race, so stop // as soon as you have a connectingId (which means you got their // message before noticing they were in range in this loop) // just in case we re-enter before stopping stopWaiting(); debug("started looking for waiting avatars"); waitingInterval = Script.setInterval(function () { if (state === STATES.WAITING && !connectingId) { setupCandidate(); } else { // something happened, stop looking for avatars to connect stopWaiting(); debug("stopped looking for waiting avatars"); } }, WAITING_INTERVAL);