Roofed areas and better map engine

Fri May 02 2025 15:06:07 GMT+0100 (British Summer Time) canvasgamedevtilemaptutorialmapsjavascripthtmlarchived

Modified and new globals

Roofs! Houses are terrible without them! In this tutorial, we'll look at adding roofed areas to our map that disappear whilst the player is underneath them. With this you can add a bit of mystery to your maps, and have you players need to explore to find everything they require.

We'll also change the way the map and map tiles are managed - this'll lay the ground work for some of our future lessons, and begin making our map engine more enginey.

Tile map Roofed Areas example

We'll also need a slightly modified tileset - here you see we've added 2 tiles to use for roofs.

Tileset

First of all, we'll change our gameMap array to accomodate how our new map will look:

var gameMap = [
    0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 2, 2, 2, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 0, 0, 0, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 4, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
    0, 1, 1, 2, 2, 2, 2, 2, 0, 4, 4, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 1, 1, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 1, 1, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 0, 0, 0, 1, 1, 4, 1, 1, 0, 0, 0, 2, 0, 0,
    0, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 2, 2, 2, 2, 2, 2, 1, 4, 4, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 4, 1, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
];

After this, we'll add a new global, mapTileData, which will be a new instance of the TileMap class we'll create momentarily.

var mapTileData = new TileMap();

We can also remove the tileEvents map we created before - we won't have any fancy triggered tile events on this new map yet, and when we do we'll handle them differently. You can also remove the drawBridge function from the previous lesson if you have it in you code as we won't be using that now.

Our Roofs

Now we'll add our list of roofs. We'll store them in an array, and each entry will be an object with the properties x, y which is the start position of the roof on our map, w, h, the dimensions (width and height) of the roof, and data, which is an array sized w x h containing a list of tile types (the index of the corresponding tileType array entry) to draw for this roof segment. Any 0 values will not be drawn - this helps for creating none-rectangular roofs.

var roofList = [
    { x:5, y:3, w:4, h:7, data: [
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11
    ]},
    { x:15, y:5, w:5, h:4, data: [
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11
    ]},
    { x:14, y:9, w:6, h:7, data: [
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11
    ]}
];

We're also going to extend the list of tileType entries to add our new roof tile images. We'll give them a floorType solid - this value doesn't matter for roofs though, as the player does not collide with them.

    10 : { colour:"#ccaa00", floor:floorTypes.solid, sprite:[{x:40,y:120,w:40,h:40}]},
    11 : { colour:"#ccaa00", floor:floorTypes.solid, sprite:[{x:80,y:120,w:40,h:40}]}

Map engine and Tile object

Our new map will make use of two new classes; TileMap, which stores and manages our loaded map data, and Tile, which stores information for each map tile. Lets look at the Tile class first:

function Tile(tx, ty, tt)
{
    this.x            = tx;
    this.y            = ty;
    this.type        = tt;
    this.roof        = null;
    this.roofType    = 0;
    this.eventEnter    = null;
}

A tile object is created with 3 arguments: tx, ty: the position of the tile on the map, and tt: the id of the tileType entry of the tile at this position.

