Tile events and triggered functions

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

Adding triggers to map tiles

When Characters arrive at certain tiles, you might want certain events to happen. You may wish to have the player win the current level, teleport elsewhere on the map, or have the tile work as a button to modify the map is some way.

View example
tileset

It's very simple to do! In our example, we'll have a few map tiles do some clever things.

We'll begin by modifying our map a bit - this is just to make it clear where things are taking place on the map, and we won't be using any new tile types or making dramatic changes:


var gameMap = [
    0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 2, 1, 2, 4, 2, 1, 7, 7, 7, 1, 1, 1, 1, 1, 1, 0, 2, 2, 0,
    0, 2, 1, 0, 4, 0, 1, 9, 1, 1, 1, 1, 1, 1, 1, 1, 0, 2, 2, 0,
    0, 2, 1, 1, 4, 1, 1, 9, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 0,
    0, 2, 1, 1, 4, 1, 1, 9, 2, 3, 3, 2, 1, 1, 2, 1, 0, 0, 0, 0,
    0, 2, 2, 2, 4, 2, 2, 2, 2, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0,
    0, 2, 1, 1, 4, 2, 4, 1, 1, 1, 1, 6, 6, 6, 2, 1, 1, 1, 1, 0,
    4, 4, 4, 4, 4, 2, 4, 1, 1, 1, 1, 8, 1, 1, 2, 1, 1, 1, 1, 0,
    0, 2, 5, 1, 5, 2, 4, 4, 4, 4, 4, 8, 1, 1, 2, 2, 2, 2, 1, 0,
    0, 1, 5, 5, 5, 2, 3, 2, 1, 1, 4, 8, 1, 1, 1, 3, 3, 2, 1, 0,
    0, 1, 2, 2, 2, 2, 1, 2, 1, 1, 4, 1, 1, 1, 1, 1, 3, 2, 1, 0,
    0, 1, 2, 3, 3, 2, 1, 2, 1, 1, 4, 4, 4, 4, 4, 4, 4, 2, 4, 4,
    0, 1, 2, 3, 3, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 0,
    0, 1, 2, 3, 4, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 0, 1, 2, 1, 0,
    0, 3, 2, 3, 4, 4, 1, 2, 2, 2, 2, 2, 2, 2, 1, 0, 1, 2, 1, 0,
    0, 3, 2, 3, 4, 4, 3, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 3, 0,
    0, 3, 2, 3, 4, 1, 3, 2, 1, 3, 1, 1, 1, 2, 1, 1, 1, 2, 3, 0,
    0, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 1, 1, 2, 2, 2, 2, 2, 3, 0,
    0, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 4, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
];

We'll next be creating an object, tileEvents, where we'll associate the tile index we want an event to be triggered from with a function, by name, or an anonymous function. We'll demonstrate both options here:


var tileEvents = {
    23 : drawBridge,
    25 : drawBridge,
    121 : function(c) { c.placeAt(1,8); },
    161 : function(c) { c.placeAt(1,6); }
};

As you can see, if a Character moves to the tile at index 23 or 25, they'll trigger the drawBridge method. If they move to tile index 121 then the Character, which will be passed to the function as argument c will be moved to tile 1,8, and if the move to tile index 161 they will be moved to tile 1,6. The placeAt Character method, used with tileEvents allows us to create "warp tiles", or "teleporters".

Our example drawBridge function looks like this:


function drawBridge()
{
    gameMap[toIndex(4,5)] = (gameMap[toIndex(4,5)]==4 ? 2 : 4);
}

It changes one tile in the gameMap, at position 4,5. After checking its current type, it changes it to either 2 or 4. If you look at the tileTypes array, you'll see these correspond to path and water respectively.

Finally, in the Character processMovement method, we'll add another check once the Character has reached their destination tile. Before checking if the floorType is Ice or Conveyor, we'll check if there's an entry in the tilesEvents object for the Characters current tile:


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

If there is, we'll call the corresponding function, passing a reference to the Character (this) as an argument, in case the function wants to do something with the Character directly, and then close this if block:


            tileEvents[toIndex(this.tileTo[0], this.tileTo[1])](this);
        }

Save the HTML document and load it up, or load our online example, and try moving your Character to the bits of path tile directly to the right, or directly down, from the starting position!

