A Simple Game Loop in JavaScript

The code included below implements a simple game in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser running JavaScript.

The object of the game is to collect all the briefcases and leave the room through the door, while avoiding contact with enemy agents. Press the W, A, S, and D keys to move, and the F key to fire.

SimpleRealTimeGame

<html>
<body>
<script type="text/javascript">

// application

function Simulation()
{}
{
	var prototype = Simulation.prototype;
	prototype.main = function()
	{
		Camera.initializeStatic();

		var imageMoverProjectile = ImageHelper.buildImageFromStrings
		(
			[
				"yyy",
				"y.y",
				"yyy",
			]			
		);

		var imageItem = ImageHelper.buildImageFromStrings
		(
			[
				".....oooo.....",
				"....o....o....",
				"oooooooooooooo",
				"onnnnnnnnnnnno",
				"onnnnnnnnnnnno",
				"onnnnnnnnnnnno",
				"onnnnnnnnnnnno",
				"onnnnnnnnnnnno",
				"onnnnnnnnnnnno",
				"onnnnnnnnnnnno",
				"onnnnnnnnnnnno",
				"oooooooooooooo",
			]
		);

		var imageMessageLose = ImageHelper.buildImageFromStringsScaled
		(
			Coords.Instances.Twos,
			[
				"r.....rrrrr.rrrrr.rrrrr",
				"r.....r...r.r.....r....",
				"r.....r...r.rrrrr.rrrrr",
				"r.....r...r.....r.r....",
				"rrrrr.rrrrr.rrrrr.rrrrr",
			]			
		);

		var imageMessageWin = ImageHelper.buildImageFromStringsScaled
		(
			Coords.Instances.Twos,
			[
				"g...........g.ggggg.gg....g.g",
				".g.........g....g...g.g...g.g",
				"..g...g...g.....g...g..g..g.g",
				"...g.g.g.g......g...g...g.g..",
				"....g...g.....ggggg.g....gg.g",
			]			
		);

		var imageMoverPlayer0 = ImageHelper.buildImageFromStrings
		(
			[
				".......bb..bb.......",
				"......baabbaab......",
				"......baaaaaab......",
				"....bbbbbbbbbbbb....",
				"......baaaaaab......",
				"......baaaaaab......",
				"......baaaaaab......",
				"......baaaaaab......",
				".......bbbbbb.......",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".bbbbbbbbbbbbbbbbbb.",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				"........b..b........",
				".......b....b.......",
				"......b......b......",
				".....b........b.....",
				"....b..........b....",
				"...b............b...",
				"..b..............b..",
				".b................b.",
				"b..................b",
			]
		);

		var imageMoverPlayer1 = ImageHelper.buildImageFromStrings
		(
			[
				".......bb..bb.......",
				"......baabbaab......",
				"......baaaaaab......",
				"....bbbbbbbbbbbb....",
				"......baaaaaab......",
				"......baaaaaab......",
				"......baaaaaab......",
				"......baaaaaab......",
				".......bbbbbb.......",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".bbbbbbbbbbbbbbbbbb.",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
				".........bb.........",
			]
		);

		var imageMoverEnemy = ImageHelper.buildImageFromStrings
		(
			[
				".......rr..rr.......",
				"......raarraar......",
				"......raaaaaar......",
				"....rrrrrrrrrrrr....",
				"......raaaaaar......",
				"......raaaaaar......",
				"......raaaaaar......",
				"......raaaaaar......",
				".......rrrrrr.......",
				".........rr.........",
				".........rr.........",
				".........rr.........",
				".rrrrrrrrrrrrrrrrrr.",
				".........rr.........",
				".........rr.........",
				".........rr.........",
				".........rr.........",
				".........rr.........",
				".........rr.........",
				".........rr.........",
				".........rr.........",
				"........r..r........",
				".......r....r.......",
				"......r......r......",
				".....r........r.....",
				"....r..........r....",
				"...r............r...",
				"..r..............r..",
				".r................r.",
				"r..................r",
			]
		);

		var imagePortal = ImageHelper.buildImageFromStrings
		(
			[
				"nnnnnnnnnnnnnnnnnnnn",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"nooooooooooooooyyoon",
				"nooooooooooooooyyoon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"noooooooooooooooooon",
				"nnnnnnnnnnnnnnnnnnnn",
			]
		);

		var bodyDefnItem = new BodyDefn
		(
			"Item",
			[
				Category.Instances.Collidable,
				Category.Instances.Drawable, 
				Category.Instances.Item,
			],
			1, // integrityMax
			-1, // ticksToLive
			1, // mass,
			0, // force,
			0, // speedMax
			new Coords(16, 16),
			ActivityDefn.Instances.DoNothing,
			AnimationDefnSet.buildFromImage(imageItem)
		);

		var bodyDefnMessageLose = new BodyDefn
		(
			"MessageLose",
			[Category.Instances.Drawable],
			1, // integrityMax
			-1, // ticksToLive
			1, // mass,
			0, // force,
			0, // speedMax
			new Coords(23, 5),
			ActivityDefn.Instances.DoNothing,
			AnimationDefnSet.buildFromImage(imageMessageLose)
		);

		var bodyDefnMessageWin = new BodyDefn
		(
			"MessageWin",
			[Category.Instances.Drawable],
			1, // integrityMax
			-1, // ticksToLive
			1, // mass,
			0, // force,
			0, // speedMax
			new Coords(29, 5),
			ActivityDefn.Instances.DoNothing,
			AnimationDefnSet.buildFromImage(imageMessageWin)			
		);

		var bodyDefnMoverProjectile = new BodyDefn
		(
			"Projectile",
			[
				Category.Instances.Actor, 
				Category.Instances.Collidable,
				Category.Instances.Drawable,
				Category.Instances.Ephemeral,
				Category.Instances.Mover, 
				Category.Instances.Projectile,
			],
			1, // integrityMax
			16, // ticksToLive
			1, // mass,
			1, // force,
			16, // speedMax
			new Coords(3, 3),
			ActivityDefn.Instances.Age,
			AnimationDefnSet.buildFromImage(imageMoverProjectile)
		);

		var bodyDefnMoverPlayer = new BodyDefn
		(
			"Player", 	
			[
				Category.Instances.Actor,
				Category.Instances.Collidable, 
				Category.Instances.Drawable,
				Category.Instances.Killable,
				Category.Instances.Mover, 
				Category.Instances.Player
			],
			1, // integrityMax
			-1, // ticksToLive
			1, // mass
			2, // forcePerTick
			8, // speedMax
			new Coords(20, 25),
			ActivityDefn.Instances.UserInputAccept,
			new AnimationDefnSet
			(
				"AnimationDefnSetPlayer",
				[
					new AnimationDefn
					(
						"AnimationDefnPlayer",
						"AnimationDefnPlayer", // animationDefnNameNext
						[
							new AnimationFrame(imageMoverPlayer0),
							new AnimationFrame(imageMoverPlayer1),
						]
					)
				]
			)
		);

		var bodyDefnMoverEnemy = new BodyDefn
		(
			"Enemy", 	
			[
				Category.Instances.Actor,
				Category.Instances.Collidable,
				Category.Instances.Drawable,
				Category.Instances.Enemy,
				Category.Instances.Killable, 
				Category.Instances.Mover, 
			],
			1, // integrityMax
			-1, // ticksToLive
			1, // mass
			1, // forcePerTick
			4, // speedMax
			new Coords(20, 25),
			ActivityDefn.Instances.MoveRandomly,
			AnimationDefnSet.buildFromImage(imageMoverEnemy)
		);

		var bodyDefnPortal = new BodyDefn
		(
			"Portal",
			[Category.Instances.Drawable, Category.Instances.Portal],
			1, // integrityMax
			-1, // ticksToLive
			1, // mass,
			0, // force,
			0, // speedMax
			new Coords(20, 25),
			ActivityDefn.Instances.DoNothing,
			AnimationDefnSet.buildFromImage(imagePortal)
		);

		var venueDefns = 
		[
			new VenueDefn
			(
				"VenueDefn0",
				[
					Camera.BodyDefn,
					bodyDefnItem,
					bodyDefnMessageLose,
					bodyDefnMessageWin,
					bodyDefnMoverProjectile,
					bodyDefnMoverEnemy,
					bodyDefnMoverPlayer,
					bodyDefnPortal,
				],
				[
					new InputBinding(InputKey.Instances.A, new Action_Accelerate(new Coords(-1, 0))),
					new InputBinding(InputKey.Instances.D, new Action_Accelerate(new Coords(1, 0))),
					new InputBinding(InputKey.Instances.S, new Action_Accelerate(new Coords(0, 1))),
					new InputBinding(InputKey.Instances.W, new Action_Accelerate(new Coords(0, -1))),
					new InputBinding(InputKey.Instances.F, new Action_Fire()),
				]
			),
		];

		var venue0 = new Venue
		(
			"Venue0",
			venueDefns[0],
			new Coords(480, 480), // sizeInPixels
			new Map
			(
				"Venue0Map",
				new Coords(30, 30), // cellSizeInPixels
				[
					"xxxxxxxxxxxxxxxx",
					"x..............x",
					"x..............x",
					"x..............x",
					"x..............x",
					"x..............x",
					"x..............x",
					"x..............x",
					"x..............x",
					"x..............x",
					"x..............x",
					"x..............x",
					"x..............x",
					"x..............x",
					"x..............x",
					"xxxxxxxxxxxxxxxx",
				]
			),
			new Array
			(
				// portals

				new Body("Portal 0", bodyDefnPortal, new Coords(400, 100)),

				// player

				new Body("Player", bodyDefnMoverPlayer, new Coords(100, 100)),

				// other movers

				new Body("Enemy 0", bodyDefnMoverEnemy, new Coords(200, 100)),
				new Body("Enemy 1", bodyDefnMoverEnemy, new Coords(300, 100)),

				// items

				new Body("Item 0", bodyDefnItem, new Coords(100, 200)),
				new Body("Item 1", bodyDefnItem, new Coords(100, 300)),
				new Body("Item 2", bodyDefnItem, new Coords(200, 300)),
				new Body("Item 3", bodyDefnItem, new Coords(300, 300))

			)
		);

		var universe0 = new Universe
		(
			"Universe0",
			venueDefns, 
			[venue0]
		);

		Globals.Instance.initialize
		(
			100, //realWorldMillisecondsPerTick,
			new Coords(640, 480), //viewSizeInPixels, 
			universe0
		);
	}
}

