Build a HTML5 game like old Flash glory “Totem Destroyer” using Phaser and Planck.js physics engine

Talking about Totem Destroyer game, Box2D, Game development, HTML5, Javascript and Phaser.

Do you remember Totem Destroyer? The link points to a YouTube video, because being a Flash game, it’s not longer playable.

Your mission is to destroy a series of totems without letting the golden Idol (aka Tot) fall into the ground.

I started writing about this game back in 2008 – 13 years ago! – building a prototype using Flash and AS3 which obviously now it doesn’t work anymore.

If you follow my tutorial series about Totem Destroyer, you’ll see I built prototypes using Flash, Dreemchest (which is dead nowadays), PhysicsJS and Phaser 2 with p2.js.

The game is fun and the prototypes are definitively outdated, so here is a new prototype using Phaser 3 and Box2D powered by Planck.js.

Click on a light brick to destroy it. Light bricks are the only ones which can be destroyed. Don’t let the idol hit the ground.

Although it’s a simple prototype, it requires these features

* Queries to check for bodies under the pointer

* Custom data management for each body

* Collision detection

And they all are explained line by line in the commented source code:

let game;

let gameOptions = {

    // conversion unit from pixels to meters. 30 pixels = 1 meter
    worldScale: 30
}

window.onload = function() {
    let gameConfig = {
        type: Phaser.AUTO,
        backgroundColor:0x87ceea,
        scale: {
            mode: Phaser.Scale.FIT,
            autoCenter: Phaser.Scale.CENTER_BOTH,
            parent: "thegame",
            width: 800,
            height: 400
        },
        scene: playGame
    }
    game = new Phaser.Game(gameConfig);
    window.focus();
}

// constants to store block types
const TERRAIN = 0;
const IDOL = 1;
const BREAKABLE = 2;
const UNBREAKABLE = 3;

class playGame extends Phaser.Scene {
    constructor() {
        super("PlayGame");
    }
    create() {

        // world gravity, as a Vec2 object. It's just a x, y vector
        let gravity = planck.Vec2(0, 3);

        // this is how we create a Box2D world
        this.world = planck.World(gravity);

        // totem creation
        this.createBox(game.config.width / 2, game.config.height - 20, game.config.width, 40, false, TERRAIN, 0x049b15);
        this.createBox(game.config.width / 2 - 60, game.config.height - 60, 40, 40, true, BREAKABLE, 0x6e5d42);
        this.createBox(game.config.width / 2 + 60, game.config.height - 60, 40, 40, true, BREAKABLE, 0x6e5d42);
        this.createBox(game.config.width / 2, game.config.height - 100, 160, 40, true, BREAKABLE, 0x6e5d42);
        this.createBox(game.config.width / 2, game.config.height - 140, 80, 40, true, UNBREAKABLE, 0x3b3b3b);
        this.createBox(game.config.width / 2 - 20, game.config.height - 180, 120, 40, true, BREAKABLE, 0x6e5d42);
        this.createBox(game.config.width / 2, game.config.height - 240, 160, 80, true, UNBREAKABLE, 0x3b3b3b);
        this.idol = this.createBox(game.config.width / 2, game.config.height - 320, 40, 80, true, IDOL, 0xfff43a);

        // input listener to call destroyBlock method
        this.input.on("pointerdown", this.destroyBlock, this);

    }

    // method to destroy a block
    destroyBlock(e) {

        // convert pointer coordinates to world coordinates
        let worldX = this.toWorldScale(e.x);
        let worldY = this.toWorldScale(e.y);
        let worldPoint = planck.Vec2(worldX, worldY);

        // query for the world coordinates to check fixtures under the pointer
        this.world.queryAABB(planck.AABB(worldPoint, worldPoint), function(fixture) {

            // get the body from the fixture
            let body = fixture.getBody();

            // get the userdata from the body
            let userData = body.getUserData();

            // is a breakable body?
            if (userData.blockType == BREAKABLE) {

                // destroy the sprite
                userData.sprite.destroy();

                // destroy the body
                this.world.destroyBody(body);
            }
        }.bind(this));
    }

    // simple function to convert pixels to meters
    toWorldScale(n) {
        return n / gameOptions.worldScale;
    }

    // totem block creation
    createBox(posX, posY, width, height, isDynamic, blockType, color) {

        // this is how we create a generic Box2D body
        let box = this.world.createBody();
        if (isDynamic) {

            // Box2D bodies born as static bodies, but we can make them dynamic
            box.setDynamic();
        }

        // a body can have one or more fixtures. This is how we create a box fixture inside a body
        box.createFixture(planck.Box(width / 2 / gameOptions.worldScale, height / 2 / gameOptions.worldScale));

        // now we place the body in the world
        box.setPosition(planck.Vec2(posX / gameOptions.worldScale, posY / gameOptions.worldScale));

        // time to set mass information
        box.setMassData({
            mass: 1,
            center: planck.Vec2(),

            // I have to say I do not know the meaning of this "I", but if you set it to zero, bodies won't rotate
            I: 1
        });

        // now we create a graphics object representing the body
        let borderColor = Phaser.Display.Color.IntegerToColor(color);
        borderColor.darken(20);

        let userData = {
            blockType: blockType,
            sprite: this.add.graphics()
        }
        userData.sprite.fillStyle(color);
        userData.sprite.fillRect(- width / 2, - height / 2, width, height);
        userData.sprite.lineStyle(4, borderColor.color)
        userData.sprite.strokeRect(- width / 2 + 2, - height / 2 + 2, width - 4, height - 4);

        // a body can have anything in its user data, normally it's used to store its sprite
        box.setUserData(userData);

        return box;
    }

    update(t, dt) {

        // advance world simulation
        this.world.step(dt / 1000 * 2);

        // crearForces  method should be added at the end on each step
        this.world.clearForces();

        // get idol contact list
        for (let ce = this.idol.getContactList(); ce; ce = ce.next) {

            // get the contact
            let contact = ce.contact;

            // get the fixture from the contact
            let fixture = contact.getFixtureA();

            // get the body from the fixture
            let body = fixture.getBody();

            // the the userdata from the body
            let userData = body.getUserData();

            // did the idol hit the terrain?
            if (userData.blockType == TERRAIN) {

                // oh no!!
                this.cameras.main.setBackgroundColor(0xa90000)
            }
        }

        // iterate through all bodies
        for (let b = this.world.getBodyList(); b; b = b.getNext()) {

            // get body position
            let bodyPosition = b.getPosition();

            // get body angle, in radians
            let bodyAngle = b.getAngle();

            // get body user data, the graphics object
            let userData = b.getUserData();

            // adjust graphic object position and rotation
            userData.sprite.x = bodyPosition.x * gameOptions.worldScale;
            userData.sprite.y = bodyPosition.y * gameOptions.worldScale;
            userData.sprite.rotation = bodyAngle;
        }
    }
};

And old Flash glory could revive thanks to Phaser and Planck.js

Do you have a map of all original levels? Download the source code and try to build your own Totem Destroyer game.