Fun with spells using Phaser

Last modified: 07 Feb 2018

Summary

I’ve always love seeing spell effects in games, here’s an attempt to do a few ones.

Source

Creating the Spells

Because spells have common properties, it makes sense to build a base class to store them:

icon's x, y and image position
cooldown
duration

All spells that inherits from the Spell class needs to implement a few methods that defines their distinct behavior.

  1. create- called during the game creation
  2. perform- called when casting the spell. Normally starts the animation and tweens.
  3. update- some spells needs to be updated on every frame, like the magic bolt which homes in to their targets and every position needs to be updated.
  4. expire- some spells have durations. Eg, enchanments. This is called when a spell runs through its course so we can stop them.

Each spell has a fully commented code describing how each of them are created, effects shown and how they pick and move towards their targets.

Magic Bolt

3 bolts of pure magic that randomly find their targets. magicbolt.js

Firewall

A wall of flames that damages your enemies foolish enough to walk into it. firewall.js

Ice Cage

Freezes upto 3 targets. icecage.js

Lightning Bolt

Strikes a single target with intense energy. lightningbolt.js

Fire Storm

Rain fire down to your enemies. firestorm.js

Spell Cooldown

A progressive pie is rendered on top of each icon to show when the player will be able to cast the spell again. More on the pieprogress.js code.

About depth sorting and grouping

I have most of the sprites added in game.world so they automatically show correct z-ordering. I create Phaser.Group only if I need to control where the sprites should show up.

You can see these how I did these for the following:

Credits

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
'use strict';

class GameState extends Phaser.State {
  preload() {
    this.load.onLoadStart.add(this.loadStart, this);
    this.load.onFileComplete.add(this.fileComplete, this);
    this.load.onLoadComplete.add(this.loadComplete, this);
  }

  loadStart() {
    this.loadingText = this.add.text(20, this.world.height - 32, 'Loading...', { font: '20px Arial', fill: '#ffffff' });
  }

  fileComplete(progress, cacheKey, success, totalLoaded, totalFiles) {
    this.loadingText.setText('File Complete: ' + progress + '% - ' + totalLoaded + ' out of ' + totalFiles);
  }

  loadComplete() {
    game.world.remove(this.loadingText);

    this.time.advancedTiming = true;
  }

  create() {
    this.showDebug = false;
    game.input.keyboard.addKey(Phaser.KeyCode.D).onDown.add(() => {
      this.showDebug = !this.showDebug;
    });

    game.camera.x = game.world.centerX - game.width / 2;
  }

  createKeyboardMovement() {
    this.keyboardCursors = game.input.keyboard.createCursorKeys();
    this.moveSpeed = { x: 0, y: 0 }

    this.wasd = {
      up: game.input.keyboard.addKey(Phaser.Keyboard.W),
      down: game.input.keyboard.addKey(Phaser.Keyboard.S),
      left: game.input.keyboard.addKey(Phaser.Keyboard.A),
      right: game.input.keyboard.addKey(Phaser.Keyboard.D),
    };
  }

  createVirtualGamepad() {
    // create virtual gamepad
    let gamepad = game.plugins.add(Phaser.Plugin.VirtualGamepad)
    this.joystick = gamepad.addJoystick(60, game.height - 60, 0.5, 'gamepad');

    // plugin wants the creation of a button
    // but there is no usage for it here so i'm just going to hide it
    this.gamepadbutton = gamepad.addButton(game.width - 60, game.height - 60, 0.5, 'gamepad');
    this.gamepadbutton.visible = false;
  }

  goingLeft() {
    return this.keyboardCursors.left.isDown || this.wasd.left.isDown || (this.joystick && this.joystick.properties.left);
  }

  goingRight() {
    return this.keyboardCursors.right.isDown || this.wasd.right.isDown || (this.joystick && this.joystick.properties.right);
  }

  update() {
  }

  render() {
    game.debug.text(game.time.fps, 5, 14, '#00ff00')
  }

