mirror of
https://github.com/AleziaKurdis/Overte-community-apps.git
synced 2025-08-08 06:28:02 +02:00
v1.6 (By Alezia Kurdis for Overte, August 12th, 2023) Replace the Color Picker Component (sadly GPLv3) by a simple hue selector. Add an "uninstall" button Keep the icon active if the switch is on. The appreciation dodecahedron is now emissive, and glows as the intensity grows.
1138 lines
41 KiB
JavaScript
1138 lines
41 KiB
JavaScript
/*
|
|
appreciate_app.js
|
|
|
|
Created by Zach Fox on January 30th, 2019
|
|
Copyright 2019 High Fidelity, Inc.
|
|
Copyright 2023, Overte e.V.
|
|
|
|
"Appreciate" application.
|
|
Show someone else that you like what they're doing.
|
|
Open the app to see usage instructions and some options!
|
|
(version 1.6.0)
|
|
|
|
Distributed under the Apache License, Version 2.0
|
|
See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
*/
|
|
|
|
(function () {
|
|
// *************************************
|
|
// START UTILITY FUNCTIONS
|
|
// *************************************
|
|
// #region Utilities
|
|
var MS_PER_S = 1000;
|
|
var CM_PER_M = 100;
|
|
var HALF = 0.5;
|
|
|
|
|
|
// Returns the first valid joint position from the list of supplied test joint positions.
|
|
// If none are valid, returns MyAvatar.position.
|
|
function getValidJointPosition(jointsToTest) {
|
|
var currentJointIndex;
|
|
|
|
for (var i = 0; i < jointsToTest.length; i++) {
|
|
currentJointIndex = MyAvatar.getJointIndex(jointsToTest[i]);
|
|
|
|
if (currentJointIndex > -1) {
|
|
return MyAvatar.getJointPosition(jointsToTest[i]);
|
|
}
|
|
}
|
|
|
|
return MyAvatar.position;
|
|
}
|
|
|
|
|
|
// Returns the world position halfway between the user's hands
|
|
function getAppreciationPosition() {
|
|
var validLeftJoints = ["LeftHandMiddle2", "LeftHand", "LeftArm"];
|
|
var leftPosition = getValidJointPosition(validLeftJoints);
|
|
|
|
var validRightJoints = ["RightHandMiddle2", "RightHand", "RightArm"];;
|
|
var rightPosition = getValidJointPosition(validRightJoints);
|
|
|
|
var centerPosition = Vec3.sum(leftPosition, rightPosition);
|
|
centerPosition = Vec3.multiply(centerPosition, HALF);
|
|
|
|
return centerPosition;
|
|
}
|
|
|
|
|
|
// Returns a linearly scaled value based on `factor` and the other inputs
|
|
function linearScale(factor, minInput, maxInput, minOutput, maxOutput) {
|
|
return minOutput + (maxOutput - minOutput) *
|
|
(factor - minInput) / (maxInput - minInput);
|
|
}
|
|
|
|
function randomFloat(min, max) {
|
|
return Math.random() * (max - min) + min;
|
|
}
|
|
|
|
// Updates the Current Intensity Meter UI element. Called when intensity changes.
|
|
function updateCurrentIntensityUI() {
|
|
ui.sendMessage({method: "updateCurrentIntensityUI", currentIntensity: currentIntensity});
|
|
}
|
|
// #endregion
|
|
// *************************************
|
|
// END UTILITY FUNCTIONS
|
|
// *************************************
|
|
|
|
// If the interval that updates the intensity interval exists,
|
|
// clear it.
|
|
var updateIntensityEntityInterval = false;
|
|
var UPDATE_INTENSITY_ENTITY_INTERVAL_MS = 75;
|
|
function maybeClearUpdateIntensityEntityInterval() {
|
|
if (updateIntensityEntityInterval) {
|
|
Script.clearInterval(updateIntensityEntityInterval);
|
|
updateIntensityEntityInterval = false;
|
|
}
|
|
|
|
if (intensityEntity) {
|
|
Entities.deleteEntity(intensityEntity);
|
|
intensityEntity = false;
|
|
intensityEntityMaterial = Uuid.NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// Determines if any XYZ JSON object has changed "enough" based on
|
|
// last xyz values and current xyz values.
|
|
// Used for determining if angular velocity and dimensions have changed enough.
|
|
var lastAngularVelocity = {
|
|
"x": 0,
|
|
"y": 0,
|
|
"z": 0
|
|
};
|
|
var ANGVEL_DISTANCE_THRESHOLD_PERCENT_CHANGE = 0.35;
|
|
var lastDimensions= {
|
|
"x": 0,
|
|
"y": 0,
|
|
"z": 0
|
|
};
|
|
var DIMENSIONS_DISTANCE_THRESHOLD_PERCENT_CHANGE = 0.2;
|
|
function xyzVecChangedEnough(current, last, thresh) {
|
|
var currentLength = Math.sqrt(
|
|
Math.pow(current.x, TWO) + Math.pow(current.y, TWO) + Math.pow(current.z, TWO));
|
|
var lastLength = Math.sqrt(
|
|
Math.pow(last.x, TWO) + Math.pow(last.y, TWO) + Math.pow(last.z, TWO));
|
|
|
|
var change = Math.abs(currentLength - lastLength);
|
|
if (change/lastLength > thresh) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// Determines if color values have changed "enough" based on
|
|
// last color and current color
|
|
var lastColor = {
|
|
"red": 0,
|
|
"blue": 0,
|
|
"green": 0
|
|
};
|
|
var COLOR_DISTANCE_THRESHOLD_PERCENT_CHANGE = 0.35;
|
|
var TWO = 2;
|
|
function colorChangedEnough(current, last, thresh) {
|
|
var currentLength = Math.sqrt(
|
|
Math.pow(current.red, TWO) + Math.pow(current.green, TWO) + Math.pow(current.blue, TWO));
|
|
var lastLength = Math.sqrt(
|
|
Math.pow(last.red, TWO) + Math.pow(last.green, TWO) + Math.pow(last.blue, TWO));
|
|
|
|
var change = Math.abs(currentLength - lastLength);
|
|
if (change/lastLength > thresh) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// Updates the intensity entity based on the user's avatar's hand position and the
|
|
// current intensity of their appreciation.
|
|
// Many of these property values are empirically determined.
|
|
var intensityEntity = false;
|
|
var intensityEntityMaterial = Uuid.NULL;
|
|
var INTENSITY_ENTITY_MAX_DIMENSIONS = {
|
|
"x": 0.24,
|
|
"y": 0.24,
|
|
"z": 0.24
|
|
};
|
|
var INTENSITY_ENTITY_MIN_ANGULAR_VELOCITY = {
|
|
"x": -0.21,
|
|
"y": -0.21,
|
|
"z": -0.21
|
|
};
|
|
var INTENSITY_ENTITY_MAX_ANGULAR_VELOCITY = {
|
|
"x": 0.21,
|
|
"y": 0.21,
|
|
"z": 0.21
|
|
};
|
|
var intensityEntityColorMin = {
|
|
"red": 82,
|
|
"green": 196,
|
|
"blue": 145
|
|
};
|
|
var INTENSITY_ENTITY_COLOR_MAX_DEFAULT = {
|
|
"red": 5,
|
|
"green": 255,
|
|
"blue": 5
|
|
};
|
|
var MIN_COLOR_MULTIPLIER = 0.4;
|
|
var MAX_GLOW = 3;
|
|
var intensityEntityColorMax = JSON.parse(Settings.getValue("appreciate/entityColor",
|
|
JSON.stringify(INTENSITY_ENTITY_COLOR_MAX_DEFAULT)));
|
|
var ANGVEL_ENTITY_MULTIPLY_FACTOR = 62;
|
|
var INTENSITY_ENTITY_NAME = "Appreciation Dodecahedron";
|
|
var INTENSITY_ENTITY_PROPERTIES = {
|
|
"name": INTENSITY_ENTITY_NAME,
|
|
"type": "Shape",
|
|
"shape": "Dodecahedron",
|
|
"dimensions": {
|
|
"x": 0,
|
|
"y": 0,
|
|
"z": 0
|
|
},
|
|
"angularVelocity": {
|
|
"x": 0,
|
|
"y": 0,
|
|
"z": 0
|
|
},
|
|
"angularDamping": 0,
|
|
"grab": {
|
|
"grabbable": false
|
|
},
|
|
"collisionless": true,
|
|
"ignoreForCollisions": true,
|
|
"damping": 0,
|
|
"color": intensityEntityColorMin
|
|
};
|
|
var currentInitialAngularVelocity = {
|
|
"x": 0,
|
|
"y": 0,
|
|
"z": 0
|
|
};
|
|
function updateIntensityEntity() {
|
|
if (!showAppreciationEntity) {
|
|
return;
|
|
}
|
|
|
|
if (currentIntensity > 0) {
|
|
var matData = {
|
|
"materialVersion": 1,
|
|
"materials": [
|
|
{
|
|
"name": "intensity",
|
|
"albedo": [intensityEntityColorMax.red/255, intensityEntityColorMax.green/255, intensityEntityColorMax.blue/255],
|
|
"metallic": 0.001,
|
|
"roughness": 0.9,
|
|
"emissive": [(intensityEntityColorMax.red/255) * (MAX_GLOW * currentIntensity), (intensityEntityColorMax.green/255) * (MAX_GLOW * currentIntensity), (intensityEntityColorMax.blue/255) * (MAX_GLOW * currentIntensity)],
|
|
"cullFaceMode": "CULL_BACK",
|
|
"model": "hifi_pbr"
|
|
}
|
|
]
|
|
};
|
|
|
|
if (intensityEntity) {
|
|
|
|
var propsToUpdate = {
|
|
position: getAppreciationPosition()
|
|
};
|
|
|
|
var currentDimensions = Vec3.multiply(INTENSITY_ENTITY_MAX_DIMENSIONS, currentIntensity);
|
|
if (xyzVecChangedEnough(currentDimensions, lastDimensions, DIMENSIONS_DISTANCE_THRESHOLD_PERCENT_CHANGE)) {
|
|
propsToUpdate.dimensions = currentDimensions;
|
|
lastDimensions = currentDimensions;
|
|
}
|
|
|
|
var currentAngularVelocity = Vec3.multiply(currentInitialAngularVelocity,
|
|
currentIntensity * ANGVEL_ENTITY_MULTIPLY_FACTOR);
|
|
if (xyzVecChangedEnough(currentAngularVelocity, lastAngularVelocity, ANGVEL_DISTANCE_THRESHOLD_PERCENT_CHANGE)) {
|
|
propsToUpdate.angularVelocity = currentAngularVelocity;
|
|
lastAngularVelocity = currentAngularVelocity;
|
|
}
|
|
|
|
Entities.editEntity(intensityEntity, propsToUpdate);
|
|
Entities.editEntity(intensityEntityMaterial, {"materialData": JSON.stringify(matData)});
|
|
} else {
|
|
var props = INTENSITY_ENTITY_PROPERTIES;
|
|
props.position = getAppreciationPosition();
|
|
|
|
currentInitialAngularVelocity.x =
|
|
randomFloat(INTENSITY_ENTITY_MIN_ANGULAR_VELOCITY.x, INTENSITY_ENTITY_MAX_ANGULAR_VELOCITY.x);
|
|
currentInitialAngularVelocity.y =
|
|
randomFloat(INTENSITY_ENTITY_MIN_ANGULAR_VELOCITY.y, INTENSITY_ENTITY_MAX_ANGULAR_VELOCITY.y);
|
|
currentInitialAngularVelocity.z =
|
|
randomFloat(INTENSITY_ENTITY_MIN_ANGULAR_VELOCITY.z, INTENSITY_ENTITY_MAX_ANGULAR_VELOCITY.z);
|
|
props.angularVelocity = currentInitialAngularVelocity;
|
|
|
|
intensityEntity = Entities.addEntity(props, "avatar");
|
|
intensityEntityMaterial = Entities.addEntity({
|
|
"type": "Material",
|
|
"name": "Intensity Entity Material",
|
|
"parentID": intensityEntity,
|
|
"materialURL": "materialData",
|
|
"materialData": JSON.stringify(matData),
|
|
"priority": 2,
|
|
"parentMaterialName": "0"
|
|
}, "avatar");
|
|
}
|
|
} else {
|
|
if (intensityEntity) {
|
|
Entities.deleteEntity(intensityEntity);
|
|
intensityEntity = false;
|
|
intensityEntityMaterial = Uuid.NULL;
|
|
}
|
|
|
|
maybeClearUpdateIntensityEntityInterval();
|
|
}
|
|
}
|
|
|
|
|
|
// Function that AppUI calls when the App's UI opens
|
|
function onOpened() {
|
|
updateCurrentIntensityUI();
|
|
}
|
|
|
|
function onClosed() {
|
|
if (appreciateEnabled) {
|
|
ui.buttonActive(true);
|
|
} else {
|
|
ui.buttonActive(false);
|
|
}
|
|
}
|
|
|
|
// Locally pre-caches all of the sounds in the sounds/claps and sounds/whistles
|
|
// directories.
|
|
var NUM_CLAP_SOUNDS = 16;
|
|
var NUM_WHISTLE_SOUNDS = 17;
|
|
var clapSounds = [];
|
|
var whistleSounds = [];
|
|
function getSounds() {
|
|
for (var i = 1; i < NUM_CLAP_SOUNDS + 1; i++) {
|
|
clapSounds.push(SoundCache.getSound(Script.resolvePath(
|
|
"resources/sounds/claps/" + ("0" + i).slice(-2) + ".wav")));
|
|
}
|
|
for (i = 1; i < NUM_WHISTLE_SOUNDS + 1; i++) {
|
|
whistleSounds.push(SoundCache.getSound(Script.resolvePath(
|
|
"resources/sounds/whistles/" + ("0" + i).slice(-2) + ".wav")));
|
|
}
|
|
}
|
|
|
|
|
|
// Locally pre-caches the Cheering and Clapping animations
|
|
var whistlingAnimation = false;
|
|
var clappingAnimation = false;
|
|
function getAnimations() {
|
|
var animationURL = Script.resolvePath("resources/animations/Cheering.fbx");
|
|
var resource = AnimationCache.prefetch(animationURL);
|
|
var animation = AnimationCache.getAnimation(animationURL);
|
|
whistlingAnimation = {url: animationURL, animation: animation, resource: resource};
|
|
|
|
animationURL = Script.resolvePath("resources/animations/Clapping.fbx");
|
|
resource = AnimationCache.prefetch(animationURL);
|
|
animation = AnimationCache.getAnimation(animationURL);
|
|
clappingAnimation = {url: animationURL, animation: animation, resource: resource};
|
|
}
|
|
|
|
|
|
// If we're currently fading out the appreciation sounds on an interval,
|
|
// clear that interval.
|
|
function maybeClearSoundFadeInterval() {
|
|
if (soundFadeInterval) {
|
|
Script.clearInterval(soundFadeInterval);
|
|
soundFadeInterval = false;
|
|
}
|
|
}
|
|
|
|
|
|
// Fade out the appreciation sounds by quickly
|
|
// lowering the global current intensity.
|
|
var soundFadeInterval = false;
|
|
var FADE_INTERVAL_MS = 20;
|
|
var FADE_OUT_STEP_SIZE = 0.05; // Unitless
|
|
function fadeOutAndStopSound() {
|
|
maybeClearSoundFadeInterval();
|
|
|
|
soundFadeInterval = Script.setInterval(function() {
|
|
currentIntensity -= FADE_OUT_STEP_SIZE;
|
|
|
|
if (currentIntensity <= 0) {
|
|
if (soundInjector) {
|
|
soundInjector.stop();
|
|
soundInjector = false;
|
|
}
|
|
|
|
updateCurrentIntensityUI();
|
|
|
|
maybeClearSoundFadeInterval();
|
|
}
|
|
|
|
fadeIntensity(currentIntensity, INTENSITY_MAX_STEP_SIZE_DESKTOP);
|
|
}, FADE_INTERVAL_MS);
|
|
}
|
|
|
|
|
|
// Calculates the audio injector volume based on
|
|
// the current global appreciation intensity and some min/max values.
|
|
var MIN_VOLUME_CLAP = 0.05;
|
|
var MAX_VOLUME_CLAP = 1.0;
|
|
var MIN_VOLUME_WHISTLE = 0.07;
|
|
var MAX_VOLUME_WHISTLE = 0.16;
|
|
function calculateInjectorVolume() {
|
|
var minInputVolume = 0;
|
|
var maxInputVolume = MAX_CLAP_INTENSITY;
|
|
var minOutputVolume = MIN_VOLUME_CLAP;
|
|
var maxOutputVolume = MAX_VOLUME_CLAP;
|
|
|
|
if (currentSound === "whistle") {
|
|
minInputVolume = MAX_CLAP_INTENSITY;
|
|
maxInputVolume = MAX_WHISTLE_INTENSITY;
|
|
minOutputVolume = MIN_VOLUME_WHISTLE;
|
|
maxOutputVolume = MAX_VOLUME_WHISTLE;
|
|
}
|
|
|
|
var vol = linearScale(currentIntensity, minInputVolume,
|
|
maxInputVolume, minOutputVolume, maxOutputVolume);
|
|
return vol;
|
|
}
|
|
|
|
|
|
// Modifies the global currentIntensity. Moves towards the targetIntensity,
|
|
// but never moves faster than a given max step size per function call.
|
|
// Also clamps the intensity to a min of 0 and a max of 1.0.
|
|
var currentIntensity = 0;
|
|
var INTENSITY_MAX_STEP_SIZE = 0.003; // Unitless, determined empirically
|
|
var INTENSITY_MAX_STEP_SIZE_DESKTOP = 1; // Unitless, determined empirically
|
|
var MAX_CLAP_INTENSITY = 0.55; // Unitless, determined empirically
|
|
var MAX_WHISTLE_INTENSITY = 1.0; // Unitless, determined empirically
|
|
function fadeIntensity(targetIntensity, maxStepSize) {
|
|
if (!maxStepSize) {
|
|
maxStepSize = INTENSITY_MAX_STEP_SIZE;
|
|
}
|
|
|
|
var volumeDelta = targetIntensity - currentIntensity;
|
|
volumeDelta = Math.min(Math.abs(volumeDelta), maxStepSize);
|
|
|
|
if (targetIntensity < currentIntensity) {
|
|
volumeDelta *= -1;
|
|
}
|
|
|
|
currentIntensity += volumeDelta;
|
|
|
|
currentIntensity = Math.max(0.0, Math.min(
|
|
neverWhistleEnabled ? MAX_CLAP_INTENSITY : MAX_WHISTLE_INTENSITY, currentIntensity));
|
|
|
|
updateCurrentIntensityUI();
|
|
|
|
// Don't adjust volume or position while a sound is playing.
|
|
if (!soundInjector || soundInjector.isPlaying()) {
|
|
return;
|
|
}
|
|
|
|
var injectorOptions = {
|
|
position: getAppreciationPosition(),
|
|
volume: calculateInjectorVolume()
|
|
};
|
|
|
|
soundInjector.setOptions(injectorOptions);
|
|
}
|
|
|
|
|
|
// Call this function to actually play a sound.
|
|
// Doesn't play a new sound if a sound is playing AND (you're whistling OR you're in HMD)
|
|
// Injectors are placed between the user's hands (at the same location as the apprecation
|
|
// entity) and are randomly pitched between a MIN and MAX value.
|
|
// Only uses one injector, ever.
|
|
var soundInjector = false;
|
|
var MINIMUM_PITCH = 0.85;
|
|
var MAXIMUM_PITCH = 1.15;
|
|
function playSound(sound) {
|
|
if (soundInjector && soundInjector.isPlaying() && (currentSound === "whistle" || HMD.active)) {
|
|
return;
|
|
}
|
|
|
|
if (soundInjector) {
|
|
soundInjector.stop();
|
|
soundInjector = false;
|
|
}
|
|
|
|
soundInjector = Audio.playSound(sound, {
|
|
position: getAppreciationPosition(),
|
|
volume: calculateInjectorVolume(),
|
|
pitch: randomFloat(MINIMUM_PITCH, MAXIMUM_PITCH)
|
|
});
|
|
}
|
|
|
|
|
|
// Returns true if the global intensity and user settings dictate that clapping is the
|
|
// right thing to do.
|
|
function shouldClap() {
|
|
return (currentIntensity > 0.0 && neverWhistleEnabled) ||
|
|
(currentIntensity > 0.0 && currentIntensity <= MAX_CLAP_INTENSITY);
|
|
}
|
|
|
|
|
|
// Returns true if the global intensity and user settings dictate that whistling is the
|
|
// right thing to do.
|
|
function shouldWhistle() {
|
|
return currentIntensity > MAX_CLAP_INTENSITY &&
|
|
currentIntensity <= MAX_WHISTLE_INTENSITY;
|
|
}
|
|
|
|
|
|
// Selects the correct sound, then plays it.
|
|
var currentSound;
|
|
function selectAndPlaySound() {
|
|
if (shouldClap()) {
|
|
currentSound = "clap";
|
|
playSound(clapSounds[Math.floor(Math.random() * clapSounds.length)]);
|
|
} else if (shouldWhistle()) {
|
|
currentSound = "whistle";
|
|
playSound(whistleSounds[Math.floor(Math.random() * whistleSounds.length)]);
|
|
}
|
|
}
|
|
|
|
|
|
// If there exists a VR debounce timer (used for not playing sounds too often),
|
|
// clear it.
|
|
function maybeClearVRDebounceTimer() {
|
|
if (vrDebounceTimer) {
|
|
Script.clearTimeout(vrDebounceTimer);
|
|
vrDebounceTimer = false;
|
|
}
|
|
}
|
|
|
|
|
|
// Calculates the current intensity of appreciation based on the user's
|
|
// hand velocity (rotational and linear).
|
|
// Each type of velocity is weighted differently when determining the final intensity.
|
|
// The VR debounce timer length changes based on current intensity. This forces
|
|
// sounds to play further apart when the user isn't appreciating hard.
|
|
var MAX_VELOCITY_CM_PER_SEC = 110; // determined empirically
|
|
var MAX_ANGULAR_VELOCITY_LENGTH = 1.5; // determined empirically
|
|
var LINEAR_VELOCITY_WEIGHT = 0.7; // This and the line below must add up to 1.0
|
|
var ANGULAR_VELOCITY_LENGTH_WEIGHT = 0.3; // This and the line below must add up to 1.0
|
|
var vrDebounceTimer = false;
|
|
var VR_DEBOUNCE_TIMER_TIMEOUT_MIN_MS = 20; // determined empirically
|
|
var VR_DEBOUNCE_TIMER_TIMEOUT_MAX_MS = 200; // determined empirically
|
|
function calculateHandEffect(linearVelocity, angularVelocity){
|
|
var leftHandLinearVelocityCMPerSec = linearVelocity.left;
|
|
var rightHandLinearVelocityCMPerSec = linearVelocity.right;
|
|
var averageLinearVelocity = (leftHandLinearVelocityCMPerSec + rightHandLinearVelocityCMPerSec) / 2;
|
|
averageLinearVelocity = Math.min(averageLinearVelocity, MAX_VELOCITY_CM_PER_SEC);
|
|
|
|
var leftHandAngularVelocityLength = Vec3.length(angularVelocity.left);
|
|
var rightHandAngularVelocityLength = Vec3.length(angularVelocity.right);
|
|
var averageAngularVelocityIntensity = (leftHandAngularVelocityLength + rightHandAngularVelocityLength) / 2;
|
|
averageAngularVelocityIntensity = Math.min(averageAngularVelocityIntensity, MAX_ANGULAR_VELOCITY_LENGTH);
|
|
|
|
var appreciationIntensity =
|
|
averageLinearVelocity / MAX_VELOCITY_CM_PER_SEC * LINEAR_VELOCITY_WEIGHT +
|
|
averageAngularVelocityIntensity / MAX_ANGULAR_VELOCITY_LENGTH * ANGULAR_VELOCITY_LENGTH_WEIGHT;
|
|
|
|
fadeIntensity(appreciationIntensity);
|
|
|
|
var vrDebounceTimeout = VR_DEBOUNCE_TIMER_TIMEOUT_MIN_MS +
|
|
(VR_DEBOUNCE_TIMER_TIMEOUT_MAX_MS - VR_DEBOUNCE_TIMER_TIMEOUT_MIN_MS) * (1.0 - appreciationIntensity);
|
|
// This timer forces a minimum tail duration for all sound clips
|
|
if (!vrDebounceTimer) {
|
|
selectAndPlaySound();
|
|
vrDebounceTimer = Script.setTimeout(function() {
|
|
vrDebounceTimer = false;
|
|
}, vrDebounceTimeout);
|
|
}
|
|
}
|
|
|
|
|
|
// Gets both hands' linear velocity.
|
|
var lastLeftHandPosition = false;
|
|
var lastRightHandPosition = false;
|
|
function getHandsLinearVelocity() {
|
|
var linearVelocity = {
|
|
left: 0,
|
|
right: 0
|
|
};
|
|
|
|
var leftHandPosition = MyAvatar.getJointPosition("LeftHand");
|
|
var rightHandPosition = MyAvatar.getJointPosition("RightHand");
|
|
|
|
if (!lastLeftHandPosition || !lastRightHandPosition) {
|
|
lastLeftHandPosition = leftHandPosition;
|
|
lastRightHandPosition = rightHandPosition;
|
|
return linearVelocity;
|
|
}
|
|
|
|
var leftHandDistanceCM = Vec3.distance(leftHandPosition, lastLeftHandPosition) * CM_PER_M;
|
|
var rightHandDistanceCM = Vec3.distance(rightHandPosition, lastRightHandPosition) * CM_PER_M;
|
|
|
|
linearVelocity.left = leftHandDistanceCM / HAND_VELOCITY_CHECK_INTERVAL_MS * MS_PER_S;
|
|
linearVelocity.right = rightHandDistanceCM / HAND_VELOCITY_CHECK_INTERVAL_MS * MS_PER_S;
|
|
|
|
lastLeftHandPosition = leftHandPosition;
|
|
lastRightHandPosition = rightHandPosition;
|
|
|
|
return linearVelocity;
|
|
}
|
|
|
|
|
|
// Gets both hands' angular velocity.
|
|
var lastLeftHandRotation = false;
|
|
var lastRightHandRotation = false;
|
|
function getHandsAngularVelocity() {
|
|
var angularVelocity = {
|
|
left: {x: 0, y: 0, z: 0},
|
|
right: {x: 0, y: 0, z: 0}
|
|
};
|
|
|
|
var leftHandRotation = MyAvatar.getJointRotation(MyAvatar.getJointIndex("LeftHand"));
|
|
var rightHandRotation = MyAvatar.getJointRotation(MyAvatar.getJointIndex("RightHand"));
|
|
|
|
if (!lastLeftHandRotation || !lastRightHandRotation) {
|
|
lastLeftHandRotation = leftHandRotation;
|
|
lastRightHandRotation = rightHandRotation;
|
|
return angularVelocity;
|
|
}
|
|
|
|
var leftHandAngleDelta = Quat.multiply(leftHandRotation, Quat.inverse(lastLeftHandRotation));
|
|
var rightHandAngleDelta = Quat.multiply(rightHandRotation, Quat.inverse(lastRightHandRotation));
|
|
|
|
leftHandAngleDelta = Quat.safeEulerAngles(leftHandAngleDelta);
|
|
rightHandAngleDelta = Quat.safeEulerAngles(rightHandAngleDelta);
|
|
|
|
angularVelocity.left = Vec3.multiply(leftHandAngleDelta, 1 / HAND_VELOCITY_CHECK_INTERVAL_MS);
|
|
angularVelocity.right = Vec3.multiply(rightHandAngleDelta, 1 / HAND_VELOCITY_CHECK_INTERVAL_MS);
|
|
|
|
lastLeftHandRotation = leftHandRotation;
|
|
lastRightHandRotation = rightHandRotation;
|
|
|
|
return angularVelocity;
|
|
}
|
|
|
|
|
|
// Calculates the hand effect (see above). Gets called on an interval,
|
|
// but only if the user's hands are above their head. This saves processing power.
|
|
// Also sets up the `updateIntensityEntity` interval.
|
|
function handVelocityCheck() {
|
|
if (!handsAreAboveHead) {
|
|
return;
|
|
}
|
|
|
|
var handsLinearVelocity = getHandsLinearVelocity();
|
|
var handsAngularVelocity = getHandsAngularVelocity();
|
|
|
|
calculateHandEffect(handsLinearVelocity, handsAngularVelocity);
|
|
|
|
if (!updateIntensityEntityInterval && showAppreciationEntity) {
|
|
updateIntensityEntityInterval = Script.setInterval(updateIntensityEntity, UPDATE_INTENSITY_ENTITY_INTERVAL_MS);
|
|
}
|
|
}
|
|
|
|
|
|
// If handVelocityCheckInterval is set up, clear it.
|
|
function maybeClearHandVelocityCheck() {
|
|
if (handVelocityCheckInterval) {
|
|
Script.clearInterval(handVelocityCheckInterval);
|
|
handVelocityCheckInterval = false;
|
|
}
|
|
}
|
|
|
|
|
|
// If handVelocityCheckInterval is set up, clear it.
|
|
// Also stop the sound injector and set currentIntensity to 0.
|
|
function maybeClearHandVelocityCheckIntervalAndStopSound() {
|
|
maybeClearHandVelocityCheck();
|
|
|
|
if (soundInjector) {
|
|
soundInjector.stop();
|
|
soundInjector = false;
|
|
}
|
|
|
|
currentIntensity = 0.0;
|
|
}
|
|
|
|
|
|
// Sets up an interval that'll check the avatar's hand's velocities.
|
|
// This is used for calculating the effect.
|
|
// If the user isn't in HMD, we'll never set up this interval.
|
|
var handVelocityCheckInterval = false;
|
|
var HAND_VELOCITY_CHECK_INTERVAL_MS = 10;
|
|
function maybeSetupHandVelocityCheckInterval() {
|
|
// `!HMD.active` clause isn't really necessary, just extra protection
|
|
if (handVelocityCheckInterval || !HMD.active) {
|
|
return;
|
|
}
|
|
|
|
handVelocityCheckInterval = Script.setInterval(handVelocityCheck, HAND_VELOCITY_CHECK_INTERVAL_MS);
|
|
}
|
|
|
|
|
|
// Checks the position of the user's hands to determine if they're above their head.
|
|
// If they are, sets up the hand velocity check interval (see above).
|
|
// If they aren't, clears that interval and stops the apprecation sound.
|
|
var handsAreAboveHead = false;
|
|
function handPositionCheck() {
|
|
var leftHandPosition = MyAvatar.getJointPosition("LeftHand");
|
|
var rightHandPosition = MyAvatar.getJointPosition("RightHand");
|
|
var headJointPosition = MyAvatar.getJointPosition("Head");
|
|
|
|
var headY = headJointPosition.y;
|
|
|
|
handsAreAboveHead = (rightHandPosition.y > headY && leftHandPosition.y > headY);
|
|
|
|
if (handsAreAboveHead) {
|
|
maybeSetupHandVelocityCheckInterval();
|
|
} else {
|
|
maybeClearHandVelocityCheck();
|
|
fadeOutAndStopSound();
|
|
}
|
|
}
|
|
|
|
|
|
// If handPositionCheckInterval is set up, clear it.
|
|
function maybeClearHandPositionCheckInterval() {
|
|
if (handPositionCheckInterval) {
|
|
Script.clearInterval(handPositionCheckInterval);
|
|
handPositionCheckInterval = false;
|
|
}
|
|
}
|
|
|
|
|
|
// If the app is enabled, sets up an interval that'll check if the avatar's hands are above their head.
|
|
var handPositionCheckInterval = false;
|
|
var HAND_POSITION_CHECK_INTERVAL_MS = 200;
|
|
function maybeSetupHandPositionCheckInterval() {
|
|
if (!appreciateEnabled || !HMD.active) {
|
|
return;
|
|
}
|
|
|
|
maybeClearHandPositionCheckInterval();
|
|
|
|
handPositionCheckInterval = Script.setInterval(handPositionCheck, HAND_POSITION_CHECK_INTERVAL_MS);
|
|
}
|
|
|
|
|
|
// If the interval that periodically lowers the apprecation volume is set up, clear it.
|
|
function maybeClearSlowAppreciationInterval() {
|
|
if (slowAppreciationInterval) {
|
|
Script.clearInterval(slowAppreciationInterval);
|
|
slowAppreciationInterval = false;
|
|
}
|
|
}
|
|
|
|
|
|
// Stop appreciating. Called when Appreciating from Desktop mode.
|
|
function stopAppreciating() {
|
|
maybeClearStopAppreciatingTimeout();
|
|
maybeClearSlowAppreciationInterval();
|
|
maybeClearUpdateIntensityEntityInterval();
|
|
MyAvatar.restoreAnimation();
|
|
currentAnimationFPS = INITIAL_ANIMATION_FPS;
|
|
currentlyPlayingFrame = 0;
|
|
currentAnimationTimestamp = 0;
|
|
}
|
|
|
|
|
|
// If the timeout that stops the user's apprecation is set up, clear it.
|
|
function maybeClearStopAppreciatingTimeout() {
|
|
if (stopAppreciatingTimeout) {
|
|
Script.clearTimeout(stopAppreciatingTimeout);
|
|
stopAppreciatingTimeout = false;
|
|
}
|
|
}
|
|
|
|
|
|
function calculateCurrentAnimationFPS(frameCount) {
|
|
var animationTimestampDeltaMS = Date.now() - currentAnimationTimestamp;
|
|
var frameDelta = animationTimestampDeltaMS / MS_PER_S * currentAnimationFPS;
|
|
|
|
currentlyPlayingFrame = (currentlyPlayingFrame + frameDelta) % frameCount;
|
|
|
|
currentAnimationFPS = currentIntensity * CHEERING_FPS_MAX + INITIAL_ANIMATION_FPS;
|
|
|
|
currentAnimationFPS = Math.min(currentAnimationFPS, CHEERING_FPS_MAX);
|
|
|
|
if (currentAnimation === clappingAnimation) {
|
|
currentAnimationFPS += CLAP_ANIMATION_FPS_BOOST;
|
|
}
|
|
}
|
|
|
|
|
|
// Called on an interval. Slows down the user's appreciation!
|
|
var VOLUME_STEP_DOWN_DESKTOP = 0.01; // Unitless, determined empirically
|
|
function slowAppreciation() {
|
|
currentIntensity -= VOLUME_STEP_DOWN_DESKTOP;
|
|
fadeIntensity(currentIntensity, INTENSITY_MAX_STEP_SIZE_DESKTOP);
|
|
|
|
currentAnimation = selectAnimation();
|
|
|
|
if (!currentAnimation) {
|
|
stopAppreciating();
|
|
return;
|
|
}
|
|
|
|
var frameCount = currentAnimation.animation.frames.length;
|
|
|
|
calculateCurrentAnimationFPS(frameCount);
|
|
|
|
MyAvatar.overrideAnimation(currentAnimation.url, currentAnimationFPS, true, currentlyPlayingFrame, frameCount);
|
|
|
|
currentAnimationTimestamp = Date.now();
|
|
}
|
|
|
|
|
|
// Selects the proper animation to use when Appreciating in Desktop mode.
|
|
function selectAnimation() {
|
|
if (shouldClap()) {
|
|
if (currentAnimation === whistlingAnimation) {
|
|
currentAnimationTimestamp = 0;
|
|
}
|
|
return clappingAnimation;
|
|
} else if (shouldWhistle()) {
|
|
if (currentAnimation === clappingAnimation) {
|
|
currentAnimationTimestamp = 0;
|
|
}
|
|
return whistlingAnimation;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
// Called when the Z key is pressed (and some other conditions are met).
|
|
// 1. (Maybe) clears old intervals
|
|
// 2. Steps up the global currentIntensity, then forces the effect/sound to fade/play immediately
|
|
// 3. Selects an animation to play based on various factors, then plays it
|
|
// - Stops appreciating if the selected animation is falsey
|
|
// 4. Sets up the "Slow Appreciation" interval which slows appreciation over time
|
|
// 5. Modifies the avatar's animation based on the current appreciation intensity
|
|
// - Since there's no way to modify animation FPS on-the-fly, we have to calculate
|
|
// where the animation should start based on where it was before changing FPS
|
|
// 6. Sets up the `updateIntensityEntity` interval if one isn't already setup
|
|
var INITIAL_ANIMATION_FPS = 7;
|
|
var SLOW_APPRECIATION_INTERVAL_MS = 100;
|
|
var CHEERING_FPS_MAX = 80;
|
|
var VOLUME_STEP_UP_DESKTOP = 0.035; // Unitless, determined empirically
|
|
var CLAP_ANIMATION_FPS_BOOST = 15;
|
|
var currentAnimation = false;
|
|
var currentAnimationFPS = INITIAL_ANIMATION_FPS;
|
|
var slowAppreciationInterval = false;
|
|
var currentlyPlayingFrame = 0;
|
|
var currentAnimationTimestamp;
|
|
function keyPressed() {
|
|
// Don't do anything if the animations aren't cached.
|
|
if (!whistlingAnimation || !clappingAnimation) {
|
|
return;
|
|
}
|
|
|
|
maybeClearSoundFadeInterval();
|
|
maybeClearStopAppreciatingTimeout();
|
|
|
|
currentIntensity += VOLUME_STEP_UP_DESKTOP;
|
|
fadeIntensity(currentIntensity, INTENSITY_MAX_STEP_SIZE_DESKTOP);
|
|
selectAndPlaySound();
|
|
|
|
currentAnimation = selectAnimation();
|
|
|
|
if (!currentAnimation) {
|
|
stopAppreciating();
|
|
return;
|
|
}
|
|
|
|
if (!slowAppreciationInterval) {
|
|
slowAppreciationInterval = Script.setInterval(slowAppreciation, SLOW_APPRECIATION_INTERVAL_MS);
|
|
}
|
|
|
|
var frameCount = currentAnimation.animation.frames.length;
|
|
|
|
if (currentAnimationTimestamp > 0) {
|
|
calculateCurrentAnimationFPS(frameCount);
|
|
} else {
|
|
currentlyPlayingFrame = 0;
|
|
}
|
|
|
|
MyAvatar.overrideAnimation(currentAnimation.url, currentAnimationFPS, true, currentlyPlayingFrame, frameCount);
|
|
currentAnimationTimestamp = Date.now();
|
|
|
|
if (!updateIntensityEntityInterval && showAppreciationEntity) {
|
|
updateIntensityEntityInterval = Script.setInterval(updateIntensityEntity, UPDATE_INTENSITY_ENTITY_INTERVAL_MS);
|
|
}
|
|
}
|
|
|
|
|
|
// The listener for all in-app keypresses. Listens for an unshifted, un-alted, un-ctrl'd
|
|
// "Z" keypress. Only listens when in Desktop mode. If the user is holding the key down,
|
|
// we make sure not to call the `keyPressed()` handler too often using the `desktopDebounceTimer`.
|
|
var desktopDebounceTimer = false;
|
|
var DESKTOP_DEBOUNCE_TIMEOUT_MS = 160;
|
|
function keyPressEvent(event) {
|
|
if (!appreciateEnabled) {
|
|
return;
|
|
}
|
|
|
|
if ((event.text.toUpperCase() === "Z") &&
|
|
!event.isShifted &&
|
|
!event.isMeta &&
|
|
!event.isControl &&
|
|
!event.isAlt &&
|
|
!HMD.active) {
|
|
|
|
if (event.isAutoRepeat) {
|
|
if (!desktopDebounceTimer) {
|
|
keyPressed();
|
|
|
|
desktopDebounceTimer = Script.setTimeout(function() {
|
|
desktopDebounceTimer = false;
|
|
}, DESKTOP_DEBOUNCE_TIMEOUT_MS);
|
|
}
|
|
} else {
|
|
keyPressed();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Sets up a timeout that will fade out the appreciation sound, then stop it.
|
|
var stopAppreciatingTimeout = false;
|
|
var STOP_APPRECIATING_TIMEOUT_MS = 1000;
|
|
function stopAppreciatingSoon() {
|
|
maybeClearStopAppreciatingTimeout();
|
|
|
|
if (currentIntensity > 0) {
|
|
stopAppreciatingTimeout = Script.setTimeout(fadeOutAndStopSound, STOP_APPRECIATING_TIMEOUT_MS);
|
|
}
|
|
}
|
|
|
|
|
|
// When the "Z" key is released, we want to stop appreciating a short time later.
|
|
function keyReleaseEvent(event) {
|
|
if (!appreciateEnabled) {
|
|
return;
|
|
}
|
|
|
|
if ((event.text.toUpperCase() === "Z") &&
|
|
!event.isAutoRepeat) {
|
|
stopAppreciatingSoon();
|
|
}
|
|
}
|
|
|
|
|
|
// Enables or disables the app's main functionality
|
|
var appreciateEnabled = Settings.getValue("appreciate/enabled", false);
|
|
var neverWhistleEnabled = Settings.getValue("appreciate/neverWhistle", false);
|
|
var showAppreciationEntity = Settings.getValue("appreciate/showAppreciationEntity", true);
|
|
var keyEventsWired = false;
|
|
function enableOrDisableAppreciate() {
|
|
maybeClearHandPositionCheckInterval();
|
|
maybeClearHandVelocityCheckIntervalAndStopSound();
|
|
|
|
if (appreciateEnabled) {
|
|
maybeSetupHandPositionCheckInterval();
|
|
|
|
if (!keyEventsWired && !HMD.active) {
|
|
Controller.keyPressEvent.connect(keyPressEvent);
|
|
Controller.keyReleaseEvent.connect(keyReleaseEvent);
|
|
keyEventsWired = true;
|
|
}
|
|
} else {
|
|
stopAppreciating();
|
|
|
|
if (keyEventsWired) {
|
|
Controller.keyPressEvent.disconnect(keyPressEvent);
|
|
Controller.keyReleaseEvent.disconnect(keyReleaseEvent);
|
|
keyEventsWired = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Handles incoming messages from the UI
|
|
// - "eventBridgeReady" - The App's UI will send this when it's ready to
|
|
// receive events over the Event Bridge
|
|
// - "appreciateSwitchClicked" - The App's UI will send this when the user
|
|
// clicks the main toggle switch in the top right of the app
|
|
// - "neverWhistleCheckboxClicked" - Sent when the user clicks the
|
|
// "Never Whistle" checkbox
|
|
// - "setEntityColor" - Sent when the user chooses a new Entity Color.
|
|
function onMessage(message) {
|
|
if (message.app !== "appreciate") {
|
|
return;
|
|
}
|
|
|
|
switch (message.method) {
|
|
case "eventBridgeReady":
|
|
ui.sendMessage({
|
|
method: "updateUI",
|
|
appreciateEnabled: appreciateEnabled,
|
|
neverWhistleEnabled: neverWhistleEnabled,
|
|
showAppreciationEntity: showAppreciationEntity,
|
|
isFirstRun: Settings.getValue("appreciate/firstRun", true),
|
|
entityColor: intensityEntityColorMax
|
|
});
|
|
break;
|
|
|
|
case "appreciateSwitchClicked":
|
|
Settings.setValue("appreciate/firstRun", false);
|
|
appreciateEnabled = message.appreciateEnabled;
|
|
Settings.setValue("appreciate/enabled", appreciateEnabled);
|
|
enableOrDisableAppreciate();
|
|
break;
|
|
|
|
case "neverWhistleCheckboxClicked":
|
|
neverWhistleEnabled = message.neverWhistle;
|
|
Settings.setValue("appreciate/neverWhistle", neverWhistleEnabled);
|
|
break;
|
|
|
|
case "showAppreciationEntityCheckboxClicked":
|
|
showAppreciationEntity = message.showAppreciationEntity;
|
|
Settings.setValue("appreciate/showAppreciationEntity", showAppreciationEntity);
|
|
break;
|
|
|
|
case "setEntityColor":
|
|
intensityEntityColorMax = message.entityColor;
|
|
Settings.setValue("appreciate/entityColor", JSON.stringify(intensityEntityColorMax));
|
|
break;
|
|
|
|
case "uninstall":
|
|
ScriptDiscoveryService.stopScript(Script.resolvePath(''), false);
|
|
break;
|
|
|
|
case "zKeyDown":
|
|
var pressEvent = {
|
|
"text": "Z",
|
|
"isShifted": false,
|
|
"isMeta": false,
|
|
"isControl": false,
|
|
"isAlt": false,
|
|
"isAutoRepeat": message.repeat
|
|
};
|
|
keyPressEvent(pressEvent);
|
|
break;
|
|
|
|
case "zKeyUp":
|
|
var releaseEvent = {
|
|
"text": "Z",
|
|
"isShifted": false,
|
|
"isMeta": false,
|
|
"isControl": false,
|
|
"isAlt": false,
|
|
"isAutoRepeat": false
|
|
};
|
|
keyReleaseEvent(releaseEvent);
|
|
break;
|
|
|
|
default:
|
|
console.log("Unhandled message from appreciate_ui.js: " + JSON.stringify(message));
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// Searches through all of your avatar entities and deletes any with the name
|
|
// that equals the one set when rezzing the Intensity Entity
|
|
function cleanupOldIntensityEntities() {
|
|
MyAvatar.getAvatarEntitiesVariant().forEach(function(avatarEntity) {
|
|
var name = Entities.getEntityProperties(avatarEntity.id, 'name').name;
|
|
if (name === INTENSITY_ENTITY_NAME && avatarEntity.id !== intensityEntity) {
|
|
Entities.deleteEntity(avatarEntity.id);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// Called when the script is stopped. STOP ALL THE THINGS!
|
|
function onScriptEnding() {
|
|
maybeClearHandPositionCheckInterval();
|
|
maybeClearHandVelocityCheckIntervalAndStopSound();
|
|
maybeClearSoundFadeInterval();
|
|
maybeClearVRDebounceTimer();
|
|
maybeClearUpdateIntensityEntityInterval();
|
|
cleanupOldIntensityEntities();
|
|
|
|
maybeClearStopAppreciatingTimeout();
|
|
stopAppreciating();
|
|
|
|
if (desktopDebounceTimer) {
|
|
Script.clearTimeout(desktopDebounceTimer);
|
|
desktopDebounceTimer = false;
|
|
}
|
|
|
|
if (keyEventsWired) {
|
|
Controller.keyPressEvent.disconnect(keyPressEvent);
|
|
Controller.keyReleaseEvent.disconnect(keyReleaseEvent);
|
|
keyEventsWired = false;
|
|
}
|
|
|
|
if (intensityEntity) {
|
|
Entities.deleteEntity(intensityEntity);
|
|
intensityEntity = false;
|
|
intensityEntityMaterial = Uuid.NULL;
|
|
}
|
|
|
|
HMD.displayModeChanged.disconnect(enableOrDisableAppreciate);
|
|
}
|
|
|
|
// When called, this function will stop the versions of this script that are
|
|
// baked into the client installation IF there's another version of the script
|
|
// running that ISN'T the baked version.
|
|
function maybeStopBakedScriptVersions() {
|
|
var THIS_SCRIPT_FILENAME = "appreciate_app.js";
|
|
var RELATIVE_PATH_TO_BAKED_SCRIPT = "system/experiences/appreciate/appResources/appData/" + THIS_SCRIPT_FILENAME;
|
|
var bakedLocalScriptPaths = [];
|
|
var alsoRunningNonBakedVersion = false;
|
|
|
|
var runningScripts = ScriptDiscoveryService.getRunning();
|
|
runningScripts.forEach(function(scriptObject) {
|
|
if (scriptObject.local && scriptObject.url.indexOf(RELATIVE_PATH_TO_BAKED_SCRIPT) > -1) {
|
|
bakedLocalScriptPaths.push(scriptObject.path);
|
|
}
|
|
|
|
if (scriptObject.name === THIS_SCRIPT_FILENAME && scriptObject.url.indexOf(RELATIVE_PATH_TO_BAKED_SCRIPT) === -1) {
|
|
alsoRunningNonBakedVersion = true;
|
|
}
|
|
});
|
|
|
|
if (alsoRunningNonBakedVersion && bakedLocalScriptPaths.length > 0) {
|
|
for (var i = 0; i < bakedLocalScriptPaths.length; i++) {
|
|
ScriptDiscoveryService.stopScript(bakedLocalScriptPaths[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Called when the script starts up
|
|
var BUTTON_NAME = "APPRECIATE";
|
|
var APP_UI_URL = Script.resolvePath('resources/appreciate_ui.html');
|
|
var CLEANUP_INTENSITY_ENTITIES_STARTUP_DELAY_MS = 5000;
|
|
var AppUI = Script.require('appUi');
|
|
var ui;
|
|
function startup() {
|
|
ui = new AppUI({
|
|
buttonName: BUTTON_NAME,
|
|
home: APP_UI_URL,
|
|
// clap by Rena from the Noun Project
|
|
graphicsDirectory: Script.resolvePath("./resources/images/icons/"),
|
|
onOpened: onOpened,
|
|
onMessage: onMessage,
|
|
onClosed: onClosed
|
|
});
|
|
|
|
cleanupOldIntensityEntities();
|
|
// We need this because sometimes avatar entities load after this script does.
|
|
Script.setTimeout(cleanupOldIntensityEntities, CLEANUP_INTENSITY_ENTITIES_STARTUP_DELAY_MS);
|
|
enableOrDisableAppreciate();
|
|
getSounds();
|
|
getAnimations();
|
|
HMD.displayModeChanged.connect(enableOrDisableAppreciate);
|
|
maybeStopBakedScriptVersions();
|
|
|
|
if (appreciateEnabled) {
|
|
ui.buttonActive(true);
|
|
} else {
|
|
ui.buttonActive(false);
|
|
}
|
|
}
|
|
|
|
|
|
Script.scriptEnding.connect(onScriptEnding);
|
|
startup();
|
|
})();
|
|
|