API Docs for:
Show:

File: gameplay\movement\grounded-movement\System.js

/**
 System responsible for horizontal movement. It handles movement while on the ground and in the air. Moving requires that the
 entity is a valid dynamic physics object, also the GroundedMovement component is required. This system also controlls the
 FacingDirection, if present. Movement is executed via the input and AI systems triggering the 'moveLeft' and 'moveRight' events.
 To ensure smooth movement, this system controlls friction directly, so don't rely on the friction set on the Collidable component.
 This system plays animations. The run animations should be tuned for a specified speed, because this system will modify the rate
 of run animations based on the entity's horizontal velocity. The animations played are 'runLeft' and 'runRight' if an entity has moved
 in the direction it wanted while on the ground. The 'stopLeft' and 'stopRight' animations are played if an entity is moving on the ground
 unwillingly. The 'stopOppositeLeft' animation is played if an entity is moving to the right, but is facing to the left, for the opposite
 the 'stopOppositeRight' animation will play. While the entity is in the air, if it's moving upwards the 'airAscendLeft' and 'airAscendRight'
 animations will play. If the entity is moving downwards the 'airDescendLeft' and 'airDescendRight' animations will play.
 @class GroundedMovementSystem
 @constructor
 @param entitySystemManager {Manager} The entity system manager whose entities this system will be working on.
 @param inputSystem {KeyboardInputSystem}
 @param aISystem {AISystem}
 @param physicsSystem {PhysicsSystem}
 @param collisionMasks {Object} An object holding bit masks used for determining collision categories.
 */
