A Scrolling Map Tile Engine in JavaScript

The JavaScript below implements a simple engine for allowing an animated character to move around a map made of tiles. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

This code was originally intended to be the first steps in implementing the exploration engine for a role-playing game.

ScrollableMapWithAnimatedCharacter.png


<html>
<body>

<div id="divMain"></div>

<script type="text/javascript">

// main

function main()
{
	var display = new Display(new Coords(200, 200));
	var mapCellSizeInPixels = new Coords(16, 16);
	var world = World.demo(display.sizeInPixels, mapCellSizeInPixels);
	var universe = new Universe
	(
		10, // timerTicksPerSecond
		display, 
		world
	);
	universe.start();
}

// extensions

function ArrayExtensions()
{
	// Extension class.
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			this[key] = element;
		}
		return this;
	}
	
	Array.prototype.clone = function()
	{
		var returnValues = [];
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var elementCloned = element.clone();
			returnValues.push(elementCloned);
		}
		return returnValues;
	}
	
	Array.prototype.remove = function(element)
	{
		var elementIndex = this.indexOf(element);
		if (elementIndex >= 0)
		{
			this.splice(elementIndex, 1);
		}
		return this;
	}
}

// classes

function Activity(defnName, target)
{
	this.defnName = defnName;
	this.target = target;
}
{
	Activity.prototype.defn = function(world)
	{
		return world.activityDefns[this.defnName];
	}
	
	Activity.prototype.perform = function(universe, world, venue, actor)
	{
		this.defn(world).perform(universe, world, venue, actor, this);
	}
}

function ActivityDefn(name, perform)
{
	this.name = name;
	this.perform = perform;
}

function Camera(viewSize, pos)
{
	this.viewSize = viewSize;
	this.pos = pos;
	
	this.viewSizeHalf = this.viewSize.clone().half();
}

function Color(name, code, componentsRGBA)
{
	this.name = name;
	this.code = code;
	this.componentsRGBA = componentsRGBA;
	
	this.systemColor = 
		"rgba(" 
		+ Math.floor(this.componentsRGBA[0] * Color.ComponentMax) + ","
		+ Math.floor(this.componentsRGBA[1] * Color.ComponentMax) + ","
		+ Math.floor(this.componentsRGBA[2] * Color.ComponentMax) + ","
		+ this.componentsRGBA[3] // ?
		+ ")";	
}
{
	Color.ComponentMax = 255;

	Color.Instances = function()
	{
		if (Color._instances == null)
		{
			Color._instances = new Color_Instances();
		}
		return Color._instances;
	}
	
	function Color_Instances()
	{
		this._Transparent = new Color("Transparent", ".", [0, 0, 0, 0]);
		this.Black 	= new Color("Black", "k", [0, 0, 0, 1]);
		this.Blue 	= new Color("Blue", "b", [0, 0, 1, 1]);
		this.Cyan 	= new Color("Cyan", "c", [0, 1, 1, 1]);	
		this.Gray 	= new Color("Gray", "a", [.5, .5, .5, 1]);
		this.GrayDark = new Color("GrayDark", "A", [.25, .25, .25, 1]);
		this.GrayLight = new Color("GrayLight", "-", [.75, .75, .75, 1]);		
		this.Green 	= new Color("Green", "g", [0, 1, 0, 1]);
		this.Orange = new Color("Orange", "o", [1, .5, 0, 1]);
		this.Red 	= new Color("Red", "r", [1, 0, 0, 1]);
		this.Violet = new Color("Violet", "v", [1, 0, 1, 1]);
		this.White 	= new Color("White", "w", [1, 1, 1, 1]);
		this.Yellow = new Color("Yellow", "y", [1, 1, 0, 1]);		

		this._All = 
		[
			this._Transparent,
			this.Black,
			this.Blue,
			this.Cyan,
			this.Gray,
			this.GrayDark,
			this.GrayLight,
			this.Green,
			this.Orange,
			this.Red,
			this.Violet,
			this.White,
			this.Yellow,
		].addLookups("code");
	}
}

function Constraint_Follow(target)
{
	this.target = target;
}
{
	Constraint_Follow.prototype.apply = function(constrainable)
	{
		constrainable.pos.overwriteWith(this.target.pos);
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		return this;
	}
	
	Coords.prototype.addXY = function(x, y)
	{
		this.x += x;
		this.y += y;
		return this;
	}
	
	Coords.prototype.ceiling = function()
	{
		this.x = Math.ceil(this.x);
		this.y = Math.ceil(this.y);
		return this;
	}
		
	Coords.prototype.clear = function()
	{
		this.x = 0;
		this.y = 0;
		return this;
	}
	
	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}
	
	Coords.prototype.divide = function(other)
	{
		this.x /= other.x;
		this.y /= other.y;
		return this;
	}
		
	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;
		return this;
	}
	
	Coords.prototype.floor = function()
	{
		this.x = Math.floor(this.x);
		this.y = Math.floor(this.y);
		return this;
	}
		
	Coords.prototype.half = function()
	{
		return this.divideScalar(2);
	}
	
	Coords.prototype.isInRangeMax = function(max)
	{
		var returnValue = 
		(
			this.x >= 0
			&& this.x <= max.x
			&& this.y >= 0
			&& this.y <= max.y
		);
		
		return returnValue;
	}
		
	Coords.prototype.magnitude = function()
	{
		return Math.sqrt(this.x * this.x + this.y * this.y);
	}

	Coords.prototype.multiply = function(other)
	{
		this.x *= other.x;
		this.y *= other.y;
		return this;
	}
	
	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		return this;
	}
	
	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;
		return this;
	}
	
	Coords.prototype.overwriteWithXY = function(x, y)
	{
		this.x = x;
		this.y = y;
		return this;
	}
	
	Coords.prototype.round = function()
	{
		this.x = Math.round(this.x);
		this.y = Math.round(this.y);
		return this;
	}
			
	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		return this;
	}
	
	Coords.prototype.trimToRangeMax = function(max)
	{
		if (this.x < 0)
		{
			this.x = 0;
		}
		else if (this.x > max.x)
		{
			this.x = max.x;
		}
		
		if (this.y < 0)
		{
			this.y = 0;
		}
		else if (this.y > max.y)
		{
			this.y = max.y;
		}
		
		return this;
	}
}

