Merge pull request #9180 from huffman/feat/tutorial-touch

Add touch support to tutorial
This commit is contained in:
Stephen Birarda 2016-12-16 17:12:22 -08:00 committed by GitHub
commit 59f6c8857d
31 changed files with 932 additions and 354 deletions

View file

@ -1389,16 +1389,31 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
} }
qCDebug(interfaceapp) << "Server content version: " << contentVersion; qCDebug(interfaceapp) << "Server content version: " << contentVersion;
bool hasTutorialContent = contentVersion >= 1; static const int MIN_VIVE_CONTENT_VERSION = 1;
static const int MIN_OCULUS_TOUCH_CONTENT_VERSION = 27;
bool hasSufficientTutorialContent = false;
bool hasHandControllers = false;
// Only specific hand controllers are currently supported, so only send users to the tutorial
// if they have one of those hand controllers.
if (PluginUtils::isViveControllerAvailable()) {
hasHandControllers = true;
hasSufficientTutorialContent = contentVersion >= MIN_VIVE_CONTENT_VERSION;
} else if (PluginUtils::isOculusTouchControllerAvailable()) {
hasHandControllers = true;
hasSufficientTutorialContent = contentVersion >= MIN_OCULUS_TOUCH_CONTENT_VERSION;
}
Setting::Handle<bool> firstRun { Settings::firstRun, true }; Setting::Handle<bool> firstRun { Settings::firstRun, true };
bool hasHMDAndHandControllers = PluginUtils::isHMDAvailable("OpenVR (Vive)") && PluginUtils::isHandControllerAvailable();
bool hasHMDAndHandControllers = PluginUtils::isHMDAvailable() && hasHandControllers;
Setting::Handle<bool> tutorialComplete { "tutorialComplete", false }; Setting::Handle<bool> tutorialComplete { "tutorialComplete", false };
bool shouldGoToTutorial = hasHMDAndHandControllers && hasTutorialContent && !tutorialComplete.get(); bool shouldGoToTutorial = hasHMDAndHandControllers && hasSufficientTutorialContent && !tutorialComplete.get();
qCDebug(interfaceapp) << "Has HMD + Hand Controllers: " << hasHMDAndHandControllers << ", current plugin: " << _displayPlugin->getName(); qCDebug(interfaceapp) << "Has HMD + Hand Controllers: " << hasHMDAndHandControllers << ", current plugin: " << _displayPlugin->getName();
qCDebug(interfaceapp) << "Has tutorial content: " << hasTutorialContent; qCDebug(interfaceapp) << "Has sufficient tutorial content (" << contentVersion << ") : " << hasSufficientTutorialContent;
qCDebug(interfaceapp) << "Tutorial complete: " << tutorialComplete.get(); qCDebug(interfaceapp) << "Tutorial complete: " << tutorialComplete.get();
qCDebug(interfaceapp) << "Should go to tutorial: " << shouldGoToTutorial; qCDebug(interfaceapp) << "Should go to tutorial: " << shouldGoToTutorial;

View file

