Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Magick game, Game development, HTML5, Javascript, Phaser and TypeScript.

I hope you are enjoying my Magick HTML5 prototype, and today I am going to add a new feature: the ability to stop the player. In the original game, you could stop the player by tapping and holding the screen.

In this version, you can stop the player by clicking or tapping anywhere on a solid tile. I prefer this way rather than using tap and hold, because there won’t be any delay.

Stopping the player is not an issue, the problem is when you stop the player in situations like this one:

It shouldnt’ be possible for the player to stand still in this position, it’s just too much on the edge.

This can happen in two cases, which have to be handled differently:

Player is moving to the right, this means it just jumped on the light green tile, and we decided to stop it. It should fall down to the left, then start climbing again the tile.

Player is moving to the left, this means it was about to fall down but we decided to stop it. It should fall down anyway, then keep moving to the left.

See the prototype in action:

The player walks and climbs on his own, you can only click anywhere on an empty spot to summon a block, but you can summon only one block at once.

Moreover, clicking on the player will make it change direction, and clicking and holding on a solid tile will make the player stop.

The source code, though still uncommented, is pretty straightforward and consists in one HTML file, one CSS file and six 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: #000000;    
}

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 = {

    player : {
        speed : 120,
        jumpSpeed : {
            x : 30,
            y : -100
        },
        gravity : 400,
        triggerRadius : 32
    },

    tileSize : 32
}

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 : 800,
    height : 320
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
    type : Phaser.AUTO,
    backgroundColor : 0x000000,
    scale : scaleObject,
    scene : [PreloadAssets, PlayGame],
    physics : {
        default : 'arcade',
        arcade : {
            gravity : {
                y : 0
            }
        }
    }
}

// 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('tiles', 'assets/sprites/tiles.png');
        this.load.tilemapTiledJSON('map', 'assets/maps/map.json');
        this.load.image('player', 'assets/sprites/player.png');
       
    }
 
    // 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 { GameOptions } from './gameOptions';
import { Player } from './player';
import { MapUtils } from './mapUtils';

enum TileType {
    None,
    Ground,
    Block,
    Deadly,
    Exit    
}

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

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

    map : Phaser.Tilemaps.Tilemap;
    player : Player;
    levelLayer : Phaser.Tilemaps.TilemapLayer;
    specialTiles : Phaser.Tilemaps.Tile[];
    currentLevel : number;

    init(data : any) : void {
        if (data.level != undefined) {
            if (data.level > 4) {
                data.level = 1;
            }
            this.currentLevel = data.level;
        }
        else {
            this.currentLevel = 1;
        }
    }
  
    // method to be executed when the scene has been created
    create() : void {
        this.map = this.make.tilemap({
            key : 'map'
        });        
        let tileSet : Phaser.Tilemaps.Tileset = this.map.addTilesetImage('tileset01', 'tiles') as Phaser.Tilemaps.Tileset;
        this.levelLayer = this.map.createLayer('Level' + this.currentLevel.toString(), tileSet) as Phaser.Tilemaps.TilemapLayer;
        this.map.setCollisionBetween(TileType.Ground, TileType.Block);
        this.specialTiles = this.map.filterTiles(this.filterSpecialTiles, this) as Phaser.Tilemaps.Tile[];
        this.player = new Player(this, 128, 128);
        this.input.on('pointerdown', this.handlePointerDown, this);
        this.input.on('pointerup', this.handlePointerUp, this);
    }

    filterSpecialTiles(tile : Phaser.Tilemaps.Tile) : boolean {
        return tile.index == TileType.Deadly || tile.index == TileType.Exit;
    }

    handlePointerUp() {
        this.player.canMove = true;
    }

    handlePointerDown(pointer : Phaser.Input.Pointer) : void {
        if (Phaser.Math.Distance.Between(pointer.x, pointer.y, this.player.x, this.player.y) < GameOptions.player.triggerRadius) {
            this.player.changeDirection();    
        }  
        else {
            if (MapUtils.isWallTile(this.map, pointer.x, pointer.y)) {
                this.player.canMove = false;
            }
            else {
                MapUtils.addTile(this.map, pointer.x, pointer.y);
            }
        }
    }

    handleSpecialTiles(player : any, tile : any) : void {
        switch (tile.index) {
            case TileType.Deadly :
                this.scene.start('PlayGame', {
                    level : this.currentLevel
                }); 
                break;
            case TileType.Exit :
                this.scene.start('PlayGame', {
                    level : this.currentLevel + 1
                }); 
                break;
        }
    }

    movePlayer() : void {
        this.player.move(this.map);
    }

    update(time : number) : void {
        this.player.stopMoving();
        this.physics.world.collide(this.player, this.levelLayer, this.movePlayer, undefined, this);
        this.physics.world.overlapTiles(this.player, this.specialTiles, this.handleSpecialTiles, undefined, this);
        this.player.fall(this.map);
    }

}