// classes

function Action(name)
{
	this.name = name;
}

function Action_Accelerate(directionToAccelerate)
{
	this.name = "Accelerate " + directionToAccelerate.toString();
	this.directionToAccelerate = directionToAccelerate;
}
{
	Action_Accelerate.prototype.perform = function(actor)
	{
		var forceToAdd = this.directionToAccelerate.clone().multiplyScalar
		(
			actor.defn.force
		);

		actor.loc.force.add(forceToAdd);
	}
}

function Action_Fire()
{
	// do nothing
}
{
	Action_Fire.prototype.perform = function(actor)
	{
		var venue = actor.loc.venue;
		var bodyDefnProjectile = venue.defn.bodyDefns["Projectile"];

		var bodyToSpawn = new Body
		(
			"[projectile]", 
			bodyDefnProjectile,
			actor.loc.pos.clone()
		);

		bodyToSpawn.loc.vel = actor.loc.vel.clone().normalize().multiplyScalar
		(
			bodyDefnProjectile.speedMax
		);

		venue.bodiesToSpawn.push(bodyToSpawn);
	}	
}

function Activity(actor, defn, target)
{
	this.actor = actor;
	this.defn = defn;
	this.target = target;
}
{
	// instance methods

	var prototype = Activity.prototype;

	prototype.initialize = function()
	{
		this.vars = new Activity_Variables();
		this.defn.initialize(this);
	}

	prototype.perform = function()
	{
		this.defn.perform(this);
	}

	// inner classes
	function Activity_Variables()
	{}
}

function ActivityDefn()
{}
{
	ActivityDefn.Instances = new ActivityDefn_Instances();

	function ActivityDefn_Instances()
	{
		this.DoNothing = new function()
		{
			this.initialize = function(activity) 
			{
				// do nothing
			}

			this.perform = function(activity)
			{
				// do nothing
			}
		}

		//

		this.Age = new function()
		{
			this.initialize = function(activity) 
			{
				// do nothing
			}

			this.perform = function(activity)
			{
				activity.actor.integrity--;
			}
		}

		this.MoveRandomly = new function()
		{
			this.initialize = function(activity) 
			{}

			this.perform = function(activity)
			{
				var actor = activity.actor;
				var actorLoc = actor.loc;
				var actorPos = actorLoc.pos;

				if (activity.vars.target == null)
				{
					activity.vars.target = this.perform_ChooseNewTarget
					(
						activity
					);
				}

				var target = activity.vars.target;

				var displacementToTarget = target.clone().subtract(actorPos);
				var distanceToTarget = displacementToTarget.magnitude();
				var directionToAccelerate = displacementToTarget.clone().normalize();

				var speedCurrent = actorLoc.vel.magnitude();
				if (speedCurrent > 0)
				{
					var timeToTarget = 
						distanceToTarget / speedCurrent;

					var accelerationCurrent = 
						actor.defn.force / actor.massCurrent();

					if (speedCurrent > accelerationCurrent * timeToTarget)
					{
						directionToAccelerate.multiplyScalar(-1);	
					}
				}

				var distanceThreshold = 8;
				if (distanceToTarget < distanceThreshold)
				{
					activity.vars.target = null;
				}
				else
				{
					var forceToApplyTowardTarget = directionToAccelerate.clone().multiplyScalar(actor.defn.force);
					actorLoc.force.add(forceToApplyTowardTarget);
				}
			}

			this.perform_ChooseNewTarget = function(activity)
			{
				var actor = activity.actor;
				var actorLoc = actor.loc;
				var actorVenue = actorLoc.venue;
				var venueSizeInPixels = actorVenue.sizeInPixels;

				var returnValue = new Coords
				(
					Math.floor(Math.random() * venueSizeInPixels.x), 
					Math.floor(Math.random() * venueSizeInPixels.y)
				);	

				return returnValue;
			}
		}

		this.UserInputAccept = new function()
		{
			this.initialize = function(activity) 
			{}

			this.perform = function(activity)
			{
				var inputHelper = Globals.Instance.inputHelper;
				var actionsFromInput = inputHelper.actionsBeingPerformed;
				var actionsFromActor = activity.actor.actions; 

				for (var a = 0; a < actionsFromInput.length; a++)
				{
					actionsFromActor.push(actionsFromInput[a]);
				}	
			}
		}

	}
}

function AnimationDefn(name, animationDefnNameNext, frames)
{
	this.name = name;
	this.animationDefnNameNext = animationDefnNameNext;
	this.frames = frames;
}
{
	AnimationDefn.buildManyFromImageSets = function(imageSetsForFrames)
	{
		var returnValues = new Array();

		var numberOfImageSets = imageSetsForFrames.length;

		for (var i = 0; i < numberOfImageSets; i++)
		{
			var imageSet = imageSetsForFrames[i];

			var animationDefn = new AnimationDefn
			(
				"" + i,
				"" + i,
				AnimationFrame.buildManyFromImages(imageSet)
			);

			returnValues.push(animationDefn);
		}

		return returnValues;
	}
}