@ -49,12 +49,17 @@ glm::vec2 HMDScriptingInterface::overlayToSpherical(const glm::vec2 & position)
return qApp->getApplicationCompositor().overlayToSpherical(position); return qApp->getApplicationCompositor().overlayToSpherical(position);
} }
bool HMDScriptingInterface::isHMDAvailable() { bool HMDScriptingInterface::isHMDAvailable(const QString& name) {
return PluginUtils::isHMDAvailable(); return PluginUtils::isHMDAvailable(name);
} }
bool HMDScriptingInterface::isHandControllerAvailable() { bool HMDScriptingInterface::isHandControllerAvailable(const QString& name) {
return PluginUtils::isHandControllerAvailable(); return PluginUtils::isHandControllerAvailable(name);
}
bool HMDScriptingInterface::isSubdeviceContainingNameAvailable(const QString& name) {
return PluginUtils::isSubdeviceContainingNameAvailable(name);
} }
void HMDScriptingInterface::requestShowHandControllers() { void HMDScriptingInterface::requestShowHandControllers() {

View file

@ -38,8 +38,9 @@ public:
Q_INVOKABLE QString preferredAudioInput() const; Q_INVOKABLE QString preferredAudioInput() const;
Q_INVOKABLE QString preferredAudioOutput() const; Q_INVOKABLE QString preferredAudioOutput() const;
Q_INVOKABLE bool isHMDAvailable(); Q_INVOKABLE bool isHMDAvailable(const QString& name = "");
Q_INVOKABLE bool isHandControllerAvailable(); Q_INVOKABLE bool isHandControllerAvailable(const QString& name = "");
Q_INVOKABLE bool isSubdeviceContainingNameAvailable(const QString& name);
Q_INVOKABLE void requestShowHandControllers(); Q_INVOKABLE void requestShowHandControllers();
Q_INVOKABLE void requestHideHandControllers(); Q_INVOKABLE void requestHideHandControllers();

View file

@ -25,14 +25,15 @@ void UserActivityLoggerScriptingInterface::toggledAway(bool isAway) {
} }
void UserActivityLoggerScriptingInterface::tutorialProgress( QString stepName, int stepNumber, float secondsToComplete, void UserActivityLoggerScriptingInterface::tutorialProgress( QString stepName, int stepNumber, float secondsToComplete,
float tutorialElapsedTime, QString tutorialRunID, int tutorialVersion) { float tutorialElapsedTime, QString tutorialRunID, int tutorialVersion, QString controllerType) {
logAction("tutorial_progress", { logAction("tutorial_progress", {
{ "tutorial_run_id", tutorialRunID }, { "tutorial_run_id", tutorialRunID },
{ "tutorial_version", tutorialVersion }, { "tutorial_version", tutorialVersion },
{ "step", stepName }, { "step", stepName },
{ "step_number", stepNumber }, { "step_number", stepNumber },
{ "seconds_to_complete", secondsToComplete }, { "seconds_to_complete", secondsToComplete },
{ "tutorial_elapsed_seconds", tutorialElapsedTime } { "tutorial_elapsed_seconds", tutorialElapsedTime },
{ "controller_type", controllerType }
}); });
} }

View file

@ -24,7 +24,7 @@ public:
Q_INVOKABLE void openedMarketplace(); Q_INVOKABLE void openedMarketplace();
Q_INVOKABLE void toggledAway(bool isAway); Q_INVOKABLE void toggledAway(bool isAway);
Q_INVOKABLE void tutorialProgress(QString stepName, int stepNumber, float secondsToComplete, Q_INVOKABLE void tutorialProgress(QString stepName, int stepNumber, float secondsToComplete,
float tutorialElapsedTime, QString tutorialRunID = "", int tutorialVersion = 0); float tutorialElapsedTime, QString tutorialRunID = "", int tutorialVersion = 0, QString controllerType = "");
private: private:
void logAction(QString action, QJsonObject details = {}); void logAction(QString action, QJsonObject details = {});

View file

@ -24,16 +24,16 @@ bool PluginUtils::isHMDAvailable(const QString& pluginName) {
return false; return false;
} }
bool PluginUtils::isHandControllerAvailable() { bool PluginUtils::isHandControllerAvailable(const QString& pluginName) {
for (auto& inputPlugin : PluginManager::getInstance()->getInputPlugins()) { for (auto& inputPlugin : PluginManager::getInstance()->getInputPlugins()) {
if (inputPlugin->isHandController()) { if (inputPlugin->isHandController() && (pluginName.isEmpty() || inputPlugin->getName() == pluginName)) {
return true; return true;
} }
} }
return false; return false;
}; };
bool isSubdeviceContainingNameAvailable(QString name) { bool PluginUtils::isSubdeviceContainingNameAvailable(QString name) {
for (auto& inputPlugin : PluginManager::getInstance()->getInputPlugins()) { for (auto& inputPlugin : PluginManager::getInstance()->getInputPlugins()) {
if (inputPlugin->isActive()) { if (inputPlugin->isActive()) {
auto subdeviceNames = inputPlugin->getSubdeviceNames(); auto subdeviceNames = inputPlugin->getSubdeviceNames();

View file

@ -15,7 +15,8 @@
class PluginUtils { class PluginUtils {
public: public:
static bool isHMDAvailable(const QString& pluginName = ""); static bool isHMDAvailable(const QString& pluginName = "");
static bool isHandControllerAvailable(); static bool isHandControllerAvailable(const QString& pluginName = "");
static bool isSubdeviceContainingNameAvailable(QString name);
static bool isViveControllerAvailable(); static bool isViveControllerAvailable();
static bool isOculusTouchControllerAvailable(); static bool isOculusTouchControllerAvailable();
static bool isXboxControllerAvailable(); static bool isXboxControllerAvailable();

View file

@ -850,6 +850,12 @@ void Model::setTextures(const QVariantMap& textures) {
_needsUpdateTextures = true; _needsUpdateTextures = true;
_needsFixupInScene = true; _needsFixupInScene = true;
_renderGeometry->setTextures(textures); _renderGeometry->setTextures(textures);
} else {
// FIXME(Huffman): Disconnect previously connected lambdas so we don't set textures multiple
// after the geometry has finished loading.
connect(&_renderWatcher, &GeometryResourceWatcher::finished, this, [this, textures]() {
_renderGeometry->setTextures(textures);
});
} }
} }

View file

@ -21,14 +21,17 @@ function clamp(value, min, max) {
} }
function resolveHardware(path) { function resolveHardware(path) {
var parts = path.split("."); if (typeof path === 'string') {
function resolveInner(base, path, i) { var parts = path.split(".");
if (i >= path.length) { function resolveInner(base, path, i) {
return base; if (i >= path.length) {
return base;
}
return resolveInner(base[path[i]], path, ++i);
} }
return resolveInner(base[path[i]], path, ++i); return resolveInner(Controller.Hardware, parts, 0);
} }
return resolveInner(Controller.Hardware, parts, 0); return path;
} }
var DEBUG = true; var DEBUG = true;
@ -132,7 +135,9 @@ createControllerDisplay = function(config) {
overlayID = Overlays.addOverlay("model", properties); overlayID = Overlays.addOverlay("model", properties);
if (part.type === "rotational") { if (part.type === "rotational") {
mapping.from([part.input]).peek().to(function(controller, overlayID, part) { var input = resolveHardware(part.input);
print("Mapping to: ", part.input, input);
mapping.from([input]).peek().to(function(controller, overlayID, part) {
return function(value) { return function(value) {
value = clamp(value, part.minValue, part.maxValue); value = clamp(value, part.minValue, part.maxValue);
@ -157,18 +162,85 @@ createControllerDisplay = function(config) {
}(controller, overlayID, part)); }(controller, overlayID, part));
} else if (part.type === "touchpad") { } else if (part.type === "touchpad") {
var visibleInput = resolveHardware(part.visibleInput); var visibleInput = resolveHardware(part.visibleInput);
var xinput = resolveHardware(part.xInput); var xInput = resolveHardware(part.xInput);
var yinput = resolveHardware(part.yInput); var yInput = resolveHardware(part.yInput);
// TODO: Touchpad inputs are currently only working for half // TODO: Touchpad inputs are currently only working for half
// of the touchpad. When that is fixed, it would be useful // of the touchpad. When that is fixed, it would be useful
// to update these to display the current finger position. // to update these to display the current finger position.
mapping.from([visibleInput]).peek().to(function(value) { mapping.from([visibleInput]).peek().to(function(value) {
}); });
mapping.from([xinput]).peek().to(function(value) { mapping.from([xInput]).peek().to(function(value) {
}); });
mapping.from([yinput]).peek().invert().to(function(value) { mapping.from([yInput]).peek().invert().to(function(value) {
}); });
} else if (part.type === "joystick") {
(function(controller, overlayID, part) {
const xInput = resolveHardware(part.xInput);
const yInput = resolveHardware(part.yInput);
var xvalue = 0;
var yvalue = 0;
function calculatePositionAndRotation(xValue, yValue) {
var rotation = Quat.fromPitchYawRollDegrees(yValue * part.xHalfAngle, 0, xValue * part.yHalfAngle);
var offset = { x: 0, y: 0, z: 0 };
if (part.originOffset) {
offset = Vec3.multiplyQbyV(rotation, part.originOffset);
offset = Vec3.subtract(part.originOffset, offset);
}
var partPosition = Vec3.sum(controller.position,
Vec3.multiplyQbyV(controller.rotation, Vec3.sum(offset, part.naturalPosition)));
var partRotation = Quat.multiply(controller.rotation, rotation)
return {
position: partPosition,
rotation: partRotation
}
}
mapping.from([xInput]).peek().to(function(value) {
xvalue = value;
//print(overlayID, xvalue.toFixed(3), yvalue.toFixed(3));
var posRot = calculatePositionAndRotation(xvalue, yvalue);
Overlays.editOverlay(overlayID, {
localPosition: posRot.position,
localRotation: posRot.rotation
});
});
mapping.from([yInput]).peek().to(function(value) {
yvalue = value;
var posRot = calculatePositionAndRotation(xvalue, yvalue);
Overlays.editOverlay(overlayID, {
localPosition: posRot.position,
localRotation: posRot.rotation
});
});
})(controller, overlayID, part);
} else if (part.type === "linear") {
(function(controller, overlayID, part) {
const input = resolveHardware(part.input);
mapping.from([input]).peek().to(function(value) {
//print(value);
var axis = Vec3.multiplyQbyV(controller.rotation, part.axis);
var offset = Vec3.multiply(part.maxTranslation * value, axis);
var partPosition = Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, part.naturalPosition));
var position = Vec3.sum(partPosition, offset);
Overlays.editOverlay(overlayID, {
localPosition: position
});
});
})(controller, overlayID, part);
} else if (part.type === "static") { } else if (part.type === "static") {
// do nothing // do nothing
} else { } else {

View file

@ -15,6 +15,7 @@
Script.include("controllerDisplay.js"); Script.include("controllerDisplay.js");
Script.include("viveControllerConfiguration.js"); Script.include("viveControllerConfiguration.js");
Script.include("touchControllerConfiguration.js");
var HIDE_CONTROLLERS_ON_EQUIP = false; var HIDE_CONTROLLERS_ON_EQUIP = false;
@ -41,12 +42,28 @@ ControllerDisplayManager = function() {
function updateControllers() { function updateControllers() {
if (HMD.active && HMD.shouldShowHandControllers()) { if (HMD.active && HMD.shouldShowHandControllers()) {
var leftConfig = null;
var rightConfig = null;
if ("Vive" in Controller.Hardware) { if ("Vive" in Controller.Hardware) {
if (!controllerLeft) { leftConfig = VIVE_CONTROLLER_CONFIGURATION_LEFT;
controllerLeft = createControllerDisplay(VIVE_CONTROLLER_CONFIGURATION_LEFT); rightConfig = VIVE_CONTROLLER_CONFIGURATION_RIGHT;
}
if ("OculusTouch" in Controller.Hardware) {
leftConfig = TOUCH_CONTROLLER_CONFIGURATION_LEFT;
rightConfig = TOUCH_CONTROLLER_CONFIGURATION_RIGHT;
}
if (leftConfig !== null && rightConfig !== null) {
print("Loading controllers");
if (controllerLeft === null) {
controllerLeft = createControllerDisplay(leftConfig);
controllerLeft.setVisible(true);
} }
if (!controllerRight) { if (controllerRight === null) {
controllerRight = createControllerDisplay(VIVE_CONTROLLER_CONFIGURATION_RIGHT); controllerRight = createControllerDisplay(rightConfig);
controllerRight.setVisible(true);
} }
// We've found the controllers, we no longer need to look for active controllers // We've found the controllers, we no longer need to look for active controllers
if (controllerCheckerIntervalID) { if (controllerCheckerIntervalID) {

View file

@ -0,0 +1,353 @@
//
// touchControllerConfiguration.js
//
// Created by Ryan Huffman on 12/06/16
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* globals TOUCH_CONTROLLER_CONFIGURATION_LEFT:true TOUCH_CONTROLLER_CONFIGURATION_RIGHT:true */
/* eslint camelcase: ["error", { "properties": "never" }] */
var leftBaseRotation = Quat.multiply(
Quat.fromPitchYawRollDegrees(-90, 0, 0),
Quat.fromPitchYawRollDegrees(0, 0, 90)
);
var rightBaseRotation = Quat.multiply(
Quat.fromPitchYawRollDegrees(-90, 0, 0),
Quat.fromPitchYawRollDegrees(0, 0, -90)
);
// keep these in sync with the values from OculusHelpers.cpp
var CONTROLLER_LENGTH_OFFSET = 0.0762;
var CONTROLLER_LATERAL_OFFSET = 0.0381;
var CONTROLLER_VERTICAL_OFFSET = 0.0381;
var CONTROLLER_FORWARD_OFFSET = 0.1524;
var leftBasePosition = Vec3.multiplyQbyV(leftBaseRotation, {
x: -CONTROLLER_LENGTH_OFFSET / 2.0,
y: CONTROLLER_LENGTH_OFFSET / 2.0,
z: CONTROLLER_LENGTH_OFFSET * 1.5
});
var rightBasePosition = Vec3.multiplyQbyV(rightBaseRotation, {
x: CONTROLLER_LENGTH_OFFSET / 2.0,
y: CONTROLLER_LENGTH_OFFSET / 2.0,
z: CONTROLLER_LENGTH_OFFSET * 1.5
});
var BASE_URL = Script.resourcesPath() + "meshes/controller/touch/";
TOUCH_CONTROLLER_CONFIGURATION_LEFT = {
name: "Touch",
controllers: [
{
modelURL: BASE_URL + "touch_l_body.fbx",
jointIndex: MyAvatar.getJointIndex("_CONTROLLER_LEFTHAND"),
naturalPosition: { x: 0.01648625358939171, y: -0.03551870584487915, z: -0.018527675420045853 },
dimensions: { x: 0.11053799837827682, y: 0.0995776429772377, z: 0.10139888525009155 },
rotation: leftBaseRotation,
position: leftBasePosition,
parts: {
tips: {
type: "static",
modelURL: BASE_URL + "Oculus-Labels-L.fbx",
naturalPosition: { x: -0.022335469722747803, y: 0.00022516027092933655, z: 0.020340695977211 },
textureName: "blank",
defaultTextureLayer: "blank",
textureLayers: {
blank: {
defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Blank.png"
},
trigger: {
defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Trigger.png"
},
arrows: {
defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Rotate.png"
},
grip: {
defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Grip-oculus.png"
},
teleport: {
defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Teleport.png"
},
}
},
trigger: {
type: "rotational",
modelURL: BASE_URL + "touch_l_trigger.fbx",
naturalPosition: { x: 0.0008544912561774254, y: -0.019867943599820137, z: 0.018800459802150726 },
// rotational
input: Controller.Standard.LT,
origin: { x: 0, y: -0.015, z: -0.00 },
minValue: 0.0,
maxValue: 1.0,
axis: { x: 1, y: 0, z: 0 },
maxAngle: 17,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_l_trigger.fbx/touch_l_trigger.fbm/L_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_l_trigger.fbx/touch_l_trigger.fbm/L_controller-highlight_DIF.jpg",
}
}
},
grip: {
type: "linear",
modelURL: BASE_URL + "touch_l_bumper.fbx",
naturalPosition: { x: 0.00008066371083259583, y: -0.02715788595378399, z: -0.02448512241244316 },
// linear properties
// Offset from origin = 0.36470, 0.11048, 0.11066
input: "OculusTouch.LeftGrip",
axis: { x: 1, y: 0.302933918, z: 0.302933918 },
maxTranslation: 0.003967,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_l_bumper.fbx/touch_l_bumper.fbm/L_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_l_bumper.fbx/touch_l_bumper.fbm/L_controller-highlight_DIF.jpg",
}
}
},
joystick: {
type: "joystick",
modelURL: BASE_URL + "touch_l_joystick.fbx",
naturalPosition: { x: 0.0075613949447870255, y: -0.008225866593420506, z: 0.004792703315615654 },
// joystick
xInput: "OculusTouch.LX",
yInput: "OculusTouch.LY",
originOffset: { x: 0, y: -0.0028564, z: -0.00 },
xHalfAngle: 20,
yHalfAngle: 20,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_l_joystick.fbx/touch_l_joystick.fbm/L_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_l_joystick.fbx/touch_l_joystick.fbm/L_controller-highlight_DIF.jpg",
}
}
},
button_a: {
type: "linear",
modelURL: BASE_URL + "touch_l_button_x.fbx",
naturalPosition: { x: -0.009307309985160828, y: -0.00005015172064304352, z: -0.012594521045684814 },
input: "OculusTouch.X",
axis: { x: 0, y: -1, z: 0 },
maxTranslation: 0.001,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_l_button_x.fbx/touch_l_button_x.fbm/L_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_l_button_x.fbx/touch_l_button_x.fbm/L_controller-highlight_DIF.jpg",
}
}
},
button_b: {
type: "linear",
modelURL: BASE_URL + "touch_l_button_y.fbx",
naturalPosition: { x: -0.01616849936544895, y: -0.000050364527851343155, z: 0.0017703399062156677 },
input: "OculusTouch.Y",
axis: { x: 0, y: -1, z: 0 },
maxTranslation: 0.001,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_l_button_y.fbx/touch_l_button_y.fbm/L_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_l_button_y.fbx/touch_l_button_y.fbm/L_controller-highlight_DIF.jpg",
}
}
},
}
}
]
};
TOUCH_CONTROLLER_CONFIGURATION_RIGHT = {
name: "Touch",
controllers: [
{
modelURL: BASE_URL + "touch_r_body.fbx",
jointIndex: MyAvatar.getJointIndex("_CONTROLLER_RIGHTHAND"),
naturalPosition: { x: -0.016486231237649918, y: -0.03551865369081497, z: -0.018527653068304062 },
dimensions: { x: 0.11053784191608429, y: 0.09957750141620636, z: 0.10139875113964081 },
rotation: rightBaseRotation,
position: rightBasePosition,
parts: {
tips: {
type: "static",
modelURL: BASE_URL + "Oculus-Labels-R.fbx",
naturalPosition: { x: 0.009739525616168976, y: -0.0017818436026573181, z: 0.016794726252555847 },
textureName: "Texture",
defaultTextureLayer: "blank",
textureLayers: {
blank: {
defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Blank.png"
},
trigger: {
defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Trigger.png"
},
arrows: {
defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Rotate.png"
},
grip: {
defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Grip-oculus.png"
},
teleport: {
defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Teleport.png"
},
}
},
trigger: {
type: "rotational",
modelURL: BASE_URL + "touch_r_trigger.fbx",
naturalPosition: { x: -0.0008544912561774254, y: -0.019867943599820137, z: 0.018800459802150726 },
// rotational
input: "OculusTouch.RT",
origin: { x: 0, y: -0.015, z: 0 },
minValue: 0.0,
maxValue: 1.0,
axis: { x: 1, y: 0, z: 0 },
maxAngle: 17,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_r_trigger.fbx/touch_r_trigger.fbm/R_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_r_trigger.fbx/touch_r_trigger.fbm/R_controller-highlight_DIF.jpg",
}
}
},
grip: {
type: "linear",
modelURL: BASE_URL + "touch_r_bumper.fbx",
naturalPosition: { x: -0.0000806618481874466, y: -0.027157839387655258, z: -0.024485092610120773 },
// linear properties
// Offset from origin = 0.36470, 0.11048, 0.11066
input: "OculusTouch.RightGrip",
axis: { x: -1, y: 0.302933918, z: 0.302933918 },
maxTranslation: 0.003967,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_r_bumper.fbx/touch_r_bumper.fbm/R_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_r_bumper.fbx/touch_r_bumper.fbm/R_controller-highlight_DIF.jpg",
}
}
},
joystick: {
type: "joystick",
modelURL: BASE_URL + "touch_r_joystick.fbx",
naturalPosition: { x: -0.007561382371932268, y: -0.008225853554904461, z: 0.00479268841445446 },
// joystick
xInput: "OculusTouch.RX",
yInput: "OculusTouch.RY",
originOffset: { x: 0, y: -0.0028564, z: 0 },
xHalfAngle: 20,
yHalfAngle: 20,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_r_joystick.fbx/touch_r_joystick.fbm/R_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_r_joystick.fbx/touch_r_joystick.fbm/R_controller-highlight_DIF.jpg",
}
}
},
button_a: {
type: "linear",
modelURL: BASE_URL + "touch_r_button_a.fbx",
naturalPosition: { x: 0.009307296946644783, y: -0.00005015172064304352, z: -0.012594504281878471 },
input: "OculusTouch.A",
axis: { x: 0, y: -1, z: 0 },
maxTranslation: 0.001,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_r_button_a.fbx/touch_r_button_a.fbm/R_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_r_button_a.fbx/touch_r_button_a.fbm/R_controller-highlight_DIF.jpg",
}
}
},
button_b: {
type: "linear",
modelURL: BASE_URL + "touch_r_button_b.fbx",
naturalPosition: { x: 0.01616847701370716, y: -0.000050364527851343155, z: 0.0017703361809253693 },
input: "OculusTouch.B",
axis: { x: 0, y: -1, z: 0 },
maxTranslation: 0.001,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_r_button_b.fbx/touch_r_button_b.fbm/R_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_r_button_b.fbx/touch_r_button_b.fbm/R_controller-highlight_DIF.jpg",
}
}
},
}
}
]
};

