// // question_app.js // // Created by Rebecca Stankus on 02/21/19 // Copyright 2019 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 // /* global Audio, Camera, Controller, Entities, HMD, Messages, MyAvatar, Quat, Script, Settings, SoundCache, Tablet, Users, Vec3, Window */ (function() { // ************************************* // START UTILITY FUNCTIONS // ************************************* var PickRayController = Script.require('./resources/modules/pickRayController.js?' + Date.now()); var pickRayController = new PickRayController(); /* PLAY A SOUND: Plays the specified sound at the specified position using the volume and playback modes requested. */ var injector; function playSound(sound, volume, position, localOnly, loop){ if (sound.downloaded) { if (injector) { injector.stop(); injector = null; } injector = Audio.playSound(sound, { position: position, volume: volume, localOnly: localOnly, loop: loop }); } } // ************************************* // END UTILITY FUNCTIONS // ************************************* /* CREATE A QUESTION MARK: Checks that question Mark does not already exist, then calculates position above avatar's head and creates a question mark entity there */ var GROWTH_INTERVAL_MS = 2000; // takes 4 min 38 sec to reach height of 0.8 from 0.2 at ratio:1.005, interval: 1000 var MAX_HEIGHT_M = 0.8; var GROWTH_RATIO = 1.005; var QUESTION_MARK_PROPERTY_NAME = "Question App Mark"; var QUESTION_MARK_START_DIMENSIONS_M = { x: 0.1, y: 0.2, z: 0.02 }; var HALF = 0.5; var Y_OFFSET_HEAD_TOP_TO_ENTITY_M = 0.1; var questionMark; var changingInterval; var parentJointIndex; var lastDimensions = QUESTION_MARK_START_DIMENSIONS_M; function createQuestionMark() { if (questionMark) { return; } parentJointIndex = MyAvatar.getJointIndex("Hips"); if (parentJointIndex === -1) { parentJointIndex = 0; } var entitySpawnPosition = MyAvatar.position; entitySpawnPosition.y = entitySpawnPosition.y + HALF * MyAvatar.getHeight() + HALF * QUESTION_MARK_START_DIMENSIONS_M.y + Y_OFFSET_HEAD_TOP_TO_ENTITY_M; var questionMarkProperties = { name: QUESTION_MARK_PROPERTY_NAME, type: "Model", modelURL: Script.resolvePath("resources/models/questionMark.fbx"), lifetime: 360000, parentID: MyAvatar.sessionUUID, parentJointIndex: parentJointIndex, localRotation: Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }), position: entitySpawnPosition, dimensions: QUESTION_MARK_START_DIMENSIONS_M, grab: { grabbable: false }, collisionless: true }; questionMark = Entities.addEntity(questionMarkProperties, 'avatar'); changingInterval = Script.setInterval(function() { if (changingInterval) { var questionMarkDimensions = Entities.getEntityProperties(questionMark, 'dimensions').dimensions; if (questionMarkDimensions.y < MAX_HEIGHT_M) { questionMarkDimensions = Vec3.multiply(questionMarkDimensions, GROWTH_RATIO); var entitySpawnPosition = MyAvatar.position; entitySpawnPosition.y = entitySpawnPosition.y + HALF * MyAvatar.getHeight() + HALF * questionMarkDimensions.y + Y_OFFSET_HEAD_TOP_TO_ENTITY_M; Entities.editEntity(questionMark, { dimensions: questionMarkDimensions, position: entitySpawnPosition }); lastDimensions = questionMarkDimensions; } else { Script.clearInterval(changingInterval); changingInterval = null; } } }, GROWTH_INTERVAL_MS); } /* ON CLICKING APP BUTTON: (on the toolbar or tablet) if we are opening the app, play a sound and get the question mark. If we are closing the app, remove the question mark and play a different sound */ var OPEN_SOUND = SoundCache.getSound(Script.resolvePath('resources/sounds/open.mp3')); var OPEN_SOUND_VOLUME = 0.2; var CLOSE_SOUND = SoundCache.getSound(Script.resolvePath('resources/sounds/close.mp3')); var CLOSE_SOUND_VOLUME = 0.3; var QUESTION_CHANNEL = "QuestionChannel"; var messagesReceivedConnected; var scaleChangedConnected; var skeletonChangedConnected; function onClicked() { if (questionMark) { cleanUp(); disconnectSignalHandlers(); playSound(CLOSE_SOUND, CLOSE_SOUND_VOLUME, MyAvatar.position, true, false); } else { button.editProperties({ isActive: true }); playSound(OPEN_SOUND, OPEN_SOUND_VOLUME, MyAvatar.position, true, false); createQuestionMark(); if (!messagesReceivedConnected) { Messages.messageReceived.connect(checkMessage); messagesReceivedConnected = true; } if (!scaleChangedConnected) { MyAvatar.scaleChanged.connect(avatarScaleChanged); scaleChangedConnected = true; } if (!skeletonChangedConnected) { MyAvatar.skeletonModelURLChanged.connect(skeletonChanged); skeletonChangedConnected = true; } } } /* Handles avatar being selected */ pickRayController .registerEventHandler(selectAvatar) .setType("avatar") .setMapName("hifi_question") .create(); /* SELECT AVATAR: When an admin clicks/triggers on an avatar, they are selected to speak next and their UUID is sent out via message */ function selectAvatar(uuid, intersection) { Messages.sendMessage(QUESTION_CHANNEL, JSON.stringify({ UUID: uuid })); } /* DISCONNECT SIGNAL HANDLERS: disconnect any open signal handlers */ function disconnectSignalHandlers() { if (messagesReceivedConnected) { Messages.messageReceived.disconnect(checkMessage); messagesReceivedConnected = false; } if (scaleChangedConnected) { MyAvatar.scaleChanged.disconnect(avatarScaleChanged); scaleChangedConnected = false; } if (skeletonChangedConnected) { MyAvatar.skeletonModelURLChanged.disconnect(skeletonChanged); skeletonChangedConnected = false; } } /* AVATAR SELECTED: When a user is selected, the question mark disappears and a sound plays for the selected user as their app toggles off. */ var SELECTED_SOUND = SoundCache.getSound(Script.resolvePath('resources/sounds/selected.mp3')); var SELECTED_SOUND_VOLUME = 0.3; function avatarSelected() { if (questionMark) { cleanUp(); disconnectSignalHandlers(); playSound(SELECTED_SOUND, SELECTED_SOUND_VOLUME, MyAvatar.position, true, false); } } /* ON STOPPING THE SCRIPT: Disconnect signals and clean up */ function appEnding() { cleanUp(); Messages.unsubscribe(QUESTION_CHANNEL); Users.canKickChanged.disconnect(adminStatusCheck); button.clicked.disconnect(onClicked); tablet.removeButton(button); pickRayController.destroy(); Window.domainChanged.disconnect(domainChanged); } /* REMOVE QUESTION MARK: Remove referenced question mark if it exists and any other strays */ function removeQuestionMarkEntities() { if (questionMark) { Entities.deleteEntity(questionMark); questionMark = null; } MyAvatar.getAvatarEntitiesVariant().forEach(function(avatarEntity) { var name = Entities.getEntityProperties(avatarEntity.id, 'name').name; if (name === QUESTION_MARK_PROPERTY_NAME) { Entities.deleteEntity(avatarEntity.id); questionMark = null; } }); } /* CLEANUP: Remove question mark, search for any unreferenced question marks to clean up */ function cleanUp() { removeQuestionMarkEntities(); if (changingInterval) { Script.clearInterval(changingInterval); changingInterval = null; } if (injector) { injector.stop(); injector = null; } button.editProperties({ isActive: false }); } /* WHEN USER DOMAIN CHANGES: Close app to remove question mark when leaving the domain */ var WAIT_TO_CLEAN_UP_MS = 2000; function domainChanged() { Script.setTimeout(function() { cleanUp(); }, WAIT_TO_CLEAN_UP_MS); } /* CHECK MESSAGE RECEIVED: If uuid that was broadcast matches, this user has been selected to speak next */ function checkMessage(channel, message, sender) { if (channel === QUESTION_CHANNEL) { try { message = JSON.parse(message); } catch (error) { print("Couldn't parse message: " + error); return; } if (message.UUID === MyAvatar.sessionUUID && questionMark) { avatarSelected(); } } } /* ADMIN STATUS CHANGED: User permissions have changed, set adminStatus */ var adminStatus; function adminStatusCheck() { adminStatus = Users.getCanKick() ? true : false; if (adminStatus) { pickRayController.enable(); } else { pickRayController.disable(); } } /* AVATAR SCALE CHANGED: Reset question mark entity back to appropriate size */ function avatarScaleChanged() { var questionMarkDimensions = Entities.getEntityProperties(questionMark, 'dimensions').dimensions; var entitySpawnPosition = MyAvatar.position; entitySpawnPosition.y = entitySpawnPosition.y + HALF * MyAvatar.getHeight() + HALF * questionMarkDimensions.y + Y_OFFSET_HEAD_TOP_TO_ENTITY_M; Entities.editEntity(questionMark, { dimensions: lastDimensions, position: entitySpawnPosition }); } /* AVATAR SKELETON CHANGED: Close the app if it is open */ function skeletonChanged() { if (changingInterval) { Script.clearInterval(changingInterval); changingInterval = null; } if (injector) { injector.stop(); injector = null; } button.editProperties({ isActive: false }); questionMark = null; disconnectSignalHandlers(); playSound(CLOSE_SOUND, CLOSE_SOUND_VOLUME, MyAvatar.position, true, false); } Messages.subscribe(QUESTION_CHANNEL); var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'); var button = tablet.addButton({ text: 'QUESTION', icon: Script.resolvePath('resources/icons/question-i.png'), activeIcon: Script.resolvePath('resources/icons/question-a.png'), sortOrder: 1 }); adminStatusCheck(); Users.canKickChanged.connect(adminStatusCheck); button.clicked.connect(onClicked); Window.domainChanged.connect(domainChanged); Script.scriptEnding.connect(appEnding); }());