/*
* Copyright (c) 2013, Octagon Software LLC
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
// Browser detect:
if (/MSIE (\d+\.\d+);/.test(navigator.userAgent)) {
var ieversion = new Number(RegExp.$1);
if (ieversion <= 8) {
console.log("Sorry, this page requires Internet Explorer 9+, Chrome, or Firefox.");
}
} else {
/**
* @global
* @class PodJS
* @classdesc Main environment for <a href="http://podjs.com">pod.js</a>.
* <p>
* The class has a static registry of pod classes which have been loaded. Note that only one version of each Pod class can be
* registered globally per page. Pod classes get registered by their corresponding scripts, loaded by the html page.
* <p>
* Multiple PodJS environments can be created on the same page via instances of this class. Each instance has its own timer
* and its own instances of the registered pods, created lazily in dependency order when requested by the application.
* <p>
* The optional options object provided contains both environment settings and settings that are applicable to specific
* pods. Pods will consume only the options they care about.
* <p>
* Author: markroth8
*
* @constructor
* @desc Constructs a new PodJS environment with the given options.
* @param {object} [options] An Object containing options for the environment. Parameters:
* <table>
* <tr>
* <th>Property</th>
* <th>Type</th>
* <th>Argument</th>
* <th>Default</th>
* <th>Description</th>
* </tr>
* <tr>
* <td>fps</td>
* <td>int</td>
* <td><optional></td>
* <td>30</td>
* <td>Frames per second the ticks should ideally run.</td>
* </tr>
* </table>
*/
PodJS = function(options) {
var podJSThis = this;
/**
* Options provided when this environment was created.
*
* @private
* @var {object} PodJS._options
*/
var _options;
/**
* Pod instances currently registered to this Environment.
* Key = pod name, value = pod instance.
*
* @private
* @var {object} PodJS._pods
*/
var _pods = {};
/**
* @method getOptions
* @memberof PodJS
* @instance
* @returns {PodJS.Options} A clone of the options passed to this environment.
*/
this.getOptions = function() {
var result = {};
for (var property in _options) {
if (_options.hasOwnProperty(property)) {
result[property] = _options[property];
}
}
return result;
};
/**
* Retrieves the instance of the pod with the given name that is bound to this environment. If there is no such pod
* registered, the PodJS environment will instantiate one. Note this might also cause a cascade of other pods to be
* instantiated and registered.
* <p>
* Only pod classes that have been registered can be instantiated. This is normally accomplished by adding script tags
* to the html file.
*
* @method pod
* @memberof PodJS
* @instance
* @param name {string} The name and major version of the pod to retrieve (e.g. "scratch").
* @returns {Pod} The instance of the pod with the given name that is registered with this environment.
*/
this.pod = function(name) {
if (!_pods.hasOwnProperty(name)) {
if (!PodJS.POD_CLASSES.hasOwnProperty(name)) {
throw "No pod registered with name '" + name + "'. Use a <script> tag to register the pod class.";
}
var podClass = PodJS.POD_CLASSES[name];
var initParams = new PodJS.PodInitParams();
initParams.env = this;
for (var name in _options) {
if (_options.hasOwnProperty(name)) {
initParams[name] = _options[name];
}
}
var podInstance = new podClass.constructor(initParams);
_pods[name] = podInstance;
}
return _pods[name];
};
/**
* Create a new ScriptBuilder which will build a script attached to the provided resource.
* <p>
* The ScriptBuilder will be bound to this environment and so the available blocks will come from all pods registered in
* this environment.
*
* @instance
* @method newScriptBuilder
* @param {PodJS.Script} blockScript The script to add blocks to (should already be created and bound to its environment and
* resource).
* @return {PodJS.ScriptBuilder} The ScriptBuilder that will build the script.
* @memberof PodJS.Pod
*/
this.newScriptBuilder = function(blockScript) {
/**
* @class PodJS.ScriptBuilder
* @classdesc Factory class that assembles blocks to form new {@link PodJS.Script}s attached to {@link PodJS.Pod#Resource}s.
* <p>
* Instances of this class can only be created by the environment itself.
* <p>
* The ScriptBuilder, when created, will automatically be populated with instance members that have the same name
* as the blocks registered by all pods in the environment. Those instance members will each return a function that,
* when called, will add a new block of that type to the script and then return the ScriptBuilder so the next block
* in the sequence can be easily added.
*/
var ScriptBuilder = function() {
var scriptBuilderThis = this;
/**
* The script this ScriptBuilder is creating
*
* @instance
* @member blockScript
* @type {PodJS.Script}
* @memberof {PodJS.ScriptBuilder}
*/
this.blockScript = blockScript;
/**
* Add a constant block to the script. A constant block is a special-purpose block that takes a parameter and
* always uses that parameter. Note that, as the name implies, the value of this block is evaluated at script
* creation time but is not re-evaluated in the future.
*
* @instance
* @method c
* @param {number|string} value The value to specify as a constant.
* @return {PodJS.ScriptBuilder} The builder, so additional blocks can be chained.
* @memberof {PodJS.ScriptBuilder}
*/
this.c = function(value) {
if (typeof(value) !== "number" && typeof(value) !== "string") {
throw new Error("For block 'c', parameter must be a number or string.");
}
var blockContext = {
environment : blockScript.context.environment,
pod : null, // core block is not associated with any pod.
resource : blockScript.context.resource,
blockScript : blockScript,
block : null
};
var constantBlock = new PodJS.ConstantBlock(blockContext, value);
blockContext.block = constantBlock;
blockScript.addBlock(constantBlock);
return scriptBuilderThis;
};
/**
* Add a JavaScript function block to the script. A JavaScript function block is a special-purpose block that takes
* a function parameter and evaluates the function each time the block is executed.
*
* @instance
* @method f
* @param {function} fn The function to evaluate when the block is evaluated.
* @return {PodJS.ScriptBuilder} The builder, so additional blocks can be chained.
* @memberof {PodJS.ScriptBuilder}
*/
this.f = function(fn) {
if (typeof(fn) !== "function") {
throw new Error("For block 'f', parameter must be a function.");
}
var blockContext = {
environment : blockScript.context.environment,
pod : null, // core block is not associated with any pod.
resource : blockScript.context.resource,
blockScript : blockScript,
block : null
};
var functionBlock = new PodJS.FunctionBlock(blockContext, fn);
blockContext.block = functionBlock;
blockScript.addBlock(functionBlock);
return scriptBuilderThis;
};
var construct = function(blockScript) {
for (var podName in _pods) {
if (_pods.hasOwnProperty(podName)) {
var pod = _pods[podName];
var blockTypes = pod.getBlockTypes();
for (var i = 0; i < blockTypes.length; i++) {
var blockInfo = blockTypes[i];
if (!blockInfo.hasOwnProperty("compatibleWith") ||
blockInfo.compatibleWith(blockScript.context.resource))
{
var addForBlockType = function(pod, blockInfo) {
var blockType = blockInfo.blockType;
Object.defineProperty(scriptBuilderThis, blockType, { get : function() {
// builder pattern - return instance of the ScriptBuilder.
// but as a side-effect, add the block to the script
var block = pod.newBlock(blockType, blockScript.context.resource, blockScript);
blockScript.addBlock(block);
return scriptBuilderThis;
} });
};
addForBlockType(pod, blockInfo);
}
}
}
}
};
construct(blockScript);
};
return new ScriptBuilder();
};
/**
* Resets all scripts in all pods back to the first instruction.
*
* @method resetAllScripts
* @memberof PodJS
* @instance
*/
this.resetAllScripts = function() {
for (var name in _pods) {
if (_pods.hasOwnProperty(name)) {
var pod = _pods[name];
var resources = pod.getAllResources();
for (var resType in resources) {
if (resources.hasOwnProperty(resType)) {
var resourceByType = resources[resType];
for (var resName in resourceByType) {
if (resourceByType.hasOwnProperty(resName)) {
var resource = resourceByType[resName];
var scripts = resource.scripts;
for (var i = 0; i < scripts.length; i++) {
var script = scripts[i];
script.reset();
}
}
}
}
}
}
}
};
/**
* Called at approximately 30 frames per second to animate all resources.
* <p>
* This calls tick on each registered pod, resource and script.
*
* @method tick
* @memberof PodJS
* @instance
*/
this.tick = function() {
for (var name in _pods) {
if (_pods.hasOwnProperty(name)) {
var pod = _pods[name];
// first, tick all resources in that pod
var resources = pod.getAllResources();
for (var resType in resources) {
if (resources.hasOwnProperty(resType)) {
var resourceByType = resources[resType];
for (var resName in resourceByType) {
if (resourceByType.hasOwnProperty(resName)) {
var resource = resourceByType[resName];
// first, tick all scripts attached to that resource
var scripts = resource.scripts;
for (var i = 0; i < scripts.length; i++) {
var script = scripts[i];
script.tick();
}
// next, tick the resource itself.
resource.tick();
}
}
}
}
// next, tick the pod itself
pod.tick();
}
}
};
// Constructor
var construct = function(options) {
_options = options;
// Always load core pod
podJSThis.pod("core");
// Start the timer right away.
setInterval(podJSThis.tick, 1000 / 30);
};
construct(options);
};
/**
* @class PodJS.PodInitParams
* @classdesc Initialization parameters for pods. An instance of this object is provided to the pod at construction time.
* @property {PodJS} env Environment this pod should be bound to.
*/
PodJS.PodInitParams = function() {
return this;
};
PodJS.PodInitParams.prototype = {
env : null
};
/**
* Object providing a map of all pod class names to the corresponding pod class.
* New pod classes are registered via {@link PodJS.REGISTER_POD_CLASS}.
*
* @member {object} PodJS.POD_CLASSES
*/
PodJS.POD_CLASSES = {};
/**
* Called by Pods to reigster with the environment.
* <p>
* This should never be called by an application. Pods call this method when the browser has loaded their class.
*
* @function PodJS.REGISTER_POD_CLASS
* @param podName The universally-unique name (across all Pods anyone has ever written) of the pod to register. Use podjs.com to
* register your name.
* @param podClass The pointer to the constructor function of the class that is used to create a new pod.
*/
PodJS.REGISTER_POD_CLASS = function(podName, podClass) {
if (PodJS.POD_CLASSES.hasOwnProperty(podName)) {
throw "Invalid attempt to register pod '" + podName + "' more than once.";
}
PodJS.POD_CLASSES[podName] = podClass;
};
/////////////////////////////////////////////////////////////////////
// PodJS.ScriptContext
/**
* @static
* @class PodJS.ScriptContext
* @classdesc Contains a reference to the environment and resource the script is bound to.
*
* @constructor
* @desc Constructs a new ScriptContext
* @param {PodJS} environment The environment the script is bound to
* @param {PodJS.Pod#Resource} resource The resource the script is bound to
*/
PodJS.ScriptContext = function(environment, resource) {
/**
* The environment the script is bound to
*
* @instance
* @member environment
* @type {PodJS}
* @memberof PodJS.ScriptContext
*/
this.environment = environment;
/**
* The resource the script is bound to
*
* @instance
* @member resource
* @type {PodJS.Pod#Resource}
* @memberof PodJS.ScriptContext
*/
this.resource = resource;
};
/////////////////////////////////////////////////////////////////////
// PodJS.Script
/**
* @static
* @class PodJS.Script
* @classdesc Executable sequence of blocks that is associated with a resource.
*
* @constructor
* @desc Constructs a new Script bound to the environment and resource provided in the given context object.
* @param {PodJS.ScriptContext} context containing the environment and resource this script is bound to.
*/
PodJS.Script = function(context) {
/**
* The context, containing a reference to the environment and resource the script is bound to.
*
* @instance
* @member context
* @type {PodJS.ScriptContext}
* @memberof PodJS.Script
*/
this.context = context;
/**
* The instruction pointer to which block number is next to be executed.
*
* @instance
* @member index
* @type {number}
* @memberof PodJS.Script
*/
this.index = 0;
/**
* Stack of instruction pointers for begin / end pairs.
*
* @instance
* @member ipStack
* @type {number}
* @memberof PodJS.Script
*/
this.ipStack = [];
/**
* Mechanism that allows a block to signal that the script is ready to yield because enough was done for this tick.
* <p>
* Before each script tick, yield is set to false and blocks are executed until one block sets yield to true. At that point,
* the next instruction will be executed in the next cycle.
*
* @instance
* @member yield
* @type {boolean}
* @memberof PodJS.Script
*/
this.yield = false;
/**
* If true, this script has an error, so it is disabled.
*
* @instance
* @member scriptHasError
* @type {boolean}
* @memberof PodJS.Script
*/
this.scriptHasError = false;
/**
* The sequence of blocks present in this script.
*
* @instance
* @private
* @type {PodJS.Resource#Block[]}
* @memberof PodJS.Script
*/
var _blocks = [];
/**
* Adds the given block to the end of the script.
*
* @instance
* @method addBlock
* @memberof PodJS.Script
* @param {PodJS.Pod#Block} block The instance of the block to add.
*/
this.addBlock = function(block) {
_blocks.push(block);
};
/**
* Returns a clone of the sequence of blocks present in this script.
* <p>
* A clone is returned so the caller cannot accidentally mutate the script without the script being aware.
*
* @instance
* @method getBlocks
* @memberof PodJS.Script
* @return {PodJS.Resource#Block[]} A clone of the sequence of blocks
*/
this.getBlocks = function() {
var result = [];
for (var i = 0; i < _blocks.length; i++) {
result.push(_blocks[i]);
}
return result;
};
/**
* Advances the instruction pointer to the next block
*
* @instance
* @method nextBlock
* @memberof PodJS.Script
*/
this.nextBlock = function() {
this.index++;
};
/**
* Peeks at the block at the current IP but does not advance the iP.
*
* @instance
* @method peekBlock
* @return The block at the current IP or null if no block there.
* @memberof PodJS.Script
*/
this.peekBlock = function() {
var result = null;
if (this.index < _blocks.length) {
result = _blocks[this.index];
}
return result;
};
/**
* Pushes the current instruction pointer to the stack (usually done just before a loop).
*
* @instance
* @method pushIP
* @memberof PodJS.Script
*/
this.pushIP = function() {
this.ipStack.push(this.index);
};
/**
* Pops the current instruction pointer from the stack (usually done at the end of a loop).
*
* @instance
* @method popIP
* @memberof PodJS.Script
*/
this.popIP = function() {
this.index = this.ipStack.pop();
};
// TODO: validate method
/**
* Advance the IP and read the next block as an argument. Note the block might be composite, causing additional blocks to
* be read (e.g. in the case of something like add.c(1).c(2)). This method is typically called by a block implementation.
*
* @instance
* @method nextArgument
* @return {string|number} A constant value to be interpreted by the block
* @memberof PodJS.Script
*/
this.nextArgument = function() {
this.index++;
if (this.index < _blocks.length) {
var block = _blocks[this.index];
if (!block.blockInfo.returnsValue) {
throw new Error("Expecting a block that returns a value but got '" + block.blockType + "'");
}
return block.tick();
}
};
/**
* Start at the begin block and advance the IP past the next matching end block, skipping all blocks in-between.
*
* @instance
* @method skipBeginEndBlock
* @memberof PodJS.Script
*/
this.skipBeginEndBlock = function() {
if (this.index < _blocks.length) {
var block = _blocks[this.index];
if (block.blockType !== "begin") {
throw new Error("Expecting a 'begin' block but got '" + block.blockType + "'");
}
this.index++;
var foundEnd = false;
var beginCount = 0;
while (this.index < _blocks.length) {
block = _blocks[this.index];
this.index++;
if (block.blockType === "begin") {
beginCount++;
} else if (block.blockType === "end") {
if (beginCount > 0) {
beginCount--;
} else {
foundEnd = true;
break;
// IP will already have been incremented.
}
}
}
if (!foundEnd) {
throw new Error("Passed the end of the script looking for matching end block");
}
}
};
/**
* Reset this script to the first instruction.
*
* @instance
* @method reset
* @memberof PodJS.Script
*/
this.reset = function() {
this.index = 0;
this.ipStack = [];
this.yield = false;
// Reset all blocks in the script so they can wipe out their state
for (var i = 0; i < _blocks.length; i++) {
var block = _blocks[i];
block.reset();
}
};
/**
* Gets called by the environment when this script is active.
* <p>
* Pods are not responsible for calling tick() on their own scripts. That is taken care of by the environment.
*
* @method tick
* @memberOf PodJS.Script
* @instance
*/
this.tick = function() {
try {
if (this.scriptHasError) {
return;
}
this.yield = false;
// check that, if this is the first statement, it has to be an event block.
var block = _blocks[this.index];
var eventBlock = block.blockInfo.hasOwnProperty("eventBlock") && block.blockInfo.eventBlock;
if (this.index === 0 && !eventBlock) {
if (!this.hasOwnProperty("warnScriptDoesNotStartWithEventBlock")) {
console.log("Warning: Script does not start with event block, so skipping.");
this.warnScriptDoesNotStartWithEventBlock = true;
}
return;
}
// always run event block at start of script in case the event becomes true again.
var prevIndex = this.index;
this.index = 0;
block = _blocks[this.index];
block.tick();
if (this.index === 0) {
// event is not true. continue.
this.index = prevIndex;
this.yield = false;
} else {
// event was true - reset script and continue.
var prevIndex = this.index;
this.reset();
this.index = prevIndex;
}
// Keep executing statements until yield is true or we hit the end of the script
var blocksRemainingBeforeForcedYield = 32;
while (!this.yield && this.index < _blocks.length && blocksRemainingBeforeForcedYield > 0) {
block = _blocks[this.index];
block.tick();
blocksRemainingBeforeForcedYield--;
}
// If we're at the end, reset
if (this.index === _blocks.length) {
this.reset();
}
} catch (e) {
this.scriptHasError = true;
console.log("Script has an error. Disabling.");
this.reset();
throw e;
}
};
};
/////////////////////////////////////////////////////////////////////
// PodJS.Pod
/**
* @static
* @class PodJS.Pod
* @classdesc Abstract Base class for a convenience library containing a set of blocks and resources, providing new
* capabilities to an application.
* <p>
* Makes it convenient to import a set of blocks together and associate them with resources, and provides useful
* utility methods.
* <p>
* To create a new Pod, create a new JavaScript file that contains a concrete class that extends this class and registers
* the class via {@link PodJS.REGISTER_POD_CLASS}. Applications will need to include your JavaScript file.
*
* @constructor
* @desc Constructs a new Pod with the given initialization parameters.
* @param {PodJS.PodInitParams} initParams Initialization parameters provided by the environment.
*
* @author markroth8
*/
PodJS.Pod = function(initParams) {
var _pod = this;
/**
* Maps resource types to maps of resource names to resource instances.
*
* @private
* @type {object}
*/
var _resourceRegistry = {};
/**
* The environment this pod is bound to.
*
* @private
* @type {PodJS}
*/
var _environment = initParams.env;
/**
* Deletes the resource with the given name and deregisters it from the pod.
*
* @method deleteResourceByName
* @memberof PodJS.Pod
* @instance
* @param {string} resourceName The name of the resource to delete
* @throws {Error} If a resource with the given name could not be found
*/
this.deleteResourceByName = function(resourceName) {
var success = false;
var resource = null;
for (var type in _resourceRegistry) {
if (_resourceRegistry.hasOwnProperty(type)) {
var resources = _resourceRegistry[type];
if (resources.hasOwnProperty(resourceName)) {
resource = resources[resourceName];
delete resources[resourceName];
resource.release();
success = true;
break;
}
}
}
if (!success) {
throw new Error("Could not find resource with the name '" + resourceName + "'");
}
};
/**
* Returns a list of block types that this Pod provides.
* <p>
* Note that block type names must be globally unique across all pods. Pod authors should use the website
* {@link http://podjs.com/} to register block type names so there are no conflicts.
*
* @abstract
* @method getBlockTypes
* @memberof PodJS.Pod
* @instance
* @returns {PodJS.BlockInfo[]} An array of supported block types.
*/
this.getBlockTypes = function() {
throw new Error('Method is abstract and must be overridden by a subclass.');
};
/**
* Returns the resource with the given name or null if no such resource was found.
*
* @method getResourceByName
* @memberof PodJS.Pod
* @instance
* @param {string} resourceName The name of the resource being queried.
* @returns {PodJS.Pod#Resource} The resource with the given name or null if no such resource was found.
*/
this.getResourceByName = function(resourceName) {
var result = null;
for (var type in _resourceRegistry) {
if (_resourceRegistry.hasOwnProperty(type)) {
var resources = _resourceRegistry[type];
if (resources.hasOwnProperty(resourceName)) {
result = resources[resourceName];
break;
}
}
}
return result;
};
/**
* Returns a list of resources of the given type that have been created.
*
* @method getResourcesByType
* @memberof PodJS.Pod
* @instance
* @param {string} resourceType The type of resource being queried.
* @returns {object} An object mapping resource name to resource instance.
*/
this.getResourcesByType = function(resourceType) {
var result = {};
if (this.getResourceTypeNames().indexOf(resourceType) === -1) {
throw new Error("Pod does not support resource type '" + resourceType + "'.");
}
if (_resourceRegistry.hasOwnProperty(resourceType)) {
result = _resourceRegistry[resourceType];
}
return result;
};
/**
* Returns an object containing a list of all registered resources, keyed by type.
* <p>
* This method is the most efficient way to enumerate all resources.
*
* @method getAllResources
* @memberof PodJS.Pod
* @instance
* @returns {object} Map of resource type to all resources registered to this pod of that type.
*/
this.getAllResources = function() {
return _resourceRegistry;
};
/**
* Returns a list of information about resource types that this Pod provides.
* <p>
* Note that resource type names must be globally unique across all pods. Pod authors should use the website
* {@link http://podjs.com/} to register resource type names so there are no conflicts.
*
* @abstract
* @method getResourceTypes
* @memberof PodJS.Pod
* @instance
* @returns {PodJS.ResourceInfo[]} An array of supported resource types.
*/
this.getResourceTypes = function() {
throw new Error('Method is abstract and must be overridden by a subclass.');
};
/**
* Returns a list of names of resource types supported by this pod.
* <p>
* This is a convenience method that effectively calls {@link PodJS.Pod#getResourceTypes} and then returns an array of all
* resourceType property values of each info object.
*
* @abstract
* @method getResourceTypeNames
* @memberof PodJS.Pod
* @instance
* @returns {string[]} An array of supported resource type names
*/
this.getResourceTypeNames = function() {
var result = [];
var types = this.getResourceTypes();
for (var i = 0; i < types.length; i++) {
var info = types[i];
result.push(info.resourceType);
}
return result;
};
/**
* Called when a {@link PodJS.ScriptBuilder} wishes to create a new instance of a block.
* <p>
* Subclasses should call {@link PodJS.Pod#newBlockClass} to return the constructor for the super-object of the block.
* The super-object will be bound to the Pod base class. This pattern is mostly done to enforce the contract that
* no blocks can be created other than for the types specified in getBlockTypes(), and for any future bookkeeping needs.
*
* @abstract
* @method newBlock
* @memberof PodJS.Pod
* @instance
* @param {string} blockType The type of block to be created (e.g. "gotoXY"). Must be one of the block types
* returned by {@link PodJS.Pod#getBlockTypes}.
* @param {PodJS.Block#Resource} resource The resource this block is to be bound to.
* @param {PodJS.Script} script The script this block is to be bound to.
* @returns {PodJS.Pod#Block} The instance of the block.
* @throws {Error} If the block type provided was not one of the valid block types returned by {@link PodJS.Pod#getBlockTypes}.
* This check is performed by {@link PodJS.Pod#newBlockClass}.
* @throws {Error} If the block to be returned would not be compatible with the resource provided. This check
* must be performed by the subclass.
*/
this.newBlock = function(blockType, resource, script) {
throw new Error('Method is abstract and must be overridden by a subclass.');
};
/**
* Called by the concrete Pod class when it wishes to create a new Block, in order to provide the constructor of the
* super-class of the block object.
* <p>
* The constructor will be bound to the Pod base class so that it can update the resource registry during the lifecycle
* of the block.
*
* @method newBlockClass
* @memberof PodJS.Pod
* @instance
* @param {string} blockType The type of block to be created (e.g. "gotoXY"). Must be one of the block types
* returned by {@link PodJS.Pod#getBlockTypes}.
* @param {PodJS.Block#Resource} resource The resource this block is to be bound to.
* @param {PodJS.Script} blockScript The script this block is to be bound to.
* @returns {Function} The constructor of the Block class that the subclass should create a new instance of.
* @throws {Error} If the block type provided was not one of the valid block types returned by {@link PodJS.Pod#getBlockTypes}.
*/
this.newBlockClass = function(blockType, resource, blockScript) {
// Check that block type is one of the block types supported by this pod.
var found = null;
for (var i = 0; i < this.getBlockTypes().length; i++) {
var blockInfo = this.getBlockTypes()[i];
if (blockInfo.blockType === blockType) {
found = blockInfo;
break;
}
}
if (found === null) {
throw new Error("This pod does not know how to create blocks of type '" + blockType + "'");
}
// Check that the block is compatible with the resource
if (blockInfo.hasOwnProperty("compatibleWith") && !blockInfo.compatibleWith(resource)) {
throw new Error("Block '" + blockType + "' is not compatible with specified resource of type '" +
resource.resourceType + "'");
}
/**
* @class PodJS.Pod#BlockContext
* @classdesc Context provided to blocks that gives them access to the environment, pod and resource.
*/
var blockContext = {
/**
* The environment this block is bound to.
*
* @instance
* @member {PodJS} environment
* @memberOf PodJS.Pod#BlockContext
*/
environment : _environment,
/**
* The pod this block is bound to.
*
* @instance
* @member {PodJS.Pod} pod
* @memberOf PodJS.Pod#BlockContext
*/
pod : _pod,
/**
* The resource this block is bound to.
*
* @instance
* @member {PodJS.Pod#Resource} resource
* @memberOf PodJS.Pod#BlockContext
*/
resource : resource,
/**
* The script this block is bound to.
*
* @instance
* @member {PodJS.Script} blockScript
* @memberOf PodJS.Pod#BlockContext
*/
blockScript : blockScript,
/**
* The instance of the block.
*
* @instance
* @member {PodJS.Pod#Block} block
* @memberOf PodJS.Pod#BlockContext
*/
block : null
};
/**
* @class PodJS.Pod#Block
* @classdesc Abstract Base class for block implementations (provided by pods).
* <p>
* A block is an atomic unit of a script.
* <p>
* New types of blocks are added to the system via pods.
* <p>
* Blocks are attached to {@link PodJS.Pod#Resource}s and can manipulate them.
* <p>
* Blocks also have access to the script and to the environment so they can take global actions.
* <p>
* This is an inner class bound to the Pod superclass that has access to the internal resources of the pod.
* Pod sub-classes should ensure that all created resources extend from the resource
* superclass returned by {@link PodJS.Pod#newBlock}.
*/
var Block = {
/**
* The name of the type of block this is an instance of.
*
* @instance
* @member {string} blockType
* @memberOf PodJS.Pod#Block
*/
blockType : blockType,
/**
* Information about this block
*
* @instance
* @member {PodJS.BlockInfo} blockInfo
* @memberOf PodJS.Pod#Block
*/
blockInfo : blockInfo,
/**
* The BlockContext containing a reference to the resource to which it is bound and
* a reference to the script in which it is executing.
*
* @instance
* @member {PodJS.BlockContext} context
* @memberOf PodJS.Pod#Block
*/
context : blockContext,
/**
* Gets called by the environment when this block is active.
* <p>
* Pods are not responsible for calling tick() on their own blocks. That is taken care of by the environment.
* <p>
* The super-class version of tick() calls tick() on the blockInfo, if present. Subclasses can optionally override to
* perform additional actions on each environment tick.
* <p>
* Sub-classes should be sure to set context.script.yield to true if the resource has completed its tick.
* <p>
* Sub-classes are also responsible for advancing the instruction pointer to the next instruction.
* <p>
* If this is a reporter block, then tick will return a value.
*
* @method tick
* @memberOf PodJS.Pod#Block
* @instance
* @return {undefined|string|number} If this is a reporter block, returns the value, else returns undefined.
*/
tick : function() {
if (blockInfo.hasOwnProperty("tick")) {
return blockInfo.tick(this.context);
}
},
/**
* Gets called by the environment when a script is reset, giving the block an opportunity to clear its state.
* <p>
* The default behvaior calls reset() in the blockInfo for the block, if it is present.
*
* @method reset
* @memberOf PodJS.Pod#Block
* @instance
*/
reset : function() {
if (blockInfo.hasOwnProperty("reset") && (blockInfo.reset !== null)) {
blockInfo.reset(this.context);
}
},
/**
* Release the system resources associated with this block.
* <p>
* Sub-classes can optionally override this method, but must always call the super-class to ensure the proper
* bookkeeping takes place. Note that blocks will continue to receive ticks until deleted / released.
*
* @method release
* @memberOf PodJS.Pod#Block
* @instance
*/
release : function() {
}
};
blockContext.block = Block;
return Block;
};
/**
* Called when the environment or an application wishes to create a new resource of the given type.
* <p>
* Most Pods also provide convenience methods (e.g. newSprite(...) instead of newResource("sprite", ...) but
* this method is necessary for reflection-style access.
* <p>
* Subclasses should call {@link PodJS.Pod#newResourceClass} to return the constructor for the super-class of the resource.
* The constructor will be bound to the Pod base class so that it can update the resource registry during the lifecycle
* of the resource.
*
* @abstract
* @method newResource
* @memberof PodJS.Pod
* @instance
* @param {string} resourceType The type of resource to be created (e.g. "sprite"). Must be one of the resource types
* returned by {@link getResourceTypes}.
* @param {string} resourceName The name of the resource to create. This name must be unique for the type of resource so that
* the resource can be later retrieved and, if necessary, deleted.
* @param {object} [options] Set of parameters to be used when creating the resource.
* @returns {PodJS.Pod#Resource} Returns the instance of the resource.
* @throws {Error} If the resource type provided was not valid. This checking is handled by {@link PodJS.Pod#newResourceClass}.
* @throws {Error} If a resource of this type already exists with the given name. This checking is handled by
* {@link PodJS.Pod#newResourceClass}.
*/
this.newResource = function(resourceType, resourceName, options) {
throw new Error('Method is abstract and must be overridden by a subclass.');
};
/**
* Called by the concrete Pod class when it wishes to create a new Resource, in order to provide the constructor of the
* super-class of the object.
* <p>
* The constructor will be bound to the Pod base class so that it can update the resource registry during the lifecycle
* of the resource.
*
* @method newResourceClass
* @memberof PodJS.Pod
* @instance
* @param {string} resourceType The type of resource to be created (e.g. "sprite"). Must be one of the resource types
* returned by {@link getResourceTypes}.
* @param {string} resourceName The name of the resource to create. This name must be unique for the type of resource so that
* the resource can be later retrieved and, if necessary, deleted.
* @param {object} [options] Set of parameters to be used when creating the resource.
* @returns {Function} the constructor of the Resource class that the subclass should create a new instance of.
* @throws {Error} If the resource type provided was not valid.
* @throws {Error} If a resource of this type already exists with the given name.
*/
this.newResourceClass = function(resourceType, resourceName, options) {
// Check that resource type is one of the resource types supported by this pod.
if (this.getResourceTypeNames().indexOf(resourceType) === -1) {
throw new Error("This pod does not know how to create resources of type '" + resourceType + "'");
}
// Get ready to register with the resource registry (Resource.register() completes the process).
if (!_resourceRegistry.hasOwnProperty(resourceType)) {
_resourceRegistry[resourceType] = {};
}
var resources = _resourceRegistry[resourceType];
if (resources.hasOwnProperty(resourceName)) {
throw new Error("A resource of type '" + resourceType + "' already exists in this Pod with the name '" +
resourceName + "'. Please choose a different name.");
}
/**
* @class PodJS.Pod#ResourceContext
* @classdesc Context provided to resources that gives them access to the environment and pod.
*/
var resourceContext = {
/**
* The environment this resource is bound to.
*
* @instance
* @member {PodJS} environment
* @memberOf PodJS.Pod#ResourceContext
*/
environment : _environment,
/**
* The pod this resource is bound to.
*
* @instance
* @member {PodJS.Pod} pod
* @memberOf PodJS.Pod#ResourceContext
*/
pod : _pod
};
/**
* @class PodJS.Pod#Resource
* @classdesc Base class for a resource that belongs to a Pod.
* <p>
* This is an inner class bound to the Pod superclass that has access to the internal resource registry that is
* present in every pod. Pod sub-classes should ensure that all created resources extend from the resource
* superclass returned by {@link PodJS.Pod#newResource}.
*/
var Resource = {
/**
* The name of this resource, unique for the resource type.
*
* @type {string}
* @memberof PodJS.Pod#Resource
* @instance
*/
resourceName : resourceName,
/**
* The type of this resource.
*
* @type {string}
* @memberof PodJS.Pod#Resource
* @instance
*/
resourceType : resourceType,
/**
* Options for this resource.
*
* @type {object}
* @memberof PodJS.Pod#Resource
* @instance
*/
options : options,
/**
* Context containing references to the environment and pod to which this resource is bound.
*
* @type {PodJS.Pod#ResourceContext}
* @memberof PodJS.Pod#Resource
* @instance
*/
context : resourceContext,
/**
* Scripts associated with this resource.
*
* @type {PodJS.Script[]}
* @memberof PodJS.Pod#Resource
* @instance
*/
scripts : [],
/**
* Create a new {@link PodJS.ScriptBuilder} which will build a script attached to this resource.
*
* @method newScript
* @return {PodJS.ScriptBuilder} The ScriptBuilder that will build the script.
* @memberof PodJS.Pod#Resource
* @instance
*/
newScript : function() {
var scriptContext = new PodJS.ScriptContext(this.context.environment, this);
var script = new PodJS.Script(scriptContext);
this.scripts.push(script);
return _environment.newScriptBuilder(script);
},
/**
* Release the system resources associated with this resource and remove it from the pod's resource registry.
* <p>
* Sub-classes can optionally override this method, but must always call the super-class to ensure the proper
* bookkeeping takes place. Note that resources will continue to receive ticks until deleted / released.
*
* @method release
* @memberof PodJS.Pod#Resource
* @instance
*/
release : function() {
var resources = _resourceRegistry[resourceType];
delete resources[resourceName];
},
/**
* Gets called periodically by the environment when the next action is to take place.
* <p>
* The super-class version of tick() does nothing. Subclasses can optionally override to perform additional actions
* on each environment tick.
*
* @method tick
* @memberof PodJS.Pod#Resource
* @instance
*/
tick : function() {
},
/**
* Called by the subclass to complete the registration process once the subclass
* is fully intialized. Resources will not have tick() called on them or be
* returned until register() is called.
*
* @method register
* @memberof PodJS.Pod#Resource
* @param {object} subInstance The instance of the resource that should be registered.
* @instance
*/
register : function(subInstance) {
resources[resourceName] = subInstance;
}
};
return Resource;
};
/**
* Gets called periodically by the environment when the next action is to take place.
* <p>
* Pods are not responsible for calling tick() on their own resources or blocks. That is taken care of by the environment.
* <p>
* The super-class version of tick() does nothing. Subclasses can optionally override to perform additional actions on each
* environment tick.
*
* @method tick
* @memberof PodJS.Pod
* @instance
*/
this.tick = function() {
};
var construct = function() {
};
construct();
};
/////////////////////////////////////////////////////////////////////
// PodJS.ConstantBlock
PodJS.ConstantBlock = function(blockContext, value) {
this.blockType = "constant";
this.context = blockContext;
this.value = value;
this.blockInfo = {
blockType : "constant",
description : "Returns a constant number or string.",
parameterInfo : [],
returnsValue : true,
compatibleWith : function(resource) { return true; }
};
this.tick = function() {
console.log("constant " + value);
return value;
};
this.release = function() {};
this.reset = function() {};
};
/////////////////////////////////////////////////////////////////////
// PodJS.FunctionBlock
PodJS.FunctionBlock = function(blockContext, fn) {
this.blockType = "function";
this.context = blockContext;
this.fn = fn;
this.blockInfo = {
blockType : "function",
description : "Evaluates a JavaScript function and uses that value.",
parameterInfo : [],
returnsValue : true,
compatibleWith : function(resource) { return true; },
};
this.tick = function() {
var value = fn.call();
console.log("function " + value);
return value;
};
this.release = function() {};
this.reset = function() {};
};
/////////////////////////////////////////////////////////////////////
// PodJS.BlockInfo
/**
* @static
* @class PodJS.BlockInfo
* @classdesc Information about a block provided by a Pod, including which resource types it is compatible with and
* what parameters are accepted. Note that the object provided does not have to extend from this object - it must merely
* have the same properties.
*/
PodJS.BlockInfo = {
/**
* Name of the block type
*
* @instance
* @type {string}
* @member blockType
* @memberOf PodJS.BlockInfo
*/
blockType : null,
/**
* Description of this block
*
* @instance
* @type {string}
* @member description
* @memberOf PodJS.BlockInfo
*/
description : null,
/**
* Description of Ordered list of parameters accepted by this block. If this property is not present, it is assumed the
* block requires no parameters.
*
* @instance
* @type {PodJS.BlockInfo.ParameterInfo[]}
* @member parameterInfo
* @memberOf PodJS.BlockInfo
*/
parameterInfo : [],
/**
* Optional function to reset the state of the block. If present, this will be called whenever the script is reset.
*
* @instance
* @method reset
* @param {PodJS.Pod#BlockContext} context So that the block can have access to its environment, etc. during reset.
* @memberOf PodJS.BlockInfo
*/
reset : null,
/**
* If true, this block is a reporter block (i.e. it returns a value, either a number or a string). The block will return a
* value from its tick method. Else, if false, the tick method will return undefined.
*
* @instance
* @type {boolean}
* @member returnsValue
* @memberOf PodJS.BlockInfo
*/
returnsValue : false,
/**
* Returns true if this block is compatible with the specified resource, or false if not.
* <p>
* If not compatible, the {@link PodJS.ScriptBuilder} will refuse to attach the block to the resource.
* <p>
* The default implementation of this method is to return true. Subclasses must override if the block will not work with all
* resources. If an object is provided with no compatibleWith method, it is assumed the block is compatible with all resources.
*
* @method compatibleWith
* @memberOf PodJS.BlockInfo
* @param {PodJS.Pod#Resource} resource The resource to check for compatibility.
* @return {boolean} True if the block is compatible, or false if not.
* @instance
*/
compatibleWith : function(resource) {
return true;
}
};
/////////////////////////////////////////////////////////////////////
// PodJS.BlockInfo.ParameterInfo
/**
* @static
* @class PodJS.BlockInfo.ParameterInfo
* @classdesc Information about a parameter provided to a block.
*/
PodJS.BlockInfo.ParameterInfo = {
/**
* Name of the parameter
*
* @instance
* @type {string}
* @member name
* @memberOf PodJS.BlockInfo.ParameterInfo
*/
name : null,
/**
* Description of this parameter
*
* @instance
* @type {string}
* @member description
* @memberOf PodJS.BlockInfo.ParameterInfo
*/
description : null
};
/////////////////////////////////////////////////////////////////////
// PodJS.ResourceInfo
/**
* @static
* @class PodJS.ResourceInfo
* @classdesc Information about a resource provided by a Pod, including name and description.
*/
PodJS.ResourceInfo = {
/**
* Name of the resource type
*
* @instance
* @type {string}
* @member resourceType
* @memberOf PodJS.ResourceInfo
*/
resourceType : null,
/**
* Description of this resource
*
* @instance
* @type {string}
* @member description
* @memberOf PodJS.ResourceInfo
*/
description : null
};
/////////////////////////////////////////////////////////////////////
// Core library
var CorePod = function(initParams) {
PodJS.Pod.call(this, initParams);
this.getBlockTypes = function() {
return [
{
blockType : "begin",
description : "Delineates the start of a new group of blocks.",
parameterInfo : [],
returnsValue : false,
compatibleWith : function(resource) {
return true;
},
tick : function(context) {
console.log("begin");
context.blockScript.nextBlock();
}
},
{
blockType : "end",
description : "Delineates the end of a new group of blocks.",
parameterInfo : [],
returnsValue : false,
compatibleWith : function(resource) {
return true;
},
tick : function(context) {
console.log("end");
context.blockScript.popIP();
}
}
];
};
this.getResourceTypes = function() {
return [];
};
this.newResource = function(resourceType, resourceName, options) {
var resourceBase = this.newResourceClass(resourceType, resourceName, options);
var resource = Object.create(resourceBase);
return resource;
};
this.newBlock = function(blockType, resource, script) {
var blockClass = this.newBlockClass(blockType, resource, script);
var block = Object.create(blockClass);
return block;
};
};
CorePod.prototype = Object.create(PodJS.Pod.prototype);
CorePod.constructor = CorePod;
PodJS.REGISTER_POD_CLASS("core", CorePod);
} // end browser detect