function AnimationDefnSet(name, animationDefns)
{
	this.name = name;
	this.animationDefns = animationDefns;

	for (var i = 0; i < this.animationDefns.length; i++)
	{
		var animationDefn = this.animationDefns[i];
		this.animationDefns[animationDefn.name] = animationDefn;
	}
}
{
	AnimationDefnSet.buildFromImage = function(image)
	{
		var imageAsImageSets = 
		[
			[
				image
			]
		];

		var returnValue = new AnimationDefnSet
		(
			image.id, 
			AnimationDefn.buildManyFromImageSets
			(
				imageAsImageSets
			)
		);

		return returnValue;
	}

	AnimationDefnSet.buildFromImageForHeadings = function(image, numberOfHeadings)
	{
		var imageAsImageSet = new Array();
		imageAsImageSet.push(image);
		var imageAsImageSets = new Array();

		for (var i = 0; i < numberOfHeadings; i++)
		{
			imageAsImageSets.push(imageAsImageSet);
		}

		var returnValue = new AnimationDefnSet
		(
			image.id, 
			AnimationDefn.buildManyFromImageSets
			(
				imageAsImageSets
			)
		);

		return returnValue;
	}

}

function AnimationFrame(image, ticksToHold)
{
	this.image = image;
	this.ticksToHold = ticksToHold;
}
{
	AnimationFrame.buildManyFromImages = function(imagesForFrames)
	{
		var returnValues = new Array();

		var numberOfImages = imagesForFrames.length;

		for (var i = 0; i < numberOfImages; i++)
		{
			var imageForFrame = imagesForFrames[i];
			var frame = new AnimationFrame(imageForFrame, 1);

			returnValues.push(frame);
		}

		return returnValues;
	}
}

function AnimationRun(animationDefnSet)
{
	this.animationDefnSet = animationDefnSet;

	this.animationDefnNameCurrent = this.animationDefnSet.animationDefns[0].name;
	this.frameIndexCurrent = 0;
}
{
	var prototype = AnimationRun.prototype;

	prototype.advance = function()
	{
		this.frameIndexCurrent++;

		var animationDefnCurrent = this.animationDefnCurrent();
		if (this.frameIndexCurrent >= animationDefnCurrent.frames.length)
		{
			this.animationDefnNameCurrent_Set
			(
				animationDefnCurrent.animationDefnNameNext
			);
			this.frameIndexCurrent = 0;
		}
	}

	prototype.animationDefnCurrent = function()
	{
		return this.animationDefnSet.animationDefns[this.animationDefnNameCurrent];
	}

	prototype.animationDefnNameCurrent_Set = function(value)
	{
		if (this.animationDefnNameCurrent != value)
		{
			this.animationDefnNameCurrent = value;
			this.frameIndexCurrent = 0;
		}
	}

	prototype.frameCurrent = function()
	{
		return this.animationDefnCurrent().frames[this.frameIndexCurrent];
	}

	prototype.htmlElementBuild = function()
	{
		var returnValue = document.createElement("div");
		returnValue.id = "AnimationRun_" + this.name;

		this.htmlElementForImage = document.createElement("img");
		this.htmlElementForImage.src = this.animationDefnSet.animationDefns[0].frames[0].image.htmlElement.src;
		returnValue.appendChild(this.htmlElementForImage);

		this.htmlElement = returnValue;

		return this.htmlElement;
	}

	prototype.htmlElementUpdate = function()
	{
		var htmlElementForImageNext = this.frameCurrent().image.htmlElement;

		if (this.htmlElementForImage != htmlElementForImageNext)
		{
			this.htmlElementForImage.src = htmlElementForImageNext.src;
		}
	}
}

function Body(name, defn, pos)
{
	this.name = name;
	this.defn = defn;
	this.loc = new Location(null, pos);

	this.integrity = this.defn.integrityMax;
	this.items = new Array();
	//this.animationRun = new AnimationRun(this.defn.animationDefnSet);
}
{
	Body.BodyText = "Body";

	var prototype = Body.prototype;

	prototype.activity_Get = function() { return this._activity; }
	prototype.activity_Set = function(value) 
	{ 
		this._activity = value; 
		this._activity.initialize(); 
	}

	prototype.htmlElementBuild = function()
	{
		this.htmlElement = this.animationRun.htmlElementBuild();
		this.htmlElement.style.position = "absolute";

		return this.htmlElement;
	}

	prototype.htmlElementUpdate = function()
	{	
		var camera = this.loc.venue.camera;
		var cameraPos = camera.body.loc.pos;

		var pos = this.loc.pos;
		var sizeHalf = this.defn.sizeInPixelsHalf;

		this.htmlElement.style.left = (pos.x - sizeHalf.x - cameraPos.x) + "px";
		this.htmlElement.style.top = (pos.y - sizeHalf.y - cameraPos.y) + "px";

		this.animationRun.htmlElementUpdate();
	}

	prototype.massCurrent = function()
	{
		var returnValue = this.defn.massBase;

		for (var i = 0; i < this.items.length; i++)
		{
			returnValue += this.items[i].defn.massBase;
		}

		return returnValue;
	}
}

function BodyDefn
(
	name, 
	categories, 
	integrityMax, 
	ticksToLive,
	massBase, 
	force, 
	speedMax, 
	sizeInPixels, 
	activityDefnInitial,
	animationDefnSet
)
{
	this.name = name;
	this.categories = categories;
	this.ticksToLive = ticksToLive;
	this.integrityMax = integrityMax;
	this.massBase = massBase;
	this.force = force;
	this.speedMax = speedMax;
	this.sizeInPixels = sizeInPixels;
	this.activityDefnInitial = activityDefnInitial;
	this.animationDefnSet = animationDefnSet;

	this.sizeInPixelsHalf = this.sizeInPixels.clone().divideScalar(2);
}
{
}

function Camera(name, viewSizeInPixels)
{
	this.name = name;
	this.viewSizeInPixels = viewSizeInPixels;

	this.viewSizeInPixelsHalf = this.viewSizeInPixels.clone().divideScalar(2);

	this.body = new Body
	(
		this.name + "_CameraBody",
		Camera.BodyDefn,
		new Coords(0, 0)
	);
}
{
	// constants

	Camera.initializeStatic = function()
	{
		Camera.ViewSizeStandard = new Coords(200, 200);

		Camera.BodyDefn = new BodyDefn
		(
			"CameraBody",
			[Category.Instances.Camera],
			1, // integrityMax
			-1, // ticksToLive
			1, // mass,
			0, // force,
			0, // speedMax
			new Coords(0, 0), // sizeInPixels
			ActivityDefn.Instances.DoNothing,
			null // animationDefnSet
		);

	}
}

