API Docs for:
Show:

File: gameplay\attacking\System.js

/**
 System reponsible for handling attacking. For an attack to trigger, a Position, a FacingDirection and an Attacking component are required.
 The triggering of attacks happens via the input and AI systems. The attack system will listen for 'attack' events from those.
 Beside that, the system will make use of spell effects. If an AttackEnchantSpellEffect is present on the SpellEffects component,
 the enchant will be triggered instead of the attack. If AttackSpeedSpellEffects are present, they're used during attack cooldown
 calculation. If an entity is grappling anything, checked via the WallGrappling component, the attack will be prevented.
 This system triggers animations. 'attackLeft' or 'attackRight' animations are played depending on the facing direction when
 an attack is performed. The animations will be sped up if an animation lasts longer than the attack cooldown.
 @class AttackSystem
 @constructor
 @param entitySystemManager {Manager} The entity system manager whose entities this system will be working on.
 @param inputSystem {KeyboardInputSystem}
 @param aISystem {AISystem}
 @param healthSystem {HealthSystem}
 @param spellSystem {SpellSystem}
 @param physicsSystem {PhysicsSystem}
 @param forceGeneratorRegistry {ForceGeneratorRegistry}
 @param collisionMasks {Object} An object holding bit masks used for determining collision categories.
 */
