From 6763ca9b27bc073904d1767dcce0c217fcb8bc7e Mon Sep 17 00:00:00 2001 From: Rob Kayson <rkayson@gmail.com> Date: Mon, 3 Apr 2017 11:55:47 -0700 Subject: [PATCH] added tetherball create and entity scripts --- scripts/tutorials/createTetherballStick.js | 73 ++++ .../entity_scripts/tetherballStick.js | 411 ++++++++++++++++++ 2 files changed, 484 insertions(+) create mode 100644 scripts/tutorials/createTetherballStick.js create mode 100644 scripts/tutorials/entity_scripts/tetherballStick.js diff --git a/scripts/tutorials/createTetherballStick.js b/scripts/tutorials/createTetherballStick.js new file mode 100644 index 0000000000..a975c3a247 --- /dev/null +++ b/scripts/tutorials/createTetherballStick.js @@ -0,0 +1,73 @@ +"use strict"; +/* jslint vars: true, plusplus: true, forin: true*/ +/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// createTetherballStick.js +// +// Created by Triplelexx on 17/03/04 +// Updated by MrRoboman on 17/03/26 +// Copyright 2017 High Fidelity, Inc. +// +// Creates an equippable stick with a tethered ball +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +var STICK_SCRIPT_URL = Script.resolvePath("./entity_scripts/tetherballStick.js?v=" + Date.now()); +var STICK_MODEL_URL = "http://hifi-content.s3.amazonaws.com/caitlyn/production/raveStick/newRaveStick2.fbx"; + +var avatarOrientation = MyAvatar.orientation; +avatarOrientation = Quat.safeEulerAngles(avatarOrientation); +avatarOrientation.x = 0; +avatarOrientation = Quat.fromVec3Degrees(avatarOrientation); +var startPosition = Vec3.sum(MyAvatar.getRightPalmPosition(), Vec3.multiply(1, Quat.getFront(avatarOrientation))); + +var STICK_PROPERTIES = { + type: 'Model', + name: "TetherballStick Stick", + modelURL: STICK_MODEL_URL, + position: startPosition, + rotation: MyAvatar.orientation, + dimensions: { + x: 0.0651, + y: 0.0651, + z: 0.5270 + }, + script: STICK_SCRIPT_URL, + color: { + red: 200, + green: 0, + blue: 20 + }, + shapeType: 'box', + lifetime: 3600, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true, + spatialKey: { + rightRelativePosition: { + x: 0.05, + y: 0, + z: 0 + }, + leftRelativePosition: { + x: -0.05, + y: 0, + z: 0 + }, + relativeRotation: { + x: 0.4999999701976776, + y: 0.4999999701976776, + z: -0.4999999701976776, + w: 0.4999999701976776 + } + }, + invertSolidWhileHeld: true + }, + ownerID: MyAvatar.sessionUUID + }) +}; + +Entities.addEntity(STICK_PROPERTIES); +Script.stop(); diff --git a/scripts/tutorials/entity_scripts/tetherballStick.js b/scripts/tutorials/entity_scripts/tetherballStick.js new file mode 100644 index 0000000000..6fb249dc28 --- /dev/null +++ b/scripts/tutorials/entity_scripts/tetherballStick.js @@ -0,0 +1,411 @@ +"use strict"; +/* jslint vars: true, plusplus: true, forin: true*/ +/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// tetherballStick.js +// +// Created by Triplelexx on 17/03/04 +// Updated by MrRoboman on 17/03/26 +// Copyright 2017 High Fidelity, Inc. +// +// Entity script for an equippable stick with a tethered ball +// +// 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 _this; + + var NULL_UUID = "{00000000-0000-0000-0000-000000000000}"; + var ENTITY_CHECK_INTERVAL = 5; // time in sec + var LINE_WIDTH = 0.02; + var BALL_SIZE = 0.175; + var BALL_DAMPING = 0.5; + var BALL_ANGULAR_DAMPING = 0.5; + var BALL_RESTITUTION = 0.4; + var BALL_DENSITY = 1000; + var MAX_DISTANCE_MULTIPLIER = 2; + var ACTION_DISTANCE = 0.25; + var ACTION_TIMESCALE = 0.025; + var ACTION_TAG = "TetherballStick Action"; + var BALL_NAME = "TetherballStick Ball"; + var LINE_NAME = "TetherballStick Line"; + var COLLISION_SOUND_URL = "http://public.highfidelity.io/sounds/Footsteps/FootstepW3Left-12db.wav"; + var INTERACT_SOUND_URL = "http://hifi-public.s3.amazonaws.com/sounds/color_busters/powerup.wav"; + var INTERACT_SOUND_VOLUME = 0.2; + var USE_INTERACT_SOUND = false; + var AVATAR_CHECK_RANGE = 5; // in meters + var TIP_OFFSET = 0.26; + var LIFETIME = 3600; + + tetherballStick = function() { + _this = this; + return; + }; + + tetherballStick.prototype = { + lastCheckForEntities: ENTITY_CHECK_INTERVAL, + originalDimensions: null, + ownerID: NULL_UUID, + userID: NULL_UUID, + ballID: NULL_UUID, + lineID: NULL_UUID, + actionID: NULL_UUID, + INTERACT_SOUND: undefined, + + preload: function(entityID) { + this.entityID = entityID; + if (USE_INTERACT_SOUND) { + this.INTERACT_SOUND = SoundCache.getSound(INTERACT_SOUND_URL); + } + this.originalDimensions = Entities.getEntityProperties(this.entityID).dimensions; + Script.update.connect(this.update); + }, + + unload: function() { + Script.update.disconnect(this.update); + }, + + update: function(dt) { + // _this during update due to loss of scope + if (_this.lastCheckForEntities >= ENTITY_CHECK_INTERVAL) { + _this.lastCheckForEntities = 0; + // everyone will be running this update loop + // the only action is to increment the timer till ENTITY_CHECK_INTERVAL + // when that is reached the userdata ownerID is simply validated by everyone + // if it's invalid call setNewOwner, to set a new valid user + if (!_this.checkForOwner()) { + return; + } + + // only one person should ever be running this far + if (_this.ownerID == MyAvatar.sessionUUID) { + _this.checkForEntities(); + } + } else { + _this.lastCheckForEntities += dt; + } + + _this.drawLine(); + }, + + getScale: function() { + var stickProps = Entities.getEntityProperties(this.entityID); + return stickProps.dimensions.z / this.originalDimensions.z; + }, + + checkForOwner: function() { + var stickProps = Entities.getEntityProperties(this.entityID); + try { + var stickData = JSON.parse(stickProps.userData); + var owner = AvatarManager.getAvatar(stickData.ownerID); + if (owner.sessionUUID !== undefined) { + this.ownerID = owner.sessionUUID; + return true; + } else { + // UUID is invalid + this.setNewOwner(); + return false; + } + } catch (e) { + // all other errors + this.setNewOwner(); + return false; + } + }, + + setNewOwner: function() { + var stickProps = Entities.getEntityProperties(this.entityID); + // I only want the closest client to be in charge of creating objects. + // the AvatarList also contains a null representing MyAvatar, + // a new array is created to start with containing the proper UUID + var avatarList = AvatarList.getAvatarIdentifiers() + .filter(Boolean) // remove the null + .concat(MyAvatar.sessionUUID); // add the ID + + var closestAvatarID = undefined; + var closestAvatarDistance = AVATAR_CHECK_RANGE; + avatarList.forEach(function(avatarID) { + var avatar = AvatarList.getAvatar(avatarID); + var distFrom = Vec3.distance(avatar.position, stickProps.position); + if (distFrom < closestAvatarDistance) { + closestAvatarDistance = distFrom; + closestAvatarID = avatarID; + } + }); + + // add reference to userData + try { + var stickData = JSON.parse(stickProps.userData); + stickData.ownerID = closestAvatarID; //NOTE: this will assign undefined if all avatars are further than AVATAR_CHECK_RANGE + Entities.editEntity(this.entityID, { + userData: JSON.stringify(stickData) + }); + } catch (e) { + } + }, + + checkForEntities: function() { + if (!this.hasBall()) { + this.createBall(); + } + if (!this.hasAction()) { + this.createAction(); + } + }, + + playInteractionSound: function() { + var stickProps = Entities.getEntityProperties(this.entityID); + var INTERACT_SOUND_OPTIONS = { + volume: INTERACT_SOUND_VOLUME, + loop: false, + position: stickProps.position + }; + Audio.playSound(this.INTERACT_SOUND, INTERACT_SOUND_OPTIONS); + }, + + getTipPosition: function() { + var stickProps = Entities.getEntityProperties(this.entityID); + + var scale = this.getScale(); + var frontVec = Quat.getFront(stickProps.rotation); + var frontOffset = Vec3.multiply(frontVec, TIP_OFFSET * scale); + var tipPosition = Vec3.sum(stickProps.position, frontOffset); + + return tipPosition; + }, + + getStickFrontPosition: function() { + var stickProps = Entities.getEntityProperties(this.entityID); + var stickFront = Quat.getFront(stickProps.rotation); + var tipPosition = this.getTipPosition(); + return Vec3.sum(tipPosition, Vec3.multiply(TIP_OFFSET*.4, stickFront)); + }, + + capBallDistance: function() { + var stickProps = Entities.getEntityProperties(this.entityID); + var ballProps = Entities.getEntityProperties(this.ballID); + var tipPosition = this.getTipPosition(); + var scale = this.getScale(); + var distance = Vec3.distance(tipPosition, ballProps.position); + var maxDistance = ACTION_DISTANCE * MAX_DISTANCE_MULTIPLIER * scale; + + if(distance > maxDistance) { + var direction = Vec3.normalize(Vec3.subtract(ballProps.position, tipPosition)); + var newPosition = Vec3.sum(tipPosition, Vec3.multiply(maxDistance, direction)); + Entities.editEntity(this.ballID, { + position: newPosition + }) + } + }, + + startNearGrab: function(id, params) { + var stickProps = Entities.getEntityProperties(this.entityID); + // set variables from data in case someone else created the components + try { + var stickData = JSON.parse(stickProps.userData); + this.userID = MyAvatar.sessionUUID; + this.ballID = stickData.ballID; + this.actionID = stickData.actionID; + if(!this.hasLine()) { + this.createLine(); + } + if (USE_INTERACT_SOUND) { + var hand = params[0]; + Controller.triggerShortHapticPulse(1, hand); + this.playInteractionSound(); + } + } catch (e) { } + }, + + releaseGrab: function(id, params) { + this.userID = NULL_UUID; + if (USE_INTERACT_SOUND) { + this.playInteractionSound(); + } + }, + + continueNearGrab: function(id, params) { + this.repositionAction(); + this.updateDimensions(); + this.capBallDistance(); + }, + + createLine: function() { + var stickProps = Entities.getEntityProperties(this.entityID); + this.lineID = Entities.addEntity({ + type: "PolyLine", + name: LINE_NAME, + color: { + red: 0, + green: 120, + blue: 250 + }, + textures: "https://hifi-public.s3.amazonaws.com/alan/Particles/Particle-Sprite-Smoke-1.png", + position: stickProps.position, + dimensions: { + x: 10, + y: 10, + z: 10 + }, + lifetime: LIFETIME + }); + }, + + deleteLine: function() { + Entities.deleteEntity(this.lineID); + this.lineID = NULL_UUID; + }, + + hasLine: function() { + var lineProps = Entities.getEntityProperties(this.lineID); + return lineProps.name == LINE_NAME; + }, + + createBall: function() { + var stickProps = Entities.getEntityProperties(this.entityID); + + this.ballID = Entities.addEntity({ + type: "Model", + modelURL: "http://hifi-content.s3.amazonaws.com/Examples%20Content/production/marblecollection/Star.fbx", + name: BALL_NAME, + shapeType: "Sphere", + position: this.getStickFrontPosition(), + lifetime: LIFETIME, + collisionSoundURL: COLLISION_SOUND_URL, + dimensions: { + x: BALL_SIZE, + y: BALL_SIZE, + z: BALL_SIZE + }, + gravity: { + x: 0.0, + y: -9.8, + z: 0.0 + }, + damping: BALL_DAMPING, + angularDamping: BALL_ANGULAR_DAMPING, + density: BALL_DENSITY, + restitution: BALL_RESTITUTION, + dynamic: true, + collidesWith: "static,dynamic,otherAvatar,", + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }) + }); + + // add reference to userData + try { + var stickData = JSON.parse(stickProps.userData); + stickData.ballID = this.ballID; + Entities.editEntity(this.entityID, { + userData: JSON.stringify(stickData) + }); + } catch (e) {} + }, + + hasBall: function() { + // validate the userData to handle unexpected item deletion + var stickProps = Entities.getEntityProperties(this.entityID); + try { + var data = JSON.parse(stickProps.userData); + var ballProps = Entities.getEntityProperties(data.ballID); + return ballProps.name == BALL_NAME; + } catch (e) { + return false; + } + }, + + createAction: function() { + var stickProps = Entities.getEntityProperties(this.entityID); + this.actionID = Entities.addAction("offset", this.ballID, { + pointToOffsetFrom: stickProps.position, + linearDistance: ACTION_DISTANCE, + tag: ACTION_TAG, + linearTimeScale: ACTION_TIMESCALE + }); + + // add reference to userData + try { + var stickData = JSON.parse(stickProps.userData); + stickData.actionID = this.actionID; + Entities.editEntity(this.entityID, { + userData: JSON.stringify(stickData) + }); + } catch (e) {} + }, + + hasAction: function() { + // validate the userData to handle unexpected item deletion + var stickProps = Entities.getEntityProperties(this.entityID); + try { + var stickData = JSON.parse(stickProps.userData); + var actionProps = Entities.getActionArguments(stickData.ballID, stickData.actionID); + return actionProps.tag == ACTION_TAG; + } catch (e) { + return false; + } + }, + + hasRequiredComponents: function() { + return this.hasBall() && this.hasAction() && this.hasLine(); + }, + + updateDimensions: function() { + var scale = this.getScale(); + Entities.editEntity(this.ballID, { + dimensions: { + x: BALL_SIZE * scale, + y: BALL_SIZE * scale, + z: BALL_SIZE * scale + } + }); + }, + + drawLine: function() { + if(!this.hasLine()) + return; + + var stickProps = Entities.getEntityProperties(this.entityID); + var tipPosition = this.getTipPosition(); + var ballProps = Entities.getEntityProperties(this.ballID); + var cameraQuat = Vec3.multiplyQbyV(Camera.getOrientation(), Vec3.UNIT_NEG_Z); + var linePoints = []; + var normals = []; + var strokeWidths = []; + linePoints.push(Vec3.ZERO); + normals.push(cameraQuat); + strokeWidths.push(LINE_WIDTH); + linePoints.push(Vec3.subtract(ballProps.position, tipPosition)); + normals.push(cameraQuat); + strokeWidths.push(LINE_WIDTH); + + var lineProps = Entities.getEntityProperties(this.lineID); + Entities.editEntity(this.lineID, { + linePoints: linePoints, + normals: normals, + strokeWidths: strokeWidths, + position: tipPosition, + }); + }, + + repositionAction: function() { + var stickProps = Entities.getEntityProperties(this.entityID); + var tipPosition = this.getTipPosition(); + var scale = this.getScale(); + + Entities.updateAction(this.ballID, this.actionID, { + pointToOffsetFrom: tipPosition, + linearDistance: ACTION_DISTANCE * scale, + tag: ACTION_TAG, + linearTimeScale: ACTION_TIMESCALE + }); + } + }; + + // entity scripts should return a newly constructed object of our type + return new tetherballStick(); +});