content/hifi-content/luis/test_scripts/Sitting/sitClientV2.js
2022-02-14 02:04:11 +01:00

636 lines
No EOL
22 KiB
JavaScript

//
// sitClient.js
//
// Created by Robin Wilson 5/7/2019
//
// Original sit script created by Clement Brisset on 3/3/17
// Previous script modified by Robin Wilson, Rebecca Stankus, and Alexia Mandeville June 2018
//
// 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
//
// Allows avatars to sit in High Fidelity after clicking on a local entity or on the chair. Works with sitServer.js.
//
/* global DriveKeys */
(function () {
var DEBUG = 0;
// #region UTILITIES
// Returns entity properties for an overlay in front of user's camera in desktop and VR
function getEntityPropertiesForImageInFrontOfCamera(positionInFront, dimensions, url) {
return {
type: "Image",
grab: { grabbable: false },
dynamic: false,
parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"),
imageURL: url,
position: Vec3.sum(Camera.position, Vec3.multiplyQbyV(Camera.orientation, positionInFront)),
dimensions: dimensions,
rotation: Camera.rotation,
parentID: MyAvatar.sessionUUID,
ignoreRayIntersection: false,
visible: true,
emissive: true,
renderLayer: "front"
};
}
// String utility
function startsWith(str, searchString, position) {
position = position || 0;
return str.substr(position, searchString.length) === searchString;
}
// Checks for Avatar Spine Error in VR to avoid uncomfortable looking avatar position
var NEG_ONE = -1;
var HALF = 0.5;
function setHeadToHipsDistance() {
// get hips world pos while sitting
if (MyAvatar.getJointIndex("Head") === NEG_ONE) {
// this can probably be adjusted to be more accurate
_this.headToHipsDistance = MyAvatar.getHeight() * HALF;
} else {
var headTranslation = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(MyAvatar.getJointIndex("Head"));
var hipsTranslation = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(MyAvatar.getJointIndex("Hips"));
_this.headToHipsDistance = Math.abs(headTranslation.y - hipsTranslation.y);
}
}
// Calculates where the avatar's hips will seat
// Used to calculate pin hip position. Adds CHAIR_OFFSET_RATIO * chair's y dimension to the y center of the seat.
// Avatar sit position y = CHAIR_OFFSET_RATIO * height of chair
var CHAIR_OFFSET_RATIO = 0.1;
function calculateSeatCenterPositionForPinningAvatarHips() {
var properties = Entities.getEntityProperties(_this.entityID);
var yOffset = properties.dimensions.y * CHAIR_OFFSET_RATIO;
_this.seatCenterPosition = properties.position;
_this.seatCenterPosition.y = properties.position.y + yOffset;
}
// #endregion UTILITIES
// #region SIT DOWN / STAND UP SEQUENCE
// 1st of sit down sequence
// Called from entity server script to begin sitting down sequence
var SETTING_KEY_AVATAR_SITTING = "com.highfidelity.avatar.isSitting";
var SIT_SETTLE_TIME_MS = 350; // Do not pop avatar out of chair immediately if there's an issue
var SIT_DELAY_MS = 50; // ms for timeouts in sit
var CAN_SIT_M = 5; // zone radius
var changedSeats = false;
function startSitDown() {
var sitEntity = Settings.getValue(SETTING_KEY_AVATAR_SITTING, false);
if (sitEntity && sitEntity !== _this.entityID) {
changedSeats = true;
}
if (DEBUG) {
console.log("startSitDown in ", _this.entityID);
}
Entities.callEntityServerMethod(
_this.entityID,
"removeAllOtherSittableOverlays",
AvatarList.getAvatarsInRange(_this.seatCenterPosition, CAN_SIT_M)
);
deleteSittableUI();
// Set isSitting value in Settings
var sitNewSettings = _this.entityID;
Settings.setValue(SETTING_KEY_AVATAR_SITTING, sitNewSettings);
if (HMD.active) {
createPresit();
Script.setTimeout(function () {
beforeSitDown();
}, PRESIT_FRAME_DURATION * _this.preSitLoadedImages.length);
} else {
beforeSitDown();
}
}
// 3rd of sit down sequence (callback for inside 2nd)
// Used before pinning hips, calculate the math required and lock the chair
function beforeSitDown() {
// Set the value of head to hips distance
// If avatar deviates outside of the minimum and maximum, the avatar will pop out of the chair
setHeadToHipsDistance();
// Lock chair and save old setting
_this.locked = Entities.getEntityProperties(_this.entityID, 'locked').locked;
Entities.editEntity(_this.entityID, { locked: true });
calculateSeatCenterPositionForPinningAvatarHips();
sitDownAndPinAvatar();
}
// Listen to action events. If jump is pressed, the user will stand up
var JUMP_ACTION_ID = 50;
var jumpPressed = false;
function onActionEvent(actionID, value) {
if (actionID === JUMP_ACTION_ID) {
jumpPressed = true;
standUp();
}
}
var UPDATE_INTERVAL_MS = 400;
var isConnected = false;
var driveKeysDisabled = false;
function sitDownAndPinAvatar() {
if (HMD.active) {
deletePresit();
}
driveKeysDisabled = true;
if (!isConnected) {
Controller.actionEvent.connect(onActionEvent);
isConnected = true;
}
Script.setTimeout(function () {
var properties = Entities.getEntityProperties(_this.entityID);
MyAvatar.beginSit(_this.seatCenterPosition, properties.rotation);
_this.sitDownSettlePeriod = Date.now() + SIT_SETTLE_TIME_MS;
stopUpdateInterval();
_this.whileSittingUpdateIntervalID = Script.setInterval(_this.whileSittingUpdate, UPDATE_INTERVAL_MS);
deleteSittableUI();
if (!_this.connectedSignals) {
MyAvatar.scaleChanged.connect(_this.standUp);
MyAvatar.onLoadComplete.connect(_this.standUp);
location.hostChanged.connect(_this.standUp);
Script.scriptEnding.connect(_this.standUp);
MyAvatar.wentAway.connect(_this.standUpWrapper);
HMD.displayModeChanged.connect(_this.standUpWrapper);
_this.connectedSignals = true;
}
}, SIT_DELAY_MS);
}
// 5th of sit down sequence
// Handles stand up conditions
// Update interval while the avatar is sitting
// Listens for drive keys held, spine error (in VR), and if avatar teleported away from chair
var AVATAR_MOVED_TOO_FAR_DISTANCE_M = 0.5;
var MAX_HEAD_DEVIATION_RATIO = 1.2;
var MIN_HEAD_DEVIATION_RATIO = 0.8;
var HEAD_DEVIATION_MAX_TIME_TO_STAND_MS = 1000;
function whileSittingUpdate() {
var now = Date.now();
if (_this.sitDownSettlePeriod && now < _this.sitDownSettlePeriod) {
// below considerations only apply if sit down settle period has passed
return;
} else {
_this.sitDownSettlePeriod = false;
}
// AVATAR DISTANCE
if (Vec3.distance(MyAvatar.position, _this.seatCenterPosition) > AVATAR_MOVED_TOO_FAR_DISTANCE_M) {
_this.standUp();
if (DEBUG) {
console.log("avatar distance caused standup");
}
return;
}
// AVATAR SPINE ERROR
if (HMD.active) {
// If head is more than certain distance from hips or less than certain distance, stand up
var headPoseValue = Controller.getPoseValue(Controller.Standard.Head);
var headWorldPosition = Vec3.sum(
Vec3.multiplyQbyV(MyAvatar.orientation, headPoseValue.translation),
MyAvatar.position
);
var headDeviation = Vec3.distance(headWorldPosition, _this.seatCenterPosition);
var headDeviationRatio = headDeviation / _this.headToHipsDistance;
if (headDeviationRatio < MIN_HEAD_DEVIATION_RATIO || headDeviationRatio > MAX_HEAD_DEVIATION_RATIO) {
if (!_this.deviationTimeStart) {
_this.deviationTimeStart = now;
}
if (now - _this.deviationTimeStart > HEAD_DEVIATION_MAX_TIME_TO_STAND_MS) {
_this.deviationTimeStart = false;
_this.standUp();
if (DEBUG) {
console.log("avatar spine error caused standup");
}
}
} else {
_this.deviationTimeStart = false;
}
}
}
// Clear whileSitting update interval
function stopUpdateInterval() {
if (_this.whileSittingUpdateIntervalID) {
Script.clearInterval(_this.whileSittingUpdateIntervalID);
_this.whileSittingUpdateIntervalID = false;
}
}
// To help with the clashing HMD and My Avatar away signals.
// A timeout here seems to be the only thing to prevent the default animation not being restored
var standUpWrapperCalled = false;
var STAND_UP_WRAPPER_WAIT_MS = 500;
function standUpWrapper(){
if (standUpWrapperCalled) {
return;
}
standUpWrapperCalled = true;
Script.setTimeout(function(){
standUp();
}, STAND_UP_WRAPPER_WAIT_MS);
}
// Standup functionality
var STANDUP_BUMP = 0.225;
var roles = [];
function standUp() {
if (DEBUG) {
console.log("standup from ", _this.entityID);
}
if (standUpWrapperCalled) {
standUpWrapperCalled = false;
}
// get the entityID, previous position and orientation
var settingsEntityID = Settings.getValue(SETTING_KEY_AVATAR_SITTING);
changedSeats = false;
// STANDING FROM THIS CHAIR
// Make avatar stand up (if changed seat do not do this)
if (settingsEntityID === _this.entityID) { // POSSIBLE RACE CONDITION WITH SETTINGS BEING CHANGED BY NEW SEAT
Settings.setValue(SETTING_KEY_AVATAR_SITTING, null);
standAvatarUp();
}
// RESET SETTINGS FOR THIS CHAIR
// Could have changed seats, keep avatar sitting if did not go through above procedure
if (!_this.locked) {
Entities.editEntity(_this.entityID, { locked: false });
}
_this.driveKeyPressedStart = false;
_this.sitDownSettlePeriod = false;
Entities.callEntityServerMethod(_this.entityID, "onStandUp");
Entities.callEntityServerMethod(
_this.entityID,
"addAllOtherSittableOverlays",
AvatarList.getAvatarsInRange(_this.seatCenterPosition, CAN_SIT_M)
);
stopUpdateInterval();
if (_this.connectedSignals) {
MyAvatar.scaleChanged.disconnect(_this.standUp);
MyAvatar.onLoadComplete.disconnect(_this.standUp);
location.hostChanged.disconnect(_this.standUp);
Script.scriptEnding.disconnect(_this.standUp);
MyAvatar.wentAway.disconnect(_this.standUpWrapper);
HMD.displayModeChanged.disconnect(_this.standUpWrapper);
_this.connectedSignals = false;
}
}
var rolesOverRidden = false;
var MAX_ROLES_TO_RETRY = 3;
var ROLES_TIMEOUT_MS = 100;
var roleTryAmount = 0;
function overRideRoles(){
if (roles.length === 0) {
roleTryAmount++;
if (roleTryAmount === MAX_ROLES_TO_RETRY) {
return;
} else {
Script.setTimeout(overRideRoles, ROLES_TIMEOUT_MS)
}
}
for (var j = 0; j < roles.length; j++){
MyAvatar.restoreRoleAnimation(roles[j]);
if (j === roles.length -1) {
rolesOverRidden = true;
}
}
}
var MAX_STANDUP_AVATAR_TO_RETRY = 3;
var STANDUP_AVATAR_TIMEOUT_MS = 150;
var standUpAvatarAmount = 0;
function standAvatarUp(){
if (isConnected) {
Controller.actionEvent.disconnect(onActionEvent);
isConnected = false;
}
MyAvatar.endSit(MyAvatar.position, MyAvatar.orientation);
}
// Remotely called from canSitZone
var AVATAR_SITTING_IN_CHAIR_RANGE = 0.01;
var EDIT_SETTING = "io.highfidelity.isEditing";
function onEnterCanSitZone(id, params) {
if (params && params.length > 0) {
_this.zoneID = params[0];
}
if (!Settings.getValue(EDIT_SETTING, false)) {
calculateSeatCenterPositionForPinningAvatarHips();
var isSittingInChair = AvatarList.isAvatarInRange(_this.seatCenterPosition, AVATAR_SITTING_IN_CHAIR_RANGE);
if (DEBUG) {
console.log("onEnterCanSitZone" + !_this.sittableUIID + !isSittingInChair);
}
if (!_this.sittableUIID && !isSittingInChair) {
createSittableUI();
}
} else {
if (DEBUG) {
console.log("entered zone and isInEditMode");
}
// is in edit mode do not create sittable
}
}
// Remotely called by the canSitZone
function onLeaveCanSitZone() {
deleteSittableUI();
}
// #endregion SIT DOWN / STAND UP SEQUENCE
// #region PRESIT - LOCAL ENTITY shown in HMD before sitting and after clicking sittable overlay
// Has the sitting animation and "Please Face Forward"
// Prefetch all presit overlay images into user's client
var PRESIT_FRAME_DURATION = 160; // ms time duration for HMD presit overlay
var PRESIT_URL_ROOT = Script.resolvePath("./resources/images/presit/sitConfirm");
var PRESIT_URL_POSTFIX = ".png";
var PRESIT_URL_NUM = 12;
var PRESIT_URL_TEXT = Script.resolvePath("./resources/images/presit/pleaseFaceForward.png");
function prefetchPresitImages() {
var str;
for (var i = 0; i < PRESIT_URL_NUM; i++) {
str = i + 1;
_this.preSitLoadedImages[i] = TextureCache.prefetch(Script.resolvePath(PRESIT_URL_ROOT + str + PRESIT_URL_POSTFIX));
}
_this.preSitTextLoadedImage = TextureCache.prefetch(PRESIT_URL_TEXT);
}
// Create the VR presit animation local entity in front of user's screen
var SIT_ANIMATION_POSITION_IN_FRONT = { x: 0, y: 0.1, z: -1 };
var SIT_ANIMATION_DIMENSIONS = { x: 0.2, y: 0.2 };
var PLEASE_FACE_FORWARD_POSITION_IN_FRONT = { x: 0, y: -0.05, z: -1 };
var PLEASE_FACE_FORWARD_DIMENSIONS = { x: 0.425, y: 0.425 };
function createPresit() {
var currentPresitAnimationFrame = 0;
_this.presitAnimationImageID = Entities.addEntity(
getEntityPropertiesForImageInFrontOfCamera(
SIT_ANIMATION_POSITION_IN_FRONT,
SIT_ANIMATION_DIMENSIONS,
_this.preSitLoadedImages[currentPresitAnimationFrame].url
),
"local"
);
_this.presitTextID = Entities.addEntity(
getEntityPropertiesForImageInFrontOfCamera(
PLEASE_FACE_FORWARD_POSITION_IN_FRONT,
PLEASE_FACE_FORWARD_DIMENSIONS,
_this.preSitTextLoadedImage.url
),
"local"
);
// Flash through the presit animation images via overlay for a smooth avatar sitting animation
_this.presitIntervalID = Script.setInterval(function () {
if (_this.presitAnimationImageID) {
currentPresitAnimationFrame = currentPresitAnimationFrame + 1;
if (currentPresitAnimationFrame >= _this.preSitLoadedImages.length - 1) {
deletePresit();
}
Entities.editEntity(_this.presitAnimationImageID, { imageURL:
_this.preSitLoadedImages[currentPresitAnimationFrame].url });
}
}, PRESIT_FRAME_DURATION);
}
// Delete presit local entity in user's screen
function deletePresit() {
if (_this.presitAnimationImageID) {
Entities.deleteEntity(_this.presitAnimationImageID);
_this.presitAnimationImageID = false;
}
if (_this.presitTextID) {
Entities.deleteEntity(_this.presitTextID);
_this.presitTextID = false;
}
if (_this.presitIntervalID) {
Script.clearInterval(_this.presitIntervalID);
_this.presitIntervalID = false;
}
}
// #endregion PRESIT
// #region SITTABLE LOCAL ENTITY
// Create sittable UI on the chair
var SITTABLE_START_ALPHA = 0.7;
var SITTABLE_DIMENSIONS = { x: 0.3, y: 0.3 };
var SITTABLE_IMAGE_URL_HMD = Script.resolvePath("./resources/images/triggerToSit.png");
var SITTABLE_IMAGE_URL_DESKTOP = Script.resolvePath("./resources/images/clickToSit.png");
var SITTABLE_Y_OFFSET = 0.01;
function createSittableUI() {
if (_this.sittableID) {
// already created
return;
}
if (DEBUG) {
console.log("createSittableUI()");
}
var properties = Entities.getEntityProperties(_this.entityID);
// calculate
var localOffset = { x: 0, y: SITTABLE_Y_OFFSET, z: 0 };
var worldOffset = Vec3.multiplyQbyV(properties.rotation, localOffset);
var sittablePosition = Vec3.sum(properties.position, worldOffset);
_this.sittableID = Entities.addEntity({
type: "Image",
grab: {
grabbable: false
},
dynamic: false,
position: sittablePosition,
rotation: Quat.multiply(
properties.rotation,
Quat.fromVec3Degrees({ x: -90, y: 180, z: 0 })
),
parentID: _this.entityID,
dimensions: SITTABLE_DIMENSIONS,
imageURL: HMD.active ? SITTABLE_IMAGE_URL_HMD : SITTABLE_IMAGE_URL_DESKTOP,
ignoreRayIntersection: false,
alpha: SITTABLE_START_ALPHA,
script: Script.resolvePath("./resources/sittableUIClient.js"),
visible: true,
emissive: true
},
"local"
);
}
// Remove sittable local entity if it exists
function deleteSittableUI() {
if (DEBUG) {
print("deleteSittableUI");
}
if (_this.sittableID) {
Entities.deleteEntity(_this.sittableID);
_this.sittableID = false;
}
}
// #endregion SITTABLE
// #region ENTITY LIFETIME FUNCTIONS
// Check userData for configurable settings
// If userData is not configured with the right keys update with defaults
var DEFAULT_SIT_USER_DATA_WITH_CUSTOM_SETTINGS = {
canClickOnModelToSit: false
};
function updateUserData() {
var properties = Entities.getEntityProperties(_this.entityID);
try {
_this.userData = JSON.parse(properties.userData);
} catch (e) {
console.log("Issue parsing userData" + e);
}
if (!_this.userData || _this.userData.canClickOnModelToSit === undefined) {
Entities.editEntity(_this.entityID, { userData: JSON.stringify(DEFAULT_SIT_USER_DATA_WITH_CUSTOM_SETTINGS) });
}
}
// Preload entity method
function preload(id) {
_this.entityID = id;
prefetchPresitImages();
updateUserData();
}
// Unload entity method
function unload() {
var sitCurrentSettings = Settings.getValue(SETTING_KEY_AVATAR_SITTING);
deleteSittableUI();
deletePresit();
standUp();
if (_this.connectedSignals) {
MyAvatar.scaleChanged.disconnect(_this.standUp);
MyAvatar.onLoadComplete.disconnect(_this.standUp);
location.hostChanged.disconnect(_this.standUp);
Script.scriptEnding.disconnect(_this.standUp);
MyAvatar.wentAway.disconnect(_this.standUpWrapper);
HMD.displayModeChanged.disconnect(_this.standUpWrapper);
_this.connectedSignals = false;
}
}
// Called by server script in heartbeat to ensure avatar is still sitting in chair
function check() {
Entities.callEntityServerMethod(_this.entityID, "checkResolved");
}
// Can sit when clicking on chair when enabled via userData
function mousePressOnEntity(id, event) {
if (event.isPrimaryButton && !Settings.getValue(EDIT_SETTING, false)) {
updateUserData();
if (_this.userData && _this.userData.canClickOnModelToSit) {
Entities.callEntityServerMethod(_this.entityID, "onSitDown", [MyAvatar.sessionUUID]);
}
}
}
// #endregion ENTITY LIFETIME FUNCTIONS
// Constructor
var _this = null;
function SitClient() {
_this = this;
this.sittableUIID = false;
this.canSitZoneID = false;
// for presit local entity and animation
this.presitIntervalID = false;
this.presitTextID = false;
this.presitAnimationImageID = false;
// for prefetch on presit
this.preSitLoadedImages = [];
this.preSitTextLoadedImage = null;
this.locked = null;
this.seatCenterPosition = null;
this.headToHipsDistance = null;
this.sitDownSettlePeriod = false;
this.whileSittingUpdateIntervalID = false;
this.standUpID = false;
this.driveKeyPressedStart = false;
this.deviationTimeStart = false;
this.zoneID = false;
this.connectedSignals = false;
// CUSTOMIZATIONS
this.userData = {};
this.canClickOnModelToSit = false;
}
// Entity methods
SitClient.prototype = {
remotelyCallable: [
"onEnterCanSitZone",
"onLeaveCanSitZone",
"startSitDown",
"check"
],
// Zone methods
onEnterCanSitZone: onEnterCanSitZone,
onLeaveCanSitZone: onLeaveCanSitZone,
// Entity liftime methods
preload: preload,
unload: unload,
// Sitting lifetime methods
mousePressOnEntity: mousePressOnEntity,
startSitDown: startSitDown,
whileSittingUpdate: whileSittingUpdate,
check: check,
standUp: standUp,
standUpWrapper: standUpWrapper
};
return new SitClient();
});