Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Down The Mountain game, Game development, HTML5, Javascript, Phaser and TypeScript.

With the release of Phaser 3.60, it is time to upgrade various prototypes to this latest version, using TypeScript and dividing the code into classes where possible.

Today I am focusing on Down the Mountain, a mobile smashing hit back in 2015, which in my opinion still has something to say.

This post covers the conversion of the script published in this blog post, when I introduced a fake 3D movement using Bezier curves.

Look at the result:

Tap or click on the left side of the canvas to make the player move down to the left, or the right side of the canvas to make the player move down to the right.

All tiles are recycled once they reach the top of the canvas.

Now, look at the source code, still uncommented because I plan to add some features, then comment it.

We have one HTML file, one CSS file and seven TypeScript files.

index.html

The web page which hosts the game, to be run inside thegame element.

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="initial-scale=1, maximum-scale=1">
        <link rel="stylesheet" href="style.css">
        <script src="main.js"></script> 
    </head>
    <body>   
        <div id = "thegame"></div>
    </body>
</html>

style.css

The cascading style sheets of the main web page.

* {
    padding : 0;
    margin : 0;
}

body {
    background-color: #011025;    
}

canvas {
    touch-action : none;
    -ms-touch-action : none;
}

gameOptions.ts

Configurable game options. It’s a good practice to place all configurable game options, if possible, in a single and separate file, for a quick tuning of the game. I also grouped the variables to keep them more organized.

// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay

export const GameOptions = {

    hexagon : {
        width : 70,
        height : 80
    },

    gridSize : {
        x : 5,
        y : 14
    },

    scrollY : 60
}

main.ts

This is where the game is created, with all Phaser related options.

// MAIN GAME FILE

// modules to import
import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame } from './playGame';

// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
    mode : Phaser.Scale.FIT,
    autoCenter : Phaser.Scale.CENTER_BOTH,
    parent : 'thegame',
    width : 540,
    height : 540
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
    type : Phaser.AUTO,
    backgroundColor : 0x5df4f0,
    scale : scaleObject,
    scene : [PreloadAssets, PlayGame]
}

// the game itself
new Phaser.Game(configObject);

preloadAssets.ts

Here we preload all assets to be used in the game.

// CLASS TO PRELOAD ASSETS
 
// this class extends Scene class
export class PreloadAssets extends Phaser.Scene {
 
    // constructor    
    constructor() {
        super({
            key : 'PreloadAssets'
        });
    }
 
    // method to be execute during class preloading
    preload() : void {

        this.load.image('ground', 'assets/sprites/ground.png');
        this.load.spritesheet('player', 'assets/sprites/player.png', {
            frameWidth : 56,
            frameHeight : 64
        });
    }
 
    // method to be called once the instance has been created
    create() : void {

        // call PlayGame class
        this.scene.start('PlayGame');
    }
}

playGame.ts

Main game file, all game logic is stored here.

// THE GAME ITSELF

import { Player } from './player';
import { Terrain } from './terrain';

// this class extends Scene class
export class PlayGame extends Phaser.Scene {

    constructor() {
        super({
            key : 'PlayGame'
        });
    }

    player : Player;
    terrain : Terrain;

    // method to be executed when the scene has been created
    create() : void {        
        this.terrain = new Terrain(this);
        this.player = new Player(this);
        this.input.on('pointerdown', this.handleInput, this);
        this.player.on('toolow', this.scrollTerrain, this); 
        
    }

    handleInput(pointer : Phaser.Input.Pointer) : void {
        this.player.move(pointer.x < (this.game.config.width as number / 2));      
    }

    scrollTerrain(delta : number) : void {
        Phaser.Actions.IncY(this.terrain.getChildren(), delta);
    }

    update() : void {
        this.player.updatePosition();
        this.terrain.updatePosition();
    }
}

player.ts

The player, a custom class which extends Sprite object.

// PLAYER CLASS EXTENDS PHASER.GAMEOBJECTS.SPRITE

import { GameOptions } from './gameOptions';

export class Player extends Phaser.GameObjects.Sprite {

    canMove : boolean;
    row : number;
    column : number;

    constructor(scene : Phaser.Scene) {
        super(scene, scene.game.config.width as number / 2, GameOptions.scrollY + 6, 'player');
        scene.add.existing(this);
        this.canMove = true;
        this.row = 0;
        this.column = 2;
    }