function AttackSystem(entitySystemManager, inputSystem, aISystem, healthSystem, spellSystem, physicsSystem, forceGeneratorRegistry, collisionMasks){
    
    //System variables.
    var PROJECTILE_SPRITE_Z_INDEX = 0,
        PROJECTILE_MASS = 0.1,
        PROJECTILE_ANIMATION_FLYING_PRIORITY = 1,
        PROJECTILE_ANIMATION_IMPACT_PRIORITY = 2,
        ATTACK_ANIMATION_PRIORITY = 5,
    
    //Facing directions.
        right = 'right',
        left = 'left',
        
    //Animation names.
        flying = 'flying',
        impact = 'impact',
        attackLeft = 'attackLeft',
        attackRight = 'attackRight';
    
    //=====Input handling=====
    
    var attackEventCallback = (function(){
        
        var meleeAttackPosition = new Position(),
            attackPoint = new Vector2D(),
            baseAttackDirection = new Vector2D(),
            projectileVelocity = new Vector2D();
        
        return function(entity){
            var position = entity.get(Position),
                facingDirection = entity.get(FacingDirection),
                attacking = entity.get(Attacking),
                wallGrappling = entity.get(WallGrappling),
                spellEffects = entity.get(SpellEffects),
                animation = entity.get(Animation),
                //The projectile angle modifier is necessary, because rotating "upwards" if the entity is facing to the right
                //is opposite than rotating "upwards" if the entity is facing to the left.
                projectileAngleModifier,
                projectileAngleOffset, cosAngleOffset, sinAngleOffset, attackCooldown, attackEnchant, triggeredAnimation;
            
            //Make sure that the required components are present, also that the entity has an attack and that the attack cooldown is up.
            if(!(position && facingDirection && attacking && attacking._attack && attacking.canAttack())){
                return;
            }
            
            //Attacking is disabled for entities that are grappling.
            if(wallGrappling && (wallGrappling.isGrapplingLeft() || wallGrappling.isGrapplingRight())){
                return;
            }
            
            attackCooldown = attacking._attackCooldown;
            
            if(spellEffects){
                for(var node=spellEffects.getAttackSpeedSpellEffects().getFirst(); node; node=node.next){
                    attackCooldown *= node.value.getSpeedMultiplier();
                }
                
                attackEnchant = spellEffects.getAttackEnchantSpellEffect();
            }
            
            //The weapon cooldown is set for both, a regular attack and an attack enchant trigger.
            attacking._attackCooldownRemaining = attackCooldown;
            
            //If the entity has an attack enchant, then the enchant will be triggered instead of the attack.
            if(attackEnchant){
                spellSystem.castSpell(entity, attackEnchant.getSpellTriggered());
                attackEnchant._numberOfChargesRemaining--;
            }else{
                position.getPosition(attackPoint);
                switch(facingDirection.getFacingDirection()){
                    case left :
                        attackPoint.x -= attacking._rightAttackPoint.x;
                        attackPoint.y += attacking._rightAttackPoint.y
                        
                        baseAttackDirection.x = -attacking._rightAttackDirection.x;
                        baseAttackDirection.y = attacking._rightAttackDirection.y;
                        
                        projectileAngleModifier = 1;
                        
                        triggeredAnimation = attackLeft;
                        break;
                    case right :
                        attackPoint.add(attacking._rightAttackPoint);
                        
                        baseAttackDirection.x = attacking._rightAttackDirection.x;
                        baseAttackDirection.y = attacking._rightAttackDirection.y;
                        
                        projectileAngleModifier = -1;
                        
                        triggeredAnimation = attackRight;
                        break;
                }
                
                switch(attacking._attack.constructor){
                    case MeleeAttack :
                        meleeAttackPosition.setPosition(attackPoint.x, attackPoint.y);
                        var attackedEntities = physicsSystem.world.queryEntitiesWithShape(meleeAttackPosition, attacking._attack._scanShape, collisionMasks.attack, attacking._foeCollideWith),
                            attackedEntity;
                        
                        for(var i=0; i<attackedEntities.length; ++i){
                            attackedEntity = attackedEntities[i];
                            
                            //Make sure that the entity can't attack itself.
                            if(attackedEntity !== entity){
                                healthSystem.damageEntity(attackedEntity, attacking._attack._damageAmount, attacking._attack._damageType);
                            }
                        }
                        
                        break;
                    case RangedAttack :
                        var projectiles = attacking._attack._projectiles,
                            projectileData, projectileEntity, projectileAnimation;
                        
                        for(var i=0; i<projectiles.length; ++i){
                            projectileData = projectiles[i];
                            
                            projectileEntity = entitySystemManager.createEntity();
                            projectileEntity.add(new Position(attackPoint.x, attackPoint.y));
                            projectileEntity.add(new Collidable(0, 0, collisionMasks.attack, attacking._foeCollideWith | collisionMasks.wall));
                            projectileEntity.add(new Shape(projectileData.shape));
                            
                            //Calculate the projectile velocity.
                            projectileAngleOffset = projectileData.angleOffset * projectileAngleModifier;
                            cosAngleOffset = Math.cos(projectileAngleOffset);
                            sinAngleOffset = Math.sin(projectileAngleOffset);
                            
                            projectileVelocity.x = baseAttackDirection.x * cosAngleOffset - baseAttackDirection.y * sinAngleOffset;
                            projectileVelocity.y = baseAttackDirection.x * sinAngleOffset + baseAttackDirection.y * cosAngleOffset;
                            projectileVelocity.multiply(projectileData.speed);
                            
                            projectileEntity.add(new Movable(PROJECTILE_MASS, 0, false, projectileVelocity.x, projectileVelocity.y));
                            
                            if(projectileData.forceGenerator){
                                forceGeneratorRegistry.add(projectileData.forceGenerator, projectileEntity);
                            }
                            projectileEntity.add(new Projectile(projectileData.damageAmount, projectileData.damageType, projectileData.forceGenerator));
                            projectileEntity.add(new Sprite(undefined, undefined, PROJECTILE_SPRITE_Z_INDEX));
                            
                            projectileAnimation = new Animation();
                            if(projectileData.flyingAnimationHandle){
                                projectileAnimation.add(projectileData.flyingAnimationHandle, flying);
                            }
                            if(projectileData.impactAnimationHandle){
                                projectileAnimation.add(projectileData.impactAnimationHandle, impact);
                            }
                            projectileEntity.add(projectileAnimation);
                            //Autoplay the flying animation on projectile creation.
                            projectileAnimation.play(flying, PROJECTILE_ANIMATION_FLYING_PRIORITY);
                        }
                        break;
                }
                
                //The attack animation is only triggered for attacks, attack enchant triggers
                //are handled by the spell system.
                //The attack animation can't last longer than the attack cooldown.
                //To achieve this, scale the animation rate if the total duration of
                //the animation is higher than the attack cooldown. Then adjust the rate slightly
                //to account for precision errors.
                if(animation){
                    var attackAnimationHandle = animation.get(triggeredAnimation);
                    
                    if(attackAnimationHandle && attackAnimationHandle.getAsset().totalDuration >= attackCooldown){
                        animation.play(triggeredAnimation,
                                       ATTACK_ANIMATION_PRIORITY,
                                       attackCooldown / attackAnimationHandle.getAsset().totalDuration - 0.01);
                    }
                }
                
            }
        };
    })();
    
    inputSystem.subscribe('attack', attackEventCallback);
    aISystem.subscribe('attack', attackEventCallback);
    
    //=====Physics handling=====
    
    function collisionResolvedEventCallback(contactData){
        var entityA = contactData.getEntityA(),
            entityB = contactData.getEntityB(),
            projectile = entityA.get(Projectile);
        
        //If one of the entities is a projectile that has not been marked for destruction,
        //deal damage to the other entity and mark the projectile for destruction.
        
        if(projectile && !projectile._destroy){
            healthSystem.damageEntity(entityB, projectile._damageAmount, projectile._damageType);
            projectile._destroy = true;
        }
        
        projectile = entityB.get(Projectile);
        
        if(projectile && !projectile._destroy){
            healthSystem.damageEntity(entityA, projectile._damageAmount, projectile._damageType);
            projectile._destroy = true;
        }
    }
    
    physicsSystem.subscribe('collisionResolved', collisionResolvedEventCallback);
    
    var attackingEntities = entitySystemManager.createAspect([Attacking]),
        projectileEntities = entitySystemManager.createAspect([Projectile, Animation]);
        
    /**
     @method update
     @param deltaTime {Number} The time that passed since last update.
     */
    this.update = (function(){
        
        var updateProjectileEntity = (function(){
            
            function destroyEntity(entity){
                entity.destroy();
            }
            
            return function(entity){
                var projectile = entity.get(Projectile),
                    animation = entity.get(Animation);
                
                if(projectile._destroy){
                    
                    if(projectile._forceGenerator){
                        forceGeneratorRegistry.remove(projectile._forceGenerator, entity);
                    }
                
                    if(animation.has(impact)){
                        //Removing the shape removes the projectile from the physics simulation.
                        //This way the projectile won't affect other physics entities while the impact animation is playing.
                        //Removing a component is a safe operation, there's no need to check if it's present.
                        entity.remove(Shape);
                        //Removing the projectile component means that the projectile will no longer be processed unnecessarily while
                        //the impact animation is playing.
                        entity.remove(Projectile);
                        animation.play(impact, PROJECTILE_ANIMATION_IMPACT_PRIORITY, 1, destroyEntity, entity);
                    }else{
                        entity.destroy();
                    }
                }
            };
        })();
    
        return function(deltaTime){
            attackingEntities.iterate(function(entity){
                entity.get(Attacking)._attackCooldownRemaining -= deltaTime;
            });
            projectileEntities.iterate(updateProjectileEntity);
        };
    })();
    
    /**
     @method destroy
     */
    this.destroy = function(){
        inputSystem.unsubscribe('attack', attackEventCallback);
        aISystem.unsubscribe('attack', attackEventCallback);
        physicsSystem.unsubscribe('collisionResolved', collisionResolvedEventCallback);
        
        attackingEntities.destroy();
        projectileEntities.destroy();
    };
}