Turn Based Battle System using Phaser
23 Jan 2018Source
- gameState.js is common to all my Phaser demos
- demo.js are where the important parts are
- unit.js contains code that creates the knights and trolls, as well as their code to play out an
attack
orhurt
- init.js are all related to creating the different sprites for the scene and other creating of the queue
- stablePriorityQueue.js if you want to see the open source priority queue data structure I used and the additional
update
function I had to put in
Summary
Our heroes faces a group of trolls. Everyone takes turn to attack, following a priority queue system. Each unit has a priority
number and a speed
factor that decides when their next turn will be.
Queue System
The idea is each one will have a priority number. Each attack bumps up the attacker’s priority number to the max and every cycle(meaning every attack scene we perform) we decrease each unit’s priority number with their speed. The unit with the lowest number attacks next. We will use a priority queue to store these information.
Steps are as follows:
create()
- Create the queue and put everyone in, with all but one hero’s priority number randomly set to simulate combat initiative.create()
- One hero will have his priority number initially set to 1 so our player goes to attack first.updateQueue() or in onInputDown()
- When an attack plays out, we take out the first one in the queue.attack()
- Perform the attack scene and increase the priority of the attacker.updateQueue()
- When we update the queue, we decrease each unit’s priority.updateQueue()
- Add the one we took out earlier back into the queue
You’ll have to play around with max priority number. The idea is that if one has the same priority as the next one, it doesn’t put that unit anywhere ahead of it. This provides a consistent way of telling our players who the next attacker will be. Putting a very high max for your priority number will sometimes make a unit jump in front of everyone else the next turn.
Visual Queue
We provide a visual feedback to our players so they are more informed and can
strategize on their next move. We pull the head from the unit’s atlas and
create the portrait using a new sprite
and attach it to a different sprite
group
, just so we can move position them altogether.
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
createPortrait() {
let spriter = this.spriter
// create a bordered portrait
// image head pulled from the atlas
let portrait = game.add.image(0, 0, spriter.entity.name, 'head')
let border = game.add.graphics(0, 0)
border.lineStyle(10, 0xffffff)
border.drawRect(0, 0, portrait.width, portrait.height)
portrait.addChild(border)
// scale down
portrait.width = 70
portrait.height = 70
// we want to show their priority number
// style the text with a translucent background fill
let style = { font: "20px Arial", fill: "#ffffff", backgroundColor: "rgba(0, 0, 0, 0.8)" }
let text = game.add.text(0, 0, this.priority, style)
// don't scale with the portrait
text.setScaleMinMax(1, 1);
// and show it to the top left
text.anchor.set(0)
portrait.addChild(text)
// storing references
portrait.text = text
this.portrait = portrait
}
When it comes to showing them, we pull the array from our queue and loop through each, setting their position in the sprite group.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
updateVisualQueue() {
for(let i = 0; i < this.queue.array.length; i++) {
// pull the portrait from the unit
let unit = this.queue.array[i].value
let portrait = unit.portrait;
// add a little margin to set them apart
let posx = i * (portrait.width + 5)
// for the portrait we're adding back in, we just want it to popout and do a fade-in
if(portrait.alpha == 0) {
portrait.x = posx
this.game.add.tween(portrait).to( { alpha: 1 }, 500, Phaser.Easing.Linear.None, true)
} else {
// all the rest moves
this.game.add.tween(portrait).to( { x: posx }, 300, Phaser.Easing.Linear.None, true)
}
// show their priority number
portrait.text.setText(' ' + unit.priority + ' ')
}
}
Attack Scene
Our attack scene will start with an attacker and a target, sized up and background dimmed — to provide a more dramatic and engaging scene to our players.
We start with taking away the controls from the player, as we need the attack scene to play out completely before taking inputs again.
The second step is we dim the background by using a Phaser.BitmapData that is black filled with reduced opacity.
1
2
3
4
5
6
let bmd = game.add.bitmapData(game.width, game.height)
bmd.ctx.fillStyle = "rgba(0, 0, 0, 0.7)"
bmd.ctx.fillRect(0,0, game.width, game.height)
this.attackSceneBg = game.add.sprite(0, 0, bmd)
this.attackSceneBg.alpha = 0
The attackSceneBg.alpha
setting is applied to the sprite and not to the bitmap, so it doesn’t show up when the game starts and we can apply a fade effect later on.
Next we start setting up the attacker and the target by sizing them up, repositioning and showing some animation.
1
2
3
4
5
6
7
8
9
10
11
12
13
attack() {
let spriter = this.spriter
// almost double up their size
// note that we are not setting them, but instead multiplying them to the existing value
spriter.scale.x *= 1.75
spriter.scale.y *= 1.75
// start on the center of the game, offset (and some) by the width of the attacker
spriter.x = game.world.centerX - spriter.width - 100
// play a quick animation
spriter.setAnimationSpeedPercent(200)
spriter.playAnimationByName('ATTACK')
}
The same applies to the target, although we will need to consider the sequence of actions.
- Move the attacker and target to a different sprite group so they show on top of the dimmed background we created earlier.
- Both attacker and targets sizes up.
- Attacker initiate the ATTACK animation.
- Wait halfway through the attack animation before we initiate the HURT animation on the target.
- Show blood effect at the center of the target.
- Wait for a second to allow all the steps above to play out, before we reset the scene.
- Wait another second, so the reset scene completely plays out before giving back control to our players.
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
hurt(blood) {
let spriter = this.spriter
// almost double up their size
// note that we are not setting them, but instead multiplying them to the existing value
spriter.scale.x *= 1.75
spriter.scale.y *= 1.75
// start on the center of the game, offset (and some) by the width of the attacker
spriter.x = game.world.centerX - spriter.width - 100
// wait for a bit for the attacker's ATTACK animation to play out a bit
game.time.events.add(300, () => {
// and just about time the attack animation lands it's blow
// we play the target's HURT animation
spriter.setAnimationSpeedPercent(200)
spriter.playAnimationByName('HURT')
// shake the camera
game.juicy.shake(15)
// using the spriter's position
// we can more or less center the blood effect at the unit's body
let x = spriter.x;
let y = spriter.y
blood.position.set(x, y)
// show the blood effect once
blood.visible = true
blood.play('blood', 15, false)
})
}
Resetting the scene is simply restoring the original position and size of the sprites and fading out the background we laid on top. That’s where the alpha property come in handy from the sprite. We also want to remove the attacker’s portrait from the visual queue and put the sprites back to their original sprite group.
1
2
3
4
5
6
7
8
9
10
11
12
// restore starting position, size and animation
attacker.restoreOriginal()
target.restoreOriginal()
// fade out the background we put on top
game.add.tween(this.attackSceneBg).to( { alpha: 0 }, 300, Phaser.Easing.Linear.None, true)
// and remove the portrait from the visual queue
game.add.tween(attacker.portrait).to( { alpha: 0 }, 500, Phaser.Easing.Linear.None, true)
// move back to their original group
this.spritesGroup.add(attacker.spriter)
this.spritesGroup.add(target.spriter)
Please see demo.js for a complete rundown of the attack
function.
And finally we update the queue, for our next attacker…
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
updateQueue() {
// everytime we initiate an attack
// we descrease their each unit's priority number
// and cap it to 1, this way each unit will fall in line
// when both have the same priority number
this.monsters.forEach((monster) => {
monster.priority = game.math.max(1, monster.priority - monster.speed)
})
this.heroes.forEach((hero) => {
hero.priority = game.math.max(1, hero.priority - hero.speed)
})
// add the unit we took out earlier, back into the end of the queue
this.queue.add(this.attackingUnit)
// updates the queue array
this.queue.update()
// and show the contents of the queue to our player
this.updateVisualQueue()
// peek and find out whose unit is the next one
if(this.queue.peek().name.includes('knight')) {
// and we can safely bring control back to our player
this.stopInput = false
} else {
// if not - randomly attack one of the player's hero
this.attackingUnit = this.queue.poll()
this.attack(this.attackingUnit, this.heroes[game.rnd.between(0,2)])
}
}
Notes
- there is a global
ANIM_SPEED
in the code, this let me easily change the entire animation of the game by multiplying everything by this factor. Slow-mo anyone?
Inspirations
- Lord of Xulima for the visual queue, where players see whose turn will be next.
- Darkest Dungeon for both the battle and attack scene setup. They blur the background and do a little bit of tilting/zooming when an attack plays out. My equivalent is just putting an overlay and sizing up the sprites, with a little screen shake.
Thank you…
- to craftpix.net, for their knights, trolls and the game background
- stable priority queue data structure from computer science professor Daniel Lemire and his explanation
- camera shake from the juicy plugin
- blood effect from pow studios
- the spriter class I use for the skelatal animation. This class just works!
- 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
- the awesome phaser.io 2D game framework by Richard Davey
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
var ANIM_SPEED = 1
class DemoState extends GameState {
preload() {
super.preload()
;['knight', 'troll'].forEach(character => {
for(let i = 1; i < 4; i++) {
let name = character + i
this.load.atlas(name, '/files/phaser/' + name + '/atlas.png', '/files/phaser/' + name + '/atlas.json')
this.load.xml(name + 'Animations', '/files/phaser/' + name + '/animations.scml')
}
})
game.load.atlas('effects', '/files/phaser/assets/effects/blood.png', '/files/phaser/assets/effects/blood.json')
game.load.image('bg', '/files/phaser/bg/platformer_background_4.png')
}
create() {
game.juicy = game.plugins.add(new Phaser.Plugin.Juicy(this))
super.create()
// some loop will be done for monsters only, some for our player's heroes only
this.monsters = []
this.heroes = []
// the sequence of creation of the sprites are important
// so they come out with the correct z-index order
this.createBackground()
// we are storing all the sprites in a group, so we can have sort their z-index automatically
this.spritesGroup = game.add.group()
// start from center and adjust a bit
let startY = game.world.centerY + 70
// a little change on each y position so they are sorted automatically during update()
// and invert the trolls to face the heroes
let monster1 = new Unit({x: 550, y: startY, scale: -0.325, priority: 8, speed: 3, spriter: this.loadSpriter('troll1')})
let monster2 = new Unit({x: 650, y: startY - 0.01, scale: -0.325, priority: 5, speed: 1, spriter: this.loadSpriter('troll2')})
let monster3 = new Unit({x: 750, y: startY - 0.02, scale: -0.325, priority: 2, speed: 1, spriter: this.loadSpriter('troll3')})
// so we can reference them back
this.monsters.push(monster1, monster2, monster3)
// and add them in the group for sorting
this.monsters.forEach((monster) => this.spritesGroup.add(monster.spriter))
// same stuff with our heroes
startY = startY + 40
let hero1 = new Unit({x: 230, y: startY, scale: 0.125, priority: 1, speed: 5, spriter: this.loadSpriter('knight1'), introDelay: 0})
let hero2 = new Unit({x: 140, y: startY - 0.01, scale: 0.125, priority: 4, speed: 2, spriter: this.loadSpriter('knight2'), introDelay: 500})
let hero3 = new Unit({x: 50, y: startY - 0.02, scale: 0.125, priority: 3, speed: 1, spriter: this.loadSpriter('knight3'), introDelay: 1000})
this.heroes.push(hero1, hero2, hero3)
this.heroes.forEach((hero) => this.spritesGroup.add(hero.spriter))
// the attack scene background comes on top of the game bg and the units
this.createAttackSceneBg()
// we will have a group that will render on top of the attack scene bg
// so we can have our sprite showing on top when we move them in here
this.attackGroup = game.add.group()
// blood effect create afterwards so it's always on top of everything else
this.createBloodEffect()
// boolean flag to prevent input from our player
this.stopInput = false
this.initInput()
game.input.keyboard.addKey(Phaser.KeyCode.E).onDown.add(()=> {
if(this.stopInput) return
this.restartIntro()
})
// creates the data structure and the portraits
this.createQueue()
// updates the portrait's order in the visual queue
this.updateVisualQueue()
// let's roll them out
this.restartIntro()
}
attack(attacker, target) {
// prevent further inputs from our player
// until the simualation finishes
this.stopInput = true
// reduce it's priority in the queue
attacker.priority = Math.min(8, attacker.priority + 8)
// make sure they show on top of the attack scene background
this.attackGroup.add(target.spriter)
this.attackGroup.add(attacker.spriter)
// we now go to the visual stuff...
// dim the battle scene
this.attackSceneBg.alpha = 1
// initiate the attack which takes about a few ms
attacker.attack()
// initiate the hurt after a few ms after the attack
target.hurt(this.blood)
// wait for a second for all the steps to play out above
// plus a little extra for that 'moment feel' for our players
game.time.events.add(1000 * ANIM_SPEED, () => {
// restore starting position, size and animation
attacker.restoreOriginal()
target.restoreOriginal()
// fade out the background we use here
game.add.tween(this.attackSceneBg).to( { alpha: 0 }, 300 * ANIM_SPEED, Phaser.Easing.Linear.None, true)
// and remove the portrait from the visual queue
game.add.tween(attacker.portrait).to( { alpha: 0 }, 500 * ANIM_SPEED, Phaser.Easing.Linear.None, true)
// and wait for another second for the reset scene to completely play out
game.time.events.add(300 * ANIM_SPEED, () => {
// restore to their original group
this.spritesGroup.add(attacker.spriter)
this.spritesGroup.add(target.spriter)
})
game.time.events.add(1000 * ANIM_SPEED, () => {
// it's time to update the queue and return control back to our player
this.updateQueue()
})
})
}
updateQueue() {
// everytime we initiate an attack
// we descrease their each unit's priority number
// and cap it to 1, this way each unit will fall in line
// when both have the same priority number
this.monsters.forEach((monster) => {
monster.priority = game.math.max(1, monster.priority - monster.speed)
})
this.heroes.forEach((hero) => {
hero.priority = game.math.max(1, hero.priority - hero.speed)
})
// add the unit we took out earlier, back into the end of the queue
this.queue.add(this.attackingUnit)
// updates the queue array
this.queue.update()
// and show the contents of the queue to our player
this.updateVisualQueue()
// peek and find out whose unit is the next one
if(this.queue.peek().name.includes('knight')) {
// and we can safely bring control back to our player
this.stopInput = false
} else {
// if not - randomly attack one of the player's hero
this.attackingUnit = this.queue.poll()
this.attack(this.attackingUnit, this.heroes[game.rnd.between(0,2)])
}
}
updateVisualQueue() {
for(let i = 0; i < this.queue.array.length; i++) {
// pull the portrait from the unit
let unit = this.queue.array[i].value
let portrait = unit.portrait;
// add a little margin to set them apart
let posx = i * (portrait.width + 5)
// for the portrait we're adding back in, we just want it to popout and do a fade-in
if(portrait.alpha == 0) {
portrait.x = posx
this.game.add.tween(portrait).to( { alpha: 1 }, 500 * ANIM_SPEED, Phaser.Easing.Linear.None, true)
} else {
// all the rest moves
this.game.add.tween(portrait).to( { x: posx }, 300 * ANIM_SPEED, Phaser.Easing.Linear.None, true)
}
// show their priority number
portrait.text.setText(' ' + unit.priority + ' ')
}
}
update() {
// needed so our spriter animates
this.heroes.forEach((hero) => hero.spriter.updateAnimation())
this.monsters.forEach((monster) => monster.spriter.updateAnimation())
// tint monsters when player hovers their mouse on them
// we will have to pull out all the sprites and tint individually
this.monsters.forEach((monster) => {
monster.spriter.children.forEach((sprite) => {
sprite.tint = 0xffffff
})
if(!this.stopInput) {
// using for..of so we can break after we spotted the sprite
for(let sprite of monster.spriter.children) {
if(sprite.input.pointerOver()) {
monster.spriter.children.forEach((sprite) => sprite.tint = 0xff3333 )
break
}
}
}
})
// sort the sprites base on their y position
this.spritesGroup.sort('y', Phaser.Group.SORT_ASCENDING)
}
restartIntro() {
// make sure sequence finishes before restoring input back to our players
this.stopInput = true
game.time.events.add(2000 * ANIM_SPEED, () => this.stopInput = false )
// set the units starting position off the screen
let introx = game.width - this.monsters[0].spriter.width
this.monsters[0].restartIntro(introx, 0)
this.monsters[1].restartIntro(introx, 500)
this.monsters[2].restartIntro(introx, 1000)
introx = -150
this.heroes[0].restartIntro(introx, 0)
this.heroes[1].restartIntro(introx, 500)
this.heroes[2].restartIntro(introx, 1000)
}
}
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
DemoState.prototype.createBackground = function() {
game.stage.backgroundColor = "#50695A";
let bg = game.add.sprite(0, 0, 'bg');
bg.anchor.set(0);
bg.scale.set(0.45);
}
DemoState.prototype.createAttackSceneBg = function() {
let bmd = game.add.bitmapData(game.width, game.height)
bmd.ctx.fillStyle = "rgba(0, 0, 0, 0.7)"
bmd.ctx.fillRect(0,0, game.width, game.height)
this.attackSceneBg = game.add.sprite(0, 0, bmd)
this.attackSceneBg.alpha = 0
}
DemoState.prototype.createBloodEffect = function() {
let blood = game.add.sprite(0, 0, 'effects')
// anchor is based on the pivot_x and pivot_y from the spriter frames
// eg, <file id="3" name="3_body_" width="440" height="484" pivot_x="0" pivot_y="1"/>
// this puts the blood effect centered at the unit
blood.anchor.set(0.75, 0.5)
blood.scale.setTo(1.3, 1.3)
blood.visible = false
// pull out the frames from the spritesheet
let bloodEffect = blood.animations.add('blood', [0, 1, 2, 3, 4, 5])
// and automatically hide it after each play
bloodEffect.onComplete.add(() => {
blood.visible = false
})
this.blood = blood
}
DemoState.prototype.createQueue = function() {
// create our data structure to store our unit's priority numbers
this.queue = new StablePriorityQueue(function(a, b) {
// we want the lowest number to go first
return a.priority - b.priority
})
this.heroes.forEach((hero) => this.queue.add(hero))
this.monsters.forEach((monster) => this.queue.add(monster))
// the update function isn't really part of the original source
// but the add() doesn't update the queue right away
// and our game needs to show an updated queue to our player
// the function is very simple, it just drains the queue array and adds them back in
this.queue.update()
// assemble the sprite group for our portraits
this.queueGroup = this.add.group()
this.heroes.forEach((hero) => this.queueGroup.add(hero.portrait))
this.monsters.forEach((monster) => this.queueGroup.add(monster.portrait))
// positition it above the units
this.queueGroup.y = 50
// calculate the entire width by using the portrait + margin
let portraitWidth = this.queueGroup.children[0].width + 5
let totalWidth = portraitWidth * this.queueGroup.children.length
// so we can position it centered on the screen
this.queueGroup.x = this.world.centerX - totalWidth / 2
// because we're going to pop and add it back in
this.attackingUnit = null
}
DemoState.prototype.initInput = function() {
this.monsters.forEach((monster) => {
monster.spriter.children.forEach((sprite) => {
sprite.inputEnabled = true;
sprite.events.onInputDown.add(() => {
if(this.stopInput) return
this.attackingUnit = this.queue.poll()
this.attack(this.attackingUnit, monster)
})
})
})
}
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
class Unit {
// named parameters so it's easy to identify when creating instance of this
constructor({ x = 0, y = 0, scale = 1, priority = 8, speed = 1, spriter = null }) {
// store a reference to the spriter instance
this.spriter = spriter
// just storing the name, for referencing purpose
this.name = spriter.entity.name
// make sure we don't turn the unit upside down
spriter.scale.set(scale, Math.abs(scale))
// store starting position and scale, so we can restore them back to their original
this.sx = x
this.sy = y
this.sscale = scale
// keep track of where it is in the priority queue
this.priority = priority
// modifier that updates the priority of each unit after every scene
this.speed = speed
this.createPortrait()
}
createPortrait() {
let spriter = this.spriter
// create a bordered portrait
// image head pulled from the spritesheet
let portrait = game.add.image(0, 0, spriter.entity.name, 'head')
let border = game.add.graphics(0, 0)
border.lineStyle(10, 0xffffff)
border.drawRect(0, 0, portrait.width, portrait.height)
portrait.addChild(border)
// scale down
portrait.width = 70
portrait.height = 70
// we want to show their priority number
// style the text with a translucent background fill
let style = { font: "20px Arial", fill: "#ffffff", backgroundColor: "rgba(0, 0, 0, 0.8)" }
let text = game.add.text(0, 0, this.priority, style)
// don't scale with the portrait
text.setScaleMinMax(1, 1);
// and show it to the top left
text.anchor.set(0)
portrait.addChild(text)
// storing references
portrait.text = text
this.portrait = portrait
}
attack() {
let spriter = this.spriter
// almost double up their size
spriter.scale.x *= 1.75
spriter.scale.y *= 1.75
// start on the center of the game, offset (and some) by the width of the attacker
spriter.x = game.world.centerX - spriter.width - 100
// play a quick animation
spriter.setAnimationSpeedPercent(200 / ANIM_SPEED)
spriter.playAnimationByName('ATTACK')
}
hurt(blood) {
let spriter = this.spriter
// almost double up their size
// note that we are not setting them, but instead multiplying them to the existing value
spriter.scale.x *= 1.75
spriter.scale.y *= 1.75
// start on the center of the game, offset (and some) by the width of the attacker
spriter.x = game.world.centerX - spriter.width - 100
// wait for a bit for the attacker's ATTACK animation to play out a bit
game.time.events.add(300 * ANIM_SPEED, () => {
// and just about time the attack animation lands it's blow
// we play the target's HURT animation
spriter.setAnimationSpeedPercent(200 / ANIM_SPEED)
spriter.playAnimationByName('HURT')
// shake the camera
game.juicy.shake(15)
// using the spriter's position
// we can more or less center the blood effect at the unit's body
let x = spriter.x;
let y = spriter.y
blood.position.set(x, y)
// show the blood effect once
blood.visible = true
blood.play('blood', 15 / ANIM_SPEED, false)
})
}
restartIntro(introx, delay) {
let spriter = this.spriter
// set the starting position, which is outside the game extents
spriter.position.set(introx, this.sy)
// just some numbers so depending on how fast the unit is moving
// the animation speed is proportion
spriter.setAnimationSpeedPercent((350 - (delay / 200) * 40) / ANIM_SPEED)
spriter.playAnimationByName('RUN')
// fixed moving duration for everyone during intro scene
let tween = game.add.tween(spriter).to( { x: this.sx }, 1000 * ANIM_SPEED, Phaser.Easing.Linear.None, true, delay)
tween.onComplete.add(() => {
// random animatino speed for idle
// so they don't move altogether the same on the game else they'll look funny
spriter.setAnimationSpeedPercent(game.rnd.between(30, 70))
spriter.playAnimationByName('IDLE')
})
}
restoreOriginal() {
let spriter = this.spriter
// using the starting position and scale we stored in the constructor
// we use them to restore the units back to their original position and size
let duration = 300 * ANIM_SPEED
game.add.tween(spriter).to( { x: this.sx }, duration, Phaser.Easing.Linear.None, true)
game.add.tween(spriter.scale).to( { x: this.sscale, y: Math.abs(this.sscale) }, duration, Phaser.Easing.Linear.None, true)
spriter.setAnimationSpeedPercent(game.rnd.between(30, 70))
spriter.playAnimationByName('IDLE')
}
}
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
/**
* StablePriorityQueue.js : a stable heap-based priority queue in JavaScript.
* (c) the authors
* Licensed under the Apache License, Version 2.0.
*
* Stable heap-based priority queue for modern browsers and JavaScript engines.
*
* Usage :
Installation (in shell, if you use node):
$ npm install stablepriorityqueue
Running test program (in JavaScript):
// var StablePriorityQueue = require("stablepriorityqueue");// in node
var x = new StablePriorityQueue();
x.add(1);
x.add(0);
x.add(5);
x.add(4);
x.add(3);
x.peek(); // should return 0, leaves x unchanged
x.size; // should return 5, leaves x unchanged
while(!x.isEmpty()) {
console.log(x.poll());
} // will print 0 1 3 4 5
x.trim(); // (optional) optimizes memory usage
*/
"use strict";
var defaultcomparator = function (a, b) {
return a < b ? -1 : (a > b ? 1 : 0) ;
};
// the provided comparator function should take a, b and return a negative number when a < b and equality when a == b
function StablePriorityQueue(comparator) {
this.array = [];
this.size = 0;
this.runningcounter = 0;
this.compare = comparator || defaultcomparator;
this.stable_compare = function(a, b) {
var cmp = this.compare(a.value,b.value);
return (cmp < 0) || ( (cmp == 0) && (a.counter < b.counter) );
}
}
// The stable queue uses internal counters, this repacks them
// runs in O(n) time, called automatically as needed
StablePriorityQueue.prototype.renumber = function (myval) {
// the counter is exhausted, let us repack the numbers
// implementation here is probably not optimal performance-wise
// we first empty the content
var buffer = [];
var j, size;
while(! this.isEmpty() ) {
buffer.push(this.poll().value);
}
size = buffer.length;
this.runningcounter = 0; // because the buffer is safe, this is now safe
// and we reinsert it
for (j = 0; j < size ; j++) {
this.add(buffer[j]);
}
}
// Add an element the the queue
// runs in O(log n) time
StablePriorityQueue.prototype.add = function (myval) {
var i = this.size;
if ( this.runningcounter + 1 == 0) {
this.renumber();
}
var extendedmyval = {value: myval, counter: this.runningcounter};
this.array[this.size] = extendedmyval;
this.runningcounter += 1;
this.size += 1;
var p;
var ap;
var cmp;
while (i > 0) {
p = (i - 1) >> 1;
ap = this.array[p];
if (!this.stable_compare(extendedmyval, ap)) {
break;
}
this.array[i] = ap;
i = p;
}
this.array[i] = extendedmyval;
};
// for internal use
StablePriorityQueue.prototype._percolateUp = function (i) {
var myval = this.array[i];
var p;
var ap;
while (i > 0) {
p = (i - 1) >> 1;
ap = this.array[p];
if (!this.stable_compare(myval, ap)) {
break;
}
this.array[i] = ap;
i = p;
}
this.array[i] = myval;
};
// for internal use
StablePriorityQueue.prototype._percolateDown = function (i) {
var size = this.size;
var hsize = this.size >>> 1;
var ai = this.array[i];
var l;
var r;
var bestc;
while (i < hsize) {
l = (i << 1) + 1;
r = l + 1;
bestc = this.array[l];
if (r < size) {
if (this.stable_compare(this.array[r], bestc)) {
l = r;
bestc = this.array[r];
}
}
if (!this.stable_compare(bestc,ai)) {
break;
}
this.array[i] = bestc;
i = l;
}
this.array[i] = ai;
};
// Look at the top of the queue (a smallest element)
// executes in constant time
//
// Calling peek on an empty priority queue returns
// the "undefined" value.
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/undefined
//
StablePriorityQueue.prototype.peek = function () {
if(this.size == 0) return undefined;
return this.array[0].value;
};
// remove the element on top of the heap (a smallest element)
// runs in logarithmic time
//
// If the priority queue is empty, the function returns the
// "undefined" value.
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/undefined
//
// For long-running and large priority queues, or priority queues
// storing large objects, you may want to call the trim function
// at strategic times to recover allocated memory.
StablePriorityQueue.prototype.poll = function () {
if (this.size == 0)
return undefined;
var ans = this.array[0];
if (this.size > 1) {
this.array[0] = this.array[--this.size];
this._percolateDown(0 | 0);
} else {
this.size -= 1;
}
return ans.value;
};
// recover unused memory (for long-running priority queues)
StablePriorityQueue.prototype.trim = function () {
this.array = this.array.slice(0, this.size);
};
// Check whether the heap is empty
StablePriorityQueue.prototype.isEmpty = function () {
return this.size === 0;
};
// this isn't included from the source
// basically this drains the queue into a temporary array
// and adds them back so we can have an updated queue
StablePriorityQueue.prototype.update = function () {
this.trim();
var buffer = [];
while(! this.isEmpty() ) {
buffer.push(this.poll());
}
for (j = 0; j < buffer.length ; j++) {
this.add(buffer[j]);
}
};