mirror of
https://github.com/overte-org/overte.git
synced 2025-04-07 16:13:06 +02:00
Replace 3d Overlays by Local Entities This is for the system files. Another PR will follow for the developer scripts.
1019 lines
34 KiB
JavaScript
1019 lines
34 KiB
JavaScript
"use strict";
|
|
//
|
|
// Chat.js
|
|
//
|
|
// By Don Hopkins (dhopkins@donhopkins.com) on May 5th, 2017
|
|
// Copyright 2017 High Fidelity, Inc.
|
|
// Copyright 2023 Overte e.V.
|
|
//
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
//
|
|
|
|
(function() {
|
|
|
|
var webPageURL = Script.resolvePath("html/ChatPage.html"); // URL of tablet web page.
|
|
var randomizeWebPageURL = true; // Set to true for debugging.
|
|
var lastWebPageURL = ""; // Last random URL of tablet web page.
|
|
var onChatPage = false; // True when chat web page is opened.
|
|
var webHandlerConnected = false; // True when the web handler has been connected.
|
|
var channelName = "Chat"; // Unique name for channel that we listen to.
|
|
var tabletButtonName = "CHAT"; // Tablet button label.
|
|
var tabletButtonIcon = "icons/tablet-icons/menu-i.svg"; // Icon for chat button.
|
|
var tabletButtonActiveIcon = "icons/tablet-icons/menu-a.svg"; // Active icon for chat button.
|
|
var tabletButton = null; // The button we create in the tablet.
|
|
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); // The awesome tablet.
|
|
var chatLog = []; // Array of chat messages in the form of [avatarID, displayName, message, data].
|
|
var avatarIdentifiers = {}; // Map of avatar ids to dict of identifierParams.
|
|
var speechBubbleShowing = false; // Is the speech bubble visible?
|
|
var speechBubbleMessage = null; // The message shown in the speech bubble.
|
|
var speechBubbleData = null; // The data of the speech bubble message.
|
|
var speechBubbleTextID = null; // The id of the speech bubble local text entity.
|
|
var speechBubbleTimer = null; // The timer to pop down the speech bubble.
|
|
var speechBubbleParams = null; // The params used to create or edit the speech bubble.
|
|
|
|
// Persistent variables saved in the Settings.
|
|
var chatName = ''; // The user's name shown in chat.
|
|
var chatLogMaxSize = 100; // The maximum number of chat messages we remember.
|
|
var sendTyping = true; // Send typing begin and end notification.
|
|
var identifyAvatarDuration = 10; // How long to leave the avatar identity line up, in seconds.
|
|
var identifyAvatarLineColor = { red: 0, green: 255, blue: 0 }; // The color of the avatar identity line.
|
|
var identifyAvatarMyJointName = 'Head'; // My bone from which to draw the avatar identity line.
|
|
var identifyAvatarYourJointName = 'Head'; // Your bone to which to draw the avatar identity line.
|
|
var speechBubbleDuration = 10; // How long to leave the speech bubble up, in seconds.
|
|
var speechBubbleTextColor = {red: 255, green: 255, blue: 255}; // The text color of the speech bubble.
|
|
var speechBubbleBackgroundColor = {red: 0, green: 0, blue: 0}; // The background color of the speech bubble.
|
|
var speechBubbleOffset = {x: 0, y: 0.3, z: 0.0}; // The offset from the joint to whic the speech bubble is attached.
|
|
var speechBubbleJointName = 'Head'; // The name of the joint to which the speech bubble is attached.
|
|
var speechBubbleLineHeight = 0.05; // The height of a line of text in the speech bubble.
|
|
var SPEECH_BUBBLE_MAX_WIDTH = 1; // meters
|
|
|
|
var textSizeOverlay = Entities.addEntity({
|
|
"type": "Text",
|
|
"position": MyAvatar.position,
|
|
"lineHeight": speechBubbleLineHeight,
|
|
"leftMargin": 0,
|
|
"topMargin": 0,
|
|
"rightMargin": 0,
|
|
"bottomMargin": 0,
|
|
"unlit": true,
|
|
"alignment": "center",
|
|
"ignorePickIntersection": true,
|
|
"visible": false
|
|
}, "local");
|
|
|
|
// Load the persistent variables from the Settings, with defaults.
|
|
function loadSettings() {
|
|
chatName = Settings.getValue('Chat_chatName', MyAvatar.displayName);
|
|
if (!chatName) {
|
|
chatName = randomAvatarName();
|
|
}
|
|
chatLogMaxSize = Settings.getValue('Chat_chatLogMaxSize', 100);
|
|
sendTyping = Settings.getValue('Chat_sendTyping', true);
|
|
identifyAvatarDuration = Settings.getValue('Chat_identifyAvatarDuration', 10);
|
|
identifyAvatarLineColor = Settings.getValue('Chat_identifyAvatarLineColor', { red: 0, green: 255, blue: 0 });
|
|
identifyAvatarMyJointName = Settings.getValue('Chat_identifyAvatarMyJointName', 'Head');
|
|
identifyAvatarYourJointName = Settings.getValue('Chat_identifyAvatarYourJointName', 'Head');
|
|
speechBubbleDuration = Settings.getValue('Chat_speechBubbleDuration', 10);
|
|
speechBubbleTextColor = Settings.getValue('Chat_speechBubbleTextColor', {red: 255, green: 255, blue: 255});
|
|
speechBubbleBackgroundColor = Settings.getValue('Chat_speechBubbleBackgroundColor', {red: 0, green: 0, blue: 0});
|
|
speechBubbleOffset = Settings.getValue('Chat_speechBubbleOffset', {x: 0.0, y: 0.3, z:0.0});
|
|
speechBubbleJointName = Settings.getValue('Chat_speechBubbleJointName', 'Head');
|
|
speechBubbleLineHeight = Settings.getValue('Chat_speechBubbleLineHeight', 0.05);
|
|
Entities.editEntity(textSizeOverlay, {
|
|
"lineHeight": speechBubbleLineHeight
|
|
});
|
|
|
|
saveSettings();
|
|
}
|
|
|
|
// Save the persistent variables to the Settings.
|
|
function saveSettings() {
|
|
Settings.setValue('Chat_chatName', chatName);
|
|
Settings.setValue('Chat_chatLogMaxSize', chatLogMaxSize);
|
|
Settings.setValue('Chat_sendTyping', sendTyping);
|
|
Settings.setValue('Chat_identifyAvatarDuration', identifyAvatarDuration);
|
|
Settings.setValue('Chat_identifyAvatarLineColor', identifyAvatarLineColor);
|
|
Settings.setValue('Chat_identifyAvatarMyJointName', identifyAvatarMyJointName);
|
|
Settings.setValue('Chat_identifyAvatarYourJointName', identifyAvatarYourJointName);
|
|
Settings.setValue('Chat_speechBubbleDuration', speechBubbleDuration);
|
|
Settings.setValue('Chat_speechBubbleTextColor', speechBubbleTextColor);
|
|
Settings.setValue('Chat_speechBubbleBackgroundColor', speechBubbleBackgroundColor);
|
|
Settings.setValue('Chat_speechBubbleOffset', speechBubbleOffset);
|
|
Settings.setValue('Chat_speechBubbleJointName', speechBubbleJointName);
|
|
Settings.setValue('Chat_speechBubbleLineHeight', speechBubbleLineHeight);
|
|
}
|
|
|
|
// Reset the Settings and persistent variables to the defaults.
|
|
function resetSettings() {
|
|
Settings.setValue('Chat_chatName', null);
|
|
Settings.setValue('Chat_chatLogMaxSize', null);
|
|
Settings.setValue('Chat_sendTyping', null);
|
|
Settings.setValue('Chat_identifyAvatarDuration', null);
|
|
Settings.setValue('Chat_identifyAvatarLineColor', null);
|
|
Settings.setValue('Chat_identifyAvatarMyJointName', null);
|
|
Settings.setValue('Chat_identifyAvatarYourJointName', null);
|
|
Settings.setValue('Chat_speechBubbleDuration', null);
|
|
Settings.setValue('Chat_speechBubbleTextColor', null);
|
|
Settings.setValue('Chat_speechBubbleBackgroundColor', null);
|
|
Settings.setValue('Chat_speechBubbleOffset', null);
|
|
Settings.setValue('Chat_speechBubbleJointName', null);
|
|
Settings.setValue('Chat_speechBubbleLineHeight', null);
|
|
|
|
loadSettings();
|
|
}
|
|
|
|
// Update anything that might depend on the settings.
|
|
function updateSettings() {
|
|
updateSpeechBubble();
|
|
trimChatLog();
|
|
updateChatPage();
|
|
}
|
|
|
|
// Trim the chat log so it is no longer than chatLogMaxSize lines.
|
|
function trimChatLog() {
|
|
if (chatLog.length > chatLogMaxSize) {
|
|
chatLog.splice(0, chatLogMaxSize - chatLog.length);
|
|
}
|
|
}
|
|
|
|
// Clear the local chat log.
|
|
function clearChatLog() {
|
|
//print("clearChatLog");
|
|
chatLog = [];
|
|
updateChatPage();
|
|
}
|
|
|
|
// We got a chat message from the channel.
|
|
// Trim the chat log, save the latest message in the chat log,
|
|
// and show the message on the tablet, if the chat page is showing.
|
|
function handleTransmitChatMessage(avatarID, displayName, message, data) {
|
|
//print("receiveChat", "avatarID", avatarID, "displayName", displayName, "message", message, "data", data);
|
|
|
|
trimChatLog();
|
|
chatLog.push([avatarID, displayName, message, data]);
|
|
|
|
if (onChatPage) {
|
|
tablet.emitScriptEvent(
|
|
JSON.stringify({
|
|
type: "ReceiveChatMessage",
|
|
avatarID: avatarID,
|
|
displayName: displayName,
|
|
message: message,
|
|
data: data
|
|
}));
|
|
}
|
|
}
|
|
|
|
// Trim the chat log, save the latest log message in the chat log,
|
|
// and show the message on the tablet, if the chat page is showing.
|
|
function logMessage(message, data) {
|
|
//print("logMessage", message, data);
|
|
|
|
trimChatLog();
|
|
chatLog.push([null, null, message, data]);
|
|
|
|
if (onChatPage) {
|
|
tablet.emitScriptEvent(
|
|
JSON.stringify({
|
|
type: "LogMessage",
|
|
message: message,
|
|
data: data
|
|
}));
|
|
}
|
|
}
|
|
|
|
// An empty chat message was entered.
|
|
// Hide our speech bubble.
|
|
function emptyChatMessage(data) {
|
|
popDownSpeechBubble();
|
|
}
|
|
|
|
// Notification that we typed a keystroke.
|
|
function type() {
|
|
//print("type");
|
|
}
|
|
|
|
// Notification that we began typing.
|
|
// Notify everyone that we started typing.
|
|
function beginTyping() {
|
|
//print("beginTyping");
|
|
if (!sendTyping) {
|
|
return;
|
|
}
|
|
|
|
Messages.sendMessage(
|
|
channelName,
|
|
JSON.stringify({
|
|
type: 'AvatarBeginTyping',
|
|
avatarID: MyAvatar.sessionUUID,
|
|
displayName: chatName
|
|
}));
|
|
}
|
|
|
|
// Notification that somebody started typing.
|
|
function handleAvatarBeginTyping(avatarID, displayName) {
|
|
//print("handleAvatarBeginTyping:", "avatarID", avatarID, displayName);
|
|
}
|
|
|
|
// Notification that we stopped typing.
|
|
// Notify everyone that we stopped typing.
|
|
function endTyping() {
|
|
//print("endTyping");
|
|
if (!sendTyping) {
|
|
return;
|
|
}
|
|
|
|
Messages.sendMessage(
|
|
channelName,
|
|
JSON.stringify({
|
|
type: 'AvatarEndTyping',
|
|
avatarID: MyAvatar.sessionUUID,
|
|
displayName: chatName
|
|
}));
|
|
}
|
|
|
|
// Notification that somebody stopped typing.
|
|
function handleAvatarEndTyping(avatarID, displayName) {
|
|
//print("handleAvatarEndTyping:", "avatarID", avatarID, displayName);
|
|
}
|
|
|
|
// Identify an avatar by drawing a line from our head to their head.
|
|
// If the avatar is our own, then just draw a line up into the sky.
|
|
function identifyAvatar(yourAvatarID) {
|
|
//print("identifyAvatar", yourAvatarID);
|
|
|
|
unidentifyAvatars();
|
|
|
|
var myAvatarID = MyAvatar.sessionUUID;
|
|
var myJointIndex = MyAvatar.getJointIndex(identifyAvatarMyJointName);
|
|
var myJointRotation =
|
|
Quat.multiply(
|
|
MyAvatar.orientation,
|
|
MyAvatar.getAbsoluteJointRotationInObjectFrame(myJointIndex));
|
|
var myJointPosition =
|
|
Vec3.sum(
|
|
MyAvatar.position,
|
|
Vec3.multiplyQbyV(
|
|
MyAvatar.orientation,
|
|
MyAvatar.getAbsoluteJointTranslationInObjectFrame(myJointIndex)));
|
|
|
|
var yourJointIndex = -1;
|
|
var yourJointPosition;
|
|
|
|
if (yourAvatarID == myAvatarID) {
|
|
|
|
// You pointed at your own name, so draw a line up from your head.
|
|
|
|
yourJointPosition = {
|
|
x: myJointPosition.x,
|
|
y: myJointPosition.y + 1000.0,
|
|
z: myJointPosition.z
|
|
};
|
|
|
|
} else {
|
|
|
|
// You pointed at somebody else's name, so draw a line from your head to their head.
|
|
|
|
var yourAvatar = AvatarList.getAvatar(yourAvatarID);
|
|
if (!yourAvatar) {
|
|
return;
|
|
}
|
|
|
|
yourJointIndex = yourAvatar.getJointIndex(identifyAvatarMyJointName)
|
|
|
|
var yourJointRotation =
|
|
Quat.multiply(
|
|
yourAvatar.orientation,
|
|
yourAvatar.getAbsoluteJointRotationInObjectFrame(yourJointIndex));
|
|
yourJointPosition =
|
|
Vec3.sum(
|
|
yourAvatar.position,
|
|
Vec3.multiplyQbyV(
|
|
yourAvatar.orientation,
|
|
yourAvatar.getAbsoluteJointTranslationInObjectFrame(yourJointIndex)));
|
|
|
|
}
|
|
|
|
var identifierParams = {
|
|
"type": "PolyLine",
|
|
"parentID": myAvatarID,
|
|
"parentJointIndex": myJointIndex,
|
|
"lifetime": identifyAvatarDuration,
|
|
"linePoints": [
|
|
myJointPosition,
|
|
yourJointPosition
|
|
],
|
|
"strokeWidths": [ 0.02, 0.02],
|
|
//endParentID: yourAvatarID, //Currently doesn't work. Never implemented.
|
|
//endParentJointIndex: yourJointIndex, //Currently doesn't work. Never implemented.
|
|
"color": identifyAvatarLineColor
|
|
};
|
|
|
|
avatarIdentifiers[yourAvatarID] = identifierParams;
|
|
|
|
identifierParams.lineID = Entities.addEntity(identifierParams, "local");
|
|
|
|
//print("ADDOVERLAY lineID", lineID, "myJointPosition", JSON.stringify(myJointPosition), "yourJointPosition", JSON.stringify(yourJointPosition), "lineData", JSON.stringify(lineData));
|
|
|
|
identifierParams.timer =
|
|
Script.setTimeout(function() {
|
|
//print("DELETEOVERLAY lineID");
|
|
unidentifyAvatar(yourAvatarID);
|
|
}, identifyAvatarDuration * 1000);
|
|
|
|
}
|
|
|
|
// Stop identifying an avatar.
|
|
function unidentifyAvatar(yourAvatarID) {
|
|
//print("unidentifyAvatar", yourAvatarID);
|
|
|
|
var identifierParams = avatarIdentifiers[yourAvatarID];
|
|
if (!identifierParams) {
|
|
return;
|
|
}
|
|
|
|
if (identifierParams.timer) {
|
|
Script.clearTimeout(identifierParams.timer);
|
|
}
|
|
|
|
if (identifierParams.lineID) {
|
|
Entities.deleteEntity(identifierParams.lineID);
|
|
}
|
|
|
|
delete avatarIdentifiers[yourAvatarID];
|
|
}
|
|
|
|
// Stop identifying all avatars.
|
|
function unidentifyAvatars() {
|
|
var ids = [];
|
|
|
|
for (var avatarID in avatarIdentifiers) {
|
|
ids.push(avatarID);
|
|
}
|
|
|
|
for (var i = 0, n = ids.length; i < n; i++) {
|
|
var avatarID = ids[i];
|
|
unidentifyAvatar(avatarID);
|
|
}
|
|
|
|
}
|
|
|
|
// Turn to face another avatar.
|
|
function faceAvatar(yourAvatarID, displayName) {
|
|
//print("faceAvatar:", yourAvatarID, displayName);
|
|
|
|
var myAvatarID = MyAvatar.sessionUUID;
|
|
if (yourAvatarID == myAvatarID) {
|
|
// You clicked on yourself.
|
|
return;
|
|
}
|
|
|
|
var yourAvatar = AvatarList.getAvatar(yourAvatarID);
|
|
if (!yourAvatar) {
|
|
logMessage(displayName + ' is not here!', null);
|
|
return;
|
|
}
|
|
|
|
// Project avatar positions to the floor and get the direction between those points,
|
|
// then face my avatar towards your avatar.
|
|
var yourPosition = yourAvatar.position;
|
|
yourPosition.y = 0;
|
|
var myPosition = MyAvatar.position;
|
|
myPosition.y = 0;
|
|
var myOrientation = Quat.lookAtSimple(myPosition, yourPosition);
|
|
MyAvatar.orientation = myOrientation;
|
|
}
|
|
|
|
// Make a hopefully unique random anonymous avatar name.
|
|
function randomAvatarName() {
|
|
return 'Anon_' + Math.floor(Math.random() * 1000000);
|
|
}
|
|
|
|
// Change the avatar size to bigger.
|
|
function biggerSize() {
|
|
//print("biggerSize");
|
|
logMessage("Increasing avatar size", null);
|
|
MyAvatar.increaseSize();
|
|
}
|
|
|
|
// Change the avatar size to smaller.
|
|
function smallerSize() {
|
|
//print("smallerSize");
|
|
logMessage("Decreasing avatar size", null);
|
|
MyAvatar.decreaseSize();
|
|
}
|
|
|
|
// Set the avatar size to normal.
|
|
function normalSize() {
|
|
//print("normalSize");
|
|
logMessage("Resetting avatar size to normal!", null);
|
|
MyAvatar.resetSize();
|
|
}
|
|
|
|
// Send out a "Who" message, including our avatarID as myAvatarID,
|
|
// which will be sent in the response, so we can tell the reply
|
|
// is to our request.
|
|
function transmitWho() {
|
|
//print("transmitWho");
|
|
logMessage("Who is here?", null);
|
|
Messages.sendMessage(
|
|
channelName,
|
|
JSON.stringify({
|
|
type: 'Who',
|
|
myAvatarID: MyAvatar.sessionUUID
|
|
}));
|
|
}
|
|
|
|
// Send a reply to a "Who" message, with a friendly message,
|
|
// our avatarID and our displayName. myAvatarID is the id
|
|
// of the avatar who send the Who message, to whom we're
|
|
// responding.
|
|
function handleWho(myAvatarID) {
|
|
var avatarID = MyAvatar.sessionUUID;
|
|
if (myAvatarID == avatarID) {
|
|
// Don't reply to myself.
|
|
return;
|
|
}
|
|
|
|
var message = "I'm here!";
|
|
var data = {};
|
|
|
|
Messages.sendMessage(
|
|
channelName,
|
|
JSON.stringify({
|
|
type: 'ReplyWho',
|
|
myAvatarID: myAvatarID,
|
|
avatarID: avatarID,
|
|
displayName: chatName,
|
|
message: message,
|
|
data: data
|
|
}));
|
|
}
|
|
|
|
// Receive the reply to a "Who" message. Ignore it unless we were the one
|
|
// who sent it out (if myAvatarIS is our avatar's id).
|
|
function handleReplyWho(myAvatarID, avatarID, displayName, message, data) {
|
|
if (myAvatarID != MyAvatar.sessionUUID) {
|
|
return;
|
|
}
|
|
|
|
handleTransmitChatMessage(avatarID, displayName, message, data);
|
|
}
|
|
|
|
// Handle input form the user, possibly multiple lines separated by newlines.
|
|
// Each line may be a chat command starting with "/", or a chat message.
|
|
function handleChatMessage(message, data) {
|
|
|
|
var messageLines = message.trim().split('\n');
|
|
|
|
for (var i = 0, n = messageLines.length; i < n; i++) {
|
|
var messageLine = messageLines[i];
|
|
|
|
if (messageLine.substr(0, 1) == '/') {
|
|
handleChatCommand(messageLine, data);
|
|
} else {
|
|
transmitChatMessage(messageLine, data);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Handle a chat command prefixed by "/".
|
|
function handleChatCommand(message, data) {
|
|
|
|
var commandLine = message.substr(1);
|
|
var tokens = commandLine.trim().split(' ');
|
|
var command = tokens[0];
|
|
var rest = commandLine.substr(command.length + 1).trim();
|
|
|
|
//print("commandLine", commandLine, "command", command, "tokens", tokens, "rest", rest);
|
|
|
|
switch (command) {
|
|
|
|
case '?':
|
|
case 'help':
|
|
logMessage('Type "/?" or "/help" for help', null);
|
|
logMessage('Type "/name <name>" to set your chat name, or "/name" to use your display name. If your display name is not defined, a random name will be used.', null);
|
|
logMessage('Type "/close" to close your overhead chat message.', null);
|
|
logMessage('Type "/say <something>" to display a new message.', null);
|
|
logMessage('Type "/clear" to clear your chat log.', null);
|
|
logMessage('Type "/who" to ask who is in the chat session.', null);
|
|
logMessage('Type "/bigger", "/smaller" or "/normal" to change your avatar size.', null);
|
|
break;
|
|
|
|
case 'name':
|
|
if (rest == '') {
|
|
if (MyAvatar.displayName) {
|
|
chatName = MyAvatar.displayName;
|
|
saveSettings();
|
|
logMessage('Your chat name has been set to your display name "' + chatName + '".', null);
|
|
} else {
|
|
chatName = randomAvatarName();
|
|
saveSettings();
|
|
logMessage('Your avatar\'s display name is not defined, so your chat name has been set to "' + chatName + '".', null);
|
|
}
|
|
} else {
|
|
chatName = rest;
|
|
saveSettings();
|
|
logMessage('Your chat name has been set to "' + chatName + '".', null);
|
|
}
|
|
break;
|
|
|
|
case 'close':
|
|
popDownSpeechBubble();
|
|
logMessage('Overhead chat message closed.', null);
|
|
break;
|
|
|
|
case 'say':
|
|
if (rest == '') {
|
|
emptyChatMessage(data);
|
|
} else {
|
|
transmitChatMessage(rest, data);
|
|
}
|
|
break;
|
|
|
|
case 'who':
|
|
transmitWho();
|
|
break;
|
|
|
|
case 'clear':
|
|
clearChatLog();
|
|
break;
|
|
|
|
case 'bigger':
|
|
biggerSize();
|
|
break;
|
|
|
|
case 'smaller':
|
|
smallerSize();
|
|
break;
|
|
|
|
case 'normal':
|
|
normalSize();
|
|
break;
|
|
|
|
case 'resetsettings':
|
|
resetSettings();
|
|
updateSettings();
|
|
break;
|
|
|
|
case 'speechbubbleheight':
|
|
var y = parseInt(rest);
|
|
if (!isNaN(y)) {
|
|
speechBubbleOffset.y = y;
|
|
}
|
|
saveSettings();
|
|
updateSettings();
|
|
break;
|
|
|
|
case 'speechbubbleduration':
|
|
var duration = parseFloat(rest);
|
|
if (!isNaN(duration)) {
|
|
speechBubbleDuration = duration;
|
|
}
|
|
saveSettings();
|
|
updateSettings();
|
|
break;
|
|
|
|
default:
|
|
logMessage('Unknown chat command. Type "/help" or "/?" for help.', null);
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Send out a chat message to everyone.
|
|
function transmitChatMessage(message, data) {
|
|
//print("transmitChatMessage", 'avatarID', avatarID, 'displayName', displayName, 'message', message, 'data', data);
|
|
|
|
popUpSpeechBubble(message, data);
|
|
|
|
Messages.sendMessage(
|
|
channelName,
|
|
JSON.stringify({
|
|
type: 'TransmitChatMessage',
|
|
avatarID: MyAvatar.sessionUUID,
|
|
displayName: chatName,
|
|
message: message,
|
|
data: data
|
|
}));
|
|
|
|
}
|
|
|
|
// Show the speech bubble.
|
|
function popUpSpeechBubble(message, data) {
|
|
//print("popUpSpeechBubble", message, data);
|
|
|
|
popDownSpeechBubble();
|
|
|
|
speechBubbleShowing = true;
|
|
speechBubbleMessage = message;
|
|
speechBubbleData = data;
|
|
|
|
updateSpeechBubble();
|
|
|
|
if (speechBubbleDuration > 0) {
|
|
speechBubbleTimer = Script.setTimeout(
|
|
function () {
|
|
popDownSpeechBubble();
|
|
},
|
|
speechBubbleDuration * 1000);
|
|
}
|
|
}
|
|
|
|
// Update the speech bubble.
|
|
// This is factored out so we can update an existing speech bubble if any settings change.
|
|
function updateSpeechBubble() {
|
|
if (!speechBubbleShowing) {
|
|
return;
|
|
}
|
|
|
|
var jointIndex = MyAvatar.getJointIndex(speechBubbleJointName);
|
|
var dimensions = {
|
|
"x": 100.0,
|
|
"y": 100.0,
|
|
"z": 0.1
|
|
};
|
|
|
|
speechBubbleParams = {
|
|
"type": "Text",
|
|
"lifetime": speechBubbleDuration,
|
|
"parentID": MyAvatar.sessionUUID,
|
|
"jointIndex": jointIndex,
|
|
"dimensions": dimensions,
|
|
"lineHeight": speechBubbleLineHeight,
|
|
"leftMargin": 0,
|
|
"topMargin": 0,
|
|
"rightMargin": 0,
|
|
"bottomMargin": 0,
|
|
"billboardMode": "full",
|
|
"renderLayer": "front",
|
|
"ignorePickIntersection": true,
|
|
"text": speechBubbleMessage,
|
|
"textColor": speechBubbleTextColor,
|
|
"backgroundColor": speechBubbleBackgroundColor
|
|
};
|
|
|
|
// Only overlay text3d has a way to measure the text, not entities.
|
|
// So we make a temporary one just for measuring text, then delete it.
|
|
var speechBubbleTextOverlayID = Entities.addEntity(speechBubbleParams, "local");
|
|
var textSize = Entities.textSize(textSizeOverlay, speechBubbleMessage);
|
|
try {
|
|
Entities.deleteEntity(speechBubbleTextOverlayID);
|
|
} catch (e) {}
|
|
|
|
//print("updateSpeechBubble:", "speechBubbleMessage", speechBubbleMessage, "textSize", textSize.width, textSize.height);
|
|
|
|
var fudge = 0.02;
|
|
|
|
var width = textSize.width + fudge;
|
|
var height = speechBubbleLineHeight + fudge;
|
|
|
|
if (textSize.width >= SPEECH_BUBBLE_MAX_WIDTH) {
|
|
var numLines = Math.ceil(width);
|
|
height = speechBubbleLineHeight * numLines + fudge;
|
|
width = SPEECH_BUBBLE_MAX_WIDTH;
|
|
}
|
|
|
|
dimensions = {
|
|
x: width,
|
|
y: height,
|
|
z: 0.1
|
|
};
|
|
speechBubbleParams.dimensions = dimensions;
|
|
|
|
var headRotation =
|
|
Quat.multiply(
|
|
MyAvatar.orientation,
|
|
MyAvatar.getAbsoluteJointRotationInObjectFrame(jointIndex));
|
|
var headPosition =
|
|
Vec3.sum(
|
|
MyAvatar.position,
|
|
Vec3.multiplyQbyV(
|
|
MyAvatar.orientation,
|
|
MyAvatar.getAbsoluteJointTranslationInObjectFrame(jointIndex)));
|
|
var rotatedOffset =
|
|
Vec3.multiplyQbyV(
|
|
headRotation,
|
|
speechBubbleOffset);
|
|
var position =
|
|
Vec3.sum(
|
|
headPosition,
|
|
rotatedOffset);
|
|
position.y += height / 2; // offset based on half of bubble height
|
|
speechBubbleParams.position = position;
|
|
|
|
if (!speechBubbleTextID) {
|
|
speechBubbleTextID =
|
|
Entities.addEntity(speechBubbleParams, true);
|
|
} else {
|
|
Entities.editEntity(speechBubbleTextID, speechBubbleParams);
|
|
}
|
|
|
|
//print("speechBubbleTextID:", speechBubbleTextID, "speechBubbleParams", JSON.stringify(speechBubbleParams));
|
|
}
|
|
|
|
// Hide the speech bubble.
|
|
function popDownSpeechBubble() {
|
|
cancelSpeechBubbleTimer();
|
|
|
|
speechBubbleShowing = false;
|
|
|
|
//print("popDownSpeechBubble speechBubbleTextID", speechBubbleTextID);
|
|
|
|
if (speechBubbleTextID) {
|
|
try {
|
|
Entities.deleteEntity(speechBubbleTextID);
|
|
} catch (e) {}
|
|
speechBubbleTextID = null;
|
|
}
|
|
}
|
|
|
|
// Cancel the speech bubble popup timer.
|
|
function cancelSpeechBubbleTimer() {
|
|
if (speechBubbleTimer) {
|
|
Script.clearTimeout(speechBubbleTimer);
|
|
speechBubbleTimer = null;
|
|
}
|
|
}
|
|
|
|
// Show the tablet web page and connect the web handler.
|
|
function showTabletWebPage() {
|
|
var url = Script.resolvePath(webPageURL);
|
|
if (randomizeWebPageURL) {
|
|
url += '?rand=' + Math.random();
|
|
}
|
|
lastWebPageURL = url;
|
|
onChatPage = true;
|
|
tablet.gotoWebScreen(lastWebPageURL);
|
|
// Connect immediately so we don't miss anything.
|
|
connectWebHandler();
|
|
}
|
|
|
|
// Update the tablet web page with the chat log.
|
|
function updateChatPage() {
|
|
if (!onChatPage) {
|
|
return;
|
|
}
|
|
|
|
tablet.emitScriptEvent(
|
|
JSON.stringify({
|
|
type: "Update",
|
|
chatLog: chatLog
|
|
}));
|
|
}
|
|
|
|
function onChatMessageReceived(channel, message, senderID) {
|
|
|
|
// Ignore messages to any other channel than mine.
|
|
if (channel != channelName) {
|
|
return;
|
|
}
|
|
|
|
// Parse the message and pull out the message parameters.
|
|
var messageData = JSON.parse(message);
|
|
var messageType = messageData.type;
|
|
|
|
//print("MESSAGE", message);
|
|
//print("MESSAGEDATA", messageData, JSON.stringify(messageData));
|
|
|
|
switch (messageType) {
|
|
|
|
case 'TransmitChatMessage':
|
|
handleTransmitChatMessage(messageData.avatarID, messageData.displayName, messageData.message, messageData.data);
|
|
break;
|
|
|
|
case 'AvatarBeginTyping':
|
|
handleAvatarBeginTyping(messageData.avatarID, messageData.displayName);
|
|
break;
|
|
|
|
case 'AvatarEndTyping':
|
|
handleAvatarEndTyping(messageData.avatarID, messageData.displayName);
|
|
break;
|
|
|
|
case 'Who':
|
|
handleWho(messageData.myAvatarID);
|
|
break;
|
|
|
|
case 'ReplyWho':
|
|
handleReplyWho(messageData.myAvatarID, messageData.avatarID, messageData.displayName, messageData.message, messageData.data);
|
|
break;
|
|
|
|
default:
|
|
print("onChatMessageReceived: unknown messageType", messageType, "message", message);
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Handle events from the tablet web page.
|
|
function onWebEventReceived(event) {
|
|
if (!onChatPage) {
|
|
return;
|
|
}
|
|
|
|
//print("onWebEventReceived: event", event);
|
|
|
|
var eventData = JSON.parse(event);
|
|
var eventType = eventData.type;
|
|
|
|
switch (eventType) {
|
|
|
|
case 'Ready':
|
|
updateChatPage();
|
|
break;
|
|
|
|
case 'Update':
|
|
updateChatPage();
|
|
break;
|
|
|
|
case 'HandleChatMessage':
|
|
var message = eventData.message;
|
|
var data = eventData.data;
|
|
//print("onWebEventReceived: HandleChatMessage:", 'message', message, 'data', data);
|
|
handleChatMessage(message, data);
|
|
break;
|
|
|
|
case 'PopDownSpeechBubble':
|
|
popDownSpeechBubble();
|
|
break;
|
|
|
|
case 'EmptyChatMessage':
|
|
emptyChatMessage();
|
|
break;
|
|
|
|
case 'Type':
|
|
type();
|
|
break;
|
|
|
|
case 'BeginTyping':
|
|
beginTyping();
|
|
break;
|
|
|
|
case 'EndTyping':
|
|
endTyping();
|
|
break;
|
|
|
|
case 'IdentifyAvatar':
|
|
identifyAvatar(eventData.avatarID);
|
|
break;
|
|
|
|
case 'UnidentifyAvatar':
|
|
unidentifyAvatar(eventData.avatarID);
|
|
break;
|
|
|
|
case 'FaceAvatar':
|
|
faceAvatar(eventData.avatarID, eventData.displayName);
|
|
break;
|
|
|
|
case 'ClearChatLog':
|
|
clearChatLog();
|
|
break;
|
|
|
|
case 'Who':
|
|
transmitWho();
|
|
break;
|
|
|
|
case 'Bigger':
|
|
biggerSize();
|
|
break;
|
|
|
|
case 'Smaller':
|
|
smallerSize();
|
|
break;
|
|
|
|
case 'Normal':
|
|
normalSize();
|
|
break;
|
|
|
|
default:
|
|
print("onWebEventReceived: unexpected eventType", eventType);
|
|
break;
|
|
|
|
}
|
|
}
|
|
|
|
function onScreenChanged(type, url) {
|
|
//print("onScreenChanged", "type", type, "url", url, "lastWebPageURL", lastWebPageURL);
|
|
|
|
if ((type === "Web") &&
|
|
(url === lastWebPageURL)) {
|
|
if (!onChatPage) {
|
|
onChatPage = true;
|
|
connectWebHandler();
|
|
}
|
|
} else {
|
|
if (onChatPage) {
|
|
onChatPage = false;
|
|
disconnectWebHandler();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function connectWebHandler() {
|
|
if (webHandlerConnected) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
tablet.webEventReceived.connect(onWebEventReceived);
|
|
} catch (e) {
|
|
print("connectWebHandler: error connecting: " + e);
|
|
return;
|
|
}
|
|
|
|
webHandlerConnected = true;
|
|
//print("connectWebHandler connected");
|
|
|
|
updateChatPage();
|
|
}
|
|
|
|
function disconnectWebHandler() {
|
|
if (!webHandlerConnected) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
tablet.webEventReceived.disconnect(onWebEventReceived);
|
|
} catch (e) {
|
|
print("disconnectWebHandler: error disconnecting web handler: " + e);
|
|
return;
|
|
}
|
|
webHandlerConnected = false;
|
|
|
|
//print("disconnectWebHandler: disconnected");
|
|
}
|
|
|
|
// Show the tablet web page when the chat button on the tablet is clicked.
|
|
function onTabletButtonClicked() {
|
|
showTabletWebPage();
|
|
}
|
|
|
|
// Shut down the chat application when the tablet button is destroyed.
|
|
function onTabletButtonDestroyed() {
|
|
shutDown();
|
|
}
|
|
|
|
// Start up the chat application.
|
|
function startUp() {
|
|
//print("startUp");
|
|
|
|
loadSettings();
|
|
|
|
tabletButton = tablet.addButton({
|
|
icon: tabletButtonIcon,
|
|
activeIcon: tabletButtonActiveIcon,
|
|
text: tabletButtonName
|
|
});
|
|
|
|
Messages.subscribe(channelName);
|
|
|
|
tablet.screenChanged.connect(onScreenChanged);
|
|
|
|
Messages.messageReceived.connect(onChatMessageReceived);
|
|
|
|
tabletButton.clicked.connect(onTabletButtonClicked);
|
|
|
|
Script.scriptEnding.connect(onTabletButtonDestroyed);
|
|
|
|
logMessage('Type "/?" or "/help" for help with chat.', null);
|
|
|
|
//print("Added chat button to tablet.");
|
|
}
|
|
|
|
// Shut down the chat application.
|
|
function shutDown() {
|
|
//print("shutDown");
|
|
|
|
popDownSpeechBubble();
|
|
unidentifyAvatars();
|
|
disconnectWebHandler();
|
|
|
|
Entities.deleteEntity(textSizeOverlay);
|
|
|
|
if (onChatPage) {
|
|
tablet.gotoHomeScreen();
|
|
onChatPage = false;
|
|
}
|
|
|
|
tablet.screenChanged.disconnect(onScreenChanged);
|
|
|
|
Messages.messageReceived.disconnect(onChatMessageReceived);
|
|
|
|
// Clean up the tablet button we made.
|
|
tabletButton.clicked.disconnect(onTabletButtonClicked);
|
|
tablet.removeButton(tabletButton);
|
|
tabletButton = null;
|
|
|
|
//print("Removed chat button from tablet.");
|
|
}
|
|
|
|
// Kick off the chat application!
|
|
startUp();
|
|
|
|
}());
|