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();
+});