"use strict";

// types of terrain in a map.  Assigned as bitflags so searching can look for multiple at once
var DEFINE_MAP_TERRAIN_TYPES	= {
	AIR		: 1,
	WATER	: 2,
	GROUND	: 4
};
var DEFINE_TERRAIN_DETAILED		= -1;
var DEFINE_MAP_TILE_SIZE		= 4;
var DEFINE_MAP_TILE_PIXEL_SIZE	= 1<<DEFINE_MAP_TILE_SIZE;
var DEFINE_MAP_TILE_COORD_MASK	= DEFINE_MAP_TILE_PIXEL_SIZE - 1;

/*
* container class for one (DEFINE_MAP_TILE_PIXEL_SIZExDEFINE_MAP_TILE_PIXEL_SIZE) tile on a map
* manages the image index and collision data

* imageIndex: the index of the image tile in the associated map's TileSheet
* terrainType: for use in collision, what this whole tile is made of
* terrainDetail: for tiles which are not all one thing (e.g. slopes),
*	an array of DEFINE_MAP_TILE_PIXEL_SIZE arrays of DEFINE_MAP_TILE_PIXEL_SIZE terrain types.
*/
var MapTile = function(imageIndex, terrainType, terrainDetail){
	this.image_index = (imageIndex!=null && !isNaN(parseInt(imageIndex))) ? parseInt(imageIndex) : 0;
	this.image_index = Math.max(0,this.image_index);

	this.terrain_type = DEFINE_MAP_TERRAIN_TYPES.AIR;
	for (var t in DEFINE_MAP_TERRAIN_TYPES){
		if (terrainType === DEFINE_MAP_TERRAIN_TYPES[t]){
			this.terrain_type=DEFINE_MAP_TERRAIN_TYPES[t];
			break;
		}
	}

	this.terrain_detail = null;
	if (terrainType == DEFINE_TERRAIN_DETAILED && terrainDetail!=null && terrainDetail.length>=DEFINE_MAP_TILE_PIXEL_SIZE){
		this.terrain_type = DEFINE_TERRAIN_DETAILED;
		this.terrain_detail = [];
		for (var row=0; row<DEFINE_MAP_TILE_PIXEL_SIZE; row++){
			if (terrainDetail[row].length>=DEFINE_MAP_TILE_PIXEL_SIZE){
				var terrainRow = [];
				for (var col=0; col<DEFINE_MAP_TILE_PIXEL_SIZE; col++){
					terrainRow.push(DEFINE_MAP_TERRAIN_TYPES.AIR);
					for (t in DEFINE_MAP_TERRAIN_TYPES){
						if (terrainDetail[row][col] === DEFINE_MAP_TERRAIN_TYPES[t]){
							terrainRow[col] = DEFINE_MAP_TERRAIN_TYPES[t];
							break;
						}
					}
				}
				this.terrain_detail.push(terrainRow);
			}
			else{
				this.terrain_type = DEFINE_MAP_TERRAIN_TYPES.AIR;
				this.terrain_detail = null;
				break;
			}
		}
	}

	this._heap = {};
};
	/* returns the terrain type at row y column x 
	* or -1 if out of bounds
	*/
	MapTile.prototype.getTerrainAt = function(x,y){
		if (x>=0 && x<DEFINE_MAP_TILE_PIXEL_SIZE && y>=0 && y<DEFINE_MAP_TILE_PIXEL_SIZE){
			return this._getTerrainAtUnsafe(x,y);
		}
		return -1;
	};
	/* internal-use variant that foregoes bounds-check */
	MapTile.prototype._getTerrainAtUnsafe = function(x,y){
		return (this.terrain_type != DEFINE_TERRAIN_DETAILED) ? this.terrain_type : this.terrain_detail[y][x];
	};

	/* Returns the y distance to the first pixel of terrain matching terrainType
	 * starting at row y column x and working in the positive or negative y direction
	 * or returns -1 if not found before edge of tile or within limit pixels (inclusive)
	 */
	MapTile.prototype.findVertically = function(terrainType,x,y,direction,limit){
		if (x<0 || x>=DEFINE_MAP_TILE_PIXEL_SIZE) return -1;
		if (this.terrain_type != DEFINE_TERRAIN_DETAILED){
			return (this.terrain_type&terrainType) ? 0 : -1;
		}
		limit = (limit!=null && limit>=0) ? limit : DEFINE_MAP_TILE_PIXEL_SIZE;
		this._heap.searchdir = (direction!=null && direction<0) ? -1 : 1;
		this._heap.searchcap = (this._heap.searchdir>0) ? DEFINE_MAP_TILE_PIXEL_SIZE : -1;
		this._heap.searchstart = Math.min(Math.max(y,0),DEFINE_MAP_TILE_PIXEL_SIZE-1);
		this._heap.pxchecked = -1;
		for (this._heap.searchy = this._heap.searchstart; this._heap.searchy!=this._heap.searchcap && this._heap.pxchecked<limit; this._heap.searchy+=this._heap.searchdir){
			if (this._getTerrainAtUnsafe(x,this._heap.searchy)&terrainType) return Math.abs(this._heap.searchy-y);
			this._heap.pxchecked++;
		}
		return -1;
	};

	/* Returns the x distance to the first pixel of terrain matching terrainType
	 * starting at row y column x and working in the positive or negative x direction
	 * or returns -1 if not found before edge of tile or within limit pixels (inclusive)
	 */
	MapTile.prototype.findHorizontally = function(terrainType,x,y,direction,limit){
		if (y<0 || y>=DEFINE_MAP_TILE_PIXEL_SIZE) return -1;
		if (this.terrain_type != DEFINE_TERRAIN_DETAILED){
			return (this.terrain_type&terrainType) ? 0 : -1;
		}
		limit = (limit!=null && limit>=0) ? limit : DEFINE_MAP_TILE_PIXEL_SIZE;
		this._heap.searchdir = (direction!=null && direction<0) ? -1 : 1;
		this._heap.searchcap = (this._heap.searchdir>0) ? DEFINE_MAP_TILE_PIXEL_SIZE : -1;
		this._heap.searchstart = Math.min(Math.max(x,0),DEFINE_MAP_TILE_PIXEL_SIZE-1);
		this._heap.pxchecked = -1;
		for (this._heap.searchx = this._heap.searchstart; this._heap.searchx!=this._heap.searchcap && this._heap.pxchecked<limit; this._heap.searchx+=this._heap.searchdir){
			if (this._getTerrainAtUnsafe(this._heap.searchx,y)&terrainType) return Math.abs(this._heap.searchx-x);
			this._heap.pxchecked++;
		}
		return -1;
	};

	/* draws this tile of map onto the provided canvas at the given (column,row)
	* (pixel (x,y) is (col,row)*DEFINE_MAP_TILE_PIXEL_SIZE) using the provided
	* tilesheet
	*/
	MapTile.prototype.drawIntoCanvas = function(canvas,row,col,tilesheet){
		if (tilesheet!=null){
			tilesheet.drawIntoCanvas(
				canvas,
				this.image_index,
				col<<DEFINE_MAP_TILE_SIZE,
				row<<DEFINE_MAP_TILE_SIZE);
		}
		else{
			this._heap.ctx = canvas.getContext("2d");
			switch (this.terrain_type){
				case DEFINE_MAP_TERRAIN_TYPES.GROUND:
					this._heap.ctx.fillStyle = "#000000";
					this._heap.ctx.fillRect(
						col<<DEFINE_MAP_TILE_SIZE,
						row<<DEFINE_MAP_TILE_SIZE,
						DEFINE_MAP_TILE_PIXEL_SIZE,
						DEFINE_MAP_TILE_PIXEL_SIZE);
				break;
				case DEFINE_MAP_TERRAIN_TYPES.WATER:
					this._heap.ctx.fillStyle = "rgba(0,0,255,0.5)";
					this._heap.ctx.fillRect(
						col<<DEFINE_MAP_TILE_SIZE,
						row<<DEFINE_MAP_TILE_SIZE,
						DEFINE_MAP_TILE_PIXEL_SIZE,
						DEFINE_MAP_TILE_PIXEL_SIZE);
				break;
				case DEFINE_TERRAIN_DETAILED:
					for (this._heap.y=0; this._heap.y<DEFINE_MAP_TILE_PIXEL_SIZE; this._heap.y++){
						for (this._heap.x=0; this._heap.x<DEFINE_MAP_TILE_PIXEL_SIZE; this._heap.x++){
							switch (this.terrain_detail[this._heap.y][this._heap.x]){
								case DEFINE_MAP_TERRAIN_TYPES.GROUND:
									this._heap.ctx.fillStyle = "#000000";
									this._heap.ctx.fillRect(
										(col<<DEFINE_MAP_TILE_SIZE)+this._heap.x,
										(row<<DEFINE_MAP_TILE_SIZE)+this._heap.y,
										1,
										1);
								break;
								case DEFINE_MAP_TERRAIN_TYPES.WATER:
									this._heap.ctx.fillStyle = "rgba(0,0,255,0.5)";
									this._heap.ctx.fillRect(
										(col<<DEFINE_MAP_TILE_SIZE)+this._heap.x,
										(row<<DEFINE_MAP_TILE_SIZE)+this._heap.y,
										1,
										1);
								break;
							}
						}
					}
				break;
			}
		}
	};