function Category(name, methodForCollision, initializeBodyForVenue, updateBodyForVenue)
{
	this.name = name;
	this.methodForCollision = methodForCollision;
	this.initializeBodyForVenue = initializeBodyForVenue;
	this.updateBodyForVenue = updateBodyForVenue;
}
{
	// instances

	Category.Instances = new Category_Instances();

	function Category_Instances()
	{
		this.Actor = new Category
		(
			"Actor", 
			null, 
			// initialize
			function(body, venue)
			{
				body.actions = new Array();

				body.activity_Set
				(
					new Activity
					(
						body,
						body.defn.activityDefnInitial,
						null
					)
				);
			},
			// update
			function(body, venue)
			{
				body.activity_Get().perform();

				var bodyActions = body.actions;

				for (var a = 0; a < bodyActions.length; a++)
				{
					bodyActions[a].perform(body);
				}

				bodyActions.length = 0;
			}
		);

		this.Camera = new Category("Camera", null, null, null);
		this.Collidable = new Category("Collidable", null, null, null);

		this.Drawable = new Category
		(
			"Drawable", 
			null, 
			// initialize
			function(body, venue)
			{
				body.animationRun = new AnimationRun
				(
					body.defn.animationDefnSet
				);

				venue.htmlElement.appendChild(body.htmlElementBuild());
			},
			// update
			function(body, venue)
			{
				body.animationRun.advance();

				body.htmlElementUpdate();
			}
		);

		this.Enemy = new Category("Enemy", null, null, null);

		this.Ephemeral 	= new Category
		(
			"Ephemeral", 
			null, 
			// initialize
			function(body, venue) { body.ticksToLive = body.defn.ticksToLive; }, 
			// update
			function(body, venue) 
			{ 
				body.ticksToLive--; 
				if (body.ticksToLive <= 0)
				{
					venue.bodiesToRemove.push(body);
				}
			}
		);

		this.Item = new Category("Item", null, null, null);

		this.Killable = new Category
		(
			"Killable", 
			null, 
			function(body, venue) { body.integrity = body.defn.integrityMax; }, // initialize
			function(body, venue) { if (body.integrity <= 0) { venue.bodiesToRemove.push(body); } } 
		);

		this.Mover = new Category
		(
			"Mover", 
			null, 
			null, 
			// update
			function(body, venue)
			{
				var mover = body;

				var moverMass = mover.massCurrent();
				var moverLoc = mover.loc;

				var accelToAdd = moverLoc.force.clone().divideScalar(moverMass);
				moverLoc.accel.add(accelToAdd);

				moverLoc.vel.add(moverLoc.accel);
				if (moverLoc.vel.magnitude() > mover.defn.speedMax)
				{
					moverLoc.vel.normalize().multiplyScalar(mover.defn.speedMax);
				}

				moverLoc.pos.add(moverLoc.vel);

				moverLoc.pos.trimToRange(venue.sizeInPixels);

				moverLoc.accel.clear();
				moverLoc.force.clear();
			}
		);

		this.Player = new Category
		(
			"Player",
			function(bodyThis, bodyOther)
			{
				var bodyOtherCategories = bodyOther.defn.categories;
				if (bodyOtherCategories.indexOf(Category.Instances.Enemy) >= 0)
				{
					bodyThis.integrity = 0;

					var venue = bodyThis.loc.venue;

					venue.bodiesToSpawn.push
					(
						new Body
						(
							"messageLose",
							venue.defn.bodyDefns["MessageLose"],
							venue.sizeInPixelsHalf
						)
					);
				}
				else if (bodyOtherCategories.indexOf(Category.Instances.Item) >= 0)
				{
					bodyThis.items.push(bodyOther);
					bodyOther.loc.venue.bodiesToRemove.push(bodyOther);
				}
				else if (bodyOtherCategories.indexOf(Category.Instances.Portal) >= 0)
				{
					var venue = bodyThis.loc.venue;

					if (venue.bodiesByCategory[Category.Instances.Item.name].length == 0)
					{
						venue.bodiesToRemove.push(bodyThis);
						venue.bodiesToSpawn.push
						(
							new Body
							(
								"messageWin",
								venue.defn.bodyDefns["MessageWin"],
								venue.sizeInPixelsHalf
							)
						);
					}
				}
			},
			null, // initialize
			null // update
		);

		this.Portal = new Category("Portals", null, null, null);

		this.Projectile = new Category
		(
			"Projectile",
			function(bodyThis, bodyOther)
			{
				var bodyOtherCategories = bodyOther.defn.categories;
				if (bodyOtherCategories.indexOf(Category.Instances.Enemy) >= 0)
				{
					bodyOther.integrity = 0;
				}
			},
			null,
			null // update
		);

	}
}

function Collision(bodies)
{
	this.bodies = bodies;
}

function CollisionHelper()
{}
{
	var prototype = CollisionHelper.prototype;

	prototype.collideBodies = function(collision, body0, body1)
	{
		var body0DefnCategories = body0.defn.categories;

		for (var c = 0; c < body0DefnCategories.length; c++)
		{
			var methodForCollision = body0DefnCategories[c].methodForCollision;
			if (methodForCollision != null)
			{
				methodForCollision(body0, body1);
			}
		}
	}

	prototype.doBodiesCollide = function(body0, body1)
	{
		var returnValue = false;

		var bodies = new Array(body0, body1);

		var body0Pos = body0.loc.pos;
		var body1Pos = body1.loc.pos;

		var minMaxesForBodies = new Array
		(
			new Array(body0Pos, body0Pos.clone().add(body0.defn.sizeInPixels)), 
			new Array(body1Pos, body1Pos.clone().add(body1.defn.sizeInPixels))
		);

		for (var b = 0; b < bodies.length; b++)
		{
			var bodyThis = bodies[b];
			var bodyOther = bodies[1 - b];

			var minMaxForBodyThis = minMaxesForBodies[b];
			var minMaxForBodyOther = minMaxesForBodies[1 - b];

			var minForBodyOther = minMaxForBodyOther[0];
			var maxForBodyOther = minMaxForBodyOther[1];

			for (var m = 0; m < 2; m++)
			{
				var extentForBodyThis = minMaxForBodyThis[m];

				var doAllDimensionsOverlapSoFar = true;

				for (var d = 0; d < Coords.NumberOfDimensions; d++)
				{
					var dimensionThis = extentForBodyThis.dimension(d);

					if 
					(
						dimensionThis < minForBodyOther.dimension(d)
						|| dimensionThis > maxForBodyOther.dimension(d)
					)
					{
						doAllDimensionsOverlapSoFar = false;
						break;	
					}
				}	

				if (doAllDimensionsOverlapSoFar == true)
				{
					returnValue = true;
					break;
				}	
			}
		}

		return returnValue;
	}

	prototype.findCollisionsBetweenBodiesInSets = function(bodySet0, bodySet1)
	{
		var returnValues = new Array();

		var numberOfBodiesInSet0 = bodySet0.length;
		var numberOfBodiesInSet1 = bodySet1.length;

		for (var i = 0; i < numberOfBodiesInSet0; i++)
		{
			var bodyFromSet0 = bodySet0[i];

			for (var j = 0; j < numberOfBodiesInSet1; j++)
			{
				var bodyFromSet1 = bodySet1[j];

				if (this.doBodiesCollide(bodyFromSet0, bodyFromSet1) == true)
				{
					var collision = new Collision
					(
						[bodyFromSet0, bodyFromSet1]
					);
					returnValues.push(collision);
				}				
			}
		}

		return returnValues;
	}
}

function Color(name, symbol, systemColor)
{
	this.name = name;
	this.symbol = symbol;
	this.systemColor = systemColor;
}
{
	// static methods

	Color.getBySymbol = function(symbolToGet)
	{
		var returnValue = Color.Instances._All[symbolToGet];
		return returnValue;
	}

	// instances

	Color.Instances = new Color_Instances(); // must defer this?

	function Color_Instances()
	{
		// Not sure why rgba is necessary, but it is.
		this.Transparent = new Color("Transparent", ".", "rgba(0, 0, 0, 0)");

		this.Black 	= new Color("Black", 	"k", "#000000");
		this.Blue 	= new Color("Blue", 	"b", "#0000ff");
		this.BlueDark 	= new Color("BlueDark", "B", "#000080");
		this.Brown	= new Color("Brown", 	"n", "#804000");
		this.Gray 	= new Color("Gray", 	"a", "#808080");
		this.GrayDark 	= new Color("GrayDark", "A", "#404040");
		this.Green 	= new Color("Green", 	"g", "#00ff00");
		this.GreenDark 	= new Color("GreenDark","G", "#008000");
		this.Orange 	= new Color("Orange", 	"o", "#ff8000");
		this.Purple 	= new Color("Purple", 	"p", "#ff00ff");
		this.Red 	= new Color("Red", 	"r", "#ff0000");
		this.White 	= new Color("White", 	"w", "#ffffff");
		this.Yellow 	= new Color("Yellow", 	"y", "#ffff00");

		this._All = new Array
		(
			this.Transparent,

			this.Black,
			this.Blue,
			this.BlueDark,
			this.Brown,
			this.Gray,
			this.GrayDark,
			this.Green,
			this.GreenDark,
			this.Orange,
			this.Purple,
			this.Red,
			this.White,
			this.Yellow	
		);

		for (var i = 0; i < this._All.length; i++)
		{
			var color = this._All[i];
			this._All[color.symbol] = color;
		}
	}	
}

