Tutorial: Creating new bombs

Creating new bombs

In this tutorial we are going to create two bomb classes. The first one will be a simple object-oriented programming exercise (like Creating new enemies, Creating new movers and Creating new emitters), the second one will exploit JavaScript's peculiar capability of redefining methods on an already instantiated object.

BHell_Bomb_Gravity

So far, no bomb relies on something different than spawning bullets. We want to create a BHell_Bomb_Gravity class which simply will halve the hit points of every enemy on screen (and to prevent this bomb's abuse, we'll also want it to deactivate itself after 20 seconds), without spawning any bullet.

Note: Autobombing won't work during the entire time a bomb is active, so, although extremely powerful, this bomb may make the player more vulnerable.

Since no bullet will be spawned, we will hint the player that the bomb has fired by spawning an explosion on every enemy.

Let's start by creating a new bomb_gravity.js file and reopening our BHell module:

var BHell = (function (my) {    
    /* Our code will go here */
    
    return my;
}(BHell || {}));

We can now create our BHell_Bomb_Gravity class:

var BHell_Bomb_Gravity = my.BHell_Bomb_Gravity = function () {
    this.initialize.apply(this, arguments);
};

BHell_Bomb_Gravity.prototype = Object.create(my.BHell_Bomb_Base.prototype);
BHell_Bomb_Gravity.prototype.constructor = BHell_Bomb_Gravity;

BHell_Bomb_Gravity.prototype.initialize = function (parent, params, bulletList) {
    my.BHell_Bomb_Base.prototype.initialize.call(this, parent, params, bulletList);
};

The behaviour of bomb classes is handled entirely by the methods BHell.BHell_Bomb_Base#activate, BHell.BHell_Bomb_Base#update and BHell.BHell_Bomb_Base#deactivate. In this example, our desired behaviour is something that happen only at the bomb's activation, so we will rewrite the activate method to suit our purposes.

Since the bomb will directly manipulate enemies parameters, we need to check that an enemy list is in fact available (there are no enemies on the preview window and that would cause an error). We also need to deal with the bosses' bar, since it normally is updated only by BHell.BHell_Enemy_Base#hit (which we are not going to use):

BHell_Bomb_Gravity.prototype.activate = function (x, y) {
    my.BHell_Bomb_Base.prototype.activate.call(this, x, y);

    if (my.controller != null && my.controller.enemies != null) {
        my.controller.enemies.forEach(e => {
            // We are using the ceil function to prevent enemies from dying when they have less than two hit points.
            var damage = Math.ceil(e.hp / 2);
            e.hp -= damage;
            if (e.boss) {
                my.bossHp -= damage;
            }

            my.explosions.push(new my.BHell_Explosion(e.x, e.y, this.parent, my.explosions));
        });
    }
};  

We decided to deactivate our bomb after 20 seconds (1200 frames), because it would be too easy for the player to use a bomb so powerful too often. The default update behaviour sets a deactivation timeout of one seconds, so we need to rewrite that method as well:

BHell_Bomb_Gravity.prototype.update = function () {
    if (this.active === true) {
        this.i++;

        if (this.i > 1200) {
            this.deactivate();
        }
    }
};

Our complete bomb_gravity.js looks like this:

var BHell = (function (my) {

    var BHell_Bomb_Gravity = my.BHell_Bomb_Gravity = function () {
        this.initialize.apply(this, arguments);
    };

    BHell_Bomb_Gravity.prototype = Object.create(my.BHell_Bomb_Base.prototype);
    BHell_Bomb_Gravity.prototype.constructor = BHell_Bomb_Gravity;

    BHell_Bomb_Gravity.prototype.initialize = function (parent, params, bulletList) {
        my.BHell_Bomb_Base.prototype.initialize.call(this, parent, params, bulletList);
    };

    BHell_Bomb_Gravity.prototype.activate = function (x, y) {
        my.BHell_Bomb_Base.prototype.activate.call(this, x, y);

        if (my.controller != null && my.controller.enemies != null) {
            my.controller.enemies.forEach(e => {
                // We are using the ceil function to prevent enemies from dying when they have less than two hit points.
                var damage = Math.ceil(e.hp / 2);
                e.hp -= damage;
                if (e.boss) {
                    my.bossHp -= damage;
                }

                my.explosions.push(new my.BHell_Explosion(e.x, e.y, this.parent, my.explosions));
            });
        }

    };


    BHell_Bomb_Gravity.prototype.update = function () {
        if (this.active === true) {
            this.i++;

            if (this.i > 1200) {
                this.deactivate();
            }
        }
    };


    return my;
}(BHell || {}));

