/**
This system uses the physics system to determine whether an entity is grappling a wall. For an entity to grapple a wall, it needs
to push against a physics entity with a 'wall' collision category and a vertical surface. The grappling entity needs to be a dynamic
physics object, additionally it requires the GroundedMovement and WallGrappling components to be present. An entity can only grapple
a wall while it's in the air. This system triggers animations on entities. A 'grappleLeft' animation is played if an entity is grappling
a wall on the left, similarily, for the right side a 'grappleRight' animation will be played.
@class WallGrapplingSystem
@constructor
@param entitySystemManager {Manager} The entity system manager whose entities this system will be working on.
@param physicsSystem {PhysicsSystem}
@param collisionMasks {Object} An object holding bit masks used for determining collision categories.
*/
function WallGrapplingSystem(entitySystemManager, physicsSystem, collisionMasks){
//System variables.
var WALL_GRAPPLING_ANIMATION_PRIORITY = 2,
//Keep this value at 2. This way, grappling lasts for one full frame, from a physics system update
//through this system's update to this system's update in the next frame.
//If this value is higher, then the wall sliding logic might prevent the entity from wall jumping,
//since it resets the vertical velocity. If this value is lower (or if you'd just use a boolean value
//instead of a counter), then the entity would be wall grappling between the physics update to this system's update only,
//which means that for the input system callbacks the entity would never be grappling.
//There's more on this in the update function.
WALL_GRAPPLING_DURATION_IN_FRAMES = 2,
//Animation names.
grappleLeft = 'grappleLeft',
grappleRight = 'grappleRight';
var collisionResolvedEventCallback = (function(){
//Reusable vectors.
var rightVector = new Vector2D(1, 0),
collisionNormal = new Vector2D(),
velocity = new Vector2D(),
temporaryPosition = new Vector2D();
return function(contactData){
var entityA = contactData.getEntityA(),
entityB = contactData.getEntityB(),
movable = entityA.get(Movable),
groundedMovement = entityA.get(GroundedMovement),
wallGrappling = entityA.get(WallGrappling),
collidable = entityB.get(Collidable),
position, dotProduct;
if(movable && groundedMovement && !groundedMovement.isOnGround() && wallGrappling && (collidable._category & collisionMasks.wall)){
contactData.collisionData.getCollisionNormal(collisionNormal);
dotProduct = rightVector.dot(collisionNormal);
//Check if the entity grappled the left wall while in air.
if(dotProduct > 0.95){
//Displace the grappling entity slightly to make sure that it's no longer touching the wall.
//This enforces the player to push against the wall to grapple and prevents entities from sticking to walls.
//The position has to always be present, because this function is a physics system callback.
position = entityA.get(Position);
position.getPosition(temporaryPosition);
position.setPosition(temporaryPosition.x + 0.1, temporaryPosition.y);
//The entity can grapple a wall only if it's moving downwards.
movable.getVelocity(velocity);
if(velocity.y >= 0){
wallGrappling._grapplingLeftCounter = WALL_GRAPPLING_DURATION_IN_FRAMES;
}
//Check if the entity grappled the right wall while in air.
}else if(dotProduct < -0.95){
//Displace the grappling entity slightly to make sure that it's no longer touching the wall.
//This enforces the player to push against the wall to grapple and prevents entities from sticking to walls.
//The position has to always be present, because this function is a physics system callback.
position = entityA.get(Position);
position.getPosition(temporaryPosition);
position.setPosition(temporaryPosition.x - 0.1, temporaryPosition.y);
//The entity can grapple a wall only if it's moving downwards.
movable.getVelocity(velocity);
if(velocity.y >= 0){
wallGrappling._grapplingRightCounter = WALL_GRAPPLING_DURATION_IN_FRAMES;
}
}
}
/*This code is commented, because i don't expect a dynamic entity to be able to grapple another dynamic entity.
*It's here though, if such a need occurs in the future.
*
movable = entityB.get(Movable);
groundedMovement = entityB.get(GroundedMovement);
wallGrappling = entityB.get(WallGrappling);
collidable = entityA.get(Collidable);
if(movable && groundedMovement && !groundedMovement.isOnGround() && wallGrappling && (collidable._category & collisionMasks.wall)){
contactData.collisionData.getCollisionNormal(collisionNormal);
//Invert the collision normal, since it's given from entityA's perspective.
collisionNormal.invert();
dotProduct = rightVector.dot(collisionNormal);
//Check if the entity grappled the left wall while in air.
if(dotProduct > 0.95){
//Displace the grappling entity slightly to make sure that it's no longer touching the wall.
//This enforces the player to push against the wall to grapple and prevents entities from sticking to walls.
//The position has to always be present, because this function is a physics system callback.
position = entityB.get(Position);
position.getPosition(temporaryPosition);
position.setPosition(temporaryPosition.x + 0.1, temporaryPosition.y);
//The entity can grapple a wall only if it's moving downwards.
movable.getVelocity(velocity);
if(velocity.y >= 0){
wallGrappling._grapplingLeftCounter = WALL_GRAPPLING_DURATION_IN_FRAMES;
}
//Check if the entity grappled the right wall while in air.
}else if(dotProduct < -0.95){
//Displace the grappling entity slightly to make sure that it's no longer touching the wall.
//This enforces the player to push against the wall to grapple and prevents entities from sticking to walls.
//The position has to always be present, because this function is a physics system callback.
position = entityB.get(Position);
position.getPosition(temporaryPosition);
position.setPosition(temporaryPosition.x - 0.1, temporaryPosition.y);
//The entity can grapple a wall only if it's moving downwards.
movable.getVelocity(velocity);
if(velocity.y >= 0){
wallGrappling._grapplingRightCounter = WALL_GRAPPLING_DURATION_IN_FRAMES;
}
}
}*/
};
})();
physicsSystem.subscribe('collisionResolved', collisionResolvedEventCallback);
var wallGrapplingEntities = entitySystemManager.createAspect([WallGrappling, Movable]);
/**
@method update
*/
this.update = (function(){
var velocity = new Vector2D();
function updateWallGraplingEntity(entity){
var wallGrappling = entity.get(WallGrappling),
movable = entity.get(Movable),
animation = entity.get(Animation);
//It's necessary that these counters are updated before the sliding logic.
//That way, given that wall grappling lasts 2 frames, as mentioned at the top of this file,
//if an entity is pushing against a wall, then first, in the physics system, the collision callback
//will set the wall grappling counters to 2. Then this update function will first lower the counters, then
//it'll update the velocity which will cause the entity to slide along the wall. Then for the next frame the counters
//will remain at 1 which will cause the "isGrappling" functions to return true, as expected.
//Then this update function will get called and if the entity wasn't grappling for the frame, for
//example the entity jumped off a wall and thus the grappling logic in the physics callback didn't kick in,
//then the counters will be reduced to 0 and thus the sliding logic won't kick in.
//This way, the sliding logic won't reset the vertical velocity gained via a wall jump.
wallGrappling._grapplingLeftCounter--;
wallGrappling._grapplingRightCounter--;
//Set the entity's velocity to the wall sliding velocity if the entity is grappling a wall.
if(wallGrappling.isGrapplingLeft()){
movable.getVelocity(velocity);
movable.setVelocity(velocity.x, wallGrappling._slidingSpeed);
if(animation){
animation.play(grappleRight, WALL_GRAPPLING_ANIMATION_PRIORITY);
}
}else if(wallGrappling.isGrapplingRight()){
movable.getVelocity(velocity);
movable.setVelocity(velocity.x, wallGrappling._slidingSpeed);
if(animation){
animation.play(grappleLeft, WALL_GRAPPLING_ANIMATION_PRIORITY);
}
}
}
return function(){
wallGrapplingEntities.iterate(updateWallGraplingEntity);
};
})();
/**
@method destroy
*/
this.destroy = function(){
physicsSystem.unsubscribe('collisionResolved', collisionResolvedEventCallback);
wallGrapplingEntities.destroy();
}
}