/*
* Container class for linking MapTile definitions to TileSheet indices.
*
* tilesheet: the TileSheet to build against
* defaultTerrain, defaultDetail: what to set each MapTile to
*/
var MapTilePalette = function(tilesheet, defaultTerrain, defaultDetail, minFeatureWidth, minFeatureHeight){
	this.tile_sheet = tilesheet;

	this.min_feature_width = minFeatureWidth ? minFeatureWidth : 16;
	this.min_feature_width = Math.max(this.min_feature_width,1);
	this.min_feature_height = minFeatureHeight ? minFeatureHeight : 16;
	this.min_feature_height = Math.max(this.min_feature_height,1);


	var sheetCols = 1<<tilesheet.sheet_width;
	var sheetRows = 1<<tilesheet.sheet_height;
	this.map_tiles = [];
	for (var row=0; row<sheetRows; row++){
		for (var col=0; col<sheetCols; col++){
			this.map_tiles.push(new MapTile(row*sheetCols+col, defaultTerrain, defaultDetail));
		}
	}

	this._heap={};
};

/* 
* container class for a graphical layer with no interaction
* manages the data array and image TileSheet and provides access & utility functions
*
* width, height: size of layer in tiles
* palette: the MapTilePalette with which this layer is to be drawn
* layout: an array of height arrays each containing width indices into palette
*/ 
var MapLayer = function(width, height, palette, layout){
	this.width = (width!=null && !isNaN(parseInt(width))) ? parseInt(width) : 24;
	this.width = Math.max(1,this.width);
	this.pixel_width = this.width*DEFINE_MAP_TILE_PIXEL_SIZE;

	this.height = (height!=null && !isNaN(parseInt(height))) ? parseInt(height) : 15;
	this.height = Math.max(1,this.height);
	this.pixel_height = this.height*DEFINE_MAP_TILE_PIXEL_SIZE;

	//this.tile_sheet = tilesheet;
	this.palette = palette;

	this.layout = [];
	for (var row=0; row<this.height; row++){
		var layoutRow = [];
		for (var col=0; col<this.width; col++){
			if (row<layout.length && col<layout[row].length){
				layoutRow.push(layout[row][col]);
			}
			else{
				//layoutRow.push(new MapTile(0,DEFINE_MAP_TERRAIN_TYPES.AIR));
				layoutRow.push(0);
			}
		}
		this.layout.push(layoutRow);
	}

	this.baked_img = null;

	this._heap = {};
};
	/* bakes the map into a single image for faster draws */
	MapLayer.prototype.bake = function(){
		this.baked_img = document.createElement('canvas');
		this.baked_img.width = this.pixel_width;
		this.baked_img.height = this.pixel_height;
		this._drawIntoCanvas(this.baked_img);
	};
	MapLayer.prototype.unbake = function(){
		this.baked_img = null;
	};

	/* draws the baked map to the given canvas, or draws the map raw if no baked version exists */
	MapLayer.prototype.drawIntoCanvas = function(canvas){
		if (this.baked_img!=null){
			canvas.getContext("2d").drawImage(this.baked_img,0,0);
		}
		else{
			this._drawIntoCanvas(canvas);
		}
	};

	/* runs through all tiles and draws them to the provided canvas starting at (0,0) */
	MapLayer.prototype._drawIntoCanvas = function(canvas){
		for (this._heap.row=0; this._heap.row<this.height; this._heap.row++){
			for (this._heap.col=0; this._heap.col<this.width; this._heap.col++){
				this.palette.map_tiles[this.layout[this._heap.row][this._heap.col]].drawIntoCanvas(canvas,this._heap.row,this._heap.col,this.palette.tile_sheet);
			}
		}
	};

