964 lines · 40.4 KB · JavaScript
RawDownload
"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.