function Constraint(defn, bodyConstrained)
{
	this.defn = defn;
	this.bodyConstrained = bodyConstrained;
}

function ConstraintDefn(name, methodToRun)
{
	this.name = name;
	this.methodToRun = methodToRun;
}
{
	ConstraintDefn.Instances = new ConstraintDefn_Instances();

	function ConstraintDefn_Instances()
	{
		this.follow = new ConstraintDefn
		(
			"Follow",
			function (constraint)
			{
				var bodyConstrained = constraint.bodyConstrained;

				// hack
				var bodyToFollow = bodyConstrained.loc.venue.bodiesByCategory
				[
					Category.Instances.Player.name
				][0];

				bodyConstrained.loc.pos.overwriteWith
				(
					bodyToFollow.loc.pos
				);
			}
		)
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	// constants

	Coords.NumberOfDimensions = 2;

	// instances

	Coords.Instances = new Coords_Instances();

	function Coords_Instances()
	{
		this.Ones = new Coords(1, 1);
		this.Twos = new Coords(2, 2);
		this.Zeroes = new Coords(0, 0);
	}

	// instance methods

	Coords.prototype.absolute = function()
	{
		this.x = Math.abs(this.x);
		this.y = Math.abs(this.y);

		return this;
	}

	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;

		return this;
	}

	Coords.prototype.clear = function()
	{
		this.x = 0;
		this.y = 0;

		return this;
	}

	Coords.prototype.clone = function()
	{
		var returnValue = new Coords(this.x, this.y);

		return returnValue;
	}

	Coords.prototype.dimension = function(dimensionIndex)
	{
		var returnValue;

		if (dimensionIndex == 0)
		{
			returnValue = this.x;
		}
		else
		{
			returnValue = this.y;
		}

		return returnValue;
	}

	Coords.prototype.directions = function()
	{
		if (this.x > 0)
		{
			this.x = 1;
		}
		else if (this.x < 0)
		{
			this.x = -1;
		}

		if (this.y > 0)
		{
			this.y = 1;
		}
		else if (this.y < 0)
		{
			this.y = -1;
		}

		return this;
	}

	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.equals = function(other)
	{
		return (this.x == other.x && this.y == other.y);
	}

	Coords.prototype.floor = function()
	{
		this.x = Math.floor(this.x);
		this.y = Math.floor(this.y);

		return this;
	}

	Coords.prototype.magnitude = function()
	{
		var returnValue = Math.sqrt(this.x * this.x + this.y * this.y);

		return returnValue;
	}

	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.normalize = function()
	{
		var returnValue;

		var magnitude = this.magnitude();
		if (magnitude == 0)
		{
			returnValue = this;
		}
		{
			returnValue = this.divideScalar(magnitude);
		}

		return returnValue;
	}

	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;

		return this;
	}

	Coords.prototype.overwriteWithDimensions = 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.sumOfXAndY = function()
	{
		return this.x + this.y;
	}

	Coords.prototype.toString = function()
	{
		return "<Coords x='" + this.x + "' y='" + this.y + "' />"
	}

	Coords.prototype.trimToRange = function(rangeToTrimTo)
	{
		if (this.x < 0)
		{
			this.x = 0;
		}
		else if (this.x > rangeToTrimTo.x)
		{
			this.x = rangeToTrimTo.x;
		}

		if (this.y < 0)
		{
			this.y = 0;
		}
		else if (this.y > rangeToTrimTo.y)
		{
			this.y = rangeToTrimTo.y;
		}

		return this;
	}
}

function Globals()
{
	this.initialize = function
	(
		realWorldMillisecondsPerTick, 
		viewSizeInPixels, 
		universe
	)	
	{
		this.collisionHelper = new CollisionHelper();

		this.inputHelper = new InputHelper();
		this.inputHelper.initialize();

		this.realWorldMillisecondsPerTick = realWorldMillisecondsPerTick;
		this.viewSizeInPixels = viewSizeInPixels;
		this.viewSizeInPixelsHalf = this.viewSizeInPixels.clone().divideScalar(2);

		this.universe = universe;

		this.venue = this.universe.venues[0];
		this.venue.initialize();
		this.venue.htmlElementBuild();
		document.body.appendChild(this.venue.htmlElement);

		this.inputHelper.bindingsRegister(this.venue.defn.inputBindings);

		setInterval("Globals.Instance.processTick()", this.realWorldMillisecondsPerTick);
	}

	this.processTick = function()
	{
		this.venue.update();
	}
}
{
	Globals.Instance = new Globals();
}

function Heading()
{}
{
	Heading.fromCoords = function(coordsToConvert)
	{
		// todo
		var returnValue;

		if (coordsToConvert.x == 0)
		{
			if (coordsToConvert.y > 0)
			{
				returnValue = 1;
			}
			else
			{
				returnValue = 3;
			}
		}
		else if (coordsToConvert.x > 0)
		{
			returnValue = 0;
		}
		else if (coordsToConvert.x < 0)
		{
			returnValue = 2;
		}

		return returnValue;
	}
}

function Image()
{}
{
	// static methods

	Image.buildFromFilePath = function(filePath)
	{
		var returnValue = new Image();

		returnValue.filePath = filePath;

		returnValue.htmlElement = document.createElement("img");
		returnValue.htmlElement.src = filePath;

		return returnValue;
	}

	Image.buildFromSystemImage = function(systemImage)
	{
		var returnValue = new Image();

		returnValue.htmlElement = systemImage;
		returnValue.filePath = systemImage.src;

		return returnValue;
	}

	// instance methods

	Image.prototype.clone = function()
	{
		var returnValue = new Image();

		returnValue.filePath = this.filePath;
		returnValue.htmlElement = document.createElement("img");
		returnValue.htmlElement.src = this.htmlElement.src;

		return returnValue;
	}
}