/*
* container class for working with water as its own room-wide layer
*
* surfaceY: top y coordinate which will return as water (next px up will return as air); 
*	yes, this is set once at _SDS_Load_Map_Rooms and forgotten, but if any GameObjects change it, presumably they'll be around to reset it too
* surfaceTileIdx: index (into a MapTilePalette to be provided when needed) with which to draw the top row of water tiles
* fillTileIdx: index (into a MapTilePalette to be provided when needed) with which to draw the remaining water tiles
*/
var MapWaterLayer = function(surfaceY,surfaceTileIdx,fillTileIdx){
	this.surface_y = surfaceY ? surfaceY : 0;
	
	this.surface_tile_idx = (typeof(surfaceTileIdx)==="number" && !isNaN(surfaceTileIdx)) ? surfaceTileIdx : 0;
	this.fill_tile_idx = (typeof(fillTileIdx)==="number" && !isNaN(fillTileIdx)) ? fillTileIdx : 1;

	this.baked_image = null;

	this._heap = {};
	this._heap.row=0;
	this._heap.col=0;
	this._heap.ctx = null;
};
	MapWaterLayer.prototype.getTerrainAt = function(x,y){
		return (y>=this.surface_y) ? DEFINE_MAP_TERRAIN_TYPES.WATER : DEFINE_MAP_TERRAIN_TYPES.AIR;
	}
	MapWaterLayer.prototype.findVertically = function(terrainType,x,y,direction,limit,wrap){
		// NOTE: WRAP ARGUMENT NOT CURRENTLY SUPPORTED (would need room info for it to make any sense at all...)
		// but preserving it for API consistency
		if ((terrainType & DEFINE_MAP_TERRAIN_TYPES.WATER) && (terrainType & DEFINE_MAP_TERRAIN_TYPES.AIR)){
			return 0;
		}
		else if (terrainType & DEFINE_MAP_TERRAIN_TYPES.WATER){
			if (y>=this.surface_y) return 0;
			else if (direction<0){
				return -1; // looking upwards from above water
			}
			else{
				if (limit!=null && limit>=0 && this.surface_y-y>limit) return -1;
				else return this.surface_y-y;
			}
		}
		else if (terrainType & DEFINE_MAP_TERRAIN_TYPES.AIR){
			if (y<this.surface_y) return 0;
			else if (direction>0){
				return -1; // looking downwards from below water
			}
			else{
				if (limit!=null && limit>=0 && y-this.surface_y+1>limit) return -1;
				else return y-this.surface_y+1;
			}
		}
		else return -1;
	}
	MapWaterLayer.prototype.findHorizontally = function(terrainType,x,y,direction,limit,wrap){
		// NOTE: arguments preserved for API consistency, but most do nothing
		if ((terrainType & DEFINE_MAP_TERRAIN_TYPES.WATER) && (terrainType & DEFINE_MAP_TERRAIN_TYPES.AIR)){
			return 0;
		}
		else if (terrainType & DEFINE_MAP_TERRAIN_TYPES.WATER){
			if (y>=this.surface_y) return 0;
			else return -1;
		}
		else if (terrainType & DEFINE_MAP_TERRAIN_TYPES.AIR){
			if (y<this.surface_y) return 0;
			else return -1;
		}
		else return -1;
	}

	/* bakes a single image of full-map water for faster draws */
	MapWaterLayer.prototype.bake = function(tileWidth,tileHeight,palette){
		this.baked_img = document.createElement('canvas');
		this.baked_img.width = tileWidth * palette.tile_sheet.tile_pixel_width;
		this.baked_img.height = tileHeight * palette.tile_sheet.tile_pixel_height;
		
		this._drawIntoCanvas(this.baked_img,tileWidth,tileHeight,palette);
	};
	MapWaterLayer.prototype.unbake = function(){
		this.baked_img = null;
	};

	/* draws a full wixth x height of water into the canvas starting at (0,0) (i.e. assuming translation for surfaceY is already applied) */
	MapWaterLayer.prototype._drawIntoCanvas = function(canvas,tileWidth,tileHeight,palette){
		for (this._heap.col=0; this._heap.col<tileWidth; this._heap.col++){
			palette.map_tiles[this.surface_tile_idx].drawIntoCanvas(this.baked_img,0,this._heap.col,palette.tile_sheet);
		}
		for (this._heap.row=1; this._heap.row<tileHeight; this._heap.row++){
			for (this._heap.col=0; this._heap.col<tileWidth; this._heap.col++){
				palette.map_tiles[this.fill_tile_idx].drawIntoCanvas(this.baked_img,this._heap.row,this._heap.col,palette.tile_sheet);
			}
		}
	};

	/* draws the baked water to the given canvas, or draws the water raw if no baked version exists */
	MapWaterLayer.prototype.drawIntoCanvas = function(canvas,tileWidth,tileHeight,palette){
		this._heap.ctx = canvas.getContext("2d");
		this._heap.ctx.save();
		this._heap.ctx.translate(0, this.surface_y);
		
		if (this.baked_img!=null){
			canvas.getContext("2d").drawImage(this.baked_img,0,0);
		}
		else{
			this._drawIntoCanvas(canvas);
		}
		this._heap.ctx.restore();
	};