function GroundedMovementSystem(entitySystemManager, inputSystem, aISystem, physicsSystem, collisionMasks){
    
    //System variables.
    
    //The speed for which all run animations are tuned.
    //If the speed of an entity varies from this value, the animation rate will be adjusted accordingly.
    var DEFAULT_RUN_ANIMATION_SPEED = 200,
    
        GROUNDED_MOVEMENT_ANIMATION_PRIORITY = 5,
    
    //Facing directions.
        right = 'right',
        left = 'left',
        
    //Animation names.
        runLeft = 'runLeft',
        runRight = 'runRight',
        stopLeft = 'stopLeft',
        stopRight = 'stopRight',
        stopOppositeLeft = 'stopOppositeLeft',
        stopOppositeRight = 'stopOppositeRight',
        airAscendLeft = 'airAscendLeft',
        airAscendRight = 'airAscendRight',
        airDescendLeft = 'airDescendLeft',
        airDescendRight = 'airDescendRight';
    
    //=====Input handling=====
    
    var moveLeftEventCallback = (function(){
        
        var movementImpulse = new Vector2D(),
            entityVelocity = new Vector2D();
        
        return function(entity){
            var groundedMovement = entity.get(GroundedMovement),
                movable = entity.get(Movable),
                spellEffects = entity.get(SpellEffects),
                facingDirection = entity.get(FacingDirection),
                speedLimit, movementImpulseMagnitude;
            
            //If the entity doesn't have the GroundedMovement or Movable components, quit.
            if(!(groundedMovement && movable)){
                return;
            }
            
            //Set the impulse magnitude and speed limit depending on whether the entity is on the ground or in the air.
            if(groundedMovement.isOnGround()){
                movementImpulseMagnitude = groundedMovement._groundMovementImpulseMagnitude;
                speedLimit = groundedMovement._groundSpeedLimit;
            }else{
                movementImpulseMagnitude = groundedMovement._airMovementImpulseMagnitude;
                speedLimit = groundedMovement._airSpeedLimit;
            }
            
            //Modify the movement impulse magnitude and the speed limit if the entity is affected by spell effects.
            if(spellEffects){
                for(var node=spellEffects.getMovementSpeedSpellEffects().getFirst(); node; node=node.next){
                    movementImpulseMagnitude *= node.value.getSpeedMultiplier();
                    speedLimit *= node.value.getSpeedMultiplier();
                }
            }
            
            movable.getVelocity(entityVelocity);
            //Calculate the change in speed that will be caused by the impulse.
            var speedChange = movementImpulseMagnitude / movable.getMass();
            //Apply the change.
            entityVelocity.x -= speedChange;
            //Clamp the velocity.
            entityVelocity.x = Math.max(entityVelocity.x, -speedLimit);
            //Set the velocity for the entity.
            movable.setVelocity(entityVelocity.x, entityVelocity.y);
            
            //Thanks to this flag, the system will be able to set an apropriate value for the friction for this entity.
            //This means that if an entity moves in the direction it wanted to move, then its friction will be set to 0.
            if(entityVelocity.x < 0){
                groundedMovement._movedInDesiredDirection = true;
            }
            
            //The entity is now facing left.
            if(facingDirection){
                facingDirection.setFacingDirection(left);
            }
        };
    })();
    
    var moveRightEventCallback = (function(){
        
        var movementImpulse = new Vector2D(),
            entityVelocity = new Vector2D();
        
        return function(entity){
            var groundedMovement = entity.get(GroundedMovement),
                movable = entity.get(Movable),
                spellEffects = entity.get(SpellEffects),
                facingDirection = entity.get(FacingDirection),
                speedLimit, movementImpulseMagnitude;
            
            //If the entity doesn't have the GroundedMovement or Movable components, quit.
            if(!(groundedMovement && movable)){
                return;
            }
            
            //Set the impulse magnitude and speed limit depending on whether the entity is on the ground or in the air.
            if(groundedMovement.isOnGround()){
                movementImpulseMagnitude = groundedMovement._groundMovementImpulseMagnitude;
                speedLimit = groundedMovement._groundSpeedLimit;
            }else{
                movementImpulseMagnitude = groundedMovement._airMovementImpulseMagnitude;
                speedLimit = groundedMovement._airSpeedLimit;
            }
            
            //Modify the movement impulse magnitude and the speed limit if the entity is affected by spell effects.
            if(spellEffects){
                for(var node=spellEffects.getMovementSpeedSpellEffects().getFirst(); node; node=node.next){
                    movementImpulseMagnitude *= node.value.getSpeedMultiplier();
                    speedLimit *= node.value.getSpeedMultiplier();
                }
            }
            
            movable.getVelocity(entityVelocity);
            //Calculate the change in speed that will be caused by the impulse.
            var speedChange = movementImpulseMagnitude / movable.getMass();
            //Apply the change.
            entityVelocity.x += speedChange;
            //Clamp the velocity.
            entityVelocity.x = Math.min(entityVelocity.x, speedLimit);
            //Set the velocity for the entity.
            movable.setVelocity(entityVelocity.x, entityVelocity.y);
            
            //Thanks to this flag, the system will be able to set an apropriate value for the friction for this entity.
            //This means that if an entity moves in the direction it wanted to move, then its friction will be set to 0.
            if(entityVelocity.x > 0){
                groundedMovement._movedInDesiredDirection = true;
            }
            
            //The entity is now facing right.
            if(facingDirection){
                facingDirection.setFacingDirection(right);
            }
        };
    })();
    
    inputSystem.subscribe('moveLeft', moveLeftEventCallback);
    inputSystem.subscribe('moveRight', moveRightEventCallback);
    aISystem.subscribe('moveLeft', moveLeftEventCallback);
    aISystem.subscribe('moveRight', moveRightEventCallback);
    
    //=====Physics handling=====
    
    var collisionDetectedEventCallback = (function(){
        //Reusable vectors.
        var upVector = new Vector2D(0, -1),
            collisionNormal = new Vector2D();
        
        return function(contactData){
            var groundedMovementA = contactData.getEntityA().get(GroundedMovement),
                collidableA = contactData.getEntityA().get(Collidable),
                collidableB = contactData.getEntityB().get(Collidable),
                dotProduct;
            
            //Check if a grounded movement entity collided with a wall.
            if(groundedMovementA && (collidableB._category & collisionMasks.wall)){
                
                //Retrieve the collision normal.
                contactData.collisionData.getCollisionNormal(collisionNormal);
                
                dotProduct = upVector.dot(collisionNormal);
                
                //Check if the entity collided with a non-vertical surface.
                //If it did, that means it landed on the ground.
                if(dotProduct > 0.5){
                    groundedMovementA._onGroundTimeRemaining = groundedMovementA._onGroundTime;
                    
                    //Store a vector going along the ground in the right direction.
                    groundedMovementA._groundRightTangent.x = -collisionNormal.y;
                    groundedMovementA._groundRightTangent.y = collisionNormal.x;
                    
                    //If the entity landed on the ground, but is not moving in the
                    //desired direction, then set the friction to the idle friction.
                    if(!groundedMovementA._movedInDesiredDirection){
                        collidableA.setFriction(groundedMovementA._idleFriction);
                    }
                }
            }
        };
    })();
    
    physicsSystem.subscribe('collisionDetected', collisionDetectedEventCallback);
    
    var groundedMovementEntities = entitySystemManager.createAspect([GroundedMovement, Collidable]);
    
    /**
     @method update
     @param deltaTime {Number} The time that passed since last update.
     */
    this.update = (function(){
        var velocity = new Vector2D();
        
        return function(deltaTime){
            groundedMovementEntities.iterate(function(entity){
                var groundedMovement = entity.get(GroundedMovement),
                    collidable = entity.get(Collidable),
                    movable = entity.get(Movable),
                    facingDirection = entity.get(FacingDirection),
                    animation = entity.get(Animation);
                    
                groundedMovement._onGroundTimeRemaining -= deltaTime;
                
                //This code handles animations for running, stopping and for when an entity is in the air.
                if(movable && facingDirection && animation){
                    
                    movable.getVelocity(velocity);
                    
                    if(groundedMovement.isOnGround()){
                        
                        //If the entity moved in the direction it wanted, play the regular run animation.
                        if(groundedMovement._movedInDesiredDirection){
                            //Adjust the rate of the run animation according to the default speed the animation was tuned for.
                            var runAnimationRate = DEFAULT_RUN_ANIMATION_SPEED / velocity.magnitude();
                            
                            switch(facingDirection.getFacingDirection()){
                                case left : animation.play(runLeft, GROUNDED_MOVEMENT_ANIMATION_PRIORITY, runAnimationRate);
                                    break;
                            
                                case right: animation.play(runRight, GROUNDED_MOVEMENT_ANIMATION_PRIORITY, runAnimationRate);
                                    break;
                            }
                            
                        }else{
                        //If the entity hasn't moved in the desired direction, but is moving, play a stopping animation
                        //based on the direction the entity is facing.
                            if(velocity.x > 0.1){
                                if(facingDirection.getFacingDirection() === right){
                                    animation.play(stopRight, GROUNDED_MOVEMENT_ANIMATION_PRIORITY);
                                }else{
                                    animation.play(stopOppositeLeft, GROUNDED_MOVEMENT_ANIMATION_PRIORITY);
                                }
                            }else if(velocity.x < -0.1){
                                if(facingDirection.getFacingDirection() === left){
                                    animation.play(stopLeft, GROUNDED_MOVEMENT_ANIMATION_PRIORITY);
                                }else{
                                    animation.play(stopOppositeRight, GROUNDED_MOVEMENT_ANIMATION_PRIORITY);
                                }
                            }
                        }
                    }else{
                        //If the entity is in the air, play an animation depending on whether the entity is
                        //moving upwards or downwards.
                        if(velocity.y < 0){
                            switch(facingDirection.getFacingDirection()){
                                case left : animation.play(airAscendLeft, GROUNDED_MOVEMENT_ANIMATION_PRIORITY);
                                    break;
                            
                                case right: animation.play(airAscendRight, GROUNDED_MOVEMENT_ANIMATION_PRIORITY);
                                    break;
                            }
                        }else{
                            switch(facingDirection.getFacingDirection()){
                                case left : animation.play(airDescendLeft, GROUNDED_MOVEMENT_ANIMATION_PRIORITY);
                                    break;
                            
                                case right: animation.play(airDescendRight, GROUNDED_MOVEMENT_ANIMATION_PRIORITY);
                                    break;
                            }
                        }
                    }
                }
                
                //Reset the friction and the moved flag.
                collidable.setFriction(0);
                groundedMovement._movedInDesiredDirection = false;
            });
        };
    })();
    
    /**
     @method destroy
     */
    this.destroy = function(){
        inputSystem.unsubscribe('moveLeft', moveLeftEventCallback);
        inputSystem.unsubscribe('moveRight', moveRightEventCallback);
        aISystem.unsubscribe('moveLeft', moveLeftEventCallback);
        aISystem.unsubscribe('moveRight', moveRightEventCallback);
        
        physicsSystem.unsubscribe('collisionDetected', collisionDetectedEventCallback);
        
        groundedMovementEntities.destroy();
    }
}