mirror of
https://github.com/overte-org/overte.git
synced 2025-04-08 05:32:46 +02:00
1202 lines
41 KiB
JavaScript
1202 lines
41 KiB
JavaScript
//
|
|
// tutorial.js
|
|
//
|
|
// Created by Ryan Huffman on 9/1/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
|
|
//
|
|
|
|
Script.include("entityData.js");
|
|
Script.include("lighter/createButaneLighter.js");
|
|
Script.include("tutorialEntityIDs.js");
|
|
|
|
if (!Function.prototype.bind) {
|
|
Function.prototype.bind = function(oThis) {
|
|
if (typeof this !== 'function') {
|
|
// closest thing possible to the ECMAScript 5
|
|
// internal IsCallable function
|
|
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
|
|
}
|
|
|
|
var aArgs = Array.prototype.slice.call(arguments, 1),
|
|
fToBind = this,
|
|
fNOP = function() {},
|
|
fBound = function() {
|
|
return fToBind.apply(this instanceof fNOP
|
|
? this
|
|
: oThis,
|
|
aArgs.concat(Array.prototype.slice.call(arguments)));
|
|
};
|
|
|
|
if (this.prototype) {
|
|
// Function.prototype doesn't have a prototype property
|
|
fNOP.prototype = this.prototype;
|
|
}
|
|
fBound.prototype = new fNOP();
|
|
|
|
return fBound;
|
|
};
|
|
}
|
|
|
|
var DEBUG = true;
|
|
function debug() {
|
|
if (DEBUG) {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
args.unshift("tutorial.js | ");
|
|
print.apply(this, args);
|
|
}
|
|
}
|
|
|
|
var INFO = true;
|
|
function info() {
|
|
if (INFO) {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
args.unshift("tutorial.js | ");
|
|
print.apply(this, args);
|
|
}
|
|
}
|
|
|
|
const CONTROLLER_TOUCH = 'touch';
|
|
const CONTROLLER_VIVE = 'vive';
|
|
|
|
var NEAR_BOX_SPAWN_NAME = "tutorial/nearGrab/box_spawn";
|
|
var FAR_BOX_SPAWN_NAME = "tutorial/farGrab/box_spawn";
|
|
var GUN_SPAWN_NAME = "tutorial/gun_spawn";
|
|
var TELEPORT_PAD_NAME = "tutorial/teleport/pad"
|
|
|
|
var successSound = SoundCache.getSound("atp:/tutorial_sounds/good_one.L.wav");
|
|
var firecrackerSound = SoundCache.getSound("atp:/tutorial_sounds/Pops_Firecracker.wav");
|
|
|
|
|
|
var CHANNEL_AWAY_ENABLE = "Hifi-Away-Enable";
|
|
function setAwayEnabled(value) {
|
|
var message = value ? 'enable' : 'disable';
|
|
Messages.sendLocalMessage(CHANNEL_AWAY_ENABLE, message);
|
|
}
|
|
|
|
findEntity = function(properties, searchRadius, filterFn) {
|
|
var entities = findEntities(properties, searchRadius, filterFn);
|
|
return entities.length > 0 ? entities[0] : null;
|
|
}
|
|
|
|
// Return all entities with properties `properties` within radius `searchRadius`
|
|
findEntities = function(properties, searchRadius, filterFn) {
|
|
if (!filterFn) {
|
|
filterFn = function(properties, key, value) {
|
|
return value == properties[key];
|
|
}
|
|
}
|
|
searchRadius = searchRadius ? searchRadius : 100000;
|
|
var entities = Entities.findEntities({ x: 0, y: 0, z: 0 }, searchRadius);
|
|
var matchedEntities = [];
|
|
var keys = Object.keys(properties);
|
|
for (var i = 0; i < entities.length; ++i) {
|
|
var match = true;
|
|
var candidateProperties = Entities.getEntityProperties(entities[i], keys);
|
|
for (var key in properties) {
|
|
if (!filterFn(properties, key, candidateProperties[key])) {
|
|
// This isn't a match, move to next entity
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
if (match) {
|
|
matchedEntities.push(entities[i]);
|
|
}
|
|
}
|
|
|
|
return matchedEntities;
|
|
}
|
|
|
|
function findEntitiesWithTag(tag) {
|
|
return findEntities({ userData: "" }, 10000, function(properties, key, value) {
|
|
data = parseJSON(value);
|
|
return data.tag === tag;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* A controller is 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) {
|
|
data = {};
|
|
data[part] = layer;
|
|
Messages.sendLocalMessage('Controller-Set-Part-Layer', JSON.stringify(data));
|
|
}
|
|
|
|
/**
|
|
* Spawn entities and return the newly created entity's ids.
|
|
* @param {object[]} entityPropertiesList A list of properties of the entities
|
|
* to spawn.
|
|
*/
|
|
function spawn(entityPropertiesList, transform, modifyFn) {
|
|
if (!transform) {
|
|
transform = {
|
|
position: { x: 0, y: 0, z: 0 },
|
|
rotation: { x: 0, y: 0, z: 0, w: 1 }
|
|
}
|
|
}
|
|
var ids = [];
|
|
for (var i = 0; i < entityPropertiesList.length; ++i) {
|
|
var data = entityPropertiesList[i];
|
|
data.position = Vec3.sum(transform.position, data.position);
|
|
data.rotation = Quat.multiply(data.rotation, transform.rotation);
|
|
if (modifyFn) {
|
|
data = modifyFn(data);
|
|
}
|
|
var id = Entities.addEntity(data);
|
|
ids.push(id);
|
|
}
|
|
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) {
|
|
var data;
|
|
try {
|
|
data = JSON.parse(jsonString);
|
|
} catch(e) {
|
|
data = {};
|
|
}
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Spawn entities with `tag` in the userData.
|
|
* @function spawnWithTag
|
|
*/
|
|
function spawnWithTag(entityData, transform, tag) {
|
|
function modifyFn(data) {
|
|
var userData = parseJSON(data.userData);
|
|
userData.tag = tag;
|
|
data.userData = JSON.stringify(userData);
|
|
debug("In modify", tag, userData, data.userData);
|
|
return data;
|
|
}
|
|
return spawn(entityData, transform, modifyFn);
|
|
}
|
|
|
|
/**
|
|
* Delete all entities with the tag `tag` in their userData.
|
|
* @function deleteEntitiesWithTag
|
|
*/
|
|
function deleteEntitiesWithTag(tag) {
|
|
debug("searching for...:", tag);
|
|
var entityIDs = findEntitiesWithTag(tag);
|
|
for (var i = 0; i < entityIDs.length; ++i) {
|
|
Entities.deleteEntity(entityIDs[i]);
|
|
}
|
|
}
|
|
|
|
function editEntitiesWithTag(tag, propertiesOrFn) {
|
|
var entities = TUTORIAL_TAG_TO_ENTITY_IDS_MAP[tag];
|
|
|
|
debug("Editing tag: ", tag);
|
|
if (entities) {
|
|
for (entityID in entities) {
|
|
debug("Editing: ", entityID, ", ", propertiesOrFn, ", Is in local tree: ", isEntityInLocalTree(entityID));
|
|
if (isFunction(propertiesOrFn)) {
|
|
Entities.editEntity(entityID, propertiesOrFn(entityIDs[i]));
|
|
} else {
|
|
Entities.editEntity(entityID, propertiesOrFn);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// From http://stackoverflow.com/questions/5999998/how-can-i-check-if-a-javascript-variable-is-function-type
|
|
function isFunction(functionToCheck) {
|
|
var getType = {};
|
|
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() {
|
|
Audio.playSound(successSound, {
|
|
position: MyAvatar.position,
|
|
volume: 0.7,
|
|
loop: false
|
|
});
|
|
}
|
|
|
|
function playFirecrackerSound(position) {
|
|
Audio.playSound(firecrackerSound, {
|
|
position: position,
|
|
volume: 0.5,
|
|
loop: false
|
|
});
|
|
}
|
|
|
|
/**
|
|
* This disables everything, including:
|
|
*
|
|
* - The door to leave the tutorial
|
|
* - Overlays
|
|
* - Hand controlelrs
|
|
* - Teleportation
|
|
* - Advanced movement
|
|
* - Equip and far grab
|
|
* - Away mode
|
|
*/
|
|
function disableEverything() {
|
|
editEntitiesWithTag('door', { visible: true, collisionless: false });
|
|
Menu.setIsOptionChecked("Overlays", false);
|
|
Controller.disableMapping('handControllerPointer-click');
|
|
Messages.sendLocalMessage('Hifi-Advanced-Movement-Disabler', 'disable');
|
|
Messages.sendLocalMessage('Hifi-Teleport-Disabler', 'both');
|
|
Messages.sendLocalMessage('Hifi-Grab-Disable', JSON.stringify({
|
|
nearGrabEnabled: true,
|
|
holdEnabled: false,
|
|
farGrabEnabled: false,
|
|
myAvatarScalingEnabled: false,
|
|
objectScalingEnabled: false,
|
|
}));
|
|
setControllerPartLayer('touchpad', 'blank');
|
|
setControllerPartLayer('trigger', 'blank');
|
|
setControllerPartLayer('joystick', 'blank');
|
|
setControllerPartLayer('grip', 'blank');
|
|
setControllerPartLayer('button_a', 'blank');
|
|
setControllerPartLayer('button_b', 'blank');
|
|
setControllerPartLayer('tips', 'blank');
|
|
|
|
hideEntitiesWithTag('finish');
|
|
|
|
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() {
|
|
editEntitiesWithTag('door', { visible: false, collisionless: true });
|
|
Menu.setIsOptionChecked("Overlays", true);
|
|
Controller.enableMapping('handControllerPointer-click');
|
|
Messages.sendLocalMessage('Hifi-Advanced-Movement-Disabler', 'enable');
|
|
Messages.sendLocalMessage('Hifi-Teleport-Disabler', 'none');
|
|
Messages.sendLocalMessage('Hifi-Grab-Disable', JSON.stringify({
|
|
nearGrabEnabled: true,
|
|
holdEnabled: true,
|
|
farGrabEnabled: true,
|
|
myAvatarScalingEnabled: true,
|
|
objectScalingEnabled: true,
|
|
}));
|
|
setControllerPartLayer('touchpad', 'blank');
|
|
setControllerPartLayer('trigger', 'blank');
|
|
setControllerPartLayer('joystick', 'blank');
|
|
setControllerPartLayer('grip', 'blank');
|
|
setControllerPartLayer('button_a', 'blank');
|
|
setControllerPartLayer('button_b', 'blank');
|
|
setControllerPartLayer('tips', 'blank');
|
|
MyAvatar.shouldRenderLocally = true;
|
|
setAwayEnabled(true);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// STEP: DISABLE CONTROLLERS //
|
|
// //
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
var stepStart = function() {
|
|
this.name = 'start';
|
|
};
|
|
stepStart.prototype = {
|
|
start: function(onFinish) {
|
|
disableEverything();
|
|
|
|
HMD.requestShowHandControllers();
|
|
|
|
onFinish();
|
|
},
|
|
cleanup: function() {
|
|
}
|
|
};
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// STEP: ENABLE CONTROLLERS //
|
|
// //
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
var stepEnableControllers = function() {
|
|
this.shouldLog = false;
|
|
};
|
|
stepEnableControllers.prototype = {
|
|
start: function(onFinish) {
|
|
reenableEverything();
|
|
HMD.requestHideHandControllers();
|
|
onFinish();
|
|
},
|
|
cleanup: function() {
|
|
}
|
|
};
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// STEP: Orient and raise hands above head //
|
|
// //
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
var stepOrient = function(tutorialManager) {
|
|
this.name = 'orient';
|
|
this.tags = ["orient", "orient-" + tutorialManager.controllerName];
|
|
}
|
|
stepOrient.prototype = {
|
|
start: function(onFinish) {
|
|
this.active = true;
|
|
|
|
var tag = this.tag;
|
|
|
|
// Spawn content set
|
|
//editEntitiesWithTag(this.tag, { visible: true });
|
|
showEntitiesWithTags(this.tags);
|
|
|
|
this.checkIntervalID = null;
|
|
function checkForHandsAboveHead() {
|
|
debug("Orient | Checking for hands above head");
|
|
if (MyAvatar.getLeftPalmPosition().y > (MyAvatar.getHeadPosition().y + 0.1)) {
|
|
Script.clearInterval(this.checkIntervalID);
|
|
this.checkIntervalID = null;
|
|
location = "/tutorial";
|
|
Script.setTimeout(playSuccessSound, 150);
|
|
this.active = false;
|
|
onFinish();
|
|
}
|
|
}
|
|
this.checkIntervalID = Script.setInterval(checkForHandsAboveHead.bind(this), 500);
|
|
},
|
|
cleanup: function() {
|
|
debug("Orient | Cleanup");
|
|
if (this.active) {
|
|
this.active = false;
|
|
}
|
|
if (this.overlay) {
|
|
this.overlay.destroy();
|
|
this.overlay = null;
|
|
}
|
|
if (this.checkIntervalID) {
|
|
Script.clearInterval(this.checkIntervalID);
|
|
this.checkIntervalID = null;
|
|
}
|
|
//editEntitiesWithTag(this.tag, { visible: false, collisionless: 1 });
|
|
hideEntitiesWithTags(this.tags);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// STEP: Near Grab //
|
|
// //
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
var stepNearGrab = function(tutorialManager) {
|
|
this.name = 'nearGrab';
|
|
this.tags = ["bothGrab", "nearGrab", "nearGrab-" + tutorialManager.controllerName];
|
|
this.tempTag = "nearGrab-temporary";
|
|
this.birdIDs = [];
|
|
|
|
this.controllerName = tutorialManager.controllerName;
|
|
|
|
Messages.subscribe("Entity-Exploded");
|
|
Messages.messageReceived.connect(this.onMessage.bind(this));
|
|
}
|
|
stepNearGrab.prototype = {
|
|
start: function(onFinish) {
|
|
this.finished = false;
|
|
this.onFinish = onFinish;
|
|
|
|
if (this.controllerName === CONTROLLER_TOUCH) {
|
|
setControllerPartLayer('tips', 'both_triggers');
|
|
setControllerPartLayer('trigger', 'highlight');
|
|
setControllerPartLayer('grip', 'highlight');
|
|
} else {
|
|
setControllerPartLayer('tips', 'trigger');
|
|
setControllerPartLayer('trigger', 'highlight');
|
|
}
|
|
|
|
// Show content set
|
|
showEntitiesWithTags(this.tags);
|
|
|
|
var boxSpawnPosition = getEntityWithName(NEAR_BOX_SPAWN_NAME).position;
|
|
function createBlock(fireworkNumber) {
|
|
fireworkBaseProps.position = boxSpawnPosition;
|
|
fireworkBaseProps.modelURL = fireworkURLs[fireworkNumber % fireworkURLs.length];
|
|
debug("Creating firework with url: ", fireworkBaseProps.modelURL);
|
|
return spawnWithTag([fireworkBaseProps], null, this.tempTag)[0];
|
|
}
|
|
|
|
this.birdIDs = [];
|
|
this.birdIDs.push(createBlock.bind(this)(0));
|
|
this.birdIDs.push(createBlock.bind(this)(1));
|
|
this.birdIDs.push(createBlock.bind(this)(2));
|
|
this.positionWatcher = new PositionWatcher(this.birdIDs, boxSpawnPosition, -0.4, 4);
|
|
},
|
|
onMessage: function(channel, message, seneder) {
|
|
if (this.finished) {
|
|
return;
|
|
}
|
|
if (channel == "Entity-Exploded") {
|
|
debug("NearGrab | Got entity-exploded message: ", message);
|
|
|
|
var data = parseJSON(message);
|
|
if (this.birdIDs.indexOf(data.entityID) >= 0) {
|
|
debug("NearGrab | It's one of the firecrackers");
|
|
playFirecrackerSound(data.position);
|
|
playSuccessSound();
|
|
this.finished = true;
|
|
this.onFinish();
|
|
}
|
|
}
|
|
},
|
|
cleanup: function() {
|
|
debug("NearGrab | Cleanup");
|
|
this.finished = true;
|
|
setControllerPartLayer('tips', 'blank');
|
|
setControllerPartLayer('trigger', 'normal');
|
|
setControllerPartLayer('grip', 'normal');
|
|
hideEntitiesWithTags(this.tags);
|
|
deleteEntitiesWithTag(this.tempTag);
|
|
if (this.positionWatcher) {
|
|
this.positionWatcher.destroy();
|
|
this.positionWatcher = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// STEP: Far Grab //
|
|
// //
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
var stepFarGrab = function() {
|
|
this.name = 'farGrab';
|
|
this.tag = "farGrab";
|
|
this.tempTag = "farGrab-temporary";
|
|
this.finished = true;
|
|
this.birdIDs = [];
|
|
|
|
Messages.subscribe("Entity-Exploded");
|
|
Messages.messageReceived.connect(this.onMessage.bind(this));
|
|
}
|
|
stepFarGrab.prototype = {
|
|
start: function(onFinish) {
|
|
this.finished = false;
|
|
this.onFinish = onFinish;
|
|
|
|
showEntitiesWithTag('bothGrab', { visible: true });
|
|
|
|
setControllerPartLayer('tips', 'trigger');
|
|
setControllerPartLayer('trigger', 'highlight');
|
|
Messages.sendLocalMessage('Hifi-Grab-Disable', JSON.stringify({
|
|
farGrabEnabled: true,
|
|
}));
|
|
var tag = this.tag;
|
|
|
|
// Spawn content set
|
|
showEntitiesWithTag(this.tag);
|
|
|
|
var boxSpawnPosition = getEntityWithName(FAR_BOX_SPAWN_NAME).position;
|
|
function createBlock(fireworkNumber) {
|
|
fireworkBaseProps.position = boxSpawnPosition;
|
|
fireworkBaseProps.modelURL = fireworkURLs[fireworkNumber % fireworkURLs.length];
|
|
debug("Creating firework with url: ", fireworkBaseProps.modelURL);
|
|
return spawnWithTag([fireworkBaseProps], null, this.tempTag)[0];
|
|
}
|
|
|
|
this.birdIDs = [];
|
|
this.birdIDs.push(createBlock.bind(this)(3));
|
|
this.birdIDs.push(createBlock.bind(this)(4));
|
|
this.birdIDs.push(createBlock.bind(this)(5));
|
|
this.positionWatcher = new PositionWatcher(this.birdIDs, boxSpawnPosition, -0.4, 4);
|
|
},
|
|
onMessage: function(channel, message, seneder) {
|
|
if (this.finished) {
|
|
return;
|
|
}
|
|
if (channel == "Entity-Exploded") {
|
|
debug("FarGrab | Got entity-exploded message: ", message);
|
|
var data = parseJSON(message);
|
|
if (this.birdIDs.indexOf(data.entityID) >= 0) {
|
|
debug("FarGrab | It's one of the firecrackers");
|
|
playFirecrackerSound(data.position);
|
|
playSuccessSound();
|
|
this.finished = true;
|
|
this.onFinish();
|
|
}
|
|
}
|
|
},
|
|
cleanup: function() {
|
|
debug("FarGrab | Cleanup");
|
|
this.finished = true;
|
|
setControllerPartLayer('tips', 'blank');
|
|
setControllerPartLayer('trigger', 'normal');
|
|
hideEntitiesWithTag(this.tag, { visible: false});
|
|
deleteEntitiesWithTag(this.tempTag);
|
|
if (this.positionWatcher) {
|
|
this.positionWatcher.destroy();
|
|
this.positionWatcher = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
function PositionWatcher(entityIDs, originalPosition, minY, maxDistance) {
|
|
debug("Creating position watcher");
|
|
this.watcherIntervalID = Script.setInterval(function() {
|
|
for (var i = 0; i < entityIDs.length; ++i) {
|
|
var entityID = entityIDs[i];
|
|
var props = Entities.getEntityProperties(entityID, ['position']);
|
|
if (props.position.y < minY || Vec3.distance(originalPosition, props.position) > maxDistance) {
|
|
Entities.editEntity(entityID, {
|
|
position: originalPosition,
|
|
velocity: { x: 0, y: -0.01, z: 0 },
|
|
angularVelocity: { x: 0, y: 0, z: 0 }
|
|
});
|
|
}
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
PositionWatcher.prototype = {
|
|
destroy: function() {
|
|
debug("Destroying position watcher");
|
|
Script.clearInterval(this.watcherIntervalID);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// STEP: Equip //
|
|
// //
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
var stepEquip = function(tutorialManager) {
|
|
const controllerName = tutorialManager.controllerName;
|
|
this.controllerName = controllerName;
|
|
|
|
this.name = 'equip';
|
|
|
|
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.PART2 = 1;
|
|
this.PART3 = 2;
|
|
this.COMPLETE = 3;
|
|
|
|
Messages.subscribe('Tutorial-Spinner');
|
|
Messages.messageReceived.connect(this.onMessage.bind(this));
|
|
}
|
|
stepEquip.prototype = {
|
|
start: function(onFinish) {
|
|
if (this.controllerName === CONTROLLER_TOUCH) {
|
|
setControllerPartLayer('tips', 'grip');
|
|
setControllerPartLayer('grip', 'highlight');
|
|
} else {
|
|
setControllerPartLayer('tips', 'trigger');
|
|
setControllerPartLayer('trigger', 'highlight');
|
|
}
|
|
|
|
Messages.sendLocalMessage('Hifi-Grab-Disable', JSON.stringify({
|
|
holdEnabled: true,
|
|
}));
|
|
|
|
var tag = this.tag;
|
|
|
|
// Spawn content set
|
|
showEntitiesWithTags(this.tags);
|
|
showEntitiesWithTags(this.tagsPart1);
|
|
|
|
this.currentPart = this.PART1;
|
|
|
|
function createLighter() {
|
|
var transform = {};
|
|
|
|
var boxSpawnProps = getEntityWithName(GUN_SPAWN_NAME);
|
|
transform.position = boxSpawnProps.position;
|
|
transform.rotation = boxSpawnProps.rotation;
|
|
transform.velocity = { x: 0, y: -0.01, z: 0 };
|
|
transform.angularVelocity = { x: 0, y: 0, z: 0 };
|
|
this.spawnTransform = transform;
|
|
return doCreateButaneLighter(transform).id;
|
|
}
|
|
|
|
|
|
this.lighterID = createLighter.bind(this)();
|
|
this.startWatchingLighter();
|
|
debug("Created lighter", this.lighterID);
|
|
this.onFinish = onFinish;
|
|
},
|
|
startWatchingLighter: function() {
|
|
if (!this.watcherIntervalID) {
|
|
debug("Starting to watch lighter position");
|
|
this.watcherIntervalID = Script.setInterval(function() {
|
|
debug("Checking lighter position");
|
|
var props = Entities.getEntityProperties(this.lighterID, ['position']);
|
|
if (props.position.y < -0.4
|
|
|| Vec3.distance(this.spawnTransform.position, props.position) > 4) {
|
|
debug("Moving lighter back to table");
|
|
Entities.editEntity(this.lighterID, this.spawnTransform);
|
|
}
|
|
}.bind(this), 1000);
|
|
}
|
|
},
|
|
stopWatchingGun: function() {
|
|
if (this.watcherIntervalID) {
|
|
debug("Stopping watch of lighter position");
|
|
Script.clearInterval(this.watcherIntervalID);
|
|
this.watcherIntervalID = null;
|
|
}
|
|
},
|
|
onMessage: function(channel, message, sender) {
|
|
if (this.currentPart == this.COMPLETE) {
|
|
return;
|
|
}
|
|
|
|
debug("Equip | Got message", channel, message, sender, MyAvatar.sessionUUID);
|
|
|
|
if (channel == "Tutorial-Spinner") {
|
|
if (this.currentPart == this.PART1 && message == "wasLit") {
|
|
this.currentPart = this.PART2;
|
|
debug("Equip | Starting part 2");
|
|
Script.setTimeout(function() {
|
|
debug("Equip | Starting part 3");
|
|
this.currentPart = this.PART3;
|
|
hideEntitiesWithTags(this.tagsPart1);
|
|
showEntitiesWithTags(this.tagsPart2);
|
|
setControllerPartLayer('trigger', 'normal');
|
|
setControllerPartLayer('grip', 'highlight');
|
|
setControllerPartLayer('tips', 'grip');
|
|
Messages.subscribe('Hifi-Object-Manipulation');
|
|
debug("Equip | Finished starting part 3");
|
|
}.bind(this), 9000);
|
|
}
|
|
} else if (channel == "Hifi-Object-Manipulation") {
|
|
if (this.currentPart == this.PART3) {
|
|
var data = parseJSON(message);
|
|
if (data.action == 'release' && data.grabbedEntity == this.lighterID) {
|
|
debug("Equip | Got release, finishing step");
|
|
this.stopWatchingGun();
|
|
this.currentPart = this.COMPLETE;
|
|
playSuccessSound();
|
|
Script.setTimeout(this.onFinish.bind(this), 1500);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
cleanup: function() {
|
|
debug("Equip | Got yaw action");
|
|
if (this.watcherIntervalID) {
|
|
Script.clearInterval(this.watcherIntervalID);
|
|
this.watcherIntervalID = null;
|
|
}
|
|
|
|
setControllerPartLayer('tips', 'blank');
|
|
setControllerPartLayer('grip', 'normal');
|
|
setControllerPartLayer('trigger', 'normal');
|
|
this.stopWatchingGun();
|
|
this.currentPart = this.COMPLETE;
|
|
|
|
if (this.checkCollidesTimer) {
|
|
Script.clearInterval(this.checkCollidesTimer);
|
|
this.checkColllidesTimer = null;
|
|
}
|
|
|
|
hideEntitiesWithTags(this.tagsPart1);
|
|
hideEntitiesWithTags(this.tagsPart2);
|
|
hideEntitiesWithTags(this.tags);
|
|
deleteEntitiesWithTag(this.tempTag);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// STEP: Turn Around //
|
|
// //
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
var stepTurnAround = function(tutorialManager) {
|
|
this.name = 'turnAround';
|
|
|
|
this.tags = ["turnAround", "turnAround-" + tutorialManager.controllerName];
|
|
this.tempTag = "turnAround-temporary";
|
|
|
|
this.onActionBound = this.onAction.bind(this);
|
|
this.numTimesSnapTurnPressed = 0;
|
|
this.numTimesSmoothTurnPressed = 0;
|
|
}
|
|
stepTurnAround.prototype = {
|
|
start: function(onFinish) {
|
|
setControllerPartLayer('joystick', 'highlight');
|
|
setControllerPartLayer('touchpad', 'arrows');
|
|
setControllerPartLayer('tips', 'arrows');
|
|
|
|
showEntitiesWithTags(this.tags);
|
|
|
|
this.numTimesSnapTurnPressed = 0;
|
|
this.numTimesSmoothTurnPressed = 0;
|
|
this.smoothTurnDown = false;
|
|
Controller.actionEvent.connect(this.onActionBound);
|
|
|
|
this.interval = Script.setInterval(function() {
|
|
debug("TurnAround | Checking if finished",
|
|
this.numTimesSnapTurnPressed, this.numTimesSmoothTurnPressed);
|
|
var FORWARD_THRESHOLD = 90;
|
|
var REQ_NUM_TIMES_SNAP_TURN_PRESSED = 3;
|
|
var REQ_NUM_TIMES_SMOOTH_TURN_PRESSED = 2;
|
|
|
|
var dir = Quat.getFront(MyAvatar.orientation);
|
|
var angle = Math.atan2(dir.z, dir.x);
|
|
var angleDegrees = ((angle / Math.PI) * 180);
|
|
|
|
var hasTurnedEnough = this.numTimesSnapTurnPressed >= REQ_NUM_TIMES_SNAP_TURN_PRESSED
|
|
|| this.numTimesSmoothTurnPressed >= REQ_NUM_TIMES_SMOOTH_TURN_PRESSED;
|
|
var facingForward = Math.abs(angleDegrees) < FORWARD_THRESHOLD
|
|
if (hasTurnedEnough && facingForward) {
|
|
Script.clearInterval(this.interval);
|
|
this.interval = null;
|
|
playSuccessSound();
|
|
onFinish();
|
|
}
|
|
}.bind(this), 100);
|
|
},
|
|
onAction: function(action, value) {
|
|
var STEP_YAW_ACTION = 6;
|
|
var SMOOTH_YAW_ACTION = 4;
|
|
|
|
if (action == STEP_YAW_ACTION && value != 0) {
|
|
debug("TurnAround | Got step yaw action");
|
|
++this.numTimesSnapTurnPressed;
|
|
} else if (action == SMOOTH_YAW_ACTION) {
|
|
debug("TurnAround | Got smooth yaw action");
|
|
if (this.smoothTurnDown && value === 0) {
|
|
this.smoothTurnDown = false;
|
|
++this.numTimesSmoothTurnPressed;
|
|
} else if (!this.smoothTurnDown && value !== 0) {
|
|
this.smoothTurnDown = true;
|
|
}
|
|
}
|
|
},
|
|
cleanup: function() {
|
|
debug("TurnAround | Cleanup");
|
|
try {
|
|
Controller.actionEvent.disconnect(this.onActionBound);
|
|
} catch (e) {
|
|
}
|
|
|
|
setControllerPartLayer('joystick', 'normal');
|
|
setControllerPartLayer('touchpad', 'blank');
|
|
setControllerPartLayer('tips', 'blank');
|
|
|
|
if (this.interval) {
|
|
Script.clearInterval(this.interval);
|
|
}
|
|
hideEntitiesWithTags(this.tags);
|
|
deleteEntitiesWithTag(this.tempTag);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// STEP: Teleport //
|
|
// //
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
var stepTeleport = function(tutorialManager) {
|
|
this.name = 'teleport';
|
|
|
|
this.tags = ["teleport", "teleport-" + tutorialManager.controllerName];
|
|
this.tempTag = "teleport-temporary";
|
|
}
|
|
stepTeleport.prototype = {
|
|
start: function(onFinish) {
|
|
setControllerPartLayer('button_a', 'highlight');
|
|
setControllerPartLayer('touchpad', 'teleport');
|
|
setControllerPartLayer('tips', 'teleport');
|
|
|
|
Messages.sendLocalMessage('Hifi-Teleport-Disabler', 'none');
|
|
|
|
// Wait until touching teleport pad...
|
|
var padProps = getEntityWithName(TELEPORT_PAD_NAME);
|
|
var xMin = padProps.position.x - padProps.dimensions.x / 2;
|
|
var xMax = padProps.position.x + padProps.dimensions.x / 2;
|
|
var zMin = padProps.position.z - padProps.dimensions.z / 2;
|
|
var zMax = padProps.position.z + padProps.dimensions.z / 2;
|
|
function checkCollides() {
|
|
debug("Teleport | Checking if on pad...");
|
|
|
|
var pos = MyAvatar.position;
|
|
|
|
debug('Teleport | x', pos.x, xMin, xMax);
|
|
debug('Teleport | z', pos.z, zMin, zMax);
|
|
|
|
if (pos.x > xMin && pos.x < xMax && pos.z > zMin && pos.z < zMax) {
|
|
debug("Teleport | On teleport pad");
|
|
Script.clearInterval(this.checkCollidesTimer);
|
|
this.checkCollidesTimer = null;
|
|
playSuccessSound();
|
|
onFinish();
|
|
}
|
|
}
|
|
this.checkCollidesTimer = Script.setInterval(checkCollides.bind(this), 500);
|
|
|
|
showEntitiesWithTags(this.tags);
|
|
},
|
|
cleanup: function() {
|
|
debug("Teleport | Cleanup");
|
|
setControllerPartLayer('button_a', 'normal');
|
|
setControllerPartLayer('touchpad', 'blank');
|
|
setControllerPartLayer('tips', 'blank');
|
|
|
|
if (this.checkCollidesTimer) {
|
|
Script.clearInterval(this.checkCollidesTimer);
|
|
}
|
|
hideEntitiesWithTags(this.tags);
|
|
deleteEntitiesWithTag(this.tempTag);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// STEP: Finish //
|
|
// //
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
var stepFinish = function() {
|
|
this.name = 'finish';
|
|
|
|
this.tag = "finish";
|
|
this.tempTag = "finish-temporary";
|
|
}
|
|
stepFinish.prototype = {
|
|
start: function(onFinish) {
|
|
editEntitiesWithTag('door', { visible: false, collisonless: true });
|
|
showEntitiesWithTag(this.tag);
|
|
Settings.setValue("tutorialComplete", true);
|
|
onFinish();
|
|
},
|
|
cleanup: function() {
|
|
}
|
|
};
|
|
|
|
var stepCleanupFinish = function() {
|
|
this.name = 'cleanup';
|
|
|
|
this.shouldLog = false;
|
|
}
|
|
stepCleanupFinish.prototype = {
|
|
start: function(onFinish) {
|
|
hideEntitiesWithTag('finish');
|
|
onFinish();
|
|
},
|
|
cleanup: function() {
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
TutorialManager = function() {
|
|
var STEPS;
|
|
|
|
var currentStepNum = -1;
|
|
var currentStep = null;
|
|
var startedTutorialAt = 0;
|
|
var startedLastStepAt = 0;
|
|
var didFinishTutorial = false;
|
|
|
|
var wentToEntryStepNum;
|
|
var VERSION = 2;
|
|
var tutorialID;
|
|
|
|
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() {
|
|
currentStepNum = -1;
|
|
currentStep = null;
|
|
startedTutorialAt = Date.now();
|
|
|
|
// Old versions of interface do not have the Script.generateUUID function.
|
|
// If Script.generateUUID is not available, default to an empty string.
|
|
tutorialID = Script.generateUUID ? Script.generateUUID() : "";
|
|
STEPS = [
|
|
new stepStart(this),
|
|
new stepOrient(this),
|
|
new stepFarGrab(this),
|
|
new stepNearGrab(this),
|
|
new stepEquip(this),
|
|
new stepTurnAround(this),
|
|
new stepTeleport(this),
|
|
new stepFinish(this),
|
|
new stepEnableControllers(this),
|
|
];
|
|
wentToEntryStepNum = STEPS.length;
|
|
for (var i = 0; i < STEPS.length; ++i) {
|
|
STEPS[i].cleanup();
|
|
}
|
|
MyAvatar.shouldRenderLocally = false;
|
|
this.startNextStep();
|
|
}
|
|
|
|
this.onFinish = function() {
|
|
debug("onFinish", currentStepNum);
|
|
if (currentStep && currentStep.shouldLog !== false) {
|
|
self.trackStep(currentStep.name, currentStepNum);
|
|
}
|
|
|
|
self.startNextStep();
|
|
}
|
|
|
|
this.startNextStep = function() {
|
|
if (currentStep) {
|
|
currentStep.cleanup();
|
|
}
|
|
|
|
++currentStepNum;
|
|
|
|
// This always needs to be set because we use this value when
|
|
// tracking that the user has gone through the entry portal. When the
|
|
// tutorial finishes, there is a last "pseudo" step that the user
|
|
// finishes when stepping into the portal.
|
|
startedLastStepAt = Date.now();
|
|
|
|
if (currentStepNum >= STEPS.length) {
|
|
// Done
|
|
info("DONE WITH TUTORIAL");
|
|
currentStepNum = -1;
|
|
currentStep = null;
|
|
didFinishTutorial = true;
|
|
return false;
|
|
} else {
|
|
info("Starting step", currentStepNum);
|
|
currentStep = STEPS[currentStepNum];
|
|
currentStep.start(this.onFinish);
|
|
return true;
|
|
}
|
|
}.bind(this);
|
|
|
|
this.restartStep = function() {
|
|
if (currentStep) {
|
|
currentStep.cleanup();
|
|
currentStep.start(this.onFinish);
|
|
}
|
|
}
|
|
|
|
this.stopTutorial = function() {
|
|
if (currentStep) {
|
|
currentStep.cleanup();
|
|
HMD.requestHideHandControllers();
|
|
}
|
|
reenableEverything();
|
|
currentStepNum = -1;
|
|
currentStep = null;
|
|
}
|
|
|
|
this.trackStep = function(name, stepNum) {
|
|
var timeToFinishStep = (Date.now() - startedLastStepAt) / 1000;
|
|
var tutorialTimeElapsed = (Date.now() - startedTutorialAt) / 1000;
|
|
UserActivityLogger.tutorialProgress(
|
|
name, stepNum, timeToFinishStep, tutorialTimeElapsed,
|
|
tutorialID, VERSION, this.realControllerName);
|
|
}
|
|
|
|
// This is a message sent from the "entry" portal in the courtyard,
|
|
// after the tutorial has finished.
|
|
this.enteredEntryPortal = function() {
|
|
info("Got enteredEntryPortal");
|
|
if (didFinishTutorial) {
|
|
info("Tracking wentToEntry");
|
|
this.trackStep("wentToEntry", wentToEntryStepNum);
|
|
}
|
|
}
|
|
}
|
|
|
|
// To run the tutorial:
|
|
//
|
|
//var tutorialManager = new TutorialManager();
|
|
//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);
|