Inventories and Items

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

Inventory and Item classes

In this lesson we'll be looking at collectable items and simple inventories. In our example below, you can now press the letter P on the keyboard when standing on tiles with the stars to collect them!

You'll need the updated tileset to follow along with this example:

Tileset
Inventories and items example

We'll start by creating our list of itemTypes. This will be a map/associative array, where each item is an object represented by a unique numeric ID, with the properties name, the name of the item, maxStack, the maximum number of this item that can be in a single stack, sprite, which is the sprite details on the tileset, and offset, the amount the sprite position will be modified by relative to the top-left of the tile they occupy when drawn on the map.

var itemTypes = {
    1 : {
        name : "Star",
        maxStack : 2,
        sprite : [{x:240,y:0,w:40,h:40}],
        offset : [0,0]
    }
};

We'll now be creating a Stack object, that will keep track of a stack of items on the floor or in an inventory. The Stack will have two attributes; type, the ID of the itemTypes entry that this stack will contain, and qty, the number of items in the stack.

These will be populated by two arguments given when creating a new stack, id and qty.

function Stack(id, qty)
{
    this.type = id;
    this.qty = qty;
}

We'll also add an Inventory class for keeping track of groups of stacks. This will be created with one argument, s, which will be the maximum number of stacks this inventory can contain. It will have two parameters; spaces, the maximum number of stacks the inventory can contain (set by the sargument), and stacks, an empty array that will contain a list of Stack objects when items are added to the inventory.

function Inventory(s)
{
    this.spaces        = s;
    this.stacks        = [];
}

Our Inventory class will have an addItems method that will take two arguments; id, the itemTypes entry for the type of items we're trying to add, and qty, the number of this item we wish to add. We'll begin by looping through all of the possible spaces for stacks in the inventory:

Inventory.prototype.addItems = function(id, qty)
{
    for(var i = 0; i < this.spaces; i++)
    {

If the current iteration of our loop is a higher value than the current length of the stacks array, we can just add a new Stack to the array. We start by calculating maxHere - the value of which will be the maximum stack size for the given item type, or the qty argument, whichever is smaller.

        if(this.stacks.length<=i)
        {
            var maxHere = (qty > itemTypes[id].maxStack ?
                itemTypes[id].maxStack : qty);

We'll then push a new Stack object of the given item type with the quantity maxHere on to the stacks array, and reduce the qty argument by the value of maxHere before closing this if block:

            this.stacks.push(new Stack(id, maxHere));
            
            qty-= maxHere;
        }

Otherwise, we'll check and see if the current stacks entry is of the same item type id as that passed to the function, and if the Stack.qty is less than the maximum stack size for this item type:

        else if(this.stacks[i].type == id &&
            this.stacks[i].qty < itemTypes[id].maxStack)
        {

We'll then calculate how many items we can add to this stack, by subtracting the qty argument from the item types maximum stack size and storing this in the maxHere variable. If the value is higher than qty, we simply change it to this value.

We then increase the Stack.qty value by maxHere, remove this amount from the qty argument, and close the if block.

            var maxHere = (itemTypes[id].maxStack - this.stacks[i].qty);
            if(maxHere > qty) { maxHere = qty; }
            
            this.stacks[i].qty+= maxHere;
            qty-= maxHere;
        }

We check at the end of each loop iteration to see if qty has got down to 0 - if so we return 0; to leave the method and close the loop.

        if(qty==0) { return 0; }
    }

At the end of the method, we return the remaining qty value. This is the number of items we couldn't find space for in the inventory.

    return qty;
};

Placed items

When items are placed on the ground, alone or in a stack, they'll be represented by the PlacedItemStack object. Instances are created with the arguments id, the itemTypes entry id, and qty, the number of items in this stack. The class also has the properties x, y, which will be the map position this stack is placed at.

function PlacedItemStack(id, qty)
{
    this.type = id;
    this.qty = qty;
    this.x = 0;
    this.y = 0;
}

To set the position of the stack on the map, it will have a method called placeAt, which takes the arguments nx, ny, the coordinates to place the stack at. The method begins by checking if the tile at the stacks current x, y position contains a reference to this stack, and if so removes it:

PlacedItemStack.prototype.placeAt = function(nx, ny)
{
    if(mapTileData.map[toIndex(this.x, this.y)].itemStack==this)
    {
        mapTileData.map[toIndex(this.x, this.y)].itemStack = null;
    }

After, it simply sets the x, y properties of the stack, and updates the map tile at the given positions itemStack reference to point to this stack:

    this.x = nx;
    this.y = ny;
    
    mapTileData.map[toIndex(nx, ny)].itemStack = this;
};

Modifying classes and code

We now need to update some of our existing code and classes. Firstly, we'll add the parameter itemStack to the Tile object:

    this.itemStack    = null;

Modifying the Character class

Each Character will have its own inventory. To the Character class, we'll add the inventory property, which will have an Inventory object with 3 slots:

    this.inventory = new Inventory(3);

The Character class will have a new pickUp method for picking up items the character is standing on. This method will firstly check that the character is not currently moving, by ensuring the x, y properties of the tileFrom, tileTo parameters are the same. If they are not, we won't allow the character to try and pick up yet, so we'll return false:

Character.prototype.pickUp = function()
{
    if(this.tileTo[0]!=this.tileFrom[0] ||
        this.tileTo[1]!=this.tileFrom[1])
    {
        return false;
    }

We'll now get a reference to the PlacedItemStack on the tile on which the character is standing in the is variable:

    var is = mapTileData.map[toIndex(this.tileFrom[0],
                this.tileFrom[1])].itemStack;

If the is variable is not null, we'll add the item stack to the characters inventory and store the result of the addItems method to the remains variable.

    if(is!=null)
    {
        var remains = this.inventory.addItems(is.type, is.qty);

If the characters invetory was filled before collecting all the items here, the value of remains will be greater than 0 - in this case we'll just update the qty of the PlacedItemStack, is, to the number of remaining items.

Otherwise, we'll clear the reference to the PlacedItemStack for the current tile, as the stack has been "picked up". After, we can close the else/if block and the method itself, after returning true to show we collected some items (or at least attempted to do so).

        if(remains) { is.qty = remains; }
        else
        {
            mapTileData.map[toIndex(this.tileFrom[0],
                this.tileFrom[1])].itemStack = null;
        }
    }
    
    return true;
};

onload item placement

To place some example items on our map, we'll add a bit of code to the window.onload event handler. This will just create a couple of lines of the star item, one horizontally and one vertically. At each loop iteration, we just create a new PlacedItemStack object with the itemTypes id 1, and the qty 1. We then place the stack at an x, y position determined by our loops.

    for(var i = 3; i < 8; i++)
    {
        var ps = new PlacedItemStack(1, 1); ps.placeAt(i, 1);
    }
    for(var i = 3; i < 8; i++)
    {
        var ps = new PlacedItemStack(1, 1); ps.placeAt(3, i);
    }

Drawing items & Inventory

To draw items to the map, we'll add the following code inside our nested y, x drawing loops. After the if(z==0) { ... } section used to draw floor tiles, we'll add an else if statement to check the z value is 1, and if so we'll get a reference is to the itemStack property of the current tile.

If is is not null, we'll draw the sprite for the corresponding itemTypes entry here:

            else if(z==1)
            {
                var is = mapTileData.map[toIndex(x,y)].itemStack;
                if(is!=null)
                {
                    var sprite = itemTypes[is.type].sprite;
                    
                    ctx.drawImage(tileset,
                        sprite[0].x, sprite[0].y,
                        sprite[0].w, sprite[0].h,
                        viewport.offset[0] + (x*tileW) + itemTypes[is.type].offset[0],
                        viewport.offset[1] + (y*tileH) + itemTypes[is.type].offset[1],
                        sprite[0].w, sprite[0].h);
                }
            }