function Display(sizeInPixels)
{
	this.sizeInPixels = sizeInPixels;
	this.fontSizeInPixels = Math.floor(this.sizeInPixels.y / 32);
	
	this.sizeInPixelsHalf = this.sizeInPixels.clone().half();
	
	this.drawPos = new Coords();
}
{
	Display.prototype.initialize = function()
	{
		this.canvas = document.createElement("canvas");
		this.canvas.width = this.sizeInPixels.x;
		this.canvas.height = this.sizeInPixels.y;
		this.graphics = this.canvas.getContext("2d");
		this.graphics.font = this.fontSizeInPixels + "px sans-serif";
		return this;
	}
	
	Display.prototype.toImage = function(name)
	{
		var dataURL = this.canvas.toDataURL();
		var systemImage = document.createElement("img");
		systemImage.src = dataURL;
		var returnValue = new Image(name, this.sizeInPixels, systemImage);
		return returnValue;
	}
	
	// drawing
	
	Display.prototype.clear = function()
	{
		this.graphics.fillStyle = "White";
		this.graphics.fillRect(0, 0, this.sizeInPixels.x, this.sizeInPixels.y);
		this.graphics.strokeStyle = "Gray";
		this.graphics.strokeRect(0, 0, this.sizeInPixels.x, this.sizeInPixels.y);

		return this;		
	}
		
	Display.prototype.clearRectangle = function(pos, size)
	{
		this.graphics.clearRect(pos.x, pos.y, size.x, size.y);
		return this; 
	}
	
	Display.prototype.drawCircle = function(center, radius, colorFill, colorBorder)
	{
		this.graphics.beginPath();		
		this.graphics.arc(center.x, center.y, radius, 0, Polar.RadiansPerTurn);

		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.fill();	
		}
		
		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;
			this.graphics.stroke();	
		}
		
		return this;
	}
	
	Display.prototype.drawImage = function(image, pos)
	{
		this.graphics.drawImage(image.systemImage, pos.x, pos.y);
		
		return this;
	}
	
	Display.prototype.drawImageRegion = function(image, sourcePos, sourceSize, targetPos)
	{
		var targetSize = sourceSize;
	
		this.graphics.drawImage
		(
			image.systemImage, 
			sourcePos.x, sourcePos.y,
			sourceSize.x, sourceSize.y,
			targetPos.x, targetPos.y,
			targetSize.x, targetSize.y
		);
		
		return this;
	}
		
	Display.prototype.drawPolygon = function(vertices, colorFill, colorBorder)
	{
		this.graphics.beginPath();
		var vertex = vertices[0];
		this.graphics.moveTo(vertex.x, vertex.y);
		for (var i = 1; i < vertices.length; i++)
		{
			vertex = vertices[i];
			this.graphics.lineTo(vertex.x, vertex.y);
		}
		this.graphics.closePath();

		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.fill();	
		}
		
		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;
			this.graphics.stroke();	
		}
		
		return this;
	}
		
	Display.prototype.drawRectangle = function(pos, size, colorFill, colorBorder)
	{
		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.fillRect(pos.x, pos.y, size.x, size.y);
		}
		
		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;
			this.graphics.strokeRect(pos.x, pos.y, size.x, size.y);
		}
		
		return this;
	}
	
	Display.prototype.drawText = function(text, pos, colorFill, colorBorder)
	{
		if (colorBorder != null)
		{
			this.graphics.strokeStyle = color;
			this.graphics.strokeString(text, pos.x, pos.y);
		}
	
		if (colorFill != null)
		{
			this.graphics.fillStyle = color;
			this.graphics.fillString(text, pos.x, pos.y);
		}
		
		return this;
	}
}

function Image(name, size, systemImage)
{
	this.name = name;
	this.size = size;
	this.systemImage = systemImage;
	
	this.sizeHalf = this.size.clone().half();
}
{
	Image.fromStrings = function(name, colors, pixelsAsStrings)
	{
		var size = new Coords
		(
			pixelsAsStrings[0].length, pixelsAsStrings.length
		);
		var canvas = document.createElement("canvas");
		canvas.width = size.x;
		canvas.height = size.y;
		var graphics = canvas.getContext("2d");
		
		for (var y = 0; y < size.y; y++)
		{
			var pixelRowAsString = pixelsAsStrings[y];
			
			for (var x = 0; x < size.x; x++)
			{
				var pixelColorCode = pixelRowAsString[x];
				var pixelColor = colors[pixelColorCode];
				graphics.fillStyle = pixelColor.systemColor;
				graphics.fillRect(x, y, 1, 1);
			}
		}
		
		var systemImage = document.createElement("img");
		systemImage.src = canvas.toDataURL();
		
		var returnValue = new Image
		(
			name, size, systemImage
		);
		
		return returnValue;
	}
	
	Image.prototype.toDisplay = function()
	{
		return new Display(this.size).initialize().drawImage(this, new Coords(0, 0));
	}
}

function InputHelper()
{
	this.inputsPressed = [];
	this.inputsActive = [];
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);
	}
	
	InputHelper.prototype.inputActivate = function(input)
	{
		if (this.inputsActive[input] == null)
		{
			this.inputsActive[input] = input;
			this.inputsActive.push(input);
		}	
	}	
	
	InputHelper.prototype.inputAdd = function(input)
	{
		if (this.inputsPressed[input] == null)
		{
			this.inputsPressed[input] = input;
			this.inputsPressed.push(input);
			this.inputActivate(input);
		}	
	}
	
	InputHelper.prototype.inputInactivate = function(input)
	{
		if (this.inputsActive[input] != null)
		{
			delete this.inputsActive[input];
			this.inputsActive.remove(input);
		}
	}
	
	InputHelper.prototype.inputRemove = function(input)
	{
		this.inputInactivate(input);
		if (this.inputsPressed[input] != null)
		{
			delete this.inputsPressed[input];
			this.inputsPressed.remove(input);
		}
	}
	
	// events
	
	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		this.inputAdd(event.key);
	}
	
	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		this.inputRemove(event.key);
	}	
}