All we need to do now is to import the plugin and assign this bomb to a player, by adding the following parameter to its configuration in the JSON file. While we are at it, we'll also customise the icon index and the sound effect.

"bomb": {
    "sprite": "$Bullets",
    "index": 0,
    "direction": 2,
    "frame": 1,
    "icon": "IconSet",
    "icon_index": 161,
    "class": "BHell_Bomb_Gravity",
    "se": {
        "name": "Collapse4",
        "volume": 100,
        "pitch": 100,
        "pan": 0
    }
}

BHell_Bomb_Thunder

Most of the predefined bombs simply behave like emitters, spawning a barrage of bullets filling the screen. In this section we want to create a BHell_Bomb_Thunder class which will spawn 200 bullets moving erratically across the screen for ten seconds. In order to do so, we can't rely on the default BHell.BHell_Bullet#update method and we could be tempted to proceed (in the object-oriented way) by creating a derived class from BHell.BHell_Bullet, but JavaScript allows to achieve the same result (more quickly) by replacing a function definition in an already instantiated object.

This is what was done with BHell.BHell_Bomb_Earth, which makes the bullets rotate around the player, instead of moving in a straight line.

Lets' create our bomb_thunder.js plugin in the usual way:

var BHell = (function (my) {

    var BHell_Bomb_Thunder = my.BHell_Bomb_Thunder = function () {
        this.initialize.apply(this, arguments);
    };

    BHell_Bomb_Thunder.prototype = Object.create(my.BHell_Bomb_Base.prototype);
    BHell_Bomb_Thunder.prototype.constructor = BHell_Bomb_Thunder;

    BHell_Bomb_Thunder.prototype.initialize = function (parent, params, bulletList) {
        my.BHell_Bomb_Base.prototype.initialize.call(this, parent, params, bulletList);
    };

    return my;
}(BHell || {})); 

Since we are going to use bullets which will never leave the screen on their own, we need to store them on a separate array, so we can destroy them during the bomb's deactivation. Let's start by initialise it:

BHell_Bomb_Thunder.prototype.initialize = function (parent, params, bulletList) {
    my.BHell_Bomb_Base.prototype.initialize.call(this, parent, params, bulletList);

    this.bullets = [];
};

And let's destroy every bullet:

BHell_Bomb_Thunder.prototype.deactivate = function () {
    my.BHell_Bomb_Base.prototype.deactivate.call(this);
    while (this.bullets.length > 0) {
        var bullet = this.bullets.pop();
        bullet.destroy();
    }
};

Important: If you are familiar with the Iterator design pattern, you already know that using a forEach here would be a big mistake and would most certainly cause an hard to find bug. For those of you who aren't aware, the forEach construct relies on "moving" from one element to the next one in a list, but when you dynamically change the list's content while iterating over it (as in this case, since we are popping elements from it), the iterator's state (usually a simple index variable) will desynchronise: upon deletion we might skip some elements, while on insertion we might iterate over some elements more than once.

Just like our BHell_Bomb_Gravity, we want to deactivate our bomb after some time other than the default one second timeout, we'll rewrite our update method accordingly:

BHell_Bomb_Thunder.prototype.update = function () {
    if (this.active === true) {
        this.i++;

        if (this.i > 600) {
            this.deactivate();
        }
    }
};

Finally, we can focus on the most important part of this bomb: its activation.

If we were simply to spawn 200 bullets at the player's coordinates, our update method would look like this:

