// // Hookgun.js // examples // // Created by Matti 'Menithal' Lahtinen on 5/8/16. // Copyright 2016 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 // (function() { var BULLET_GRAVITY = { x: 0, y: 0, z: 0 }; var BULLET_DIMENSIONS = { x: 0.02, y: 0.02, z: 0.5 }; var BULLET_COLOR = { red: 255, green: 255, blue: 255 }; var BULLET_LINEAR_DAMPING = 0; var RELOAD_TIME = 4; var RELOAD_THRESHOLD = 0.95; var GUN_TIP_FWD_OFFSET = 0.08; var BULLET_SPAWN_OFFSET = 0.4; var GUN_TIP_UP_OFFSET = 0; var GUN_FORCE = 80; var HOOK_LIFE_TIME = 20; var MAX_DISTANCE = 150; var TRIGGER_CONTROLS = [ Controller.Standard.LT, Controller.Standard.RT ]; var SHOOTING_1_SOUND = SoundCache.getSound("http://www.norteclabs.com/HF/resources/HookGunFire1.wav"); var SHOOTING_2_SOUND = SoundCache.getSound("http://www.norteclabs.com/HF/resources/HookGunFire2.wav"); var ROPE_SOUND = SoundCache.getSound("http://www.norteclabs.com/HF/resources/RopeSwoosh.wav"); var PULL_1_LOOP_SOUND = SoundCache.getSound("http://www.norteclabs.com/HF/resources/RolePull.wav"); var PULL_2_LOOP_SOUND = SoundCache.getSound("http://www.norteclabs.com/HF/resources/WindWindingRopePull.wav"); var soundInjector; /* Helper to determine if js object is empty */ function emptyObjectCheck(obj) { return Object.keys(obj).length === 0 && JSON.stringify(obj) === JSON.stringify({}); } // Gets the weapon uuid, and returns an object with found, and if it is true, // has additional info such as the velocity, where it should spawn, but also provides information about weapon function getWeaponBulletInfo(uuid) { var properties = Entities.getEntityProperties(uuid, ["rotation", "position"]) if (emptyObjectCheck(properties)) { return { found: false }; } var frontVector = Quat.getFront(properties.rotation); var frontOffset = Vec3.multiply(frontVector, GUN_TIP_FWD_OFFSET); var bulletSpawnOffset = Vec3.multiply(frontVector, GUN_TIP_FWD_OFFSET + BULLET_SPAWN_OFFSET); var tip = Vec3.multiply(frontVector, GUN_TIP_FWD_OFFSET + BULLET_SPAWN_OFFSET); var upVector = Quat.getUp(properties.rotation); var upOffset = Vec3.multiply(upVector, GUN_TIP_UP_OFFSET); var forwardVec = Quat.getFront(Quat.multiply(properties.rotation, Quat.fromPitchYawRollDegrees(0, 0, 0))); forwardVec = Vec3.normalize(forwardVec); forwardVec = Vec3.multiply(forwardVec, GUN_FORCE); forwardVec = Vec3.sum(MyAvatar.velocity, forwardVec); // lets keep it relative var gunTipPosition = Vec3.sum(properties.position, frontOffset); var bulletSpawnPosition = Vec3.sum(properties.position, bulletSpawnOffset); return { found: true, velocity: forwardVec, tipSpawn: bulletSpawnPosition, tip: gunTipPosition, rotation: properties.rotation, position: properties.position }; } /* Hook prototype constructor Handles everything from the hook hitting and rope attached to it as a child. */ function Hook(sourceId) { this.sourceId = sourceId; print("Creating Hook"); var weapon = getWeaponBulletInfo(this.sourceId); if (!weapon.found) { print("Invalid sourceID provided.") return; } this.hookId = Entities.addEntity({ type: "Sphere", name: "Hook", visible: 0, color: BULLET_COLOR, dimensions: BULLET_DIMENSIONS, damping: BULLET_LINEAR_DAMPING, gravity: BULLET_GRAVITY, dynamic: true, lifetime: HOOK_LIFE_TIME, rotation: weapon.rotation, collisionMask:15, collidesWith:"static,dynamic,kinematic,myAvatar,", position: weapon.tipSpawn, velocity: weapon.velocity, restitution: 0 }); this.ropeId = Entities.addEntity({ collisionless: true, type: "Box", name: "Rope", dimensions: { x: 0.01, y: 0.01, z: 0.01 }, color: { red: 0, green: 0, blue: 0 }, lifetime: HOOK_LIFE_TIME, position: Entities.getEntityProperties(this.hookId, ["position"]).position }); this.secured = false; var hook = this; var timer = 0; this.updateThread = function(dt) { timer += dt; try { var state = getWeaponBulletInfo(hook.sourceId) var hookObject = Entities.getEntityProperties(hook.hookId, ["position"]); if (!state.found) { hook.cleanUp(hook); return; } hook.connectRope(state.tip, hookObject.position, hook); if (timer > HOOK_LIFE_TIME * 0.90) { hook.cleanup(hook); } } catch (e) { // Backup plan. If hook is deleted. just cancel self. print("Failed on cycle. Disconnecting ") hook.cleanup(hook); Script.update.disconnect(hook.updateThread); } }; this.collision = function(entityA, entityB, collision) { Script.removeEventHandler(entityA, "collisionWithEntity", hook.collide); var entity = Entities.getEntityProperties(entityA); var collisionEntity = Entities.getEntityProperties(entityB, ["userdata"]); /* if (collisionEntity.userData !== "") { var userData = JSON.parse(collisionEntity.userData) var isHookable = userData.hookable !== undefined ? (userData.hookable !== undefined? userData.hookable:true) : true; if (!isHookable) { print("Collided", collisionEntity.id,scollisionEntity.userData); hook.cleanup(hook); return; } }*/ hook.secured = true; Entities.editEntity(entityA, { parentID: entityB, position: collision.contactPoint, dynamic: 0, collisionless: 1, velocity: { x: 0, y: 0, z: 0 } }); }; Script.addEventHandler(this.hookId, "collisionWithEntity", this.collision); Script.update.connect(this.updateThread); }; /* Hook Prototype definition Due to some issues with keeping relativity, each function should keep context */ Hook.prototype = { connectRope: function(fromPosition, toPosition, context) { var ropeObject = Entities.getEntityProperties(context.ropeId, ["dimensions"]); var rotation = Quat.multiply(Quat.lookAt(toPosition, fromPosition, Vec3.UP), { w: 0.707, z: 0, x: 0.707, y: 0 }); var distance = Vec3.distance(fromPosition, toPosition); if (distance >= MAX_DISTANCE) { context.cleanup(context); return; } var scale = ropeObject.dimensions; scale.y = distance; var between = Vec3.mix(fromPosition, toPosition, 0.5); Entities.editEntity(this.ropeId, { dimensions: scale, rotation: rotation, position: between }); }, cleanup: function(context) { context.secured = false; Entities.deleteEntity(context.ropeId); Entities.deleteEntity(context.hookId); Script.update.disconnect(context.updateThread); context.ropeId = null; context.hookId = null; } } /* HookGun prototype constructor. Bound to the entity that this script is attached to. Each Hookgun has a hook object that defines the hook when fired, and if it is secured to an object or not. Based on popgun script */ var GRAVITY = { x: 0, y: -9, z: 0 }; var GlobalThrust = GRAVITY; // This is shared between all JS hookgun running locally. var ActiveHand = { 0: { secured: false }, 1: { secured: false } }; var ActiveEquip = false; function HookGun() { this.hook = { secured: false }; return; }; HookGun.prototype = { hand: null, canShoot: true, hookBullet: { secured: false }, canShootTimeout: null, updateThread: null, startEquip: function(entityID, args) { this.hand = args[0] == "left" ? 0 : 1; var self = this; ActiveHand[this.hand] = false; // Doing this here instead of in the prototype, // to maintain Context. "this" context is lost when it is a function passed to the Script.update thread if (!ActiveEquip) { this.updateThread = function(dt) { var secured = ActiveHand[0].secured | ActiveHand[1].secured; if (secured) { var hookEntity = Entities.getEntityProperties(self.hookBullet.hookId, ["position"]); var direction = self.calculateThrustDirection(MyAvatar.position, hookEntity.position); var distance = Vec3.distance(MyAvatar.position, hookEntity.position); GlobalThrust = Vec3.mix(GlobalThrust, Vec3.multiply(direction, 8000 * (distance / (MAX_DISTANCE / 2))), .5); } else { GlobalThrust = Vec3.mix(GlobalThrust, Vec3.multiply(GRAVITY, 100), .1); } MyAvatar.addThrust(Vec3.multiply(GlobalThrust, dt)); } ActiveEquip = true; } Script.update.connect(this.updateThread); // Start monitoring if hook is secured or not. }, calculateThrustDirection: function(fromPosition, toPosition) { var rotation = Quat.lookAt(fromPosition, toPosition, Vec3.UP); return Vec3.normalize(Quat.getFront(rotation)); }, continueEquip: function(entityID, args) { ActiveEquip = true; this.checkTriggerPressure(entityID, this.hand); }, releaseEquip: function(entityID, args) { var _this = this; Script.update.disconnect(this.updateThread) this.hookBullet = { secured: false }; ActiveEquip = false; }, checkTriggerPressure: function(entityID, gunHand) { this.triggerValue = Controller.getValue(TRIGGER_CONTROLS[gunHand]); if (this.triggerValue >= RELOAD_THRESHOLD && this.canShoot) { this.canShoot = false; var gunProperties = Entities.getEntityProperties(this.entityID, ["position", "rotation"]); this.fire(entityID, gunHand, gunProperties); ActiveHand[gunHand] = this.hookBullet; } else if (this.triggerValue < RELOAD_THRESHOLD && !this.canShoot) { this.canShoot = true; this.hookBullet.cleanup(this.hookBullet); this.hookBullet = { secured: false }; ActiveHand[gunHand] = this.hookBullet; } return; }, fire: function(entityID, gunHand,gunProperties) { var _this = this; var forwardVec = Quat.getFront(Quat.multiply(gunProperties.rotation, Quat.fromPitchYawRollDegrees(0, 0, 0))); forwardVec = Vec3.normalize(forwardVec); forwardVec = Vec3.multiply(forwardVec, GUN_FORCE); var shot = new Hook(entityID); this.hookBullet = shot; if (Math.random() > 0.5) { Audio.playSound(SHOOTING_1_SOUND, { volume: 0.25, position: gunProperties.position }); } else { Audio.playSound(SHOOTING_2_SOUND, { volume: 0.25, position: gunProperties.position }); } }, preload: function(entityID) { this.entityID = entityID; } }; var gun = new HookGun(); return gun; });