function Map(cellSizeInPixels, terrains, cellsAsStrings)
{
	this.cellSizeInPixels = cellSizeInPixels;
	this.terrains = terrains.addLookups("code");
	this.cellsAsStrings = cellsAsStrings;
	
	this.sizeInCells = new Coords
	(
		cellsAsStrings[0].length, cellsAsStrings.length
	);
	
	this.sizeInCellsMinusOnes = this.sizeInCells.clone().addXY
	(
		-1, -1
	);
	
	this.cellPos = new Coords();
	this.drawPos = new Coords();
}
{
	Map.prototype.terrainAtPosInCells = function(posInCells)
	{
		var terrainCode = this.cellsAsStrings[posInCells.y][posInCells.x];
		var terrain = this.terrains[terrainCode];
		return terrain;
	}

	// drawable

	Map.prototype.draw = function(universe, world, display, visualCamera)
	{
		var cellPos = this.cellPos;
		var drawPos = this.drawPos;
		var cell = {};
		cell.pos = drawPos;
		cell.velInCellsPerTick = new Coords(0, 0); // hack
		var halves = new Coords(.5, .5);
		var ones = new Coords(1, 1);
		
		var camera = visualCamera.camera;
		var cameraPos = camera.pos;
		var cameraViewSizeHalf = camera.viewSizeHalf;
		var cellPosMin = cameraPos.clone().subtract
		(
			cameraViewSizeHalf
		).divide
		(
			this.cellSizeInPixels
		).floor().trimToRangeMax
		(
			this.sizeInCellsMinusOnes
		);
		var cellPosMax = cameraPos.clone().add
		(
			cameraViewSizeHalf
		).divide
		(
			this.cellSizeInPixels
		).ceiling().trimToRangeMax
		(
			this.sizeInCellsMinusOnes
		);
						
		for (var y = cellPosMin.y; y <= cellPosMax.y; y++)
		{
			cellPos.y = y;
			
			for (var x = cellPosMin.x; x <= cellPosMax.x; x++)
			{
				cellPos.x = x;
				
				drawPos.overwriteWith
				(
					cellPos
				).add(halves).multiply
				(
					this.cellSizeInPixels
				);
				
				var terrain = this.terrainAtPosInCells(cellPos);
				var terrainVisual = terrain.visual;
				visualCamera.child = terrainVisual;				
				visualCamera.draw
				(
					universe, world, display, cell
				);
			}
		}

		var sizeDiminished = this.sizeInCellsMinusOnes.clone().addXY(-1, -1);
		
		var cornerPosMin = cellPosMin.clone().addXY(-1, -1).trimToRangeMax(this.sizeInCells);
		var cornerPosMax = cellPosMax.trimToRangeMax(sizeDiminished);
		var cornerPos = new Coords();
		var neighborOffsets = 
		[
			new Coords(0, 0),		
			new Coords(1, 0),
			new Coords(0, 1),			
			new Coords(1, 1),
		];
		var neighborPos = new Coords();
		var neighborTerrains = [];
				
		for (var y = cornerPosMin.y; y <= cornerPosMax.y; y++)
		{
			cornerPos.y = y;
			
			for (var x = cornerPosMin.x; x <= cornerPosMax.x; x++)
			{
				cornerPos.x = x;
				
				var neighborOffset = neighborOffsets[0];
				neighborPos.overwriteWith(cornerPos).add(neighborOffset);
				var terrainHighestSoFar = this.terrainAtPosInCells(neighborPos);
				
				for (var n = 1; n < neighborOffsets.length; n++)
				{
					var neighborOffset = neighborOffsets[n];
					neighborPos.overwriteWith(cornerPos).add(neighborOffset);
					var neighborTerrain = this.terrainAtPosInCells(neighborPos);
					var zLevelDifference = 
						neighborTerrain.zLevelForOverlays 
						- terrainHighestSoFar.zLevelForOverlays;
					if (zLevelDifference > 0)
					{
						terrainHighestSoFar = neighborTerrain;
					}
				}
				
				var terrainHighest = terrainHighestSoFar;
				var visualChildIndexSoFar = 0;
		
				for (var n = 0; n < neighborOffsets.length; n++)
				{
					var neighborOffset = neighborOffsets[n];
					neighborPos.overwriteWith(cornerPos).add(neighborOffset);
					var neighborTerrain = this.terrainAtPosInCells(neighborPos);
					if (neighborTerrain != terrainHighest)
					{
						visualChildIndexSoFar |= (1 << n);
					}
				}
				
				if (visualChildIndexSoFar > 0)
				{
					drawPos.overwriteWith
					(
						cornerPos
					).add(ones).multiply
					(
						this.cellSizeInPixels
					);
					
					var terrainVisual = terrainHighest.visual.children[visualChildIndexSoFar];
					if (terrainVisual != null) // hack
					{
						visualCamera.child = terrainVisual;				
						visualCamera.draw
						(
							universe, world, display, cell
						);
					}
				}
			}
		}
	}
}

function MapTerrain(name, code, blocksMovement, zLevelForOverlays, visual)
{
	this.name = name;
	this.code = code;
	this.blocksMovement = blocksMovement;
	this.zLevelForOverlays = zLevelForOverlays;
	this.visual = visual;
}

function MapTerrainVisual(children)
{
	this.children = children;
	
	var childNames = MapTerrainVisual.ChildNames;
		
	for (var i = 0; i < childNames.length; i++)
	{
		var childName = childNames[i];
		var child = this.children[i];
		this.children[childName] = child;
	}
}
{
	MapTerrainVisual.TestInstance = function()
	{
		// Helpful for debugging.	
		var radius = 3;
		var size = new Coords(5, 5);
		return new MapTerrainVisual
		(
			[
				new VisualRectangle(size, null, "Black"), // 0000 - center
				new VisualCircle(radius, "Red", "Black"), // 0001 - inside se
				new VisualCircle(radius, "Orange", "Black"), // 0010 - inside sw
				new VisualCircle(radius, "Yellow","Black"), // 0011 - edge n
				new VisualCircle(radius, "Green", "Black"), // 0100 - inside ne
				new VisualCircle(radius, "Blue", "Black"),  // 0101 - edge w
				new VisualCircle(radius, "Violet", "Black"), // 0110 - diagonal
				new VisualCircle(radius, "Gray", "Black"),  // 0111 - outside se
				new VisualRectangle(size, "Red", "Black"), // 1000 - inside nw
				new VisualRectangle(size, "Orange", "Black"), // 1001 - diagonal?
				new VisualRectangle(size, "Yellow", "Black"), // 1010 - edge e
				new VisualRectangle(size, "Green", "Black"), // 1011 - outside sw
				new VisualRectangle(size, "Blue", "Black"), // 1100 - edge s
				new VisualRectangle(size, "Violet", "Black"), // 1101 - outside ne
				new VisualRectangle(size, "Gray", "Black"), // 1110 - outside nw
				new VisualRectangle(size, null, "Red"), // 1111 // Never
			]
		);
	}
	
	MapTerrainVisual.ChildNames = 
	[
		"Center",
		"InsideSE",
		"InsideSW",	
		"EdgeN",		
		"InsideNE",
		"EdgeW",
		"DiagonalSlash",
		"OutsideSE",
		"InsideNW",
		"DiagonalBackslash",
		"EdgeE",
		"OutsideSW",
		"EdgeS",
		"OutsideNE",
		"OutsideNW"
	]


	MapTerrainVisual.prototype.draw = function(universe, world, display, drawable)
	{
		this.children["Center"].draw(universe, world, display, drawable);
	}
}