This will populate the Tile objects x, y and type parameters accordingly. The object also has the properties roof and roofType for handling roofed areas (roof is a reference to the roof object of which this tiles roof is a part, or null if there is no roof here, and roofType is the id of the corresponding entry for the roof graphic to use in the tileTypes array.

Additionally, Tile objects have a eventEnter property; this can be a pointer to a function (or anonymous function) to execute when a character has completed moving on to this tile, or null if this tile does not have any special events attached.

The TileMap class

The TileMap class will become our map handling class, and currently has 3 properties: w and h, the width and height of the map, and map, an array which will contain all the Tile objects that make up our map.

function TileMap()
{
    this.map    = [];
    this.w        = 0;
    this.h        = 0;
}

This class will have a buildMapFromData method that will take 3 arguments; d, an array containing the tileType id to use for each map tile, and w, h, the dimensions of the map. The method begins by setting the w, h properties of the map object to correspond to the passed dimensions:

TileMap.prototype.buildMapFromData = function(d, w, h)
{
    this.w        = w;
    this.h        = h;

Next, we do a quick check to ensure the length of the d argument is equal to w x h, and if not we return false. After, we clear the map property of any data it currently contains.

    if(d.length!=(w*h)) { return false; }
    
    this.map.length    = 0;

We'll then loop through the d array, by row (y), and then each column of each row (x), and add a new Tile object to the map array created with the x, y position from the current loop iterations and the corresponding entry from the d array as the tileType id:

    for(var y = 0; y < h; y++)
    {
        for(var x = 0; x < w; x++)
        {
            this.map.push( new Tile(x, y, d[((y*w)+x)]) );
        }
    }

Once we're done we can return true and close the method:

    return true;
};

We'll also add a method called addRoofs which will take an array of roof objects as its argument. The method will begin by looping through each entry in the array, and for readabilities sake create a reference, r, to the current roof object:

TileMap.prototype.addRoofs = function(roofs)
{
    for(var i in roofs)
    {
        var r = roofs[i];

We'll also do some checks with the current roof object; we'll check it does not start or end outside of map bounds, and check its data array length is equal to its w x h. If it does not pass these checks, we'll skip this roof and not use it:

        if(r.x < 0 || r.y < 0 || r.x >= this.w || r.y >= this.h ||
            (r.x+r.w)>this.w || (r.y+r.h)>this.h ||
            r.data.length!=(r.w*r.h))
        {
            continue;
        }

We'll then loop through all the rows (y) and columns (x) of the roof, and calculate the tileIdx of the corresponding map position by adding the x, y offsets of the roof position to the x, y roof position we're looking at.

        for(var y = 0; y < r.h; y++)
        {
            for(var x = 0; x < r.w; x++)
            {
                var tileIdx = (((r.y+y)*this.w)+r.x+x);

We'll then update the Tile object at tileIdx in the map array to set its roof property to a reference to the current roof we're adding to the map, and the roofType for the Tile to the value in the data array from this roof. After doing so, we can close the nested loops and the method itself!

                this.map[tileIdx].roof = r;
                this.map[tileIdx].roofType = r.data[((y*r.w)+x)];
            }
        }
    }
};

Modified and new code

We'll also modify the handling of triggerable tiles in the Character processMovement method; we'll replace the code:

        if(typeof tileEvents[toIndex(this.tileTo[0], this.tileTo[1])]!='undefined')
        {
            tileEvents[toIndex(this.tileTo[0], this.tileTo[1])](this);
        }

...with this...

        if(mapTileData.map[toIndex(this.tileTo[0], this.tileTo[1])].eventEnter!=null)
        {
            mapTileData.map[toIndex(this.tileTo[0], this.tileTo[1])].eventEnter(this);
        }

We also need to update the other references to gameMap in this method. We'll get the tileFloor variable like this:

        var tileFloor = tileTypes[mapTileData.map[toIndex(this.tileFrom[0], this.tileFrom[1])].type].floor;

The moveSpeed variable like this:

    var moveSpeed = this.delayMove[tileTypes[mapTileData.map[toIndex(this.tileFrom[0],this.tileFrom[1])].type].floor];

...And in the Character.canMoveTo method we'll change the line:

    if(typeof this.delayMove[tileTypes[gameMap[toIndex(x,y)]].floor]=='undefined') { return false; }

...to this...

    if(typeof this.delayMove[tileTypes[mapTileData.map[toIndex(x,y)].type].floor]=='undefined') { return false; }

More onload

The window.onload event will also be used to handle the loading of our gameMap and roofList data into our mapTileData object. Just before the end of the function, we'll add the following code to load the data:

    mapTileData.buildMapFromData(gameMap, mapW, mapH);
    mapTileData.addRoofs(roofList);

Also, we can add the following to trigger a tile event and log some text to the console when the player enters tile 2, 2; this is just to show you how the events are handled with our updated engine:

    mapTileData.map[((2*mapW)+2)].eventEnter = function()
        { console.log("Entered tile 2,2"); };

Updating the drawGame method

We only have a little bit more to do now. Firstly, in the drawGame method, after we update the viewport, we're going to add some code to calculate the Tile roof for the tileFrom and tileTo tile of the player Character:

    var playerRoof1 = mapTileData.map[toIndex(
        player.tileFrom[0], player.tileFrom[1])].roof;
    var playerRoof2 = mapTileData.map[toIndex(
        player.tileTo[0], player.tileTo[1])].roof;

Inside our nested tile drawing loops, we'll change the way we get the tileType reference variable, tile, to the following code:

            var tile = tileTypes[mapTileData.map[toIndex(x,y)].type];

After we've called the drawImage method for the current tile, we might need to draw a loop. Firstly, we'll check if the tile has a roof (and that the roofType is not 0; we'll never draw roofType = 0, and this is how we can create none rectangular roofs), and that the tiles roof is not one that the player is currently under or entering (from the reference variables we calculated before these loops):

            if(mapTileData.map[toIndex(x,y)].roofType!=0 &&
                mapTileData.map[toIndex(x,y)].roof!=playerRoof1 &&
                mapTileData.map[toIndex(x,y)].roof!=playerRoof2)
            {

If we're ok to draw this roof, we'll get the sprite as we do for the tile floor, but use the Tile.roofType property instead of its type property, and then draw as we would a floor tile. Once this is done, we can go ahead and close this if block.

                tile = tileTypes[mapTileData.map[toIndex(x,y)].roofType];
                sprite = getFrame(tile.sprite, tile.spriteDuration,
                    gameTime, tile.animated);
                ctx.drawImage(tileset,
                    sprite.x, sprite.y, sprite.w, sprite.h,
                    viewport.offset[0] + (x*tileW),
                    viewport.offset[1] + (y*tileH),
                    tileW, tileH);
            }

Code showing modifications

<!DOCTYPE html>
<html>
<head>

<script type="text/javascript">
var ctx = null;
var gameMap = [
    0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 2, 2, 2, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 0, 0, 0, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 4, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
    0, 1, 1, 2, 2, 2, 2, 2, 0, 4, 4, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 1, 1, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 1, 1, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 0, 0, 0, 1, 1, 4, 1, 1, 0, 0, 0, 2, 0, 0,
    0, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 2, 2, 2, 2, 2, 2, 1, 4, 4, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 4, 1, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
];
var mapTileData = new TileMap();

var roofList = [
    { x:5, y:3, w:4, h:7, data: [
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11
    ]},
    { x:15, y:5, w:5, h:4, data: [
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11
    ]},
    { x:14, y:9, w:6, h:7, data: [
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11
    ]}
];

var tileW = 40, tileH = 40;
var mapW = 20, mapH = 20;
var currentSecond = 0, frameCount = 0, framesLastSecond = 0, lastFrameTime = 0;

var tileset = null, tilesetURL = "tileset.jpg", tilesetLoaded = false;

var gameTime = 0;
var gameSpeeds = [
    {name:"Normal", mult:1},
    {name:"Slow", mult:0.3},
    {name:"Fast", mult:3},
    {name:"Paused", mult:0}
];
var currentSpeed = 0;

var floorTypes = {
    solid    : 0,
    path    : 1,
    water    : 2,
    ice        : 3,
    conveyorU    : 4,
    conveyorD    : 5,
    conveyorL    : 6,
    conveyorR    : 7,
    grass        : 8
};
var tileTypes = {
    0 : { colour:"#685b48", floor:floorTypes.solid, sprite:[{x:0,y:0,w:40,h:40}]    },
    1 : { colour:"#5aa457", floor:floorTypes.grass,    sprite:[{x:40,y:0,w:40,h:40}]    },
    2 : { colour:"#e8bd7a", floor:floorTypes.path,    sprite:[{x:80,y:0,w:40,h:40}]    },
    3 : { colour:"#286625", floor:floorTypes.solid,    sprite:[{x:120,y:0,w:40,h:40}]    },
    4 : { colour:"#678fd9", floor:floorTypes.water,    sprite:[
            {x:160,y:0,w:40,h:40,d:200}, {x:200,y:0,w:40,h:40,d:200},
            {x:160,y:40,w:40,h:40,d:200}, {x:200,y:40,w:40,h:40,d:200},
            {x:160,y:40,w:40,h:40,d:200}, {x:200,y:0,w:40,h:40,d:200}
        ]},
    5 : { colour:"#eeeeff", floor:floorTypes.ice,    sprite:[{x:120,y:120,w:40,h:40}]},
    6 : { colour:"#cccccc", floor:floorTypes.conveyorL,    sprite:[
            {x:0,y:40,w:40,h:40,d:200}, {x:40,y:40,w:40,h:40,d:200},
            {x:80,y:40,w:40,h:40,d:200}, {x:120,y:40,w:40,h:40,d:200}
        ]},
    7 : { colour:"#cccccc", floor:floorTypes.conveyorR,    sprite:[
            {x:120,y:80,w:40,h:40,d:200}, {x:80,y:80,w:40,h:40,d:200},
            {x:40,y:80,w:40,h:40,d:200}, {x:0,y:80,w:40,h:40,d:200}
        ]},
    8 : { colour:"#cccccc", floor:floorTypes.conveyorD,    sprite:[
            {x:160,y:200,w:40,h:40,d:200}, {x:160,y:160,w:40,h:40,d:200},
            {x:160,y:120,w:40,h:40,d:200}, {x:160,y:80,w:40,h:40,d:200}
        ]},
    9 : { colour:"#cccccc", floor:floorTypes.conveyorU,    sprite:[
            {x:200,y:80,w:40,h:40,d:200}, {x:200,y:120,w:40,h:40,d:200},
            {x:200,y:160,w:40,h:40,d:200}, {x:200,y:200,w:40,h:40,d:200}
        ]},
    
    10 : { colour:"#ccaa00", floor:floorTypes.solid, sprite:[{x:40,y:120,w:40,h:40}]},
    11 : { colour:"#ccaa00", floor:floorTypes.solid, sprite:[{x:80,y:120,w:40,h:40}]}
};

function Tile(tx, ty, tt)
{
    this.x            = tx;
    this.y            = ty;
    this.type        = tt;
    this.roof        = null;
    this.roofType    = 0;
    this.eventEnter    = null;
}

function TileMap()
{
    this.map    = [];
    this.w        = 0;
    this.h        = 0;
}
TileMap.prototype.buildMapFromData = function(d, w, h)
{
    this.w        = w;
    this.h        = h;
    
    if(d.length!=(w*h)) { return false; }
    
    this.map.
length    = 0;
    
    for(var y = 0; y < h; y++)
    {
        for(var x = 0; x < w; x++)
        {
            this.map.push( new Tile(x, y, d[((y*w)+x)]) );
        }
    }
    
    return true;
};
TileMap.prototype.addRoofs = function(roofs)
{
    for(var i in roofs)
    {
        var r = roofs[i];
        
        if(r.x < 0 || r.y < 0 || r.x >= this.w || r.y >= this.h ||
            (r.x+r.w)>this.w || (r.y+r.h)>this.h ||
            r.data.length!=(r.w*r.h))
        {
            continue;
        }
        
        for(var y = 0; y < r.h; y++)
        {
            for(var x = 0; x < r.w; x++)
            {
                var tileIdx = (((r.y+y)*this.w)+r.x+x);
                
                this.map[tileIdx].roof = r;
                this.map[tileIdx].roofType = r.data[((y*r.w)+x)];
            }
        }
    }
};

var directions = {
    up        : 0,
    right    : 1,
    down    : 2,
    left    : 3
};

var keysDown = {
    37 : false,
    38 : false,
    39 : false,
    40 : false
};

var viewport = {
    screen        : [0,0],
    startTile    : [0,0],
    endTile        : [0,0],
    offset        : [0,0],
    update        : function(px, py) {
        this.offset[0] = Math.floor((this.screen[0]/2) - px);
        this.offset[1] = Math.floor((this.screen[1]/2) - py);

        var tile = [ Math.floor(px/tileW), Math.floor(py/tileH) ];

        this.startTile[0] = tile[0] - 1 - Math.ceil((this.screen[0]/2) / tileW);
        this.startTile[1] = tile[1] - 1 - Math.ceil((this.screen[1]/2) / tileH);

        if(this.startTile[0] < 0) { this.startTile[0] = 0; }
        if(this.startTile[1] < 0) { this.startTile[1] = 0; }

        this.endTile[0] = tile[0] + 1 + Math.ceil((this.screen[0]/2) / tileW);
        this.endTile[1] = tile[1] + 1 + Math.ceil((this.screen[1]/2) / tileH);

        if(this.endTile[0] >= mapW) { this.endTile[0] = mapW-1; }
        if(this.endTile[1] >= mapH) { this.endTile[1] = mapH-1; }
    }
};

var player = new Character();

function Character()
{
    this.tileFrom    = [1,1];
    this.tileTo        = [1,1];
    this.timeMoved    = 0;
    this.dimensions    = [30,30];
    this.position    = [45,45];

    this.delayMove    = {};
    this.delayMove[floorTypes.path]            = 400;
    this.delayMove[floorTypes.grass]        = 800;
    this.delayMove[floorTypes.ice]            = 300;
    this.delayMove[floorTypes.conveyorU]    = 200;
    this.delayMove[floorTypes.conveyorD]    = 200;
    this.delayMove[floorTypes.conveyorL]    = 200;
    this.delayMove[floorTypes.conveyorR]    = 200;

    this.direction    = directions.up;
    this.sprites = {};
    this.sprites[directions.up]        = [{x:0,y:120,w:30,h:30}];
    this.sprites[directions.right]    = [{x:0,y:150,w:30,h:30}];
    this.sprites[directions.down]    = [{x:0,y:180,w:30,h:30}];
    this.sprites[directions.left]    = [{x:0,y:210,w:30,h:30}];
}
Character.prototype.placeAt = function(x, y)
{
    this.tileFrom    = [x,y];
    this.tileTo        = [x,y];
    this.position    = [((tileW*x)+((tileW-this.dimensions[0])/2)),
        ((tileH*y)+((tileH-this.dimensions[1])/2))];
};
Character.prototype.processMovement = function(t)
{
    if(this.tileFrom[0]==this.tileTo[0] && this.tileFrom[1]==this.tileTo[1]) { return false; }

    var moveSpeed = this.delayMove[tileTypes[gameMap[toIndex(this.tileFrom[0],this.tileFrom[1])]].floor];

    if((t-this.timeMoved)>=moveSpeed)
    {
        this.placeAt(this.tileTo[0], this.tileTo[1]);

        if(mapTileData.map[toIndex(this.tileTo[0], this.tileTo[1])].eventEnter!=null)
        {
            mapTileData.map[toIndex(this.tileTo[0], this.tileTo[1])].eventEnter(this);
        }

        var tileFloor = tileTypes[mapTileData.map[toIndex(this.tileFrom[0], this.tileFrom[1])].type].floor;

        if(tileFloor==floorTypes.ice)
        {
            if(this.canMoveDirection(this.direction))
            {
                this.moveDirection(this.direction, t);
            }
        }
        else if(tileFloor==floorTypes.conveyorL && this.canMoveLeft())    { this.moveLeft(t); }
        else if(tileFloor==floorTypes.conveyorR && this.canMoveRight()) { this.moveRight(t); }
        else if(tileFloor==floorTypes.conveyorU && this.canMoveUp())    { this.moveUp(t); }
        else if(tileFloor==floorTypes.conveyorD && this.canMoveDown())    { this.moveDown(t); }
    }
    else
    {
        this.position[0] = (this.tileFrom[0] * tileW) + ((tileW-this.dimensions[0])/2);
        this.position[1] = (this.tileFrom[1] * tileH) + ((tileH-this.dimensions[1])/2);

        if(this.tileTo[0] != this.tileFrom[0])
        {
            var diff = (tileW / moveSpeed) * (t-this.timeMoved);
            this.position[0]+= (this.tileTo[0]<this.tileFrom[0] ? 0 - diff : diff);
        }
        if(this.tileTo[1] != this.tileFrom[1])
        {
            var diff = (tileH / moveSpeed) * (t-this.timeMoved);
            this.position[1]+= (this.tileTo[1]<this.tileFrom[1] ? 0 - diff : diff);
        }

        this.position[0] = Math.round(this.position[0]);
        this.position[1] = Math.round(this.position[1]);
    }

    return true;
}
Character.prototype.canMoveTo = function(x, y)
{
    if(x < 0 || x >= mapW || y < 0 || y >= mapH) { return false; }
    if(typeof this.delayMove[tileTypes[mapTileData.map[toIndex(x,y)].type].floor]=='undefined') { return false; }
    return true;
};
Character.prototype.canMoveUp        = function() { return this.canMoveTo(this.tileFrom[0], this.tileFrom[1]-1); };
Character.prototype.canMoveDown     = function() { return this.canMoveTo(this.tileFrom[0], this.tileFrom[1]+1); };
Character.prototype.canMoveLeft     = function() { return this.canMoveTo(this.tileFrom[0]-1, this.tileFrom[1]); };
Character.prototype.canMoveRight     = function() { return this.canMoveTo(this.tileFrom[0]+1, this.tileFrom[1]); };
Character.prototype.canMoveDirection = function(d) {
    switch(d)
    {
        case directions.up:
            return this.canMoveUp();
        case directions.down:
            return this.canMoveDown();
        case directions.left:
            return this.canMoveLeft();
        default:
            return this.canMoveRight();
    }
};

Character.prototype.moveLeft    = function(t) { this.tileTo[0]-=1; this.timeMoved = t; this.direction = directions.left; };
Character.prototype.moveRight    = function(t) { this.tileTo[0]+=1; this.timeMoved = t; this.direction = directions.right; };
Character.prototype.moveUp        = function(t) { this.tileTo[1]-=1; this.timeMoved = t; this.direction = directions.up; };
Character.prototype.moveDown    = function(t) { this.tileTo[1]+=1; this.timeMoved = t; this.direction = directions.down; };
Character.prototype.moveDirection = function(d, t) {
    switch(d)
    {
        case directions.up:
            return this.moveUp(t);
        case directions.down:
            return this.moveDown(t);
        case directions.left:
            return this.moveLeft(t);
        default:
            return this.moveRight(t);
    }
};

function toIndex(x, y)
{
    return((y * mapW) + x);
}

function getFrame(sprite, duration, time, animated)
{
    if(!animated) { return sprite[0]; }
    time = time % duration;

    for(x in sprite)
    {
        if(sprite[x].end>=time) { return sprite[x]; }
    }
}

window.onload = function()
{
    ctx = document.getElementById('game').getContext("2d");
    requestAnimationFrame(drawGame);
    ctx.font = "bold 10pt sans-serif";

    window.addEventListener("keydown", function(e) {
        if(e.keyCode>=37 && e.keyCode<=40) { keysDown[e.keyCode] = true; }
    });
    window.addEventListener("keyup", function(e) {
        if(e.keyCode>=37 && e.keyCode<=40) { keysDown[e.keyCode] = false; }
        if(e.keyCode==83)
        {
            currentSpeed = (currentSpeed>=(gameSpeeds.length-1) ? 0 : currentSpeed+1);
        }
    });

    viewport.screen = [document.getElementById('game').width,
        document.getElementById('game').height];

    tileset = new Image();
    tileset.onerror = function()
    {
        ctx = null;
        alert("Failed loading tileset.");
    };
    tileset.onload = function() { tilesetLoaded = true; };
    tileset.src = tilesetURL;

    for(x in tileTypes)
    {
        tileTypes[x]['animated'] = tileTypes[x].sprite.length > 1 ? true : false;

        if(tileTypes[x].animated)
        {
            var t = 0;
            
            for(s in tileTypes[x].sprite)
            {
                tileTypes[x].sprite[s]['start'] = t;
                t+= tileTypes[x].sprite[s].d;
                tileTypes[x].sprite[s]['end'] = t;
            }

            tileTypes[x]['spriteDuration'] = t;
        }
    }
    
    mapTileData.buildMapFromData(gameMap, mapW, mapH);
    mapTileData.addRoofs(roofList);
    mapTileData.map[((2*mapW)+2)].eventEnter = function()
        { console.log("Entered tile 2,2"); };
};

function drawGame()
{
    if(ctx==null) { return; }
    if(!tilesetLoaded) { requestAnimationFrame(drawGame); return; }

    var currentFrameTime = Date.now();
    var timeElapsed = currentFrameTime - lastFrameTime;
    gameTime+= Math.floor(timeElapsed * gameSpeeds[currentSpeed].mult);

    var sec = Math.floor(Date.now()/1000);
    if(sec!=currentSecond)
    {
        currentSecond = sec;
        framesLastSecond = frameCount;
        frameCount = 1;
    }
    else { frameCount++; }

    if(!player.processMovement(gameTime) && gameSpeeds[currentSpeed].mult!=0)
    {
        if(keysDown[38] && player.canMoveUp())            { player.moveUp(gameTime); }
        else if(keysDown[40] && player.canMoveDown())    { player.moveDown(gameTime); }
        else if(keysDown[37] && player.canMoveLeft())    { player.moveLeft(gameTime); }
        else if(keysDown[39] && player.canMoveRight())    { player.moveRight(gameTime); }
    }

    viewport.update(player.position[0] + (player.dimensions[0]/2),
        player.position[1] + (player.dimensions[1]/2));
    
    var playerRoof1 = mapTileData.map[toIndex(
        player.tileFrom[0], player.tileFrom[
1])].roof;
    var playerRoof2 = mapTileData.map[toIndex(
        player.tileTo[0], player.tileTo[1])].roof;

    ctx.fillStyle = "#000000";
    ctx.fillRect(0, 0, viewport.screen[0], viewport.screen[1]);

    for(var y = viewport.startTile[1]; y <= viewport.endTile[1]; ++y)
    {
        for(var x = viewport.startTile[0]; x <= viewport.endTile[0]; ++x)
        {
            var tile = tileTypes[mapTileData.map[toIndex(x,y)].type];

            var sprite = getFrame(tile.sprite, tile.spriteDuration,
                gameTime, tile.animated);
            ctx.drawImage(tileset,
                sprite.x, sprite.y, sprite.w, sprite.h,
                viewport.offset[0] + (x*tileW), viewport.offset[1] + (y*tileH),
                tileW, tileH);
            
            if(mapTileData.map[toIndex(x,y)].roofType!=0 &&
                mapTileData.map[toIndex(x,y)].roof!=playerRoof1 &&
                mapTileData.map[toIndex(x,y)].roof!=playerRoof2)
            {
                tile = tileTypes[mapTileData.map[toIndex(x,y)].roofType];
                sprite = getFrame(tile.sprite, tile.spriteDuration,
                    gameTime, tile.animated);
                ctx.drawImage(tileset,
                    sprite.x, sprite.y, sprite.w, sprite.h,
                    viewport.offset[0] + (x*tileW),
                    viewport.offset[1] + (y*tileH),
                    tileW, tileH);
            }
        }
    }

    var sprite = player.sprites[player.direction];
    ctx.drawImage(tileset,
        sprite[0].x, sprite[0].y, sprite[0].w, sprite[0].h,
        viewport.offset[0] + player.position[0], viewport.offset[1] + player.position[1],
        player.dimensions[0], player.dimensions[1]);

    ctx.fillStyle = "#ff0000";
    ctx.fillText("FPS: " + framesLastSecond, 10, 20);
    ctx.fillText("Game speed: " + gameSpeeds[currentSpeed].name, 10, 40);

    lastFrameTime = currentFrameTime;
    requestAnimationFrame(drawGame);
}
</script>

</head>
<body>

<canvas id="game" width="400" height="400"></canvas>

</body>
</html>

Source code

<!DOCTYPE html>
<html>
<head>

<script type="text/javascript">
var ctx = null;
var gameMap = [
    0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 2, 2, 2, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 0, 0, 0, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 4, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
    0, 1, 1, 2, 2, 2, 2, 2, 0, 4, 4, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 1, 1, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 2, 2, 0, 1, 1, 4, 1, 1, 1, 0, 2, 2, 2, 0,
    0, 1, 1, 2, 1, 0, 0, 0, 0, 1, 1, 4, 1, 1, 0, 0, 0, 2, 0, 0,
    0, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 2, 2, 2, 2, 2, 2, 1, 4, 4, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 4, 1, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 0, 2, 2, 2, 2, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
];
var mapTileData = new TileMap();

var roofList = [
    { x:5, y:3, w:4, h:7, data: [
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11,
        10, 10, 11, 11
    ]},
    { x:15, y:5, w:5, h:4, data: [
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11,
        10, 10, 11, 11, 11
    ]},
    { x:14, y:9, w:6, h:7, data: [
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11,
        10, 10, 10, 11, 11, 11
    ]}
];

var tileW = 40, tileH = 40;
var mapW = 20, mapH = 20;
var currentSecond = 0, frameCount = 0, framesLastSecond = 0, lastFrameTime = 0;

var tileset = null, tilesetURL = "tileset.jpg", tilesetLoaded = false;

var gameTime = 0;
var gameSpeeds = [
    {name:"Normal", mult:1},
    {name:"Slow", mult:0.3},
    {name:"Fast", mult:3},
    {name:"Paused", mult:0}
];
var currentSpeed = 0;

var floorTypes = {
    solid    : 0,
    path    : 1,
    water    : 2,
    ice        : 3,
    conveyorU    : 4,
    conveyorD    : 5,
    conveyorL    : 6,
    conveyorR    : 7,
    grass        : 8
};
var tileTypes = {
    0 : { colour:"#685b48", floor:floorTypes.solid, sprite:[{x:0,y:0,w:40,h:40}]    },
    1 : { colour:"#5aa457", floor:floorTypes.grass,    sprite:[{x:40,y:0,w:40,h:40}]    },
    2 : { colour:"#e8bd7a", floor:floorTypes.path,    sprite:[{x:80,y:0,w:40,h:40}]    },
    3 : { colour:"#286625", floor:floorTypes.solid,    sprite:[{x:120,y:0,w:40,h:40}]    },
    4 : { colour:"#678fd9", floor:floorTypes.water,    sprite:[
            {x:160,y:0,w:40,h:40,d:200}, {x:200,y:0,w:40,h:40,d:200},
            {x:160,y:40,w:40,h:40,d:200}, {x:200,y:40,w:40,h:40,d:200},
            {x:160,y:40,w:40,h:40,d:200}, {x:200,y:0,w:40,h:40,d:200}
        ]},
    5 : { colour:"#eeeeff", floor:floorTypes.ice,    sprite:[{x:120,y:120,w:40,h:40}]},
    6 : { colour:"#cccccc", floor:floorTypes.conveyorL,    sprite:[
            {x:0,y:40,w:40,h:40,d:200}, {x:40,y:40,w:40,h:40,d:200},
            {x:80,y:40,w:40,h:40,d:200}, {x:120,y:40,w:40,h:40,d:200}
        ]},
    7 : { colour:"#cccccc", floor:floorTypes.conveyorR,    sprite:[
            {x:120,y:80,w:40,h:40,d:200}, {x:80,y:80,w:40,h:40,d:200},
            {x:40,y:80,w:40,h:40,d:200}, {x:0,y:80,w:40,h:40,d:200}
        ]},
    8 : { colour:"#cccccc", floor:floorTypes.conveyorD,    sprite:[
            {x:160,y:200,w:40,h:40,d:200}, {x:160,y:160,w:40,h:40,d:200},
            {x:160,y:120,w:40,h:40,d:200}, {x:160,y:80,w:40,h:40,d:200}
        ]},
    9 : { colour:"#cccccc", floor:floorTypes.conveyorU,    sprite:[
            {x:200,y:80,w:40,h:40,d:200}, {x:200,y:120,w:40,h:40,d:200},
            {x:200,y:160,w:40,h:40,d:200}, {x:200,y:200,w:40,h:40,d:200}
        ]},
    
    10 : { colour:"#ccaa00", floor:floorTypes.solid, sprite:[{x:40,y:120,w:40,h:40}]},
    11 : { colour:"#ccaa00", floor:floorTypes.solid, sprite:[{x:80,y:120,w:40,h:40}]}
};

function Tile(tx, ty, tt)
{
    this.x            = tx;
    this.y            = ty;
    this.type        = tt;
    this.roof        = null;
    this.roofType    = 0;
    this.eventEnter    = null;
}

function TileMap()
{
    this.map    = [];
    this.w        = 0;
    this.h        = 0;
}
TileMap.prototype.buildMapFromData = function(d, w, h)
{
    this.w        = w;
    this.h        = h;
    
    if(d.length!=(w*h)) { return false; }
    
    this.map.length    = 0;
    
    for(var y = 0; y < h; y++)
    {
        for(var x = 0; x < w; x++)
        {
            this.map.push( new Tile(x, y, d[((y*w)+x)]) );
        }
    }
    
    return true;
};
TileMap.prototype.addRoofs = function(roofs)
{
    for(var i in roofs)
    {
        var r = roofs[i];
        
        if(r.x < 0 || r.y < 0 || r.x >= this.w || r.y >= this.h ||
            (r.x+r.w)>this.w || (r.y+r.h)>this.h ||
            r.data.length!=(r.w*r.h))
        {
            continue;
        }
        
        for(var y = 0; y < r.h; y++)
        {
            for(var x = 0; x < r.w; x++)
            {
                var tileIdx = (((r.y+y)*this.w)+r.x+x);
                
                this.map[tileIdx].roof = r;
                this.map[tileIdx].roofType = r.data[((y*r.w)+x)];
            }
        }
    }
};

var directions = {
    up        : 0,
    right    : 1,
    down    : 2,
    left    : 3
};

var keysDown = {
    37 : false,
    38 : false,
    39 : false,
    40 : false
};

var viewport = {
    screen        : [0,0],
    startTile    : [0,0],
    endTile        : [0,0],
    offset        : [0,0],
    update        : function(px, py) {
        this.offset[0] = Math.floor((this.screen[0]/2) - px);
        this.offset[1] = Math.floor((this.screen[1]/2) - py);

        var tile = [ Math.floor(px/tileW), Math.floor(py/tileH) ];

        this.startTile[0] = tile[0] - 1 - Math.ceil((this.screen[0]/2) / tileW);
        this.startTile[1] = tile[1] - 1 - Math.ceil((this.screen[1]/2) / tileH);

        if(this.startTile[0] < 0) { this.startTile[0] = 0; }
        if(this.startTile[1] < 0) { this.startTile[1] = 0; }

        this.endTile[0] = tile[0] + 1 + Math.ceil((this.screen[0]/2) / tileW);
        this.endTile[1] = tile[1] + 1 + Math.ceil((this.screen[1]/2) / tileH);

        if(this.endTile[0] >= mapW) { this.endTile[0] = mapW-1; }
        if(this.endTile[1] >= mapH) { this.endTile[1] = mapH-1; }
    }
};

var player = new Character();

function Character()
{
    this.tileFrom    = [1,1];
    this.tileTo        = [1,1];
    this.timeMoved    = 0;
    this.dimensions    = [30,30];
    this.position    = [45,45];

    this.delayMove    = {};
    this.delayMove[floorTypes.path]            = 400;
    this.delayMove[floorTypes.grass]        = 800;
    this.delayMove[floorTypes.ice]            = 300;
    this.delayMove[floorTypes.conveyorU]    = 200;
    this.delayMove[floorTypes.conveyorD]    = 200;
    this.delayMove[floorTypes.conveyorL]    = 200;
    this.delayMove[floorTypes.conveyorR]    = 200;

    this.direction    = directions.up;
    this.sprites = {};
    this.sprites[directions.up]        = [{x:0,y:120,w:30,h:30}];
    this.sprites[directions.right]    = [{x:0,y:150,w:30,h:30}];
    this.sprites[directions.down]    = [{x:0,y:180,w:30,h:30}];
    this.sprites[directions.left]    = [{x:0,y:210,w:30,h:30}];
}
Character.prototype.placeAt = function(x, y)
{
    this.tileFrom    = [x,y];
    this.tileTo        = [x,y];
    this.position    = [((tileW*x)+((tileW-this.dimensions[0])/2)),
        ((tileH*y)+((tileH-this.dimensions[1])/2))];
};
Character.prototype.processMovement = function(t)
{
    if(this.tileFrom[0]==this.tileTo[0]
 && this.tileFrom[1]==this.tileTo[1]) { return false; }

    var moveSpeed = this.delayMove[tileTypes[mapTileData.map[toIndex(this.tileFrom[0],this.tileFrom[1])].type].floor];

    if((t-this.timeMoved)>=moveSpeed)
    {
        this.placeAt(this.tileTo[0], this.tileTo[1]);

        if(mapTileData.map[toIndex(this.tileTo[0], this.tileTo[1])].eventEnter!=null)
        {
            mapTileData.map[toIndex(this.tileTo[0], this.tileTo[1])].eventEnter(this);
        }

        var tileFloor = tileTypes[mapTileData.map[toIndex(this.tileFrom[0], this.tileFrom[1])].type].floor;

        if(tileFloor==floorTypes.ice)
        {
            if(this.canMoveDirection(this.direction))
            {
                this.moveDirection(this.direction, t);
            }
        }
        else if(tileFloor==floorTypes.conveyorL && this.canMoveLeft())    { this.moveLeft(t); }
        else if(tileFloor==floorTypes.conveyorR && this.canMoveRight()) { this.moveRight(t); }
        else if(tileFloor==floorTypes.conveyorU && this.canMoveUp())    { this.moveUp(t); }
        else if(tileFloor==floorTypes.conveyorD && this.canMoveDown())    { this.moveDown(t); }
    }
    else
    {
        this.position[0] = (this.tileFrom[0] * tileW) + ((tileW-this.dimensions[0])/2);
        this.position[1] = (this.tileFrom[1] * tileH) + ((tileH-this.dimensions[1])/2);

        if(this.tileTo[0] != this.tileFrom[0])
        {
            var diff = (tileW / moveSpeed) * (t-this.timeMoved);
            this.position[0]+= (this.tileTo[0]<this.tileFrom[0] ? 0 - diff : diff);
        }
        if(this.tileTo[1] != this.tileFrom[1])
        {
            var diff = (tileH / moveSpeed) * (t-this.timeMoved);
            this.position[1]+= (this.tileTo[1]<this.tileFrom[1] ? 0 - diff : diff);
        }

        this.position[0] = Math.round(this.position[0]);
        this.position[1] = Math.round(this.position[1]);
    }

    return true;
}
Character.prototype.canMoveTo = function(x, y)
{
    if(x < 0 || x >= mapW || y < 0 || y >= mapH) { return false; }
    if(typeof this.delayMove[tileTypes[mapTileData.map[toIndex(x,y)].type].floor]=='undefined') { return false; }
    return true;
};
Character.prototype.canMoveUp        = function() { return this.canMoveTo(this.tileFrom[0], this.tileFrom[1]-1); };
Character.prototype.canMoveDown     = function() { return this.canMoveTo(this.tileFrom[0], this.tileFrom[1]+1); };
Character.prototype.canMoveLeft     = function() { return this.canMoveTo(this.tileFrom[0]-1, this.tileFrom[1]); };
Character.prototype.canMoveRight     = function() { return this.canMoveTo(this.tileFrom[0]+1, this.tileFrom[1]); };
Character.prototype.canMoveDirection = function(d) {
    switch(d)
    {
        case directions.up:
            return this.canMoveUp();
        case directions.down:
            return this.canMoveDown();
        case directions.left:
            return this.canMoveLeft();
        default:
            return this.canMoveRight();
    }
};

Character.prototype.moveLeft    = function(t) { this.tileTo[0]-=1; this.timeMoved = t; this.direction = directions.left; };
Character.prototype.moveRight    = function(t) { this.tileTo[0]+=1; this.timeMoved = t; this.direction = directions.right; };
Character.prototype.moveUp        = function(t) { this.tileTo[1]-=1; this.timeMoved = t; this.direction = directions.up; };
Character.prototype.moveDown    = function(t) { this.tileTo[1]+=1; this.timeMoved = t; this.direction = directions.down; };
Character.prototype.moveDirection = function(d, t) {
    switch(d)
    {
        case directions.up:
            return this.moveUp(t);
        case directions.down:
            return this.moveDown(t);
        case directions.left:
            return this.moveLeft(t);
        default:
            return this.moveRight(t);
    }
};

function toIndex(x, y)
{
    return((y * mapW) + x);
}

function getFrame(sprite, duration, time, animated)
{
    if(!animated) { return sprite[0]; }
    time = time % duration;

    for(x in sprite)
    {
        if(sprite[x].end>=time) { return sprite[x]; }
    }
}

window.onload = function()
{
    ctx = document.getElementById('game').getContext("2d");
    requestAnimationFrame(drawGame);
    ctx.font = "bold 10pt sans-serif";

    window.addEventListener("keydown", function(e) {
        if(e.keyCode>=37 && e.keyCode<=40) { keysDown[e.keyCode] = true; }
    });
    window.addEventListener("keyup", function(e) {
        if(e.keyCode>=37 && e.keyCode<=40) { keysDown[e.keyCode] = false; }
        if(e.keyCode==83)
        {
            currentSpeed = (currentSpeed>=(gameSpeeds.length-1) ? 0 : currentSpeed+1);
        }
    });

    viewport.screen = [document.getElementById('game').width,
        document.getElementById('game').height];

    tileset = new Image();
    tileset.onerror = function()
    {
        ctx = null;
        alert("Failed loading tileset.");
    };
    tileset.onload = function() { tilesetLoaded = true; };
    tileset.src = tilesetURL;

    for(x in tileTypes)
    {
        tileTypes[x]['animated'] = tileTypes[x].sprite.length > 1 ? true : false;

        if(tileTypes[x].animated)
        {
            var t = 0;
            
            for(s in tileTypes[x].sprite)
            {
                tileTypes[x].sprite[s]['start'] = t;
                t+= tileTypes[x].sprite[s].d;
                tileTypes[x].sprite[s]['end'] = t;
            }

            tileTypes[x]['spriteDuration'] = t;
        }
    }
    
    mapTileData.buildMapFromData(gameMap, mapW, mapH);
    mapTileData.addRoofs(roofList);
    mapTileData.map[((2*mapW)+2)].eventEnter = function()
        { console.log("Entered tile 2,2"); };
};

function drawGame()
{
    if(ctx==null) { return; }
    if(!tilesetLoaded) { requestAnimationFrame(drawGame); return; }

    var currentFrameTime = Date.now();
    var timeElapsed = currentFrameTime - lastFrameTime;
    gameTime+= Math.floor(timeElapsed * gameSpeeds[currentSpeed].mult);

    var sec = Math.floor(Date.now()/1000);
    if(sec!=currentSecond)
    {
        currentSecond = sec;
        framesLastSecond = frameCount;
        frameCount = 1;
    }
    else { frameCount++; }

    if(!player.processMovement(gameTime) && gameSpeeds[currentSpeed].mult!=0)
    {
        if(keysDown[38] && player.canMoveUp())            { player.moveUp(gameTime); }
        else if(keysDown[40] && player.canMoveDown())    { player.moveDown(gameTime); }
        else if(keysDown[37] && player.canMoveLeft())    { player.moveLeft(gameTime); }
        else if(keysDown[39] && player.canMoveRight())    { player.moveRight(gameTime); }
    }

    viewport.update(player.position[0] + (player.dimensions[0]/2),
        player.position[1] + (player.dimensions[1]/2));
    
    var playerRoof1 = mapTileData.map[toIndex(
        player.tileFrom[0], player.tileFrom[1])].roof;
    var playerRoof2 = mapTileData.map[toIndex(
        player.tileTo[0], player.tileTo[1])].roof;

    ctx.fillStyle = "#000000";
    ctx.fillRect(0, 0, viewport.screen[0], viewport.screen[1]);

    for(var y = viewport.startTile[1]; y <= viewport.endTile[1]; ++y)
    {
        for(var x = viewport.startTile[0]; x <= viewport.endTile[0]; ++x)
        {
            var tile = tileTypes[mapTileData.map[toIndex(x,y)].type];

            var sprite = getFrame(tile.sprite, tile.spriteDuration,
                gameTime, tile.animated);
            ctx.drawImage(tileset,
                sprite.x, sprite.y, sprite.w, sprite.h,
                viewport.offset[0] + (x*tileW), viewport.offset[1] + (y*tileH),
                tileW, tileH);
            
            if(mapTileData.map[toIndex(x,y)].roofType!=0 &&
                mapTileData.map[toIndex(x,y)].roof!=playerRoof1 &&
                mapTileData.map[toIndex(x,y)].roof!=playerRoof2)
            {
                tile = tileTypes[mapTileData.map[toIndex(x,y)].roofType];
                sprite = getFrame(tile.sprite, tile.spriteDuration,
                    gameTime, tile.animated);
                ctx.drawImage(tileset,
                    sprite.x, sprite.y, sprite.w, sprite.h,
                    viewport.offset[0] + (x*tileW),
                    viewport.offset[1] + (y*tileH),
                    tileW, tileH);
            }
        }
    }

    var sprite = player.sprites[player.direction];
    ctx.drawImage(tileset,
        sprite[0].x, sprite[0].y, sprite[0].w, sprite[0].h,
        viewport.offset[0] + player.position[0], viewport.offset[1] + player.position[1],
        player.dimensions[0], player.dimensions[1]);

    ctx.fillStyle = "#ff0000";
    ctx.fillText("FPS: " + framesLastSecond, 10, 20);
    ctx.fillText("Game speed: " + gameSpeeds[currentSpeed].name, 10, 40);

    lastFrameTime = currentFrameTime;
    requestAnimationFrame(drawGame);
}
</script>

</head>
<body>

<canvas id="game" width="400" height="400"></canvas>

</body>
</html>