    move(goingLeft : boolean) : void {
        if (this.canMove) {
            if (goingLeft && (this.column > 0 || (this.row % 2 == 1))) {
                this.column -= (1 - this.row % 2);
                this.row ++;
                this.setFrame(0);
                this.lower(-1);
            }
            if (!goingLeft &&  this.column < GameOptions.gridSize.x - 1) {
                this.column += (this.row % 2);
                this.row ++;
                this.setFrame(1);
                this.lower(1);
            }
        }   
    }

    lower(delta : number) : void {
        let stepX : number = GameOptions.hexagon.width / 2 * delta;
        let stepY : number = GameOptions.hexagon.height / 4 * 3;
        this.canMove = false;
        let startPoint : Phaser.Math.Vector2 = new Phaser.Math.Vector2(this.x, this.y);
        let endPoint : Phaser.Math.Vector2 = new Phaser.Math.Vector2(this.x + stepX, this.y + stepY);
        let controlPoint1 : Phaser.Math.Vector2 = new Phaser.Math.Vector2(this.x + stepX, this.y + stepY / 2);
        let controlPoint2 : Phaser.Math.Vector2 = new Phaser.Math.Vector2(this.x + stepX, this.y + stepY / 2);
        let bezierCurve : Phaser.Curves.CubicBezier = new Phaser.Curves.CubicBezier(startPoint, controlPoint1, controlPoint2, endPoint);
        let tweenValue : any = {
            value : 0,
            previousValue : 0
        }
        this.scene.tweens.add({
            targets : tweenValue,
            value : 1,
            duration : 100,
            callbackScope : this,
            onComplete : () => {
                this.canMove = true;
            },
            onUpdate : (tween : Phaser.Tweens.Tween, target : any) => {
                let position : Phaser.Math.Vector2 = bezierCurve.getPoint(target.value);
                let prevPosition : Phaser.Math.Vector2 = bezierCurve.getPoint(target.previousValue);
                this.x += position.x - prevPosition.x;
                this.y += position.y - prevPosition.y;
                target.previousValue = target.value;
            }
        })
    }

    updatePosition() : void {
        if (this.y > GameOptions.scrollY) {
            let distance : number = (this.y - 6) / -25;
            this.y += distance;
            this.emit('toolow', distance);
        }
    }
}

tile.ts

A ground tile, a custom class which extends Sprite object.

// TILE CLASS EXTENDS PHASER.GAMEOBJECTS.SPRITE

import { GameOptions } from './gameOptions';

export class Tile extends Phaser.GameObjects.Sprite {
    
    constructor(scene : Phaser.Scene, posX : number, posY : number) {
        super(scene, posX, posY, 'ground');
        this.setOrigin(0, 0);
        scene.add.existing(this);    
    }

    updatePosition() : void {
        if (this.y < - GameOptions.hexagon.height) {
            this.y += GameOptions.hexagon.height * (GameOptions.gridSize.y * 3 / 4);
        }
    }
}

terrain.ts

The terrain, a custom class which extends Group object. The terrain is basically a group of Tile objects.

// TERRAIN CLASS EXTENDS PHASER.GAMEOBJECTS.GROUP

import { GameOptions } from './gameOptions';
import { Tile } from './tile';

export class Terrain extends Phaser.GameObjects.Group {
 
    constructor(scene : Phaser.Scene) {
        super(scene);
        scene.add.existing(this);
        for (let i : number = 0; i < GameOptions.gridSize.y; i ++) {
            this.addRow(i);
        }
    }

    addRow(i : number) : void {
        let offset : number = (this.scene.game.config.width as number - GameOptions.gridSize.x * GameOptions.hexagon.width) / 2;
        for (let j : number = 0; j < GameOptions.gridSize.x - i % 2; j ++) {
            let hexagonX : number = GameOptions.hexagon.width * j + (GameOptions.hexagon.width / 2) * (i % 2) + offset;
            let hexagonY : number = GameOptions.scrollY + GameOptions.hexagon.height * i / 4 * 3;
            let tile : Tile = new Tile(this.scene, hexagonX, hexagonY)
            this.add(tile);
        }
    }

    updatePosition() : void {
        this.getChildren().forEach((child : Phaser.GameObjects.GameObject) => {
            let tile : Tile = child as Tile;
            tile.updatePosition();
        })
    }
}

Old glories can live a new life, like this one. Next time I will be adding some obstacles, meanwhile download the source code of the entire project.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.