function Mover(name, visual, activity, posInCells)
{
	this.name = name;
	this.visual = visual;
	this.activity = activity;
	this.posInCells = posInCells;

	this.pos = new Coords();
	this.posInCellsNext = new Coords();
	this.posInCellsNextFloor = new Coords();	
	this.velInCellsPerTick = new Coords(0, 0);
}
{
	Mover.prototype.updateForTimerTick = function(universe, world, venue)
	{		
		this.posInCellsNext.overwriteWith
		(
			this.posInCells
		).add
		(
			this.velInCellsPerTick
		);
		
		var map = venue.map;		
		this.posInCellsNextFloor.overwriteWith(this.posInCellsNext).floor();		
		var mapTerrain = map.terrainAtPosInCells(this.posInCellsNextFloor);
		if (mapTerrain.blocksMovement == false)
		{
			this.posInCells.overwriteWith(this.posInCellsNext);
		}
		
		this.pos.overwriteWith
		(
			this.posInCells
		).multiply
		(
			map.cellSizeInPixels
		);
		
		this.activity.perform(universe, world, venue, this);		
	}
	
	// drawable
	
	Mover.prototype.draw = function(universe, world, visualCamera)
	{
		visualCamera.child = this.visual;
		visualCamera.draw(universe, world, universe.display, this);
	}	
}

function Polar(azimuthInTurns, radius)
{
	this.azimuthInTurns = azimuthInTurns;
	this.radius = radius;
}
{
	Polar.RadiansPerTurn = Math.PI * 2.0;

	Polar.prototype.fromCoords = function(coords)
	{
		var azimuthInRadians = Math.atan2(coords.y, coords.x);
		var azimuthInTurns = azimuthInRadians / Polar.RadiansPerTurn;
		if (azimuthInTurns < 0)
		{
			azimuthInTurns += 1;
		}
		this.azimuthInTurns = azimuthInTurns;
		this.radius = coords.magnitude();
		return this;
	}
}

function Portal(defnName, posInCells, destinationVenueName, destinationPosInCells)
{
	this.defnName = defnName;
	this.posInCells = posInCells.addXY(.5, .5);
	this.destinationVenueName = destinationVenueName;
	this.destinationPosInCells = destinationPosInCells;
	
	this.pos = new Coords();
}
{
	Portal.prototype.defn = function(world)
	{
		return world.portalDefns[this.defnName];
	}

	Portal.prototype.activate = function(universe, world, venue, actor)
	{
		var mover = actor;
		venue.moversToRemove.push(mover);
		var venueNext = world.venues[this.destinationVenueName];
		venueNext.movers.splice(0, 0, mover);
		mover.posInCells.overwriteWith(this.destinationPosInCells);
		world.venueNext = venueNext;
	}
	
	Portal.prototype.updateForTimerTick = function(universe, world, venue)
	{
		this.pos.overwriteWith(this.posInCells).multiply(venue.map.cellSizeInPixels);	
	}
	
	// drawable
		
	Portal.prototype.draw = function(universe, world, visualCamera)
	{
		var defn = this.defn(world);
		var visual = defn.visual;
		visualCamera.child = visual;
		visualCamera.draw(universe, world, universe.display, this);
	}
}

function PortalDefn(name, visual)
{
	this.name = name;
	this.visual = visual;
}

function Universe(timerTicksPerSecond, display, world)
{
	this.timerTicksPerSecond = timerTicksPerSecond;
	this.display = display;
	this.world = world;
	
	this.secondsPerTimerTick = 1 / this.timerTicksPerSecond;
}
{
	Universe.prototype.start = function()
	{
		var divMain = document.getElementById("divMain");
		divMain.appendChild(this.display.initialize().canvas);
				
		var timerTicksPerSecond = 10;
		var msPerSecond = 1000;
		var msPerTimerTick = Math.floor(msPerSecond / timerTicksPerSecond);
		this.timer = setInterval
		(
			this.updateForTimerTick.bind(this),
			msPerTimerTick
		);

		this.inputHelper = new InputHelper();
		this.inputHelper.initialize();
		
		this.world.initialize(this);		
	}
	
	Universe.prototype.updateForTimerTick = function()
	{
		this.world.draw(this);		
		this.world.updateForTimerTick(this);
	}
}

function VisualAnimation(framesPerSecond, frames)
{
	this.framesPerSecond = framesPerSecond;
	this.frames = frames;
	
	this.durationInSeconds = this.frames.length / this.framesPerSecond;
}
{
	VisualAnimation.prototype.draw = function(universe, world, display, drawable)
	{
		if (drawable.secondsSinceAnimationStarted == null)
		{
			drawable.secondsSinceAnimationStarted = 0;
		}
		
		var frameIndexCurrent = Math.floor
		(
			drawable.secondsSinceAnimationStarted * this.framesPerSecond
		);
		
		var frameCurrent = this.frames[frameIndexCurrent];
		frameCurrent.draw(universe, world, display, drawable);
		
		drawable.secondsSinceAnimationStarted += universe.secondsPerTimerTick;
		if (drawable.secondsSinceAnimationStarted >= this.durationInSeconds)
		{
			drawable.secondsSinceAnimationStarted -= this.durationInSeconds;
		}
	}
}

function VisualCamera(camera, child)
{
	this.camera = camera;
	this.child = child;
	
	this.drawablePosOriginal = new Coords();
}
{
	VisualCamera.prototype.draw = function(universe, world, display, drawable)
	{
		this.drawablePosOriginal.overwriteWith(drawable.pos);
		drawable.pos.subtract
		(
			this.camera.pos
		).add
		(
			display.sizeInPixelsHalf
		);
		this.child.draw(universe, world, display, drawable);
		drawable.pos.overwriteWith(this.drawablePosOriginal);
	}
}

function VisualCircle(radius, colorFill, colorBorder)
{
	this.radius = radius;
	this.colorFill = colorFill;
	this.colorBorder = colorBorder;
}
{
	VisualCircle.prototype.draw = function(universe, world, display, drawable)
	{
		display.drawCircle(drawable.pos, this.radius, this.colorFill, this.colorBorder);
	}
}

function VisualDirectional(visualAtRest, visualsForDirections)
{
	this.visualAtRest = visualAtRest;
	this.visualsForDirections = visualsForDirections;
	
	this.polar = new Polar();
}
{
	VisualDirectional.prototype.draw = function(universe, world, display, drawable)
	{
		var visualToDraw = null;
		var vel = drawable.velInCellsPerTick;
		if (vel.magnitude() == 0)
		{
			visualToDraw = this.visualAtRest;
		}
		else
		{
			this.polar.fromCoords(vel);
			var azimuthInTurns = this.polar.azimuthInTurns;
			var directionIndex = Math.floor(azimuthInTurns * this.visualsForDirections.length);
			visualToDraw = this.visualsForDirections[directionIndex];
		}
		visualToDraw.draw(universe, world, display, drawable);
	}
}

function VisualGroup(children)
{
	this.children = children;
}
{
	VisualGroup.prototype.draw = function(universe, world, display, drawable)
	{
		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			child.draw(universe, world, display, drawable);
		}
	}
}