function ImageHelper()
{}
{
	// static methods

	ImageHelper.buildImageFromStrings = function(stringsForPixels)
	{
		return ImageHelper.buildImageFromStringsScaled
		(
			Coords.Instances.Ones, stringsForPixels
		);
	}

	ImageHelper.buildImageFromStringsScaled = function(scaleFactor, stringsForPixels)
	{
		var sizeInPixels = new Coords(stringsForPixels[0].length, stringsForPixels.length);

		var canvas = document.createElement("canvas");
		canvas.width = sizeInPixels.x * scaleFactor.x;
		canvas.height = sizeInPixels.y * scaleFactor.y;

		var graphics = canvas.getContext("2d");

		var pixelPos = new Coords(0, 0);
		var colorForPixel = Color.Instances.Transparent;

		for (var y = 0; y < sizeInPixels.y; y++)
		{
			var stringForPixelRow = stringsForPixels[y];
			pixelPos.y = y * scaleFactor.y;

			for (var x = 0; x < sizeInPixels.x; x++)
			{
				var charForPixel = stringForPixelRow[x];
				pixelPos.x = x * scaleFactor.x;

				colorForPixel = Color.getBySymbol(charForPixel);

				graphics.fillStyle = colorForPixel.systemColor;
				graphics.fillRect
				(
					pixelPos.x, 
					pixelPos.y, 
					scaleFactor.x, 
					scaleFactor.y
				);				
			}
		}

		var imageFromCanvasURL = canvas.toDataURL("image/png");
		var htmlImageFromCanvas = document.createElement("img");
		htmlImageFromCanvas.width = canvas.width;
		htmlImageFromCanvas.height = canvas.height;
		htmlImageFromCanvas.src = imageFromCanvasURL;

		var returnValue = Image.buildFromSystemImage(htmlImageFromCanvas);

		return returnValue;
	}

	ImageHelper.buildImageFromStringsTiled = function(sizeInTiles, stringsForPixels)
	{
		var tileSizeInPixels = new Coords(stringsForPixels[0].length, stringsForPixels.length);

		var canvas = document.createElement("canvas");
		canvas.width = tileSizeInPixels.x * sizeInTiles.x;
		canvas.height = tileSizeInPixels.y * sizeInTiles.y;

		var graphics = canvas.getContext("2d");

		var pixelPos = new Coords(0, 0);
		var colorForPixel = Color.Instances.Transparent;

		for (var j = 0; j < sizeInTiles.y; j++)
		{
			for (var i = 0; i < sizeInTiles.x; i++)
			{
				for (var y = 0; y < tileSizeInPixels.y; y++)
				{
					var stringForPixelRow = stringsForPixels[y];
					pixelPos.y = j * tileSizeInPixels.y + y;

					for (var x = 0; x < tileSizeInPixels.x; x++)
					{
						var charForPixel = stringForPixelRow[x];
						pixelPos.x = i * tileSizeInPixels.x + x;

						colorForPixel = Color.getBySymbol(charForPixel);

						graphics.fillStyle = colorForPixel.systemColor;
						graphics.fillRect
						(
							pixelPos.x, 
							pixelPos.y, 
							1, 
							1
						);
					}
				}
			}
		}

		var imageFromCanvasURL = canvas.toDataURL("image/png");
		var htmlImageFromCanvas = document.createElement("img");
		htmlImageFromCanvas.width = canvas.width;
		htmlImageFromCanvas.height = canvas.height;
		htmlImageFromCanvas.src = imageFromCanvasURL;

		var returnValue = Image.buildFromSystemImage(htmlImageFromCanvas);

		return returnValue;
	}

	ImageHelper.copyRegionFromImage = function(imageToCopyFrom, regionPos, regionSize)
	{
		var canvas		 = document.createElement("canvas");
		canvas.id		 = "region_" + regionPos.x + "_" + regionPos.y;
		canvas.width		 = regionSize.x;
		canvas.height		 = regionSize.y;
		canvas.style.position	 = "absolute";

		var graphics = canvas.getContext("2d");

		graphics.drawImage
		(
			imageToCopyFrom.htmlElement,
			regionPos.x, regionPos.y, // source pos
			regionSize.x, regionSize.y, // source size
			0, 0, // destination pos
			regionSize.x, regionSize.y // destination size
		);

		var imageFromCanvasURL = canvas.toDataURL("image/png");

		var htmlImageFromCanvas = document.createElement("img");
		htmlImageFromCanvas.width = canvas.width;
		htmlImageFromCanvas.height = canvas.height;
		htmlImageFromCanvas.style.position = "absolute";
		htmlImageFromCanvas.src = imageFromCanvasURL;

		var returnValue = Image.buildFromSystemImage(htmlImageFromCanvas);

		return returnValue;
	}

	ImageHelper.sliceImageIntoTiles = function(imageToSlice, sizeInTiles)
	{
		var returnImages = new Array();

		var systemImageToSlice = imageToSlice.htmlElement;		

		var imageToSliceSize = new Coords
		(
			systemImageToSlice.width, 
			systemImageToSlice.height
		);
		var tileSize = imageToSliceSize.clone().divide(sizeInTiles);

		var tilePos = new Coords(0, 0);
		var sourcePos = new Coords(0, 0);

		for (var y = 0; y < sizeInTiles.y; y++)
		{
			tilePos.y = y;

			var returnImageRow = new Array();

			for (var x = 0; x < sizeInTiles.x; x++)
			{							
				tilePos.x = x;

				var canvas		 = document.createElement("canvas");
				canvas.id		 = "tile_" + x + "_" + y;
				canvas.width		 = tileSize.x;
				canvas.height		 = tileSize.y;
				canvas.style.position	 = "absolute";

				var graphics = canvas.getContext("2d");

				sourcePos.overwriteWith(tilePos).multiply(tileSize);

				graphics.drawImage
				(
					systemImageToSlice,
					sourcePos.x, sourcePos.y, // source pos
					tileSize.x, tileSize.y, // source size
					0, 0, // destination pos
					tileSize.x, tileSize.y // destination size
				);

				// browser dependent?
				var imageFromCanvasURL = canvas.toDataURL("image/png");

				var htmlImageFromCanvas = document.createElement("img");
				htmlImageFromCanvas.width = canvas.width;
				htmlImageFromCanvas.height = canvas.height;
				htmlImageFromCanvas.style.position = "absolute";
				htmlImageFromCanvas.src = imageFromCanvasURL;

				imageFromCanvas = Image.buildFromSystemImage
				(
					htmlImageFromCanvas
				);

				returnImageRow.push(imageFromCanvas);
			}

			returnImages.push(returnImageRow);
		}

		return returnImages;
	}
}

function ImageLoader(imageDatasToLoad, contextForCallback, callbackForLoadingComplete)
{
	this.name = "ImageLoader";
	this.imageDatasToLoad = imageDatasToLoad;
	this.contextForCallback = contextForCallback;	
	this.callbackForLoadingComplete = callbackForLoadingComplete;
	this.numberOfImagesLoadedSoFar = 0;

	this.imageLookup = new Array();

	for (var i = 0; i < this.imageDatasToLoad.length; i++)
	{
		var imageData = this.imageDatasToLoad[i];

		var image = document.createElement("img");
		image.imageLoader = this;
		image.onload = this.imageLoaded;

		image.id = imageData.id;
		image.src = imageData.url;

		this.imageLookup[image.id] = Image.buildFromSystemImage(image);
	}
}
{
	var prototype = ImageLoader.prototype;

	prototype.imageLoaded = function(event)
	{
		var imageLoader = event.target.imageLoader;
		imageLoader.numberOfImagesLoadedSoFar++;

		if (imageLoader.numberOfImagesLoadedSoFar >= imageLoader.imageDatasToLoad.length)
		{
			imageLoader.callbackForLoadingComplete.call
			(
				imageLoader.contextForCallback
			);
		}
	}
}

function ImageLoadingData(id, url)
{
	this.id = id;
	this.url = url;
}

function InputBinding(key, action)
{
	this.key = key;
	this.action = action;	
}

function InputHelper()
{
	this.mousePos = new Coords(0, 0);

	this.bindings = new Array();
	this.keyCodeToBindingLookup = new Array();

	this.actionsBeingPerformed = new Array();
	this.actionsBeingPerformedLookup = new Array();
}
{
	// static methods

	InputHelper.ProcessKeyDownEvent = function(event)
	{
		var inputHelper = Globals.Instance.inputHelper;

		var binding = inputHelper.keyCodeToBindingLookup["_" + event.which];
		if (binding != null)
		{
			var action = binding.action;

			var actionsBeingPerformedLookup = inputHelper.actionsBeingPerformedLookup;
			if (actionsBeingPerformedLookup[action.name] == null)
			{
				var actionsBeingPerformed = inputHelper.actionsBeingPerformed;

				actionsBeingPerformedLookup[action.name] = action;
				actionsBeingPerformed.push(action);
			}
		}
	}

	InputHelper.ProcessKeyUpEvent = function(event)
	{
		var inputHelper = Globals.Instance.inputHelper;

		var binding = inputHelper.keyCodeToBindingLookup["_" + event.which];

		if (binding != null)
		{
			var action = binding.action;

			var actionsBeingPerformedLookup = inputHelper.actionsBeingPerformedLookup;
			if (actionsBeingPerformedLookup[action.name] != null)
			{
				delete actionsBeingPerformedLookup[action.name];

				var actionsBeingPerformed = inputHelper.actionsBeingPerformed;
				var indexToDeleteAt = actionsBeingPerformed.indexOf(action);
				actionsBeingPerformed.splice(indexToDeleteAt, 1);
			}
		}
	}

	InputHelper.ProcessMouseDownEvent = function(event)
	{
		var inputHelper = Globals.Instance.inputHelper;

		inputHelper.mousePos.overwriteWithDimensions(event.x, event.y);
	}

	// instance methods

	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = InputHelper.ProcessKeyDownEvent;
		document.body.onkeyup = InputHelper.ProcessKeyUpEvent;
		document.body.onmousedown = InputHelper.ProcessMouseDownEvent;
	}

	InputHelper.prototype.bindingsRegister = function(bindingsToRegister)
	{
		this.bindings = new Array();
		this.keyCodeToBindingLookup = new Array();

		var numberOfBindings = bindingsToRegister.length;
		for (var b = 0; b < numberOfBindings; b++)
		{
			var binding = bindingsToRegister[b];

			this.bindings.push(binding);
			this.keyCodeToBindingLookup[binding.key.systemKeyCode] = binding;
		}
	}
}