After the nested drawing loops, we'll draw the player inventory. We'll set the text alignment to the right, then loop over all the possible spaces in the players inventory, and draw a filled rectangle at the bottom-left (moving right for each successive inventory slot) for each iteration:

    ctx.textAlign = "right";
    
    for(var i = 0; i < player.inventory.spaces; i++)
    {
        ctx.fillStyle = "#ddccaa";
        ctx.fillRect(10 + (i * 50), 350,
            40, 40);

If there's an entry in the players inventory at the specified offset, we'll get a reference to the itemTypes entry for this stack, and its sprite, and draw the sprite here:

        if(typeof player.inventory.stacks[i]!='undefined')
        {
            var it = itemTypes[player.inventory.stacks[i].type];
            var sprite = it.sprite;
                    
            ctx.drawImage(tileset,
                sprite[0].x, sprite[0].y,
                sprite[0].w, sprite[0].h,
                10 + (i * 50) + it.offset[0],
                350 + it.offset[1],
                sprite[0].w, sprite[0].h);

If the qty of the stack at this point in the inventory is more than one, we'll show the quantity at the bottom-right of this inventory icon:

            if(player.inventory.stacks[i].qty>1)
            {
                ctx.fillStyle = "#000000";
                ctx.fillText("" + player.inventory.stacks[i].qty,
                    10 + (i*50) + 38,
                    350 + 38);
            }

We'll then close the if statement and the loop. After the loop has completed, we'll reset the text alignment to the left.

        }
    }
    ctx.textAlign = "left";

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 itemTypes = {
    1 : {
        name : "Star",
        maxStack : 2,
        sprite : [{x:240,y:0,w:40,h:40}],
        offset : [0,0]
    }
};

function Stack(id, qty)
{
    this.type = id;
    this.qty = qty;
}
function Inventory(s)
{
    this.spaces        = s;
    this.stacks        = [];
}
Inventory.prototype.addItems = function(id, qty)
{
    for(var i = 0; i < this.spaces; i++)
    {
        if(this.stacks.length<=i)
        {
            var maxHere = (qty > itemTypes[id].maxStack ?
                itemTypes[id].maxStack : qty);
            this.stacks.push(new Stack(id, maxHere));
            
            qty-= maxHere;
        }
        else if(this.stacks[i].type == id &&
            this.stacks[i].qty < itemTypes[id].maxStack)
        {
            var maxHere = (itemTypes[id].maxStack - this.stacks[i].qty);
            if(maxHere > qty) { maxHere = qty; }
            
            this.stacks[i].qty+= maxHere;
            qty-= maxHere;
        }
        
        if(qty==0) { return 0; }
    }
    
    return qty;
};

function PlacedItemStack(id, qty)
{
    this.type = id;
    this.qty = qty;
    this.x = 0;
    this.y = 0;
}
PlacedItemStack.prototype.placeAt = function(nx, ny)
{
    if(mapTileData.map[toIndex(this.x, this.y)].itemStack==this)
    {
        mapTileData.map[toIndex(this.x, this.y)].itemStack = null;
    }
    
    this.x = nx;
    this.y = ny;
    
    mapTileData.map[toIndex(nx, ny)].itemStack = this;
};