/* 
* container class for a room of game map
* manages the data array and image TileSheet and provides access & utility functions
*
* roomName: string identifier, usually from map data key
* width, height: size of room in tiles
* margin: number of perimiter tiles to be kept offscreen when drawing the room
* palette: the MapTilePalette with which this room is to be drawn
* layout: an array of height arrays each containing width indices into palette
* backgrounds, foregrounds: arrays of MapLayers to be drawn behind and in front of the main layout, ordered back-to-front
* waterLayer: a MapWaterLayer object which overrides any AIR regions, and will be drawn last of all
* objectInitializers, decoInitializers: an array of function(mapRoom)s which return GameObjects or Decorations respectively after adding their sprites to the room as needed (possibly conditionally on game state)
* bgmInitializer: function() which returns the name of the BGM to play in this room
*/ 
var MapRoom = function(roomName,width, height, margin, palette, layout, backgrounds, foregrounds, waterLayer, objectInitializers, decoInitializers, bgmInitializer){
	this.name = roomName;

	this.width = (width!=null && !isNaN(parseInt(width))) ? parseInt(width) : 24;
	this.width = Math.max(1,this.width);
	this.pixel_width = this.width*DEFINE_MAP_TILE_PIXEL_SIZE;

	this.height = (height!=null && !isNaN(parseInt(height))) ? parseInt(height) : 15;
	this.height = Math.max(1,this.height);
	this.pixel_height = this.height*DEFINE_MAP_TILE_PIXEL_SIZE;

	this.margin = (margin!=null && !isNaN(parseInt(margin))) ? parseInt(margin) : 0;
	this.margin = Math.max(0,this.margin);
	this.pixel_margin = this.margin*DEFINE_MAP_TILE_PIXEL_SIZE;

	this.scroll_x = 0;
	this.scroll_y = 0;

	this.water_layer = waterLayer;

	this.getBGM = bgmInitializer ? bgmInitializer : function(){return null};

	//this.tile_sheet = tilesheet;
	this.palette = palette;

	this.layout = [];
	for (var row=0; row<this.height; row++){
		var layoutRow = [];
		for (var col=0; col<this.width; col++){
			if (row<layout.length && col<layout[row].length){
				layoutRow.push(layout[row][col]);
			}
			else{
				//layoutRow.push(new MapTile(0,DEFINE_MAP_TERRAIN_TYPES.AIR));
				layoutRow.push(0);
			}
		}
		this.layout.push(layoutRow);
	}

	this.backgrounds = (backgrounds!=null && backgrounds.length) ? backgrounds : [];
	this.foregrounds = (foregrounds!=null && foregrounds.length) ? foregrounds : [];

	this.sprites = [];
	this.object_initializers = (objectInitializers!=null) ? objectInitializers : [];

	this.decorations = [];
	this.deco_initializers = (decoInitializers!=null) ? decoInitializers : [];

	this.baked_img = null;

	this._heap = {};
	this._heap.limitsX = {min:0, max:0};
	this._heap.limitsY = {min:0, max:0};
};
	MapRoom.prototype.scroll_limits_x = function(canvas,  out){
		if (out){
			out.min = -(this.pixel_width-canvas.width-this.pixel_margin);
			out.max = -this.pixel_margin;
		}
		else return {
			min : -(this.pixel_width-canvas.width-this.pixel_margin),
			max : -this.pixel_margin
		};
	};
	MapRoom.prototype.scroll_limits_y = function(canvas,  out){
		if (out){
			out.min = -(this.pixel_height-canvas.height-this.pixel_margin);
			out.max = -this.pixel_margin;
		}
		else return {
			min : -(this.pixel_height-canvas.height-this.pixel_margin),
			max : -this.pixel_margin
		};
	};

	MapRoom.prototype.addSprite = function(sprite){
		this.sprites.push(sprite);
		sprite.map_room = this;
	};
	//this.remove_queue = [];
	MapRoom.prototype.removeSprite = function(sprite){
		this._heap.index = this.sprites.indexOf(sprite);
		if (this._heap.index!=-1){
			this.sprites.splice(this._heap.index,1);
			sprite.map_room=null;
		}
		//this.remove_queue.push(sprite);
	};
	/*MapRoom.prototype._removeSprite = function(sprite){
		this._heap.index = this.sprites.indexOf(sprite);
		if (this._heap.index!=-1){
			this.sprites.splice(this._heap.index,1);
			sprite.map_room=null;
		}
	};*/
	MapRoom.prototype.removeAllSprites = function(){
		this._heap.spr;
		while (this.sprites.length>0){
			this._heap.spr = this.sprites.pop();
			this._heap.spr.map_room=null;
		}
	};

	MapRoom.prototype.addDeco = function(deco){
		while (this.decorations.length<=deco.layer){
			this.decorations.push([]);
		}
		this.decorations[deco.layer].push(deco);
		deco.map_room = this;
	};
	MapRoom.prototype.removeDeco = function(deco){
		if (this.decorations.length<=deco.layer) return;
		this._heap.index = this.decorations[deco.layer].indexOf(deco);
		if (this._heap.index!=-1){
			this.decorations[deco.layer].splice(this._heap.index,1);
			deco.map_room=null;
		}
	};
	MapRoom.prototype.initDeco = function(){
		for (this._heap.d=0; this._heap.d<this.deco_initializers.length; this._heap.d++){
			this._heap.deco = this.deco_initializers[this._heap.d](this);
			if (this._heap.deco){
				this.addDeco(this._heap.deco);
			}
		}
	};
	MapRoom.prototype.removeAllDeco = function(){
		for (this._heap.l=0; this._heap.l<this.decorations.length; ++this._heap.l){
			while(this.decorations[this._heap.l].length>0){
				this._heap.deco = this.decorations[this._heap.l].pop();
				this._heap.deco.map_room=null;
			}
		}
	};

	/* returns the terrain type at row y column x 
	* or -1 if out of bounds
	*/
	MapRoom.prototype.getTerrainAt = function(x,y){
		if (x>=0 && x<this.pixel_width && y>=0 && y<this.pixel_height){
			if (this.water_layer!=null){
				this._heap.ter = this._getTerrainAtUnsafe(x,y);
				return (this._heap.ter & (DEFINE_MAP_TERRAIN_TYPES.AIR | DEFINE_MAP_TERRAIN_TYPES.WATER)) ? this.water_layer.getTerrainAt(x,y) : this._heap.ter;
			}
			else return this._getTerrainAtUnsafe(x,y);
		}
		return -1;
	};
	/* internal-use variant that foregoes bounds-check */
	MapRoom.prototype._getTerrainAtUnsafe = function(x,y){
		this._heap.tileRow = y >> DEFINE_MAP_TILE_SIZE;
		this._heap.tileCol = x >> DEFINE_MAP_TILE_SIZE;
		return this.palette.map_tiles[this.layout[this._heap.tileRow][this._heap.tileCol]]._getTerrainAtUnsafe(x&DEFINE_MAP_TILE_COORD_MASK, y&DEFINE_MAP_TILE_COORD_MASK);
	};
	
	MapRoom.prototype._dominantDistance = function(d1,d2){
		if (d1<0 && d2>=0) return d2;
		else if (d1>=0 && d2<0) return d1;
		else return Math.min(d1,d2);
	}

	/* Returns the y distance to the first pixel of terrain matching terrainType
	 * starting at row y column x and working in the positive or negative y direction
	 * or returns -1 if not found before edge of room (if !wrap) or within limit pixels (inclusive)
	 * if wrap, will continue checking from the opposite edge of the room once one edge is reached
	 */
	MapRoom.prototype.findVertically = function(terrainType,x,y,direction,limit,wrap){
		// thiiiiis gets confusing with the water layer.
		// handling both air and water in both the main map and the water layer would be a priorities nightmare
		// so pull all air/water distances from the water layer and splice in ground independently
		// moreover, we can't JUST use water layer for air/water tests and main layer for ground tests
		// because we may be IN the ground looking for air/water and don't want to simply return that
		// the water layer says we're good
		if (this.water_layer!=null){
			//return this._dominantDistance(
			//	this._findVertically((terrainType & DEFINE_MAP_TERRAIN_TYPES.GROUND),x,y,direction,limit,wrap),
			//	this.water_layer.findVertically((terrainType & (DEFINE_MAP_TERRAIN_TYPES.WATER | DEFINE_MAP_TERRAIN_TYPES.AIR)),x,y,direction,limit,wrap)
			//);

			this._heap.groundDist = this._findVertically((terrainType & DEFINE_MAP_TERRAIN_TYPES.GROUND),x,y,direction,limit,wrap); // how far to ground if looking for ground
			this._heap.fluidDist = -1; // how far to air/water on the water layer, accounting for how far to water-layer relevant terrain

			// first, see if there are even any water-layer-relevant pixels in our search range
			this._heap.waterLayerDist = this._findVertically((DEFINE_MAP_TERRAIN_TYPES.WATER | DEFINE_MAP_TERRAIN_TYPES.AIR),x,y,direction,limit,wrap);
			if (this._heap.waterLayerDist >= 0){ 
				// if so, continue search into water later from nearest relevant px
				this._heap.fluidDist = this.water_layer.findVertically(
					(terrainType & (DEFINE_MAP_TERRAIN_TYPES.WATER | DEFINE_MAP_TERRAIN_TYPES.AIR)),
					x,y+this._heap.waterLayerDist*(direction>=0?1:-1), // adjust search start to first relevant px
					direction,
					limit-this._heap.waterLayerDist, // adjust limit to account for distance-so-far
					wrap);
				if (this._heap.fluidDist>=0) this._heap.fluidDist += this._heap.waterLayerDist; // ONLY if found within new constraints, add diatance-so-far
				// otherwise, fluidDist remains not-found
			}

			return this._dominantDistance(
				this._heap.groundDist,
				this._heap.fluidDist
			);
		}
		else return this._findVertically(terrainType,x,y,direction,limit,wrap);
	}
	MapRoom.prototype._findVertically = function(terrainType,x,y,direction,limit,wrap){
		if (x<0 || x>=this.pixel_width) return -1;
		
		this._heap.searchdir = (direction!=null && direction<0) ? -1 : 1;

		this._heap.tileRow = y >> DEFINE_MAP_TILE_SIZE;
		this._heap.tileCol = x >> DEFINE_MAP_TILE_SIZE;

		this._heap.searchLimit = (limit!=null && limit>=0) ? limit : this.pixel_height;
		this._heap.searchedDistance=0;

		this._heap.searchcap = (this._heap.searchdir>0) ? this.height : -1;
		this._heap.searchstart = Math.min(Math.max(this._heap.tileRow,0),this.height-1);
		this._heap.searchx = x&DEFINE_MAP_TILE_COORD_MASK;
		this._heap.searchy = y&DEFINE_MAP_TILE_COORD_MASK;
		for (this._heap.searchrow = this._heap.searchstart; (wrap || this._heap.searchrow!=this._heap.searchcap) && this._heap.searchLimit>=0; this._heap.searchrow+=this._heap.searchdir){
			if (wrap){
				if (this._heap.searchrow<0) this._heap.searchrow = this.height-1;
				if (this._heap.searchrow>=this.height) this._heap.searchrow = 0;
			}

			this._heap.tileResult = this.palette.map_tiles[this.layout[this._heap.searchrow][this._heap.tileCol]].findVertically(terrainType,this._heap.searchx,this._heap.searchy,this._heap.searchdir,this._heap.searchLimit);
			
			if (this._heap.tileResult!=-1) return this._heap.searchedDistance+this._heap.tileResult;

			if (this._heap.searchdir>0){
				this._heap.searchLimit -= (DEFINE_MAP_TILE_PIXEL_SIZE-this._heap.searchy);
				this._heap.searchedDistance += (DEFINE_MAP_TILE_PIXEL_SIZE-this._heap.searchy);
				this._heap.searchy = 0;
			}
			else{
				this._heap.searchLimit -= (this._heap.searchy+1);
				this._heap.searchedDistance += (this._heap.searchy+1);
				this._heap.searchy = DEFINE_MAP_TILE_PIXEL_SIZE-1;
			}
		}
		return -1;
	};

	/* Returns the x distance to the first pixel of terrain matching terrainType
	 * starting at row y column x and working in the positive or negative x direction
	 * or returns -1 if not found before edge of room (if !wrap) or within limit pixels (inclusive)
	 * if wrap, will continue checking from the opposite edge of the room once one edge is reached
	 */
	MapRoom.prototype.findHorizontally = function(terrainType,x,y,direction,limit,wrap){
		// thiiiiis gets confusing with the water layer.
		// handling both air and water in both the main map and the water layer would be a priorities nightmare
		// so pull all air/water distances from the water layer and splice in ground independently
		// moreover, we can't JUST use water layer for air/water tests and main layer for ground tests
		// because we may be IN the ground looking for air/water and don't want to simply return that
		// the water layer says we're good
		if (this.water_layer!=null){
			//return this._dominantDistance(
			//	this._findHorizontally((terrainType & DEFINE_MAP_TERRAIN_TYPES.GROUND),x,y,direction,limit,wrap),
			//	this.water_layer.findHorizontally((terrainType & (DEFINE_MAP_TERRAIN_TYPES.WATER | DEFINE_MAP_TERRAIN_TYPES.AIR)),x,y,direction,limit,wrap)
			//);

			this._heap.groundDist = this._findHorizontally((terrainType & DEFINE_MAP_TERRAIN_TYPES.GROUND),x,y,direction,limit,wrap); // how far to ground if looking for ground
			this._heap.fluidDist = -1; // how far to air/water on the water layer, accounting for how far to water-layer relevant terrain

			// first, see if there are even any water-layer-relevant pixels in our search range
			this._heap.waterLayerDist = this._findHorizontally((DEFINE_MAP_TERRAIN_TYPES.WATER | DEFINE_MAP_TERRAIN_TYPES.AIR),x,y,direction,limit,wrap);
			if (this._heap.waterLayerDist >= 0){ 
				// if so, continue search into water later from nearest relevant px
				this._heap.fluidDist = this.water_layer.findHorizontally(
					(terrainType & (DEFINE_MAP_TERRAIN_TYPES.WATER | DEFINE_MAP_TERRAIN_TYPES.AIR)),
					x+this._heap.waterLayerDist*(direction>=0?1:-1),y, // adjust search start to first relevant px
					direction,
					limit-this._heap.waterLayerDist, // adjust limit to account for distance-so-far
					wrap);
				if (this._heap.fluidDist>=0) this._heap.fluidDist += this._heap.waterLayerDist; // ONLY if found within new constraints, add diatance-so-far
				// otherwise, fluidDist remains not-found
			}

			return this._dominantDistance(
				this._heap.groundDist,
				this._heap.fluidDist
			);
		}
		else return this._findHorizontally(terrainType,x,y,direction,limit,wrap);
	}
	MapRoom.prototype._findHorizontally = function(terrainType,x,y,direction,limit,wrap){
		if (y<0 || y>=this.pixel_height) return -1;
		
		this._heap.searchdir = (direction!=null && direction<0) ? -1 : 1;

		this._heap.tileRow = y >> DEFINE_MAP_TILE_SIZE;
		this._heap.tileCol = x >> DEFINE_MAP_TILE_SIZE;

		this._heap.searchLimit = (limit!=null && limit>=0) ? limit : this.pixel_width;
		this._heap.searchedDistance=0;

		this._heap.searchcap = (this._heap.searchdir>0) ? this.width : -1;
		this._heap.searchstart = Math.min(Math.max(this._heap.tileCol,0),this.width-1);
		this._heap.searchx = x&DEFINE_MAP_TILE_COORD_MASK;
		this._heap.searchy = y&DEFINE_MAP_TILE_COORD_MASK;
		for (this._heap.searchcol = this._heap.searchstart; (wrap || this._heap.searchcol!=this._heap.searchcap) && this._heap.searchLimit>=0; this._heap.searchcol+=this._heap.searchdir){
			if (wrap){
				if (this._heap.searchcol<0) this._heap.searchcol = this.width-1;
				if (this._heap.searchcol>=this.width) this._heap.searchcol = 0;
			}

			this._heap.tileResult = this.palette.map_tiles[this.layout[this._heap.tileRow][this._heap.searchcol]].findHorizontally(terrainType,this._heap.searchx,this._heap.searchy,this._heap.searchdir,this._heap.searchLimit);
			
			if (this._heap.tileResult!=-1) return this._heap.searchedDistance+this._heap.tileResult;

			if (this._heap.searchdir>0){
				this._heap.searchLimit -= (DEFINE_MAP_TILE_PIXEL_SIZE-this._heap.searchx);
				this._heap.searchedDistance += (DEFINE_MAP_TILE_PIXEL_SIZE-this._heap.searchx);
				this._heap.searchx = 0;
			}
			else{
				this._heap.searchLimit -= (this._heap.searchx+1);
				this._heap.searchedDistance += (this._heap.searchx+1);
				this._heap.searchx = DEFINE_MAP_TILE_PIXEL_SIZE-1;
			}
		}
		return -1;
	};

	/* bakes the map into a single image for faster draws */
	MapRoom.prototype.bake = function(){
		this.baked_img = document.createElement('canvas');
		this.baked_img.width = this.pixel_width;
		this.baked_img.height = this.pixel_height;
		this._drawMainIntoCanvas(this.baked_img);
	};
	MapRoom.prototype.unbake = function(){
		this.baked_img = null;
	};

	/* bakes all foreground and background layers */
	MapRoom.prototype.bakeLayers = function(){
		for (this._heap.i=0; this._heap.i<this.backgrounds.length; this._heap.i++){
			this.backgrounds[this._heap.i].bake();
		}
		for (this._heap.i=0; this._heap.i<this.foregrounds.length; this._heap.i++){
			this.foregrounds[this._heap.i].bake();
		}
	};
	MapRoom.prototype.unbakeLayers = function(){
		for (this._heap.i=0; this._heap.i<this.backgrounds.length; this._heap.i++){
			this.backgrounds[this._heap.i].unbake();
		}
		for (this._heap.i=0; this._heap.i<this.foregrounds.length; this._heap.i++){
			this.foregrounds[this._heap.i].unbake();
		}
	};

	/* bakes water into a single image for faster draws */
	MapRoom.prototype.bakeWater = function(){
		if (!this.water_layer) return;
		this.water_layer.bake(this.width, this.height, this.palette);
	};
	MapRoom.prototype.unbakeWater = function(){
		if (!this.water_layer) return;
		this.water_layer.unbake();
	};

	/* sets room scroll position such that the given map coordinates are in the center of the canvas */
	MapRoom.prototype.scrollCenterOn = function(canvas,centerx,centery){
		if (!centerx) centerx = this.pixel_width>>1;
		if (!centery) centery = this.pixel_height>>1;
		this._heap.drawx = (canvas.width>>1) - centerx;
		this._heap.drawy = (canvas.height>>1) - centery;

		this.scroll_limits_x(canvas,  this._heap.limitsX);
		this.scroll_limits_y(canvas,  this._heap.limitsY);

		if (this.pixel_width >= canvas.width+2*this.pixel_margin){
			this._heap.drawx = Math.max(this._heap.limitsX.min,this._heap.drawx);
			this._heap.drawx = Math.min(this._heap.limitsX.max,this._heap.drawx);
		}
		else{
			this._heap.drawx = (canvas.width-this.pixel_width)>>1;
		}
		if (this.pixel_height >= canvas.height+2*this.pixel_margin){
			this._heap.drawy = Math.max(this._heap.limitsY.min,this._heap.drawy);
			this._heap.drawy = Math.min(this._heap.limitsY.max,this._heap.drawy);
		}
		else{
			this._heap.drawy = (canvas.height-this.pixel_height)>>1;
		}
		this.scroll_x = this._heap.drawx;
		this.scroll_y = this._heap.drawy;
	};

	/* runs through all tiles and draws them to the provided canvas starting at (0,0) */
	MapRoom.prototype._drawMainIntoCanvas = function(canvas){
		for (this._heap.row=0; this._heap.row<this.height; this._heap.row++){
			for (this._heap.col=0; this._heap.col<this.width; this._heap.col++){
				this.palette.map_tiles[this.layout[this._heap.row][this._heap.col]].drawIntoCanvas(canvas,this._heap.row,this._heap.col,this.palette.tile_sheet);
			}
		}
	};

	/* draws the baked map to the given canvas, or draws the map raw if no baked version exists */
	MapRoom.prototype.drawMainIntoCanvas = function(canvas){
		this._heap.ctx = canvas.getContext("2d");
		this._heap.ctx.save();
		this._heap.ctx.translate(this.scroll_x, this.scroll_y);
		
		if (this.baked_img!=null){
			canvas.getContext("2d").drawImage(this.baked_img,0,0);
		}
		else{
			this._drawMainIntoCanvas(canvas);
		}
		this._heap.ctx.restore();
	};

	MapRoom.prototype.drawBackgroundsIntoCanvas = function(canvas){
		this._heap.ctx = canvas.getContext("2d");
		this.scroll_limits_x(canvas,  this._heap.limitsX);
		this.scroll_limits_y(canvas,  this._heap.limitsY);
		for (this._heap.i=0; this._heap.i<this.backgrounds.length; this._heap.i++){
			this._heap.bgScrollX = 0;
			this._heap.bgScrollY = 0;
			if (this.backgrounds[this._heap.i].pixel_width >= canvas.width && this._heap.limitsX.max-this._heap.limitsX.min>0){
				this._heap.scrollRangeX = this.backgrounds[this._heap.i].pixel_width - canvas.width;
				this._heap.bgScrollX = Math.round(this._heap.scrollRangeX * (this.scroll_x - this._heap.limitsX.max) / (this._heap.limitsX.max - this._heap.limitsX.min));
			}
			else{
				this._heap.bgScrollX = (canvas.width-this.backgrounds[this._heap.i].pixel_width)>>1;
			}
			if (this.backgrounds[this._heap.i].pixel_height >= canvas.height && this._heap.limitsY.max-this._heap.limitsY.min>0){
				this._heap.scrollRangeY = this.backgrounds[this._heap.i].pixel_height - canvas.height;
				this._heap.bgScrollY = Math.round(this._heap.scrollRangeY * (this.scroll_y - this._heap.limitsY.max) / (this._heap.limitsY.max - this._heap.limitsY.min));
			}
			else{
				this._heap.bgScrollY = (canvas.height-this.backgrounds[this._heap.i].pixel_height)>>1;
			}

			this._heap.ctx.save();
			this._heap.ctx.translate(this._heap.bgScrollX,this._heap.bgScrollY);
			this.backgrounds[this._heap.i].drawIntoCanvas(canvas);
			this._heap.ctx.restore();
			
		}
	};

	MapRoom.prototype.drawForegroundsIntoCanvas = function(canvas){
		this._heap.ctx = canvas.getContext("2d");
		this.scroll_limits_x(canvas,  this._heap.limitsX);
		this.scroll_limits_y(canvas,  this._heap.limitsY);
		for (this._heap.i=0; this._heap.i<this.foregrounds.length; this._heap.i++){
			this._heap.fgScrollX = 0;
			this._heap.fgScrollY = 0;
			if (this.foregrounds[this._heap.i].pixel_width >= canvas.width && this._heap.limitsX.max-this._heap.limitsX.min>0){
				this._heap.scrollRangeX = this.foregrounds[this._heap.i].pixel_width - canvas.width;
				this._heap.fgScrollX = Math.round(this._heap.scrollRangeX * (this.scroll_x - this._heap.limitsX.max) / (this._heap.limitsX.max - this._heap.limitsX.min));
			}
			else{
				this._heap.fgScrollX = (canvas.width-this.foregrounds[this._heap.i].pixel_width)>>1;
			}
			if (this.foregrounds[this._heap.i].pixel_height >= canvas.height && this._heap.limitsY.max-this._heap.limitsY.min>0){
				this._heap.scrollRangeY = this.foregrounds[this._heap.i].pixel_height - canvas.height;
				this._heap.fgScrollY = Math.round(this._heap.scrollRangeY * (this.scroll_y - this._heap.limitsY.max) / (this._heap.limitsY.max - this._heap.limitsY.min));
			}
			else{
				this._heap.fgScrollY = (canvas.height-this.foregrounds[this._heap.i].pixel_height)>>1;
			}

			this._heap.ctx.save();
			this._heap.ctx.translate(this._heap.fgScrollX,this._heap.fgScrollY);
			this.foregrounds[this._heap.i].drawIntoCanvas(canvas);
			this._heap.ctx.restore();
			
		}
	};

	MapRoom.prototype.drawSpritesIntoCanvas = function(canvas){
		this._heap.ctx = canvas.getContext("2d");
		this._heap.ctx.save();
		this._heap.ctx.translate(this.scroll_x, this.scroll_y);
		for (this._heap.s=0; this._heap.s<this.sprites.length; this._heap.s++){
			this.sprites[this._heap.s].drawIntoCanvas(canvas);
		}
		this._heap.ctx.restore();
	};

	MapRoom.prototype.drawDecoIntoCanvas = function(canvas,layer){
		layer = layer || 0;
		if (this.decorations.length<=layer) return;
		this._heap.ctx = canvas.getContext("2d");
		this._heap.ctx.save();
		this._heap.ctx.translate(this.scroll_x, this.scroll_y);
		for (this._heap.s=0; this._heap.s<this.decorations[layer].length; this._heap.s++){
			this.decorations[layer][this._heap.s].drawIntoCanvas(canvas);
		}
		this._heap.ctx.restore();
	};

	MapRoom.prototype.drawWaterIntoCanvas = function(canvas){
		if (!this.water_layer) return;

		this._heap.ctx = canvas.getContext("2d");
		this._heap.ctx.save();
		this._heap.ctx.translate(this.scroll_x, this.scroll_y);
		this.water_layer.drawIntoCanvas(canvas, this.width, this.height, this.palette);
		this._heap.ctx.restore();
	}

	MapRoom.prototype.drawIntoCanvas = function(canvas){
		this.drawBackgroundsIntoCanvas(canvas);
		this.drawMainIntoCanvas(canvas);
		this.drawDecoIntoCanvas(canvas);
		this.drawSpritesIntoCanvas(canvas);
		this.drawDecoIntoCanvas(canvas,1);
		this.drawForegroundsIntoCanvas(canvas);
		this.drawDecoIntoCanvas(canvas,2);
		this.drawWaterIntoCanvas(canvas);
		this.drawDecoIntoCanvas(canvas,3);
	};

	/* removes all sprites and baked art to free memory */
	MapRoom.prototype.flush = function(){
		this.unbake();
		this.unbakeLayers();
		this.unbakeWater();
		this.removeAllSprites();
	};

	MapRoom.prototype.Update = function(){
		/*for (this._heap.i=0; this._heap.i<this.remove_queue.length; this._heap.i++){
			this._removeSprite(this.remove_queue[this._heap.i]);
		}

		this.remove_queue = [];*/

		for (this._heap.l=0; this._heap.l<this.decorations.length; this._heap.l++){
			for (this._heap.s=0; this._heap.s<this.decorations[this._heap.l].length; this._heap.s++){
				this.decorations[this._heap.l][this._heap.s].animate();
			}
		}
	};