function InputKey(name, systemKeyCode)
{
	this.name = name;
	this.systemKeyCode = systemKeyCode;
}
{
	InputKey.Instances = new InputKey_Instances();

	function InputKey_Instances()
	{
		this.A = new InputKey("A", "_65");
		this.D = new InputKey("D", "_68");
		this.F = new InputKey("F", "_70");
		this.S = new InputKey("S", "_83");
		this.W = new InputKey("W", "_87");

		this._All = 
		[
			this.A, 
			this.D, 
			this.F,
			this.S, 
			this.W,
		];

		this.systemKeyCodeToKeyLookup = new Array();
		for (var i = 0; i < this._All.length; i++)
		{
			var key = this._All[i];
			this.systemKeyCodeToKeyLookup[key.systemKeyCode] = key;
		}
	}
}

function Location(venue, pos)
{
	this.venue = venue;
	this.pos = pos;

	this.vel = new Coords(0, 0);
	this.accel = new Coords(0, 0);
	this.force = new Coords(0, 0);
}

function Map(name, cellSizeInPixels, cellsAsStrings)
{
	this.cellsAsStrings = cellsAsStrings;	
	this.cellSizeInPixels = cellSizeInPixels;
	this.sizeInCells = new Coords
	(
		this.cellsAsStrings[0].length, 
		this.cellsAsStrings.length
	);
}
{
	Map.prototype.cellAtPos = function(cellPos)
	{
		var codeChar = this.cellsAsStrings[cellPos.y][cellPos.x];

		return new MapCell
		(
			MapTerrain.Instances._All[codeChar]
		);
	}

	Map.prototype.htmlElementBuild = function()
	{
		var returnValue = document.createElement("table");
		returnValue.style.backgroundColor = "#888888";
		returnValue.cellPadding = 0;
		returnValue.border = 0;
		returnValue.cellSpacing = 0;

		for (var y = 0; y < this.cellsAsStrings.length; y++)
		{
			var trForRow = document.createElement("tr");

			var cellRowAsString = this.cellsAsStrings[y];

			for (var x = 0; x < cellRowAsString.length; x++)
			{
				var cellAsChar = cellRowAsString[x];

				tdForCell = document.createElement("td");

				tdForCell.style.width = this.cellSizeInPixels.x;
				tdForCell.style.height = this.cellSizeInPixels.y; 

				var imgForCell = document.createElement("img");
				imgForCell.src = MapTerrain.Instances._All[cellAsChar].image.htmlElement.src;
				tdForCell.appendChild(imgForCell);

				trForRow.appendChild(tdForCell);
			}

			returnValue.appendChild(trForRow);
		}

		this.htmlElement = returnValue;

		return returnValue;
	}
}

function MapCell(terrain)
{
	this.terrain = terrain;
}

function MapTerrain(name, codeChar, costToTraverse, color, image)
{
	this.name = name;
	this.codeChar = codeChar;
	this.costToTraverse = costToTraverse;
	this.color = color;
	this.image = image;
}
{
	// instances

	MapTerrain.Instances = new MapTerrain_Instances();

	function MapTerrain_Instances()
	{
		var imageTerrainBlocked = ImageHelper.buildImageFromStringsTiled
		(
			new Coords(30, 30),
			[
				"A",
			]
		);

		var imageTerrainOpen = ImageHelper.buildImageFromStringsTiled
		(
			new Coords(30, 30),
			[
				"a",
			]
		);

		this.Blocked = new MapTerrain("Blocked", "x", 1000000, 	"#0000aa", imageTerrainBlocked);
		this.Open = new MapTerrain("Open", ".", 1, 		"#00aa00", imageTerrainOpen);

		this._All = 
		[
			this.Blocked,
			this.Open,
		];

		for (var i = 0; i < this._All.length; i++)
		{
			var terrain = this._All[i];
			this._All[terrain.codeChar] = terrain;
		}

	}
}

function Path(map, startPos, goalPos)
{
	this.map = map;
	this.startPos = startPos;
	this.goalPos = goalPos;

	var openList = new Array();
	var openLookup = new Array();
	var closedLookup = new Array();

	var startNode = new PathNode
	(
		startPos,
		0,
		goalPos.clone().subtract(startPos).absolute().sumOfXAndY(),
		null
	);

	openList.push(startNode);
	var startIndex = "_" + (startNode.cellPos.y * map.sizeInCells.x + startNode.cellPos.x);
	openLookup[startIndex] = startNode;

	while (openList.length > 0)
	{
		var current = openList[0];

		if (current.cellPos.equals(goalPos) == true)
		{	
			this.nodes = new Array();

			while (current != null)
			{
				this.nodes.splice(0, 0, current);
				current = current.prev;
			}
			break;
		}

		openList.splice(0, 1);
		var currentIndex = "_" + (current.cellPos.y * map.sizeInCells.x + current.cellPos.x);
		delete openLookup[currentIndex];

		closedLookup[currentIndex] = current;

		var neighbors = this.getNeighborsForNode(map, current, goalPos);

		for (var n = 0; n < neighbors.length; n++)
		{
			var neighbor = neighbors[n];
			var neighborPos = neighbor.cellPos;

			var neighborIndex = "_" + (neighborPos.y * map.sizeInCells.x + neighborPos.x);

			if (closedLookup[neighborIndex] == null && openLookup[neighborIndex] == null)
			{
				var i;
				for (i = 0; i < openList.length; i++)
				{
					var nodeFromOpenList = openList[i];
					if (neighbor.costFromStart < nodeFromOpenList.costFromStart)
					{
						break;
					}
				}

				openList.splice(i, 0, neighbor);
				openLookup[neighborIndex] = neighbor;
			}
		}
	}	
}
{
	var prototype = Path.prototype;

	prototype.convertToHTMLElement = function()
	{
		var returnValue = document.createElement("div");
		returnValue.cellPadding = 0;
		returnValue.border = 0;
		returnValue.cellSpacing = 0;

		var cellSizeInPixels = this.map.cellSizeInPixels;

		for (var i = 0; i < this.nodes.length; i++)
		{
			var node = this.nodes[i];
			var nodeCellPos = node.cellPos;

			var divForNode = document.createElement("div");
			divForNode.style.position = "absolute";
			divForNode.style.width = cellSizeInPixels.x + "px";
			divForNode.style.height = cellSizeInPixels.y + "px";
			divForNode.style.left = (nodeCellPos.x * cellSizeInPixels.x) + "px";
			divForNode.style.top = (nodeCellPos.y * cellSizeInPixels.y) + "px";
			divForNode.style.backgroundColor = "#ff0000";	

			returnValue.appendChild(divForNode);
		}

		return returnValue;
	}

	prototype.getNeighborsForNode = function(map, node, goalPos)
	{
		var returnValues = new Array();
		var cellPos = node.cellPos;

		var neighborPositions = new Array();

		if (cellPos.x > 0)
		{
			neighborPositions.push
			(
				new Coords(cellPos.x - 1, cellPos.y)
			);

		}
		if (cellPos.x < map.sizeInCells.x - 1)
		{
			neighborPositions.push
			(
				new Coords(cellPos.x + 1, cellPos.y)
			);
		}		
		if (cellPos.y > 0)
		{
			neighborPositions.push
			(
				new Coords(cellPos.x, cellPos.y - 1)
			);
		}
		if (cellPos.y < map.sizeInCells.y - 1)
		{
			neighborPositions.push
			(
				new Coords(cellPos.x, cellPos.y + 1)
			);
		}		

		var tempPos = new Coords(0, 0);

		for (var i = 0; i < neighborPositions.length; i++)
		{
			var neighborPos = neighborPositions[i];

			var costToTraverse = map.cellAtPos(neighborPos).terrain.costToTraverse;

			var neighborNode = new PathNode
			(
				neighborPos,
				node.costFromStart + costToTraverse,
				costToTraverse + goalPos.clone().subtract(cellPos).absolute().sumOfXAndY(),
				node
			);

			returnValues.push(neighborNode);
		}

		return returnValues;
	}
}