BHell_Bomb_Thunder.prototype.activate = function (x, y) {
    my.BHell_Bomb_Base.prototype.activate.call(this, x, y);

    var j;
    var bullet;

    for (j = 0; j < 200; j++) {
        bullet = new my.BHell_Bullet(my.player.x, my.player.y, 0, this.bulletParams, this.bulletList);

        this.bullets.push(bullet);
        this.bulletList.push(bullet);
        this.parent.addChild(bullet);
    }
};

And in fact if we were to create a derived class (let's say for example BHell_Bullet_Erratic), we would simply need to replace the constructor. But since we want to replace our bullet's behaviour on a per-instance basis, we are going to do something like this:

BHell_Bomb_Thunder.prototype.activate = function (x, y) {
    my.BHell_Bomb_Base.prototype.activate.call(this, x, y);

    var j;
    var bullet;

    for (j = 0; j < 200; j++) {
        bullet = new my.BHell_Bullet(my.player.x, my.player.y, 0, this.bulletParams, this.bulletList);

        // Create new properties for the bullet
        bullet.destX = my.player.x;
        bullet.destY = my.player.y;

        bullet.update = function () {
            var dx = this.destX - this.x;
            var dy = this.destY - this.y;

            if (Math.abs(dx) < this.speed && Math.abs(dy) < this.speed) {
                this.destX = Math.round(Math.random() * Graphics.width);
                this.destY = Math.round(Math.random() * Graphics.height);
            }
            else {
                this.angle = Math.atan2(dy, dx);
            }

            my.BHell_Bullet.prototype.update.call(this);
        };

        this.bullets.push(bullet);
        this.bulletList.push(bullet);
        this.parent.addChild(bullet);
    }
};

Our complete class looks like this:

var BHell = (function (my) {

    var BHell_Bomb_Thunder = my.BHell_Bomb_Thunder = function () {
        this.initialize.apply(this, arguments);
    };

    BHell_Bomb_Thunder.prototype = Object.create(my.BHell_Bomb_Base.prototype);
    BHell_Bomb_Thunder.prototype.constructor = BHell_Bomb_Thunder;

    BHell_Bomb_Thunder.prototype.initialize = function (parent, params, bulletList) {
        my.BHell_Bomb_Base.prototype.initialize.call(this, parent, params, bulletList);

        this.bullets = [];
    };

    BHell_Bomb_Thunder.prototype.activate = function (x, y) {
        my.BHell_Bomb_Base.prototype.activate.call(this, x, y);

        var j;
        var bullet;

        for (j = 0; j < 200; j++) {
            bullet = new my.BHell_Bullet(my.player.x, my.player.y, 0, this.bulletParams, this.bulletList);

            // Create new properties for the bullet
            bullet.destX = my.player.x;
            bullet.destY = my.player.y;

            bullet.update = function () {
                var dx = this.destX - this.x;
                var dy = this.destY - this.y;

                if (Math.abs(dx) < this.speed && Math.abs(dy) < this.speed) {
                    this.destX = Math.round(Math.random() * Graphics.width);
                    this.destY = Math.round(Math.random() * Graphics.height);
                }
                else {
                    this.angle = Math.atan2(dy, dx);
                }

                my.BHell_Bullet.prototype.update.call(this);
            };

            this.bullets.push(bullet);
            this.bulletList.push(bullet);
            this.parent.addChild(bullet);
        }
    };

    BHell_Bomb_Thunder.prototype.update = function () {
        if (this.active === true) {
            this.i++;

            if (this.i > 600) {
                this.deactivate();
            }
        }
    };

    BHell_Bomb_Thunder.prototype.deactivate = function () {
        my.BHell_Bomb_Base.prototype.deactivate.call(this);
        while (this.bullets.length > 0) {
            var bullet = this.bullets.pop();
            bullet.destroy();
        }
    };

    return my;
}(BHell || {}));

We can now import it and configure a player to use our new BHell_Bomb_Thunder:

"bomb": {
    "sprite": "$Bullets",
    "index": 0,
    "direction": 4,
    "frame": 1,
    "speed": 9,
    "icon": "IconSet",
    "icon_index": 66,
    "class": "BHell_Bomb_Thunder",
    "se": {
        "name": "Explosion2",
        "volume": 100,
        "pitch": 100,
        "pan": 0
    }
}