View file

@ -83,7 +83,6 @@ VIVE_CONTROLLER_CONFIGURATION_LEFT = {
naturalPosition: {"x":-0.004377640783786774,"y":-0.034371938556432724,"z":0.06769277155399323}, naturalPosition: {"x":-0.004377640783786774,"y":-0.034371938556432724,"z":0.06769277155399323},
textureName: "Tex.Blank", textureName: "Tex.Blank",
defaultTextureLayer: "blank", defaultTextureLayer: "blank",
textureLayers: { textureLayers: {
blank: { blank: {

View file

@ -58,11 +58,6 @@ function info() {
} }
} }
// Return a number between min (inclusive) and max (exclusive)
function randomInt(min, max) {
return min + Math.floor(Math.random() * (max - min))
}
var NEAR_BOX_SPAWN_NAME = "tutorial/nearGrab/box_spawn"; var NEAR_BOX_SPAWN_NAME = "tutorial/nearGrab/box_spawn";
var FAR_BOX_SPAWN_NAME = "tutorial/farGrab/box_spawn"; var FAR_BOX_SPAWN_NAME = "tutorial/farGrab/box_spawn";
var GUN_SPAWN_NAME = "tutorial/gun_spawn"; var GUN_SPAWN_NAME = "tutorial/gun_spawn";
@ -78,10 +73,6 @@ function setAwayEnabled(value) {
Messages.sendLocalMessage(CHANNEL_AWAY_ENABLE, message); Messages.sendLocalMessage(CHANNEL_AWAY_ENABLE, message);
} }
function beginsWithFilter(value, key) {
return value.indexOf(properties[key]) == 0;
}
findEntity = function(properties, searchRadius, filterFn) { findEntity = function(properties, searchRadius, filterFn) {
var entities = findEntities(properties, searchRadius, filterFn); var entities = findEntities(properties, searchRadius, filterFn);
return entities.length > 0 ? entities[0] : null; return entities.length > 0 ? entities[0] : null;
@ -116,32 +107,30 @@ findEntities = function(properties, searchRadius, filterFn) {
return matchedEntities; return matchedEntities;
} }
function setControllerPartsVisible(parts) { function findEntitiesWithTag(tag) {
Messages.sendLocalMessage('Controller-Display-Parts', JSON.stringify(parts)); return findEntities({ userData: "" }, 10000, function(properties, key, value) {
data = parseJSON(value);
return data.tag === tag;
});
} }
/**
* A controller in made up of parts, and each part can have multiple "layers,"
* which are really just different texures. For example, the "trigger" part
* has "normal" and "highlight" layers.
*/
function setControllerPartLayer(part, layer) { function setControllerPartLayer(part, layer) {
data = {}; data = {};
data[part] = layer; data[part] = layer;
Messages.sendLocalMessage('Controller-Set-Part-Layer', JSON.stringify(data)); Messages.sendLocalMessage('Controller-Set-Part-Layer', JSON.stringify(data));
} }
function triggerHapticPulse() { /**
function scheduleHaptics(delay, strength, duration) { * Spawn entities and return the newly created entity's ids.
Script.setTimeout(function() { * @param {object[]} entityPropertiesList A list of properties of the entities
Controller.triggerHapticPulse(strength, duration, 0); * to spawn.
Controller.triggerHapticPulse(strength, duration, 1); */
}, delay); function spawn(entityPropertiesList, transform, modifyFn) {
}
scheduleHaptics(0, 0.8, 100);
scheduleHaptics(300, 0.5, 100);
scheduleHaptics(600, 0.3, 100);
scheduleHaptics(900, 0.2, 100);
scheduleHaptics(1200, 0.1, 100);
}
function spawn(entityData, transform, modifyFn) {
debug("Creating: ", entityData);
if (!transform) { if (!transform) {
transform = { transform = {
position: { x: 0, y: 0, z: 0 }, position: { x: 0, y: 0, z: 0 },
@ -149,9 +138,8 @@ function spawn(entityData, transform, modifyFn) {
} }
} }
var ids = []; var ids = [];
for (var i = 0; i < entityData.length; ++i) { for (var i = 0; i < entityPropertiesList.length; ++i) {
var data = entityData[i]; var data = entityPropertiesList[i];
debug("Creating: ", data.name);
data.position = Vec3.sum(transform.position, data.position); data.position = Vec3.sum(transform.position, data.position);
data.rotation = Quat.multiply(data.rotation, transform.rotation); data.rotation = Quat.multiply(data.rotation, transform.rotation);
if (modifyFn) { if (modifyFn) {
@ -159,11 +147,16 @@ function spawn(entityData, transform, modifyFn) {
} }
var id = Entities.addEntity(data); var id = Entities.addEntity(data);
ids.push(id); ids.push(id);
debug(id, "data:", JSON.stringify(data));
} }
return ids; return ids;
} }
/**
* @function parseJSON
* @param {string} jsonString The string to parse.
* @return {object} Return an empty if the string was not valid JSON, otherwise
* the parsed object is returned.
*/
function parseJSON(jsonString) { function parseJSON(jsonString) {
var data; var data;
try { try {
@ -174,6 +167,10 @@ function parseJSON(jsonString) {
return data; return data;
} }
/**
* Spawn entities with `tag` in the userData.
* @function spawnWithTag
*/
function spawnWithTag(entityData, transform, tag) { function spawnWithTag(entityData, transform, tag) {
function modifyFn(data) { function modifyFn(data) {
var userData = parseJSON(data.userData); var userData = parseJSON(data.userData);
@ -185,6 +182,10 @@ function spawnWithTag(entityData, transform, tag) {
return spawn(entityData, transform, modifyFn); return spawn(entityData, transform, modifyFn);
} }
/**
* Delete all entities with the tag `tag` in their userData.
* @function deleteEntitiesWithTag
*/
function deleteEntitiesWithTag(tag) { function deleteEntitiesWithTag(tag) {
debug("searching for...:", tag); debug("searching for...:", tag);
var entityIDs = findEntitiesWithTag(tag); var entityIDs = findEntitiesWithTag(tag);
@ -192,6 +193,7 @@ function deleteEntitiesWithTag(tag) {
Entities.deleteEntity(entityIDs[i]); Entities.deleteEntity(entityIDs[i]);
} }
} }
function editEntitiesWithTag(tag, propertiesOrFn) { function editEntitiesWithTag(tag, propertiesOrFn) {
var entities = TUTORIAL_TAG_TO_ENTITY_IDS_MAP[tag]; var entities = TUTORIAL_TAG_TO_ENTITY_IDS_MAP[tag];
@ -208,19 +210,130 @@ function editEntitiesWithTag(tag, propertiesOrFn) {
} }
} }
function findEntitiesWithTag(tag) {
return findEntities({ userData: "" }, 10000, function(properties, key, value) {
data = parseJSON(value);
return data.tag == tag;
});
}
// From http://stackoverflow.com/questions/5999998/how-can-i-check-if-a-javascript-variable-is-function-type // From http://stackoverflow.com/questions/5999998/how-can-i-check-if-a-javascript-variable-is-function-type
function isFunction(functionToCheck) { function isFunction(functionToCheck) {
var getType = {}; var getType = {};
return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
} }
/**
* Return `true` if `entityID` can be found in the local entity tree, otherwise `false`.
*/
function isEntityInLocalTree(entityID) {
return Entities.getEntityProperties(entityID, 'visible').visible !== undefined;
}
/**
*
*/
function showEntitiesWithTags(tags) {
for (var i = 0; i < tags.length; ++i) {
showEntitiesWithTag(tags[i]);
}
}
function showEntitiesWithTag(tag) {
var entities = TUTORIAL_TAG_TO_ENTITY_IDS_MAP[tag];
if (entities) {
for (entityID in entities) {
var data = entities[entityID];
var collisionless = data.visible === false ? true : false;
if (data.collidable !== undefined) {
collisionless = data.collidable === true ? false : true;
}
if (data.soundKey) {
data.soundKey.playing = true;
}
var newProperties = {
visible: data.visible == false ? false : true,
collisionless: collisionless,
userData: JSON.stringify(data),
};
debug("Showing: ", entityID, ", Is in local tree: ", isEntityInLocalTree(entityID));
Entities.editEntity(entityID, newProperties);
}
} else {
debug("ERROR | No entities for tag: ", tag);
}
return;
// Dynamic method, suppressed for now
//editEntitiesWithTag(tag, function(entityID) {
// var userData = Entities.getEntityProperties(entityID, "userData").userData;
// var data = parseJSON(userData);
// var collisionless = data.visible === false ? true : false;
// if (data.collidable !== undefined) {
// collisionless = data.collidable === true ? false : true;
// }
// if (data.soundKey) {
// data.soundKey.playing = true;
// }
// var newProperties = {
// visible: data.visible == false ? false : true,
// collisionless: collisionless,
// userData: JSON.stringify(data),
// };
// Entities.editEntity(entityID, newProperties);
//});
}
function hideEntitiesWithTags(tags) {
for (var i = 0; i < tags.length; ++i) {
hideEntitiesWithTag(tags[i]);
}
}
function hideEntitiesWithTag(tag) {
var entities = TUTORIAL_TAG_TO_ENTITY_IDS_MAP[tag];
if (entities) {
for (entityID in entities) {
var data = entities[entityID];
if (data.soundKey) {
data.soundKey.playing = false;
}
var newProperties = {
visible: false,
collisionless: 1,
ignoreForCollisions: 1,
userData: JSON.stringify(data),
};
debug("Hiding: ", entityID, ", Is in local tree: ", isEntityInLocalTree(entityID));
Entities.editEntity(entityID, newProperties);
}
}
return;
// Dynamic method, suppressed for now
//editEntitiesWithTag(tag, function(entityID) {
// var userData = Entities.getEntityProperties(entityID, "userData").userData;
// var data = parseJSON(userData);
// if (data.soundKey) {
// data.soundKey.playing = false;
// }
// var newProperties = {
// visible: false,
// collisionless: 1,
// ignoreForCollisions: 1,
// userData: JSON.stringify(data),
// };
// Entities.editEntity(entityID, newProperties);
//});
}
/**
* Return the entity properties for an entity with a given name if it is in our
* cached list of entities. Otherwise, return undefined.
*/
function getEntityWithName(name) {
debug("Getting entity with name:", name);
var entityID = TUTORIAL_NAME_TO_ENTITY_PROPERTIES_MAP[name];
debug("Entity id: ", entityID, ", Is in local tree: ", isEntityInLocalTree(entityID));
return entityID;
}
function playSuccessSound() { function playSuccessSound() {
Audio.playSound(successSound, { Audio.playSound(successSound, {
position: MyAvatar.position, position: MyAvatar.position,
@ -229,7 +342,6 @@ function playSuccessSound() {
}); });
} }
function playFirecrackerSound(position) { function playFirecrackerSound(position) {
Audio.playSound(firecrackerSound, { Audio.playSound(firecrackerSound, {
position: position, position: position,
@ -238,27 +350,17 @@ function playFirecrackerSound(position) {
}); });
} }
/////////////////////////////////////////////////////////////////////////////// /**
/////////////////////////////////////////////////////////////////////////////// * This disables everything, including:
// // *
// STEP: DISABLE CONTROLLERS // * - The door to leave the tutorial
// // * - Overlays
/////////////////////////////////////////////////////////////////////////////// * - Hand controlelrs
var stepStart = function(name) { * - Teleportation
this.tag = name; * - Advanced movement
} * - Equip and far grab
stepStart.prototype = { * - Away mode
start: function(onFinish) { */
disableEverything();
HMD.requestShowHandControllers();
onFinish();
},
cleanup: function() {
}
};
function disableEverything() { function disableEverything() {
editEntitiesWithTag('door', { visible: true, collisionless: false }); editEntitiesWithTag('door', { visible: true, collisionless: false });
Menu.setIsOptionChecked("Overlays", false); Menu.setIsOptionChecked("Overlays", false);
@ -271,6 +373,11 @@ function disableEverything() {
farGrabEnabled: false, farGrabEnabled: false,
})); }));
setControllerPartLayer('touchpad', 'blank'); setControllerPartLayer('touchpad', 'blank');
setControllerPartLayer('trigger', 'blank');
setControllerPartLayer('joystick', 'blank');
setControllerPartLayer('grip', 'blank');
setControllerPartLayer('button_a', 'blank');
setControllerPartLayer('button_b', 'blank');
setControllerPartLayer('tips', 'blank'); setControllerPartLayer('tips', 'blank');
hideEntitiesWithTag('finish'); hideEntitiesWithTag('finish');
@ -278,6 +385,10 @@ function disableEverything() {
setAwayEnabled(false); setAwayEnabled(false);
} }
/**
* This reenables everything that disableEverything() disables. This can be
* used when leaving the tutorial to ensure that nothing is left disabled.
*/
function reenableEverything() { function reenableEverything() {
editEntitiesWithTag('door', { visible: false, collisionless: true }); editEntitiesWithTag('door', { visible: false, collisionless: true });
Menu.setIsOptionChecked("Overlays", true); Menu.setIsOptionChecked("Overlays", true);
@ -290,11 +401,39 @@ function reenableEverything() {
farGrabEnabled: true, farGrabEnabled: true,
})); }));
setControllerPartLayer('touchpad', 'blank'); setControllerPartLayer('touchpad', 'blank');
setControllerPartLayer('trigger', 'blank');
setControllerPartLayer('joystick', 'blank');
setControllerPartLayer('grip', 'blank');
setControllerPartLayer('button_a', 'blank');
setControllerPartLayer('button_b', 'blank');
setControllerPartLayer('tips', 'blank'); setControllerPartLayer('tips', 'blank');
MyAvatar.shouldRenderLocally = true; MyAvatar.shouldRenderLocally = true;
setAwayEnabled(true); setAwayEnabled(true);
} }
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// //
// STEP: DISABLE CONTROLLERS //
// //
///////////////////////////////////////////////////////////////////////////////
var stepStart = function() {
this.name = 'start';
};
stepStart.prototype = {
start: function(onFinish) {
disableEverything();
HMD.requestShowHandControllers();
onFinish();
},
cleanup: function() {
}
};
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// // // //
@ -302,10 +441,9 @@ function reenableEverything() {
// // // //
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
var stepEnableControllers = function(name) { var stepEnableControllers = function() {
this.tag = name;
this.shouldLog = false; this.shouldLog = false;
} };
stepEnableControllers.prototype = { stepEnableControllers.prototype = {
start: function(onFinish) { start: function(onFinish) {
reenableEverything(); reenableEverything();
@ -316,28 +454,6 @@ stepEnableControllers.prototype = {
} }
}; };
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// //
// STEP: Welcome //
// //
///////////////////////////////////////////////////////////////////////////////
var stepWelcome = function(name) {
this.tag = name;
}
stepWelcome.prototype = {
start: function(onFinish) {
this.timerID = Script.setTimeout(onFinish, 8000);
showEntitiesWithTag(this.tag);
},
cleanup: function() {
if (this.timerID) {
Script.clearTimeout(this.timerID);
this.timerID = null;
}
hideEntitiesWithTag(this.tag);
}
};
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
@ -346,9 +462,9 @@ stepWelcome.prototype = {
// STEP: Orient and raise hands above head // // STEP: Orient and raise hands above head //
// // // //
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
var stepOrient = function(name) { var stepOrient = function(tutorialManager) {
this.tag = name; this.name = 'orient';
this.tempTag = name + "-temporary"; this.tags = ["orient", "orient-" + tutorialManager.controllerName];
} }
stepOrient.prototype = { stepOrient.prototype = {
start: function(onFinish) { start: function(onFinish) {
@ -357,7 +473,8 @@ stepOrient.prototype = {
var tag = this.tag; var tag = this.tag;
// Spawn content set // Spawn content set
editEntitiesWithTag(this.tag, { visible: true }); //editEntitiesWithTag(this.tag, { visible: true });
showEntitiesWithTags(this.tags);
this.checkIntervalID = null; this.checkIntervalID = null;
function checkForHandsAboveHead() { function checkForHandsAboveHead() {
@ -386,8 +503,8 @@ stepOrient.prototype = {
Script.clearInterval(this.checkIntervalID); Script.clearInterval(this.checkIntervalID);
this.checkIntervalID = null; this.checkIntervalID = null;
} }
editEntitiesWithTag(this.tag, { visible: false, collisionless: 1 }); //editEntitiesWithTag(this.tag, { visible: false, collisionless: 1 });
deleteEntitiesWithTag(this.tempTag); hideEntitiesWithTags(this.tags);
} }
}; };
@ -399,9 +516,10 @@ stepOrient.prototype = {
// STEP: Near Grab // // STEP: Near Grab //
// // // //
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
var stepNearGrab = function(name) { var stepNearGrab = function() {
this.tag = name; this.name = 'nearGrab';
this.tempTag = name + "-temporary"; this.tag = "nearGrab";
this.tempTag = "nearGrab-temporary";
this.birdIDs = []; this.birdIDs = [];
Messages.subscribe("Entity-Exploded"); Messages.subscribe("Entity-Exploded");
@ -414,7 +532,6 @@ stepNearGrab.prototype = {
setControllerPartLayer('tips', 'trigger'); setControllerPartLayer('tips', 'trigger');
setControllerPartLayer('trigger', 'highlight'); setControllerPartLayer('trigger', 'highlight');
var tag = this.tag;
// Spawn content set // Spawn content set
showEntitiesWithTag(this.tag, { visible: true }); showEntitiesWithTag(this.tag, { visible: true });
@ -457,6 +574,7 @@ stepNearGrab.prototype = {
setControllerPartLayer('tips', 'blank'); setControllerPartLayer('tips', 'blank');
setControllerPartLayer('trigger', 'normal'); setControllerPartLayer('trigger', 'normal');
hideEntitiesWithTag(this.tag, { visible: false}); hideEntitiesWithTag(this.tag, { visible: false});
hideEntitiesWithTag('bothGrab', { visible: false});
deleteEntitiesWithTag(this.tempTag); deleteEntitiesWithTag(this.tempTag);
if (this.positionWatcher) { if (this.positionWatcher) {
this.positionWatcher.destroy(); this.positionWatcher.destroy();
@ -473,9 +591,10 @@ stepNearGrab.prototype = {
// STEP: Far Grab // // STEP: Far Grab //
// // // //
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
var stepFarGrab = function(name) { var stepFarGrab = function() {
this.tag = name; this.name = 'farGrab';
this.tempTag = name + "-temporary"; this.tag = "farGrab";
this.tempTag = "farGrab-temporary";
this.finished = true; this.finished = true;
this.birdIDs = []; this.birdIDs = [];
@ -535,7 +654,6 @@ stepFarGrab.prototype = {
setControllerPartLayer('tips', 'blank'); setControllerPartLayer('tips', 'blank');
setControllerPartLayer('trigger', 'normal'); setControllerPartLayer('trigger', 'normal');
hideEntitiesWithTag(this.tag, { visible: false}); hideEntitiesWithTag(this.tag, { visible: false});
hideEntitiesWithTag('bothGrab', { visible: false});
deleteEntitiesWithTag(this.tempTag); deleteEntitiesWithTag(this.tempTag);
if (this.positionWatcher) { if (this.positionWatcher) {
this.positionWatcher.destroy(); this.positionWatcher.destroy();
@ -576,11 +694,16 @@ PositionWatcher.prototype = {
// STEP: Equip // // STEP: Equip //
// // // //
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
var stepEquip = function(name) { var stepEquip = function(tutorialManager) {
this.tag = name; const controllerName = tutorialManager.controllerName;
this.tagPart1 = name + "-part1";
this.tagPart2 = name + "-part2"; this.name = 'equip';
this.tempTag = name + "-temporary";
this.tags = ["equip", "equip-" + controllerName];
this.tagsPart1 = ["equip-part1", "equip-part1-" + controllerName];
this.tagsPart2 = ["equip-part2", "equip-part2-" + controllerName];
this.tempTag = "equip-temporary";
this.PART1 = 0; this.PART1 = 0;
this.PART2 = 1; this.PART2 = 1;
this.PART3 = 2; this.PART3 = 2;
@ -593,6 +716,7 @@ stepEquip.prototype = {
start: function(onFinish) { start: function(onFinish) {
setControllerPartLayer('tips', 'trigger'); setControllerPartLayer('tips', 'trigger');
setControllerPartLayer('trigger', 'highlight'); setControllerPartLayer('trigger', 'highlight');
Messages.sendLocalMessage('Hifi-Grab-Disable', JSON.stringify({ Messages.sendLocalMessage('Hifi-Grab-Disable', JSON.stringify({
holdEnabled: true, holdEnabled: true,
})); }));
@ -600,8 +724,8 @@ stepEquip.prototype = {
var tag = this.tag; var tag = this.tag;
// Spawn content set // Spawn content set
showEntitiesWithTag(this.tag); showEntitiesWithTags(this.tags);
showEntitiesWithTag(this.tagPart1); showEntitiesWithTags(this.tagsPart1);
this.currentPart = this.PART1; this.currentPart = this.PART1;
@ -658,9 +782,10 @@ stepEquip.prototype = {
Script.setTimeout(function() { Script.setTimeout(function() {
debug("Equip | Starting part 3"); debug("Equip | Starting part 3");
this.currentPart = this.PART3; this.currentPart = this.PART3;
hideEntitiesWithTag(this.tagPart1); hideEntitiesWithTags(this.tagsPart1);
showEntitiesWithTag(this.tagPart2); showEntitiesWithTags(this.tagsPart2);
setControllerPartLayer('trigger', 'normal'); setControllerPartLayer('trigger', 'normal');
setControllerPartLayer('grip', 'highlight');
setControllerPartLayer('tips', 'grip'); setControllerPartLayer('tips', 'grip');
Messages.subscribe('Hifi-Object-Manipulation'); Messages.subscribe('Hifi-Object-Manipulation');
debug("Equip | Finished starting part 3"); debug("Equip | Finished starting part 3");
@ -687,16 +812,19 @@ stepEquip.prototype = {
} }
setControllerPartLayer('tips', 'blank'); setControllerPartLayer('tips', 'blank');
setControllerPartLayer('grip', 'normal');
setControllerPartLayer('trigger', 'normal'); setControllerPartLayer('trigger', 'normal');
this.stopWatchingGun(); this.stopWatchingGun();
this.currentPart = this.COMPLETE; this.currentPart = this.COMPLETE;
if (this.checkCollidesTimer) { if (this.checkCollidesTimer) {
Script.clearInterval(this.checkCollidesTimer); Script.clearInterval(this.checkCollidesTimer);
this.checkColllidesTimer = null;
} }
hideEntitiesWithTag(this.tagPart1);
hideEntitiesWithTag(this.tagPart2); hideEntitiesWithTags(this.tagsPart1);
hideEntitiesWithTag(this.tag); hideEntitiesWithTags(this.tagsPart2);
hideEntitiesWithTags(this.tags);
deleteEntitiesWithTag(this.tempTag); deleteEntitiesWithTag(this.tempTag);
} }
}; };
@ -710,9 +838,11 @@ stepEquip.prototype = {
// STEP: Turn Around // // STEP: Turn Around //
// // // //
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
var stepTurnAround = function(name) { var stepTurnAround = function(tutorialManager) {
this.tag = name; this.name = 'turnAround';
this.tempTag = name + "-temporary";
this.tags = ["turnAround", "turnAround-" + tutorialManager.controllerName];
this.tempTag = "turnAround-temporary";
this.onActionBound = this.onAction.bind(this); this.onActionBound = this.onAction.bind(this);
this.numTimesSnapTurnPressed = 0; this.numTimesSnapTurnPressed = 0;
@ -720,10 +850,11 @@ var stepTurnAround = function(name) {
} }
stepTurnAround.prototype = { stepTurnAround.prototype = {
start: function(onFinish) { start: function(onFinish) {
setControllerPartLayer('joystick', 'highlight');
setControllerPartLayer('touchpad', 'arrows'); setControllerPartLayer('touchpad', 'arrows');
setControllerPartLayer('tips', 'arrows'); setControllerPartLayer('tips', 'arrows');
showEntitiesWithTag(this.tag); showEntitiesWithTags(this.tags);
this.numTimesSnapTurnPressed = 0; this.numTimesSnapTurnPressed = 0;
this.numTimesSmoothTurnPressed = 0; this.numTimesSmoothTurnPressed = 0;
@ -776,13 +907,14 @@ stepTurnAround.prototype = {
} catch (e) { } catch (e) {
} }
setControllerPartLayer('joystick', 'normal');
setControllerPartLayer('touchpad', 'blank'); setControllerPartLayer('touchpad', 'blank');
setControllerPartLayer('tips', 'blank'); setControllerPartLayer('tips', 'blank');
if (this.interval) { if (this.interval) {
Script.clearInterval(this.interval); Script.clearInterval(this.interval);
} }
hideEntitiesWithTag(this.tag); hideEntitiesWithTags(this.tags);
deleteEntitiesWithTag(this.tempTag); deleteEntitiesWithTag(this.tempTag);
} }
}; };
@ -796,12 +928,15 @@ stepTurnAround.prototype = {
// STEP: Teleport // // STEP: Teleport //
// // // //
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
var stepTeleport = function(name) { var stepTeleport = function(tutorialManager) {
this.tag = name; this.name = 'teleport';
this.tempTag = name + "-temporary";
this.tags = ["teleport", "teleport-" + tutorialManager.controllerName];
this.tempTag = "teleport-temporary";
} }
stepTeleport.prototype = { stepTeleport.prototype = {
start: function(onFinish) { start: function(onFinish) {
setControllerPartLayer('button_a', 'highlight');
setControllerPartLayer('touchpad', 'teleport'); setControllerPartLayer('touchpad', 'teleport');
setControllerPartLayer('tips', 'teleport'); setControllerPartLayer('tips', 'teleport');
@ -831,17 +966,18 @@ stepTeleport.prototype = {
} }
this.checkCollidesTimer = Script.setInterval(checkCollides.bind(this), 500); this.checkCollidesTimer = Script.setInterval(checkCollides.bind(this), 500);
showEntitiesWithTag(this.tag); showEntitiesWithTags(this.tags);
}, },
cleanup: function() { cleanup: function() {
debug("Teleport | Cleanup"); debug("Teleport | Cleanup");
setControllerPartLayer('button_a', 'normal');
setControllerPartLayer('touchpad', 'blank'); setControllerPartLayer('touchpad', 'blank');
setControllerPartLayer('tips', 'blank'); setControllerPartLayer('tips', 'blank');
if (this.checkCollidesTimer) { if (this.checkCollidesTimer) {
Script.clearInterval(this.checkCollidesTimer); Script.clearInterval(this.checkCollidesTimer);
} }
hideEntitiesWithTag(this.tag); hideEntitiesWithTags(this.tags);
deleteEntitiesWithTag(this.tempTag); deleteEntitiesWithTag(this.tempTag);
} }
}; };
@ -856,9 +992,11 @@ stepTeleport.prototype = {
// STEP: Finish // // STEP: Finish //
// // // //
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
var stepFinish = function(name) { var stepFinish = function() {
this.tag = name; this.name = 'finish';
this.tempTag = name + "-temporary";
this.tag = "finish";
this.tempTag = "finish-temporary";
} }
stepFinish.prototype = { stepFinish.prototype = {
start: function(onFinish) { start: function(onFinish) {
@ -872,6 +1010,8 @@ stepFinish.prototype = {
}; };
var stepCleanupFinish = function() { var stepCleanupFinish = function() {
this.name = 'cleanup';
this.shouldLog = false; this.shouldLog = false;
} }
stepCleanupFinish.prototype = { stepCleanupFinish.prototype = {
@ -885,101 +1025,6 @@ stepCleanupFinish.prototype = {
function isEntityInLocalTree(entityID) {
return Entities.getEntityProperties(entityID, 'visible').visible !== undefined;
}
function showEntitiesWithTag(tag) {
var entities = TUTORIAL_TAG_TO_ENTITY_IDS_MAP[tag];
if (entities) {
for (entityID in entities) {
var data = entities[entityID];
var collisionless = data.visible === false ? true : false;
if (data.collidable !== undefined) {
collisionless = data.collidable === true ? false : true;
}
if (data.soundKey) {
data.soundKey.playing = true;
}
var newProperties = {
visible: data.visible == false ? false : true,
collisionless: collisionless,
userData: JSON.stringify(data),
};
debug("Showing: ", entityID, ", Is in local tree: ", isEntityInLocalTree(entityID));
Entities.editEntity(entityID, newProperties);
}
}
// Dynamic method, suppressed for now
return;
editEntitiesWithTag(tag, function(entityID) {
var userData = Entities.getEntityProperties(entityID, "userData").userData;
var data = parseJSON(userData);
var collisionless = data.visible === false ? true : false;
if (data.collidable !== undefined) {
collisionless = data.collidable === true ? false : true;
}
if (data.soundKey) {
data.soundKey.playing = true;
}
var newProperties = {
visible: data.visible == false ? false : true,
collisionless: collisionless,
userData: JSON.stringify(data),
};
Entities.editEntity(entityID, newProperties);
});
}
function hideEntitiesWithTag(tag) {
var entities = TUTORIAL_TAG_TO_ENTITY_IDS_MAP[tag];
if (entities) {
for (entityID in entities) {
var data = entities[entityID];
if (data.soundKey) {
data.soundKey.playing = false;
}
var newProperties = {
visible: false,
collisionless: 1,
ignoreForCollisions: 1,
userData: JSON.stringify(data),
};
debug("Hiding: ", entityID, ", Is in local tree: ", isEntityInLocalTree(entityID));
Entities.editEntity(entityID, newProperties);
}
}
// Dynamic method, suppressed for now
return;
editEntitiesWithTag(tag, function(entityID) {
var userData = Entities.getEntityProperties(entityID, "userData").userData;
var data = parseJSON(userData);
if (data.soundKey) {
data.soundKey.playing = false;
}
var newProperties = {
visible: false,
collisionless: 1,
ignoreForCollisions: 1,
userData: JSON.stringify(data),
};
Entities.editEntity(entityID, newProperties);
});
}
// Return the entity properties for an entity with a given name if it is in our
// cached list of entities. Otherwise, return undefined.
function getEntityWithName(name) {
debug("Getting entity with name:", name);
var entityID = TUTORIAL_NAME_TO_ENTITY_PROPERTIES_MAP[name];
debug("Entity id: ", entityID, ", Is in local tree: ", isEntityInLocalTree(entityID));
return entityID;
}
TutorialManager = function() { TutorialManager = function() {
@ -992,11 +1037,25 @@ TutorialManager = function() {
var didFinishTutorial = false; var didFinishTutorial = false;
var wentToEntryStepNum; var wentToEntryStepNum;
var VERSION = 1; var VERSION = 2;
var tutorialID; var tutorialID;
var self = this; var self = this;
// The real controller name is the actual detected controller name, or 'unknown'
// if one is not found.
if (HMD.isSubdeviceContainingNameAvailable("OculusTouch")) {
this.controllerName = "touch";
this.realControllerName = "touch";
} else if (HMD.isHandControllerAvailable("OpenVR")) {
this.controllerName = "vive";
this.realControllerName = "vive";
} else {
info("ERROR, no known hand controller found, defaulting to Vive");
this.controllerName = "vive";
this.realControllerName = "unknown";
}
this.startTutorial = function() { this.startTutorial = function() {
currentStepNum = -1; currentStepNum = -1;
currentStep = null; currentStep = null;
@ -1006,15 +1065,15 @@ TutorialManager = function() {
// If Script.generateUUID is not available, default to an empty string. // If Script.generateUUID is not available, default to an empty string.
tutorialID = Script.generateUUID ? Script.generateUUID() : ""; tutorialID = Script.generateUUID ? Script.generateUUID() : "";
STEPS = [ STEPS = [
new stepStart("start"), new stepStart(this),
new stepOrient("orient"), new stepOrient(this),
new stepNearGrab("nearGrab"), new stepFarGrab(this),
new stepFarGrab("farGrab"), new stepNearGrab(this),
new stepEquip("equip"), new stepEquip(this),
new stepTurnAround("turnAround"), new stepTurnAround(this),
new stepTeleport("teleport"), new stepTeleport(this),
new stepFinish("finish"), new stepFinish(this),
new stepEnableControllers("enableControllers"), new stepEnableControllers(this),
]; ];
wentToEntryStepNum = STEPS.length; wentToEntryStepNum = STEPS.length;
for (var i = 0; i < STEPS.length; ++i) { for (var i = 0; i < STEPS.length; ++i) {
@ -1027,7 +1086,7 @@ TutorialManager = function() {
this.onFinish = function() { this.onFinish = function() {
debug("onFinish", currentStepNum); debug("onFinish", currentStepNum);
if (currentStep && currentStep.shouldLog !== false) { if (currentStep && currentStep.shouldLog !== false) {
self.trackStep(currentStep.tag, currentStepNum); self.trackStep(currentStep.name, currentStepNum);
} }
self.startNextStep(); self.startNextStep();
@ -1083,7 +1142,7 @@ TutorialManager = function() {
var tutorialTimeElapsed = (Date.now() - startedTutorialAt) / 1000; var tutorialTimeElapsed = (Date.now() - startedTutorialAt) / 1000;
UserActivityLogger.tutorialProgress( UserActivityLogger.tutorialProgress(
name, stepNum, timeToFinishStep, tutorialTimeElapsed, name, stepNum, timeToFinishStep, tutorialTimeElapsed,
tutorialID, VERSION); tutorialID, VERSION, this.realControllerName);
} }
// This is a message sent from the "entry" portal in the courtyard, // This is a message sent from the "entry" portal in the courtyard,
@ -1099,6 +1158,25 @@ TutorialManager = function() {
// To run the tutorial: // To run the tutorial:
// //
// var tutorialManager = new TutorialManager(); //var tutorialManager = new TutorialManager();
// tutorialManager.startTutorial(); //tutorialManager.startTutorial();
// //
//
//var keyReleaseHandler = function(event) {
// if (event.isShifted && event.isAlt) {
// print('here', event.text);
// if (event.text == "F12") {
// if (!tutorialManager.startNextStep()) {
// tutorialManager.startTutorial();
// }
// } else if (event.text == "F11") {
// tutorialManager.restartStep();
// } else if (event.text == "F10") {
// MyAvatar.shouldRenderLocally = !MyAvatar.shouldRenderLocally;
// } else if (event.text == "r") {
// tutorialManager.stopTutorial();
// tutorialManager.startTutorial();
// }
// }
//};
//Controller.keyReleaseEvent.connect(keyReleaseHandler);

View file

@ -1,8 +1,25 @@
TUTORIAL_TAG_TO_ENTITY_IDS_MAP = { TUTORIAL_TAG_TO_ENTITY_IDS_MAP = {
"teleport": { "teleport-vive": {
"{7df1abc4-1b7c-4352-985c-f3f6ad8d65b7}": {
"tag": "teleport-vive"
}
},
"teleport-touch": {
"{ff064b9e-7fa4-4693-a386-a67b9f92a948}": { "{ff064b9e-7fa4-4693-a386-a67b9f92a948}": {
"tag": "teleport" "tag": "teleport-touch"
}, }
},
"turnAround-vive": {
"{9b14f224-b2f6-447f-bb86-f5d875cf4c33}": {
"tag": "turnAround-vive"
}
},
"turnAround-touch": {
"{ce74b3ca-d1c7-4980-bd98-2d488095a39e}": {
"tag": "turnAround-touch"
}
},
"teleport": {
"{4478f7b5-d3ac-4213-9a7b-ad8cd69575b8}": { "{4478f7b5-d3ac-4213-9a7b-ad8cd69575b8}": {
"tag": "teleport" "tag": "teleport"
} }
@ -82,17 +99,19 @@ TUTORIAL_TAG_TO_ENTITY_IDS_MAP = {
"tag": "equip-part1" "tag": "equip-part1"
} }
}, },
"equip-part2": { "equip-part2-vive": {
"{b5d17eda-90ab-40cf-b973-efcecb2e992e}": { "{b5d17eda-90ab-40cf-b973-efcecb2e992e}": {
"tag": "equip-part2" "tag": "equip-part2-vive"
},
"{6307cd16-dd1d-4988-a339-578178436b45}": {
"tag": "equip-part2"
} }
}, },
"turnAround": { "equip-part2-touch": {
"{ce74b3ca-d1c7-4980-bd98-2d488095a39e}": { "{69195139-e020-4739-bb2c-50faebc6860a}": {
"tag": "turnAround" "tag": "equip-part2-touch"
}
},
"equip-part2": {
"{6307cd16-dd1d-4988-a339-578178436b45}": {
"tag": "equip-part2"
} }
}, },
"bothGrab": { "bothGrab": {
@ -143,133 +162,144 @@ TUTORIAL_TAG_TO_ENTITY_IDS_MAP = {
"tag": "equip" "tag": "equip"
} }
}, },
"orient": { "orient-vive": {
"{95d233ab-ed0a-46e1-b047-1c542688ef3f}": { "{95d233ab-ed0a-46e1-b047-1c542688ef3f}": {
"tag": "orient" "tag": "orient-vive"
}
},
"orient-touch": {
"{1c95f945-ec46-4aac-b0f1-e64e073dbfaa}": {
"tag": "orient-touch"
} }
} }
}; };
TUTORIAL_NAME_TO_ENTITY_PROPERTIES_MAP = { TUTORIAL_NAME_TO_ENTITY_PROPERTIES_MAP = {
"tutorial/gun_spawn": { "tutorial/gun_spawn": {
"userData": "{\"tag\":\"equip\",\"visible\":false}", "clientOnly": 0,
"dimensions": {
"y": 0.0649842768907547,
"x": 0.0649842768907547,
"z": 0.0649842768907547
},
"collisionless": 1, "collisionless": 1,
"created": "2016-09-08T18:38:24Z",
"color": { "color": {
"blue": 0, "blue": 0,
"green": 0, "green": 0,
"red": 255 "red": 255
}, },
"queryAACube": { "created": "2016-09-08T18:38:24Z",
"y": 0.6283726096153259, "dimensions": {
"x": 0.6865367293357849, "x": 0.0649842768907547,
"scale": 0.11255607008934021, "y": 0.0649842768907547,
"z": 0.3359576463699341 "z": 0.0649842768907547
}, },
"visible": 0, "id": "{9df518da-9e65-4b76-8a79-eeefdb0b7310}",
"shape": "Cube", "ignoreForCollisions": 1,
"clientOnly": 0, "lastEdited": 1481926907366120,
"lastEditedBy": "{b80185ea-0936-4397-a5a4-3a64004f545f}",
"name": "tutorial/gun_spawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}", "owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"position": { "position": {
"y": 0.6846506595611572, "x": 0.60231781005859375,
"x": 0.7428147792816162, "y": 0.68465065956115723,
"z": 0.3922356963157654 "z": 0.39223569631576538
},
"queryAACube": {
"scale": 0.11255607008934021,
"x": 0.54603976011276245,
"y": 0.62837260961532593,
"z": 0.33595764636993408
}, },
"rotation": { "rotation": {
"y": 0.7066605091094971, "w": -0.025101065635681152,
"x": 0.7066605091094971, "x": 0.70666050910949707,
"z": -0.025131583213806152, "y": 0.70666050910949707,
"w": -0.025101065635681152 "z": -0.025131583213806152
}, },
"ignoreForCollisions": 1, "shape": "Cube",
"type": "Box", "type": "Box",
"id": "{9df518da-9e65-4b76-8a79-eeefdb0b7310}", "userData": "{\"visible\":false,\"tag\":\"equip\"}",
"name": "tutorial/gun_spawn" "visible": 0
}, },
"tutorial/nearGrab/box_spawn": { "tutorial/nearGrab/box_spawn": {
"userData": "{\"tag\":\"nearGrab\",\"visible\":false}", "clientOnly": 0,
"dimensions": {
"y": 0.08225371688604355,
"x": 0.08225371688604355,
"z": 0.08225371688604355
},
"collisionless": 1, "collisionless": 1,
"created": "2016-09-08T18:38:24Z",
"color": { "color": {
"blue": 255, "blue": 255,
"green": 0, "green": 0,
"red": 255 "red": 255
}, },
"queryAACube": { "created": "2016-09-08T18:38:24Z",
"y": 0.738319456577301, "dimensions": {
"x": 0.8985498547554016, "x": 0.082253716886043549,
"scale": 0.14246761798858643, "y": 0.082253716886043549,
"z": 0.29067665338516235 "z": 0.082253716886043549
}, },
"visible": 0, "id": "{5cf22b9c-fb22-4854-8821-554422980b24}",
"shape": "Cube", "ignoreForCollisions": 1,
"clientOnly": 0, "lastEdited": 1481926907334206,
"lastEditedBy": "{b80185ea-0936-4397-a5a4-3a64004f545f}",
"name": "tutorial/nearGrab/box_spawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}", "owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"position": { "position": {
"y": 0.8095532655715942, "x": 0.61857688426971436,
"x": 0.9697836637496948, "y": 0.80955326557159424,
"z": 0.36191046237945557 "z": 0.36191046237945557
}, },
"rotation": { "queryAACube": {
"y": -1.52587890625e-05, "scale": 0.14246761798858643,
"x": -1.52587890625e-05, "x": 0.54734307527542114,
"z": -1.52587890625e-05, "y": 0.73831945657730103,
"w": 1 "z": 0.29067665338516235
}, },
"ignoreForCollisions": 1, "rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shape": "Cube",
"type": "Box", "type": "Box",
"id": "{5cf22b9c-fb22-4854-8821-554422980b24}", "userData": "{\"visible\":false,\"tag\":\"nearGrab\"}",
"name": "tutorial/nearGrab/box_spawn" "visible": 0
}, },
"tutorial/farGrab/box_spawn": { "tutorial/farGrab/box_spawn": {
"userData": "{\"tag\":\"farGrab\",\"visible\":false}", "clientOnly": 0,
"dimensions": {
"y": 0.37358683347702026,
"x": 0.37358683347702026,
"z": 0.37358683347702026
},
"collisionless": 1, "collisionless": 1,
"created": "2016-09-08T18:38:24Z",
"color": { "color": {
"blue": 255, "blue": 255,
"green": 0, "green": 0,
"red": 255 "red": 255
}, },
"queryAACube": { "created": "2016-09-08T18:38:24Z",
"y": 0.3304251432418823, "dimensions": {
"x": 3.0951309204101562, "x": 0.16850528120994568,
"scale": 0.647071361541748, "y": 0.16850528120994568,
"z": 0.18027013540267944 "z": 0.16850528120994568
}, },
"visible": 0, "id": "{70fcd96c-cd59-4f23-9ca5-a167f2f85680}",
"shape": "Cube", "ignoreForCollisions": 1,
"clientOnly": 0, "lastEdited": 1481926908795578,
"lastEditedBy": "{b80185ea-0936-4397-a5a4-3a64004f545f}",
"name": "tutorial/farGrab/box_spawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}", "owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"position": { "position": {
x: 3.4866, "x": 3.4866282939910889,
y: 0.6716, "y": 0.67159509658813477,
z: 0.4789 "z": 0.47892442345619202
},
"queryAACube": {
"scale": 0.64707136154174805,
"x": 3.2037394046783447,
"y": 0.33042514324188232,
"z": 0.14542555809020996
}, },
"rotation": { "rotation": {
"y": -1.52587890625e-05, "w": 1,
"x": -1.52587890625e-05, "x": -1.52587890625e-05,
"z": -1.52587890625e-05, "y": -1.52587890625e-05,
"w": 1 "z": -1.52587890625e-05
}, },
"ignoreForCollisions": 1, "shape": "Cube",
"type": "Box", "type": "Box",
"id": "{70fcd96c-cd59-4f23-9ca5-a167f2f85680}", "userData": "{\"visible\":false,\"tag\":\"farGrab\"}",
"name": "tutorial/farGrab/box_spawn" "visible": 0
}, },
"tutorial/teleport/pad": { "tutorial/teleport/pad": {
"userData": "{\"tag\":\"teleport\"}", "userData": "{\"tag\":\"teleport\"}",

View file

@ -45,7 +45,7 @@ if (!Function.prototype.bind) {
keyReleaseHandler: function(event) { keyReleaseHandler: function(event) {
print(event.text); print(event.text);
if (event.isShifted && event.isAlt) { if (event.isShifted && event.isAlt) {
if (event.text == ",") { if (event.text == "F12") {
if (!this.tutorialManager.startNextStep()) { if (!this.tutorialManager.startNextStep()) {
this.tutorialManager.startTutorial(); this.tutorialManager.startTutorial();
} }