function PathNode(cellPos, costFromStart, costToGoalEstimated, prev)
{
	this.cellPos = cellPos;
	this.costFromStart = costFromStart;
	this.costToGoalEstimated = costToGoalEstimated;
	this.prev = prev;
}

function Universe(name, venueDefns, venues)
{
	this.name = name;
	this.venueDefns = venueDefns;
	this.venues = venues;
}

function Venue(name, defn, sizeInPixels, map, bodies)
{
	this.name = name;
	this.defn = defn;
	this.sizeInPixels = sizeInPixels;
	this.map = map;
	this.bodies = new Array();

	this.sizeInPixelsHalf = this.sizeInPixels.clone().divideScalar(2);

	this.bodiesByCategory = new Array();
	this.bodiesToSpawn = new Array();
	this.bodiesToRemove = new Array();

	for (var i = 0; i < bodies.length; i++)
	{
		this.bodiesToSpawn.push(bodies[i]);
	}

	for (var i = 0; i < this.defn.bodyDefns.length; i++)
	{
		var categories = this.defn.bodyDefns[i].categories;

		for (var c = 0; c < categories.length; c++)
		{
			var category = categories[c];

			if (this.bodiesByCategory[category.name] == null)
			{
				this.bodiesByCategory[category.name] = new Array();
			}
		}
	}

	this.camera = new Camera
	(
		"Camera",
		Camera.ViewSizeStandard
	);

	this.camera.body.loc.venue = this;

	this.bodiesToSpawn.push(this.camera.body);
}
{
	// instance methods

	Venue.prototype.htmlElementBuild = function()
	{
		var returnValue = document.createElement("div");

		returnValue.style.backgroundColor = "#000000";
		returnValue.style.width = this.sizeInPixels.x + "px";
		returnValue.style.height = this.sizeInPixels.y + "px";

		var mapAsHTMLElement = this.map.htmlElementBuild();

		returnValue.appendChild(mapAsHTMLElement);

		this.htmlElement = returnValue;

		return returnValue;
	}

	Venue.prototype.initialize = function()
	{
		// todo
	}

	Venue.prototype.update = function()
	{
		this.update_BodiesToSpawn();

		var categories = 
		[
			Category.Instances.Actor,
			Category.Instances.Mover,
			Category.Instances.Drawable,
			Category.Instances.Killable,
			Category.Instances.Ephemeral,
		];

		var numberOfCategories = categories.length;

		for (var c = 0; c < numberOfCategories; c++)
		{
			var category = categories[c];

			var bodiesInCategory = this.bodiesByCategory[category.name];

			var numberOfBodiesInCategory = bodiesInCategory.length;

			for (var b = 0; b < numberOfBodiesInCategory; b++)
			{
				var body = bodiesInCategory[b];

				if (category.updateBodyForVenue != null)
				{
					category.updateBodyForVenue(body, this);
				}
			}
		}

		this.update_Collidables();

		this.update_BodiesToRemove();
	}

	Venue.prototype.update_BodiesToRemove = function()
	{
		for (var i = 0; i < this.bodiesToRemove.length; i++)
		{
			var bodyToRemove = this.bodiesToRemove[i];

			this.bodies.splice(this.bodies.indexOf(bodyToRemove), 1);

			var bodyCategories = bodyToRemove.defn.categories;
			for (var c = 0; c < bodyCategories.length; c++)
			{
				var bodyCategory = bodyCategories[c];
				var bodyCategoryName = bodyCategory.name;
				var bodiesForCategory = this.bodiesByCategory[bodyCategoryName];
				bodiesForCategory.splice(bodiesForCategory.indexOf(bodyToRemove), 1);
			}

			this.htmlElement.removeChild(bodyToRemove.htmlElement);
		}

		this.bodiesToRemove.length = 0;
	}

	Venue.prototype.update_BodiesToSpawn = function()
	{
		for (var i = 0; i < this.bodiesToSpawn.length; i++)
		{
			var bodyToSpawn = this.bodiesToSpawn[i];

			this.bodies.push(bodyToSpawn);		

			bodyToSpawn.loc.venue = this;

			var bodyCategories = bodyToSpawn.defn.categories;
			for (var c = 0; c < bodyCategories.length; c++)
			{
				var bodyCategory = bodyCategories[c];
				var bodyCategoryName = bodyCategory.name;
				this.bodiesByCategory[bodyCategoryName].push(bodyToSpawn);

				if (bodyCategory.initializeBodyForVenue != null)
				{
					bodyCategory.initializeBodyForVenue(bodyToSpawn, this);
				}
			}
		}

		this.bodiesToSpawn.length = 0;
	}

	Venue.prototype.update_Collidables = function()
	{
		var enemies = this.bodiesByCategory[Category.Instances.Enemy.name];
		var items = this.bodiesByCategory[Category.Instances.Item.name];
		var players = this.bodiesByCategory[Category.Instances.Player.name]
		var portals = this.bodiesByCategory[Category.Instances.Portal.name];
		var projectiles = this.bodiesByCategory[Category.Instances.Projectile.name]

		var collisionHelper = Globals.Instance.collisionHelper;

		var collisionSets = 
		[
			collisionHelper.findCollisionsBetweenBodiesInSets
			(
				players, 
				enemies
			),

			collisionHelper.findCollisionsBetweenBodiesInSets
			(
				players, 
				items
			),

			collisionHelper.findCollisionsBetweenBodiesInSets
			(
				players, 
				portals
			),

			collisionHelper.findCollisionsBetweenBodiesInSets
			(
				enemies,
				projectiles
			),
		];

		for (var s = 0; s < collisionSets.length; s++)
		{
			var collisions = collisionSets[s];

			var numberOfCollisions = collisions.length;
			for (var c = 0; c < numberOfCollisions; c++)
			{
				var collision = collisions[c];

				var numberOfBodies = collision.bodies.length;

				for (var b0 = 0; b0 < numberOfBodies; b0++)
				{
					var bodyThis = collision.bodies[b0];
					for (var b1 = b0 + 1; b1 < numberOfBodies; b1++)
					{
						var bodyOther = collision.bodies[b1];

						collisionHelper.collideBodies
						(
							collision, bodyThis, bodyOther
						);
						collisionHelper.collideBodies
						(
							collision, bodyOther, bodyThis
						);
					}
				}
			}
		}
	}
}

function VenueDefn(name, bodyDefns, inputBindings)
{
	this.name = name;
	this.bodyDefns = bodyDefns;
	this.inputBindings = inputBindings;

	for (var i = 0; i < this.bodyDefns.length; i++)
	{
		var bodyDefn = this.bodyDefns[i];
		this.bodyDefns[bodyDefn.name] = bodyDefn;
	}
}

// deferred

//Color.Instances = new Color_Instances();

// run

new Simulation().main();

</script>
</body>
</html>
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

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 )

Google+ photo

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

Connecting to %s