var objectCollision = {
    none        : 0,
    solid        : 1
};
var objectTypes = {
    1 : {
        name : "Box",
        sprite : [{x:40,y:160,w:40,h:40}],
        offset : [0,0],
        collision : objectCollision.solid,
        zIndex : 1
    },
    2 : {
        name : "Broken Box",
        sprite : [{x:40,y:200,w:40,h:40}],
        offset : [0,0],
        collision : objectCollision.none,
        zIndex : 1
    },
    3 : {
        name : "Tree top",
        sprite : [{x:80,y:160,w:80,h:80}],
        offset : [-20,-20],
        collision : objectCollision.solid,
        zIndex : 3
    }
};
function MapObject(nt)
{
    this.x        = 0;
    this.y        = 0;
    this.type    = nt;
}
MapObject.prototype.placeAt = function(nx, ny)
{
    if(mapTileData.map[toIndex(this.x, this.y)].object==this)
    {
        mapTileData.map[toIndex(this.x, this.y)].object = null;
    }
    
    this.x = nx;
    this.y = ny;
    
    mapTileData.map[toIndex(nx, ny)].object = this;
};

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:[
n            {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;
    this.object        = null;
    this.itemStack    = null;
}

function TileMap()
{
    this.map    = [];
    this.w        = 0;
    this.h        = 0;
    this.levels    = 4;
}
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,
    80 : 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}];
    
    this.inventory = new Inventory(3);
}
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; }
    if(mapTileData.map[toIndex(x,y)].object!=null)
    {
        var o = mapTileData.map[toIndex(x,y)].object;
        if(objectTypes[o.type].collision==objectCollision.solid)
        {
            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);
    }
};
Character.prototype.pickUp = function()
{
    if(this.tileTo[0]!=this.tileFrom[0] ||
        this.tileTo[1]!=this.tileFrom[1])
    {
        return false;
    }
    
    var is = mapTileData.map[toIndex(this.tileFrom[0],
                this.tileFrom[1])].itemStack;
    
    if(is!=null)
    {
        var remains = this.inventory.addItems(is.type, is.qty);

        if(remains) { is.qty = remains; }
        else
        {
            mapTileData.map[toIndex(this.tileFrom[0],
                this.tileFrom[1])].itemStack = null;
        }
    }
    
    return true;
};

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; }
        if(e.keyCode==80) { 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);
        }
        if(e.keyCode==80) { keysDown[e.keyCode] = false; }
    });

    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"); };
    
    var mo1 = new MapObject(1); mo1.placeAt(2, 4);
    var mo2 = new MapObject(2); mo2.placeAt(2, 3);
    
    var mo11 = new MapObject(1); mo11.placeAt(6, 4);
    var mo12 = new MapObject(2); mo12.placeAt(7, 4);
    
    var mo4 = new MapObject(3); mo4.placeAt(4, 5);
    var mo5 = new MapObject(3); mo5.placeAt(4, 8);
    var mo6 = new MapObject(3); mo6.placeAt(4, 11);
    
    var mo7 = new MapObject(3); mo7.placeAt(2, 6);
    var mo8 = new MapObject(3); mo8.placeAt(2, 9);
    var mo9 = new MapObject(3); mo9.placeAt(2, 12);
    
    for(var i = 3; i < 8; i++)
    {
        var ps = new PlacedItemStack(1, 1); ps.placeAt(i, 1);
    }
    for(var i = 3; i < 8; i++)
    {
        var ps = new PlacedItemStack(1, 1); ps.placeAt(3, i);
    }
};

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); }
        else if(keysDown[80]) { player.pickUp(); }
    }

    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 z = 0; z < mapTileData.levels; z++)
    {

    for(var y = viewport.startTile[1]; y <= viewport.endTile[1]; ++y)
    {
        for(var x = viewport.startTile[0]; x <= viewport.endTile[0]; ++x)
        {
            if(z==0)
            {
            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);
            }
            else if(z==1)
            {
                var is = mapTileData.map[toIndex(x,y)].itemStack;
                if(is!=null)
                {
                    var sprite = itemTypes[is.type].sprite;
                    
                    ctx.drawImage(tileset,
                        sprite[0].x, sprite[0].y,
                        sprite[0].w, sprite[0].h,
                        viewport.offset[0] + (x*tileW) + itemTypes[is.type].offset[0],
                        viewport.offset[1] + (y*tileH) + itemTypes[is.type].offset[1],
                        sprite[0].w, sprite[0].h);
                }
            }
            
            var o = mapTileData.map[toIndex(x,y)].object;
            if(o!=null && objectTypes[o.type].zIndex==z)
            {
                var ot = objectTypes[o.type];
                 
                ctx.drawImage(tileset,
                    ot.sprite[0].x, ot.sprite[0].y,
                    ot.sprite[0].w, ot.sprite[0].h,
                    viewport.offset[0] + (x*tileW) + ot.offset[0],
                    viewport.offset[1] + (y*tileH) + ot.offset[1],
                    ot.sprite[0].w, ot.sprite[0].h);
            }
            
            if(z==2 &&
                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);
            }
        }
    }
    
        if(z==1)
        {
            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.textAlign = "right";
    
    for(var i = 0; i < player.inventory.spaces; i++)
    {
        ctx.fillStyle = "#ddccaa";
        ctx.fillRect(10 + (i * 50), 350,
            40, 40);
        
        if(typeof player.inventory.stacks[i]!='undefined')
        {
            var it = itemTypes[player.inventory.stacks[i].type];
            var sprite = it.sprite;
                    
            ctx.drawImage(tileset,
                sprite[0].x, sprite[0].y,
                sprite[0].w, sprite[0].h,
                10 + (i * 50) + it.offset[0],
                350 + it.offset[1],
                sprite[0].w, sprite[0].h);
            
            if(player.inventory.stacks[i].qty>1)
            {
                ctx.fillStyle = "#000000";
                ctx.fillText("" + player.inventory.stacks[i].qty,
                    10 + (i*50) + 38,
                    350 + 38);
            }
        }
    }
    ctx.textAlign = "left";

    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>

Javascript 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 itemTypes = {
    1 : {
        name : "Star",
        maxStack : 2,
        sprite : [{x:240,y:0,w:40,h:40}],
        offset : [0,0]
    }
};

function Stack(id, qty)
{
    this.type = id;
    this.qty = qty;
}
function Inventory(s)
{
    this.spaces        = s;
    this.stacks        = [];
}
Inventory.prototype.addItems = function(id, qty)
{
    for(var i = 0; i < this.spaces; i++)
    {
        if(this.stacks.length<=i)
        {
            var maxHere = (qty > itemTypes[id].maxStack ?
                itemTypes[id].maxStack : qty);
            this.stacks.push(new Stack(id, maxHere));
            
            qty-= maxHere;
        }
        else if(this.stacks[i].type == id &&
            this.stacks[i].qty < itemTypes[id].maxStack)
        {
            var maxHere = (itemTypes[id].maxStack - this.stacks[i].qty);
            if(maxHere > qty) { maxHere = qty; }
            
            this.stacks[i].qty+= maxHere;
            qty-= maxHere;
        }
        
        if(qty==0) { return 0; }
    }
    

return qty;
};

function PlacedItemStack(id, qty)
{
    this.type = id;
    this.qty = qty;
    this.x = 0;
    this.y = 0;
}
PlacedItemStack.prototype.placeAt = function(nx, ny)
{
    if(mapTileData.map[toIndex(this.x, this.y)].itemStack==this)
    {
        mapTileData.map[toIndex(this.x, this.y)].itemStack = null;
    }
    
    this.x = nx;
    this.y = ny;
    
    mapTileData.map[toIndex(nx, ny)].itemStack = this;
};

var objectCollision = {
    none        : 0,
    solid        : 1
};
var objectTypes = {
    1 : {
        name : "Box",
        sprite : [{x:40,y:160,w:40,h:40}],
        offset : [0,0],
        collision : objectCollision.solid,
        zIndex : 1
    },
    2 : {
        name : "Broken Box",
        sprite : [{x:40,y:200,w:40,h:40}],
        offset : [0,0],
        collision : objectCollision.none,
        zIndex : 1
    },
    3 : {
        name : "Tree top",
        sprite : [{x:80,y:160,w:80,h:80}],
        offset : [-20,-20],
        collision : objectCollision.solid,
        zIndex : 3
    }
};
function MapObject(nt)
{
    this.x        = 0;
    this.y        = 0;
    this.type    = nt;
}
MapObject.prototype.placeAt = function(nx, ny)
{
    if(mapTileData.map[toIndex(this.x, this.y)].object==this)
    {
        mapTileData.map[toIndex(this.x, this.y)].object = null;
    }
    
    this.x = nx;
    this.y = ny;
    
    mapTileData.map[toIndex(nx, ny)].object = this;
};

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;
    this.object        = null;
    this.itemStack    = null;
}