function VisualImage(image, size)
{
	this.image = image;
	this.size = (size == null ? this.image.size : size);
	
	this.sizeHalf = this.size.clone().half();
	
	this.drawPos = new Coords();
}
{
	VisualImage.manyFromImages = function(images)
	{
		var returnValues = [];
		for (var i = 0; i < images.length; i++)
		{
			var image = images[i];
			var visual = (image == null ? null : new VisualImage(image));
			returnValues.push(visual);
		}
		return returnValues;
	}

	VisualImage.prototype.draw = function(universe, world, display, drawable)
	{		
		var drawPos = this.drawPos.overwriteWith
		(
			drawable.pos
		).subtract
		(
			this.sizeHalf
		);
	
		display.drawImage(this.image, drawPos);
	}
}

function VisualImageRegion(image, offset, size)
{
	this.image = image;
	this.offset = offset;
	this.size = size;
	
	this.sizeHalf = this.size.clone().half();
	
	this.drawPos = new Coords();
}
{
	VisualImageRegion.prototype.draw = function(universe, world, display, drawable)
	{		
		var drawPos = this.drawPos.overwriteWith
		(
			drawable.pos
		).subtract
		(
			this.sizeHalf
		);
	
		display.drawImageRegion(this.image, this.offset, this.size, drawPos);
	}
}

function VisualOffset(offset, child)
{
	this.offset = offset;
	this.child = child;
	
	this.drawablePosOriginal = new Coords();
}
{
	VisualOffset.prototype.draw = function(universe, world, display, drawable)
	{
		this.drawablePosOriginal.overwriteWith(drawable.pos);
		drawable.pos.add(this.offset);
		this.child.draw(universe, world, display, drawable);
		drawable.pos.overwriteWith(this.drawablePosOriginal);
	}
}

function VisualPolygon(vertices, colorFill, colorBorder)
{
	this.vertices = vertices;
	this.colorFill = colorFill;
	this.colorBorder = colorBorder;
	
	this.verticesTransformed = this.vertices.clone();
}
{
	VisualPolygon.prototype.draw = function(universe, world, display, drawable)
	{
		for (var i = 0; i < this.vertices.length; i++)
		{
			var vertex = this.vertices[i];
			var vertexTransformed = this.verticesTransformed[i];
			
			vertexTransformed.overwriteWith
			(
				vertex
			).add
			(
				drawable.pos
			);
		}
		
		display.drawPolygon(this.verticesTransformed, this.colorFill, this.colorBorder);
	}
}

function VisualRectangle(size, colorFill, colorBorder)
{
	this.size = size;
	this.colorFill = colorFill;
	this.colorBorder = colorBorder;	
	
	this.sizeHalf = this.size.clone().half();
	
	this.drawPos = new Coords();
}
{
	VisualRectangle.prototype.draw = function(universe, world, display, drawable)
	{
		var drawPos = this.drawPos.overwriteWith
		(
			drawable.pos
		).subtract
		(
			this.sizeHalf
		);
		
		display.drawRectangle(drawPos, this.size, this.colorFill, this.colorBorder);
	}
}

function VisualText(text)
{
	this.text = text;
}
{
	VisualText.prototype.draw = function(universe, world, display, drawable)
	{
		display.drawText(this.text, drawable.pos);
	}
}

function Venue(name, camera, map, portals, movers)
{
	this.name = name;
	this.camera = camera;
	this.map = map;
	this.portals = portals;
	this.movers = movers;
	
	this.moversToRemove = [];	
}
{
	Venue.prototype.initialize = function(universe, world)
	{
		this.constraintCameraFollowPlayer = new Constraint_Follow
		(
			this.movers[0]
		);

		this.updateForTimerTick(universe, world);
	}

	Venue.prototype.updateForTimerTick = function(universe, world)
	{			
		for (var i = 0; i < this.portals.length; i++)
		{
			var portal = this.portals[i];
			portal.updateForTimerTick(universe, world, this);
		}
	
		for (var i = 0; i < this.movers.length; i++)
		{
			var mover = this.movers[i];
			mover.updateForTimerTick(universe, world, this);	
		}
		
		for (var i = 0; i < this.moversToRemove.length; i++)
		{
			var mover = this.moversToRemove[i];
			if (mover == this.constraintCameraFollowPlayer.target)
			{
				this.constraintCameraFollowPlayer.target = this.camera;
			}
			this.movers.remove(mover);
		}		
		this.moversToRemove.length = 0;
	}
	
	// drawable
	
	Venue.prototype.draw = function(universe, world)
	{
		this.constraintCameraFollowPlayer.apply(this.camera);
	
		universe.display.clear();
	
		var visualCamera = new VisualCamera(this.camera);
		
		this.map.draw(universe, this, universe.display, visualCamera);
				
		for (var i = 0; i < this.portals.length; i++)
		{
			var portal = this.portals[i];
			portal.draw(universe, world, visualCamera);
		}
		
		for (var i = 0; i < this.movers.length; i++)
		{
			var mover = this.movers[i];
			mover.draw(universe, world, visualCamera);
		}
	}

}

