content/hifi-content/DomainContent/production/avatarAccessories/attachScript/attachmentItemScript.js
2022-02-13 22:49:05 +01:00

494 lines
18 KiB
JavaScript

//
// attachmentItemScript.js
//
// Created by Thijs Wenker on 5/30/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/*
// User Data formats:
{
"Attachment": {
"action": "attach",
"joint": "Hips",
"options": {
"translation": {
"x": 0,
"y": -0.11,
"z": 0.04
}
}
}
}
{
"Attachment": {
"action": "attach",
"joint": "[LR]Hips",
"snapDistance": 0.31,
"options": {
"translation": {
"x": 0,
"y": -0.11,
"z": 0.04
}
}
}
}
{
"Attachment": {
"action": "clear",
"joint": "Hips"
}
}
{
"Attachment": {
"action": "clear",
"joint": "[LR]ForeArm"
}
}
*/
(function() {
var _entityID = null;
var _isGrabbing = false;
var _attachmentData = null;
var _leftRightAttachToggle = false;
var _listeningChannel = null;
var _virtualHoldController = null;
var WANT_DEBUG = true;
var ATTACHMENT_CHANNEL_PREFIX = 'attachmentItem-';
var TOTAL_HOLD_LIFETIME = 60; // seconds
var DEFAULT_SNAP_DISTANCE = 0.3;
var HAND_LEFT = 0;
var HAND_RIGHT = 1;
var LEFT_RIGHT_PLACEHOLDER = '[LR]';
var ATTACH_SOUND = SoundCache.getSound(Script.resolvePath('sound/attach_sound_1.wav'));
var DETACH_SOUND = SoundCache.getSound(Script.resolvePath('sound/detach.wav'));
var debugPrint = function(message) {};
if (WANT_DEBUG) {
debugPrint = function(message) {
print(message);
}
}
var getEntityProperty = function(entityID, property) {
return Entities.getEntityProperties(entityID, property)[property];
};
var _this;
function VirtualHoldController(hand, holdableEntity, localPosition, localRotation) {
_this = this;
_this.hand = hand;
_this.holdableEntity = holdableEntity;
_this.localPosition = localPosition;
_this.localRotation = localRotation;
_this.controllerWasReleased = false;
_this.cleanedUp = false;
_this.init();
}
VirtualHoldController.prototype = {
hand: null,
holdableEntity: null,
holdAction: null,
controllerWasReleased: null,
mappingName: null,
mapping: null,
cleanedUp: null,
onRelease: null,
init: function() {
Script.update.connect(_this.update);
Messages.sendMessage('Hifi-Hand-Disabler', _this.hand);
_this.mappingName = 'VirtualHoldController_' + _this.hand + '_' + _this.holdableEntity;
_this.mapping = Controller.newMapping(_this.mappingName);
var SMOOTH_TRIGGER_RELEASE_THRESHOLD = 0.3;
var GRIP_BUTTON_ACTIVE_THRESHOLD = 0.5;
var smoothTriggerCheck = function(value) {
if (value < SMOOTH_TRIGGER_RELEASE_THRESHOLD) {
_this.release();
}
};
var gripButtonCheck = function(value) {
if (value > GRIP_BUTTON_ACTIVE_THRESHOLD) {
_this.release();
}
}
var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05;
var NEAR_GRABBING_KINEMATIC = true;
var NEAR_GRABBING_IGNORE_IK = false;
_this.holdAction = Entities.addAction('hold', _this.holdableEntity, {
hand: _this.hand,
timeScale: NEAR_GRABBING_ACTION_TIMEFRAME,
relativePosition: _this.localPosition,
relativeRotation: _this.localRotation,
ttl: TOTAL_HOLD_LIFETIME,
kinematic: NEAR_GRABBING_KINEMATIC,
kinematicSetVelocity: true,
ignoreIK: NEAR_GRABBING_IGNORE_IK
});
if (_this.hand === 'left') {
_this.mapping.from([Controller.Standard.LT]).peek().to(smoothTriggerCheck);
_this.mapping.from([Controller.Standard.LB]).peek().to(gripButtonCheck);
_this.mapping.from([Controller.Standard.LeftGrip]).peek().to(gripButtonCheck);
} else {
_this.mapping.from([Controller.Standard.RT]).peek().to(smoothTriggerCheck);
_this.mapping.from([Controller.Standard.RB]).peek().to(gripButtonCheck);
_this.mapping.from([Controller.Standard.RightGrip]).peek().to(gripButtonCheck);
}
_this.mapping.enable();
Entities.callEntityMethod(_this.holdableEntity, 'startNearGrab', [_this.hand, MyAvatar.sessionUUID]);
},
update: function(deltaTime) {
// check if entity still exists
if (Object.keys(Entities.getEntityProperties(_this.holdableEntity)).length === 0) {
debugPrint('Could not find holdableEntity. Cleaning up!');
_this.cleanup();
return;
}
// check if controller released
if (_this.controllerWasReleased) {
debugPrint('Controller was already released. Cleaning up!');
_this.cleanup();
return;
}
Entities.callEntityMethod(_this.holdableEntity, 'continueNearGrab', [_this.hand, MyAvatar.sessionUUID]);
},
release: function() {
if (_this.controllerWasReleased) {
debugPrint('Controller was already released!');
return;
}
_this.controllerWasReleased = true;
Entities.callEntityMethod(_this.holdableEntity, 'releaseGrab', [_this.hand, MyAvatar.sessionUUID]);
Entities.deleteAction(_this.holdableEntity, _this.holdAction);
if (_this.onRelease !== null) {
_this.onRelease.call(_this);
}
debugPrint('We don\'t need a virtual controller after release. Cleaning up!');
_this.cleanup();
},
cleanup: function() {
if (_this.cleanedUp) {
return;
}
_this.mapping.disable();
Messages.sendMessage('Hifi-Hand-Disabler', 'none');
Script.update.disconnect(_this.update);
_this.cleanedUp = true;
}
};
var getUserData = function() {
try {
return JSON.parse(getEntityProperty(_entityID, 'userData'));
} catch (e) {
// e
debugPrint('Could not retrieve valid userData');
}
return null;
};
var getAttachmentData = function() {
var userDataObject = getUserData();
if (userDataObject === null || userDataObject.Attachment === undefined) {
return null;
}
return userDataObject.Attachment;
};
var getScriptTimestamp = function() {
return getEntityProperty(_entityID, 'scriptTimestamp');
}
this.preload = function(entityID) {
_entityID = entityID;
debugPrint('loaded ' + entityID);
};
this.unload = function() {
Messages.messageReceived.disconnect(messageHandler);
if (_virtualHoldController !== null) {
_virtualHoldController.cleanup();
}
}
function attachmentAction(joint, attachmentData) {
if (attachmentData === undefined) {
attachmentData = getAttachmentData();
}
if (attachmentData === null) {
return;
}
var otherJoint = null;
if (joint === undefined) {
if (attachmentData.joint.indexOf(LEFT_RIGHT_PLACEHOLDER) !== -1) {
joint = attachmentData.joint.replace(LEFT_RIGHT_PLACEHOLDER, _leftRightAttachToggle ? 'Left' : 'Right');
debugPrint('joint = ' + joint);
otherJoint = attachmentData.joint.replace(LEFT_RIGHT_PLACEHOLDER, _leftRightAttachToggle ? 'Right' : 'Left');
debugPrint('otherJoint = ' + otherJoint);
} else {
joint = attachmentData.joint;
}
// flip the state if attaching
if (attachmentData.action === 'attach') {
_leftRightAttachToggle = !_leftRightAttachToggle;
}
}
var currentAttachments = MyAvatar.getAttachmentsVariant();
var newAttachments = [];
currentAttachments.forEach(function(attachment) {
// do not add the current joint data to the list of the joint that we are changing
if (attachment.jointName !== joint && (attachmentData.action === 'attach' || attachment.jointName !== otherJoint)) {
newAttachments.push(attachment);
}
});
if (attachmentData.action === 'attach') {
var attachmentModel = getEntityProperty(_entityID, 'modelURL');
var newAttachment = {
jointName: joint,
modelUrl: attachmentModel
};
for (var key in attachmentData.options) {
if (attachmentData.options.hasOwnProperty(key)) {
newAttachment[key] = attachmentData.options[key];
}
}
newAttachments.push(newAttachment);
if (ATTACH_SOUND.downloaded) {
Audio.playSound(ATTACH_SOUND, {
position: getEntityProperty(_entityID, 'position'),
volume: 0.2,
localOnly: true
});
}
var modelPathParts = attachmentModel.split('/');
var fileName = modelPathParts[modelPathParts.length - 1];
UserActivityLogger.logAction('attachmentItemScript_attach', {
joint: joint,
model: fileName
});
} else if (attachmentData.action === 'clear') {
if (DETACH_SOUND.downloaded) {
Audio.playSound(DETACH_SOUND, {
position: getEntityProperty(_entityID, 'position'),
volume: 0.4,
localOnly: true
});
}
var joints = [joint];
if (otherJoint !== null) {
joints.push(joint);
}
UserActivityLogger.logAction('attachmentItemScript_detach', {
joints: joints
});
}
MyAvatar.setAttachmentsVariant(newAttachments);
}
this.startFarTrigger = function(entityID, args) {
var attachmentData = getAttachmentData();
// Only allow far-grab for the clear signs
if (attachmentData.action === 'clear') {
attachmentAction(undefined, attachmentData);
}
};
this.startNearTrigger = function(entityID, args) {
var attachmentData = getAttachmentData();
if (attachmentData.action === 'attach') {
var hand = args[0];
var entityProperties = Entities.getEntityProperties(entityID, ['position', 'rotation']);
var handPosition = hand === 'left' ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition();
var handRotation = hand === 'left' ? MyAvatar.getLeftPalmRotation() : MyAvatar.getRightPalmRotation();
var localPosition = Vec3.multiplyQbyV(Quat.inverse(handRotation), Vec3.subtract(entityProperties.position, handPosition));
var localRotation = Quat.multiply(Quat.inverse(handRotation), entityProperties.rotation);
debugPrint('trying to create entity');
var newProperties = Entities.getEntityProperties(_entityID);
debugPrint('received properties from ' + _entityID + ': ' + JSON.stringify(newProperties));
newProperties.position = Vec3.sum(handPosition, Vec3.multiplyQbyV(handRotation, localPosition));
newProperties.rotation = Quat.multiply(handRotation, localRotation);
newProperties.lifetime = TOTAL_HOLD_LIFETIME;
// delete some unused properties
delete newProperties.id;
delete newProperties.lastEdited;
delete newProperties.lastEditedBy;
delete newProperties.created;
delete newProperties.age;
delete newProperties.ageAsText;
delete newProperties.naturalDimensions;
delete newProperties.naturalPosition;
delete newProperties.boundingBox;
delete newProperties.actionData;
// collisionMask is already set:
delete newProperties.collidesWith;
if (newProperties.locked !== undefined) {
delete newProperties.locked;
}
if (newProperties.renderInfo !== undefined) {
delete newProperties.renderInfo;
}
if (newProperties.angularVelocity !== undefined) {
delete newProperties.angularVelocity;
}
delete newProperties.localRotation;
delete newProperties.localPosition;
delete newProperties.parentID;
delete newProperties.parentJointIndex;
delete newProperties.queryAACube;
delete newProperties.originalTextures;
delete newProperties.animation;
delete newProperties.owningAvatarID;
delete newProperties.clientOnly;
// We only want the server-side script in the locked item
if (newProperties.serverScripts !== undefined) {
delete newProperties.serverScripts;
}
try {
// attempt to modify the userData
var userData = JSON.parse(newProperties.userData);
userData.grabbableKey.wantsTrigger = false;
userData.grabbableKey.grabbable = true;
// userData.attachmentServer = _listeningChannel;
newProperties.userData = JSON.stringify(userData);
} catch (e) {
debugPrint('Something went wrong while trying to modify the userData.');
}
// must be dynamic for hold action:
newProperties.dynamic = true;
if (newProperties.shapeType === undefined || newProperties.shapeType === 'none') {
// must have dynamic shapeType for hold action:
newProperties.shapeType = 'box';
}
var entityID = Entities.addEntity(newProperties, true);
debugPrint('created ' + entityID + ' with properties: ' + JSON.stringify(newProperties));
// We don't need this attempts system down here anymore, but it works:
var attempts = 0;
var MAX_ATTEMPTS = 10;
var attachAttemptInterval = null;
var attachOnEntityFound = function() {
if (Object.keys(Entities.getEntityProperties(entityID, 'position')).length === 0) {
attempts++;
if (attempts >= MAX_ATTEMPTS) {
Script.clearInterval(attachAttemptInterval);
}
return;
}
Script.clearInterval(attachAttemptInterval);
if (_virtualHoldController !== null) {
_virtualHoldController.cleanup();
_virtualHoldController = null;
}
_virtualHoldController = new VirtualHoldController(hand, entityID, localPosition, localRotation);
_virtualHoldController.onRelease = function() {
debugPrint('Virtual hold controller released.');
};
};
attachAttemptInterval = Script.setInterval(attachOnEntityFound, 100);
} else if (attachmentData.action === 'clear') {
attachmentAction(undefined, attachmentData);
}
};
this.startNearGrab = function(entityID, args) {
debugPrint('NearGrabStarting ' + JSON.stringify(args));
_isGrabbing = true;
_attachmentData = getAttachmentData();
};
this.continueNearGrab = function(entityID, args) {
if (_isGrabbing && _attachmentData !== null) {
var snapDistance = _attachmentData.snapDistance !== undefined ? _attachmentData.snapDistance :
DEFAULT_SNAP_DISTANCE;
var entityPosition = Entities.getEntityProperties(entityID, 'position').position;
var joints = [];
var closestJoint = null;
if (_attachmentData.joint.indexOf(LEFT_RIGHT_PLACEHOLDER) !== -1) {
joints.push(_attachmentData.joint.replace(LEFT_RIGHT_PLACEHOLDER, 'Left'));
joints.push(_attachmentData.joint.replace(LEFT_RIGHT_PLACEHOLDER, 'Right'));
} else {
joints.push(_attachmentData.joint);
}
joints.forEach(function(joint) {
var targetJointPosition = MyAvatar.getJointPosition(joint);
var distance = Vec3.distance(entityPosition, targetJointPosition);
if (distance < snapDistance && (closestJoint === null || distance < closestJoint.distance)) {
closestJoint = {
name: joint,
distance: distance
};
}
});
if (closestJoint !== null) {
debugPrint('attach' + closestJoint.name);
attachmentAction(closestJoint.name);
Controller.triggerShortHapticPulse(0.3, args[0] === 'left' ? HAND_LEFT : HAND_RIGHT);
Entities.deleteEntity(_entityID);
}
}
};
this.releaseGrab = function(entityID, args) {
if (_isGrabbing) {
_isGrabbing = false;
Entities.deleteEntity(_entityID);
}
};
this.clickReleaseOnEntity = function(entityID, mouseEvent) {
if (mouseEvent.isLeftButton) {
attachmentAction();
}
};
});