diff --git a/examples/frisbee.js b/examples/frisbee.js new file mode 100644 index 0000000000..e893a29309 --- /dev/null +++ b/examples/frisbee.js @@ -0,0 +1,443 @@ +// +// frisbee.js +// examples +// +// Created by Thijs Wenker on 7/5/14. +// Copyright 2014 High Fidelity, Inc. +// +// Requirements: Razer Hydra's +// +// Fun game to throw frisbee's to eachother. Hold the trigger on any of the hydra's to create or catch a frisbee. +// +// Tip: use this together with the squeezeHands.js script to make it look nicer. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +Script.include("toolBars.js"); + +const LEFT_PALM = 0; +const LEFT_TIP = 1; +const LEFT_BUTTON_FWD = 5; +const LEFT_BUTTON_3 = 3; + +const RIGHT_PALM = 2; +const RIGHT_TIP = 3; +const RIGHT_BUTTON_FWD = 11; +const RIGHT_BUTTON_3 = 9; + +const FRISBEE_RADIUS = 0.08; +const GRAVITY_STRENGTH = 0.5; + +const CATCH_RADIUS = 0.5; +const MIN_SIMULATION_SPEED = 0.15; +const THROWN_VELOCITY_SCALING = 1.5; + +const SOUNDS_ENABLED = true; +const FRISBEE_BUTTON_URL = "http://test.thoys.nl/hifi/images/frisbee/frisbee_button_by_Judas.svg"; +const FRISBEE_MODEL_SCALE = 275; +const FRISBEE_MENU = "Toys>Frisbee"; +const FRISBEE_DESIGN_MENU = "Toys>Frisbee>Design"; +const FRISBEE_ENABLED_SETTING = "Frisbee>Enabled"; +const FRISBEE_CREATENEW_SETTING = "Frisbee>CreateNew"; +const FRISBEE_DESIGN_SETTING = "Frisbee>Design"; +const FRISBEE_FORCE_MOUSE_CONTROLS_SETTING = "Frisbee>ForceMouseControls"; + +//Add your own designs in FRISBEE_DESIGNS, be sure to put frisbee in the URL if you want others to be able to catch it without having a copy of your frisbee script. +const FRISBEE_DESIGNS = [ + {"name":"Interface", "model":"http://test.thoys.nl/hifi/models/frisbee/frisbee.fbx"}, + {"name":"Pizza", "model":"http://test.thoys.nl/hifi/models/frisbee/pizza.fbx"}, + {"name":"Swirl", "model":"http://test.thoys.nl/hifi/models/frisbee/swirl.fbx"}, + {"name":"Mayan", "model":"http://test.thoys.nl/hifi/models/frisbee/mayan.fbx"}, + ]; +const FRISBEE_MENU_DESIGN_POSTFIX = " Design"; +const FRISBEE_DESIGN_RANDOM = "Random"; + +const SPIN_MULTIPLIER = 1000; +const FRISBEE_LIFETIME = 300; // 5 minutes + +var windowDimensions = Controller.getViewportDimensions(); +var toolHeight = 50; +var toolWidth = 50; +var frisbeeToggle; +var toolBar; +var frisbeeEnabled = true; +var newfrisbeeEnabled = false; +var forceMouseControls = false; +var hydrasConnected = false; +var selectedDesign = FRISBEE_DESIGN_RANDOM; + +function loadSettings() { + frisbeeEnabled = Settings.getValue(FRISBEE_ENABLED_SETTING, "true") == "true"; + newfrisbeeEnabled = Settings.getValue(FRISBEE_CREATENEW_SETTING, "false") == "true"; + forceMouseControls = Settings.getValue(FRISBEE_FORCE_MOUSE_CONTROLS_SETTING, "false") == "true"; + selectedDesign = Settings.getValue(FRISBEE_DESIGN_SETTING, "Random"); +} + +function saveSettings() { + Settings.setValue(FRISBEE_ENABLED_SETTING, frisbeeEnabled ? "true" : "false"); + Settings.setValue(FRISBEE_CREATENEW_SETTING, newfrisbeeEnabled ? "true" : "false"); + Settings.setValue(FRISBEE_FORCE_MOUSE_CONTROLS_SETTING, forceMouseControls ? "true" : "false"); + Settings.setValue(FRISBEE_DESIGN_SETTING, selectedDesign); +} + +function moveOverlays() { + var newViewPort = Controller.getViewportDimensions(); + if (typeof(toolBar) === 'undefined') { + initToolBar(); + } else if (windowDimensions.x == newViewPort.x && + windowDimensions.y == newViewPort.y) { + return; + } + + windowDimensions = newViewPort; + var toolsX = windowDimensions.x - 8 - toolBar.width; + var toolsY = (windowDimensions.y - toolBar.height) / 2 + 80; + toolBar.move(toolsX, toolsY); +} + +function frisbeeURL() { + return selectedDesign == FRISBEE_DESIGN_RANDOM ? FRISBEE_DESIGNS[Math.floor(Math.random() * FRISBEE_DESIGNS.length)].model : getFrisbee(selectedDesign).model; +} + +//This function checks if the modelURL is inside of our Designs or contains "frisbee" in it. +function validFrisbeeURL(frisbeeURL) { + for (var frisbee in FRISBEE_DESIGNS) { + if (FRISBEE_DESIGNS[frisbee].model == frisbeeURL) { + return true; + } + } + return frisbeeURL.toLowerCase().indexOf("frisbee") !== -1; +} + +function getFrisbee(frisbeeName) { + for (var frisbee in FRISBEE_DESIGNS) { + if (FRISBEE_DESIGNS[frisbee].name == frisbeeName) { + return FRISBEE_DESIGNS[frisbee]; + } + } + return undefined; +} + +function Hand(name, palm, tip, forwardButton, button3, trigger) { + this.name = name; + this.palm = palm; + this.tip = tip; + this.forwardButton = forwardButton; + this.button3 = button3; + this.trigger = trigger; + this.holdingFrisbee = false; + this.particle = false; + this.palmPosition = function() { return Controller.getSpatialControlPosition(this.palm); } + this.grabButtonPressed = function() { + return ( + Controller.isButtonPressed(this.forwardButton) || + Controller.isButtonPressed(this.button3) || + Controller.getTriggerValue(this.trigger) > 0.5 + ) + }; + this.holdPosition = function() { return this.palm == LEFT_PALM ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); }; + this.holdRotation = function() { + var q = Controller.getSpatialControlRawRotation(this.palm); + q = Quat.multiply(MyAvatar.orientation, q); + return {x: q.x, y: q.y, z: q.z, w: q.w}; + }; + this.tipVelocity = function() { return Controller.getSpatialControlVelocity(this.tip); }; +} + +function MouseControl(button) { + this.button = button; +} + +var leftHand = new Hand("LEFT", LEFT_PALM, LEFT_TIP, LEFT_BUTTON_FWD, LEFT_BUTTON_3, 0); +var rightHand = new Hand("RIGHT", RIGHT_PALM, RIGHT_TIP, RIGHT_BUTTON_FWD, RIGHT_BUTTON_3, 1); + +var leftMouseControl = new MouseControl("LEFT"); +var middleMouseControl = new MouseControl("MIDDLE"); +var rightMouseControl = new MouseControl("RIGHT"); +var mouseControls = [leftMouseControl, middleMouseControl, rightMouseControl]; +var currentMouseControl = false; + +var newSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/throw.raw"); +var catchSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/catch.raw"); +var throwSound = new Sound("http://highfidelity-public.s3-us-west-1.amazonaws.com/sounds/Switches%20and%20sliders/slider%20-%20whoosh1.raw"); + +var simulatedFrisbees = []; + +var wantDebugging = false; +function debugPrint(message) { + if (wantDebugging) { + print(message); + } +} + +function playSound(sound, position) { + if (!SOUNDS_ENABLED) { + return; + } + var options = new AudioInjectionOptions(); + options.position = position; + options.volume = 1.0; + Audio.playSound(sound, options); +} + +function cleanupFrisbees() { + simulatedFrisbees = []; + var particles = Particles.findParticles(MyAvatar.position, 1000); + for (particle in particles) { + Particles.deleteParticle(particles[particle]); + } +} + +function checkControllerSide(hand) { + // If I don't currently have a frisbee in my hand, then try to catch closest one + if (!hand.holdingFrisbee && hand.grabButtonPressed()) { + var closestParticle = Particles.findClosestParticle(hand.palmPosition(), CATCH_RADIUS); + var modelUrl = Particles.getParticleProperties(closestParticle).modelURL; + if (closestParticle.isKnownID && validFrisbeeURL(Particles.getParticleProperties(closestParticle).modelURL)) { + Particles.editParticle(closestParticle, {modelScale: 1, inHand: true, position: hand.holdPosition(), shouldDie: true}); + Particles.deleteParticle(closestParticle); + debugPrint(hand.message + " HAND- CAUGHT SOMETHING!!"); + + var properties = { + position: hand.holdPosition(), + velocity: { x: 0, y: 0, z: 0}, + gravity: { x: 0, y: 0, z: 0}, + inHand: true, + radius: FRISBEE_RADIUS, + damping: 0.999, + modelURL: modelUrl, + modelScale: FRISBEE_MODEL_SCALE, + modelRotation: hand.holdRotation(), + lifetime: FRISBEE_LIFETIME + }; + + newParticle = Particles.addParticle(properties); + + hand.holdingFrisbee = true; + hand.particle = newParticle; + + playSound(catchSound, hand.holdPosition()); + + return; // exit early + } + } + + // If '3' is pressed, and not holding a frisbee, make a new one + if (hand.grabButtonPressed() && !hand.holdingFrisbee && newfrisbeeEnabled) { + var properties = { + position: hand.holdPosition(), + velocity: { x: 0, y: 0, z: 0}, + gravity: { x: 0, y: 0, z: 0}, + inHand: true, + radius: FRISBEE_RADIUS, + damping: 0.999, + modelURL: frisbeeURL(), + modelScale: FRISBEE_MODEL_SCALE, + modelRotation: hand.holdRotation(), + lifetime: FRISBEE_LIFETIME + }; + + newParticle = Particles.addParticle(properties); + hand.holdingFrisbee = true; + hand.particle = newParticle; + + // Play a new frisbee sound + playSound(newSound, hand.holdPosition()); + + return; // exit early + } + + if (hand.holdingFrisbee) { + // If holding the frisbee keep it in the palm + if (hand.grabButtonPressed()) { + debugPrint(">>>>> " + hand.name + "-FRISBEE IN HAND, grabbing, hold and move"); + var properties = { + position: hand.holdPosition(), + modelRotation: hand.holdRotation() + }; + Particles.editParticle(hand.particle, properties); + } else { + debugPrint(">>>>> " + hand.name + "-FRISBEE IN HAND, not grabbing, THROW!!!"); + // If frisbee just released, add velocity to it! + + var properties = { + velocity: Vec3.multiply(hand.tipVelocity(), THROWN_VELOCITY_SCALING), + inHand: false, + lifetime: FRISBEE_LIFETIME, + gravity: { x: 0, y: -GRAVITY_STRENGTH, z: 0}, + modelRotation: hand.holdRotation() + }; + + Particles.editParticle(hand.particle, properties); + + simulatedFrisbees.push(hand.particle); + + hand.holdingFrisbee = false; + hand.particle = false; + + playSound(throwSound, hand.holdPosition()); + } + } +} + +function initToolBar() { + toolBar = new ToolBar(0, 0, ToolBar.VERTICAL); + frisbeeToggle = toolBar.addTool({ + imageURL: FRISBEE_BUTTON_URL, + subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, + width: toolWidth, + height: toolHeight, + visible: true, + alpha: 0.9 + }, true); + enableNewFrisbee(newfrisbeeEnabled); +} + +function hydraCheck() { + var numberOfButtons = Controller.getNumberOfButtons(); + var numberOfTriggers = Controller.getNumberOfTriggers(); + var numberOfSpatialControls = Controller.getNumberOfSpatialControls(); + var controllersPerTrigger = numberOfSpatialControls / numberOfTriggers; + hydrasConnected = (numberOfButtons == 12 && numberOfTriggers == 2 && controllersPerTrigger == 2); + return hydrasConnected; +} + +function checkController(deltaTime) { + moveOverlays(); + if (!frisbeeEnabled) { + return; + } + // this is expected for hydras + if (hydraCheck()) { + checkControllerSide(leftHand); + checkControllerSide(rightHand); + } + if (!hydrasConnected || forceMouseControls) { + //TODO: add mouse cursor control code here. + } +} + +function controlFrisbees(deltaTime) { + var killSimulations = []; + for (frisbee in simulatedFrisbees) { + var properties = Particles.getParticleProperties(simulatedFrisbees[frisbee]); + //get the horizon length from the velocity origin in order to get speed + var speed = Vec3.length({x:properties.velocity.x, y:0, z:properties.velocity.z}); + if (speed < MIN_SIMULATION_SPEED) { + //kill the frisbee simulation when speed is low + killSimulations.push(frisbee); + continue; + } + Particles.editParticle(simulatedFrisbees[frisbee], {modelRotation: Quat.multiply(properties.modelRotation, Quat.fromPitchYawRollDegrees(0, speed * deltaTime * SPIN_MULTIPLIER, 0))}); + + } + for (var i = killSimulations.length - 1; i >= 0; i--) { + simulatedFrisbees.splice(killSimulations[i], 1); + } +} + +//catches interfering calls of hydra-cursors +function withinBounds(coords) { + return coords.x >= 0 && coords.x < windowDimensions.x && coords.y >= 0 && coords.y < windowDimensions.y; +} + +function mouseMoveEvent(event) { + //TODO: mouse controls //print(withinBounds(event)); //print("move"+event.x); +} + +function mousePressEvent(event) { + print(event.x); + var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); + if (frisbeeToggle == toolBar.clicked(clickedOverlay)) { + newfrisbeeEnabled = !newfrisbeeEnabled; + saveSettings(); + enableNewFrisbee(newfrisbeeEnabled); + } +} + +function enableNewFrisbee(enable) { + if (toolBar.numberOfTools() > 0) { + toolBar.tools[0].select(enable); + } +} + +function mouseReleaseEvent(event) { + //TODO: mouse controls //print(JSON.stringify(event)); +} + +function setupMenus() { + Menu.addMenu(FRISBEE_MENU); + Menu.addMenuItem({ + menuName: FRISBEE_MENU, + menuItemName: "Frisbee Enabled", + isCheckable: true, + isChecked: frisbeeEnabled + }); + Menu.addMenuItem({ + menuName: FRISBEE_MENU, + menuItemName: "Cleanup Frisbees" + }); + Menu.addMenuItem({ + menuName: FRISBEE_MENU, + menuItemName: "Force Mouse Controls", + isCheckable: true, + isChecked: forceMouseControls + }); + Menu.addMenu(FRISBEE_DESIGN_MENU); + Menu.addMenuItem({ + menuName: FRISBEE_DESIGN_MENU, + menuItemName: FRISBEE_DESIGN_RANDOM + FRISBEE_MENU_DESIGN_POSTFIX, + isCheckable: true, + isChecked: selectedDesign == FRISBEE_DESIGN_RANDOM + }); + for (frisbee in FRISBEE_DESIGNS) { + Menu.addMenuItem({ + menuName: FRISBEE_DESIGN_MENU, + menuItemName: FRISBEE_DESIGNS[frisbee].name + FRISBEE_MENU_DESIGN_POSTFIX, + isCheckable: true, + isChecked: selectedDesign == FRISBEE_DESIGNS[frisbee].name + }); + } +} + +//startup calls: +loadSettings(); +setupMenus(); +function scriptEnding() { + toolBar.cleanup(); + Menu.removeMenu(FRISBEE_MENU); +} + +function menuItemEvent(menuItem) { + if (menuItem == "Cleanup Frisbees") { + cleanupFrisbees(); + return; + } else if (menuItem == "Frisbee Enabled") { + frisbeeEnabled = Menu.isOptionChecked(menuItem); + saveSettings(); + return; + } else if (menuItem == "Force Mouse Controls") { + forceMouseControls = Menu.isOptionChecked(menuItem); + saveSettings(); + return; + } + if (menuItem.indexOf(FRISBEE_MENU_DESIGN_POSTFIX, menuItem.length - FRISBEE_MENU_DESIGN_POSTFIX.length) !== -1) { + var item_name = menuItem.substring(0, menuItem.length - FRISBEE_MENU_DESIGN_POSTFIX.length); + if (item_name == FRISBEE_DESIGN_RANDOM || getFrisbee(item_name) != undefined) { + Menu.setIsOptionChecked(selectedDesign + FRISBEE_MENU_DESIGN_POSTFIX, false); + selectedDesign = item_name; + saveSettings(); + Menu.setIsOptionChecked(selectedDesign + FRISBEE_MENU_DESIGN_POSTFIX, true); + } + } +} + +// register the call back so it fires before each data send +Controller.mouseMoveEvent.connect(mouseMoveEvent); +Controller.mousePressEvent.connect(mousePressEvent); +Controller.mouseReleaseEvent.connect(mouseReleaseEvent); +Menu.menuItemEvent.connect(menuItemEvent); +Script.scriptEnding.connect(scriptEnding); +Script.update.connect(checkController); +Script.update.connect(controlFrisbees); \ No newline at end of file