Turn Based Battle System using Phaser

Last modified: 07 Feb 2018

Source

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:

  1. create() - Create the queue and put everyone in, with all but one hero’s priority number randomly set to simulate combat initiative.
  2. create() - One hero will have his priority number initially set to 1 so our player goes to attack first.
  3. updateQueue() or in onInputDown() - When an attack plays out, we take out the first one in the queue.
  4. attack() - Perform the attack scene and increase the priority of the attacker.
  5. updateQueue() - When we update the queue, we decrease each unit’s priority.
  6. 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.

  1. Move the attacker and target to a different sprite group so they show on top of the dimmed background we created earlier.
  2. Both attacker and targets sizes up.
  3. Attacker initiate the ATTACK animation.
  4. Wait halfway through the attack animation before we initiate the HURT animation on the target.
  5. Show blood effect at the center of the target.
  6. Wait for a second to allow all the steps above to play out, before we reset the scene.
  7. 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

Inspirations

Thank you…

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]);
  }
};

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