494 lines
18 KiB
JavaScript
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();
|
|
}
|
|
};
|
|
});
|