// // gun.js // // created by Rebecca Stankus on 11/07/18 // Copyright 2018 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 // // This is the client script on the gun that will create the entities that it shoots /* global Pointers, Graphics */ function exponentialSmoothing(target, current) { var smoothingConstant = 0.75; return target * (1 - smoothingConstant) + current * smoothingConstant; } (function() { var _this; var TRIGGER_CONTROLS = [Controller.Standard.LT, Controller.Standard.RT]; var TRIGGER_THRESHOLD = 0.97; // How far down the trigger is pressed var AUDIO_VOLUME_LEVEL = 0.1; var BARREL_LOCAL_OFFSET = {x: 0, y: 0, z: 0}; // Can adjust the position of the gun barrel for different shapes var BARREL_LOCAL_DIRECTION = {x: 1000, y: 0, z: 0}; // Which direction the gun shoots in var DESKTOP_HOW_TO_IMAGE_URL = Script.resolvePath("assets/textures/desktopFireUnequip.png"); var DESKTOP_HOW_TO_IMAGE_WIDTH = 384; var DESKTOP_HOW_TO_IMAGE_HEIGHT = 128; var FIRE_KEY = "f"; var HAND = {LEFT: 0, RIGHT: 1}; var DESKTOP_HOW_TO_OVERLAY = true; var CAN_FIRE_AGAIN_TIMEOUT_MS = 250; var Y_OFFSET_FOR_WINDOW = 24; var VELOCITY_FACTOR = 5; var LIFETIME = 900; // How long each item should persist before being deleted var currentHand = null; var canShoot = true; var injector; var canFire = true; var mouseEquipAnimationHandler; var desktopHowToOverlay = null; var previousHMDActive; var previousLeftYPosition = 0; var previousLeftXRotation = 0; var previousLeftZRotation = 0; var previousRightYPosition = 0; var previousRightXRotation = 0; var previousRightZRotation = 0; var offsetMultiplier = 0.8; var treeProperties = { description: "CC_BY Alan Zimmerman", dimensions: { x: 0.06, y: 0.1, z: 0.06 }, restitution: 0.1, angularDamping: 1, modelURL: Script.resolvePath("assets/models/tree-spruce-low-poly.fbx"), name: "Holiday App Tree", gravity: { x: 0, y: -0.5, z: 0 }, lifetime: LIFETIME, dynamic: true, type: "Model", serverScripts: Script.resolvePath("itemGrow.js"), shapeType: "simple-compound", userData: "{\"grabbableKey\":{\"grabbable\":false}}" }; var starProperties = { description: "CC_BY Poly By Google", dimensions: { x: 0.025, y: 0.1, z: 0.1 }, lifetime: LIFETIME, restitution: 0, linearDamping: 0, angularDamping: 1, dynamic: true, modelURL: Script.resolvePath("assets/models/star.fbx"), name: "Holiday App Star", rotation: { w: 0.23338675498962402, x: 0.5388723611831665, y: 0.36150145530700684, z: -0.7241779565811157 }, type: "Model", script: Script.resolvePath("item.js"), serverScripts: Script.resolvePath("starSpawnLights.js"), shapeType: "simple-compound", userData: "{\"grabbableKey\":{\"grabbable\":true}}" }; var stockingProperties = { description: "CC_BY Alan Zimmerman", dimensions: { x: 0.08, y: 0.1, z: 0.025 }, lifetime: LIFETIME, restitution: 0.1, angularDamping: 1, dynamic: true, modelURL: Script.resolvePath("assets/models/stocking.fbx"), name: "Holiday App Stocking", script: Script.resolvePath("item.js"), shapeType: "simple-compound", type: "Model", userData: "{\"grabbableKey\":{\"grabbable\":true}}" }; var icicleProperties = { description: "CC_BY Alan Zimmerman", dimensions: { x: 0.0378, y: 0.1, z: 0.0378 }, lifetime: LIFETIME, restitution: 0.1, angularDamping: 1, dynamic: true, modelURL: Script.resolvePath("assets/models/icicle.fbx"), name: "Holiday App Icicle", script: Script.resolvePath("item.js"), shapeType: "simple-compound", type: "Model", userData: "{\"grabbableKey\":{\"grabbable\":true}}" }; var gingerbreadManProperties = { description: "CC_BY Alan Zimmerman", dimensions: { x: 0.0136, y: 0.1, z: 0.09 }, lifetime: LIFETIME, restitution: 0.1, angularDamping: 1, dynamic: true, modelURL: Script.resolvePath("assets/models/gingerbread.fbx"), name: "Holiday App Gingerbread Man", script: Script.resolvePath("edibleItem.js"), shapeType: "simple-compound", type: "Model", userData: "{\"grabbableKey\":{\"grabbable\":true}}" }; var ornamentProperties = { description: "CC_BY Alan Zimmerman", dimensions: { x: 0.08, y: 0.1, z: 0.08 }, lifetime: LIFETIME, restitution: 0.1, angularDamping: 1, dynamic: true, modelURL: Script.resolvePath("assets/models/ornament.fbx"), name: "Holiday App Ornament", script: Script.resolvePath("item.js"), shapeType: "simple-compound", type: "Model", userData: "{\"grabbableKey\":{\"grabbable\":true}}" }; var blueLightProperties = { description: "CC_BY Alan Zimmerman", dimensions: { x: 0.1, y: 0.1, z: 0.1 }, lifetime: LIFETIME, restitution: 0, linearDamping: 0, dynamic: true, modelURL: Script.resolvePath("assets/models/Glow-ball-blue.fbx"), name: "Holiday App Blue Light", script: Script.resolvePath("item.js"), shapeType: "simple-compound", type: "Model", userData: "{\"grabbableKey\":{\"grabbable\":false}}" }; var redLightProperties = { description: "CC_BY Alan Zimmerman", dimensions: { x: 0.1, y: 0.1, z: 0.1 }, lifetime: LIFETIME, textures: JSON.stringify({ Texture: Script.resolvePath("assets/images/ember-red.png") }), texture: Script.resolvePath("assets/images/particle-ember-red.png"), restitution: 0, linearDamping: 0, dynamic: true, modelURL: Script.resolvePath("assets/models/Glow-ball-blue.fbx"), name: "Holiday App Red Light", script: Script.resolvePath("item.js"), shapeType: "simple-compound", type: "Model", userData: "{\"grabbableKey\":{\"grabbable\":false}}" }; var greenLightProperties = { description: "CC_BY Alan Zimmerman", dimensions: { x: 0.1, y: 0.1, z: 0.1 }, lifetime: LIFETIME, textures: JSON.stringify({ Texture: Script.resolvePath("assets/images/ember-green.png") }), texture: Script.resolvePath("assets/images/particle-ember-green.png"), restitution: 0, linearDamping: 0, dynamic: true, modelURL: Script.resolvePath("assets/models/Glow-ball-blue.fbx"), name: "Holiday App Green Light", script: Script.resolvePath("item.js"), shapeType: "simple-compound", type: "Model", userData: "{\"grabbableKey\":{\"grabbable\":false}}" }; var yellowLightProperties = { description: "CC_BY Alan Zimmerman", dimensions: { x: 0.1, y: 0.1, z: 0.1 }, lifetime: LIFETIME, textures: JSON.stringify({ Texture: Script.resolvePath("assets/images/ember-yellow.png") }), texture: Script.resolvePath("assets/images/particle-ember-yellow.png"), restitution: 0, linearDamping: 0, dynamic: true, modelURL: Script.resolvePath("assets/models/Glow-ball-blue.fbx"), name: "Holiday App Yellow Light", script: Script.resolvePath("item.js"), shapeType: "simple-compound", type: "Model", userData: "{\"grabbableKey\":{\"grabbable\":false}}" }; var whiteLightProperties = { description: "CC_BY Alan Zimmerman", dimensions: { x: 0.1, y: 0.1, z: 0.1 }, lifetime: LIFETIME, textures: JSON.stringify({ Texture: Script.resolvePath("assets/images/ember-white.png") }), texture: Script.resolvePath("assets/images/particle-ember-white.png"), restitution: 0, linearDamping: 0, dynamic: true, modelURL: Script.resolvePath("assets/models/Glow-ball-blue.fbx"), name: "Holiday App White Light", script: Script.resolvePath("item.js"), shapeType: "simple-compound", type: "Model", userData: "{\"grabbableKey\":{\"grabbable\":false}}" }; var candyCaneProperties = { description: "CC_BY Poly By Google", dimensions: { x: 0.0365, y: 0.085, z: 0.008 }, lifetime: LIFETIME, restitution: 0, angularDamping: 1, linearDamping: 0, dynamic: true, modelURL: Script.resolvePath("assets/models/candyCane/275_Candy%20Cane.obj"), name: "Holiday App Candy Cane", script: Script.resolvePath("edibleItem.js"), serverScripts: Script.resolvePath("itemGrow.js"), shapeType: "simple-compound", type: "Model", userData: "{\"grabbableKey\":{\"grabbable\":true}}" }; var giftProperties = { description: "CC_BY Aaron Clifford", dimensions: { x: 0.061, y: 0.1, z: 0.061 }, lifetime: LIFETIME, restitution: 0, angularDamping: 1, linearDamping: 0, dynamic: true, modelURL: Script.resolvePath("assets/models/gift.fbx"), name: "Holiday App Gift", script: Script.resolvePath("item.js"), serverScripts: Script.resolvePath("itemGrow.js"), shapeType: "simple-compound", type: "Model", userData: "{\"grabbableKey\":{\"grabbable\":false}}" }; var giftsProperties = { description: "CC-BY Jarlan Perez", dimensions: { x: 0.1, y: 0.05, z: 0.08 }, lifetime: LIFETIME, restitution: 0, angularDamping: 1, linearDamping: 0, dynamic: true, modelURL: Script.resolvePath("assets/models/gifts.obj"), name: "Holiday App Gifts", script: Script.resolvePath("item.js"), serverScripts: Script.resolvePath("itemGrow.js"), shapeType: "simple-compound", type: "Model", userData: "{\"grabbableKey\":{\"grabbable\":false}}" }; var snowmanProperties = { description: "CC_BY Alex ?SAFFY? Safa", dimensions: { x: 0.05, y: 0.1, z: 0.07 }, lifetime: LIFETIME, restitution: 0, angularDamping: 1, linearDamping: 0, dynamic: true, modelURL: Script.resolvePath("assets/models/snowMan.fbx"), name: "Holiday App Snowman", script: Script.resolvePath("item.js"), serverScripts: Script.resolvePath("itemGrow.js"), shapeType: "simple-compound", type: "Model", userData: "{\"grabbableKey\":{\"grabbable\":false}}" }; var currentSpawnItem = treeProperties; // default item if nothing else has been selected function Gun() { _this = this; } Gun.prototype = { remotelyCallable: ['setSpawn'], // this will be called from the app when a button has been pressed preload: function(entityID) { _this.entityID = entityID; previousHMDActive = HMD.active; // storing whether or not the user is in HMD for later }, setSpawn: function(thisID, params) { var item = params[0]; switch (item) { case "redLight": currentSpawnItem = redLightProperties; break; case "blueLight": currentSpawnItem = blueLightProperties; break; case "yellowLight": currentSpawnItem = yellowLightProperties; break; case "greenLight": currentSpawnItem = greenLightProperties; break; case "whiteLight": currentSpawnItem = whiteLightProperties; break; case "stocking": currentSpawnItem = stockingProperties; break; case "icicle": currentSpawnItem = icicleProperties; break; case "gingerbreadMan": currentSpawnItem = gingerbreadManProperties; break; case "ornament": currentSpawnItem = ornamentProperties; break; case "tree": currentSpawnItem = treeProperties; break; case "snowman": currentSpawnItem = snowmanProperties; break; case "candyCane": currentSpawnItem = candyCaneProperties; break; case "gift": currentSpawnItem = giftProperties; break; case "gifts": currentSpawnItem = giftsProperties; break; case "star": currentSpawnItem = starProperties; break; default: currentSpawnItem = treeProperties; } }, // When the gun is equipped, store which hand it is in and whether the user is in HMD, listen for key release // events, and if the user is in desktop, animate their avatar to be holding the gun and set up an instructional // overlay so they know how to unequip and fire startEquip: function(id, params) { currentHand = params[0] === "left" ? 0 : 1; Controller.keyReleaseEvent.connect(_this.keyReleaseEvent); if (!HMD.active) { _this.addMouseEquipAnimation(); _this.addDesktopOverlay(); } previousHMDActive = HMD.active; }, // While the gun is held, if the user switches between HMD and desktop, change the gun setup to account for it // and listen for trigger pulls continueEquip: function(id, params) { if (currentHand === null) { return; } if (HMD.active !== previousHMDActive) { if (HMD.active) { _this.removeDesktopOverlay(); _this.removeMouseEquipAnimation(); } else { _this.addDesktopOverlay(); _this.addMouseEquipAnimation(); } previousHMDActive = HMD.active; } _this.toggleWithTriggerPressure(); }, // When the user releases the gun, remove the animation and instructional over lay if needed, stop listening // for key events and set the current hand to none releaseEquip: function(id, params) { currentHand = null; Controller.keyReleaseEvent.disconnect(_this.keyReleaseEvent); _this.removeMouseEquipAnimation(); _this.removeDesktopOverlay(); }, // On firing the gun, we trigger haptic feedback for 20ms at full strength. Then, we calculate the direction // to shoot the item based on the position and rotation of the gun and change the rotation of the item to match. // Then we create the item with velocity so it will be moving in the correct direction fire: function() { var HAPTIC_STRENGTH = 1; var HAPTIC_DURATION = 20; Controller.triggerHapticPulse(HAPTIC_STRENGTH, HAPTIC_DURATION, currentHand); var fireStart = this.getBarrelPosition(); var barrelDirection = this.getBarrelDirection(); var normalizedDirection = Vec3.normalize(barrelDirection); var velocity = Vec3.multiply(normalizedDirection, VELOCITY_FACTOR); currentSpawnItem.position = fireStart; currentSpawnItem.velocity = velocity; var gunRotation = Entities.getEntityProperties(_this.entityID, 'rotation').rotation; // the stocking and candy cane need to be rotated an extra 90 degrees if (currentSpawnItem.name === "Holiday App Stocking" || currentSpawnItem.name === "Holiday App Candy Cane") { var newRotation = Quat.fromPitchYawRollRadians(0, 90, 0 ); currentSpawnItem.rotation = Quat.multiply(gunRotation, newRotation); } else { currentSpawnItem.rotation = Quat.cancelOutRollAndPitch(gunRotation); } Entities.addEntity(currentSpawnItem); }, playSound: function(position, sound) { if (sound.downloaded) { if (injector) { injector.stop(); } injector = Audio.playSound(sound, { position: Entities.getEntityProperties(_this.entityID, 'position').position, volume: AUDIO_VOLUME_LEVEL }); } }, getBarrelPosition: function() { var properties = Entities.getEntityProperties(_this.entityID, ['position', 'rotation']); var barrelLocalPosition = Vec3.multiplyQbyV(properties.rotation, BARREL_LOCAL_OFFSET); var barrelWorldPosition = Vec3.sum(properties.position, barrelLocalPosition); return barrelWorldPosition; }, getBarrelDirection: function() { var rotation = Entities.getEntityProperties(_this.entityID, ['rotation']).rotation; var barrelAdjustedDirection = Vec3.multiplyQbyV(rotation, BARREL_LOCAL_DIRECTION); return barrelAdjustedDirection; }, // When the trigger is pressed past the set threshold while the gun is equipped, we either shoot or set a // variable to allow it to shoot next time the trigger is pressed toggleWithTriggerPressure: function() { var triggerValue = Controller.getValue(TRIGGER_CONTROLS[currentHand]); if (triggerValue >= TRIGGER_THRESHOLD) { if (canShoot === true) { _this.fire(); canShoot = false; } } else { canShoot = true; } }, // Adding the overlay that tells users how to operate the gun in desktop. We must find the size of the screen // and then position the overlay accordingly and store its state in the userData. If the overlay has already // been created, we can reuse it's former properties addDesktopOverlay: function() { _this.removeDesktopOverlay(); var userDataProperties = JSON.parse(Entities.getEntityProperties(_this.entityID, 'userData').userData); if (currentHand === null || !DESKTOP_HOW_TO_OVERLAY) { return; } var showOverlay = true; var otherHandDesktopOverlay = _this.getOtherHandDesktopOverlay(); if (otherHandDesktopOverlay !== null) { desktopHowToOverlay = userDataProperties.desktopHowToOverlay; showOverlay = false; } if (showOverlay) { var viewport = Controller.getViewportDimensions(); var windowHeight = viewport.y; desktopHowToOverlay = Overlays.addOverlay("image", { imageURL: DESKTOP_HOW_TO_IMAGE_URL, x: 0, y: windowHeight - DESKTOP_HOW_TO_IMAGE_HEIGHT - Y_OFFSET_FOR_WINDOW, width: DESKTOP_HOW_TO_IMAGE_WIDTH, height: DESKTOP_HOW_TO_IMAGE_HEIGHT, alpha: 1.0, visible: true }); userDataProperties.desktopHowToOverlay = desktopHowToOverlay; Entities.editEntity(_this.entityID, { userData: JSON.stringify(userDataProperties) }); } }, // checks userdata to reuse properties of the desktop overlay if possible getOtherHandDesktopOverlay: function() { var otherHandDesktopOverlay = null; if (currentHand !== null) { var handJointIndex = MyAvatar.getJointIndex(currentHand === HAND.LEFT ? "RightHand" : "LeftHand"); var children = Entities.getChildrenIDsOfJoint(MyAvatar.SELF_ID, handJointIndex); children.forEach(function(childID) { var userDataProperties = JSON.parse(Entities.getEntityProperties(childID, 'userData').userData); if (userDataProperties.desktopHowToOverlay) { otherHandDesktopOverlay = userDataProperties.desktopHowToOverlay; } }); } return otherHandDesktopOverlay; }, removeDesktopOverlay: function() { var otherHandDesktopOverlay = _this.getOtherHandDesktopOverlay(); if (desktopHowToOverlay !== null && otherHandDesktopOverlay === null) { Overlays.deleteOverlay(desktopHowToOverlay); desktopHowToOverlay = null; } }, addMouseEquipAnimation: function() { _this.removeMouseEquipAnimation(); if (currentHand === HAND.LEFT) { mouseEquipAnimationHandler = MyAvatar.addAnimationStateHandler(_this.leftHandMouseEquipAnimation, []); } else if (currentHand === HAND.RIGHT) { mouseEquipAnimationHandler = MyAvatar.addAnimationStateHandler(_this.rightHandMouseEquipAnimation, []); } }, removeMouseEquipAnimation: function() { if (mouseEquipAnimationHandler) { mouseEquipAnimationHandler = MyAvatar.removeAnimationStateHandler(mouseEquipAnimationHandler); } }, // Here we calculate a position for the avatars left hand and the rest of the arm will position itself around // this. We find the length of the arm, and position of the head (if a "Head" joint exists), then set the // postion of the hand relative to them. leftHandMouseEquipAnimation: function() { var result = {}; result.leftHandType = 0; var leftHandPosition = MyAvatar.getJointPosition("LeftHand"); var leftShoulderPosition = MyAvatar.getJointPosition("LeftShoulder"); var shoulderToHandDistance = Vec3.distance(leftHandPosition, leftShoulderPosition); var cameraForward = Quat.getForward(Camera.orientation); var newForward = Vec3.multiply(cameraForward, shoulderToHandDistance); var newLeftHandPosition = Vec3.sum(leftShoulderPosition, newForward); var newLeftHandPositionAvatarFrame = Vec3.subtract(newLeftHandPosition, MyAvatar.position); var headIndex = MyAvatar.getJointIndex("Head"); var offset = 0.5; if (headIndex) { offset = offsetMultiplier* MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y; } result.leftHandPosition = Vec3.multiply(offset, {x: 0.25, y: 0.6, z: 0.9}); var yPosition = exponentialSmoothing(newLeftHandPositionAvatarFrame.y, previousLeftYPosition); result.leftHandPosition.y = yPosition; previousLeftYPosition = yPosition; var leftHandPositionNew = Vec3.sum(MyAvatar.position, result.leftHandPosition); var rotation = Quat.lookAtSimple(leftHandPositionNew, leftShoulderPosition); var rotationAngles = Quat.safeEulerAngles(rotation); var xRotation = exponentialSmoothing(rotationAngles.x, previousLeftXRotation); var zRotation = exponentialSmoothing(rotationAngles.z, previousLeftZRotation); var newRotation = Quat.fromPitchYawRollDegrees(rotationAngles.x, 0, rotationAngles.z); previousLeftXRotation = xRotation; previousLeftZRotation = zRotation; result.leftHandRotation = Quat.multiply(newRotation, Quat.fromPitchYawRollDegrees(80, -20, -90)); return result; }, // see "leftHandMouseEquipAnimation" description rightHandMouseEquipAnimation: function() { var result = {}; result.rightHandType = 0; var rightHandPosition = MyAvatar.getJointPosition("RightHand"); var rightShoulderPosition = MyAvatar.getJointPosition("RightShoulder"); var shoulderToHandDistance = Vec3.distance(rightHandPosition, rightShoulderPosition); var cameraForward = Quat.getForward(Camera.orientation); var newForward = Vec3.multiply(cameraForward, shoulderToHandDistance); var newRightHandPosition = Vec3.sum(rightShoulderPosition, newForward); var newRightHandPositionAvatarFrame = Vec3.subtract(newRightHandPosition, MyAvatar.position); var headIndex = MyAvatar.getJointIndex("Head"); var offset = 0.5; if (headIndex) { offset = offsetMultiplier * MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y; } result.rightHandPosition = Vec3.multiply(offset, {x: -0.25, y: 0.6, z: 0.9}); var yPosition = exponentialSmoothing(newRightHandPositionAvatarFrame.y, previousRightYPosition); result.rightHandPosition.y = yPosition; previousRightYPosition = yPosition; var rightHandPositionNew = Vec3.sum(MyAvatar.position, result.rightHandPosition); var rotation = Quat.lookAtSimple(rightHandPositionNew, rightShoulderPosition); var rotationAngles = Quat.safeEulerAngles(rotation); var xRotation = exponentialSmoothing(rotationAngles.x, previousRightXRotation); var zRotation = exponentialSmoothing(rotationAngles.z, previousRightZRotation); var newRotation = Quat.fromPitchYawRollDegrees(rotationAngles.x, 0, rotationAngles.z); previousRightXRotation = xRotation; previousRightZRotation = zRotation; result.rightHandRotation = Quat.multiply(newRotation, Quat.fromPitchYawRollDegrees(80, 0, 90)); return result; }, // listening for key events in desktop mode and preventing the gun from firing multiple times in succession // by setting a "canShoot" variable keyReleaseEvent: function(event) { if ((event.text).toLowerCase() === FIRE_KEY) { if (canFire) { canFire = false; _this.fire(); Script.setTimeout(function() { canFire = true; }, CAN_FIRE_AGAIN_TIMEOUT_MS); } } }, unload: function() { this.removeMouseEquipAnimation(); this.removeDesktopOverlay(); } }; return new Gun(); });