API Documentation for: 0.11.10
Show:

File:src\components\NodeResident.js

                        /**
                        # COMPONENT **NodeResident**
                        This component connects an entity to its parent's [[NodeMap]]. It manages navigating the NodeMap and triggering events on the entity related to its position.
                        
                        ## Dependencies
                        - [[NodeMap]] (on entity's parent) - This component uses the `NodeMap` to determine its location and navigate to other nodes.
                        - [[HandlerLogic]] (on entity's parent) - This component listens for a logic tick message to maintain and update its location.
                        
                        ## Messages
                        
                        ### Listens for:
                        - **handle-logic** - On a `tick` logic message, the component updates its location and triggers messages regarding its neighbors.
                          - @param message.delta (Number) - This component uses the current time to determine its progress along an edge if moving from node to node on the map.
                        - **on-node** - Sets the entity's position to the sent node, updates its coordinates, and triggers messages regarding its neighbors if any.
                          - @param node (Node) - The node that this entity should be located on.
                        - **leave-node** - Removes the entity from its current node if it's on one.
                        - **goto-node** - Begins moving the entity along edges to get to sent node.
                          - @param node (Node) - The node that this entity should move to.
                        - **follow** - Causes this entity to follow another entity. The leading entity must also have a `NodeResident` component and exist in the NodeMap.
                          - @param entity (Entity) - The entity that this entity should follow.
                        
                        ### Local Broadcasts:
                        - **next-to-[entity-type]** - This message is triggered when the entity is placed on a node. It will trigger on all neighboring entities, as well as on itself on behalf of neighboring entities.
                          - @param entity (Entity) - The entity that is next to the listening entity.
                        - **with-[entity-type]** - This message is triggered when the entity is placed on a node. It will trigger on all entities residing on the same node, as well as on itself on behalf of all resident entities.
                          - @param entity (Entity) - The entity that is with the listening entity.
                        - **left-node** - Triggered when the entity leaves a node.
                          - @param node (Node) - The node that the entity just left.
                        - **[Messages specified in definition]** - When the entity is placed on a node, it checks out the type of node and triggers a message on the entity if an event is listed for the current node type.
                        
                        ## States
                        - **on-node** - This state is true when the entity is on a node.
                        - **moving** - This state is true when the entity is moving from one node to another.
                        - **going-[direction]** - This state is true when the entity is moving (or has just moved) in a direction (determined by the NodeMap) from one node to another.
                          
                        ## JSON Definition
                            {
                              "type": "NodeResident",
                              
                              "nodeId": "city-hall",
                              // Optional. The id of the node that this entity should start on. Uses the entity's nodeId property if not set here.
                              
                              "nodes": {"path": "walking", "sidewalk": "walking", "road": "driving"],
                              // Optional. This is a list of node types that this entity can reside on. If not set, entity can reside on any type of node.
                              
                              "shares": ['friends','neighbors','city-council-members'],
                              // Optional. This is a list of entities that this entity can reside with on the same node. If not set, this entity can reside with any entities on the same node.
                              
                              "speed": 5,
                              // Optional. Sets the speed with which the entity moves along an edge to an adjacent node. Default is 0 (instantaneous movement).
                              
                              "updateOrientation": true
                              // Optional. Determines whether the entity's orientation is updated by movement across the NodeMap. Default is false.
                            }
                        */
                        
                        /**
                         * This component connects an entity to its parent's [[NodeMap]]. It manages navigating the NodeMap and triggering events on the entity related to its position.
                         *
                         * @namespace platypus.components
                         * @class NodeResident
                         * @uses platypus.Component
                         */
                        /* global platypus */
                        (function () {
                            'use strict';
                            
                            var createGateway = function (nodeDefinition, map, gateway) {
                                    return function () {
                                        // ensure it's a node if one is available at this gateway
                                        var node = map.getNode(nodeDefinition);
                        
                                        if (this.isPassable(node)) {
                                            this.destinationNodes.length = 0;
                                            this.destinationNodes.push(node);
                        
                                            if (this.node) {
                                                this.onEdge(node);
                                            } else {
                                                this.distance = 0;
                                            }
                                            this.progress = 0;
                        
                                            this.setState('going-' + gateway);
                                            return true;
                                        }
                        
                                        return false;
                                    };
                                },
                                distance = function (origin, destination) {
                                    var x = destination.x - origin.x,
                                        y = destination.y - origin.y,
                                        z = destination.z - origin.z;
                        
                                    return Math.sqrt(x * x + y * y + z * z);
                                },
                                angle = function (origin, destination, distance, ratio) {
                                    var x = destination.x - origin.x,
                                        y = destination.y - origin.y,
                                        a = 0;
                        
                                    if (origin.rotation && destination.rotation) {
                                        x = (origin.rotation + 180) % 360;
                                        y = (destination.rotation + 180) % 360;
                                        return (x * (1 - ratio) + y * ratio + 180) % 360;
                                    } else {
                                        if (!distance) {
                                            return a;
                                        }
                        
                                        a = Math.acos(x / distance);
                                        if (y < 0) {
                                            a = (Math.PI * 2) - a;
                                        }
                                        return a * 180 / Math.PI;
                                    }
                                },
                                axisProgress = function (r, o, d, f) {
                                    return o * (1 - r) + d * r + f;
                                },
                                isFriendly = function (entities, kinds) {
                                    var x = 0,
                                        y = 0,
                                        found = false;
                        
                                    if (kinds === null) {
                                        return true;
                                    }
                        
                                    for (x = 0; x < entities.length; x++) {
                                        for (y = 0; y < kinds.length; y++) {
                                            if (entities[x].type === kinds[y]) {
                                                found = true;
                                            }
                                        }
                                        if (!found) {
                                            return false;
                                        } else {
                                            found = false;
                                        }
                                    }
                        
                                    return true;
                                };
                        
                            return platypus.createComponentClass({
                                
                                id: 'NodeResident',
                                
                                publicProperties: {
                        
                                    /**
                                     * This describes the rate at which a node resident should progress along an edge to another node. This property is set on the entity itself and can be manipulated in real-time.
                                     *
                                     * @property speed
                                     * @type Number
                                     * @default 0
                                     */
                                    speed: 0
                                },
                                
                                initialize: function (definition) {
                                    var offset = definition.offset || this.owner.nodeOffset || {};
                                    
                                    this.nodeId = this.owner.nodeId = definition.nodeId || this.owner.nodeId;
                                    
                                    this.neighbors = {};
                                    this.friendlyNodes = definition.nodes || null;
                                    this.friendlyEntities = definition.shares || null;
                                    this.snapToNodes = definition.snapToNodes || false;
                                    this.updateOrientation = definition.updateOrientation || false;
                                    this.distance = 0;
                                    this.buffer   = definition.buffer || 0;
                                    this.progress = 0;
                                    this.offset = {
                                        x: offset.x || 0,
                                        y: offset.y || 0,
                                        z: offset.z || 0
                                    };
                                    this.destinationNodes = Array.setUp();
                                    this.algorithm = definition.algorithm || distance;
                                    
                                    this.state = this.owner.state;
                                    this.state.set('moving', false);
                                    this.state.set('on-node', false);
                                    this.currentState = '';
                                },
                                
                                events: {
                                    "set-algorithm": function (algorithm) {
                                        this.algorithm = algorithm || distance;
                                    },
                                    "handle-logic": function (resp) {
                                        var i = 0,
                                            ratio    = 0,
                                            momentum = 0,
                                            node     = null,
                                            arr = null;
                                        
                                        if (!this.owner.node) {
                                            arr = Array.setUp(this.owner.x, this.owner.y);
                                            this.owner.triggerEvent('on-node', this.owner.parent.getClosestNode(arr));
                                            arr.recycle();
                                            
                                            /**
                                             * This event is triggered if the entity is placed on the map but not assigned a node. It is moved to the nearest node and "in-location" is triggered.
                                             *
                                             * @event 'in-location'
                                             * @param entity {platypus.Entity} The entity that is in location.
                                             */
                                            this.owner.triggerEvent('in-location', this.owner);
                                        }
                        
                                        if (this.followEntity) {
                                            node = this.followEntity.node || this.followEntity;
                                            if (node && node.isNode && (node !== this.node)) {
                                                this.lag = 0;
                                                this.state.set('moving', this.gotoNode());
                                                if (this.followDistance) {
                                                    momentum = this.lag;
                                                }
                                            } else {
                                                this.followEntity = null;
                                            }
                                        } else {
                                            momentum = this.speed * resp.delta;
                                        }
                        
                                        // if goto-node was blocked, try again.
                                        if (this.blocked) {
                                            this.blocked = false;
                                            if (this.goingToNode) {
                                                this.owner.triggerEvent('goto-closest-node', this.goingToNode);
                                            }
                                        }
                                        
                                        if (this.destinationNodes.length) {
                                            this.state.set('moving', (this.speed !== 0));
                                            if (this.node) {
                                                this.onEdge(this.destinationNodes[0]);
                                            } else if (!this.lastNode) {
                                                this.owner.triggerEvent('on-node', this.destinationNodes[0]);
                                                this.destinationNodes.greenSplice(0);
                                                if (!this.destinationNodes.length) {
                                                    this.state.set('moving', false);
                                                    return;
                                                }
                                            }
                                            
                                            if (this.snapToNodes) {
                                                for (i = 0; i < this.destinationNodes.length; i++) {
                                                    this.owner.triggerEvent('on-node', this.destinationNodes[i]);
                                                }
                                                this.destinationNodes.length = 0;
                                            } else {
                                                while (this.destinationNodes.length && momentum) {
                                                    if ((this.progress + momentum) >= this.distance) {
                                                        node = this.destinationNodes[0];
                                                        momentum -= (this.distance - this.progress);
                                                        this.progress = 0;
                                                        this.destinationNodes.greenSplice(0);
                                                        this.owner.triggerEvent('on-node', node);
                                                        if (this.destinationNodes.length && momentum) {
                                                            this.onEdge(this.destinationNodes[0]);
                                                        }
                                                    } else {
                                                        this.progress += momentum;
                                                        ratio = this.progress / this.distance;
                                                        this.owner.x = axisProgress(ratio, this.lastNode.x, this.destinationNodes[0].x, this.offset.x);
                                                        this.owner.y = axisProgress(ratio, this.lastNode.y, this.destinationNodes[0].y, this.offset.y);
                                                        this.owner.z = axisProgress(ratio, this.lastNode.z, this.destinationNodes[0].z, this.offset.z);
                                                        if (this.updateOrientation) {
                                                            this.owner.rotation = angle(this.lastNode, this.destinationNodes[0], this.distance, ratio);
                                                        }
                                                        momentum = 0;
                                                    }
                                                }
                                            }
                                        } else {
                                            this.state.set('moving', false);
                                        }
                                    },
                                    "on-node": function (node) {
                                        var j = 0,
                                            entities = null;
                                        
                                        this.owner.node = this.node = node; //TODO: not sure if this needs to be accessible outside this component.
                                        this.node.removeFromEdge(this.owner);
                                        if (this.lastNode) {
                                            this.lastNode.removeFromEdge(this.owner);
                                        }
                                        this.node.addToNode(this.owner);
                                        
                                        this.setState('on-node');
                                        
                                        this.owner.x = this.node.x + this.offset.x;
                                        this.owner.y = this.node.y + this.offset.y;
                                        this.owner.z = this.node.z + this.offset.z;
                                        if (this.updateOrientation && this.node.rotation) {
                                            this.owner.rotation = this.node.rotation;
                                        }
                                        
                                        //add listeners for directions
                                        this.owner.triggerEvent('set-directions');
                                        
                                        //trigger mapped messages for node types
                                        if (this.friendlyNodes && this.friendlyNodes[node.type]) {
                                            this.owner.trigger(this.friendlyNodes[node.type], node);
                                        }
                        
                                        //trigger "with" events
                                        entities = node.contains;
                                        for (j = 0; j < entities.length; j++) {
                                            if (this.owner !== entities[j]) {
                                                entities[j].triggerEvent("with-" + this.owner.type, this.owner);
                                                this.owner.triggerEvent("with-" + entities[j].type, entities[j]);
                                            }
                                        }
                                    },
                                    "leave-node": function () {
                                        if (this.node) {
                                            this.node.removeFromNode(this.owner);
                                            this.owner.triggerEvent('left-node', this.node);
                                            this.owner.triggerEvent('remove-directions');
                                        }
                                        this.lastNode = this.node;
                                        this.node = null;
                                    },
                                    "goto-node": function (node) {
                                        this.gotoNode(node);
                                    },
                                    "follow": function (entityOrNode) {
                                        if (entityOrNode.entity) {
                                            this.followDistance = entityOrNode.distance;
                                            this.followEntity = entityOrNode.entity;
                                        } else {
                                            this.followDistance = 0;
                                            this.followEntity = entityOrNode;
                                        }
                                    },
                                    "goto-closest-node": (function () {
                                        var checkList = function (here, list) {
                                                var i = 0;
                        
                                                for (i = 0; i < list.length; i++) {
                                                    if (list[i] === here) {
                                                        return true;
                                                    }
                                                }
                        
                                                return false;
                                            },
                                            checkType = function (here, type) {
                                                return (here.type === type);
                                            },
                                            checkObjectType = function (here, node) {
                                                return (here.type === node.type);
                                            };
                                        
                                        return function (nodesOrNodeType) {
                                            var travResp = null,
                                                depth    = 20, //arbitrary limit
                                                origin   = this.node || this.lastNode,
                                                test     = null,
                                                steps    = nodesOrNodeType.steps || 0,
                                                nodes    = null;
                        
                                            this.goingToNode = nodesOrNodeType;
                                            
                                            if (typeof nodesOrNodeType === 'string') {
                                                test = checkType;
                                            } else if (typeof nodesOrNodeType.type === 'string') {
                                                test = checkObjectType;
                                            } else {
                                                test = checkList;
                                            }
                                            
                                            if (origin && nodesOrNodeType && !test(origin, nodesOrNodeType)) {
                                                nodes = Array.setUp();
                                                travResp = this.traverseNode({
                                                    depth: depth,
                                                    origin: origin,
                                                    position: origin,
                                                    test: test,
                                                    destination: nodesOrNodeType,
                                                    nodes: nodes,
                                                    shortestPath: Infinity,
                                                    distance: 0,
                                                    found: false,
                                                    algorithm: this.algorithm,
                                                    blocked: false
                                                });
                                                
                                                travResp.distance -= this.progress;
                                                
                                                if (travResp.found) {
                                                    //TODO: should probably set this up apart from this containing function
                                                    if (this.followEntity) {
                                                        if (!this.followDistance) {
                                                            this.setPath(travResp, steps);
                                                        } else if ((travResp.distance + (this.followEntity.progress || 0)) > this.followDistance) {
                                                            this.lag = travResp.distance + (this.followEntity.progress || 0) - this.followDistance;
                                                            this.setPath(travResp, steps);
                                                        } else {
                                                            this.lag = 0;
                                                        }
                                                    } else {
                                                        this.setPath(travResp, steps);
                                                    }
                                                } else if (travResp.blocked) {
                                                    this.blocked = true;
                                                }
                                                
                                                nodes.recycle();
                                            }
                                        };
                                    }()),
                                    "set-directions": function () {
                                        var i = '',
                                            j = 0,
                                            entities = null,
                                            node     = this.node,
                                            nextNode = null;
                                        
                                        this.owner.triggerEvent('remove-directions');
                                        
                                        for (i in node.neighbors) {
                                            if (node.neighbors.hasOwnProperty(i)) {
                                                this.neighbors[i] = createGateway(node.neighbors[i], node.map, i);
                                                this.addEventListener(i, this.neighbors[i]);
                        
                                                //trigger "next-to" events
                                                nextNode = node.map.getNode(node.neighbors[i]);
                                                if (nextNode) {
                                                    entities = nextNode.contains;
                                                    for (j = 0; j < entities.length; j++) {
                                                        entities[j].triggerEvent("next-to-" + this.owner.type, this.owner);
                                                        this.owner.triggerEvent("next-to-" + entities[j].type, entities[j]);
                                                    }
                                                }
                                            }
                                        }
                                    },
                                    "remove-directions": function () {
                                        var i = '';
                                        
                                        for (i in this.neighbors) {
                                            if (this.neighbors.hasOwnProperty(i)) {
                                                this.removeEventListener(i, this.neighbors[i]);
                                                delete this.neighbors[i];
                                            }
                                        }
                                    }
                                },
                                
                                methods: {
                                    gotoNode: (function () {
                                        var test = function (here, there) {
                                            return (here === there);
                                        };
                                        
                                        return function (node) {
                                            var travResp = null,
                                                depth = 20, //arbitrary limit
                                                origin = this.node || this.lastNode,
                                                nodes = null,
                                                moving = false;
                                            
                                            if (!node && this.followEntity) {
                                                node = this.followEntity.node || this.followEntity.lastNode || this.followEntity;
                                            }
                                            
                                            if (origin && node && (this.node !== node)) {
                                                nodes = Array.setUp();
                                                
                                                travResp = this.traverseNode({
                                                    depth: depth,
                                                    origin: origin,
                                                    position: origin,
                                                    test: test,
                                                    destination: node,
                                                    nodes: nodes,
                                                    shortestPath: Infinity,
                                                    distance: 0,
                                                    found: false,
                                                    algorithm: this.algorithm,
                                                    blocked: false
                                                });
                                                
                                                travResp.distance -= this.progress;
                                                
                                                if (travResp.found) {
                                                    //TODO: should probably set this up apart from this containing function
                                                    if (this.followEntity) {
                                                        if (!this.followDistance) {
                                                            this.setPath(travResp);
                                                            moving = true;
                                                        } else if ((travResp.distance + (this.followEntity.progress || 0)) > this.followDistance) {
                                                            this.lag = travResp.distance + (this.followEntity.progress || 0) - this.followDistance;
                                                            this.setPath(travResp);
                                                            moving = true;
                                                        } else {
                                                            this.lag = 0;
                                                        }
                                                    } else {
                                                        this.setPath(travResp);
                                                        moving = true;
                                                    }
                                                } else if (travResp.blocked) {
                                                    this.blocked = true;
                                                }
                                                
                                                nodes.recycle();
                                            }
                                            
                                            return moving;
                                        };
                                    }()),
                                    
                                    isPassable: function (node) {
                                        return node && (this.node !== node) && (!this.friendlyNodes || (typeof this.friendlyNodes[node.type] !== 'undefined')) && (!node.contains.length || isFriendly(node.contains, this.friendlyEntities));
                                    },
                                    traverseNode: function (record) {
                                        //TODO: may want to make this use A*. Currently node traversal order is arbitrary and essentially searches entire graph, but does clip out paths that are too long.
                                        
                                        var i         = 1,
                                            j         = '',
                                            map       = record.position.map,
                                            neighbors = null,
                                            node      = null,
                                            nodeList  = null,
                                            resp      = null,
                                            algorithm = record.algorithm || distance,
                                            savedResp = {
                                                shortestPath: Infinity,
                                                found: false,
                                                blocked: false
                                            },
                                            blocked   = true,
                                            hasNeighbor = false;
                        
                                        if ((record.depth === 0) || (record.distance > record.shortestPath)) {
                                            // if we've reached our search depth or are following a path longer than our recorded successful distance, bail
                                            return record;
                                        } else if (record.test(record.position, record.destination)) {
                                            // if we've reached our destination, set shortest path information and bail.
                                            record.found = true;
                                            record.shortestPath = record.distance;
                                            return record;
                                        } else {
                                            //Make sure we do not trace an infinite node loop.
                                            nodeList = record.nodes;
                                            for (i = 1; i < nodeList.length - 1; i++) {
                                                if (nodeList[i] === record.position) {
                                                    return record;
                                                }
                                            }
                                                
                                            neighbors = record.position.neighbors;
                                            for (j in neighbors) {
                                                if (neighbors.hasOwnProperty(j)) {
                                                    node = map.getNode(neighbors[j]);
                                                    hasNeighbor = true;
                                                    if (this.isPassable(node)) {
                                                        nodeList = record.nodes.greenSlice();
                                                        nodeList.push(node);
                                                        resp = this.traverseNode({
                                                            depth: record.depth - 1,
                                                            origin: record.origin,
                                                            position: node,
                                                            destination: record.destination,
                                                            test: record.test,
                                                            algorithm: algorithm,
                                                            nodes: nodeList,
                                                            shortestPath: record.shortestPath,
                                                            distance: record.distance + algorithm(record.position, node),
                                                            gateway: record.gateway || j,
                                                            found: false,
                                                            blocked: false
                                                        });
                                                        nodeList.recycle();
                                                        if (resp.found && (savedResp.shortestPath > resp.shortestPath)) {
                                                            savedResp = resp;
                                                        }
                                                        blocked = false;
                                                    }
                                                }
                                            }
                                            savedResp.blocked = (hasNeighbor && blocked);
                                            return savedResp;
                                        }
                                    },
                                    setPath: function (resp, steps) {
                                        if (resp.nodes[0] === this.node) {
                                            resp.nodes.greenSplice(0);
                                        }
                                        this.destinationNodes.recycle();
                                        this.destinationNodes = resp.nodes.greenSlice();
                                        if (steps) {
                                            this.destinationNodes.length = Math.min(steps, this.destinationNodes.length);
                                        }
                                    },
                                    setState: function (state) {
                                        if (state === 'on-node') {
                                            this.state.set('on-node', true);
                                        } else {
                                            this.state.set('on-node', false);
                                            if (this.currentState) {
                                                this.state.set(this.currentState, false);
                                            }
                                            this.currentState = state;
                                            this.state.set(state, true);
                                        }
                                    },
                                    onEdge: function (toNode) {
                                        this.distance = distance(this.node, toNode);
                                        if (this.updateOrientation) {
                                            this.owner.rotation = angle(this.node, toNode, this.distance, this.progress / this.distance);
                                        }
                                        this.node.addToEdge(this.owner);
                                        toNode.addToEdge(this.owner);
                                        this.owner.triggerEvent('leave-node');
                                    },
                                    destroy: function () {
                                        this.destinationNodes.recycle();
                                        this.state = null;
                                    }
                                }
                            });
                        }());