API Docs for:
Show:

File: gameplay\spell-casting\spells\System.js

/**
 System responsible for casting and handling spells.
 @class SpellSystem
 @constructor
 @param entitySystemManager {Manager} The entity system manager whose entities this system will be working on.
 @param assetManager {AssetManager} The manager is required, because spell assets are retrieved at runtime.
 @param healthSystem {HealthSystem}
 @param physicsSystem {PhysicsSystem}
 @param forceGeneratorRegistry {ForceGeneratorRegistry}
 @param collisionMasks {Object} An object holding bit masks used for determining collision categories.
 */
function SpellSystem(entitySystemManager, assetManager, healthSystem, physicsSystem, forceGeneratorRegistry, collisionMasks){

    var thisSpellSystem = this,
    
    //System variables.
        SPELL_CAST_ANIMATION_PRIORITY = 5,
        
        SPELL_SPRITE_Z_INDEX = 0,
        
    //Facing directions.
        left = 'left',
        right = 'right',
    
    //Color strings.
        cyan = 'c',
        magenta = 'm',
        yellow = 'y',
        white = 'w',
 
    //Spell effect names.
        movementSpeed = 'movementSpeed',
        jumpPower = 'jumpPower',
        resistance = 'resistance',
        shield = 'shield',
        damageOverTime = 'damageOverTime',
        healingOverTime = 'healingOverTime',
        attackSpeed = 'attackSpeed',
        attackEnchant = 'attackEnchant',
        
    //Animation names.
        castSpellLeft = 'castSpellLeft.',
        castSpellRight = 'castSpellRight.';
        
    /*
     *Templates for applied spell effects objects.
     *

     {
        name : 'movementSpeed',
        speedMultiplier : Number,
        duration : Number
     }
     
     {
        name : 'jumpPower',
        jumpPowerMultiplier : Number,
        duration : Number
     }
     
     {
        name : 'resistance',
        resistanceAmount : Number,
        resistanceType : String,
        duration : Number
     }
     
     {
        name : 'shield',
        absorbtionAmount : Number,
        duration : Number
     }
     
     {
        name : 'damageOverTime',
        damageInterval : Number,
        damageAmount : Number,
        damageType : String,
        duration : Number
     }
     
     {
        name : 'healingOverTime',
        healingInterval : Number,
        healingAmount : Number,
        duration : Number
     }
     
     {
        name : 'attackSpeed',
        speedMultiplier : Number,
        duration : Number
     }
     
     {
        name : 'attackEnchant',
        numberOfCharges : Number,
        spellTriggered : String,
        duration : Number
     }
     */
    
    //Applies the spell effects to the entity. The spell effects in the array sholud be in the template form.
    function applySpellEffects(entity, spellEffectArray){
        var spellEffectsComponent = entity.get(SpellEffects),
            spellEffect;
        
        if(spellEffectsComponent){
            for(var i=0; i<spellEffectArray.length; ++i){
                spellEffect = spellEffectArray[i];
                
                switch(spellEffect.name){
                    case movementSpeed :
                        spellEffectsComponent.applySpellEffect(new MovementSpeedSpellEffect(spellEffect.duration, spellEffect.speedMultiplier));
                        break;
                    case jumpPower : 
                        spellEffectsComponent.applySpellEffect(new JumpPowerSpellEffect(spellEffect.duration, spellEffect.jumpPowerMultiplier));
                        break;
                    case resistance : 
                        spellEffectsComponent.applySpellEffect(new ResistanceSpellEffect(spellEffect.duration, spellEffect.resistanceAmount, spellEffect.resistanceType));
                        break;
                    case shield : 
                        spellEffectsComponent.applySpellEffect(new ShieldSpellEffect(spellEffect.duration, spellEffect.absorbtionAmount));
                        break;
                    case damageOverTime : 
                        spellEffectsComponent.applySpellEffect(new DamageOverTimeSpellEffect(spellEffect.duration, spellEffect.damageInterval, spellEffect.damageAmount, spellEffect.damageType));
                        break;
                    case healingOverTime : 
                        spellEffectsComponent.applySpellEffect(new HealingOverTimeSpellEffect(spellEffect.duration, spellEffect.healingInterval, spellEffect.healingAmount));
                        break;
                    case attackSpeed : 
                        spellEffectsComponent.applySpellEffect(new AttackSpeedSpellEffect(spellEffect.duration, spellEffect.speedMultiplier));
                        break;
                    case attackEnchant : 
                        spellEffectsComponent.applySpellEffect(new AttackEnchantSpellEffect(spellEffect.duration, spellEffect.numberOfCharges, spellEffect.spellTriggered));
                        break;
                }
            }
        }
    }
    
    //=====Spell templates=====
    
    this.spellTemplates = {
        cyanMissile : {
            damageAmount : 100,
            damageType : cyan,
            appliesEffects : [{
                                name : movementSpeed,
                                speedMultiplier : 0,
                                duration : 2
                            },
                            {
                                name : jumpPower,
                                jumpPowerMultiplier : 0,
                                duration : 2
                            }],
            cooldown : 5,
            shape : new Circle(20),
            flightSpeed : 400,
            flightDuration : 3,
            spriteSheetPath : 'assets/example.spritesheet',
            spriteIndex : 2
        }
    };
    
    //=====Spell creation and scripts=====
    
    /*The "onUpdate" and "onCollision" scripts describe spell behavior during updates and
     *when they collide with something. The scripts will be assigned directly to components,
     *so "this" should be used as if it's pointing to the Spell component within the scripts.
     */
    
    var castCyanMissile = (function(){
        
        function onUpdateCyanMissile(deltaTime){
            //Update the flight duration.
            this._spellData.flightDurationRemaining -= deltaTime;
            //Mark the spell for destruction if the flight duration is up.
            if(this._spellData.flightDurationRemaining <= 0){
                this._destroy = true;
            }
        }
        
        function onCollisionCyanMissile(entityCollidedWith){
            var cyanMissileTemplate = thisSpellSystem.spellTemplates.cyanMissile;
            //Deal damage to the entity and apply the spell effects.
            healthSystem.damageEntity(entityCollidedWith, cyanMissileTemplate.damageAmount, cyanMissileTemplate.damageType);
            applySpellEffects(entityCollidedWith, cyanMissileTemplate.appliesEffects);
            //Mark the spell for destruction.
            this._destroy = true;
        }
        
        return function(castingEntity, castPoint, castDirection, friendCollideWith, foeCollideWith){
            var cyanMissileTemplate = thisSpellSystem.spellTemplates.cyanMissile,
                spriteSheetHandle,
            //Fill the spell data using the spell template data.
                spellData = {
                    flightDurationRemaining : cyanMissileTemplate.flightDuration
                },
                
            //Create the cyan missile.
                spellEntity = entitySystemManager.createEntity();
                
            spellEntity.add(new Position(castPoint.x, castPoint.y));
            spellEntity.add(new Collidable(0, 0, collisionMasks.spell, foeCollideWith | collisionMasks.wall));
            spellEntity.add(new Shape(cyanMissileTemplate.shape));
            switch(castDirection){
                case left :
                    spellEntity.add(new Movable(1, 0, false, -cyanMissileTemplate.flightSpeed, 0));
                    break;
                case right :
                    spellEntity.add(new Movable(1, 0, false, cyanMissileTemplate.flightSpeed, 0));
                    break;
            }
            spellEntity.add(new Spell(spellEntity, spellData, onUpdateCyanMissile, onCollisionCyanMissile));
            spriteSheetHandle = assetManager.get(cyanMissileTemplate.spriteSheetPath);
            if(spriteSheetHandle){
                spellEntity.add(new Sprite(spriteSheetHandle, cyanMissileTemplate.spriteIndex, SPELL_SPRITE_Z_INDEX));
            }
        };
    })();
    
    //=====Spell creation interface=====
    
    var spellCastingFunctions = {
        cyanMissile : castCyanMissile
    };
    
    /**
     Causes the entity to cast the specified spell. This function requires that the Position, SpellCasting and FacingDirection components
     are present. Additionally, if a WallGrappling component is present, the entity will cast the spell away from the wall it's grappling
     independently of the facing direction. This function plays animations. 'castSpellLeft.spellName' or 'castSpellRight.spellName' animation
     will play depending on the direction the spell was cast in. The 'spellName' after the dot in the animation name is the name of the spell cast.
     @method castSpell
     @param castingEntity {Entity}
     @param spellName {String}
     */
    this.castSpell = (function(){
        var castPoint = new Vector2D();
        
        return function(castingEntity, spellName){
            var spellCastingFunction = spellCastingFunctions[spellName],
                position = castingEntity.get(Position),
                spellCasting = castingEntity.get(SpellCasting),
                facingDirection = castingEntity.get(FacingDirection),
                wallGrappling = castingEntity.get(WallGrappling),
                animation = castingEntity.get(Animation),
                castDirection, triggeredAnimation;
                
            //Make sure that the required components are present on the casting entity and that the spell name is valid.
            if(!(spellCastingFunction && position && spellCasting && facingDirection)){
                return;
            }
                
            //Check if the spell is off cooldown.
            if(!spellCasting.canCastSpell(spellName)){
                return;
            }
            
            //If the entity has a wall grappling component and is grappling, then the spell will be
            //cast away from the grappling direction. If the entity is not grappling or does not have the grappling
            //component, then the spell will be cast as normal, in the direction the entity is facing.
            position.getPosition(castPoint);
            if(wallGrappling && wallGrappling.isGrapplingLeft()){
                castPoint.add(spellCasting._rightCastPoint);
                castDirection = right;
                triggeredAnimation = castSpellRight;
            }else if(wallGrappling && wallGrappling.isGrapplingRight()){
                castPoint.x -= spellCasting._rightCastPoint.x;
                castPoint.y += spellCasting._rightCastPoint.y;
                castDirection = left;
                triggeredAnimation = castSpellLeft;
            }else{
                switch(facingDirection.getFacingDirection()){
                    case left :
                        castPoint.x -= spellCasting._rightCastPoint.x;
                        castPoint.y += spellCasting._rightCastPoint.y;
                        castDirection = left;
                        triggeredAnimation = castSpellLeft;
                        break;
                    case right :
                        castPoint.add(spellCasting._rightCastPoint);
                        castDirection = right;
                        triggeredAnimation = castSpellRight;
                        break;
                }
            }
            
            //Cast the spell.
            spellCastingFunction(castingEntity, castPoint, castDirection, spellCasting._friendCollideWith, spellCasting._foeCollideWith);
            
            //Set the cooldown.
            spellCasting.setSpellCooldown(spellName, thisSpellSystem.spellTemplates[spellName].cooldown);
            
            //Trigger the animation.
            if(animation){
                animation.play(triggeredAnimation + spellName, SPELL_CAST_ANIMATION_PRIORITY);
            }
        };
    })();
    
    //=====Physics handling=====
    
    function collisionResolvedEventCallback(contactData){        
        var entityA = contactData.getEntityA(),
            entityB = contactData.getEntityB(),
            
        //If the first entity is a spell, pass the second entity to its "onCollision" script, if provided.
            spell = entityA.get(Spell);
        if(spell && spell._onCollisionScript && !spell._destroy){
            spell._onCollisionScript(entityB);
        }
        
        //If the second entity is a spell, pass the first entity to its "onCollision" script, if provided.
        spell = entityB.get(Spell);
        if(spell && spell._onCollisionScript && !spell._destroy){
            spell._onCollisionScript(entityA);
        }
    }
    
    physicsSystem.subscribe('collisionResolved', collisionResolvedEventCallback);
    
    var spellEntities = entitySystemManager.createAspect([Spell]),
        spellCastingEntities = entitySystemManager.createAspect([SpellCasting]);
    
    /**
     @method update
     @param deltaTime {Number} The time that passed since last update.
     */
    this.update = function(deltaTime){
        spellEntities.iterate(function(entity){
            var spell = entity.get(Spell);
            
            //If the spell was marked for destruction, destroy it, otherwise call the update script, if provided.
            if(spell._destroy){
                entity.destroy();
            }else if(spell._onUpdateScript){
                spell._onUpdateScript(deltaTime);
            }
        });
        
        spellCastingEntities.iterate(function(entity){
            var spellCasting = entity.get(SpellCasting);
            
            for(var spellName in spellCasting._spellCooldowns){
                spellCasting._spellCooldowns[spellName] -= deltaTime;
            }
        });
    }
    
    /**
     @method destroy
     */
    this.destroy = function(){
        physicsSystem.unsubscribe('collisionResolved', collisionResolvedEventCallback);
        
        spellEntities.destroy();
        spellCastingEntities.destroy();
    };
}