Modified 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, 2, 1, 2, 4, 2, 1, 7, 7, 7, 1, 1, 1, 1, 1, 1, 0, 2, 2, 0,
    0, 2, 1, 0, 4, 0, 1, 9, 1, 1, 1, 1, 1, 1, 1, 1, 0, 2, 2, 0,
    0, 2, 1, 1, 4, 1, 1, 9, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 0,
    0, 2, 1, 1, 4, 1, 1, 9, 2, 3, 3, 2, 1, 1, 2, 1, 0, 0, 0, 0,
    0, 2, 2, 2, 4, 2, 2, 2, 2, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0,
    0, 2, 1, 1, 4, 2, 4, 1, 1, 1, 1, 6, 6, 6, 2, 1, 1, 1, 1, 0,
    4, 4, 4, 4, 4, 2, 4, 1, 1, 1, 1, 8, 1, 1, 2, 1, 1, 1, 1, 0,
    0, 2, 5, 1, 5, 2, 4, 4, 4, 4, 4, 8, 1, 1, 2, 2, 2, 2, 1, 0,
    0, 1, 5, 5, 5, 2, 3, 2, 1, 1, 4, 8, 1, 1, 1, 3, 3, 2, 1, 0,
    0, 1, 2, 2, 2, 2, 1, 2, 1, 1, 4, 1, 1, 1, 1, 1, 3, 2, 1, 0,
    0, 1, 2, 3, 3, 2, 1, 2, 1, 1, 4, 4, 4, 4, 4, 4, 4, 2, 4, 4,
    0, 1, 2, 3, 3, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 0,
    0, 1, 2, 3, 4, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 0, 1, 2, 1, 0,
    0, 3, 2, 3, 4, 4, 1, 2, 2, 2, 2, 2, 2, 2, 1, 0, 1, 2, 1, 0,
    0, 3, 2, 3, 4, 4, 3, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 3, 0,
    0, 3, 2, 3, 4, 1, 3, 2, 1, 3, 1, 1, 1, 2, 1, 1, 1, 2, 3, 0,
    0, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 1, 1, 2, 2, 2, 2, 2, 3, 0,
    0, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 4, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
];
var tileW = 40, tileH = 40;
var mapW = 20, mapH = 20;
var currentSecond = 0, frameCount = 0, framesLastSecond = 0, lastFrameTime = 0;

var tileEvents = {
    23 : drawBridge,
    25 : drawBridge,
    121 : function(c) { c.placeAt(1,8); },
    161 : function(c) { c.placeAt(1,6); }
};
function drawBridge()
{
    gameMap[toIndex(4,5)] = (gameMap[toIndex(4,5)]==4 ? 2 : 4);
}

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}
        ]}
};

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(typeof tileEvents[toIndex(this.tileTo[0], this.tileTo[1])]!='undefined')
        {
            tileEvents[toIndex(this.tileTo[0], this.tileTo[1])](this);
        }

        var tileFloor = tileTypes[gameMap[toIndex(this.tileFrom[0], this.tileFrom[1])]].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[gameMap[toIndex(x,y)]].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;
        }
    }
};

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));

    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[gameMap[toIndex(x,y)]];
            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);
        }
    }

    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>

Tile triggers 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, 2, 1, 2, 4, 2, 1, 7, 7, 7, 1, 1, 1, 1, 1, 1, 0, 2, 2, 0,
    0, 2, 1, 0, 4, 0, 1, 9, 1, 1, 1, 1, 1, 1, 1, 1, 0, 2, 2, 0,
    0, 2, 1, 1, 4, 1, 1, 9, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 0,
    0, 2, 1, 1, 4, 1, 1, 9, 2, 3, 3, 2, 1, 1, 2, 1, 0, 0, 0, 0,
    0, 2, 2, 2, 4, 2, 2, 2, 2, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0,
    0, 2, 1, 1, 4, 2, 4, 1, 1, 1, 1, 6, 6, 6, 2, 1, 1, 1, 1, 0,
    4, 4, 4, 4, 4, 2, 4, 1, 1, 1, 1, 8, 1, 1, 2, 1, 1, 1, 1, 0,
    0, 2, 5, 1, 5, 2, 4, 4, 4, 4, 4, 8, 1, 1, 2, 2, 2, 2, 1, 0,
    0, 1, 5, 5, 5, 2, 3, 2, 1, 1, 4, 8, 1, 1, 1, 3, 3, 2, 1, 0,
    0, 1, 2, 2, 2, 2, 1, 2, 1,
 1, 4, 1, 1, 1, 1, 1, 3, 2, 1, 0,
    0, 1, 2, 3, 3, 2, 1, 2, 1, 1, 4, 4, 4, 4, 4, 4, 4, 2, 4, 4,
    0, 1, 2, 3, 3, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 0,
    0, 1, 2, 3, 4, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 0, 1, 2, 1, 0,
    0, 3, 2, 3, 4, 4, 1, 2, 2, 2, 2, 2, 2, 2, 1, 0, 1, 2, 1, 0,
    0, 3, 2, 3, 4, 4, 3, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 3, 0,
    0, 3, 2, 3, 4, 1, 3, 2, 1, 3, 1, 1, 1, 2, 1, 1, 1, 2, 3, 0,
    0, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 1, 1, 2, 2, 2, 2, 2, 3, 0,
    0, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 4, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
];
var tileW = 40, tileH = 40;
var mapW = 20, mapH = 20;
var currentSecond = 0, frameCount = 0, framesLastSecond = 0, lastFrameTime = 0;

var tileEvents = {
    23 : drawBridge,
    25 : drawBridge,
    121 : function(c) { c.placeAt(1,8); },
    161 : function(c) { c.placeAt(1,6); }
};
function drawBridge()
{
    gameMap[toIndex(4,5)] = (gameMap[toIndex(4,5)]==4 ? 2 : 4);
}

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}
        ]}
};

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(typeof tileEvents[toIndex(this.tileTo[0], this.tileTo[1])]!='undefined')
        {
            tileEvents[toIndex(this.tileTo[0], this.tileTo[1])](this);
        }

        var tileFloor = tileTypes[gameMap[toIndex(this.tileFrom[0], this.tileFrom[1])]].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[gameMap[toIndex(x,y)]].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;
        }
    }
};

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));

    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[gameMap[toIndex(x,y)]];
            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);
        }
    }

    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>