  loadSpriter(key) {
    if(!this.spriterLoader) this.spriterLoader = new Spriter.Loader();

    let spriterFile = new Spriter.SpriterXml(game.cache.getXML(key + 'Animations'));

    // process loaded xml/json and create internal Spriter objects - these data can be used repeatly for many instances of the same animation
    let spriter = this.spriterLoader.load(spriterFile);

    return new Spriter.SpriterGroup(game, spriter, key, key);
  }

  drawIsoGrid() {
    let isoGrid = new IsoGrid(game);
    isoGrid.drawGrid();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
'use strict';

class DemoState extends GameState {
  preload() {
    super.preload();

    game.load.image('bg', '/files/phaser/assets/bg/background2.png');
    game.load.image('burnMark', '/files/phaser/assets/spells/burnmark.png');
    game.load.image('groundCrack', '/files/phaser/assets/spells/groundcrack.png');

    game.load.image('iconMagicBolt', '/files/phaser/assets/icons/fireball-eerie-2.png');
    game.load.image('iconFireWall', '/files/phaser/assets/icons/Wall of Fire.png');
    game.load.image('iconLightningBolt', '/files/phaser/assets/icons/Thunder.png');
    game.load.image('iconIceCage', '/files/phaser/assets/icons/Blizzard.png');
    game.load.image('iconFireStorm', '/files/phaser/assets/icons/fire-arrows-3.png');

    game.load.atlas('player', '/files/phaser/assets/knight/atlas.png', '/files/phaser/assets/knight/atlas.json');
    game.load.atlas('zombie1', '/files/phaser/assets/zombie1/atlas.png', '/files/phaser/assets/zombie1/atlas.json');

    game.load.atlas('bolt', '/files/phaser/assets/spells/bolt/atlas.png', '/files/phaser/assets/spells/bolt/atlas.json');
    game.load.atlas('flame', '/files/phaser/assets/spells/fire/atlas.png', '/files/phaser/assets/spells/fire/atlas.json');
    game.load.atlas('ice', '/files/phaser/assets/spells/ice/atlas.png', '/files/phaser/assets/spells/ice/atlas.json');
  }
  create() {
    game.juicy = game.plugins.add(new Phaser.Plugin.Juicy(this));

    // add game background a group so it doesn't get sorted with the game.world
    // when we sort it during update
    let bgGroup = game.add.group();
    let bg = game.add.sprite(0, 20, 'bg');
    bg.anchor.setTo(0, 0);
    bg.scale.set(0.3);
    bgGroup.add(bg);

    this.player = new Player(80, 200, 'player');

    this.zombies = [];
    var offsetX = 120;
    var offsetY = 110;
    for(let y = 1; y < 4; y++) {
        for(let x = 1; x < 5; x++) {
            let posx = x * 100 + ((y % 2) ? 0 : 50) + offsetX;
            let posy = y * 50 + offsetY;
            this.createZombie(posx, posy);
        }
    }

    // icon position, icon key, cooldown, duration
    let magicBolt = new MagicBolt(50, 430, 'iconMagicBolt', 2500);
    let fireWall = new FireWall(130, 430, 'iconFireWall', 5000, 3000);
    let lightningBolt = new LightningBolt(210, 430, 'iconLightningBolt', 2000);
    let iceCage = new IceCage(290, 430, 'iconIceCage', 3000, 2000);
    let fireStorm = new FireStorm(370, 430, 'iconFireStorm', 6000);

    // store a reference for these because we need to call their update method
    this.magicBolt = magicBolt;
    this.fireStorm = fireStorm;

    // we need a reference to the player's position
    magicBolt.player = this.player;

    // for these spells, we need a reference to the list of zombies
    // so we can target them
    iceCage.zombies = this.zombies;
    magicBolt.zombies = this.zombies;
    lightningBolt.zombies = this.zombies;

    // create keyboard and touch inputs
    game.input.addPointer();
    this.enableInput(magicBolt, Phaser.KeyCode.ONE);
    this.enableInput(fireWall, Phaser.KeyCode.TWO);
    this.enableInput(lightningBolt, Phaser.KeyCode.THREE);
    this.enableInput(iceCage, Phaser.KeyCode.FOUR);
    this.enableInput(fireStorm, Phaser.KeyCode.FIVE);
  }

  enableInput(spell, keycode) {
    game.input.keyboard.addKey(keycode).onDown.add(() => {
      this.castSpell(spell);
    });

    spell.icon.inputEnabled = true;
    spell.icon.events.onInputDown.add((icon) => {
      this.castSpell(spell);
    });
  }

  castSpell(spell) {
    if(spell.active) {
      this.player.play('attack', 20, false);
      // only cast the spell after a certain ms the animation plays
      this.game.time.events.add(300, () => {
          spell.cast();
      });
    }
  }

  createZombie(x, y) {
    let zombie = game.add.sprite(x, y, 'zombie1');
    zombie.anchor.setTo(0.5, 0.5);

    var idle = zombie.animations.add('idle', [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]);
    var die = zombie.animations.add('die', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
    var raise = zombie.animations.add('raise', [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]);
    zombie.play('raise', 10, false);
    zombie.scale.setTo(-0.4, 0.4);
    // everytime a zombie dies, we raise them back
    die.onComplete.add(() => {
        zombie.play('raise', game.rnd.between(15, 20), false);
    });
    // set them to idle after they have risen
    // the random play speed gives us a nice effect
    // so that their movements are not sync with each other
    raise.onComplete.add(() => {
        zombie.play('idle', game.rnd.between(9, 20), true);
    });

    // store in our regular array
    this.zombies.push(zombie);
  }

  update() {
    game.world.sort('y', Phaser.Group.SORT_ASCENDING);

    this.magicBolt.update();
    this.fireStorm.update();
  }

  render() {
    super.render();

    game.debug.text('Press 1 - magic bolt', 400, 400);
    game.debug.text('2 - fire wall', 430, 416);
    game.debug.text('3 - lightning bolt', 430, 432);
    game.debug.text('4 - ice cage', 430, 448);
    game.debug.text('5 - fire storm', 430, 464);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
'use strict';

class Spell {
  // x, y - icon position
  // icon - key
  // cooldown - amount in seconds before the spell can be cast again
  // duration - amount in seconds for spells with duration eg, enchantments
  constructor(x, y, icon, cooldown, duration) {
    // we are not using the Phaser's group class
    // because we want the z-order to work with some sprites
    // so many of the spell sprites are added to the game.world
    // but it's still handy to put them all in an array so we can just iterate each
    this.group = []
    // boolean flag that enables/disable casting of the spell again
    this.active = true;
    // cooldown timer
    this.cooldown = cooldown;
    // some spells, like firewall have a duration
    this.duration = duration;

    // create each spell icons
    this.createIcon(x, y, icon);

    // and then create the necessary sprites, animations and emitters
    this.create();
  }

  // to be overridden by child classes
  create() {}
  perform() {}
  update() {}
  expire() {}

  cast() {
    // don't allow casting if cooldown is still in effect
    if(!this.active) return false;

    // perform each child spell actual behaviour
    this.perform();

    // don't allow casting again
    this.active = false;

    // activate the cooldown animation
    this.icon.pie.progress = 0;
    let tween = game.add.tween(this.icon.pie).to({progress: 1}, this.cooldown, Phaser.Easing.Quadratic.InOut, true, 0);
    tween.onComplete.add(() => {
      this.active = true;
    });

    // expire the spell if it has a duration
    if(this.duration) {
      game.time.events.add(this.duration, () => {
        this.expire();
      });
    }
  }

  createIcon(x, y, key) {
    // create a group for the icon so they don't get sorted along with the sprites in the game.world
    let iconGroup = game.add.group();

    // position the icon
    let icon = game.add.sprite(x, y, key);
    icon.anchor.setTo(0.5, 0.5);

    // create a progress pie on top of the icon
    let pie = new PieProgress(game, icon.x, icon.y, 40, '0x000000');
    pie.alpha = 0.5;

    // put a circle frame so we have rounded spell icons
    let g = game.add.graphics(0, 0);
    let radius = 40;
    g.lineStyle(20, 0x000000, 1);
    g.anchor.setTo(0, 0);
    let xo = icon.x;
    let yo = icon.y;
    g.moveTo(xo,yo + radius);
    for (let i = 0; i <= 360; i++){
      let x = xo+ Math.sin(i * (Math.PI / 180)) * radius;
      let y = yo+ Math.cos(i * (Math.PI / 180)) * radius;
      g.lineTo(x,y);
    }
    iconGroup.add(icon);
    iconGroup.add(g);
    iconGroup.add(pie);

    icon.pie = pie;
    this.icon = icon;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
'use strict';

class Player {
  constructor(x, y, key) {
    // just some common setup to show our hero
    let player = game.add.sprite(x, y, key);
    player.animations.add('idle', [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);

    let attack = player.animations.add('attack', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
    player.play('idle', 12, true);
    attack.onComplete.add(() => {
        player.play('idle', game.rnd.between(15, 20), false);
    });

    player.anchor.setTo(0.5, 0.5);
    player.scale.set(0.4);

    return player;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
'use strict';

class MagicBolt extends Spell {
  constructor(x, y, key, cooldown, duration) {
    super(x, y, key, cooldown, duration);
  }
  create() {
    // we create an actual Phaser.Group
    // because we want these sprites to
    // be on top of all the zombies
    this.boltGroup = game.add.group();

    // create 3 magic bolts
    for(var i = 0; i < 3; i++) {
      let bolt = game.add.sprite(0, 0, 'bolt');
      // add the animations in
      // one for moving and another when it hits their targets
      bolt.animations.add('move', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
      bolt.animations.add('sizzle', [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);
      bolt.scale.set(0.6);
      bolt.anchor.set(0.5);
      // don't show the bolts
      bolt.kill();
      bolt.SPEED = 400; // speed pixels/second
      bolt.TURN_RATE = 5; // turn rate in degrees/frame

      // enable physics because we'removing this sprite using velocity
      game.physics.enable(bolt, Phaser.Physics.ARCADE);

      this.group.push(bolt)
      this.boltGroup.add(bolt);
    }
  }
  perform() {
    this.group.forEach((bolt) => {
      // pick a random zombie to target
      let target = game.rnd.pick(this.zombies.filter(function(e) { return e.alive; }));
      // flag it so it doesn't get pick again
      target.alive = false;

      // random properties for each bolt
      bolt.revive();
      bolt.scale.set(0.6);
      bolt.play('move', 40, true);
      // make it come out from the player's sword
      bolt.x = this.player.x + 50;
      bolt.y = this.player.y - 30;

      // store a reference so we can access it later on
      bolt.target = target;

      // to start with, I want the bolts to randomly fly
      // towards the top of the game screen
      let startX = game.rnd.between(this.player.x, this.player.y + 100);
      let startY = game.rnd.between(0, 50);

      // set a fix speed regardless of the bolt's distance from their targets
      let duration = (game.math.distance(startX, startY, bolt.x, bolt.y) / bolt.SPEED) * 1000;

      // make the bolt face the target
      var targetAngle = game.math.angleBetween(
          bolt.x, bolt.y,
          startX, startY
        );
      bolt.rotation = targetAngle;

      // tween the bolt
      let tween = game.add.tween(bolt).to({ x: startX, y: startY }, duration, Phaser.Easing.Linear.None, true);
      tween.onComplete.add(() => {
        // once the bolt reaches the top of the game screen
        // we start making it home in to their target
        bolt.homeIn = true;
      });
    })
  }

  update() {
    // we want our bolts to be on top of their targets
    game.world.bringToTop(this.boltGroup);

    this.group.forEach((bolt) => {
      if(bolt.homeIn) {
        var targetAngle = game.math.angleBetween(
            bolt.x, bolt.y,
            bolt.target.x, bolt.target.y
        );
		    // Gradually (this.TURN_RATE) aim the missile towards the target angle
        if (bolt.rotation !== targetAngle) {
          // Calculate difference between the current angle and targetAngle
          var delta = targetAngle - bolt.rotation;

          // Keep it in range from -180 to 180 to make the most efficient turns.
          if (delta > Math.PI) delta -= Math.PI * 2;
          if (delta < -Math.PI) delta += Math.PI * 2;

          if (delta > 0) {
              // Turn clockwise
              bolt.angle += bolt.TURN_RATE;
          } else {
              // Turn counter-clockwise
              bolt.angle -= bolt.TURN_RATE;
          }

          // Just set angle to target angle if they are close
          if (Math.abs(delta) < game.math.degToRad(bolt.TURN_RATE)) {
              bolt.rotation = targetAngle;
          }
        }
        // Calculate velocity vector based on this.rotation and this.SPEED
        bolt.body.velocity.x = Math.cos(bolt.rotation) * bolt.SPEED;
        bolt.body.velocity.y = Math.sin(bolt.rotation) * bolt.SPEED;

        // distance check if it hits the target
        if(game.math.distance(bolt.x, bolt.y, bolt.target.x, bolt.target.y) < 20) {
            // play the hit animation
            bolt.target.play('die', game.rnd.between(9, 15), false);
            bolt.target.revive();

            // stop the bolt from moving
            bolt.homeIn = false;
            bolt.body.velocity.x = 0;
            bolt.body.velocity.y = 0;

            // show the hit animation with random size
            let size = game.rnd.realInRange(0.7, 1);
            bolt.scale.set(size);
            bolt.play('sizzle', 25, false, true);
        }
      }
    });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
'use strict';

class FireWall extends Spell {

  constructor(x, y, key, cooldown, duration) {
    super(x, y, key, cooldown, duration);
  }

  create() {
  	// create a group for the burn marks so it appears below all the flames
  	this.burnMarksGroup = game.add.group();

  	// create 8 flames that spawns in front of our hero
  	for(var i = 0; i < 8; i++) {
  		let offsetY = 180;
  		let posY = i * 15 + offsetY;
			let flame = game.add.sprite(game.rnd.between(150, 160), posY, 'flame');
      flame.alpha = 0;
      flame.animations.add('move', [15, 16, 17, 18, 19, 20]);
      flame.anchor.setTo(0.5, 0.5);

      // we don't need that much burn marks showing on the ground
      if(i % 2 > 0) {
				let mark = game.add.sprite(game.rnd.between(150, 160), posY + 10, 'burnMark');
				mark.alpha = 0;
				mark.anchor.setTo(0.5, 0.5);

				// store a reference
      	flame.mark = mark;

				this.burnMarksGroup.add(mark);
      }

      this.group.push(flame);
  	}
  }

  perform() {
    this.group.forEach((flame) => {
      flame.alpha = 1;
      // we show a growing flame
      flame.scale.set(0);

      // random growth size for each flame
      let size = game.rnd.realInRange(0.7, 1);
      game.add.tween(flame.scale).to({x: size, y: size}, game.rnd.between(300, 1000), Phaser.Easing.Linear.None, true);

      // play the animation
      flame.play('move', game.rnd.between(8, 15), true);

      if(flame.mark) {
      	// random angle and size for our burn marks
	      flame.mark.alpha = 1;
	      flame.mark.scale.set(0);
	      flame.mark.angle = game.rnd.between(0, 360);

	      let size = game.rnd.realInRange(0.15, 0.30);

        // show it a few moment after the flame shows up
	      game.time.events.add(500, () => {
	        game.add.tween(flame.mark.scale).to({x: size, y: size}, game.rnd.between(500, 1000), Phaser.Easing.Linear.None, true);
	      });
      }
    });
  }

  expire() {
  	// expire just fades them out
    this.group.forEach((flame) =>{
      game.add.tween(flame).to({alpha: 0}, 500, Phaser.Easing.Quadratic.InOut, true, 0);
      if(flame.mark)
      	game.add.tween(flame.mark).to({alpha: 0}, 500, Phaser.Easing.Quadratic.InOut, true, 0);
    });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
'use strict';

class IceCage extends Spell {

  constructor(x, y, key, cooldown, duration) {
    super(x, y, key, cooldown, duration);
  }

  create() {
    // the ice cage spells spawn 3 ice cages
    for(var i = 0; i < 3; i++) {
      let ice = game.add.sprite(0, 0, 'ice');
      ice.anchor.set(0.5);

      // add the animations in
      let summon = ice.animations.add('summon', [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]);
      ice.animations.add('idle', [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]);
      ice.animations.add('shatter', [48, 49, 50, 51, 52, 53, 54]);

      // when the summon completes
      // we play the idle animation
      summon.onComplete.add(() => {
          ice.play('idle', game.rnd.between(5, 10), true);
      });

      this.group.push(ice);
    }
  }

  perform() {
    this.group.forEach((ice) => {
      // pick a random target
      let target = game.rnd.pick(this.zombies.filter(function(e) { return e.alive; }));

      // flag it so we don't pick it out from the list again
      target.alive = false;

      // store a reference, we need to access it later on
      ice.target = target;

      // spawn the ice on the target's position
      // offset a bit so it appears below the target
      ice.x = target.x;
      ice.y = target.y + 20;

      // random size, revive and play the animation
      ice.scale.set(game.rnd.realInRange(0.5, 0.8));
      ice.alpha = 1;
      ice.revive();
      ice.play('summon', 25, false);

      // freeze the target after a few ms after the lightning plays
      game.time.events.add(300, () => {
        // tween the target to blue
        this.tweenTint(target, 0xffffff, 0x0000aa, 500);
        // and stop it's animation
        target.animations.paused = true;
      });
    });
  }

  expire() {
    this.group.forEach((ice) => {
      // after the spell expires, we play the shatter animation
      ice.play('shatter', 15, false, true);

      // can be re-targeted
      ice.target.alive = true;

      // tween the target back to its original color
      this.tweenTint(ice.target, 0x0000aa, 0xffffff, 500);

      // resume target's animation
      ice.target.animations.paused = false;
    });
  }

  tweenTint(obj, startColor, endColor, time) {
    // create an object to tween with our step value at 0
    var colorBlend = {step: 0};

    // create the tween on this object and tween its step property to 100
    var colorTween = game.add.tween(colorBlend).to({step: 100}, time);

    // run the interpolateColor function every time the tween updates, feeding it the
    // updated value of our tween each time, and set the result as our tint
    colorTween.onUpdateCallback(function() {
        obj.tint = Phaser.Color.interpolateColor(startColor, endColor, 100, colorBlend.step);
    });

    // set the object to the start color straight away
    obj.tint = startColor;

    // start the tween
    colorTween.start();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
'use strict';


class LightningBolt extends Spell {
    constructor(x, y, key, cooldown, duration) {
        super(x, y, key, cooldown, duration);
    }
    create() {
        let lightningBolt = game.add.sprite(0, 0, 'bolt');
        let animation = lightningBolt.animations.add('move', [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]);
        lightningBolt.scale.setTo(1.5, 1);
        lightningBolt.anchor.setTo(0.5, 0.5);
        lightningBolt.kill();

        animation.onComplete.add(() => {
          lightningBolt.target.tint = 0xffffff;
          lightningBolt.target.play('die', game.rnd.between(9, 15), false);
        });

        // create the ground crack decal
        // once again, we add this to its own group so it doesn't get affected
        // when we are sorting the world and this shows always below the zombie
        let groundCrackGroup = game.add.group();
        let groundCrack = game.add.sprite(0, 0, 'groundCrack');
        groundCrack.scale.set(0.2);
        groundCrack.anchor.setTo(0.5, 0.5);
        groundCrack.alpha = 0;
        groundCrackGroup.add(groundCrack);

        lightningBolt.crack = groundCrack;

        // there's a single instance of this so we don't need the array
        this.lightningBolt = lightningBolt;
    }
    perform() {
      // find a random zombie that is alive to target
      let zombie = game.rnd.pick(this.zombies.filter(function(e) { return e.alive; }));

      let lightningBolt = this.lightningBolt;
      lightningBolt.revive();
      lightningBolt.target = zombie;
      // a little variety where the starting position of the lightning
      lightningBolt.x = zombie.x + game.rnd.between(-20, 20);
      // just making sure it's on top of the zombie's head
      lightningBolt.y = zombie.y - (lightningBolt.height / 2) + 20; // offset

      // find the target angle to the zombie so the lightning's end is pointing at the zombie's position
      var targetAngle = game.math.angleBetween(
          lightningBolt.x, lightningBolt.y,
          zombie.x, zombie.y
      );
      lightningBolt.rotation = targetAngle;

      // play the animation
      lightningBolt.play('move', 20, false, true);

      // and make sure it stays on top
      game.world.bringToTop(lightningBolt);

      // add some effect after the bolts hit the zombie
      game.time.events.add(150, () => {
        // show a crack on the ground
        lightningBolt.crack.angle = game.rnd.between(0, 360);
        lightningBolt.crack.alpha = 1;
        lightningBolt.crack.position.set(zombie.x, zombie.y + 30);
        // fade out the crack
        game.add.tween(lightningBolt.crack).to({alpha: 0}, 1000);

        // blacken the zombie to show our zombie got fried from the bolt
        zombie.tint = 0x555555;
        // shake and flash the zombie
        game.add.tween(zombie).to({x: lightningBolt.x - 10}, 50, Phaser.Easing.Bounce.InOut, true, 0, 0, true);
        game.add.tween(zombie).to({alpha: 0.2 }, 200, Phaser.Easing.Bounce.InOut, true, 0, 0, true);
      });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
'use strict';

class FireStorm extends Spell {

  constructor(x, y, key, cooldown, duration) {
    super(x, y, key, cooldown, duration);
  }

  create() {
    for(var i = 0; i < 5; i++) {
      // setup an emitter for each fireball for the smoke trail
      let smokeTrail = game.add.emitter(0, 0, 400);
      // limit the emitter width
      smokeTrail.width = 10;
      // limit the amount of speed and rotation
      smokeTrail.setXSpeed(-30, -10);
      smokeTrail.setYSpeed(-50, -25);
      smokeTrail.setRotation(10, 20);
      // random size, alpha
      smokeTrail.setScale(1, 1.5);
      smokeTrail.setAlpha(0.3, 0.7, 3000);
      // no gravity, we want them going up
      smokeTrail.gravity = 0;
      // just pull from the existing spritesheet
      smokeTrail.makeParticles('flame', ['zsmoke.png']);
      // start the emitter but pause it until the spell is cast
      smokeTrail.start(false, game.rnd.between(1000, 4000), 150);
      smokeTrail.on = false;

      // our fireball sprite
      let fireball = game.add.sprite(0, 0, 'flame');
      fireball.scale.set(game.rnd.realInRange(0.5, 1));
      fireball.anchor.set(0.5);
      // there 2 animations to play
      // 1. while it's moving and 2. when it hits the ground
      let move = fireball.animations.add('move', [0, 1, 2, 3, 4, 5]);
      let hit = fireball.animations.add('hit', [6, 7, 8, 9, 10, 11, 12, 13, 14]);

      // we're adding another animation once it hits the ground
      // but we need to create a separate sprite for the smoke animation
      // because we play this together with the fireball hit animation
      let smoke = game.add.sprite(0, 0, 'flame');
      smoke.animations.add('smoke', [21, 22, 23, 24, 25, 26, 27, 28, 29, 30]);
      smoke.anchor.set(0.5);
      smoke.scale.setTo(fireball.scale.x, fireball.scale.y);

      // we start with these hidden away
      fireball.kill();
      smoke.kill();

      // store reference
      fireball.smoke = smoke;
      fireball.smokeTrail = smokeTrail;

      this.group.push(fireball);
    }
  }

  perform() {
    this.group.forEach((fireball) => {
      // we start with reving and resuming the emitters
      fireball.revive();
      fireball.smokeTrail.on = true;
      // play the animation
      fireball.play('move', game.rnd.between(15, 25), true);
      // start from the upper left of the game screen
      fireball.x = game.rnd.between(0, 100);
      fireball.y = 0;

      // randomly hit where the zombies are standing
      var targetX = game.rnd.between(200, game.world.width);
      var targetY = game.rnd.between(200, 350);
      // make it face and target the ground
      var targetAngle = game.math.angleBetween(
          fireball.x, fireball.y,
          targetX, targetY
        );
      fireball.rotation = targetAngle;

      // tween the movement
      var tween = game.add.tween(fireball).to({ x: targetX, y: targetY }, game.rnd.between(1000, 2000), Phaser.Easing.Linear.None, true);
      tween.onComplete.add(() => {
        // when the fireball hits the ground, we do the following:

        // show the smoke sprite animation
        fireball.smoke.revive();
        fireball.smoke.position = fireball.position;

        // play the fireball hit animation
        fireball.play('hit', game.rnd.between(15, 20), false, true);
        // play the smoke animation
        fireball.smoke.play('smoke', game.rnd.between(10, 15), false, true);
        // turn off the somke trail emitter;
        fireball.smokeTrail.on = false;
        // and we shake the screen for each fireball that hits the ground
        game.juicy.shake();
      });
    });
  }

  update() {
    this.group.forEach((fireball) => {
      // for each smoke trail particle, we slowly fade them out
      fireball.smokeTrail.forEachAlive( (particle) => {
        // by using each particle's lifespan
        particle.alpha = game.math.clamp(particle.lifespan / 1000, 0, 0.5);
      });

      // here we're making sure that each emitted particle from the emitter
      // emits from where the fireball position currently is
      fireball.smokeTrail.x = fireball.x;
      fireball.smokeTrail.y = fireball.y;
    });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var PieProgress = function(game, x, y, radius, color, angle) {
    this._radius = radius;
    this._progress = 1;
    this.bmp = game.add.bitmapData(radius * 2, radius * 2);
    Phaser.Sprite.call(this, game, x, y, this.bmp);

    this.anchor.set(0.5);
    this.angle = angle || -90;
    this.color = color || "#fff";
    this.updateProgress();
}

PieProgress.prototype = Object.create(Phaser.Sprite.prototype);
PieProgress.prototype.constructor = PieProgress;

PieProgress.prototype.updateProgress = function() {
    var progress = this._progress;
    progress = Phaser.Math.clamp(progress, 0.00001, 0.99999);

    this.bmp.clear();
    this.bmp.ctx.fillStyle = this.color;
    this.bmp.ctx.beginPath();
    this.bmp.ctx.arc(this._radius, this._radius, this._radius, 0, (Math.PI * 2) * progress, true);
    this.bmp.ctx.lineTo(this._radius, this._radius);
    this.bmp.ctx.closePath();
    this.bmp.ctx.fill();
    this.bmp.dirty = true;
}

Object.defineProperty(PieProgress.prototype, 'radius', {
    get: function() {
        return this._radius;
    },
    set: function(val) {
        this._radius = (val > 0 ? val : 0);
        this.bmp.resize(this._radius * 2, this._radius * 2);
        this.updateProgress();
    }
});

Object.defineProperty(PieProgress.prototype, 'progress', {
    get: function() {
        return this._progress;
    },
    set: function(val) {
        this._progress = Phaser.Math.clamp(val, 0, 1);
        this.updateProgress();
    }
});

Look! I have new stuff coming right up about... but really, I rarely send out emails! Promise.