player.ts

The player, a custom class which extends Phaser.Physics.Arcade.Sprite object.

// PLAYER CLASS EXTENDS PHASER.PHYSICS.ARCADE.SPRITE

import { GameOptions } from './gameOptions';

export class Player extends Phaser.Physics.Arcade.Sprite {

    isJumping : boolean;
    canMove : boolean;
    direction : number;
    body : Phaser.Physics.Arcade.Body;

    constructor(scene : Phaser.Scene, posX : number, posY : number) {
        super(scene, posX, posY, 'player');
        scene.add.existing(this);
        scene.physics.add.existing(this);
        this.isJumping = false;
        this.direction = 1;
        this.canMove = true;
        this.body.gravity.y = GameOptions.player.gravity;
    }

    move(map : Phaser.Tilemaps.Tilemap) : void {
        if (this.body.blocked.down && this.canMove) {
            this.body.setVelocityX(GameOptions.player.speed * this.direction);
            this.isJumping = false;
        }
        if (this.body.blocked.right && this.direction == 1) {
            if ((!map.getTileAtWorldXY(this.x + GameOptions.tileSize, this.y - GameOptions.tileSize) && !map.getTileAtWorldXY(this.x, this.y - GameOptions.tileSize)) || this.isJumping) {
                this.jump();
            }
            else {
                this.direction *= -1;
            }
        }
        if(this.body.blocked.left && this.direction == -1) {
            if((!map.getTileAtWorldXY(this.x - GameOptions.tileSize, this.y - GameOptions.tileSize) && !map.getTileAtWorldXY(this.x, this.y - GameOptions.tileSize)) || this.isJumping) {
                this.jump();
            }
            else {
                this.direction *= -1;
            }
        }
    }

    fall(map : Phaser.Tilemaps.Tilemap) : void {
        if (!this.canMove && this.body.blocked.down && map.getTileAtWorldXY(this.x, this.y + GameOptions.tileSize) == null) {
            let offset : number = this.x % map.tileWidth;
            if (offset < map.tileWidth / 2 && offset > map.tileWidth / 8) {
                this.body.setVelocityX(GameOptions.player.speed);
            }
            if (offset > map.tileWidth / 2 && offset < map.tileWidth / 8 * 7) {
                this.body.setVelocityX(-GameOptions.player.speed);
            }
        }
    }

    changeDirection() : void {
        this.direction *= -1;
    }

    stopMoving() : void {
        this.setVelocityX(0);
    }

    jump() : void {
        this.body.setVelocity(this.canMove ? GameOptions.player.jumpSpeed.x * this.direction : 0, GameOptions.player.jumpSpeed.y);
        this.isJumping = true;
    }
}

mapUtils.ts

A simple class to store map utilities.

export class MapUtils {

    static tilePoint : Phaser.Math.Vector2 | null = null;

    static isWallTile(map: Phaser.Tilemaps.Tilemap, posX: number, posY: number) : boolean {
        let tile : Phaser.Tilemaps.Tile | null = map.getTileAtWorldXY(posX, posY);
        return tile != null && tile.index == 1;
    }
    
    static addTile(map: Phaser.Tilemaps.Tilemap, posX: number, posY: number) : void {
        if (!map.getTileAtWorldXY(posX, posY)) {
            if (this.tilePoint != null) {
                map.removeTileAtWorldXY(this.tilePoint.x, this.tilePoint.y);
            }
            map.putTileAtWorldXY(2, posX, posY);
            this.tilePoint = new Phaser.Math.Vector2(posX, posY);
        }      
    }
}

Now player movement is completed, next time I’ll add some enemies, 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.