function TileMap()
{
    this.map    = [];
    this.w        = 0;
    this.h        = 0;
    this.levels    = 4;
}
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,
    80 : 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}];
    
    this.inventory = new Inventory(3);
}
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; }
    if(mapTileData.map[toIndex(x,y)].object!=null)
    {
        var o = mapTileData.map[toIndex(x,y)].object;
        if(objectTypes[o.type].collision==objectCollision.solid)
        {
            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);
    }
};
Character.prototype.pickUp = function()
{
    if(this.tileTo[0]!=this.tileFrom[0] ||
        this.tileTo[1]!=this.tileFrom[1])
    {
        return false;
    }
    
    var is = mapTileData.map[toIndex(this.tileFrom[0],
                this.tileFrom[1])].itemStack;
    
    if(is!=null)
    {
        var remains = this.inventory.addItems(is.type, is.qty);

        if(remains) { is.qty = remains; }
        else
        {
            mapTileData.map[toIndex(this.tileFrom[0],
                this.tileFrom[1])].itemStack = null;
        }
    }
    
    return true;
};

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; }
        if(e.keyCode==80) { 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);
        }
        if(e.keyCode==80) { keysDown[e.keyCode] = false; }
    });

    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"); };
    
    var mo1 = new MapObject(1); mo1.placeAt(2, 4);
    var mo2 = new MapObject(2); mo2.placeAt(2, 3);
    
    var mo11 = new MapObject(1); mo11.placeAt(6, 4);
    var mo12 = new MapObject(2); mo12.placeAt(7, 4);
    
    var mo4 = new MapObject(3); mo4.placeAt(4, 5);
    var mo5 = new MapObject(3); mo5.placeAt(4, 8);
    var mo6 = new MapObject(3); mo6.placeAt(4, 11);
    
    var mo7 = new MapObject(3); mo7.placeAt(2, 6);
    var mo8 = new MapObject(3); mo8.placeAt(2, 9);
    var mo9 = new MapObject(3); mo9.placeAt(2, 12);
    
    for(var i = 3; i < 8; i++)
    {
        var ps = new PlacedItemStack(1, 1); ps.placeAt(i, 1);
    }
    for(var i = 3; i < 8; i++)
    {
        var ps = new PlacedItemStack(1, 1); ps.placeAt(3, i);
    }
};

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); }
        else if(keysDown[80]) { player.pickUp(); }
    }

    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 z = 0; z < mapTileData.levels; z++)
    {

    for(var y = viewport.startTile[1]; y <= viewport.endTile[1]; ++y)
    {
        for(var x = viewport.startTile[0]; x <= viewport.endTile[0]; ++x)
        {
            if(z==0)
            {
            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);
            }
            else if(z==1)
            {
                var is = mapTileData.map[toIndex(x,y)].itemStack;
                if(is!=null)
                {
                    var sprite = itemTypes[is.type].sprite;
                    
                    ctx.drawImage(tileset,
                        sprite[0].x, sprite[0].y,
                        sprite[0].w, sprite[0].h,
                        viewport.offset[0] + (x*tileW) + itemTypes[is.type].offset[0],
                        viewport.offset[1] + (y*tileH) + itemTypes[is.type].offset[1],
                        sprite[0].w, sprite[0].h);
                }
            }
            
            var o = mapTileData.map[toIndex(x,y)].object;
            if(o!=null && objectTypes[o.type].zIndex==z)
            {
                var ot = objectTypes[o.type];
                 
                ctx.drawImage(tileset,
                    ot.sprite[0].x, ot.sprite[0].y,
                    ot.sprite[0].w, ot.sprite[0].h,
                    viewport.offset[0] + (x*tileW) + ot.offset[0],
                    viewport.offset[1] + (y*tileH) + ot.offset[1],
                    ot.sprite[0].w, ot.sprite[0].h);
            }
            
            if(z==2 &&
                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);
            }
        }
    }
    
        if(z==1)
        {
            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.textAlign = "right";
    
    for(var i = 0; i < player.inventory.spaces; i++)
    {
        ctx.fillStyle = "#ddccaa";
        ctx.fillRect(10 + (i * 50), 350,
            40, 40);
        
        if(typeof player.inventory.stacks[i]!='undefined')
        {
            var it = itemTypes[player.inventory.stacks[i].type];
            var sprite = it.sprite;
                    
            ctx.drawImage(tileset,
                sprite[0].x, sprite[0].y,
                sprite[0].w, sprite[0].h,
                10 + (i * 50) + it.offset[0],
                350 + it.offset[1],
                sprite[0].w, sprite[0].h);
            
            if(player.inventory.stacks[i].qty>1)
            {
                ctx.fillStyle = "#000000";
                ctx.fillText("" + player.inventory.stacks[i].qty,
                    10 + (i*50) + 38,
                    350 + 38);
            }
        }
    }
    ctx.textAlign = "left";

    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>