Fun with spells using Phaser
22 Dec 2016Summary
I’ve always love seeing spell effects in games, here’s an attempt to do a few ones.
Source
- gameState.js is common to all my Phaser demos
- demo.js is code specific to this demo
- spell.js is the isometric grid drawer you can just drop into your code
- player.js is the isometric grid drawer you can just drop into your code
- and the spells - magicbolt.js, firewall.js, lightningbolt.js, icecage.js, firestorm.js
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.
create
- called during the game creationperform
- called when casting the spell. Normally starts the animation and tweens.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.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:
- Burn marks that appear below the flames
- The crack that appears when a lightning bolt is cast
- The magic bolts that needs to always show in front of their targets.
Credits
- Knight and Zombie Sprites from Game Art 2D
- Spell Sprites from Pow Studios
- Spell Icons from OpenGameArt
- Thanks to lewster32 for the progress pie code
- To lewster32 again for the TweenTint function
- Thanks to codevinsky for the camera shake using it’s Juicy plugin
- Homing missile tutorial
-
- TexturePacker creator Andreas Loew without his tool for me to pack the sprite frames into a single spritesheet file, I would let my users download more files over the wire and slow down the loading process
- and of course the awesome HTML5 Phaser game framework by Richard Davey and his website
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();
}
});