function World(name, portalDefns, activityDefns, venues)
{
	this.name = name;
	this.portalDefns = portalDefns.addLookups("name");
	this.activityDefns = activityDefns.addLookups("name");
	this.venues = venues.addLookups("name");
	
	this.venueNext = this.venues[0];
}
{
	World.demo = function(displaySizeInPixels, cellSizeInPixels)
	{				
		var portalSize = cellSizeInPixels.clone().multiplyScalar(.75);;
		var portalDefns = 
		[
			new PortalDefn
			(
				"PortalTown",
				new VisualPolygon
				(
					[
						new Coords(-.5, 0).multiply(portalSize),
						new Coords(.5, 0).multiply(portalSize),
						new Coords(.5, -.5).multiply(portalSize),
						new Coords(0, -1).multiply(portalSize),
						new Coords(-.5, -.5).multiply(portalSize),
					],
					"LightGreen", "Green"
				)
			),
			new PortalDefn
			(
				"PortalExit",
				new VisualPolygon
				(
					[
						new Coords(-.5, 0).multiply(portalSize),
						new Coords(0, -.5).multiply(portalSize),
						new Coords(0, -.25).multiply(portalSize),
						new Coords(.5, -.25).multiply(portalSize),
						new Coords(.5, .25).multiply(portalSize),
						new Coords(0, .25).multiply(portalSize),
						new Coords(0, .5).multiply(portalSize),
					],
					"LightGreen", "Green"
				)
			)
			
		];
		
		var activityDefns = 
		[
			new ActivityDefn
			(
				"DoNothing", 
				function perform(universe, world, venue, actor, activity) 
				{
					// Do nothing.
				} 
			),
			
			new ActivityDefn
			(
				"MoveRandomly",
				function perform(universe, world, venue, actor, activity) 
				{
					while (activity.target == null)
					{
						actor.posInCells.round();
						var directionToMove = new Coords();
						var heading = Math.floor(4 * Math.random());
						if (heading == 0)
						{
							directionToMove.overwriteWithXY(0, 1);
						}
						else if (heading == 1)
						{
							directionToMove.overwriteWithXY(-1, 0);
						}
						else if (heading == 2)
						{
							directionToMove.overwriteWithXY(1, 0);
						}
						else if (heading == 3)
						{
							directionToMove.overwriteWithXY(0, -1);
						}
						
						var target = actor.posInCells.clone().add
						(
							directionToMove
						);
						
						if (target.isInRangeMax(venue.map.sizeInCells) == true)
						{
							var terrainAtTarget = venue.map.terrainAtPosInCells(target);
							if (terrainAtTarget.blocksMovement == false)
							{
								activity.target = target;
							}
						}
					}
					
					var target = activity.target;
					
					var displacementToTarget = target.clone().subtract
					(
						actor.posInCells
					);
					
					var distanceToTarget = displacementToTarget.magnitude();

					var speedInCellsPerTick = 0.1;
					
					if (distanceToTarget <= speedInCellsPerTick)
					{
						actor.posInCells.overwriteWith(target);
						activity.target = null;
					}
					else
					{
						var directionToTarget = displacementToTarget.divideScalar
						(
							distanceToTarget
						);
						actor.velInCellsPerTick.overwriteWith
						(
							directionToTarget
						).multiplyScalar
						(
							speedInCellsPerTick
						);
					}
				}
			),
			
			new ActivityDefn
			(
				"UserInputAccept", 
				function perform(universe, world, venue, actor, activity) 
				{
					var actorVel = actor.velInCellsPerTick;
					actorVel.clear();
					var inputHelper = universe.inputHelper;
					var inputsActive = inputHelper.inputsActive;
					for (var i = 0; i < inputsActive.length; i++)
					{
						var input = inputsActive[i];
						if (input == null)
						{
							// do nothing
						}
						else if (input.startsWith("Arrow") == true)
						{
							if (input == "ArrowDown")
							{
								actorVel.overwriteWithXY(0, 1);
							}
							else if (input == "ArrowLeft")
							{
								actorVel.overwriteWithXY(-1, 0);
							}
							else if (input == "ArrowRight")
							{
								actorVel.overwriteWithXY(1, 0);
							}
							else if (input == "ArrowUp")
							{
								actorVel.overwriteWithXY(0, -1);
							}
							
							var speedInCellsPerTick = 0.1;
							actorVel.multiplyScalar(speedInCellsPerTick);							
						}
						else if (input == "Enter")
						{
							inputHelper.inputInactivate(input);
							var displacement = new Coords();
							var portals = venue.portals;
							for (var i = 0; i < portals.length; i++)
							{
								var portal = portals[i];
								var distance = displacement.overwriteWith
								(
									portal.pos
								).subtract
								(
									actor.pos
								).magnitude();
								var distanceMax = venue.map.cellSizeInPixels.x;
								if (distance <= distanceMax)
								{
									portal.activate(universe, world, venue, actor);
								}
							}
						}
					}
				} 
			),
			
		];
		
		var colors = Color.Instances()._All;

		var mapTerrainVisualDesert = new MapTerrainVisual(VisualImage.manyFromImages
		([
			Image.fromStrings
			(
				"DesertCenter", 
				colors, 
				[ 
					"yywwyywwyywwyyww",
					"ywwyywwyywwyywwy",
					"wwyywwyywwyywwyy",
					"wyywwyywwyywwyyw",
					"yywwyywwyywwyyww",
					"ywwyywwyywwyywwy",
					"wwyywwyywwyywwyy",
					"wyywwyywwyywwyyw",
					"yywwyywwyywwyyww",
					"ywwyywwyywwyywwy",
					"wwyywwyywwyywwyy",
					"wyywwyywwyywwyyw",
					"yywwyywwyywwyyww",
					"ywwyywwyywwyywwy",
					"wwyywwyywwyywwyy",
					"wyywwyywwyywwyyw",
				]
			),
			Image.fromStrings
			(
				"DesertInsideSE", 
				colors, 
				[ 
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"aaaawyyw........",
					"yywwyyww........",
					"ywwyywwy........",
					"wwyywwyy........",
					"wyywwyyw........",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
				]
			),			
			Image.fromStrings
			(
				"DesertInsideSW", 
				colors, 
				[ 
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywaaaa",
					"........yywwyyww",
					"........ywwyywwy",
					"........wwyywwyy",
					"........wyywwyyw",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
				]
			),			
			Image.fromStrings
			(
				"DesertEdgeN", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"aaaaaaaaaaaaaaaa",
					"yywwyywwyywwyyww",
					"ywwyywwyywwyywwy",
					"wwyywwyywwyywwyy",
					"wyywwyywwyywwyyw",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
				]
			),
			Image.fromStrings
			(
				"DesertInsideNE", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"yywwyyww........",
					"ywwyywwy........",
					"wwyywwyy........",
					"wyywwyyw........",
					"aaaayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
				]
			),
			Image.fromStrings
			(
				"DesertEdgeW", 
				colors, 
				[ 
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
				]
			),			
			Image.fromStrings
			(
				"DesertDiagonalBackslash", 
				colors, 
				[ 
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywaaaa",
					"........yywwyyww",
					"........ywwyywwy",
					"........wwyywwyy",
					"........wyywwyyw",
					"yywwyyww........",
					"ywwyywwy........",
					"wwyywwyy........",
					"wyywwyyw........",
					"aaaayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
				]
			),
			Image.fromStrings
			(
				"DesertOutsideNW", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"...aaaaaaaaaaaaa",
					"...ayywwyywwyyww",
					"...aywwyywwyywwy",
					"...awwyywwyywwyy",
					"...awyywwyywwyyw",
					"...ayywwy.......",
					"...aywwyy.......",
					"...awwyyw.......",
					"...awyyww.......",
					"...ayywwy.......",
					"...aywwyy.......",
					"...awwyyw.......",
					"...awyyww.......",
				]
			),
			Image.fromStrings
			(
				"DesertInsideNW", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"........yywwyyww",
					"........ywwyywwy",
					"........wwyywwyy",
					"........wyywwyyw",
					"........yywwaaaa",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
				]
			),
			Image.fromStrings
			(
				"DesertDiagonalSlash", 
				colors, 
				[ 
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"aaaawyyw........",
					"yywwyyww........",
					"ywwyywwy........",
					"wwyywwyy........",
					"wyywwyyw........",
					"........yywwyyww",
					"........ywwyywwy",
					"........wwyywwyy",
					"........wyywwyyw",
					"........yywwaaaa",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
				]
			),
			Image.fromStrings
			(
				"DesertEdgeE", 
				colors, 
				[ 
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
				]
			),			
			Image.fromStrings
			(
				"DesertOutsideNE", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"aaaaaaaaaaaaa...",
					"yywwyywwyywwa...",
					"ywwyywwyywwya...",
					"wwyywwyywwyya...",
					"wyywwyywwyywa...",
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
				]
			),
			Image.fromStrings
			(
				"DesertEdgeS", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"yywwyywwyywwyyww",
					"ywwyywwyywwyywwy",
					"wwyywwyywwyywwyy",
					"wyywwyywwyywwyyw",
					"aaaaaaaaaaaaaaaa",
					"................",
					"................",
					"................",
				]
			),
			Image.fromStrings
			(
				"DesertOutsideSW", 
				colors, 
				[ 
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
					"...ayyww........",
					"...aywwy........",
					"...awwyy........",
					"...awyyw........",
					"...ayywwyywwyyww",
					"...aywwyywwyywwy",
					"...awwyywwyywwyy",
					"...awyywwyywwyyw",
					"...aaaaaaaaaaaaa",
					"................",
					"................",
					"................",
				]
			),
			Image.fromStrings
			(
				"DesertOutsideSE", 
				colors, 
				[ 
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
					"........yywwa...",
					"........ywwya...",
					"........wwyya...",
					"........wyywa...",
					"yywwyywwyywwa...",
					"ywwyywwyywwya...",
					"wwyywwyywwyya...",
					"wyywwyywwyywa...",
					"aaaaaaaaaaaaa...",
					"................",
					"................",
					"................",
				]
			),
		]));
		
		var mapTerrainVisualRock = new MapTerrainVisual(VisualImage.manyFromImages
		([
			Image.fromStrings
			(
				"RockCenter", 
				colors, 
				[ 
					"--AA--AA--AA--AA",
					"-AA--AA--AA--AA-",
					"AA--AA--AA--AA--",
					"A--AA--AA--AA--A",
					"--AA--AA--AA--AA",
					"-AA--AA--AA--AA-",
					"AA--AA--AA--AA--",
					"A--AA--AA--AA--A",
					"--AA--AA--AA--AA",
					"-AA--AA--AA--AA-",
					"AA--AA--AA--AA--",
					"A--AA--AA--AA--A",
					"--AA--AA--AA--AA",
					"-AA--AA--AA--AA-",
					"AA--AA--AA--AA--",
					"A--AA--AA--AA--A",
				]
			),
			Image.fromStrings
			(
				"RockInsideSE", 
				colors, 
				[ 
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"aaaaA--A........",
					"--AA--AA........",
					"-AA--AA-........",
					"AA--AA--........",
					"A--AA--A........",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
				]
			),			
			Image.fromStrings
			(
				"RockInsideSW", 
				colors, 
				[ 
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aaaaa",
					"........--AA--AA",
					"........-AA--AA-",
					"........AA--AA--",
					"........A--AA--A",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
				]
			),			
			Image.fromStrings
			(
				"RockEdgeN", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"aaaaaaaaaaaaaaaa",
					"--AA--AA--AA--AA",
					"-AA--AA--AA--AA-",
					"AA--AA--AA--AA--",
					"A--AA--AA--AA--A",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
				]
			),
			Image.fromStrings
			(
				"RockInsideNE", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"--AA--AA........",
					"-AA--AA-........",
					"AA--AA--........",
					"A--AA--A........",
					"aaaa--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
				]
			),
			Image.fromStrings
			(
				"RockEdgeW", 
				colors, 
				[ 
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
				]
			),			
			Image.fromStrings
			(
				"RockDiagonalBackslash", 
				colors, 
				[ 
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aaaaa",
					"........--AA--AA",
					"........-AA--AA-",
					"........AA--AA--",
					"........A--AA--A",
					"--AA--AA........",
					"-AA--AA-........",
					"AA--AA--........",
					"A--AA--A........",
					"aaaa--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
				]
			),
			Image.fromStrings
			(
				"RockOutsideNW", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"...aaaaaaaaaaaaa",
					"...a--AA--AA--AA",
					"...a-AA--AA--AA-",
					"...aAA--AA--AA--",
					"...aA--AA--AA--A",
					"...a--AA-.......",
					"...a-AA--.......",
					"...aAA--A.......",
					"...aA--AA.......",
					"...a--AA-.......",
					"...a-AA--.......",
					"...aAA--A.......",
					"...aA--AA.......",
				]
			),
			Image.fromStrings
			(
				"RockInsideNW", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"........--AA--AA",
					"........-AA--AA-",
					"........AA--AA--",
					"........A--AA--A",
					"........--AAaaaa",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
				]
			),
			Image.fromStrings
			(
				"RockDiagonalSlash", 
				colors, 
				[ 
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"aaaaA--A........",
					"--AA--AA........",
					"-AA--AA-........",
					"AA--AA--........",
					"A--AA--A........",
					"........--AA--AA",
					"........-AA--AA-",
					"........AA--AA--",
					"........A--AA--A",
					"........--AAaaaa",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
				]
			),
			Image.fromStrings
			(
				"RockEdgeE", 
				colors, 
				[ 
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
				]
			),			
			Image.fromStrings
			(
				"RockOutsideNE", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"aaaaaaaaaaaaa...",
					"--AA--AA--AAa...",
					"-AA--AA--AA-a...",
					"AA--AA--AA--a...",
					"A--AA--AA--Aa...",
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
				]
			),
			Image.fromStrings
			(
				"RockEdgeS", 
				colors, 
				[ 
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"................",
					"--AA--AA--AA--AA",
					"-AA--AA--AA--AA-",
					"AA--AA--AA--AA--",
					"A--AA--AA--AA--A",
					"aaaaaaaaaaaaaaaa",
					"................",
					"................",
					"................",
				]
			),
			Image.fromStrings
			(
				"RockOutsideSW", 
				colors, 
				[ 
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
					"...a--AA........",
					"...a-AA-........",
					"...aAA--........",
					"...aA--A........",
					"...a--AA--AA--AA",
					"...a-AA--AA--AA-",
					"...aAA--AA--AA--",
					"...aA--AA--AA--A",
					"...aaaaaaaaaaaaa",
					"................",
					"................",
					"................",
				]
			),
			Image.fromStrings
			(
				"RockOutsideSE", 
				colors, 
				[ 
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
					"........--AAa...",
					"........-AA-a...",
					"........AA--a...",
					"........A--Aa...",
					"--AA--AA--AAa...",
					"-AA--AA--AA-a...",
					"AA--AA--AA--a...",
					"A--AA--AA--Aa...",
					"aaaaaaaaaaaaa...",
					"................",
					"................",
					"................",
				]
			),
		]));
		
		//mapTerrainVisualDesert = MapTerrainVisual.TestInstance();
				
		var mapTerrains = 
		[
			new MapTerrain
			(
				"Desert", 
				".", 
				false, // blocksMovement
				1, // zLevelForOverlays
				mapTerrainVisualDesert
			),
			new MapTerrain
			(
				"Rocks", 
				"x", 
				true, // blocksMovement 
				2, // zLevelForOverlays
				mapTerrainVisualRock
			),
			
			new MapTerrain
			(
				"Water", 
				"~", 
				true, // blocksMovement 
				0, // zLevelForOverlays
				new MapTerrainVisual([new VisualImage
				(
					Image.fromStrings
					(
						"Water", 
						colors, 
						[ 
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",
							"cccccccccccccccc",								
						]
					)
				)])
			)
		];
					
		var moverVisual = new VisualDirectional
		(
			// visualAtRest
			new VisualGroup
			([
				new VisualPolygon
				(
					[
						new Coords(0, -1).multiply(cellSizeInPixels),
						new Coords(-.5, 0).multiply(cellSizeInPixels),
						new Coords(.5, 0).multiply(cellSizeInPixels),
					],
					"Gray", null
				),
				new VisualOffset
				(
					new Coords(0, -cellSizeInPixels.y / 2),
					new VisualCircle(cellSizeInPixels.x / 4, "Tan", null)
				)
			]),
			// visualsForDirections
			[
				// east
				new VisualAnimation
				(
					4, // framesPerSecond
					[
						new VisualPolygon
						(
							[
								new Coords(.4, -1).multiply(cellSizeInPixels),
								new Coords(-.5, 0).multiply(cellSizeInPixels),
								new Coords(.5, 0).multiply(cellSizeInPixels),
							],
							"Gray", null
						),
						new VisualPolygon
						(
							[
								new Coords(.5, -.9).multiply(cellSizeInPixels),
								new Coords(-.5, 0).multiply(cellSizeInPixels),
								new Coords(.5, 0).multiply(cellSizeInPixels),
							],
							"Gray", null
						),						
					]
				),
				
				// south
				new VisualAnimation
				(
					4, // framesPerSecond
					[
						new VisualGroup
						([
							new VisualPolygon
							(
								[
									new Coords(0, -1).multiply(cellSizeInPixels),
									new Coords(-.5, 0).multiply(cellSizeInPixels),
									new Coords(.5, 0).multiply(cellSizeInPixels),
								],
								"Gray", null
							),
							new VisualOffset
							(
								new Coords(0, -cellSizeInPixels.y * .5),
								new VisualCircle(cellSizeInPixels.x / 4, "Tan", null)
							)
						]),

						new VisualGroup
						([
							new VisualPolygon
							(
								[
									new Coords(0, -.9).multiply(cellSizeInPixels),
									new Coords(-.5, 0).multiply(cellSizeInPixels),
									new Coords(.5, 0).multiply(cellSizeInPixels),
								],
								"Gray", null
							),
							new VisualOffset
							(
								new Coords(0, -cellSizeInPixels.y * .4),							
								new VisualCircle(cellSizeInPixels.x / 4, "Tan", null)
							)
						]),							
					]
				),

				// west
				new VisualAnimation
				(
					4, // framesPerSecond
					[
						new VisualPolygon
						(
							[
								new Coords(-.4, -1).multiply(cellSizeInPixels),
								new Coords(-.5, 0).multiply(cellSizeInPixels),
								new Coords(.5, 0).multiply(cellSizeInPixels),
							],
							"Gray", null
						),
						new VisualPolygon
						(
							[
								new Coords(-.5, -.9).multiply(cellSizeInPixels),
								new Coords(-.5, 0).multiply(cellSizeInPixels),
								new Coords(.5, 0).multiply(cellSizeInPixels),
							],
							"Gray", null
						),						
					]
				),

				// north
				new VisualAnimation
				(
					4, // framesPerSecond
					[
						new VisualPolygon
						(
							[
								new Coords(0, -1).multiply(cellSizeInPixels),
								new Coords(-.5, 0).multiply(cellSizeInPixels),
								new Coords(.5, 0).multiply(cellSizeInPixels),
							],
							"Gray", null
						),
						
						new VisualPolygon
						(
							[
								new Coords(0, -.9).multiply(cellSizeInPixels),
								new Coords(-.5, 0).multiply(cellSizeInPixels),
								new Coords(.5, 0).multiply(cellSizeInPixels),
							],
							"Gray", null
						),							
					]
				),
			]
		);
						
		var venues = 
		[ 
			new Venue
			(
				"Overworld",
				new Camera(displaySizeInPixels, new Coords(0, 0)),
				new Map
				(
					cellSizeInPixels,
					mapTerrains,
					[
						"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~",
						"~....x.........................~",
						"~..........~~..................~",
						"~.........~~~~.................~",
						"~.........~~~~...xx............~",
						"~..........~~....x...xx........~",
						"~..............xxxx.x.x........~",
						"~................x.............~",
						"~.........~.~.......x..........~",
						"~..........~...................~",
						"~..............................~",
						"~..............................~",
						"~..............................~",
						"~..............................~",
						"~..............................~",
						"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~",
					]
				),
				[
					new Portal
					(
						"PortalTown", 
						new Coords(17, 9),
						"Lonelytown", // destinationVenueName
						new Coords(1, 4) // destinationPosInCells
					)
				],
				[
					new Mover
					(
						"Player",
						moverVisual,
						new Activity("UserInputAccept", null), 
						new Coords(16, 8) // posInCells
					),
				]
			),
			
			new Venue
			(
				"Lonelytown",
				new Camera(displaySizeInPixels, new Coords(0, 0)),
				new Map
				(
					cellSizeInPixels,
					mapTerrains,
					[
						"xxxxxxxxxxxxxxxx",
						"x..............x",
						"x..............x",
						"x..............x",
						"x..............x",
						"x..............x",
						"x..............x",
						"xxxxxxxxxxxxxxxx",
					]
				),
				[
					new Portal
					(
						"PortalExit", 
						new Coords(1, 4), // posInCells
						"Overworld", // destinationVenueName
						new Coords(17, 9) // destinationPosInCells
					)
				],
				[
					new Mover
					(
						"Stranger",
						moverVisual,
						new Activity("MoveRandomly", null), 
						new Coords(4, 4) // posInCells
					),
				]
			),
			
		];
	
		var returnValue = new World
		(
			"WorldDemo",
			portalDefns,
			activityDefns,
			venues
		);
		
		return returnValue;
	}
	
	// instance methods
	
	World.prototype.initialize = function(universe)
	{
		this.updateForTimerTick(universe);
	}
		
	World.prototype.updateForTimerTick = function(universe)
	{
		if (this.venueNext != null)
		{
			this.venueCurrent = this.venueNext;
			this.venueCurrent.initialize(universe, this);
			this.venueNext = null;
		}
		this.venueCurrent.updateForTimerTick(universe, this);
	}
	
	World.prototype.draw = function(universe)
	{
		this.venueCurrent.draw(universe, this, universe.display);
	}
		
}

// run

main();

</script>

</body>
</html>

Advertisement
This entry was posted in Uncategorized and tagged , , , , . Bookmark the permalink.

1 Response to A Scrolling Map Tile Engine in JavaScript

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s