A 4X Space Strategy Game in JavaScript

The JavaScript code below implements a prototype of a 4X strategy game set in space, similar to the old DOS game Ascendancy. It makes use of pieces of functionality from several previous posts.  As usual, it’s nowhere near finished yet, and what code is in place could stand to be cleaned up.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit the URL “https://thiscouldbebetter.neocities.org/spacestrategygame.html“.

UPDATE 2018/01/06 – A much more advanced, albeit still unfinished, version of this code is available at the URL “https://github.com/thiscouldbebetter/SpaceStrategy4XGame“.

SpaceStrategy-StarCluster

SpaceStrategy-SolarSystem

SpaceStrategy-PlanetSurface

UPDATE 2017/12/19 – Some time ago I moved this to Github, at the URL “https://github.com/thiscouldbebetter/SpaceStrategy4XGame“. Some updates have been made to it since then, and future updates will likely be made there as well.


<html>
<body>

<script type="text/javascript">

// main

function main()
{
	//localStorage.clear(); 

	var viewSize = new Coords(400, 300, 1);

	var universe0 = Universe.new(null);

	var applicationName = "Fretwork";

	Globals.prototype.initialize
	(
		applicationName,
		50, // millisecondsPerTimerTick
		viewSize,
		universe0,
		// sounds
		[
			new Sound("Click", "Click.wav"),
			new Sound("Music", "Music.mp3"),
		],
		// videos
		[
			new Video("Intro", "Intro.webm"),
		]
	);
}

// extensions

function ArrayExtensions()
{
	// do nothing
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var item = this[i];
			var key = DataBinding.get(item, keyName);
			this[key] = item;
		}
	}

	Array.prototype.intersectionWith = function(other)
	{
		var returnValues = [];

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

			if (other.indexOf(item) >= 0)
			{
				returnValues.push(item);
			}
		}

		return returnValues;
	}
}

// classes

function Action_CameraMove(displacementAmountsRightAndDown)
{
	this.displacementAmountsRightAndDown = displacementAmountsRightAndDown;
	this.displacement = new Coords(0, 0, 0);
}
{
	Action_CameraMove.prototype.perform = function(camera)
	{
		this.displacement.overwriteWith
		(
			camera.orientation.right
		).multiplyScalar
		(
			this.displacementAmountsRightAndDown[0]
		).add
		(
			camera.orientation.down.clone().multiplyScalar
			(
				this.displacementAmountsRightAndDown[1]
			)
		);
		var cameraPos = camera.loc.pos;
		cameraPos.add(this.displacement);
		var cameraPosAsPolar = Polar.fromCoords(cameraPos);
		cameraPosAsPolar.radius = camera.focalLength;
		cameraPos.overwriteWith(cameraPosAsPolar.toCoords());

		var cameraOrientationForward = cameraPos.clone().multiplyScalar(-1);

		camera.orientation = new Orientation
		(
			cameraOrientationForward,
			camera.orientation.down	
		);
	}
}

function Action_CylinderMove_Yaw(cyclesToMove)
{
	this.cyclesToMove = cyclesToMove;
}
{
	Action_CylinderMove_Yaw.prototype.perform = function(actor)
	{
		var constraintCylinder = actor.constraints["PositionOnCylinder"];

		constraintCylinder.yaw = NumberHelper.wrapValueToRangeMinMax
		(
			constraintCylinder.yaw + this.cyclesToMove,
			0, 
			1
		);
	}
}

function Action_CylinderMove_DistanceAlongAxis(distanceToMove)
{
	this.distanceToMove = distanceToMove;
}
{
	Action_CylinderMove_DistanceAlongAxis.prototype.perform = function(actor)
	{
		var constraintCylinder = actor.constraints["PositionOnCylinder"];

		constraintCylinder.distanceFromCenterAlongAxis += this.distanceToMove;
	}
}

function Action_CylinderMove_Radius(distanceToMove)
{
	this.distanceToMove = distanceToMove;
}
{
	Action_CylinderMove_Radius.prototype.perform = function(actor)
	{
		var constraintCylinder = actor.constraints["PositionOnCylinder"];

		constraintCylinder.radius += this.distanceToMove;
	}
}

function Activity(defnName, variables)
{
	this.defnName = defnName;
	this.variables = variables;
}
{
	Activity.prototype.defn = function()
	{
		return (Globals.Instance.universe.activityDefns[this.defnName]);
	}

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

function ActivityDefn(name, perform)
{
	this.name = name;
	this.perform = perform;
}
{
	function ActivityDefn_Instances()
	{
		this.DoNothing = new ActivityDefn
		(
			"DoNothing",
			function(actor, activity)
			{
				// do nothing
			}
		);	

		this.MoveToTarget = new ActivityDefn
		(
			"MoveToTarget",
			// perform
			function(actor, activity)
			{	
				var variables = activity.variables;
				var target = variables[0];

				var distanceMovedThisStep = variables[1];
				if (distanceMovedThisStep == null)
				{
					distanceMovedThisStep = 0;
				}
				var distanceToMoveThisTick = 3; // hack
				var distancePerStepMax = 100000; // hack
				if (distanceMovedThisStep + distanceToMoveThisTick > distancePerStepMax)
				{
					distanceToMoveThisTick = distancePerStepMax - distanceMovedThisStep;	
				}

				var actorPos = actor.loc.pos;				

				var targetPos = target.loc.pos;
				var displacementFromActorToTarget = targetPos.clone().subtract
				(
					actorPos	
				);
				var distanceFromActorToTarget = displacementFromActorToTarget.magnitude();

				if (distanceFromActorToTarget < distanceToMoveThisTick)
				{
					distanceToMoveThisTick = distanceFromActorToTarget;
					actorPos.overwriteWith(targetPos);

					actor.activity = null; // hack
					actor.order = null; // hack

					Globals.Instance.inputHelper.isEnabled = true;
					
					var targetDefnName = target.defn.name;
					if (targetDefnName == "LinkPortal")
					{
						var universe = Globals.Instance.universe;
						
						var links = universe.world.network.links;

						var venueCurrent = universe.venueCurrent;
						var starsystemFrom = venueCurrent.model;

						var starsystemNamesFromAndTo = target.starsystemNamesFromAndTo;
						var starsystemNameFrom = starsystemNamesFromAndTo[0];
						var starsystemNameTo = starsystemNamesFromAndTo[1];
						
						var link = links[starsystemNameFrom][starsystemNameTo];

						starsystemFrom.ships.splice
						(
							starsystemFrom.ships.indexOf(actor), 1
						);
						venueCurrent.bodies.splice
						(
							venueCurrent.bodies.indexOf(actor), 1
						)
						
						link.ships.push(actor);

						var direction;
						if (link.nodesLinked()[0].starsystem == starsystemFrom)
						{
							direction = 1;	
						}
						else
						{
							direction = -1;
						}
						
						actorPos.overwriteWithDimensions(0, 0, 0);
						var speed = 10; // hack
						actor.vel.overwriteWithDimensions(speed * direction, 0, 0);
					}
					else if (targetDefnName == "Planet")
					{
						alert("todo - planet collision");
					}
				}
				else
				{
					var directionFromActorToTarget = displacementFromActorToTarget.divideScalar
					(
						distanceFromActorToTarget
					);

					actor.vel.overwriteWith
					(
						directionFromActorToTarget
					).multiplyScalar
					(
						distanceToMoveThisTick
					);

					actorPos.add(actor.vel);
				}
				
				distanceMovedThisStep += distanceToMoveThisTick;
				variables[1] = distanceMovedThisStep;
			}
		);

		this._All = 
		[
			this.DoNothing,

			this.MoveToTarget,
		];
	}

	ActivityDefn.Instances = new ActivityDefn_Instances();
}

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

function BodyDefn(name, size, visual)
{
	this.name = name;
	this.size = size;
	this.visual = visual;

	this.sizeHalf = this.size.clone().divideScalar(2);
}

function Bounds(min, max)
{
	this.min = min;
	this.max = max;
}

function BuildableDefn(name, systemColor, industryToBuild)
{
	this.name = name;
	this.systemColor = systemColor;
	this.industryToBuild = industryToBuild;	
}

function Camera(viewSize, focalLength, pos, orientation)
{
	this.viewSize = viewSize;
	this.focalLength = focalLength;
	this.loc = new Location(pos);
	this.orientation = orientation;

	this.viewSizeHalf = this.viewSize.clone().divideScalar(2);
}
{
	Camera.prototype.convertWorldCoordsToViewCoords = function(coordsToConvert)
	{
		coordsToConvert.subtract
		(
			this.loc.pos
		);

		coordsToConvert.overwriteWithDimensions
		(
			this.orientation.right.dotProduct(coordsToConvert),
			this.orientation.down.dotProduct(coordsToConvert),
			this.orientation.forward.dotProduct(coordsToConvert)
		);

		var distanceForwardInFocalLengths = coordsToConvert.z / this.focalLength;

		coordsToConvert.x /= distanceForwardInFocalLengths;
		coordsToConvert.y /= distanceForwardInFocalLengths;

		coordsToConvert.x += this.viewSize.x / 2;
		coordsToConvert.y += this.viewSize.y / 2;

		return coordsToConvert;
	}

	Camera.prototype.rayToViewPos = function(posToProjectRayTo)
	{
		var orientation = this.orientation;	
		var returnValue = new Ray
		(
			this.loc.pos,
			orientation.forward.clone().multiplyScalar
			(
				this.focalLength
			).add
			(
				orientation.right.clone().multiplyScalar
				(
					posToProjectRayTo.x
				)
			).add
			(
				orientation.down.clone().multiplyScalar
				(
					posToProjectRayTo.y
				)
			)	
		);	

		return returnValue;
	}
}

function Collision()
{	
	this.pos = new Coords(0, 0, 0);
	this.distanceToCollision = null;
	this.colliders = [];
}
{
	// static methods

	Collision.rayAndBodies = function(ray, bodies, bodyRadius, listToAddTo)
	{
		for (var i = 0; i < bodies.length; i++)
		{
			var body = bodies[i];

			var collisionOfRayWithBody = new Collision().rayAndSphere
			(
				ray,
				new Sphere
				(
					bodyRadius,
					body.loc.pos
				)
			);

			if (collisionOfRayWithBody.distanceToCollision != null)
			{
				collisionOfRayWithBody.colliders.push
				(
					body
				);

				listToAddTo.push
				(
					collisionOfRayWithBody
				);
			}
		}

		return listToAddTo;
	}

	// instance methods

	Collision.prototype.rayAndFace = function(ray, face)
	{
		this.rayAndPlane
		(
			ray,
			face.plane
		);

		if (this.colliders["Plane"] != null)
		{
			if (this.isPosWithinFace(face) == false)
			{
				this.colliders["Face"] = null;
			}
			else
			{
				this.colliders["Face"] = face;
	
				var displacementFromVertex0ToCollision = new Coords(0, 0, 0);

				for (var t = 0; t < face.triangles.length; t++)
				{
					var triangle = face.triangles[t];
					if (this.isPosWithinFace(triangle) == true)
					{
						this.colliders["Triangle"] = triangle;
						break;
					}
				}
			}
		}

		return this;
	}

	Collision.prototype.rayAndPlane = function(ray, plane)
	{
		this.distanceToCollision = 
			(
				plane.distanceFromOrigin 
				- plane.normal.dotProduct(ray.startPos)
			)
			/ plane.normal.dotProduct(ray.direction);

		if (this.distanceToCollision >= 0)
		{
			this.pos.overwriteWith
			(
				ray.direction
			).multiplyScalar
			(
				this.distanceToCollision
			).add
			(
				ray.startPos
			);

			this.colliders["Plane"] = plane;
		}

		return this;
	}

	Collision.prototype.rayAndSphere = function(ray, sphere)
	{
		var rayDirection = ray.direction;
		var displacementFromSphereCenterToCamera = ray.startPos.clone().subtract
		(
			sphere.centerPos
		);
		var sphereRadius = sphere.radius;
		var sphereRadiusSquared = sphereRadius * sphereRadius;

		var a = rayDirection.dotProduct(rayDirection);

		var b = 2 * rayDirection.dotProduct
		(
			displacementFromSphereCenterToCamera
		);

		var c = displacementFromSphereCenterToCamera.dotProduct
		(
			displacementFromSphereCenterToCamera
		) - sphereRadiusSquared;

		var discriminant = (b * b) - 4 * a * c;

		if (discriminant >= 0)
		{
			var rootOfDiscriminant = Math.sqrt(discriminant);

			var distanceToCollision1 = 
				(rootOfDiscriminant - b) 
				/ (2 * a);

			var distanceToCollision2 = 
				(0 - rootOfDiscriminant - b) 
				/ (2 * a);

			if (distanceToCollision1 >= 0)
			{
				if 
				(
					distanceToCollision2 >= 0 
					&& distanceToCollision2 < distanceToCollision1
				)
				{
					this.distanceToCollision = distanceToCollision2;
				}
				else
				{
					this.distanceToCollision = distanceToCollision1;
				}
			}
			else
			{
				this.distanceToCollision = distanceToCollision2;				
			}
	
			this.pos.overwriteWith
			(
				ray.direction
			).multiplyScalar
			(
				this.distanceToCollision
			).add
			(
				ray.startPos
			);

			this.colliders["Sphere"] = sphere;
		}

		return this;
	}

	Collision.prototype.isPosWithinFace = function(face)
	{
		var displacementFromVertex0ToCollision = new Coords(0, 0);

		var isPosWithinAllEdgesOfFaceSoFar = true;

		var edges = face.edgesRectified;

		for (var e = 0; e < edges.length; e++)
		{
			var edgeFromFace = edges[e];

			displacementFromVertex0ToCollision.overwriteWith
			(
				this.pos
			).subtract
			(
				edgeFromFace.vertices[0].pos
			);
		
			// hack?
			var epsilon = .01;

			var dotProduct = displacementFromVertex0ToCollision.dotProduct
			(
				edgeFromFace.transverse
			);
			if (dotProduct >= epsilon)
			{
				isPosWithinAllEdgesOfFaceSoFar = false;
				break;
			}	
		}

		return isPosWithinAllEdgesOfFaceSoFar;
	}

}

function Color(name, systemColor)
{
	this.name = name;
	this.systemColor = systemColor;
}
{
	function Color_Instances()
	{
		this.Black = new Color("Black", "rgb(0, 0, 0)");
		this.Blue = new Color("Blue", "rgb(0, 0, 255)");
		this.Brown = new Color("Brown", "Brown");
		this.Cyan = new Color("Cyan", "rgb(0, 255, 255)");
		//this.CyanHalfTranslucent = new Color("CyanHalfTranslucent", "rgba(0, 128, 128, .5)");
		this.CyanHalfTranslucent = new Color("CyanHalfTranslucent", "rgba(128, 128, 128, .1)");
		this.Gray = new Color("Gray", "rgb(128, 128, 128)");
		this.GrayLight = new Color("Gray", "rgb(224, 224, 224)");
		this.Green = new Color("Green", "rgb(0, 255, 0)");
		this.Orange = new Color("Orange", "Orange");
		this.Red = new Color("Red", "rgb(255, 0, 0)");
		this.Violet = new Color("Violet", "Violet");
		this.Yellow = new Color("Yellow", "rgb(255, 255, 0)");
		this.YellowDark = new Color("YellowDark", "rgb(192, 192, 0)");
		this.White = new Color("White", "rgb(255, 255, 255)");
	}

	Color.Instances = new Color_Instances();
}

function Constraint_Cursor()
{
	this.name = "Cursor";
}
{
	Constraint_Cursor.prototype.applyToBody = function(body)
	{
		var cursor = body;
		var venue = Globals.Instance.universe.venueCurrent;
		var mousePos = Globals.Instance.inputHelper.mouseMovePos.clone();

		var camera = venue.camera;
		var cameraPos = camera.loc.pos;
		var cameraOrientation = camera.orientation;
		mousePos.subtract(camera.viewSizeHalf);

		var xyPlaneNormal = new Coords(0, 0, 1);

		var boundsToRestrictTo;
		var cursorPos = cursor.loc.pos;

		var cameraForward = cameraOrientation.forward;
		var displacementFromCameraToMousePosProjected = cameraForward.clone().multiplyScalar
		(
			camera.focalLength
		).add
		(
			cameraOrientation.right.clone().multiplyScalar
			(
				mousePos.x
			)
		).add
		(
			cameraOrientation.down.clone().multiplyScalar
			(
				mousePos.y
			)
		);

		var rayFromCameraToMousePos = new Ray
		(
			cameraPos,
			displacementFromCameraToMousePosProjected
		);

		if (cursor.hasXYPositionBeenSpecified == false)
		{
			boundsToRestrictTo = new Bounds
			(
				new Coords
				(
					Number.NEGATIVE_INFINITY, 
					Number.NEGATIVE_INFINITY, 
					0
				),
				new Coords
				(
					Number.POSITIVE_INFINITY, 
					Number.POSITIVE_INFINITY, 
					0
				)
			);

			var planeToRestrictTo = new Plane
			(
				xyPlaneNormal,
				0
			);

			var collisionPos = new Collision().rayAndPlane
			(
				rayFromCameraToMousePos,
				planeToRestrictTo
			).pos;

			if (collisionPos != null)
			{
				body.loc.pos.overwriteWith(collisionPos);
			}
		}
		else
		{
			boundsToRestrictTo = new Bounds
			(
				new Coords
				(
					cursorPos.x, 
					cursorPos.y, 
					Number.NEGATIVE_INFINITY
				),
				new Coords
				(
					cursorPos.x, 
					cursorPos.y, 
					Number.POSITIVE_INFINITY
				)
			);

			var planeNormal = xyPlaneNormal.clone().crossProduct
			(
				cameraOrientation.right
			);

			cursorPos.z = 0;

			var planeToRestrictTo = new Plane
			(
				planeNormal,
				cursorPos.dotProduct(planeNormal)
			);

			var collisionPos = new Collision().rayAndPlane
			(
				rayFromCameraToMousePos,
				planeToRestrictTo
			).pos;

			if (collisionPos != null)
			{
				cursorPos.z = collisionPos.z;
			}			
		}

		cursorPos.trimToRangeMinMax
		(
			boundsToRestrictTo.min,	
			boundsToRestrictTo.max
		);
	}	
}

function Constraint_LookAtBody
(
	targetBody
)
{
	this.name = "LookAtBody";
	this.targetBody = targetBody;
}
{
	Constraint_LookAtBody.prototype.applyToBody = function(body)
	{
		var targetPos = this.targetBody; // hack 
		var bodyPos = body.loc.pos

		var bodyOrientationForward = targetPos.clone().subtract
		(
			bodyPos
		).normalize();

		body.orientation = new Orientation
		(
			bodyOrientationForward,
			body.orientation.down	
		);		
	}
}

function Constraint_PositionOnCylinder
(
	center, orientation, yaw, radius, distanceFromCenterAlongAxis
)
{
	this.name = "PositionOnCylinder";
	this.center = center;
	this.orientation = orientation;
	this.yaw = yaw;
	this.radius = radius;
	this.distanceFromCenterAlongAxis = distanceFromCenterAlongAxis;
}
{
	Constraint_PositionOnCylinder.prototype.applyToBody = function(body)
	{
		this.yaw = NumberHelper.wrapValueToRangeMinMax(this.yaw, 0, 1);
		var yawInRadians = this.yaw * Polar.RadiansPerCycle;

		var bodyPos = body.loc.pos;		

		bodyPos.overwriteWith
		(
			this.orientation.down
		).multiplyScalar
		(
			this.distanceFromCenterAlongAxis
		).add
		(
			this.center
		).add
		(
			this.orientation.forward.clone().multiplyScalar
			(
				Math.cos(yawInRadians)
			).add
			(
				this.orientation.right.clone().multiplyScalar
				(
					Math.sin(yawInRadians)
				)
			).multiplyScalar
			(
				this.radius
			)

		)

		body.orientation.overwriteWith(this.orientation);
	}	
}

function Control()
{}
{
	Control.isEnabled = function()
	{
		return this.dataBindingForIsEnabled.get();
	}	
}

function ControlBuilder()
{}
{
	// constants

	ControlBuilder.ColorsForeAndBackDefault = [ "Gray", "White" ];

	// static methods

	ControlBuilder.configure = function()
	{
		var displayHelper = Globals.Instance.displayHelper;
		var containerSize = displayHelper.viewSize.clone();
		var controlHeight = containerSize.y / 12;
		var columnWidth = 100;
		var labelWidth = 75;
		var margin = 10;
		var buttonWidth = (columnWidth - margin) / 2;

		var returnValue = new ControlContainer
		(
			"containerConfigure",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords(0, 0), // pos
			containerSize,
			// children
			[
				new ControlButton
				(
					"buttonSave",
					new Coords((containerSize.x - columnWidth) / 2, 15), // pos
					new Coords(columnWidth, controlHeight), // size
					"Save",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var universe = Globals.Instance.universe;
						var profile = universe.profile;
						var world = universe.world;

						world.dateSaved = DateTime.now();

						Globals.Instance.profileHelper.profileSave
						(
							profile
						);

						var venueNext = new VenueWorld(world);
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),

				new ControlLabel
				(
					"labelMusicVolume",
					new Coords((containerSize.x - columnWidth) / 2, 45), // pos
					new Coords(labelWidth, controlHeight), // size
					false, // isTextCentered
					new DataBinding("Music Volume:")
				),

				new ControlSelect
				(
					"selectMusicVolume",
					new Coords((containerSize.x - columnWidth) / 2 + labelWidth, 45), // pos
					new Coords(controlHeight, controlHeight), // size

					// dataBindingForValueSelected
					new DataBinding
					(
						Globals.Instance.soundHelper, 
						"musicVolume"
					),

					// dataBindingForOptions
					new DataBinding
					(
						SoundHelper.controlSelectOptionsVolume(),
						null
					),

					"value", // bindingExpressionForOptionValues,
					"text", // bindingExpressionForOptionText
					new DataBinding(true) // dataBindingForIsEnabled
				),

				new ControlLabel
				(
					"labelSoundVolume",
					new Coords((containerSize.x - columnWidth) / 2, 75), // pos
					new Coords(labelWidth, controlHeight), // size
					false, // isTextCentered
					new DataBinding("Sound Volume:")
				),

				new ControlSelect
				(
					"selectSoundVolume",
					new Coords
					(
						(containerSize.x - columnWidth) 
						/ 2 
						+ labelWidth, 
						75
					), // pos
					new Coords(controlHeight, controlHeight), // size

					// dataBindingForValueSelected
					new DataBinding
					(
						Globals.Instance.soundHelper, 
						"soundVolume"
					),

					// dataBindingForOptions
					new DataBinding
					(
						SoundHelper.controlSelectOptionsVolume(),
						null
					),
					"value", // bindingExpressionForOptionValues,
					"text", // bindingExpressionForOptionText
					new DataBinding(true) // dataBindingForIsEnabled
				),

				new ControlButton
				(
					"buttonReturn",
					new Coords((containerSize.x - columnWidth) / 2, 105), // pos
					new Coords(buttonWidth, controlHeight), // size
					"Return",
					null, // dataBindingForIsEnabled
					// click
					function()
					{
						var universe = Globals.Instance.universe;
						var world = universe.world;
						var venueNext = new VenueWorld(world);
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),
	
				new ControlButton
				(
					"buttonQuit",
					new Coords((containerSize.x - columnWidth) / 2 + buttonWidth + margin, 105), // pos
					new Coords(buttonWidth, controlHeight), // size
					"Quit",
					null, // dataBindingForIsEnabled
					// click
					function()
					{
						Globals.Instance.reset();
						var universe = Globals.Instance.universe;
						var venueNext = new VenueControls
						(
							ControlBuilder.title()
						);
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),
			]
		);

		return returnValue;
	}

	ControlBuilder.confirm = function(message, confirm, cancel)
	{
		var containerSize = Globals.Instance.displayHelper.viewSize.clone();
		var controlHeight = containerSize.y / 12;
		var buttonWidth = 45;
		var margin = 10;

		var returnValue = new ControlContainer
		(
			"containerConfirm",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords(0, 0), // pos
			containerSize,
			// children
			[
				new ControlLabel
				(
					"labelMessage",
					new Coords((containerSize.x - 100) / 2, 50), // pos
					new Coords(100, controlHeight), // size
					true, // isTextCentered
					new DataBinding(message)
				),

				new ControlButton
				(
					"buttonConfirm",
					new Coords
					(
						(
							containerSize.x 
							- (buttonWidth * 2 + margin)
						) / 2, 
						100
					), // pos
					new Coords(buttonWidth, controlHeight), // size
					"Confirm",
					null, // dataBindingForIsEnabled
					confirm
				),

				new ControlButton
				(
					"buttonCancel",
					new Coords
					(
						(
							containerSize.x 
							- (buttonWidth * 2 + margin)
						) / 2 
						+ buttonWidth + margin, 
						100
					), // pos
					new Coords(buttonWidth, controlHeight), // size
					"Cancel",
					null, // dataBindingForIsEnabled
					cancel
				),
			]
		);

		return returnValue;
	}

	ControlBuilder.profileDetail = function()
	{
		var containerSize = Globals.Instance.displayHelper.viewSize.clone();
		var controlHeight = containerSize.y / 12;
		var buttonWidth = 45;
		var margin = 10;

		var returnValue = new ControlContainer
		(
			"containerProfileDetail",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords(0, 0), // pos
			containerSize,
			// children
			[
				new ControlLabel
				(
					"labelProfileName",
					new Coords
					(
						(containerSize.x - 100) / 2, 
						controlHeight
					), // pos
					new Coords(100, controlHeight), // size
					true, // isTextCentered
					new DataBinding(Globals.Instance.universe.profile.name)
				),

				new ControlSelect
				(
					"listWorlds",
					new Coords((containerSize.x - 150) / 2, 50), // pos
					new Coords(150, 50), // size
					// dataBindingForValueSelected
					new DataBinding(Globals.Instance.universe, "world"), 
					// dataBindingForOptions
					new DataBinding
					(
						Globals.Instance.universe.profile.worlds,
						null
					),
					null, // bindingExpressionForOptionValues
					"name", // bindingExpressionForOptionText
					new DataBinding(true), // dataBindingForIsEnabled
					4 // numberOfItemsVisible
				),

				new ControlButton
				(
					"buttonBack",
					new Coords(margin, margin), // pos
					new Coords(controlHeight, controlHeight), // size
					"<",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var venueNext = new VenueControls
						(
							ControlBuilder.profileSelect()
						);
						venueNext = new VenueFader(venueNext);
						var universe = Globals.Instance.universe;
						universe.venueNext = venueNext;
					}
				),
	
				new ControlButton
				(
					"buttonNew",
					new Coords
					(
						(
							containerSize.x 
							- (buttonWidth * 2 + margin)
						) / 2, 
						110
					), // pos
					new Coords(buttonWidth, controlHeight), // size
					"New",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var world = World.new();

						var universe = Globals.Instance.universe;
						var profile = universe.profile;
						profile.worlds.push(world);

						Globals.Instance.profileHelper.profileSave
						(
							profile
						);				

						universe.world = world;
						var venueNext = new VenueControls
						(
							ControlBuilder.worldDetail()
						);
						venueNext = new VenueVideo
						(
							"Intro", // videoName
							venueNext
						);
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),

				new ControlButton
				(
					"buttonSelectWorld",
					new Coords
					(
						(
							containerSize.x 
							- (buttonWidth * 2 + margin)
						) / 2 + 45 + margin, 
						110
					), // pos
					new Coords(buttonWidth, controlHeight), // size
					"Select",
					// dataBindingForIsEnabled
					new DataBinding(Globals.Instance.universe, "isWorldSelected"), 
					// click
					function ()
					{
						var venueNext = new VenueControls
						(
							ControlBuilder.worldDetail()
						);
						venueNext = new VenueFader(venueNext);
						Globals.Instance.universe.venueNext = venueNext;
					}
				),	

				new ControlButton
				(
					"buttonDeleteProfile",
					new Coords(containerSize.x - controlHeight - margin, margin), // pos
					new Coords(controlHeight, controlHeight), // size
					"X",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var universe = Globals.Instance.universe;
						var profile = universe.profile;

						var controlConfirm = ControlBuilder.confirm
						(
							"Delete Profile \"" 
								+ profile.name 
								+ "\"?",
							// confirm
							function()
							{
								var universe = Globals.Instance.universe;
								var profile = universe.profile;
								Globals.Instance.profileHelper.profileDelete
								(
									profile
								);
								universe.profile = null;

								var venueNext = new VenueControls
								(
									ControlBuilder.profileSelect()
								);
								venueNext = new VenueFader(venueNext);
								universe.venueNext = venueNext;
							},
							// cancel
							function()
							{
								var venueNext = new VenueControls
								(
									ControlBuilder.profileDetail()
								);
								venueNext = new VenueFader(venueNext);
								var universe = Globals.Instance.universe;
								universe.venueNext = venueNext;
							}						
						);

						var venueNext = new VenueControls(controlConfirm);
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),
			]
		);

		return returnValue;
	}

	ControlBuilder.profileNew = function()
	{
		var containerSize = Globals.Instance.displayHelper.viewSize.clone();
		var buttonWidth = 45;
		var controlHeight = containerSize.y / 12;
		var margin = 10;

		return new ControlContainer
		(
			"containerProfileNew",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords(0, 0), // pos
			containerSize,
			// children
			[
				new ControlLabel
				(
					"labelName",
					new Coords((containerSize.x - 100) / 2, 25), // pos
					new Coords(100, 25), // size
					true, // isTextCentered
					new DataBinding("Name:")
				),

				new ControlTextBox
				(
					"textBoxName",
					new Coords((containerSize.x - 100) / 2, 50), // pos
					new Coords(100, 25), // size
					""
				),

				new ControlButton
				(
					"buttonCreate",
					new Coords((containerSize.x - (buttonWidth * 2 + margin)) / 2, 80), // pos
					new Coords(buttonWidth, controlHeight), // size
					"Create",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var container = Globals.Instance.universe.venueCurrent.controlRoot;
						var textBoxName = container.children["textBoxName"];
						var profileName = textBoxName.text();
						if (profileName == "")
						{
							return;
						}

						var profile = new Profile(profileName, []);
						Globals.Instance.profileHelper.profileAdd
						(
							profile
						);

						var universe = Globals.Instance.universe;
						universe.profile = profile;
						var venueNext = new VenueControls
						(
							ControlBuilder.profileDetail()
						);
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),

				new ControlButton
				(
					"buttonCancel",
					new Coords
					(
						(
							containerSize.x 
							- (buttonWidth * 2 + margin)
						) / 2 
						+ buttonWidth 
						+ margin, 
						80
					), // pos
					new Coords(buttonWidth, controlHeight), // size
					"Cancel",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var universe = Globals.Instance.universe;
						var venueNext = new VenueControls
						(
							ControlBuilder.profileSelect()
						);
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),
			]
		);
	}

	ControlBuilder.profileSelect = function()
	{
		var profiles = Globals.Instance.profileHelper.profiles();
		
		var containerSize = Globals.Instance.displayHelper.viewSize.clone();
		var controlHeight = containerSize.y / 12;
		var buttonWidth = 45;
		var margin = 10;

		var returnValue = new ControlContainer
		(
			"containerProfileSelect",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords(0, 0), // pos
			containerSize,
			// children
			[
				new ControlLabel
				(
					"labelSelectAProfile",
					new Coords((containerSize.x - 100) / 2, 25), // pos
					new Coords(100, controlHeight), // size
					true, // isTextCentered
					new DataBinding("Select a Profile:")
				),

				new ControlSelect
				(
					"listProfiles",
					new Coords((containerSize.x - 100) / 2, 55), // pos
					new Coords(100, 50), // size
					// dataBindingForValueSelected
					new DataBinding(Globals.Instance.universe, "profile"), 
					new DataBinding(profiles), // options
					null, // bindingExpressionForOptionValues
					"name", // bindingExpressionForOptionText,
					new DataBinding(true), // isEnabled
					4 // numberOfItemsVisible
				),

				new ControlButton
				(
					"buttonNew",
					new Coords((containerSize.x - 100) / 2, 110), // pos
					new Coords(buttonWidth, controlHeight), // size
					"New",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var universe = Globals.Instance.universe;
						var venueNext = new VenueControls
						(
							ControlBuilder.profileNew()
						);
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),

				new ControlButton
				(
					"buttonInstant",
					new Coords((containerSize.x - 100) / 2, 150), // pos
					new Coords(100, controlHeight), // size
					"Instant",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{

						var universe = Globals.Instance.universe;

						var profile = new Profile("Default", []);
						universe.profile = profile;
						var world = World.new();
						profile.worlds.push(world);
						universe.world = world;
						var venueNext = new VenueWorld
						(
							universe.world
						);
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),				

				new ControlButton
				(
					"buttonSelectProfile",
					new Coords((containerSize.x - 100) / 2 + buttonWidth + margin, 110), // pos
					new Coords(buttonWidth, controlHeight), // size
					"Select",
					// dataBindingForIsEnabled
					new DataBinding(Globals.Instance.universe, "isProfileSelected"),
					// click
					function()
					{						
						var venueNext = new VenueControls
						(
							ControlBuilder.profileDetail()
						);				
						venueNext = new VenueFader(venueNext);		
						Globals.Instance.universe.venueNext = venueNext;
					}
				),

			]
		);

		return returnValue;
	}

	ControlBuilder.selection = function
	(
		pos, 
		size, 
		margin,
		controlHeight
	)
	{	
		var returnValue = new ControlContainer
		(
			"containerSelected",
			ControlBuilder.ColorsForeAndBackDefault,
			pos.clone(),
			size.clone(),
			// children
			[
				new ControlLabel
				(
					"labelSelected",
					new Coords(margin, 0), // pos
					new Coords(0, controlHeight), // size
					false, // isTextCentered
					new DataBinding("Selection:")
				),

				new ControlPlaceholder
				(
					"placeholderSelection",
					new Coords(margin, controlHeight), // pos
					size.clone(), // size - hack
					"[none]",
					new DataBinding
					(
						Globals.Instance.universe,
						"venueCurrent.selection.controlBuild_Selection"
					)
				),
			]
		);

		return returnValue;
	}

	ControlBuilder.timeAndPlace = function
	(
		containerMainSize, 
		containerInnerSize, 
		margin,
		controlHeight
	)
	{
		var returnValue = new ControlContainer
		(
			"containerTimeAndPlace",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords
			(
				margin,
				margin
			),
			containerInnerSize,
			// children
			[
				new ControlLabel
				(
					"textDate",
					new Coords(margin,  margin), // pos
					new Coords
					(
						containerInnerSize.x - 30 - margin * 3, 
						controlHeight
					), // size
					false, // isTextCentered
					new DataBinding(Globals.Instance.universe, "venueCurrent.model.name")
				),

				new ControlLabel
				(
					"labelTurn",
					new Coords(margin, margin + controlHeight), // pos
					new Coords(0, controlHeight), // size
					false, // isTextCentered
					new DataBinding("Turn:")
				),

				new ControlLabel
				(
					"textTurn",
					new Coords(margin + 25, margin + controlHeight), // pos
					new Coords
					(
						containerInnerSize.x - 30 - margin * 3, 
						controlHeight
					), // size
					false, // isTextCentered
					new DataBinding(Globals.Instance.universe.world, "turnsSoFar")
				),

				new ControlButton
				(
					"buttonTurnNext", // name, 
					new Coords(margin + 50, margin + controlHeight), // pos
					new Coords(controlHeight, controlHeight), // size, 
					">", // text, 
					null, // dataBindingForIsEnabled, 
					// click
					function() 
					{ 
						var universe = Globals.Instance.universe;
						universe.world.updateForTurn();
					}
				),

				new ControlButton
				(
					"buttonTurnFastForward", // name, 
					new Coords(margin + 50 + controlHeight, margin + controlHeight), // pos
					new Coords(controlHeight, controlHeight), // size, 
					">>", // text, 
					null, // dataBindingForIsEnabled, 
					// click
					function() 
					{ 
						alert("todo - fast forward");
					}
				)

			]
		);

		return returnValue;
	}

	ControlBuilder.title = function()
	{
		var containerSize = Globals.Instance.displayHelper.viewSize.clone();

		var returnValue = new ControlContainer
		(
			"containerTitle",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords(0, 0), // pos
			containerSize,
			// children
			[
				new ControlImage
				(
					"imageTitle",
					new Coords(0, 0),
					containerSize,
					"Title.png"
				),
	
				new ControlButton
				(
					"buttonStart",
					new Coords
					(
						(containerSize.x - 50) / 2, 
						containerSize.y - 50
					), // pos
					new Coords(50, 25), // size
					"Start",
					null, // dataBindingForIsEnabled
					// click
					function()
					{
						var venueNext = new VenueControls
						(
							ControlBuilder.profileSelect()
						);
						venueNext = new VenueFader(venueNext);
						var universe = Globals.Instance.universe;
						universe.venueNext = venueNext;
					}
				),
			]
		);

		return returnValue;
	}

	ControlBuilder.view = function
	(
		containerMainSize, 
		containerInnerSize, 
		margin,
		controlHeight
	)
	{
		var cameraSpeed = 10;

		var returnValue = new ControlContainer
		(
			"containerViewControls",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords
			(
				margin,
				containerMainSize.y 
					- margin
					- containerInnerSize.y
			),
			containerInnerSize,
			// children
			[
				new ControlLabel
				(
					"labelControls",
					new Coords(margin, 0),// pos
					new Coords(0, controlHeight), // size
					false, // isTextCentered
					new DataBinding("View")
				),

				new ControlButton
				(
					"buttonViewUp",
					new Coords
					(
						(containerInnerSize.x - (controlHeight * 3)) / 2 + controlHeight, 
						controlHeight
					), // pos
					new Coords(controlHeight, controlHeight), // size
					"^",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var venueCurrent = Globals.Instance.universe.venueCurrent;
						var camera = venueCurrent.camera;
						new Action_CameraMove([0, cameraSpeed]).perform(camera);
					}
				),

				new ControlButton
				(
					"buttonViewDown",
					new Coords
					(
						(
							containerInnerSize.x 
							- (controlHeight * 3)
						) 
						/ 2 
						+ controlHeight, 
						controlHeight * 2
					), // pos
					new Coords(controlHeight, controlHeight), // size
					"v",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var universe = Globals.Instance.universe;
						var venueCurrent = universe.venueCurrent;
						var camera = venueCurrent.camera;
						new Action_CameraMove([0, 0 - cameraSpeed]).perform(camera);
					}
				),
				
				new ControlButton
				(
					"buttonViewLeft",
					new Coords
					(
						(containerInnerSize.x - (controlHeight * 3)) / 2, 
						controlHeight * 2
					), // pos
					new Coords(controlHeight, controlHeight), // size
					"<",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var venueCurrent = Globals.Instance.universe.venueCurrent;
						var camera = venueCurrent.camera;
						new Action_CameraMove([cameraSpeed, 0]).perform(camera);
					}
				),

				new ControlButton
				(
					"buttonViewRight",
					new Coords
					(
						(
							containerInnerSize.x 
							- (controlHeight * 3)
						) / 2 
						+ controlHeight * 2, 
						controlHeight * 2
					), // pos
					new Coords(controlHeight, controlHeight), // size
					">",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var venueCurrent = Globals.Instance.universe.venueCurrent;
						var camera = venueCurrent.camera;
						new Action_CameraMove([0 - cameraSpeed, 0]).perform
						(
							camera
						);
					}
				),
			]
		);

		return returnValue;
	}

	ControlBuilder.worldDetail = function()
	{
		var universe = Globals.Instance.universe;
		var world = universe.world;

		var containerSize = Globals.Instance.displayHelper.viewSize.clone();
		var controlHeight = containerSize.y / 12;
		var margin = 10;

		var returnValue = new ControlContainer
		(
			"containerWorldDetail",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords(0, 0), // pos
			containerSize,
			// children
			[
				new ControlLabel
				(
					"labelProfileName",
					new Coords((containerSize.x - 100) / 2, 15), // pos
					new Coords(100, controlHeight), // size
					true, // isTextCentered
					new DataBinding(universe.profile.name)
				),
				new ControlLabel
				(
					"labelWorldName",
					new Coords((containerSize.x - 100) / 2, 30), // pos
					new Coords(100, controlHeight), // size
					true, // isTextCentered
					new DataBinding(world.name)
				),
				new ControlLabel
				(
					"labelStartDate",
					new Coords((containerSize.x - 100) / 2, 45), // pos
					new Coords(100, controlHeight), // size
					true, // isTextCentered
					new DataBinding
					(
						"Started:" 
						+ world.dateCreated.toStringTimestamp()
					)
				),
				new ControlLabel
				(
					"labelSavedDate",
					new Coords((containerSize.x - 100) / 2, 60), // pos
					new Coords(100, controlHeight), // size
					true, // isTextCentered
					new DataBinding
					(
						"Saved:" 
						+ world.dateSaved.toStringTimestamp()
					)
				),

				new ControlButton
				(
					"buttonStart",
					new Coords((containerSize.x - 100) / 2, 100), // pos
					new Coords(100, controlHeight), // size
					"Start",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var universe = Globals.Instance.universe;
						var venueNext = new VenueWorld
						(
							universe.world
						);
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),

				new ControlButton
				(
					"buttonBack",
					new Coords(margin, margin), // pos
					new Coords(controlHeight, controlHeight), // size
					"<",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var venueNext = new VenueControls
						(
							ControlBuilder.profileDetail()
						);
						venueNext = new VenueFader(venueNext);
						var universe = Globals.Instance.universe;
						universe.venueNext = venueNext;
					}
				),	

				new ControlButton
				(
					"buttonDeleteWorld",
					new Coords(containerSize.x - margin - controlHeight, margin), // pos
					new Coords(controlHeight, controlHeight), // size
					"X",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var universe = Globals.Instance.universe;
						var profile = universe.profile;
						var world = universe.world;

						var controlConfirm = ControlBuilder.confirm
						(
							"Delete World \"" 
								+ world.name 
								+ "\"?",
							// confirm
							function()
							{
								var universe = Globals.Instance.universe;
								var profile = universe.profile;
								var world = universe.world;
								var worlds = profile.worlds;
								var worldIndex = worlds.indexOf(world);

								worlds.splice
								(
									worldIndex,
									1
								);
								universe.world = null;
								
								Globals.Instance.profileHelper.profileSave
								(
									profile
								);

								var venueNext = new VenueControls
								(
									ControlBuilder.profileDetail()
								);
								venueNext = new VenueFader(venueNext);
								universe.venueNext = venueNext;
							},
							// cancel
							function()
							{
								var venueNext = new VenueControls
								(
									ControlBuilder.worldDetail()
								);
								venueNext = new VenueFader(venueNext);
								var universe = Globals.Instance.universe;
								universe.venueNext = venueNext;
							}						
						);

						var venueNext = new VenueControls(controlConfirm);
						venueNext = new VenueFader(venueNext);

						universe.venueNext = venueNext;
					}
				),
			]
		);

		return returnValue;
	}	
}

function ControlPlaceholder(name, pos, size, textToDisplayIfEmpty, dataBinding)
{
	this.name = name;
	this.pos = pos;
	this.size = size;
	this.textToDisplayIfEmpty = textToDisplayIfEmpty;
	this.dataBinding = dataBinding;

	this.controlLabelToDisplayIfEmpty = new ControlLabel
	(
		this.name + "_Empty",
		this.pos,
		new Coords(0, 0), // this.size
		false, // isTextCentered
		new DataBinding(this.textToDisplayIfEmpty)
	);
}
{
	ControlPlaceholder.prototype.childWithFocus = function()
	{
		return this.controlBound().childWithFocus();
	}

	ControlPlaceholder.prototype.childrenAtPosAddToList = function
	(
		posAbsolute,
		posToCheck, 
		listToAddTo, 
		addFirstChildOnly
	)
	{
		return this.controlBound().childrenAtPosAddtoList
		(
			posAbsolute, posToCheck, listToAddTo, addFirstChildOnly
		);
	}

	ControlPlaceholder.prototype.controlBound = function()
	{
		var returnValue = this.dataBinding.get();

		if (returnValue == null)
		{
			returnValue = this.controlLabelToDisplayIfEmpty;
		}

		return returnValue;
	}

	ControlPlaceholder.prototype.draw = function(pos)
	{
		this.controlBound().draw(pos);
	}

	ControlPlaceholder.prototype.isEnabled = function()
	{
		return this.controlBound().isEnabled();
	}

	ControlPlaceholder.prototype.mouseClick = function(mouseClickPos, pos)
	{
		var controlBound = this.controlBound();
		if (controlBound.mouseClick != null)
		{
			controlBound.mouseClick(mouseClickPos, pos);
		}
	}

	ControlPlaceholder.prototype.mouseEnter = function(mouseMovePos)
	{
		// fix - see mouseClick
		var mouseEnter = this.controlBound().mouseEnter;
		if (mouseEnter != null) 
		{
			mouseEnter(mouseMovePos); 
		}
	}

	ControlPlaceholder.prototype.mouseExit = function(mouseMovePos)
	{
		// fix - see mouseClick
		var mouseExit = this.controlBound().mouseExit;
		if (mouseExit != null)
		{
			mouseExit(mouseMovePos);
		}
	}
}


function ControlButton(name, pos, size, text, dataBindingForIsEnabled, click)
{
	this.name = name;
	this.pos = pos;
	this.size = size;
	this.text = text;
	this.dataBindingForIsEnabled = dataBindingForIsEnabled;
	this.click = click;

	if (this.dataBindingForIsEnabled == null)
	{
		this.dataBindingForIsEnabled = new DataBinding(true);
	}

	this.isHighlighted = false;	
}
{
	ControlButton.prototype.draw = function(pos)
	{
		Globals.Instance.displayHelper.drawControlButton(this, pos);
	}

	ControlButton.prototype.isEnabled = Control.isEnabled;

	ControlButton.prototype.mouseClick = function(clickPos)
	{
		if (this.isEnabled() == true)
		{
			this.click();
		}
		Globals.Instance.inputHelper.isMouseLeftPressed = false;
	}

	ControlButton.prototype.mouseEnter = function(mouseMovePos)
	{
		this.isHighlighted = true;
	}

	ControlButton.prototype.mouseExit = function(mouseMovePos)
	{
		this.isHighlighted = false;
	}
}

function ControlContainer(name, colorsForeAndBack, pos, size, children)
{
	if (colorsForeAndBack == null)
	{
		colorsForeAndBack = [ "Gray", "rgba(0, 0, 0, 0)" ];	
	}

	this.name = name;
	this.colorsForeAndBack = colorsForeAndBack;
	this.pos = pos;
	this.size = size;
	this.children = children;

	this.children.addLookups("name");

	this.indexOfChildWithFocus = null;
	this.childrenContainingPos = [];
	this.childrenContainingPosPrev = [];
}
{
	// instance methods

	ControlContainer.prototype.childWithFocus = function()
	{
		var returnValue = 
		(
			this.indexOfChildWithFocus == null 
			? null 
			: this.children[this.indexOfChildWithFocus]
		);

		return returnValue;
	}

	ControlContainer.prototype.childrenAtPosAddToList = function
	(
		posAbsolute,
		posToCheck, 
		listToAddTo, 
		addFirstChildOnly
	)
	{
		var children = this.children;
		for (var i = children.length - 1; i >= 0; i--)
		{
			var child = children[i];
			var childPos = child.pos.clone().add(posAbsolute);

			var doesChildContainPos = posToCheck.isWithinRangeMinMax
			(
				childPos,
				childPos.clone().add(child.size)
			);

			if (doesChildContainPos == true)
			{
				listToAddTo.push(child);
				if (addFirstChildOnly == true)
				{
					break;
				}
			}
		}

		return listToAddTo;
	}

	ControlContainer.prototype.draw = function(pos)
	{
		Globals.Instance.displayHelper.drawControlContainer(this, pos);
	}

	ControlContainer.prototype.keyPressed = function
	(
		keyCodePressed, isShiftKeyPressed
	)
	{
		var childWithFocus = this.childWithFocus();
		if (childWithFocus != null)
		{
			if (childWithFocus.keyPressed != null)
			{
				childWithFocus.keyPressed
				(
					keyCodePressed, isShiftKeyPressed
				);
			}
		}
	}

	ControlContainer.prototype.mouseClick = function(mouseClickPos, pos)
	{
		var childrenContainingPos = this.childrenContainingPos;
		childrenContainingPos.length = 0;
		
		this.childrenAtPosAddToList
		(
			pos,
			mouseClickPos,
			childrenContainingPos,
			true
		);

		for (var i = 0; i < childrenContainingPos.length; i++)
		{
			var child = childrenContainingPos[i];
			if (child.mouseClick != null)
			{
				this.indexOfChildWithFocus = this.children.indexOf(child);
				child.mouseClick(mouseClickPos, pos.clone().add(child.pos));
			}
		}
	}

	ControlContainer.prototype.mouseMove = function(mouseMovePos, pos)
	{
		var temp = this.childrenContainingPosPrev;
		this.childrenContainingPosPrev = this.childrenContainingPos;
		this.childrenContainingPos = temp;

		var childrenContainingPos = this.childrenContainingPos;
		childrenContainingPos.length = 0;
		this.childrenAtPosAddToList
		(
			pos, 
			mouseMovePos,
			childrenContainingPos,
			true
		);

		for (var i = 0; i < childrenContainingPos.length; i++)
		{
			var child = childrenContainingPos[i];

			if (child.mouseMove != null)
			{
				child.mouseMove(mouseMovePos, pos.clone().add(child.pos));
			}
			if (this.childrenContainingPosPrev.indexOf(child) == -1)
			{
				if (child.mouseEnter != null)
				{
					child.mouseEnter();
				}
			}
		}

		for (var i = 0; i < this.childrenContainingPosPrev.length; i++)
		{
			var child = this.childrenContainingPosPrev[i];
			if (childrenContainingPos.indexOf(child) == -1)
			{
				if (child.mouseExit != null)
				{
					child.mouseExit();
				}
			}
		}
	}
}

function ControlImage(name, pos, size, imageSrc)
{
	this.name = name;
	this.pos = pos;
	this.size = size;
	this.imageSrc = imageSrc;
}
{
	ControlImage.prototype.draw = function(pos)
	{
		if (this.systemImage == null)
		{
			this.systemImage = document.createElement("img");
			this.systemImage.src = this.imageSrc;
		}

		Globals.Instance.displayHelper.drawControlImage(this, pos);
	}
}

function ControlLabel(name, pos, size, isTextCentered, dataBindingForText)
{
	this.name = name;
	this.pos = pos;
	this.size = size;
	this.isTextCentered = isTextCentered;
	this.dataBindingForText = dataBindingForText;

	if (this.dataBindingForText.constructor.name != "DataBinding")
	{
		this.dataBindingForText = new DataBinding(this.dataBindingForText);
	}
}
{
	ControlLabel.prototype.draw = function(pos)
	{
		Globals.Instance.displayHelper.drawControlLabel(this, pos);
	}

	ControlLabel.prototype.text = function()
	{
		return this.dataBindingForText.get();
	}
}

function ControlScrollbar
(
	name,
	pos, 
	size,
	dataBindingForMin,
	dataBindingForMax,
	dataBindingForValue
)
{
	this.name = name;
	this.pos = pos;
	this.size = size;
	this.dataBindingForMin = dataBindingForMin;
	this.dataBindingForMax = dataBindingForMax;
	this.dataBindingForValue = dataBindingForValue
}
{
	ControlScrollbar.prototype.draw = function(pos)
	{
		Globals.Instance.displayHelper.drawControlScrollbar(this, pos);
	}

	ControlScrollbar.prototype.max = function()
	{
		return this.dataBindingForMax.get();
	}

	ControlScrollbar.prototype.min = function()
	{
		return this.dataBindingForMin.get();
	}

	ControlScrollbar.prototype.range = function()
	{
		return this.max() - this.min();
	}

	ControlScrollbar.prototype.value = function()
	{
		return this.dataBindingForValue.get();
	}
}

function ControlSelect
(
	name, 
	pos, 
	size, 
	dataBindingForValueSelected,
	dataBindingForOptions,
	bindingExpressionForOptionValues,
	bindingExpressionForOptionText,
	dataBindingForIsEnabled,
	numberOfItemsVisible
)
{
	this.name = name;
	this.pos = pos;
	this.size = size;
	this.dataBindingForValueSelected = dataBindingForValueSelected;
	this.dataBindingForOptions = dataBindingForOptions;
	this.bindingExpressionForOptionValues = bindingExpressionForOptionValues;
	this.bindingExpressionForOptionText = bindingExpressionForOptionText;
	this.dataBindingForIsEnabled = dataBindingForIsEnabled;
	this.numberOfItemsVisible = (numberOfItemsVisible == null ? 1 : numberOfItemsVisible);

	if (this.dataBindingForValueSelected == null)
	{
		this.dataBindingForValueSelected = new DataBinding(this, "_valueSelected");
	}

	var options = this.options();
	var numberOfOptions = (options == null ? 0 : options.length);
	this.indexOfFirstOptionVisible = 0;
	var scrollbarWidth = 12;
	this.scrollbar = new ControlScrollbar
	(
		name, 
		new Coords(this.size.x - scrollbarWidth, 0), // pos
		new Coords(scrollbarWidth, this.size.y), // size
		new DataBinding(0), // dataBindingForMin
		new DataBinding(numberOfOptions), // dataBindingForMax
		new DataBinding(0) // dataBindingForValue
	);
}
{
	ControlSelect.prototype.draw = function(pos)
	{
		if (this.numberOfItemsVisible == 1)
		{
			Globals.Instance.displayHelper.drawControlSelect(this, pos);
		}
		else
		{
			Globals.Instance.displayHelper.drawControlList(this, pos);	
		}
	}

	ControlSelect.prototype.isEnabled = Control.isEnabled;

	ControlSelect.prototype.optionSelected = function()
	{
		var returnValue = null;

		var options = this.options();

		if (options != null)
		{
			var valueSelected = this.dataBindingForValueSelected.get();
	
			for (var i = 0; i < options.length; i++)
			{
				var optionAsObject = options[i];
		
				var optionValue = DataBinding.get
				(
					optionAsObject, 
					this.bindingExpressionForOptionValues
				);
	
				if (optionValue == valueSelected)
				{
					returnValue = optionAsObject;
					break;
				}
			}
		}
	
		return returnValue;	
	}

	ControlSelect.prototype.options = function()
	{
		return this.dataBindingForOptions.get();
	}

	ControlSelect.prototype.mouseClick = function(clickPos, pos)
	{
		if (this.isEnabled() == false)
		{
			return;
		}
		
		if (this.numberOfItemsVisible == 1)
		{
			this.mouseClick1(clickPos, pos);
		}
		else
		{
			this.mouseClick2(clickPos, pos);
		}
		Globals.Instance.inputHelper.isMouseLeftPressed = false;
	}

	ControlSelect.prototype.mouseClick1 = function(clickPos, pos)
	{
		var options = this.options();

		var valueSelected = this.dataBindingForValueSelected.get();
		var optionSelected = this.optionSelected();

		if (optionSelected == null)
		{
			optionSelected = options[0];
		}
		else for (var i = 0; i < options.length; i++)
		{
			var optionAsObject = options[i];
			var optionValue = DataBinding.get
			(
				optionAsObject, 
				this.bindingExpressionForOptionValues
			);

			if (optionValue == valueSelected)
			{
				i++;
				if (i >= options.length) 
				{ 
					i = 0; 
				}
				optionSelected = options[i];

				break;
			}
		}

		valueSelected = DataBinding.get
		(
			optionSelected, this.bindingExpressionForOptionValues
		);

		this.dataBindingForValueSelected.set(valueSelected);
	}

	ControlSelect.prototype.mouseClick2 = function(clickPos, pos)
	{
		var itemSpacing = 12; // hack

		var offsetOfOptionClicked = clickPos.y - pos.y;
		var indexOfOptionClicked = 
			this.indexOfFirstOptionVisible 
			+ Math.floor
			(
				offsetOfOptionClicked 
				/ itemSpacing
			);

		var options = this.options();
		if (indexOfOptionClicked < options.length)
		{
			var optionClicked = options[indexOfOptionClicked];
			valueSelected = DataBinding.get
			(
				optionClicked, 
				this.bindingExpressionForOptionValues
			);
			this.dataBindingForValueSelected.set
			(
				valueSelected
			);
		}	

	}

	ControlSelect.prototype.valueSelected = function()
	{
		return this.dataBindingForValueSelected.get();
	}
}

function ControlSelectOption(value, text)
{
	this.value = value;
	this.text = text;
}

function ControlTextBox(name, pos, size, dataBindingForText)
{
	this.name = name;
	this.pos = pos;
	this.size = size;
	this.dataBindingForText = dataBindingForText;

	if (this.dataBindingForText.constructor.name == "String")
	{
		// hack
		this.dataBindingForText = new DataBinding(this.dataBindingForText);
	}

	this.isHighlighted = false;
}
{
	ControlTextBox.prototype.draw = function(pos)
	{
		Globals.Instance.displayHelper.drawControlTextBox(this, pos);
	}
	
	ControlTextBox.prototype.keyPressed = function(keyCodePressed, isShiftKeyPressed)
	{
		var text = this.text();
		if (keyCodePressed == 8) // backspace
		{
			text = text.substr(0, this.text.length - 1);
		}
		else
		{
			var charTyped = String.fromCharCode(keyCodePressed);
			if (isShiftKeyPressed == false)
			{
				charTyped = charTyped.toLowerCase();
			}

			text += charTyped;
		}

		this.dataBindingForText.set(text);

		// hack
		Globals.Instance.inputHelper.keyCodePressed = null;
	}

	ControlTextBox.prototype.mouseClick = function(mouseClickPos)
	{
		this.isHighlighted = true;
	}

	ControlTextBox.prototype.text = function()
	{
		return this.dataBindingForText.get();
	}
}

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

	Coords.NumberOfDimensions = 3;

	// instances

	function Coords_Instances()
	{
		this.Zeroes = new Coords(0, 0, 0);
	}

	Coords.Instances = new Coords_Instances();

	// methods

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

		return this;
	}

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

		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y, this.z);
	}

	Coords.prototype.crossProduct = function(other)
	{
		return this.overwriteWithDimensions
		(
			this.y * other.z - this.z * other.y,
			this.z * other.x - this.x * other.z,
			this.x * other.y - this.y * other.x
		);
	}

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

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

		return returnValue;
	}

	Coords.prototype.dimensions = function()
	{
		return [ this.x, this.y, this.z ];
	}

	Coords.prototype.dimension_Set = function(dimensionIndex, value)
	{
		if (dimensionIndex == 0)
		{
			this.x = value;
		}
		else if (dimensionIndex == 1)
		{
			this.y = value;
		}
		else 
		{
			this.z = value;
		}

		return this;
	}

	Coords.prototype.divide = function(other)
	{
		this.x /= other.x;
		this.y /= other.y;
		this.z /= other.z;

		return this;
	}

	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;
		this.z /= scalar;

		return this;
	}

	Coords.prototype.dotProduct = function(other)
	{
		var returnValue = 
			this.x * other.x 
			+ this.y * other.y
			+ this.z * other.z;

		return returnValue;
	}

	Coords.prototype.equals = function(other)
	{
		var returnValue = 
		(
			this.x == other.x 
			&& this.y == other.y
			&& this.z == other.z
		);

		return returnValue;
	}

	Coords.prototype.floor = function(other)
	{
		this.x = Math.floor(this.x);
		this.y = Math.floor(this.y);
		this.z = Math.floor(this.z);
		return this;
	}

	Coords.prototype.isWithinRangeMax = function(max)
	{
		var returnValue = 
		(
			this.x >= 0
			&& this.x <= max.x
			&& this.y >= 0
			&& this.y <= max.y
			&& this.z >= 0
			&& this.z <= max.z
		);

		return returnValue;
	}

	Coords.prototype.isWithinRangeMinMax = function(min, max)
	{
		var returnValue = 
		(
			this.x >= min.x
			&& this.x <= max.x
			&& this.y >= min.y
			&& this.y <= max.y
		);

		return returnValue;
	}

	Coords.prototype.magnitude = function()
	{
		return Math.sqrt
		(
			this.x * this.x
			+ this.y * this.y
			+ this.z * this.z
		);
	}

	Coords.prototype.multiply = function(other)
	{
		this.x *= other.x;
		this.y *= other.y;
		this.z *= other.z;

		return this;
	}

	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		this.z *= scalar;

		return this;
	}

	Coords.prototype.normalize = function()
	{
		return this.divideScalar(this.magnitude());
	}

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

		return this;
	}

	Coords.prototype.overwriteWithDimensions = function(x, y, z)
	{
		this.x = x;
		this.y = y;
		this.z = z;

		return this;
	}

	Coords.prototype.perpendicular2D = function()
	{
		var temp = this.x;
		this.x = this.y;
		this.y = 0 - temp;

		return this;
	}

	Coords.prototype.perpendicular3D = function()
	{
		var dimensionsAbsolute = this.clone().absolute().dimensions();

		var dimensionAbsoluteLeast = dimensionsAbsolute[0];
		var indexOfDimensionAbsoluteLeast = 0;

		for (var d = 1; d < dimensionsAbsolute.length; d++)
		{
			var dimensionAbsolute = dimensionsAbsolute[d];
			if (dimensionAbsolute < dimensionAbsoluteLeast)
			{
				dimensionAbsoluteLeast = dimensionAbsolute;
				indexOfDimensionAbsoluteLeast = d;
			}
		}

		var basisVector = new Coords(0, 0, 0).dimension_Set
		(
			indexOfDimensionAbsoluteLeast,
			1
		)

		this.crossProduct
		(
			basisVector
		);

		return this;
	}

	Coords.prototype.randomize = function()
	{
		this.overwriteWithDimensions
		(
			Math.random(),
			Math.random(),
			Math.random()
		);
		return this;
	}

	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		this.z -= other.z;

		return this;
	}

	Coords.prototype.trimToRangeMinMax = function(min, max)
	{
		for (var d = 0; d < Coords.NumberOfDimensions; d++)
		{
			var thisDimension = this.dimension(d);

			var minDimension = min.dimension(d);
			var maxDimension = max.dimension(d);

			if (thisDimension < minDimension)
			{
				thisDimension = minDimension;
			} 
			else if (thisDimension > maxDimension)
			{
				thisDimension = maxDimension;				
			}

			this.dimension_Set(d, thisDimension);
		}
	}
}

function Cursor(bodyParent)
{
	this.bodyParent = bodyParent;
	this.hasXYPositionBeenSpecified = false;
	this.hasZPositionBeenSpecified = false;

	this.defn = Cursor.BodyDefn;

	this.loc = this.bodyParent.loc.clone();

	this.constraints = 
	[
		new Constraint_Cursor()
	];
}
{
	Cursor.BodyDefn = new BodyDefn
	(
		"Cursor", 
		new Coords(10, 10), // size
		new VisualGroup
		([
			new VisualRectangle(Color.Instances.Brown, new Coords(10, 10)),
		])
	);

	// controls

	Cursor.prototype.controlBuild_Selection = function()
	{
		return this.bodyParent.controlBuild_Selection();
	}
}

function DataBinding(context, bindingExpression)
{
	this.context = context;
	this.bindingExpression = bindingExpression;
}
{
	DataBinding.get = function(context, bindingExpression)
	{
		var expressionValue = context;

		if (bindingExpression != null)
		{
			var bindingExpressionHierarchy = bindingExpression.split(".");
			
			for (var i = 0; i < bindingExpressionHierarchy.length; i++)
			{
				var bindingExpressionLevel = bindingExpressionHierarchy[i]; 
				var expressionValue = context[bindingExpressionLevel];

				if (expressionValue == null)
				{
					break;
				}
				else
				{
					if (expressionValue.constructor.name == "Function")
					{
						expressionValue = expressionValue.call(context);	
					}

					context = expressionValue;
				}
			}
		}

		return expressionValue;
	}

	DataBinding.set = function(context, bindingExpression, valueToSet)
	{
		var contextCurrent = context;

		if (bindingExpression != null)
		{
			var bindingExpressionHierarchy = bindingExpression.split(".");
			var hierarchySizeMinusOne = bindingExpressionHierarchy.length - 1;

			for (var i = 0; i < hierarchySizeMinusOne; i++)
			{
				var bindingExpressionLevel = bindingExpressionHierarchy[i]; 
				contextCurrent = contextCurrent[bindingExpressionLevel];

				if (contextCurrent == null)
				{
					break;
				}
				else
				{
					if (contextCurrent.constructor.name == "Function")
					{
						contextCurrent = contextCurrent.call(context);	
					}
				}
			}

			var bindingExpressionLevelLast = bindingExpressionHierarchy[hierarchySizeMinusOne];
			contextCurrent[bindingExpressionLevelLast] = valueToSet;
		}
	}

	// instance methods

	DataBinding.prototype.get = function()
	{
		return DataBinding.get(this.context, this.bindingExpression);
	}

	DataBinding.prototype.set = function(valueToSet)
	{
		if (this.bindingExpression == null)
		{
			this.context = valueToSet;
		}
		else
		{
			DataBinding.set(this.context, this.bindingExpression, valueToSet);
		}
	}
}

function DateTime(year, month, day, hours, minutes, seconds)
{
	this.year = year;
	this.month = month;
	this.day = day;
	this.hours = hours;
	this.minutes = minutes;
	this.seconds = seconds;
}
{
	// static methods

	DateTime.fromSystemDate = function(systemDate)
	{
		var returnValue = new DateTime
		(
			systemDate.getFullYear(),
			systemDate.getMonth() + 1,
			systemDate.getDate(),
			systemDate.getHours(),
			systemDate.getMinutes(),
			systemDate.getSeconds()
		);
	
		return returnValue;
	}

	DateTime.now = function()
	{
		return DateTime.fromSystemDate(new Date());
	}

	// instance methods

	DateTime.prototype.toStringTimestamp = function()
	{
		var returnValue = 
			""
			+ this.year
			+ "/"
			+ this.month.toString().padLeft(2, "0")
			+ "/"
			+ this.day.toString().padLeft(2, "0")
			+ "-"
			+ this.hours.toString().padLeft(2, "0")
			+ ":"
			+ this.minutes.toString().padLeft(2, "0")
			+ ":"
			+ this.seconds.toString().padLeft(2, "0")

		return returnValue;
	}
}

function Device(defn)
{
	this.defn = defn;
}
{
	Device.prototype.use = function(actor, target)
	{
		this.defn.use(actor, this, target);
	}
}

function DeviceDefn(name, range, use)
{
	this.name = name;
	this.range = range;
	this.use = use;
}

function DiplomaticAction(name, effect)
{
	this.name = name;
	this.effect = effect;
}
{
	function DiplomaticAction_Instances()
	{
		this.War = new DiplomaticAction
		(
			"Declare War on",
			function(universe, factionActing, factionReceiving) 
			{	
				var message;

				var stateExisting = factionActing.relationships[factionReceiving.name].state;
				if (stateExisting == Relationship.States.War)
				{
					message = 
						"The " 
						+ factionActing.name 
						+ " are already at war with the " 
						+ factionReceiving.name + ".";
				}
				else
				{
					var relationship = factionActing.relationships[factionReceiving.name];
					relationship.state = Relationship.States.War;
					relationship = factionReceiving.relationships[factionActing.name];
					relationship.state = Relationship.States.War;
					message = 
						"The " + factionActing.name 
						+ " have declared war on the " 
						+ factionReceiving.name + ".";
				}


				alert(message);
			}
		),

		this.Peace = new DiplomaticAction
		(
			"Offer Peace to", 
			function(universe, factionActing, factionReceiving) 
			{
				var message;

				var stateExisting = factionActing.relationships[factionReceiving.name].state;
				if (stateExisting == Relationship.States.Alliance)
				{
					message = 
						"The " + factionReceiving.name 
						+ " are already allied with the " + factionActing.name + ".";
				}
				else if (stateExisting == Relationship.States.Peace)
				{
					message = 
						"The " + factionReceiving.name 
						+ " are already at peace with the " + factionActing.name + ".";
				}
				else // if (stateExisting == Relationship.States.War)
				{
					var strengthOfEnemies = DiplomaticRelationship.strengthOfFactions
					(
						factionReceiving.enemies()
					);
	
					var strengthOfSelfAndAllies = DiplomaticRelationship.strengthOfFactions
					(
						factionReceiving.selfAndAllies()
					);
			
					var strengthOfAlliesMinusEnemies = 
						strengthOfSelfAndAllies - strengthOfEnemies;
	
					if (strengthOfAlliesMinusEnemies <= 0)
					{
						var relationship = factionActing.relationships[factionReceiving.name];
						relationship.state = Relationship.States.Peace;
						relationship = factionReceiving.relationships[factionActing.name];
						relationship.state = Relationship.States.Peace;
						message = 
							"The " + factionReceiving.name
							+ " have accepted a peace offer from the " 
							+ factionActing.name + ".";
					}
					else
					{
						message = 
							"The " + factionReceiving.name 
							+ " have rejected a peace offer from the " 
							+ factionActing.name + ".";
					}
				}

				alert(message);
			}
		);

		this.Alliance = new DiplomaticAction
		(
			"Propose Alliance with",
			// effect
			function(universe, factionActing, factionReceiving) 
			{
				var message;

				var stateExisting = factionActing.relationships[factionReceiving.name].state;
				if (stateExisting == Relationship.States.Alliance)
				{
					message = 
						"The " + factionReceiving.name 
						+ " are already allied with the " 
						+ factionActing.name + ".";
				}
				else if (stateExisting == Relationship.States.War)
				{
					message = 
						"The " + factionReceiving.name 
						+ " are currently at war with the " 
						+ factionActing.name + ".";
				}
				else
				{
					var factions = [ factionActing, factionReceiving ];
					var doAlliesAndEnemiesOfFactionsClash = false;

					var enemiesOfEitherFaction = [];

					for (var f = 0; f < factions.length; f++)
					{
						var factionThis = factions[f];
						var factionOther = factions[1 - f];

						var enemiesOfFactionThis = factionThis.enemies();
						enemiesOfEitherFaction = enemiesOfEitherFaction.concat
						(
							enemiesOfFactionThis
						);
						var alliesOfFactionOther = factionOther.allies();
						var intersection = enemiesOfFactionThis.intersectionWith
						(
							alliesOfFactionOther
						);

						if (intersection.length > 0)
						{
							doAlliesAndEnemiesOfFactionsClash = true;

							message = 
								"An alliance between the " 
								+ factionThis.name 
								+ " and the " 
								+ factionOther.name
								+ " is impossible because the " 
								+ factionThis.name 
								+ " are at war with some allies of the "
								+ factionOther.name
								+ " ("
								+ intersection.join(", ")
								+ ").";
						}
					}

					if (doAlliesAndEnemiesOfFactionsClash == false)
					{
						var factionsDeclaringWarOnActing = [];
						var factionsDeclaringWarOnReceiving = [];
						for (var i = 0; i < enemiesOfEitherFaction.length; i++)
						{
							var enemy = enemiesOfEitherFaction[i];
							if (factionActing.enemies().indexOf(enemy) == -1)
							{
								factionsDeclaringWarOnActing.push(enemy);
							}
							if (factionReceiving.enemies().indexOf() == -1)
							{
								factionsDeclaringWarOnReceiving.push(enemy);
							}			
						}

						var strengthOfNewEnemies = DiplomaticRelationship.strengthOfFactions
						(
							factionsDeclaringWarOnReceiving
						);
						if (strengthOfNewEnemies >= factionActing.strength)
						{
							message = 
								"The " 
								+ factionReceiving.name 
								+ " have declined to join an alliance with the " 
								+ factionActing.name + ".";
						}
						else
						{
							var relationship = factionActing.relationships[factionReceiving.name]
							relationship.state = Relationship.States.Alliance;
							relationship = factionReceiving.relationships[factionActing.name];
							relationship.state = Relationship.States.Alliance;

							message = 
								"The " 
								+ factionReceiving.name 
								+ " have joined an alliance with the " 
								+ factionActing.name + ".";

							if (factionsDeclaringWarOnActing.length > 0)
							{
								message += 
									"  Some enemies of the " 
									+ factionReceiving.name
									+ " (" 
									+ factionsDeclaringWarOnActing.join(", ") 
									+ ") have declared war on the "
									+ factionActing.name 
									+ "."
							}

							if (factionsDeclaringWarOnReceiving.length > 0)
							{
								message += 
									"  Some enemies of the " 
									+ factionActing.name
									+ " (" 
									+ factionsDeclaringWarOnReceiving.join(", ") 
									+ ") have declared war on the "
									+ factionReceiving.name 
									+ "."
							}

						} // end if strengthOfNewEnemies >= factionActing.strength else

					} // end if doAlliesAndEnemiesOfFactionsClash

				} // end if stateExisting

				alert(message);
				
			} // end effect
		); // end new DiplomaticAction

		this._All = 
		[
			this.Peace,
			this.Alliance,
			this.War,
		];
	}

	DiplomaticAction.Instances = new DiplomaticAction_Instances();
}

function DiplomaticRelationship(factionNameOther, state)
{
	this.factionNameOther = factionNameOther;
	this.state = state;
}
{
	// static methods

	DiplomaticRelationship.initializeForFactions = function(factions)
	{
		for (var f = 0; f < factions.length; f++)
		{
			var factionThis = factions[f];

			for (var g = 0; g < f; g++)
			{
				var factionOther = factions[g];

				factionThis.relationships.push
				(
					new DiplomaticRelationship
					(
						factionOther.name, 
						DiplomaticRelationship.States.Peace
					)
				);
				factionOther.relationships.push
				(
					new DiplomaticRelationship
					(
						factionThis.name, 
						DiplomaticRelationship.States.Peace
					)
				);
			}
		}

		for (var f = 0; f < factions.length; f++)
		{
			var faction = factions[f];
			faction.relationships.addLookups("factionNameOther");
		}
	}

	DiplomaticRelationship.setStateForFactions = function(factions, state)
	{
		for (var i = 0; i < factions.length; i++)
		{
			var faction = factions[i];
			var factionOther = factions[1 - i];

			faction.relationships[factionOther.name].state = state;
		}

		return state;
	}

	DiplomaticRelationship.strengthOfFactions = function(factions)
	{
		var returnValue = 0;

		for (var i = 0; i < factions.length; i++)
		{
			var faction = factions[i];
			returnValue += faction.strength();
		}

		return returnValue;
	}

	// instances

	function DiplomaticRelationship_States()
	{
		this.Alliance = "Alliance";
		this.Peace = "Peace";
		this.War = "War";
	}

	DiplomaticRelationship.States = new DiplomaticRelationship_States();

	// instance methods

	DiplomaticRelationship.prototype.factionOther = function()
	{
		return Globals.Instance.universe.factions[this.factionNameOther];
	}

	DiplomaticRelationship.prototype.toString = function()
	{
		var returnValue = this.factionNameOther + ":" + this.state;
		return returnValue;
	}
}

function DiplomaticSession(diplomaticActions, factionActing, factions)
{
	this.diplomaticActions = diplomaticActions;
	this.diplomaticActions.addLookups("name");

	this.factionActing = factionActing;
	this.factions = factions;
	this.factions.addLookups("name");

	this.factionSelected = null;
}
{
	// static methods

	DiplomaticSession.buildExample = function(factionActing, factions)
	{
		var diplomaticActions = DiplomaticAction.Instances._All;

		var session = new DiplomaticSession
		(
			diplomaticActions,
			factionActing,
			factions
		);

		return session;
	}

	// instance methods

	DiplomaticSession.prototype.isFactionSelected = function()
	{
		return (this.factionSelected != null);
	}

	DiplomaticSession.prototype.talkSessionInitialize = function()
	{
		var universe = Globals.Instance.universe;
		var talkSession = TalkSession.buildExample
		(
			this.factionActing,
			this.factionSelected
		);
		var venueNext = new VenueTalkSession
		(
			universe.venueCurrent, talkSession
		);
		venueNext = new VenueFader(venueNext);
		universe.venueNext = venueNext;
	}

	// controls

	DiplomaticSession.prototype.controlBuild = function()
	{
		var containerSize = Globals.Instance.displayHelper.viewSize.clone();
		var margin = 10;
		var controlHeight = 20;
		var listWidth = 100;

		var returnValue = new ControlContainer
		(
			"containerProfileSelect",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords(0, 0), // pos
			containerSize,
			// children
			[
				new ControlButton
				(
					"buttonDone",
					new Coords
					(
						margin, margin	
					), // pos
					new Coords
					(
						controlHeight, controlHeight
					), // size
					"<",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var universe = Globals.Instance.universe;
						var venueNext = universe.venueCurrent.venueParent;
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),

				new ControlLabel
				(
					"labelFactions",
					new Coords(margin, margin * 2 + controlHeight), // pos
					new Coords(100, controlHeight), // size
					false, // isTextCentered
					new DataBinding("Factions:")
				),

				new ControlSelect
				(
					"listFactions",
					new Coords(margin, margin * 2 + controlHeight * 2), // pos
					new Coords(listWidth, controlHeight * 4), // size
					// dataBindingForValueSelected
					new DataBinding(this, "factionSelected"), 
					new DataBinding(this.factions), // options
					null, // bindingExpressionForOptionValues
					"name", // bindingExpressionForOptionText,
					new DataBinding(true), // isEnabled
					6 // numberOfItemsVisible
				),

				new ControlButton
				(
					"buttonTalk",
					new Coords(margin, margin * 3 + controlHeight * 6), // pos
					new Coords(listWidth, controlHeight), // size
					"Talk",
					// dataBindingForIsEnabled
					new DataBinding(this, "isFactionSelected"), 
					// click
					this.talkSessionInitialize.bind(this)
				),

				Faction.controlBuild_Intelligence
				(
					this,
					new Coords(margin * 2 + listWidth, 0), // pos
					new Coords
					(
						containerSize.x - listWidth - margin * 2, 
						containerSize.y
					)
				),


			] 
		); 

		return returnValue;
	}
}

function DisplayHelper(viewSize, fontHeightInPixels)
{
	this.viewSize = viewSize;
	this.fontHeightInPixels = fontHeightInPixels;
}
{
	DisplayHelper.prototype.clear = function(colorBorder, colorBack)
	{
		this.drawRectangle
		(
			new Coords(0, 0), 
			this.viewSize, 
			(colorBorder == null ? "Gray" : colorBorder), 
			(colorBack == null ? "White" : colorBack)
		);
	}

	DisplayHelper.prototype.drawControlButton = function(control, pos)
	{
		var size = control.size;

		var colorsForeAndBack;

		if (control.isEnabled() == false)
		{
			colorsForeAndBack = ["LightGray", "White"];
		}
		else if (control.isHighlighted == true)
		{
			colorsForeAndBack = ["White", "Gray"];
		}
		else
		{
			colorsForeAndBack = ["Gray", "White"];
		}

		this.drawRectangle
		(
			pos, size, 
			colorsForeAndBack[0], colorsForeAndBack[1]
		)

		var text = control.text;

		var textWidth = this.graphics.measureText(text).width;
		var textMarginLeft = (size.x - textWidth) / 2;
		var textHeight = this.fontHeightInPixels;
		var textMarginTop = (size.y - textHeight) / 2;

		this.graphics.fillStyle = colorsForeAndBack[0];
		this.graphics.fillText
		(
			text,
			pos.x + textMarginLeft,
			pos.y + textMarginTop + textHeight
		);
	}

	DisplayHelper.prototype.drawControlContainer = function(container, pos)
	{
		var size = container.size;

		this.drawRectangle
		(
			pos, 
			size, 
			container.colorsForeAndBack[0], 
			container.colorsForeAndBack[1]
		)

		var children = container.children;
		for (var i = 0; i < children.length; i++)
		{
			var child = children[i];
			child.draw
			(
				pos.clone().add(child.pos)
			);
		}
	}

	DisplayHelper.prototype.drawControlImage = function(controlImage, pos)
	{
		var size = controlImage.size;

		this.drawRectangle
		(
			pos, size, "Gray", "White"
		)

		this.graphics.drawImage
		(
			controlImage.systemImage,
			pos.x, pos.y,
			this.viewSize.x, this.viewSize.y
		);
	}

	DisplayHelper.prototype.drawControlLabel = function(control, pos)
	{
		var size = control.size;
		var text = control.text();

		var textHeight = this.fontHeightInPixels;

		var textMargins;

		if (control.isTextCentered == true)
		{
			var textWidth = this.graphics.measureText(text).width;
			textMargins = new Coords
			(
				(size.x - textWidth) / 2,
				(size.y - textHeight) / 2
			);
		}
		else
		{
			textMargins = new Coords
			(
				2,
				(size.y - textHeight) / 2
			);
		}

		this.graphics.fillStyle = "Gray";
		this.graphics.fillText
		(
			text,
			pos.x + textMargins.x ,
			pos.y + textMargins.y + textHeight
		);				
	}

	DisplayHelper.prototype.drawControlList = function(list, pos)
	{
		var size = list.size;

		this.drawRectangle
		(
			pos, size, "Gray", "White"
		);

		this.graphics.fillStyle = "Gray";
		var itemSpacing = 12; // hack
		var itemSizeY = itemSpacing;
		var textMarginLeft = 2;
		var itemPosY = pos.y;

		var items = list.options();
		var numberOfItems = (items == null ? 0 : items.length);

		var numberOfItemsVisible = Math.floor(size.y / itemSizeY);
		var indexStart = list.indexOfFirstOptionVisible;
		var indexEnd = indexStart + numberOfItemsVisible - 1;
		if (indexEnd >= numberOfItems)
		{
			indexEnd = numberOfItems - 1;
		}

		var optionSelected = list.optionSelected();

		var scrollbar = list.scrollbar;
		var scrollbarSize = scrollbar.size;
		var textAreaSize = new Coords(size.x - size.y, size.y);

		for (var i = indexStart; i <= indexEnd; i++)
		{
			var item = items[i];

			if (item == optionSelected)
			{
				this.graphics.fillStyle = "Gray";
				this.graphics.fillRect

				(
					pos.x + textMarginLeft, 
					itemPosY,
					size.x - textMarginLeft * 2,
					itemSizeY
				);
				this.graphics.fillStyle = "White";
			}
			else
			{
				this.graphics.fillStyle = "Gray";
			}

			var text = DataBinding.get
			(
				item, list.bindingExpressionForOptionText
			);

			itemPosY += itemSizeY;


			this.graphics.fillText
			(
				text,
				pos.x + textMarginLeft,
				itemPosY
			);			
		}

		this.drawControlScrollbar
		(
			scrollbar, 
			scrollbar.pos.clone().add(pos)
		);
	}

	DisplayHelper.prototype.drawControlScrollbar = function(control, pos)
	{
		var size = control.size;

		this.drawRectangle
		(
			pos,
			control.size,			
			"White", "Gray"
		);

		var buttonSize = new Coords(size.x, size.x);
		var buttonPositions = 
		[
			pos,
			new Coords
			(
				pos.x,
				pos.y + size.y - buttonSize.y
			)
		];

		var verticesForButtonArrow = 
		[
			new Coords(1/3, 2/3),
			new Coords(2/3, 2/3),
			new Coords(1/2, 1/3),
		];

		var vertexPos = new Coords(0, 0, 0);
		var ones = new Coords(1, 1, 1);

		for (var b = 0; b < buttonPositions.length; b++)
		{
			var buttonPos = buttonPositions[b];

			this.drawRectangle
			(
				buttonPos,
				buttonSize,			
				"Gray", "White"
			);
	
			this.graphics.beginPath();

			for (var v = 0; v < verticesForButtonArrow.length; v++)
			{
				vertexPos.overwriteWith(verticesForButtonArrow[v]);

				if (b == 1)
				{
					vertexPos.multiplyScalar(-1).add
					(
						ones
					);
				}

				vertexPos.multiply
				(
					buttonSize
				).add
				(
					buttonPos
				);

				if (v == 0)
				{
					this.graphics.moveTo
					(
						vertexPos.x, vertexPos.y
					);
				}
				else
				{
					this.graphics.lineTo
					(
						vertexPos.x, vertexPos.y
					);
				}
			}
			this.graphics.closePath();
			this.graphics.stroke();
		}

		var value = control.value();
		var min = control.min();
		var range = control.range();

		var sliderSize = new Coords(size.x, size.x);
		var slideRangeInPixels = size.y - buttonSize.y * 2 - sliderSize.y;

		var sliderPos = new Coords
		(
			0, 
			buttonSize.y + ((value - min) / range) * slideRangeInPixels
		).add
		(
			pos
		);

		this.drawRectangle
		(
			sliderPos,
			sliderSize,			
			"Gray", "White"
		);
	}

	DisplayHelper.prototype.drawControlSelect = function(control, pos)
	{
		var size = control.size;

		var colorsForeAndBack = ["Gray", "White"];

		this.drawRectangle
		(
			pos, size, 
			colorsForeAndBack[0], colorsForeAndBack[1]
		)

		var optionSelected = control.optionSelected();
		var text;
		if (optionSelected == null)
		{
			text = "[none]";
		}
		else
		{
			text = DataBinding.get(optionSelected, control.bindingExpressionForOptionText);
		}
		
		var textWidth = this.graphics.measureText(text).width;
		var textMarginLeft = (control.size.x - textWidth) / 2;
		var textHeight = this.fontHeightInPixels;
		var textMarginTop = (control.size.y - textHeight) / 2;

		this.graphics.fillStyle = colorsForeAndBack[0];
		this.graphics.fillText
		(
			text,
			pos.x + textMarginLeft,
			pos.y + textMarginTop + textHeight
		);
	}


	DisplayHelper.prototype.drawControlTextBox = function(control, pos)
	{
		var size = control.size;

		var text = control.text();
		var colorsForeAndBack;

		if (control.isHighlighted == true)
		{
			colorsForeAndBack = ["White", "Gray"];
			text += "|";
		}
		else
		{
			colorsForeAndBack = ["Gray", "White"];
		}

		this.drawRectangle
		(
			pos, size, 
			colorsForeAndBack[0], colorsForeAndBack[1]
		)

		var textWidth = this.graphics.measureText(text).width;
		var textMarginLeft = (size.x - textWidth) / 2;
		var textHeight = this.fontHeightInPixels;		
		var textMarginTop = (size.y - textHeight) / 2;

		this.graphics.fillStyle = colorsForeAndBack[0];
		this.graphics.fillText
		(
			text,
			pos.x + textMarginLeft ,
			pos.y + textMarginTop + textHeight
		);				
	}

	DisplayHelper.prototype.drawNetworkForCamera = function(network, camera)
	{
		var drawPos = this.drawPos;
		var drawPosFrom = new Coords(0, 0, 0);
		var drawPosTo = new Coords(0, 0, 0);

		var cameraPos = camera.loc.pos;

		var networkNodes = network.nodes;
		var numberOfNetworkNodes = networkNodes.length;

		var networkLinks = network.links;
		var numberOfNetworkLinks = networkLinks.length;

		this.graphics.fillStyle = "rgba(128, 128, 128, .1)";

		var nodeRadiusActual = NetworkNode.RadiusActual;

		for (var i = 0; i < numberOfNetworkLinks; i++)
		{
			var link = networkLinks[i];
			this.drawNetworkForCamera_Link
			(
				network, camera, link, 
				nodeRadiusActual, drawPos, 
				drawPosFrom, drawPosTo
			);
		}

		for (var i = 0; i < numberOfNetworkNodes; i++)
		{
			var node = networkNodes[i];
			this.drawNetworkForCamera_Node
			(
				node, nodeRadiusActual, camera, drawPos
			);
		}
	}

	DisplayHelper.prototype.drawNetworkForCamera_Link = function
	(
		network, 
		camera, 
		link,
		nodeRadiusActual, 
		drawPos, 
		drawPosFrom, 
		drawPosTo
	)
	{
		var nodesLinked = link.nodesLinked(network);
		var nodeFromPos = nodesLinked[0].loc.pos;
		var nodeToPos = nodesLinked[1].loc.pos;

		camera.convertWorldCoordsToViewCoords
		(
			drawPosFrom.overwriteWith(nodeFromPos)
		);
		
		camera.convertWorldCoordsToViewCoords
		(
			drawPosTo.overwriteWith(nodeToPos)
		);

		var directionFromNode0To1InView = drawPosTo.clone().subtract
		(
			drawPosFrom
		).normalize();

		var perpendicular = directionFromNode0To1InView.clone().perpendicular2D();

		var perspectiveFactorFrom = 
			camera.focalLength / drawPosFrom.z;
		var perspectiveFactorTo = 
			camera.focalLength / drawPosTo.z;
			
		var radiusApparentFrom = 
			nodeRadiusActual * perspectiveFactorFrom;
		var radiusApparentTo = 
			nodeRadiusActual * perspectiveFactorTo;

		this.graphics.beginPath();
		this.graphics.moveTo(drawPosFrom.x, drawPosFrom.y);
		this.graphics.lineTo(drawPosTo.x, drawPosTo.y);
		this.graphics.lineTo
		(
			drawPosTo.x + perpendicular.x * radiusApparentTo,
			drawPosTo.y + perpendicular.y * radiusApparentTo
		);
		this.graphics.lineTo
		(
			drawPosFrom.x + perpendicular.x * radiusApparentFrom, 
			drawPosFrom.y + perpendicular.y * radiusApparentFrom
		);
		this.graphics.fill();

		for (var i = 0; i < link.ships.length; i++)
		{
			var ship = link.ships[i];
			this.drawNetworkForCamera_Link_Ship
			(
				camera, link, ship, drawPos, nodeFromPos, nodeToPos
			);
		}
	}

	DisplayHelper.prototype.drawNetworkForCamera_Link_Ship = function
	(
		camera, link, ship, drawPos, nodeFromPos, nodeToPos
	)
	{
		var forward = link.direction();
		var linkLength = link.length();

		var fractionOfLinkTraversed = ship.loc.pos.x / linkLength; 

		if (ship.vel.x < 0)
		{
			fractionOfLinkTraversed = 1 - fractionOfLinkTraversed;
			forward.multiplyScalar(-1);
		}

		drawPos.overwriteWith
		(
			nodeFromPos
		).multiplyScalar
		(
			1 - fractionOfLinkTraversed
		).add
		(
			nodeToPos.clone().multiplyScalar
			(
				fractionOfLinkTraversed
			)
		);

		var right = forward.clone().perpendicular3D();
		var down = right.clone().crossProduct(forward);

		var scaleFactorLength = 8;
		var scaleFactorBase = scaleFactorLength / 3;
		forward.multiplyScalar(scaleFactorLength);
		right.multiplyScalar(scaleFactorBase);
		down.multiplyScalar(scaleFactorBase);

		var vertices = 
		[
			drawPos.clone().add(forward),
			drawPos.clone().add(right).add(down),
			drawPos.clone().add(right).subtract(down),
			drawPos.clone().subtract(right).subtract(down),
			drawPos.clone().subtract(right).add(down),
		];

		var vertexIndicesForFaces =
		[
			[ 0, 1, 2 ],
			[ 0, 2, 3 ],
			[ 0, 3, 4 ],
			[ 0, 4, 1 ],
		];

		this.graphics.strokeStyle = ship.faction().color.systemColor;
		
		for (var f = 0; f < vertexIndicesForFaces.length; f++)
		{
			var vertexIndicesForFace = vertexIndicesForFaces[f];

			this.graphics.beginPath();
	
			for (var vi = 0; vi < vertexIndicesForFace.length; vi++)
			{
				var vertexIndex = vertexIndicesForFace[vi];
				drawPos.overwriteWith(vertices[vertexIndex]);
				camera.convertWorldCoordsToViewCoords(drawPos);
				var moveToOrLineTo = (vi == 0 ? this.graphics.moveTo : this.graphics.lineTo);
				moveToOrLineTo.call(this.graphics, drawPos.x, drawPos.y);
			}

			this.graphics.closePath();
			this.graphics.stroke();
		}
	}

	DisplayHelper.prototype.drawNetworkForCamera_Node = function
	(
		node, nodeRadiusActual, camera, drawPos
	)
	{
		var nodePos = node.loc.pos;

		drawPos.overwriteWith(nodePos);
		camera.convertWorldCoordsToViewCoords(drawPos);

		var perspectiveFactor = camera.focalLength / drawPos.z;
		var radiusApparent = nodeRadiusActual * perspectiveFactor;

		var alpha = Math.pow(perspectiveFactor, 4); // hack

		//var nodeColor = node.defn.color.systemColor;
		var nodeColor = "rgba(128, 128, 128, " + alpha + ")"

		this.graphics.strokeStyle = nodeColor; 
		this.graphics.fillStyle = nodeColor;

		this.graphics.beginPath();
		this.graphics.arc
		(
			drawPos.x, drawPos.y, 
			radiusApparent, 
			0, 2 * Math.PI, // start and stop angles 
			false // counterClockwise
		);
		this.graphics.stroke();

		this.graphics.fillText
		(
			node.starsystem.name, 
			(drawPos.x + radiusApparent), 
			drawPos.y
		);
	}

	DisplayHelper.prototype.drawLayout = function(layout)
	{
		this.clear();
		this.drawMap(layout.map);
	}

	DisplayHelper.prototype.drawMap = function(map)
	{
		var pos = map.pos;
		var mapSizeInCells = map.sizeInCells;

		var cellPos = new Coords(0, 0);
		var drawPos = this.drawPos;
		var cellSizeInPixels = map.cellSizeInPixels;
		var cellSizeInPixelsHalf = 
			cellSizeInPixels.clone().divideScalar(2);

		for (var y = 0; y < mapSizeInCells.y; y++)
		{
			cellPos.y = y;

			for (var x = 0; x < mapSizeInCells.x; x++)
			{
				cellPos.x = x;

				drawPos.overwriteWith
				(
					cellPos
				).multiply
				(
					cellSizeInPixels
				).add
				(
					pos
				);

				var cell = map.cellAtPos(cellPos);
				var cellBody = cell.body;

				var colorFill = 
				(
					cellBody == null 
					? "Transparent" 
					: cellBody.defn.color
				);
				var colorBorder = cell.terrain.color;

				this.graphics.fillStyle = colorFill;
				this.graphics.fillRect
				(
					drawPos.x,
					drawPos.y,
					cellSizeInPixels.x,
					cellSizeInPixels.y
				);

				this.graphics.strokeStyle = colorBorder;
				this.graphics.strokeRect
				(
					drawPos.x,
					drawPos.y,
					cellSizeInPixels.x,
					cellSizeInPixels.y
				);

			}
		}

		var cursor = map.cursor;
		var cursorPos = cursor.pos;
		var cursorIsWithinMap = cursorPos.isWithinRangeMax
		(
			map.sizeInCellsMinusOnes
		);

		if (cursorIsWithinMap == true)
		{
			drawPos.overwriteWith
			(
				cursorPos
			).multiply
			(
				cellSizeInPixels
			).add
			(
				pos
			);			

			this.graphics.strokeStyle = "Cyan";

			if (cursor.bodyDefn == null)
			{
				this.graphics.beginPath();
				this.graphics.moveTo(drawPos.x, drawPos.y);
				this.graphics.lineTo
				(
					drawPos.x + cellSizeInPixels.x, 
					drawPos.y + cellSizeInPixels.y
				);
				this.graphics.moveTo
				(
					drawPos.x + cellSizeInPixels.x, 
					drawPos.y
				);
				this.graphics.lineTo
				(
					drawPos.x, 
					drawPos.y + cellSizeInPixels.y
				);
				this.graphics.stroke();
			}			
			else
			{
				this.graphics.fillStyle = cursor.bodyDefn.color;
				this.graphics.fillRect
				(
					drawPos.x,
					drawPos.y,
					cellSizeInPixels.x,
					cellSizeInPixels.y
				);
			}

			this.graphics.strokeRect
			(
				drawPos.x,
				drawPos.y,
				cellSizeInPixels.x,
				cellSizeInPixels.y
			);
		}
	}

	DisplayHelper.prototype.drawRectangle = function
	(
		pos, size, colorBorder, colorBack
	)
	{
		this.graphics.fillStyle = colorBack;
		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
			);
		}
	}

	DisplayHelper.prototype.drawStarsystemForCamera = function(starsystem, camera)
	{
		var cameraViewSize = camera.viewSize;
		var cameraPos = camera.loc.pos;

		var drawPos = this.drawPos;
		var drawPosFrom = new Coords(0, 0, 0);
		var drawPosTo = new Coords(0, 0, 0);

		var gridCellSizeInPixels = new Coords(10, 10, 0);
		var gridSizeInCells = new Coords(40, 40, 0); 
		var gridSizeInPixels = gridSizeInCells.clone().multiply
		(
			gridCellSizeInPixels
		);
		var gridSizeInCellsHalf = gridSizeInCells.clone().divideScalar(2);
		var gridSizeInPixelsHalf = gridSizeInPixels.clone().divideScalar(2);

		var graphics = this.graphics;

		graphics.strokeStyle = Color.Instances.CyanHalfTranslucent.systemColor;

		for (var d = 0; d < 2; d++)
		{
			var multiplier = new Coords(0, 0, 0);
			multiplier.dimension_Set(d, gridCellSizeInPixels.dimension(d));

			for (var i = 0 - gridSizeInCellsHalf.x; i <= gridSizeInCellsHalf.x; i++)			
			{
				drawPosFrom.overwriteWith
				(
					gridSizeInPixelsHalf
				).multiplyScalar(-1);
				drawPosTo.overwriteWith(gridSizeInPixelsHalf);

				drawPosFrom.dimension_Set(d, 0);
				drawPosTo.dimension_Set(d, 0);

				drawPosFrom.add(multiplier.clone().multiplyScalar(i));
				drawPosTo.add(multiplier.clone().multiplyScalar(i));

				camera.convertWorldCoordsToViewCoords(drawPosFrom);
				camera.convertWorldCoordsToViewCoords(drawPosTo);

				graphics.beginPath();
				graphics.moveTo(drawPosFrom.x, drawPosFrom.y);
				graphics.lineTo(drawPosTo.x, drawPosTo.y);
				graphics.stroke();
			}
		}

		var bodiesByType =
		[
			[ starsystem.star ],
			starsystem.linkPortals,
			starsystem.planets,
			starsystem.ships,
		];

		for (var t = 0; t < bodiesByType.length; t++)
		{
			var bodies = bodiesByType[t];

			for (var i = 0; i < bodies.length; i++)
			{
				var body = bodies[i];
				this.drawStarsystemForCamera_Body
				(
					camera, 
					body
				);
			}

		}
	}

	DisplayHelper.prototype.drawStarsystemForCamera_Body = function(camera, body)
	{
		var graphics = this.graphics;
		var drawPos = this.drawPos;

		var bodyDefn = body.defn;
		var bodySize = bodyDefn.size;
		var bodySizeHalf = bodyDefn.sizeHalf;

		var bodyPos = body.loc.pos;
		drawPos.overwriteWith(bodyPos);
		camera.convertWorldCoordsToViewCoords(drawPos);

		bodyDefn.visual.draw(drawPos);

		if (bodyPos.z < 0)
		{
			graphics.strokeStyle = Color.Instances.Green.systemColor;
		}
		else
		{
			graphics.strokeStyle = Color.Instances.Red.systemColor;
		}

		graphics.beginPath();
		graphics.moveTo(drawPos.x, drawPos.y);

		drawPos.overwriteWith(bodyPos);
		drawPos.z = 0;
		camera.convertWorldCoordsToViewCoords(drawPos);

		graphics.lineTo(drawPos.x, drawPos.y);
		graphics.stroke();			
	}

	DisplayHelper.prototype.hide = function()
	{
		Globals.Instance.divMain.removeChild(this.canvasLive);		
	}

	DisplayHelper.prototype.initialize = function()
	{
		this.canvasBuffer = document.createElement("canvas");
		this.canvasBuffer.width = this.viewSize.x;
		this.canvasBuffer.height = this.viewSize.y;

		this.graphics = this.canvasBuffer.getContext("2d");
		this.graphics.font = 
			"" + this.fontHeightInPixels + "px sans-serif";

		// hack - double-buffering test
		this.canvasLive = this.canvasBuffer;
		this.graphicsLive = this.graphics;

		this.drawPos = new Coords(0, 0, 0);

		Globals.Instance.divMain.appendChild(this.canvasLive);
	}

	DisplayHelper.prototype.refresh = function()
	{
		this.graphicsLive.drawImage(this.canvasBuffer, 0, 0);
	}

	DisplayHelper.prototype.show = function()
	{
		Globals.Instance.divMain.appendChild(this.canvasLive);
	}
}

function Faction(name, color, relationships, technology, planets, ships, knowledge)
{
	this.name = name;
	this.color = color;
	this.relationships = relationships;
	this.technology = technology;
	this.planets = planets;
	this.ships = ships;
	this.knowledge = knowledge;

	this.notifications = 
	[
		new Notification("Default", "This is a test."),
	];
}
{
	// static methods

	// controls

	Faction.controlBuild_Intelligence = function(diplomaticSession, pos, containerSize)
	{
		var margin = 10;
		var controlSpacing = 20;
		var listWidth = 100;
		var columnWidth = 60;

		var returnValue = new ControlContainer
		(
			"containerFactionIntelligence",
			ControlBuilder.ColorsForeAndBackDefault,
			pos,
			containerSize,
			// children
			[
				new ControlLabel
				(
					"labelFaction",
					new Coords(margin, margin), // pos
					new Coords(columnWidth, controlSpacing), // size
					false, // isTextCentered
					new DataBinding("Faction:")
				),

				new ControlLabel
				(
					"textFaction",
					new Coords(margin + columnWidth, margin), // pos
					new Coords(columnWidth, controlSpacing), // size,
					false, // isTextCentered
					new DataBinding(diplomaticSession, "factionSelected.name")
				),

				new ControlLabel
				(
					"labelRelationship",
					new Coords(margin, margin + controlSpacing), // pos
					new Coords(columnWidth, controlSpacing), // size
					false, // isTextCentered
					new DataBinding("Relationship:")
				),

				new ControlLabel
				(
					"textRelationship",
					new Coords(margin + columnWidth, margin + controlSpacing), // pos
					new Coords(columnWidth, controlSpacing), // size,
					false, // isTextCentered
					new DataBinding("[relationship]")
				),

				new ControlLabel
				(
					"labelPlanets",
					new Coords(margin, margin + controlSpacing * 2), // pos
					new Coords(columnWidth, controlSpacing), // size
					false, // isTextCentered
					new DataBinding("Planets:")
				),

				new ControlSelect
				(
					"listPlanets",
					new Coords(margin, margin + controlSpacing * 3), // pos
					new Coords(listWidth, controlSpacing * 4), // size
					// dataBindingForValueSelected
					new DataBinding(diplomaticSession, "factionSelected.planetSelected"), 
					new DataBinding(diplomaticSession, "factionSelected.planets"), // options
					null, // bindingExpressionForOptionValues
					"name", // bindingExpressionForOptionText,
					new DataBinding(true), // isEnabled
					6 // numberOfItemsVisible
				),

				new ControlLabel
				(
					"labelShips",
					new Coords(margin, margin + controlSpacing * 7), // pos
					new Coords(columnWidth, controlSpacing), // size
					false, // isTextCentered
					new DataBinding("Ships:")
				),

				new ControlSelect
				(
					"listShips",
					new Coords(margin, margin + controlSpacing * 8), // pos
					new Coords(listWidth, controlSpacing * 4), // size
					// dataBindingForValueSelected
					new DataBinding(diplomaticSession, "factionSelected.shipSelected"), 
					new DataBinding(diplomaticSession, "factionSelected.ships"), // options
					null, // bindingExpressionForOptionValues
					"name", // bindingExpressionForOptionText,
					new DataBinding(true), // isEnabled
					6 // numberOfItemsVisible
				),
			]
		);

		return returnValue;
	}

	// instance methods

	Faction.prototype.researchInitialize = function()
	{
		var researchSession = new TechnologyResearchSession
		(
			Globals.Instance.universe.technologyTree,
			this.technology
		);
		var venueNext = new VenueTechnologyResearchSession(researchSession);
		venueNext = new VenueFader(venueNext);
		var universe = Globals.Instance.universe;
		universe.venueNext = venueNext;
	}

	Faction.prototype.toString = function()
	{
		return this.name;
	}

	// controls

	Faction.prototype.controlBuild = function
	(
		containerMainSize,
		containerInnerSize, 
		margin, 
		controlHeight,
		buttonWidth
	)
	{
		var returnValue = new ControlContainer
		(
			"containerFaction",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords
			(
				containerMainSize.x 
					- margin 
					- containerInnerSize.x, 
				margin
			),
			containerInnerSize,
			// children
			[

				new ControlLabel
				(
					"textBoxFaction",
					new Coords(margin, 0),// pos
					new Coords
					(
						containerInnerSize.x 
							- 40 - margin * 3, 
						controlHeight
					), // size
					false, // isTextCentered
					new DataBinding(this.name)
				),

				new ControlButton
				(
					"buttonTechnology",
					new Coords(margin, controlHeight), // pos
					new Coords(buttonWidth, controlHeight), // size
					"Tech",
					null, // dataBindingForIsEnabled
					this.researchInitialize.bind(this)
				),

				new ControlButton
				(
					"buttonRelations",
					new Coords
					(
						margin * 2 + buttonWidth, 
						controlHeight
					), // pos
					new Coords(buttonWidth, controlHeight), // size
					"Others",
					null, // dataBindingForIsEnabled
					// click
					this.relationsInitialize.bind(this)
				),

				new ControlButton
				(
					"buttonNotifications",
					new Coords
					(
						margin, 
						controlHeight * 2
					), // pos
					new Coords(buttonWidth, controlHeight), // size
					"Notes",
					null, // dataBindingForIsEnabled
					// click
					this.notificationSessionInitialize.bind(this)
				),
			]
		);

		return returnValue;
	}

	// diplomacy

	Faction.prototype.allianceProposalAcceptFrom = function(factionOther)	
	{
		return true;
	}

	Faction.prototype.allies = function()
	{
		return this.factionsMatchingRelationshipState
		(
			Relationship.States.Alliance
		);
	}

	Faction.prototype.enemies = function()
	{
		return this.factionsMatchingRelationshipState
		(
			Relationship.States.War
		);
	}

	Faction.prototype.factionsMatchingRelationshipState = function(stateToMatch)
	{
		var returnValues = [];	

		for (var i = 0; i < this.relationships.length; i++)
		{
			var relationship = this.relationships[i];
			if (relationship.state == stateToMatch)
			{
				var factionOther = relationship.factionOther();
				returnValues.push(factionOther);
			}
		}

		return returnValues;
	}

	Faction.prototype.notificationSessionInitialize = function()
	{
		var universe = Globals.Instance.universe;
		var notificationSession = new NotificationSession(this.notifications);
		var notificationSessionAsControl = notificationSession.controlBuild();
		var venueNext = new VenueControls(notificationSessionAsControl);
		venueNext = new VenueFader(venueNext);
		universe.venueNext = venueNext;
	}

	Faction.prototype.peaceOfferAcceptFrom = function(factionOther)	
	{
		return true;
	}

	Faction.prototype.relationsInitialize = function()
	{
		var universe = Globals.Instance.universe;
		var world = universe.world;
		var factionCurrent = world.factionCurrent();
		var factionsOther = world.factionsOtherThanCurrent();
		var diplomaticSession = DiplomaticSession.buildExample
		(
			factionCurrent,
			factionsOther
		);
		var diplomaticSessionAsControl = diplomaticSession.controlBuild();
		var venueNext = new VenueControls(diplomaticSessionAsControl, universe.venueCurrent);
		venueNext = new VenueFader(venueNext);
		universe.venueNext = venueNext;
	}

	Faction.prototype.selfAndAllies = function()
	{
		var returnValues = this.factionsMatchingRelationshipState
		(
			Relationship.States.Alliance
		);

		returnValues.push(this);

		return returnValues;
	}

	Faction.prototype.strength = function()
	{
		var returnValue = 0;

		var ships = this.ships;
		for (var i = 0; i < ships.length; i++)
		{
			var ship = ships[i];
			returnValue += ship.strength();
		}

		var planets = this.planets;
		for (var i = 0; i < planets.length; i++)
		{
			var planet = planets[i];
			returnValue += planet.strength();
		}

		this.technologyResearcher.strength();

		return returnValue;
	}

	Faction.prototype.warThreatOfferConcessionsTo = function(factionOther)	
	{
		return true;
	}

	// turns

	Faction.prototype.updateForTurn = function()
	{
		for (var i = 0; i < this.planets.length; i++)
		{
			var planet = this.planets[i];
			planet.updateForTurn();
		}

		for (var i = 0; i < this.ships.length; i++)
		{
			var ship = this.ships[i];
			ship.updateForTurn();
		}

		this.technology.updateForTurn();
	}
}

function FactionKnowledge(starsystems, links)
{
	this.starsystems = starsystems;
	this.links = links;
}

function Globals()
{}
{
	// instance

	Globals.Instance = new Globals();

	// instance methods

	Globals.prototype.handleEventTimerTick = function()
	{	
		this.universe.updateForTimerTick();
	}

	Globals.prototype.initialize = function
	(
		programName,
		millisecondsPerTimerTick, 
		viewSize, 
		universe,
		sounds,
		videos
	)
	{
		this.programName = programName;
		
		this.serializer = new Serializer
		([
			Body,
			BodyDefn,
			Camera,
			Color,
			Coords,
			Cursor,
			DateTime,
			Device,
			DeviceDefn,
			DiplomaticRelationship,
			Faction,
			Layout,
			LayoutElement,
			LayoutElementDefn,
			LinkPortal,
			Map,
			MapCell,
			MapCursor,
			MapTerrain,
			Network,
			NetworkLink,
			NetworkNode,
			NetworkNodeDefn,
			Orientation,
			Planet,
			PlanetDemographics,
			PlanetIndustry,
			Profile,
			Ship,
			Starsystem,
			TechnologyResearcher,
			World,
			Universe,
			VisualGroup,
			VisualRectangle,
			VisualSphere,
			VisualText,
		]);
		this.displayHelper = new DisplayHelper(viewSize, 10);
		this.inputHelper = new InputHelper();
		this.profileHelper = new ProfileHelper();
		this.soundHelper = new SoundHelper(sounds);
		this.videoHelper = new VideoHelper(videos);

		this.universe = universe;

		var divMain = document.createElement("div");
		divMain.style.position = "absolute";
		divMain.style.left = "50%";
		divMain.style.top = "50%";
		divMain.style.marginTop = 0 - viewSize.x / 2;
		divMain.style.marginLeft = 0 - viewSize.y / 2;
		document.body.appendChild(divMain);
		this.divMain = divMain;

		this.displayHelper.initialize();
		this.inputHelper.initialize();
		this.universe.initialize();

		this.timer = setInterval
		(
			this.handleEventTimerTick.bind(this),
			millisecondsPerTimerTick
		);
	}

	Globals.prototype.reset = function()
	{
		this.soundHelper.reset();
	}
}

function Gradient(stops)
{
	this.stops = stops;
}
{
	Gradient.prototype.toSystemGraphicsStyle = function(graphics, center, radius)
	{
		var returnValue = graphics.createRadialGradient
		(
			center.x,
			center.y,
			0, // startRadius
			center.x, 
			center.y,
			radius
		);

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

			returnValue.addColorStop(stop.position, stop.systemColor);
			returnValue.addColorStop(stop.position, stop.systemColor);
		}

		return returnValue;
	}
}

function GradientStop(position, systemColor)
{
	this.position = position;
	this.systemColor = systemColor;
}

function InputHelper()
{
	this.keyCodePressed = null;
	this.isShiftKeyPressed = false;
	this.isMouseLeftPressed = false;
	this.mouseClickPos = new Coords(0, 0);
	this.mouseMovePos = new Coords(0, 0);
	this.mouseMovePosPrev = new Coords(0, 0);

	this.tempPos = new Coords(0, 0);
}
{
	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		if (this.isEnabled == true)
		{
			event.preventDefault();
			this.isShiftKeyPressed = event.shiftKey;
			this.keyCodePressed = event.keyCode;
		}
	}

	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		this.keyCodePressed = null;
	}

	InputHelper.prototype.handleEventMouseDown = function(event)
	{
		if (this.isEnabled == true)
		{
			this.isMouseLeftPressed = true;
			this.mouseClickPos.overwriteWithDimensions
			(
				event.layerX,
				event.layerY,
				0
			);
		}
	}

	InputHelper.prototype.handleEventMouseMove = function(event)
	{
		if (this.isEnabled == true)
		{
			this.mouseMovePosPrev.overwriteWith
			(
				this.mouseMovePos
			);
		
			this.mouseMovePos.overwriteWithDimensions
			(
				event.layerX,
				event.layerY,
				0
			);
		}
	}

	InputHelper.prototype.handleEventMouseUp = function(event)
	{
		this.isMouseLeftPressed = false;
	}	

	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);
		var divMain = Globals.Instance.divMain;
		divMain.onmousedown = this.handleEventMouseDown.bind(this);
		divMain.onmousemove = this.handleEventMouseMove.bind(this);
		divMain.onmouseup = this.handleEventMouseUp.bind(this);

		this.isEnabled = true;
	}
}

function Layout(sizeInPixels, bodyDefns, map, bodies)
{
	this.sizeInPixels = sizeInPixels;
	this.bodyDefns = bodyDefns;
	this.map = map;
	this.bodies = bodies;

	for (var i = 0; i < this.bodies.length; i++)
	{
		var body = this.bodies[i];
		var bodyPosInCells = body.loc.pos;
		var cell = this.map.cellAtPos(bodyPosInCells);
		cell.body = body;
	}
}
{
	// static methods

	Layout.generateRandom = function(parent)
	{
		var terrains = 
		[
			new MapTerrain("Default", ".", "LightGray", false),
		];
		terrains.addLookups("codeChar");
	
		var viewSize = Globals.Instance.displayHelper.viewSize;
		var mapSizeInPixels = viewSize.clone().multiplyScalar(.5);
		var mapPosInPixels = viewSize.clone().subtract
		(
			mapSizeInPixels
		).divideScalar
		(
			2
		);
		mapPosInPixels.z = 0;
	
		var map = Map.fromCellsAsStrings
		(
			mapSizeInPixels,
			mapPosInPixels,
			terrains,
			// cellsAsStrings
			[
				"........",
				"........",
				"........",
				"........",
				"........",
				"........",
				"........",
				"........",
			]
		);

		var bodyDefns = 
		[
			new LayoutElementDefn("Default", "Gray", 10),
			new LayoutElementDefn("Factory", "Red", 20),
			new LayoutElementDefn("Farm", "Green", 30),
			new LayoutElementDefn("Laboratory", "Blue", 40),
		];
		bodyDefns.addLookups("name");

		var layout = new Layout
		(
			viewSize.clone(), // sizeInPixels
			bodyDefns,
			map,
			// bodies
			[
				new LayoutElement(bodyDefns["Default"], new Coords(0, 0))
			] 
		);

		return layout;
	}

	// instance methods

	Layout.prototype.elementAdd = function(elementToAdd)
	{
		this.bodies.push(elementToAdd);
		this.map.cellAtPos(elementToAdd.loc.pos).body = elementToAdd;
	}

	Layout.prototype.elementRemove = function(elementToRemove)
	{
		this.bodies.splice(this.bodies.indexOf(elementToRemove), 1);
		var elementPos = elementToRemove.loc.pos;
		var cell = this.map.cellAtPos(elementPos);
		cell.body = null;
	}

	Layout.prototype.updateForTurn = function()
	{
		var cells = this.map.cells;
		for (var i = 0; i < cells.length; i++)
		{
			var cell = cells[i];
			var cellBody = cell.body;
			if (cellBody != null)
			{
				if (cellBody.updateForTurn != null)
				{
					cellBody.updateForTurn();
				}
			}
		}

	}
}

function LayoutElement(defn, pos)
{
	this.defn = defn;
	this.loc = new Location(pos);
}

function LayoutElementDefn(name, color, industryToBuild)
{
	this.name = name;
	this.color = color;
	this.industryToBuild = industryToBuild;
}

function LinkPortal(name, defn, pos, starsystemNamesFromAndTo)
{
	this.name = name;
	this.defn = defn;
	this.loc = new Location(pos);

	this.starsystemNamesFromAndTo = starsystemNamesFromAndTo;
}
{
	LinkPortal.prototype.link = function()
	{
		// todo
	}

	LinkPortal.prototype.starsystemNameTo = function()
	{
		return this.starsystemNamesFromAndTo[0];
	}

	// controls

	LinkPortal.prototype.controlBuild_Selection = function()
	{
		var returnValue = new ControlLabel
		(
			"labelLinkPortalAsSelection",
			new Coords(0, 0),
			new Coords(0, 0), // this.size
			false, // isTextCentered
			new DataBinding("Link to " + this.starsystemNamesFromAndTo[1])
		);

		return returnValue;
	}
}

function Location(pos)
{
	this.pos = pos;
}
{
	Location.prototype.clone = function()
	{
		return new Location(this.pos.clone());
	}

	Location.prototype.equals = function(other)
	{
		var returnValue = 
		(
			this.pos.equals(other.pos)
		);
		return returnValue;
	}
}

function Map(sizeInPixels, sizeInCells, pos, terrains, cells)
{
	this.sizeInPixels = sizeInPixels;
	this.sizeInCells = sizeInCells;
	this.pos = pos;
	this.terrains = terrains;
	this.cells = cells;

	this.sizeInCellsMinusOnes = 
		this.sizeInCells.clone().subtract
		(
			new Coords(1, 1, 1)
		);
	this.cellSizeInPixels = 
		this.sizeInPixels.clone().divide
		(
			this.sizeInCells
		);

	this.cursor = new MapCursor(null, new Coords(0, 0, 0));
}
{
	// static methods
	
	Map.fromCellsAsStrings = function
	(
		sizeInPixels, pos, terrains, cellsAsStrings
	)
	{
		var sizeInCells = new Coords
		(
			cellsAsStrings[0].length,
			cellsAsStrings.length,
			1
		);

		var cells = [];

		var cellPos = new Coords(0, 0, 0);

		for (var y = 0; y < sizeInCells.y; y++)
		{
			cellPos.y = y;
			cellRowAsString = cellsAsStrings[y];

			for (var x = 0; x < sizeInCells.x; x++)
			{
				cellPos.x = x;
				var cellAsChar = cellRowAsString[x];
				var cellTerrain = terrains[cellAsChar];

				var cell = new MapCell
				(
					cellPos.clone(),
					cellTerrain,
					null // body
				);

				cells.push(cell);
			}
		}

		var returnValue = new Map
		(
			sizeInPixels,
			sizeInCells,
			pos,
			terrains,
			cells
		);

		return returnValue;
	}

	// instance methods

	Map.prototype.cellAtPos = function(cellPos)
	{
		var cellIndex = this.indexOfCellAtPos(cellPos);
		var returnValue = this.cells[cellIndex];
		return returnValue;
	}

	Map.prototype.indexOfCellAtPos = function(cellPos)
	{
		return cellPos.y * this.sizeInCells.x + cellPos.x;
	}
}

function MapCell(pos, terrain, body)
{
	this.pos = pos;
	this.terrain = terrain;
	this.body = body;
}

function MapCursor(bodyDefn, pos)
{
	this.bodyDefn = bodyDefn;
	this.pos = pos;
}

function MapTerrain(name, codeChar, color, isBlocked)
{
	this.name = name
	this.codeChar = codeChar;
	this.color = color;
	this.isBlocked = isBlocked;
}

function NameGenerator()
{
	// static class
}
{
	NameGenerator.generateName = function()
	{
		var returnValue = "";

		var numberOfSyllablesMin = 2;
		var numberOfSyllablesMax = 3;
		var numberOfSyllablesRange = 
			numberOfSyllablesMax - numberOfSyllablesMin;
		
		var numberOfSyllables = 
			numberOfSyllablesMin
			+ Math.floor
			(
				Math.random() * numberOfSyllablesRange
			);

		var consonants = "bdfghjklmnprstvwxyz";
		var vowels = "aeiou";		

		for (var s = 0; s < numberOfSyllables; s++)
		{
			var syllable = 
				consonants[Math.floor(Math.random() * consonants.length)]
				+ vowels[Math.floor(Math.random() * vowels.length)]
				+ consonants[Math.floor(Math.random() * consonants.length)];

			returnValue += syllable;
		}

		returnValue = returnValue[0].toUpperCase() + returnValue.substr(1);

		return returnValue;
	}
}

function Network(name, nodes, links)
{
	this.name = name;
	this.nodes = nodes;
	this.links = links;

	this.nodes.addLookups("name");

	for (var i = 0; i < this.links.length; i++)
	{
		var link = this.links[i];
		var namesOfNodesLinked = link.namesOfNodesLinked;

		for (var n = 0; n < namesOfNodesLinked.length; n++)
		{
			var nameOfNodeFrom = namesOfNodesLinked[n];
			var nameOfNodeTo = namesOfNodesLinked[1 - n];

			var linksOriginatingAtNodeFrom = this.links[nameOfNodeFrom];
			if (linksOriginatingAtNodeFrom == null)
			{
				linksOriginatingAtNodeFrom = [];
				this.links[nameOfNodeFrom] = linksOriginatingAtNodeFrom;
			}

			linksOriginatingAtNodeFrom[nameOfNodeTo] = link;
		}
	}
}
{
	Network.generateRandom = function
	(
		name, 
		nodeDefns, 
		numberOfNodes, 
		minAndMaxDistanceOfNodesFromOrigin, 
		distanceBetweenNodesMin
	)
	{
		var nodesNotYetLinked = [];

		var radiusMinAndMax = minAndMaxDistanceOfNodesFromOrigin;
		var radiusMin = radiusMinAndMax[0]; 
		var radiusMax = radiusMinAndMax[1];
		var radiusRange = radiusMax - radiusMin; 

		var nodePos = new Coords(0, 0, 0);
		var displacementOfNodeNewFromOther = new Coords(0, 0, 0);
		var minusOnes = new Coords(-1, -1, -1);

		for (var i = 0; i < numberOfNodes; i++)
		{
			var distanceOfNodeNewFromExisting = 0;

			while (distanceOfNodeNewFromExisting < distanceBetweenNodesMin)
			{
				nodePos.randomize().multiplyScalar(2).add
				(
					minusOnes
				).normalize().multiplyScalar
				(
					radiusMin + radiusRange * Math.random()
				);

				distanceOfNodeNewFromExisting = distanceBetweenNodesMin;

				for (var j = 0; j < i; j++)
				{
					var nodeOtherPos = nodesNotYetLinked[j].loc.pos;
					 
					displacementOfNodeNewFromOther.overwriteWith
					(
						nodePos
					).subtract
					(
						nodeOtherPos
					);

					var distanceOfNodeNewFromOther = 
						displacementOfNodeNewFromOther.magnitude();

					if (distanceOfNodeNewFromOther < distanceBetweenNodesMin)
					{
						distanceOfNodeNewFromExisting = distanceOfNodeNewFromOther;
						break;
					}	

					
				}
			}

			var nodeDefnIndexRandom = Math.floor(nodeDefns.length * Math.random());
			var nodeDefn = nodeDefns[nodeDefnIndexRandom];
			var nodeStarsystem = Starsystem.generateRandom();	

			var node = new NetworkNode
			(
				nodeStarsystem.name,
				nodeDefn,
				nodePos.clone(),
				nodeStarsystem
			);

			nodesNotYetLinked.push(node);
		}

		var nodesLinked = [ nodesNotYetLinked[0] ];
		nodesNotYetLinked.splice(0, 1);
		var links = [];

		var bodyDefnLinkPortal = new BodyDefn
		(
			"LinkPortal", 
			new Coords(10, 10), // size
			new VisualGroup
			([
				new VisualSphere(Color.Instances.Gray, 10)
			])
		);

		var tempPos = new Coords(0, 0, 0);

		while (nodesLinked.length < numberOfNodes)
		{
			var nodePairClosestSoFar = null;
			var distanceBetweenNodePairClosestSoFar = minAndMaxDistanceOfNodesFromOrigin[1] * 4;

			for (var i = 0; i < nodesLinked.length; i++)
			{
				var nodeLinked = nodesLinked[i];
				var nodeLinkedPos = nodeLinked.loc.pos;

				for (var j = 0; j < nodesNotYetLinked.length; j++)
				{
					var nodeToLink = nodesNotYetLinked[j];

					var distanceBetweenNodes = tempPos.overwriteWith
					(
						nodeLinkedPos
					).subtract
					(
						nodeToLink.loc.pos
					).magnitude();

					if (distanceBetweenNodes <= distanceBetweenNodePairClosestSoFar)
					{
						distanceBetweenNodePairClosestSoFar = distanceBetweenNodes;
						nodePairClosestSoFar = [nodeToLink, nodeLinked];
					}
				}
			}

			var nodeToLink = nodePairClosestSoFar[0];
			var nodeLinked = nodePairClosestSoFar[1];

			var link = new NetworkLink
			([ 
				nodeToLink.name,
				nodeLinked.name
			]);
			var linkIndex = links.length;
			links.push(link);
			nodesLinked.push(nodeToLink);
			nodesNotYetLinked.splice(nodesNotYetLinked.indexOf(nodeToLink), 1);

			for (var i = 0; i < nodePairClosestSoFar.length; i++)
			{
				var node = nodePairClosestSoFar[i];
				var starsystem = node.starsystem;
				var starsystemSize = starsystem.size;
				var starsystemOther = nodePairClosestSoFar[1 - i];
				
				var linkPortal = new LinkPortal
				(
					"LinkPortal" + linkIndex,
					bodyDefnLinkPortal,
					new Coords().randomize().multiply
					(
						starsystemSize
					).multiplyScalar
					(
						2
					).subtract
					(
						starsystemSize
					),
					// starsystemNamesFromAndTo
					[
						starsystem.name,
						starsystemOther.name
					]
				);

				var starsystemPortals = starsystem.linkPortals;
				starsystemPortals.push(linkPortal);
				starsystemPortals[starsystemOther.name] = linkPortal;
			};
		}

		var returnValue = new Network
		(
			name,
			nodesLinked,
			links
		);

		return returnValue;
	}

	// turns

	Network.prototype.updateForTurn = function()
	{
		for (var i = 0; i < this.links.length; i++)
		{
			var link = this.links[i];
			link.updateForTurn(this);
		}
	}
}

function NetworkLink(namesOfNodesLinked)
{
	this.namesOfNodesLinked = namesOfNodesLinked;
	this.ships = [];
}
{
	NetworkLink.prototype.direction = function()
	{
		return this.displacement().normalize();
	}

	NetworkLink.prototype.displacement = function()
	{
		var nodesLinked = this.nodesLinked();

		var returnValue = nodesLinked[1].loc.pos.clone().subtract
		(
			nodesLinked[0].loc.pos
		);

		return returnValue;
	}

	NetworkLink.prototype.length = function()
	{
		return this.displacement().magnitude();
	}

	NetworkLink.prototype.nodesLinked = function()
	{
		var network = Globals.Instance.universe.world.network;

		var returnValue = 
		[
			network.nodes[this.namesOfNodesLinked[0]],
			network.nodes[this.namesOfNodesLinked[1]],
		];

		return returnValue;
	}

	// turns

	NetworkLink.prototype.updateForTurn = function(network)
	{
		if (this.ships.length > 0)
		{
			var nodesLinked = this.nodesLinked();
	
			var length = this.length();

			var shipsExitingLink = [];

			for (var i = 0; i < this.ships.length; i++)
			{
				var ship = this.ships[i];
				var shipPos = ship.loc.pos;
				shipPos.x += Math.abs(ship.vel.x);
				if (shipPos.x >= length)
				{
					var indexOfNodeDestination = (ship.vel.x > 0 ? 1 : 0);
					var indexOfNodeSource = 1 - indexOfNodeDestination;

					var nodeDestination = nodesLinked[indexOfNodeDestination];
					var nodeSource = nodesLinked[indexOfNodeSource];

					var starsystemDestination = nodeDestination.starsystem;
					var starsystemSource = nodeSource.starsystem;

					var portalToExitFrom = starsystemDestination.linkPortals[starsystemSource.name];
					var exitPos = portalToExitFrom.loc.pos;
					shipPos.overwriteWith(exitPos);

					starsystemDestination.ships.push(ship);
					shipsExitingLink.push(ship);
				}
			}

			for (var i = 0; i < shipsExitingLink.length; i++)
			{
				this.ships.splice
				(
					this.ships.indexOf(shipsExitingLink[i]),
					1
				);
			}
		}

		
		
	}
}

function NetworkNode(name, defn, pos, starsystem)
{
	this.name = name;
	this.defn = defn;
	this.loc = new Location(pos);
	this.starsystem = starsystem;
}
{
	// constants 

	NetworkNode.RadiusActual = 4;
}
{
	NetworkNode.prototype.controlBuild_Selection = function()
	{
		/*
		var returnValue = new ControlLabel
		(
			"labelNetworkNodeAsSelection",
			new Coords(0, 0),
			new Coords(0, 0), // this.size
			false, // isTextCentered
			new DataBinding(this.starsystem.name)
		);
		*/

		var viewSize = Globals.Instance.displayHelper.viewSize;
		var containerSize = new Coords(100, 80);
		var margin = 10;
		var controlSpacing = 8;
		var buttonSize = new Coords
		(
			containerSize.x - margin * 4,
			10
		);
		var starsystem = this.starsystem;

		var returnValue = new ControlContainer
		(
			"containerStarsystem",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords(viewSize.x - margin - containerSize.x, margin), // pos
			new Coords(containerSize.x - margin * 2, 40), // size
			// children
			[
				new ControlLabel
				(	
					"labelStarsystemName",
					new Coords(margin, margin),
					new Coords(0, 0), // this.size
					false, // isTextCentered
					new DataBinding(starsystem.name)
				),

				new ControlLabel
				(	
					"labelStarsystemHolder",
					new Coords(margin, margin + controlSpacing),
					new Coords(0, 0), // this.size
					false, // isTextCentered
					new DataBinding(starsystem, "faction.name")
				),

				new ControlButton
				(
					"buttonView",
					new Coords(margin, margin + controlSpacing * 2), // pos
					buttonSize, // size
					"View",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						alert("todo - view")
					}
				),
			]
		);

		return returnValue;
	}
}

function NetworkNodeDefn(name, color)
{
	this.name = name;
	this.color = color;
}
{
	function NetworkNodeDefn_Instances()
	{
		var colors = Color.Instances;

		this.Blue = new NetworkNodeDefn("Blue", colors.GrayLight);
		this.Green = new NetworkNodeDefn("Green", colors.GrayLight);
		this.Red = new NetworkNodeDefn("Red", colors.GrayLight);

		this._All = 
		[
			this.Blue,
			this.Green,
			this.Red,			
		];
	}

	NetworkNodeDefn.Instances = new NetworkNodeDefn_Instances();
}

function Notification(typeName, message)
{
	this.typeName = typeName;
	this.message = message;	
}
{
	Notification.prototype.defn = function()
	{
		return NotificationType.Instances._All[this.defnName];
	}	
}

function NotificationSession(notifications)
{
	this.notifications = notifications;
	this.notificationSelected = null;
}
{
	NotificationSession.prototype.controlBuild = function()
	{
		var displayHelper = Globals.Instance.displayHelper;
		var containerSize = displayHelper.viewSize.clone();
		var controlHeight = containerSize.y / 12;
		var margin = 10;
		var columnWidth = containerSize.x - margin * 2;
		var buttonWidth = (columnWidth - margin) / 2;

		var returnValue = new ControlContainer
		(
			"containerNotificationSession",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords(0, 0), // pos
			containerSize,
			// children
			[
				new ControlLabel
				(
					"labelNotifications",
					new Coords(margin, margin), // pos
					new Coords(columnWidth, controlHeight), // size
					false, // isTextCentered
					new DataBinding("Notifications:")
				),

				new ControlSelect
				(
					"listNotifications",
					new Coords(margin, margin + controlHeight), // pos
					new Coords(columnWidth, controlHeight * 4), // size
					// dataBindingForValueSelected
					new DataBinding(this, "notificationSelected"), 
					// dataBindingForOptions
					new DataBinding(this.notifications),
					null, // bindingExpressionForOptionValues
					"message", // bindingExpressionForOptionText
					new DataBinding(true), // dataBindingForIsEnabled
					4 // numberOfItemsVisible
				),

				new ControlLabel
				(
					"labelMessage",
					new Coords(margin, margin * 2 + controlHeight * 5), // pos
					new Coords(columnWidth, controlHeight), // size
					false, // isTextCentered
					new DataBinding("Details:")
				),

				new ControlLabel
				(
					"textMessage",
					new Coords(margin, margin * 2 + controlHeight * 6), // pos
					new Coords(columnWidth, controlHeight), // size
					false, // isTextCentered
					new DataBinding(this, "notificationSelected.message")
				),

				new ControlButton
				(
					"buttonGoTo",
					new Coords(margin, margin * 2 + controlHeight * 7), // pos
					new Coords(columnWidth, controlHeight), // size
					"Go To",
					null, // dataBindingForIsEnabled
					// click
					function()
					{
						alert("todo - goto");
					}
				),


				new ControlButton
				(
					"buttonDone",
					new Coords(margin, containerSize.y - margin - controlHeight), // pos
					new Coords(columnWidth, controlHeight), // size
					"Done",
					null, // dataBindingForIsEnabled
					// click
					function()
					{
						var universe = Globals.Instance.universe;
						var world = universe.world;
						var venueNext = new VenueWorld(world);
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),
			]
		);

		return returnValue;
	}
}

function NotificationType(name)
{
	this.name = name;
}
{
	function NotificationType_Instances()
	{
		this.Default = new NotificationType("Default");
	}

	NotificationType.Instances = new NotificationType_Instances();
}

function NumberHelper()
{}
{
	NumberHelper.wrapValueToRangeMinMax = function(value, min, max)
	{
		var range = max - min;

		while (value < min)
		{
			value += range;
		}
		while (value > max)
		{
			value -= range;
		}

		return value;
	}
}

function Order(defnName, target)
{
	this.defnName = defnName;
	this.target = target;	
	this.isComplete = false;
}
{
	Order.prototype.defn = function()
	{
		return OrderDefn.Instances._All[this.defnName];
	}

	Order.prototype.obey = function(actor)
	{
		if (this.isComplete == false)
		{
			this.defn().obey(actor, this);
		}
	}	
}

function OrderDefn(name, obey)
{
	this.name = name;
	this.obey = obey;
}
{
	function OrderDefn_Instances()
	{
		this.Go = new OrderDefn
		(
			"Go",
			// obey
			function(actor, order)
			{
				var target = order.target;
				if (actor.loc.equals(target.loc) == true)
				{
					order.isComplete = true;
				}
				else if (actor.activity == null)
				{
					actor.activity = new Activity
					(
						"MoveToTarget",
						[ order.target ]
					);					
				}

			}
		);

		this._All = 
		[
			this.Go,
		];

		this._All.addLookups("name");
	}

	OrderDefn.Instances = new OrderDefn_Instances();
}

function Orientation(forward, down)
{
	this.forward = forward.clone().normalize();
	this.right = down.clone().crossProduct(this.forward).normalize();
	this.down = this.forward.clone().crossProduct(this.right).normalize();
}
{
	Orientation.prototype.overwriteWith = function(other)
	{
		this.forward.overwriteWith(other.forward);
		this.right.overwriteWith(other.right);
		this.down.overwriteWith(other.down);
	}
}

function Plane(normal, distanceFromOrigin)
{
	this.normal = normal;
	this.distanceFromOrigin = distanceFromOrigin;
}

function Planet(name, factionName, pos, demographics, industry, layout)
{
	this.name = name;	
	this.factionName = factionName;
	this.loc = new Location(pos);
	this.demographics = demographics;
	this.industry = industry;
	this.layout = layout;

	this.defn = Planet.BodyDefn;
}
{
	// constants

	Planet.BodyDefn = new BodyDefn
	(
		"Planet", 
		new Coords(10, 10), // size
		new VisualGroup
		([
			new VisualSphere(Color.Instances.Cyan, 10),
		])
	);

	// instance methods

	Planet.prototype.faction = function()
	{
		return (this.factionName == null ? null : Globals.Instance.universe.world.factions[this.factionName]);
	}

	// controls

	Planet.prototype.controlBuild_Selection = function()
	{
		var returnValue = new ControlLabel
		(
			"labelPlanetAsSelection",
			new Coords(0, 0),
			new Coords(0, 0), // this.size
			false, // isTextCentered
			new DataBinding(this.name)
		);

		return returnValue;
	}

	// diplomacy

	Planet.prototype.strength = function()
	{
		return 1;
	}

	// turns

	Planet.prototype.updateForTurn = function()
	{	
		this.layout.updateForTurn();
		this.industry.updateForTurn(this);
		this.demographics.updateForTurn(this);
	}
}

function PlanetDemographics(population)
{
	this.population = population;
}
{
	PlanetDemographics.prototype.updateForTurn = function(planet)
	{
		var prosperityThisTurn = 1;
		this.prosperityAccumulated += prosperityThisTurn;
		var prosperityRequiredForGrowth = this.population * 2;
		if (this.prosperityAccumulated >= prosperityRequiredForGrowth)
		{
			this.prosperityAccumulated = 0;
			var populationMax = 10; // todo
			if (this.population < populationMax);
			{
				this.population++;
			}
		}	
	}
}

function PlanetIndustry(industryAccumulated, buildableInProgress)
{
	this.industryAccumulated = industryAccumulated;
	this.buildableInProgress = buildableInProgress;
}
{
	PlanetIndustry.prototype.updateForTurn = function(planet)
	{
		var industryThisTurn = 1; // hack
		this.industryAccumulated += industryThisTurn;
		if (this.buildableInProgress != null)
		{
			var buildableDefn = this.buildableInProgress.defn;
			var industryToComplete = buildableDefn.industryToComplete;
			if (this.industryAccumulated >= industryToComplete)
			{
				this.industryAccumulated = 0;
				// todo
				var one = 1;			
			}
		}
	}
}

function Polar(azimuth, elevation, radius)
{
	// values in radians

	this.azimuth = azimuth;
	this.elevation = elevation;
	this.radius = radius;
}
{
	// constants

	Polar.RadiansPerCycle = Math.PI * 2;
	Polar.RadiansPerRightAngle = Math.PI / 2;

	// static methods

	Polar.fromCoords = function(coordsToConvert)
	{
		var azimuthInRadians = Math.atan2(coordsToConvert.y, coordsToConvert.x);
		var azimuth = azimuthInRadians / Polar.RadiansPerCycle;
		if (azimuth < 0)
		{
			azimuth += 1;
		}

		var radius = coordsToConvert.magnitude();

		var elevationInRadians = Math.asin(coordsToConvert.z / radius);
		var elevation = elevationInRadians / Polar.RadiansPerRightAngle;

		var returnValue = new Polar
		(
			azimuth,
			elevation,
			radius
		);

		return returnValue;
	}

	// instance methods

	Polar.random = function()
	{
		return new Polar
		(
			Math.random(),
			Math.random() * 2 - 1,
			Math.random()
		);
	}

	Polar.prototype.toCoords = function()
	{
		var azimuthInRadians = this.azimuth * Polar.RadiansPerCycle;
		var elevationInRadians = this.elevation * Polar.RadiansPerRightAngle;
		var cosineOfElevation = Math.cos(elevationInRadians);

		var returnValue = new Coords
		(
			Math.cos(azimuthInRadians) * cosineOfElevation,
			Math.sin(azimuthInRadians) * cosineOfElevation,
			Math.sin(elevationInRadians)
		).multiplyScalar(this.radius);

		return returnValue;
	}
}

function Profile(name, worlds)
{
	this.name = name;
	this.worlds = worlds;
}

function ProfileHelper()
{}
{
	ProfileHelper.prototype.profileAdd = function(profile)
	{
		var profiles = this.profiles();
		profiles.push(profile);
		var propertyName = 
			Globals.Instance.programName
			+ ".Profiles";
		StorageHelper.save
		(
			propertyName,
			profiles
		);
	}

	ProfileHelper.prototype.profileDelete = function(profileToDelete)
	{
		var profilesStored = this.profiles();
		
		var profileIndex = this.profileIndexFindByName
		(
			profilesStored,
			profileToDelete.name
		);

		profilesStored.splice
		(
			profileIndex,
			1
		);

		var propertyName = 
			Globals.Instance.programName
			+ ".Profiles";

		StorageHelper.save
		(
			propertyName,
			profilesStored
		);
	}


	ProfileHelper.prototype.profileIndexFindByName = function
	(
		profiles, profileNameToFind
	)
	{
		var returnValue = null;

		for (var i = 0; i < profiles.length; i++)
		{
			var profile = profiles[i];
			if (profile.name == profileNameToFind)
			{
				returnValue = i;
				break;
			}
		}

		return returnValue;
	}

	ProfileHelper.prototype.profileSave = function(profileToSave)
	{
		var profilesStored = this.profiles();
		
		var profileIndex = this.profileIndexFindByName
		(
			profilesStored,
			profileToSave.name
		);

		profilesStored.splice
		(
			profileIndex,
			1,
			profileToSave
		);

		var propertyName = 
			Globals.Instance.programName
			+ ".Profiles";

		StorageHelper.save
		(
			propertyName,
			profilesStored
		);
	}

	ProfileHelper.prototype.profiles = function()
	{
		var propertyName = 
			Globals.Instance.programName
			+ ".Profiles";

		var profiles = StorageHelper.load
		(
			propertyName
		);

		if (profiles == null)
		{
			profiles = [];
			StorageHelper.save
			(
				propertyName, 
				profiles
			);
		}

		return profiles;
	}
}

function Ray(startPos, direction)
{
	this.startPos = startPos;
	this.direction = direction;
}

function Serializer(knownTypes)
{
	this.knownTypes = knownTypes;

	for (var i = 0; i < this.knownTypes.length; i++)
	{
		var knownType = this.knownTypes[i];
		this.knownTypes[knownType.name] = knownType;
	}
}
{
	// constants

	Serializer.TypeNamesBuiltIn = [ "Boolean", "Function", "Number", "String" ];

	// instance methods

	Serializer.prototype.deleteClassNameRecursively = function(objectToDeleteClassNameOn)
	{
		if (objectToDeleteClassNameOn == null)
		{
			return;
		}

		var className = objectToDeleteClassNameOn.constructor.name;
		if (this.knownTypes[className] != null)
		{
			delete objectToDeleteClassNameOn.className;

			for (var childPropertyName in objectToDeleteClassNameOn)
			{
				var childProperty = objectToDeleteClassNameOn[childPropertyName];
				this.deleteClassNameRecursively(childProperty);
			}
		}
		else if (className == "Array")
		{
			for (var i = 0; i < objectToDeleteClassNameOn.length; i++)
			{
				var element = objectToDeleteClassNameOn[i];
				this.deleteClassNameRecursively(element);
			}
		}
	}

	Serializer.prototype.deserialize = function(stringToDeserialize)
	{
		var objectDeserialized = JSON.parse(stringToDeserialize);

		this.setPrototypeRecursively(objectDeserialized);

		this.deleteClassNameRecursively(objectDeserialized);

		return objectDeserialized;
	}

	Serializer.prototype.serialize = function(objectToSerialize)
	{
		this.setClassNameRecursively(objectToSerialize);

		var returnValue = JSON.stringify(objectToSerialize);

		this.deleteClassNameRecursively(objectToSerialize);

		return returnValue;
	}

	Serializer.prototype.setClassNameRecursively = function(objectToSetClassNameOn)
	{
		if (objectToSetClassNameOn == null)
		{
			return;
		}

		var className = objectToSetClassNameOn.constructor.name;
		
		if (this.knownTypes[className] != null)
		{
			for (var childPropertyName in objectToSetClassNameOn)
			{
				var childProperty = objectToSetClassNameOn[childPropertyName];
				this.setClassNameRecursively(childProperty);
			}

			objectToSetClassNameOn.className = className;
		}
		else if (className == "Array")
		{
			for (var i = 0; i < objectToSetClassNameOn.length; i++)
			{
				var element = objectToSetClassNameOn[i];
				this.setClassNameRecursively(element);
			}
		}
		else if (Serializer.TypeNamesBuiltIn.indexOf(className) == -1)
		{
			throw "Unknown type!";
		}
	}

	Serializer.prototype.setPrototypeRecursively = function(objectToSetPrototypeOn)
	{
		if (objectToSetPrototypeOn == null)
		{
			return;
		}
		var className = objectToSetPrototypeOn.className;

		var typeOfObjectToSetPrototypeOn = this.knownTypes[className];

		if (typeOfObjectToSetPrototypeOn != null)
		{
			objectToSetPrototypeOn.__proto__ = typeOfObjectToSetPrototypeOn.prototype;
	
			for (var childPropertyName in objectToSetPrototypeOn)
			{
				var childProperty = objectToSetPrototypeOn[childPropertyName];
				this.setPrototypeRecursively(childProperty);
			}
		}
		else 
		{
			var typeName = objectToSetPrototypeOn.constructor.name;
			if (typeName == "Array")
			{
				for (var i = 0; i < objectToSetPrototypeOn.length; i++)
				{
					var element = objectToSetPrototypeOn[i];
					this.setPrototypeRecursively(element);
				}
			}
			else if (Serializer.TypeNamesBuiltIn.indexOf(typeName) == -1)
			{
				throw "Unknown type!";
			}
		}
	}
}

function Ship(name, defn, pos, factionName)
{
	this.name = name;
	this.defn = defn;
	this.loc = new Location(pos);
	this.factionName = factionName;

	this.vel = new Coords(0, 0, 0);

	var deviceDefnDefault = new DeviceDefn
	(
		"DeviceDefn0",
		100, // range
		function (actor, device, target)
		{
			var universe = Globals.Instance.universe;
			var venueCurrent = universe.venueCurrent;

			var projectile = new Ship
			(
				"[projectile]",
				Ship.bodyDefnBuild(Color.Instances.Yellow),
				actor.loc.pos.clone(),
				actor.factionName
			);

			projectile.activity = new Activity
			(
				"MoveToTarget",
				[ target ]
			);

			venueCurrent.model.bodies.push(projectile);
		}
	);

	this.devices = 
	[
		new Device(deviceDefnDefault),
		new Device(deviceDefnDefault),
		new Device(deviceDefnDefault),
	];

	this.movesThisTurn = 0;
}
{
	// static methods

	Ship.bodyDefnBuild = function(color)
	{
		var returnValue = new BodyDefn
		(
			"Ship", 
			new Coords(10, 10), // size
			new VisualGroup
			([
				new VisualRectangle(color, new Coords(10, 10)),
			])
		);

		return returnValue;
	}

	// instance methods

	Ship.prototype.faction = function()
	{
		return (this.factionName == null ? null : Globals.Instance.universe.world.factions[this.factionName]);
	}

	Ship.prototype.id = function()
	{
		return this.factionName + this.name;
	}

	// controls

	Ship.prototype.controlBuild_Selection = function()
	{
		var viewSize = Globals.Instance.displayHelper.viewSize;
		var containerSize = new Coords(100, viewSize.y);

		var margin = 10;
		var controlSpacing = 20;
		var buttonSize = new Coords
		(
			containerSize.x - margin * 4,
			15
		);

		var returnValue = new ControlContainer
		(
			"containerShip",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords(viewSize.x - margin - containerSize.x, margin), // pos
			new Coords(containerSize.x - margin * 2, containerSize.y - margin * 4),
			// children
			[
				new ControlLabel
				(	
					"labelShipAsSelection",
					new Coords(margin, margin),
					new Coords(0, 0), // this.size
					false, // isTextCentered
					new DataBinding(this.name)
				),

				new ControlButton
				(
					"buttonView",
					new Coords(margin, margin + controlSpacing), // pos
					buttonSize, // size
					"View",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						alert("todo - view")
					}
				),

				new ControlButton
				(
					"buttonMove",
					new Coords(margin, margin + controlSpacing * 2), // pos
					buttonSize,
					"Move",
					null, // dataBindingForIsEnabled
					function ()
					{
						var venueCurrent = Globals.Instance.universe.venueCurrent;
						venueCurrent.cursorBuild();
					}
				),
				new ControlButton
				(
					"buttonRepeat",
					new Coords(margin, margin + controlSpacing * 3), // pos
					buttonSize,
					"Repeat",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						alert("todo - repeat")
					}
				),

				new ControlLabel
				(	
					"labelDevices",
					new Coords(margin, margin + controlSpacing * 4), // pos
					new Coords(0, 0), // this.size
					false, // isTextCentered
					new DataBinding("Devices:")
				),

				new ControlSelect
				(
					"listDevices",
					new Coords(margin, margin + controlSpacing * 5), // pos
					new Coords(buttonSize.x, controlSpacing * 4), // size
					// dataBindingForValueSelected
					new DataBinding(Globals.Instance.universe.venueCurrent.selection, "deviceSelected"),
					// dataBindingForOptions
					new DataBinding
					(
						this.devices,
						null
					),
					null, // bindingExpressionForOptionValues
					"defn.name", // bindingExpressionForOptionText
					new DataBinding(true), // dataBindingForIsEnabled
					4 // numberOfItemsVisible
				),

				new ControlButton
				(
					"buttonDeviceUse",
					new Coords(margin, margin + controlSpacing * 10), // pos
					buttonSize,
					"Use Device",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						alert("todo - use device")
					}
				),


			]
		);

		return returnValue;
	}

	// diplomacy

	Ship.prototype.strength = function()
	{
		return 1;
	}

	// turns

	Ship.prototype.updateForTurn = function()
	{
		// todo
	}
}


function Sound(name, sourcePath)
{
	this.name = name;
	this.sourcePath = sourcePath;

	this.offsetInSeconds = 0;
}
{	
	Sound.prototype.domElementBuild = function(volume)
	{
		this.domElement = document.createElement("audio");
		this.domElement.sound = this;
		this.domElement.autoplay = true;
		this.domElement.oncanplay = this.play2.bind(this);
		this.domElement.onended = this.stop.bind(this);
		this.domElement.volume = volume;

		var domElementForSoundSource = document.createElement("source");
		domElementForSoundSource.src = this.sourcePath;

		this.domElement.appendChild
		(
			domElementForSoundSource
		);

		return this.domElement;
	}

	Sound.prototype.pause = function()
	{
		var offsetInSeconds = this.domElement.currentTime;
		this.stop();
		this.offsetInSeconds = offsetInSeconds;
	}

	Sound.prototype.play = function(volume)
	{
		Globals.Instance.divMain.appendChild
		(
			this.domElementBuild(volume)
		);
	}

	Sound.prototype.play2 = function()
	{
		this.domElement.currentTime = this.offsetInSeconds;
		this.domElement.play();
	}

	Sound.prototype.reset = function()
	{
		this.offsetInSeconds = 0;
	}

	Sound.prototype.stop = function(event)
	{
		var domElement = (event == null ? this.domElement : event.srcElement);
		Globals.Instance.divMain.removeChild(domElement);
		this.offsetInSeconds = 0;
	}
}

function SoundHelper(sounds)
{
	this.sounds = sounds;
	this.sounds.addLookups("name");

	this.musicVolume = 1;
	this.soundVolume = 1;

	this.soundForMusic = null;
}
{
	SoundHelper.controlSelectOptionsVolume = function()
	{
		var returnValue =
		[
			new ControlSelectOption(1, "100%"),
			new ControlSelectOption(0, "0%"),
			new ControlSelectOption(.1, "10%"),
			new ControlSelectOption(.2, "20%"),
			new ControlSelectOption(.3, "30%"),
			new ControlSelectOption(.4, "40%"),
			new ControlSelectOption(.5, "50%"),
			new ControlSelectOption(.6, "60%"),
			new ControlSelectOption(.7, "70%"),
			new ControlSelectOption(.8, "80%"),
			new ControlSelectOption(.9, "90%"),
		];

		return returnValue;
	}

	// instance methods

	SoundHelper.prototype.reset = function()
	{
		for (var i = 0; i < this.sounds.length; i++)
		{
			var sound = this.sounds[i];
			sound.offsetInSeconds = 0;
		}
	}

	SoundHelper.prototype.soundWithNamePlayAsEffect = function(soundName)
	{
		this.sounds[soundName].play(this.soundVolume);
	}

	SoundHelper.prototype.soundWithNamePlayAsMusic = function(soundName)
	{
		if 
		(
			this.soundForMusic != null 
			&& this.soundForMusic.name != soundName
		)
		{
			this.soundForMusic.stop();
		}
		this.soundForMusic = this.sounds[soundName];
		this.soundForMusic.play(this.musicVolume);
	}
}

function Sphere(radius, centerPos)
{
	this.radius = radius;
	this.centerPos = centerPos;
}

function Starsystem(name, size, star, linkPortals, planets, factionName)
{
	this.name = name;
	this.size = size;

	this.star = star;
	this.linkPortals = linkPortals;
	this.planets = planets;
	this.factionName = factionName;

	this.ships = [];
}
{
	// constants

	Starsystem.SizeStandard = new Coords(100, 100, 100);

	// static methods

	Starsystem.generateRandom = function()
	{
		var name = NameGenerator.generateName();
		var size = Starsystem.SizeStandard;

		var star = new Body
		(
			"Star", 
			new BodyDefn
			(
				"Star", 
				new Coords(40, 40), // size
				new VisualGroup
				([
					new VisualSphere(Color.Instances.Yellow, 40),
					new VisualText(name)
				])
			),
			new Coords(0, 0, -10)
		);

		var numberOfPlanetsMin = 1;
		var numberOfPlanetsMax = 4;
		var numberOfPlanetsRange = 
			numberOfPlanetsMax - numberOfPlanetsMin;
		var numberOfPlanets = numberOfPlanetsMin + Math.floor
		(
			Math.random() * numberOfPlanetsRange
		);

		var spaceBetweenPlanets = 40;

		var planets = [];
		for (var i = 0; i < numberOfPlanets; i++)
		{
			var planet = new Planet
			(
				name + " " + (i + 1),
				null, // factionName
				// pos
				new Coords().randomize().multiply
				(
					size
				).multiplyScalar
				(
					2
				).subtract
				(
					size
				),
				new PlanetDemographics(1),
				new PlanetIndustry(0, null),
				Layout.generateRandom()
			);

			planets.push(planet);
		}

		var returnValue = new Starsystem
		(
			name,
			size,
			star,
			[], // linkPortals - generated later
			planets
		);

		return returnValue;
	}

	// instance methods

	Starsystem.prototype.faction = function()
	{
		return (this.factionName == null ? null : Globals.Instance.universe.world.factions[this.factionName]);
	}

	Starsystem.prototype.links = function()
	{
		var returnValues = [];

		for (var i = 0; i < this.linkPortals.length; i++)
		{
			var linkPortal = this.linkPortals[i];
			var link = linkPortal.link();
			returnValues.push(link);
		}

		return returnValues;
	}

	// moves

	Starsystem.prototype.updateForMove = function()
	{
		alert("todo");
	}	

	// turns

	Starsystem.prototype.updateForTurn = function()
	{
		for (var i = 0; i < this.bodies.length; i++)
		{
			var body = this.bodies[i];
			if (body.updateForTurn != null)
			{
				body.updateForTurn();
			}
		}
	}
	
}

function StorageHelper()
{}
{
	StorageHelper.load = function(propertyName)
	{
		var returnValue;

		var returnValueAsString = localStorage.getItem
		(
			propertyName
		);

		if (returnValueAsString == null)
		{
			returnValue = null;
		}
		else
		{
			returnValue = Globals.Instance.serializer.deserialize
			(
				returnValueAsString
			);
		}

		return returnValue;
	}

	StorageHelper.save = function(propertyName, valueToSave)
	{
		var valueToSaveSerialized = Globals.Instance.serializer.serialize
		(
			valueToSave
		);

		localStorage.setItem
		(
			propertyName, 
			valueToSaveSerialized
		);
	}
}

function StringHelper()
{
	// static class
}
{
	String.prototype.padLeft = function
	(
		lengthAfterPadding, 
		characterToPadWith
	)
	{
		var stringToPad = this;
		while (stringToPad.length < lengthAfterPadding)
		{
			stringToPad = characterToPadWith + stringToPad;
		}
	
		return stringToPad;
	}
}

// classes

function TalkDefn(name, talkNodes)
{
	this.name = name;
	this.talkNodes = talkNodes;

	for (var i = 0; i < this.talkNodes.length; i++)
	{
		var talkNode = this.talkNodes[i];
		if (talkNode.defn.name == "label")
		{
			var label = talkNode.parameters[0];
			this.talkNodes[TalkNode.Underscore + label] = talkNode; 
		}
	}
}
{
	TalkDefn.prototype.talkNodeByLabel = function(nameOfTalkNodeToGet)
	{
		return this.talkNodes[TalkNode.Underscore + nameOfTalkNodeToGet];
	}
}

function TalkNode(defn, parameters)
{
	this.defn = defn;
	this.parameters = parameters;
}
{
	// constant

	TalkNode.Underscore = "_"; // Prepended for array lookup in case name is numeric

	// static methods

	TalkNode.fromString = function(stringToParse)
	{
		var returnValue = null;

		stringToParse = stringToParse.trim();
		var stringSplitOnQuotes = stringToParse.split("'");

		for (var i = 1; i < stringSplitOnQuotes.length; i += 2)
		{
			var stringLiteral = stringSplitOnQuotes[i];
			stringLiteral = stringLiteral.split(" ").join("_");
			stringSplitOnQuotes[i] = stringLiteral;
		}
	
		stringToParse = stringSplitOnQuotes.join("'");
		stringToParse = stringToParse.split("\t").join(" ");

		var talkNodeDefns = TalkNodeDefn.Instances._All;

		var stringAsTokens = stringToParse.trim().split(" ");
		for (var i = 0; i < stringAsTokens.length; i++)
		{
			var token = stringAsTokens[i];

			token = token.split("_").join(" ");
			token = token.split("'").join("");
			token = token.split("\"").join("'");
			stringAsTokens[i] = token;

			if (token == "")
			{
				stringAsTokens.splice(i, 1);
				i--;
			}
		}

		var talkNodeDefnName = stringAsTokens[0];
		var talkNodeDefn = talkNodeDefns[talkNodeDefnName];
		if (talkNodeDefn == null)
		{
			throw "Unrecognized command.";
		}
		else
		{
			var parameters = stringAsTokens.slice(1);

			returnValue = new TalkNode
			(
				talkNodeDefn,
				parameters
			)
		}

		return returnValue;
	}

	// instance methods

	TalkNode.prototype.click = function(talkSession, scope)
	{
		if (this.defn.click != null)
		{
			this.defn.click(talkSession, scope, this);
		}
	}

	TalkNode.prototype.execute = function(talkSession, scope)
	{
		this.defn.execute(talkSession, scope, this);
	}

	TalkNode.prototype.htmlElementBuild = function(talkSession)
	{
		var returnValue = document.createElement("div");
		returnValue.innerHTML = this.parameters;
		returnValue.talkSession = talkSession;
		returnValue.onclick = TalkNode.handleClickEvent;

		returnValue.talkNode = this;
		this.htmlElement = returnValue;

		return returnValue;
	}

	TalkNode.prototype.text = function()
	{
		return this.parameters[1]; // hack
	}

}

function TalkNodeDefn(name, execute)
{
	this.name = name;
	this.execute = execute;
}
{
	// instances

	TalkNodeDefn.Instances = new TalkNodeDefn_Instances();
	
	function TalkNodeDefn_Instances()
	{
		this.Display = new TalkNodeDefn
		(
			"display",
			// execute
			function(talkSession, scope, talkNode) 
			{ 
				scope.areOptionsVisible = false;
				var textToDisplay = talkNode.parameters[0];

				var textToDisplaySplit = textToDisplay.split("$");
				for (var i = 1; i < textToDisplaySplit.length; i += 2)
				{
					var variableName = textToDisplaySplit[i];
					var variableValue = talkSession.variableLookup[variableName];
					textToDisplaySplit[i] = variableValue;
					
				}
				textToDisplay = textToDisplaySplit.join("");
				talkSession.log.push(talkSession.factions[1] + ": " + textToDisplay);

				scope.displayTextCurrent = textToDisplay;
				scope.talkNodeAdvance(talkSession);

			}
		);

		this.Goto = new TalkNodeDefn
		(
			"goto", 
			// execute
			function(talkSession, scope, talkNode) 
			{ 
				scope.talkNodeCurrent = talkSession.defn.talkNodeByLabel
				(
					talkNode.parameters[0]
				);

				talkSession.update();
			}
		);

		this.IfGoto = new TalkNodeDefn
		(
			"ifGoto", 
			// execute
			function(talkSession, scope, talkNode) 
			{ 
				var parameters = talkNode.parameters;
				var variableName = parameters[0];
				var variableValueToMatch = parameters[1];
				if (variableValueToMatch == "null")
				{
					variableValueToMatch = null;
				}

				var variableValueActual = talkSession.variableLookup[variableName];
				if (variableValueActual != null)
				{
					variableValueActual = variableValueActual.toString();
				}

				if (variableValueActual == variableValueToMatch)
				{
					var nameOfDestinationNode = parameters[2];

					scope.talkNodeCurrent = talkSession.defn.talkNodeByLabel
					(
						nameOfDestinationNode
					);
				}
				else
				{
					scope.talkNodeAdvance(talkSession);
				}

				talkSession.update();
			}
		);

		this.Label = new TalkNodeDefn
		(
			"label", 
			// execute
			function(talkSession, scope, talkNode) 
			{ 
				// do nothing
				scope.talkNodeAdvance(talkSession);
				talkSession.update();
			}
		);

		this.Option = new TalkNodeDefn
		(
			"option", 
			// execute
			function(talkSession, scope, talkNode) 
			{ 
				scope.talkNodesForOptions.push
				(
					talkNode
				);

				scope.talkNodeAdvance(talkSession);

				talkSession.update();
			}
		);

		this.OptionsClear = new TalkNodeDefn
		(
			"optionsClear", 
			// execute
			function(talkSession, scope, talkNode) 
			{ 
				scope.talkNodesForOptions.length = 0;

				scope.talkNodeAdvance(talkSession);

				talkSession.update();
			}
		);

		this.Pop = new TalkNodeDefn
		(
			"pop", 	
			function(talkSession, scope, talkNode) 
			{ 	
				var scope = scope.parent;
				talkSession.scopeCurrent = scope;

				scope.talkNodeCurrent = talkSession.defn.talkNodeByLabel
				(
					talkNode.parameters[0]
				);

				talkSession.update();
			}
		);

		this.Program = new TalkNodeDefn
		(
			"program", 	
			function(talkSession, scope, talkNode) 
			{ 
				var parameters = talkNode.parameters;
				var variableNameForReturnValue = parameters[0];
				var programAsText = parameters[1];

				var program = new Function
				(
					"talkSession", "scope", "talkNode",
					programAsText
				);
				var returnValue = program(talkSession, scope, talkNode);
				talkSession.variableLookup[variableNameForReturnValue] = returnValue;

				scope.talkNodeAdvance(talkSession);
				talkSession.update();
			}
		);

		this.Prompt = new TalkNodeDefn
		(
			"prompt", 	
			function(talkSession, scope, talkNode) 
			{ 
				var optionSelected = talkSession.optionSelected;
				if (optionSelected == null)
				{
					scope.areOptionsVisible = true;
				}
				else
				{
					talkSession.log.push(talkSession.factions[0].name + ":" + optionSelected.parameters[1]);
					talkSession.optionSelected = null;
					var nameOfTalkNodeNext = optionSelected.parameters[0];
					var talkNodeNext = talkSession.defn.talkNodeByLabel(nameOfTalkNodeNext);
					scope.talkNodeCurrent = talkNodeNext;
					talkSession.update();
				}
			}
		);

		this.Push = new TalkNodeDefn
		(
			"push", 
			function(talkSession, scope, talkNode) 
			{ 
				var runDefn = talkSession.defn;
				var talkNodeIndex = runDefn.talkNodes.indexOf(talkNode);
				var talkNodeNext = runDefn.talkNodes[talkNodeIndex + 1];

				talkSession.scopeCurrent = new TalkScope
				(
					scope, // parent
					talkNodeNext,
					[] // options
				);

				talkSession.update();
			}
		);

		this.Quit = new TalkNodeDefn
		(
			"quit", 	
			function(talkSession, scope, talkNode) 
			{ 
				talkSession.isTerminated = true;
				// todo
			}	
		);


		this.Set = new TalkNodeDefn
		(
			"set", 	
			function(talkSession, scope, talkNode) 
			{ 
				var parameters = talkNode.parameters;
				var variableName = parameters[0];
				var variableValueToSet = parameters[1];
				if (variableValueToSet == "null")
				{
					variableValueToSet = null;
				}

				talkSession.variableLookup[variableName] = variableValueToSet;

				scope.talkNodeAdvance(talkSession);
				talkSession.update();
			}	
		);

		this._All = 
		[
			this.Display,
			this.Goto,
			this.IfGoto,
			this.Label,
			this.Option,
			this.OptionsClear,
			this.Pop,
			this.Program,
			this.Prompt,
			this.Push,
			this.Set,
			this.Quit,
		];
		this._All.addLookups("name");

	}
}

function TalkSession(defn, factions)
{
	this.defn = defn;
	this.factions = factions;

	var talkNodeStart = this.defn.talkNodes[0];

	this.scopeCurrent = new TalkScope
	(
		null, // parent
		talkNodeStart,
		// talkNodesForOptions
		[]
	);

	this.variableLookup = [];
	this.isTerminated = false;

	this.log = [];
}
{
	// static methods

	TalkSession.buildExample = function(factionActing, factionReceiving)
	{
		var factions = [factionActing, factionReceiving];

		var talkNodesAsStrings = 
		[
			"program 	'RelationsState'	' return talkSession.factions[0].relationships[talkSession.factions[1].name].state; ' ",
			"set FactionName '" + factionReceiving.name + "' ",
		 	"display 	'Greetings.  We are the $FactionName$.' ", 

			"label Subject",
			"display 	'What do you want to talk about?' ",
			"label SubjectOptions",
			"optionsClear",
			"ifGoto 	RelationsState	Peace 		SubjectOptions.PeaceOrAlliance ", 
			"ifGoto 	RelationsState	Alliance 	SubjectOptions.PeaceOrAlliance ",
			"option		OfferPeace	'War is waste of everyone\"s resources.  Let there be peace between us.' ",
			"goto SubjectOptions.End",
			"label SubjectOptions.PeaceOrAlliance",
			"ifGoto		RelationsState	Alliance SubjectOptions.Alliance",
			"option 	DeclareWar 		'Your abuses have become intolerable.  We hereby declare war on you.' ", 
			"option 	ProposeAlliance 	'Would you consider joining an alliance against our common enemies?' ",
			"goto SubjectOptions.End",
			"label SubjectOptions.Alliance",
			"option 	'TradeInfo' 		'As allies, would you like to trade information with us?' ",
			"option 	'CancelAlliance' 	'We regret to say that we can no longer participate in this alliance.' ",
			"label SubjectOptions.End",
			"option 	Quit 			'That\"s all for now.' ",
			"label SubjectPrompt",
			"prompt",

			"label OfferPeace",
			"program 	'PeaceOfferAccepted' ' return talkSession.factions[1].peaceOfferAcceptFrom(talkSession.factions[0]); ' ",
			"ifGoto 	'PeaceOfferAccepted' true 'OfferPeace.Accepted' ",

			"label OfferPeace.Accepted",
			"program 	'RelationsState'	' return DiplomaticRelationship.setStateForFactions(talkSession.factions, DiplomaticRelationship.States.Peace); ' ",
			"display	'We accept your offer of peace with relief.' ",
			"goto 		SubjectOptions",

			"label OfferPeace.Rejected",
			"display 	'Your pathetic begging notwitstanding, this war will continue.' ",
			"prompt",

			"label DeclareWar",
			"program 	OfferAppeasement 	' return talkSession.factions[1].warThreatOfferConcessionsTo(talkSession.factions[0]); ' ",
			"ifGoto 	OfferAppeasement 	null 	DeclareWar.Bargain ",

			"label DeclareWar.Acquiesce",
			"program 	RelationsState	' return DiplomaticRelationship.setStateForFactions(talkSession.factions, DiplomaticRelationship.States.War); ' ",
			"display 'War it is, then.  You will regret this unprovoked belligerence.' ",
			"goto SubjectOptions",

			"label DeclareWar.Bargain",
			"push",
			"display 	'Let us not be rash.  How can we change your mind?' ",
			"option 	DeclareWar.Bargain.Tech 	'We demand technological secrets.' ",
			"option 	DeclareWar.Bargain.Territory 	'We demand that you cede starsystems to us.' ",
			"option 	DeclareWar.Bargain.Refuse 	'There is nothing you can say that will blunt our resolve.' ",
			"prompt",

			"label DeclareWar.Bargain.Tech",
			"display 'Our secrets are our own, and shall remain so.' ", 
			"prompt",

			"label DeclareWar.Bargain.Territory",
			"display 'You would threaten our territorial integrity?  We will never agree to this.' ",
			"prompt",

			"label DeclareWar.Bargain.Refuse",
			"display 'If you refuse to be even slightly reasonable about it...' ",
			"pop DeclareWar.Acquiesce",

			"label ProposeAlliance",
			"program 	AcceptAlliance ' return talkSession.factions[1].allianceProposalAcceptFrom(talkSession.factions[0]); ' ",	
			"ifGoto 	AcceptAllance false ProposeAlliance.Reject",

			"label ProposeAlliance.Accept",
			"program 	RelationsState		' return DiplomaticRelationship.setStateForFactions(talkSession.factions, DiplomaticRelationship.States.Alliance); ' ",
			"display 	'Yes, the universe is a dangerous place.  Let us join forces.' ",
			"goto SubjectOptions",

			"label ProposeAlliance.Reject",
			"display 	'While we value your friendship, we wish to avoid such entanglements for now.' ",			
			"prompt",

			"label CancelAlliance",
			"program 	RelationsState	' return DiplomaticRelationship.setStateForFactions(talkSession.factions, DiplomaticRelationship.States.Peace); ' ",
			"display 	'This is unfortunate news.  We shall miss being your comrades.' ",
			"goto SubjectOptions",

			"label TradeInfo",
			"push",
			"display 	'It depends on what you have to offer.' ",
			"display 	'What kind of information are you interested in trading?' ",
			"option 	TradeInfo.Tech 		'Let\"s trade technology.' ",
			"option 	TradeInfo.Maps 		'Let\"s compare maps.' ",
			"option 	TradeInfo.Factions 	'Can you offer any information on other factions?' ",
			"option 	TradeInfo.Exit		'That\"s all we\"d like to trade for now.' ",
			"prompt",

			"label TradeInfo.Factions",
			"display 'We embrace this opportunity to better know our neighbors.' ",
			"prompt",

			"label TradeInfo.Maps",
			"display 	'Yes, we would like to know what you know of this space we share.' ",
			"prompt",

			"label TradeInfo.Tech",
			"display 'The free exchange of scientific ideas enriches us both.' ",
			"prompt",

			"label TradeInfo.Exit",
			"display 'Very well.' ",
			"pop Subject ",

			"label Quit",
			"display 	'Until next time...' ",
			"display 	'[This conversation is over.]' ",
			"quit",
		];

		var returnValue = TalkSession.fromStrings
		(
			factions, talkNodesAsStrings
		);

		return returnValue;
	}

	TalkSession.fromStrings = function(factions, talkNodesAsStrings)
	{
		var talkNodes = [];

		for (var i = 0; i < talkNodesAsStrings.length; i++)
		{
			var talkNodeAsString = talkNodesAsStrings[i];

			var talkNode = TalkNode.fromString(talkNodeAsString);
			talkNodes.push(talkNode);
		}

		var talkDefn = new TalkDefn
		(
			"TalkDefn",
			talkNodes
		);

		var talkSession = new TalkSession
		(
			talkDefn,
			factions
		);

		return talkSession;
	}

	// instance methods

	TalkSession.prototype.displayTextCurrent = function()
	{
		return this.scopeCurrent.displayTextCurrent;
	}

	TalkSession.prototype.hasResponseBeenSpecified = function()
	{
		var returnValue = 
		(
			(this.optionsAvailable().length == 0)
			|| (this.optionSelected != null)
		);
		return returnValue;
	}

	TalkSession.prototype.optionsAvailable = function()
	{
		var returnValues = (this.scopeCurrent.areOptionsVisible ? this.scopeCurrent.talkNodesForOptions : []);

		return returnValues;
	}

	TalkSession.prototype.respond = function()
	{
		this.update();
	}

	TalkSession.prototype.start = function()
	{
		document.body.appendChild(this.htmlElementBuild());

		//this.update();		
	}	

	TalkSession.prototype.update = function()
	{
		this.scopeCurrent.update(this);
	}
}

function TalkScope(parent, talkNodeCurrent, talkNodesForOptions)
{
	this.parent = parent;
	this.talkNodeCurrent = talkNodeCurrent;
	this.talkNodesForOptions = talkNodesForOptions;

	this.displayTextCurrent = "";
	this.areOptionsVisible = false;
}
{
	TalkScope.prototype.talkNodeAdvance = function(talkSession)
	{
		var talkNodeIndex = talkSession.defn.talkNodes.indexOf(this.talkNodeCurrent);
		var talkNodeNext = talkSession.defn.talkNodes[talkNodeIndex + 1];
		this.talkNodeCurrent = talkNodeNext;
	}

	TalkScope.prototype.update = function(talkSession)
	{
		this.talkNodeCurrent.execute(talkSession, this);	
	}
}

function Technology
(
	name, 
	researchRequired, 
	namesOfPrerequisiteTechnologies, 
	namesOfBuildablesEnabled
)
{
	this.name = name;
	this.researchRequired = researchRequired;
	this.namesOfPrerequisiteTechnologies = namesOfPrerequisiteTechnologies;
	this.namesOfBuildablesEnabled = namesOfBuildablesEnabled;
}

function TechnologyResearcher
(
	name, 
	nameOfTechnologyBeingResearched,
	researchAccumulated,
	namesOfTechnologiesKnown
)
{
	this.name = name;
	this.nameOfTechnologyBeingResearched = nameOfTechnologyBeingResearched;
	this.researchAccumulated = researchAccumulated;
	this.namesOfTechnologiesKnown = namesOfTechnologiesKnown;
}
{
	TechnologyResearcher.prototype.buildablesAvailable = function()
	{
		var returnValues = [];

		return returnValues;
	}

	TechnologyResearcher.prototype.strength = function()
	{
		var returnValue = 0;

		var technologiesKnown = this.technologiesKnown();
		for (var i = 0; i < technologiesKnown.length; i++)
		{
			var tech = technologiesKnown[i];
			returnValue += tech.strength();
		}

		return returnValue;
	}

	TechnologyResearcher.prototype.technologiesAvailable = function()
	{
		var technologyTree = Globals.Instance.universe.technologyTree;

		var technologies = technologyTree.technologies;
		var technologiesKnown = this.namesOfTechnologiesKnown;
		var technologiesUnknown = [];

		for (var i = 0; i < technologies.length; i++)
		{
			var technology = technologies[i];
			var technologyName = technology.name;
	
			var isAlreadyKnown = (technologiesKnown.indexOf(technologyName) >= 0);
			
			if (isAlreadyKnown == false)
			{
				technologiesUnknown.push(technology);
			}
		}
	
		var technologiesUnknownWithKnownPrerequisites = [];
	
		for (var i = 0; i < technologiesUnknown.length; i++)
		{
			var technology = technologiesUnknown[i];
			var prerequisites = technology.namesOfPrerequisiteTechnologies;
	
			var areAllPrerequisitesKnown = true;
		
			for (var p = 0; p < prerequisites.length; p++)
			{
				var prerequisite = prerequisites[p];
				var isPrerequisiteKnown = 
				(
					technologiesKnown.indexOf(prerequisite) >= 0
				);

				if (isPrerequisiteKnown == false)
				{
					areAllPrerequisitesKnown = false;
					break;
				}
			}

			if (areAllPrerequisitesKnown == true)
			{
				technologiesUnknownWithKnownPrerequisites.push
				(
					technology
				);
			}
		}

		return technologiesUnknownWithKnownPrerequisites;
	}

	TechnologyResearcher.prototype.technologiesKnown = function()
	{
		var returnValues = [];

		for (var i = 0; i < this.namesOfTechnologiesKnown.length; i++)
		{
			var techName = this.namesOfTechnologiesKnown[i];
			var technology = Globals.Instance.universe.technologyTree.technologies[techName];
			returnValues.push(technology);
		}

		return returnValues;
	}

	TechnologyResearcher.prototype.technologyAccumulatedIncrement = function()
	{
		var technologyBeingResearched = this.technologyBeingResearched();

		this.researchAccumulated += amountToIncrement;

		if (this.researchAccumulated >= technologyBeingResearched.researchRequired)
		{
			this.namesOfTechnologiesKnown.push
			(
				this.nameOfTechnologyBeingResearched
			);
			this.nameOfTechnologyBeingResearched = null;
			this.researchAccumulated = 0;
		}
	}

	TechnologyResearcher.prototype.technologyBeingResearched = function()
	{
		var technologyTree = Globals.Instance.universe.technologyTree;

		var returnValue = technologyTree.technologies[this.nameOfTechnologyBeingResearched];

		return returnValue;
	}

	// turns

	TechnologyResearcher.prototype.updateForTurn = function()
	{
		var todo = 1;	
	}
}

function TechnologyResearchSession(technologyTree, researcher)
{
	this.technologyTree = technologyTree;
	this.researcher = researcher;
}
{
	// static methods

	TechnologyResearchSession.buildExample = function()
	{
		var technologies =
		[
			new Technology("A", 		5, 	[], 			[]),
			new Technology("A.1", 		8, 	["A"], 			[]),
			new Technology("A.2", 		8, 	["A"], 			[]),
			new Technology("A.3", 		8, 	["A"], 			[]),
			new Technology("B", 		5, 	[], 			[]),
			new Technology("C", 		5, 	[], 			[]),

			new Technology("A+B", 		10, 	["A", "B"], 		[]),
			new Technology("A+C", 		10, 	["A", "C"], 		[]),
			new Technology("B+C", 		10, 	["B", "C"], 		[]),

			new Technology("A+B+C", 	15, 	["A", "B", "C"], 	[]),
	
			new Technology("(A+B)+(B+C)", 	20, 	["A+B", "B+C"], 	[]),
		];

		var technologyTree = new TechnologyTree
		(
			"All Technologies",
			technologies
		);

		var researcher = new TechnologyResearcher
		(
			"Researcher0",
			null, // nameOfTechnologyBeingResearched,
			0, // researchAccumulated
			// namesOfTechnologiesKnown
			[
				"A",
			]
		);

		var researchSession = new TechnologyResearchSession
		(
			technologyTree,
			researcher
		);

		return researchSession;
	}

	// instance methods
	
	TechnologyResearchSession.prototype.isResearchNotInProgress = function()
	{
		var returnValue = (this.researcher.researchAccumulated == 0);
		return returnValue;
	}

	TechnologyResearchSession.prototype.isTechnologyBeingResearched = function()
	{
		return (this.researcher.nameOfTechnologyBeingResearched != null);
	}

	TechnologyResearchSession.prototype.researchAccumulatedIncrement = function
	(
		amountToIncrement
	)
	{
		this.researcher.researchAccumulatedIncrement();
	}
	
	TechnologyResearchSession.prototype.researchRequired = function()
	{
		var technologyBeingResearched = this.technologyBeingResearched();
		var returnValue = 
		(
			technologyBeingResearched == null 
			? 0 
			: technologyBeingResearched.researchRequired
		);
		return returnValue;
	}

	TechnologyResearchSession.prototype.technologyBeingResearched = function()
	{
		var techName = this.researcher.nameOfTechnologyBeingResearched;
		var returnValue = this.technologyTree.technologies[techName];

		return returnValue;
	}

	// controls

	TechnologyResearchSession.prototype.controlBuild = function(size)
	{
		var margin = 10;

		var returnValue = new ControlContainer
		(
			"containerResearchSession", // name, 
			[ "Gray", "White" ], // colorsForeAndBack, 
			new Coords(0, 0), // pos, 
			size, 
			// children
			[
				new ControlLabel
				(
					"labelResearcher", // name, 
					new Coords(margin, margin), // pos, 
					new Coords(10, 10), // size, 
					false, // isTextCentered, 
					new DataBinding("Researcher:") //text
				),

				new ControlLabel
				(
					"textResearcher", // name, 
					new Coords(70, margin), // pos, 
					new Coords(10, 10), // size, 
					false, // isTextCentered, 
					new DataBinding(this.researcher.name) //text
				),

				new ControlLabel
				(
					"labelTechnologiesKnown", // name, 
					new Coords(margin, 30), // pos, 
					new Coords(10, 10), // size, 
					false, // isTextCentered, 
					new DataBinding("Technologies Known:") //text
				),

				new ControlSelect
				(
					"listTechnologiesKnown",
					new Coords(margin, 45), // pos
					new Coords(110, 50), // size
					null, // dataBindingForValueSelected
					// options
					new DataBinding
					(
						this.researcher.namesOfTechnologiesKnown,
						null
					),
					null, // optionvalues,
					null, // optionText,
					new DataBinding(true), // isEnabled
					4 // itemsVisible
				),

				new ControlLabel
				(
					"labelTechnologiesAvailable", // name, 
					new Coords(140, 30), // pos, 
					new Coords(10, 10), // size, 
					false, // isTextCentered, 
					new DataBinding("Technologies Available:") // text
				),

				new ControlSelect
				(
					"listTechnologiesAvailable", // name, 
					new Coords(140, 45), // pos, 
					new Coords(110, 50), // size, 
					// dataBindingForValueSelected,
					new DataBinding
					(
						this.researcher, 
						"nameOfTechnologyBeingResearched"
					),
					// dataBindingForOptions,
					new DataBinding
					(
						this.researcher, "technologiesAvailable" 
					),
					"name", // bindingExpressionForOptionValues,
					"name", // bindingExpressionForOptionText
					// dataBindingForIsEnabled
					new DataBinding(this, "isResearchNotInProgress"),
					4 // numberOfItemsVisible
				),
			
				new ControlLabel
				(
					"labelTechnologyBeingResearched", // name, 
					new Coords(margin, 120), // pos, 
					new Coords(10, 10), // size, 
					false, // isTextCentered, 
					new DataBinding("Technology Being Researched:") // text
				),

				new ControlLabel
				(
					"textTechnologyBeingResearched", // name, 
					new Coords(160, 120), // pos, 
					new Coords(10, 10), // size, 
					false, // isTextCentered, 
					new DataBinding
					(
						this.researcher, 
						"nameOfTechnologyBeingResearched"
					)
				),

				new ControlLabel
				(
					"labelResearchAccumulated", // name, 
					new Coords(10, 135), // pos, 
					new Coords(10, 10), // size, 
					false, // isTextCentered, 
					new DataBinding("Research Accumulated:") // text
				),

				new ControlLabel
				(
					"textResearchAccumulated", // name, 
					new Coords(120, 135), // pos, 
					new Coords(30, 10), // size, 
					true, // isTextCentered, 
					new DataBinding(this.researcher, "researchAccumulated") // text
				),

				new ControlLabel
				(
					"labelSlash", // name, 
					new Coords(130, 135), // pos, 
					new Coords(30, 10), // size, 
					true, // isTextCentered, 
					new DataBinding("/") // text
				),

				new ControlLabel
				(
					"textResearchRequired", // name, 
					new Coords(140, 135), // pos, 
					new Coords(30, 10), // size, 
					true, // isTextCentered, 
					new DataBinding(this, "researchRequired") // text
				),

				new ControlButton
				(
					"buttonResearchPlusOne", //name, 
					new Coords(10, 155), //pos, 
					new Coords(100, 25), // size, 
					"Research + 1", // text, 
					new DataBinding
					(
						this,
						"isTechnologyBeingResearched"
					), // dataBindingForIsEnabled
					// click
					function() 
					{ 
						var universe = Globals.Instance.universe;
						var session = universe.venueCurrent.researchSession;
						session.researchAccumulatedIncrement(1);
					}
				)
			]
		);

		this.control = returnValue;

		return returnValue;
	}
}

function TechnologyTree(name, technologies)
{
	this.name = name;
	this.technologies = technologies;

	for (var i = 0; i < this.technologies.length; i++)
	{
		var technology = this.technologies[i];
		this.technologies[technology.name] = technology;
	}
}
{
	TechnologyTree.buildExample = function()
	{
		var returnValue = new TechnologyTree
		(
			"All Technologies",
			// technologies
			[
				new Technology("A", 		5, []),
				new Technology("A.1", 		8, ["A"]),
				new Technology("A.2", 		8, ["A"]),
				new Technology("A.3", 		8, ["A"]),
				new Technology("B", 		5, []),
				new Technology("C", 		5, []),
	
				new Technology("A+B", 		10, ["A", "B"]),
				new Technology("A+C", 		10, ["A", "C"]),
				new Technology("B+C", 		10, ["B", "C"]),
	
				new Technology("A+B+C", 	15, ["A", "B", "C"]),
	
				new Technology("(A+B)+(B+C)", 	20, ["A+B", "B+C"]),
			]
		);

		return returnValue;
	}
}

function Universe(name, activityDefns, technologyTree, world)
{
	this.name = name;
	this.activityDefns = activityDefns;
	this.technologyTree = technologyTree;
	this.world = world;

	this.activityDefns.addLookups("name");
	this.venueNext = null;

}
{
	// static methods

	Universe.new = function(world)
	{
		var technologyTree = TechnologyTree.buildExample();

		var returnValue = new Universe
		(
			"Universe0",
			ActivityDefn.Instances._All,
			technologyTree,
			world,
			// venues
			[
				// none
			]
		);

		return returnValue;
	}

	// instance methods

	Universe.prototype.initialize = function()
	{
		var venueControlsTitle = new VenueControls
		(
			ControlBuilder.title()
		);

		venueControlsTitle = new VenueFader
		(
			venueControlsTitle, venueControlsTitle
		);

		this.venueNext = venueControlsTitle;
	}

	Universe.prototype.isProfileSelected = function()
	{
		return (this.profile != null);
	}

	Universe.prototype.isWorldSelected = function()
	{
		return (this.world != null);
	}

	Universe.prototype.updateForTimerTick = function()
	{
		if (this.venueNext != null)
		{
			if 
			(
				this.venueCurrent != null 
				&& this.venueCurrent.finalize != null
			)
			{
				this.venueCurrent.finalize();
			}			

			this.venueCurrent = this.venueNext;
			this.venueNext = null;

			if (this.venueCurrent.initialize != null)
			{
				this.venueCurrent.initialize();
			}
		}
		this.venueCurrent.updateForTimerTick();
		this.venueCurrent.draw();
	}
}

function VenueControls(controlRoot, venueParent)
{
	this.controlRoot = controlRoot;
	this.venueParent = venueParent;
}
{
	VenueControls.prototype.draw = function()
	{
		this.controlRoot.draw(Coords.Instances.Zeroes);
	}

	VenueControls.prototype.initialize = function()
	{
		// do nothing
	}

	VenueControls.prototype.updateForTimerTick = function()
	{
		this.draw();

		var inputHelper = Globals.Instance.inputHelper;
		if (inputHelper.isMouseLeftPressed == true)
		{
			var mouseClickPos = inputHelper.mouseClickPos;
			this.controlRoot.mouseClick(mouseClickPos, Coords.Instances.Zeroes);

			// inputHelper.isMouseLeftPressed = false;
		}
		else if (inputHelper.keyCodePressed != null)
		{
			var keyCodePressed = inputHelper.keyCodePressed;
			var isShiftKeyPressed = inputHelper.isShiftKeyPressed;
			this.controlRoot.keyPressed
			(
				keyCodePressed, 
				isShiftKeyPressed
			);
		}

		var mouseMovePos = inputHelper.mouseMovePos;
		var mouseMovePosPrev = inputHelper.mouseMovePosPrev;

		if (mouseMovePos.equals(mouseMovePosPrev) == false)
		{
			this.controlRoot.mouseMove
			(
				mouseMovePos, Coords.Instances.Zeroes
			);
		}
	}
}

function VenueFader
(
	venueToFadeTo, 
	venueToFadeFrom,
	millisecondsPerFade
)
{
	this.millisecondsPerFade = millisecondsPerFade;	
	if (this.millisecondsPerFade == null)
	{
		this.millisecondsPerFade = 250;
	}

	if (venueToFadeFrom == null)
	{
		venueToFadeFrom = Globals.Instance.universe.venueCurrent;
	}

	this.venuesToFadeFromAndTo = 
	[
		venueToFadeFrom,
		venueToFadeTo
	];

	if (venueToFadeFrom == venueToFadeTo)
	{
		this.venueIndexCurrent = 1;
		this.millisecondsPerFade *= 2;
	}
	else
	{
		this.venueIndexCurrent = 0;
	}
}
{
	VenueFader.prototype.draw = function()
	{
		var venueCurrent = this.venuesToFadeFromAndTo[this.venueIndexCurrent];

		venueCurrent.draw();

		var now = new Date();

		if (this.timeFadeStarted == null)
		{
			this.timeFadeStarted = now;
		}

		var millisecondsSinceFadeStarted = now - this.timeFadeStarted;

		var fractionOfFadeCompleted = 
			millisecondsSinceFadeStarted 
			/ this.millisecondsPerFade;

		var alphaOfFadeColor;

		if (this.venueIndexCurrent == 0)
		{
			if (fractionOfFadeCompleted > 1)
			{			
				fractionOfFadeCompleted = 1;
				this.venueIndexCurrent++;
				this.timeFadeStarted = null;

				var venueToFadeTo = this.venuesToFadeFromAndTo[1];
				if (venueToFadeTo.draw == null)
				{
					Globals.Instance.universe.venueNext = venueToFadeTo;
				}

			}
			alphaOfFadeColor = fractionOfFadeCompleted;
		}
		else // this.venueIndexCurrent == 1
		{
			if (fractionOfFadeCompleted > 1)
			{
				fractionOfFadeCompleted = 1;
				Globals.Instance.universe.venueNext = venueCurrent;
			}

			alphaOfFadeColor = 1 - fractionOfFadeCompleted;
		}

		alphaOfFadeColor *= alphaOfFadeColor;

		var displayHelper = Globals.Instance.displayHelper;

		displayHelper.drawRectangle
		(
			new Coords(0, 0), 
			displayHelper.viewSize, 
			null,
			"rgba(0, 0, 0, " + alphaOfFadeColor + ")"
		);

	}

	VenueFader.prototype.initialize = function()
	{
		var venueToFadeTo = this.venuesToFadeFromAndTo[1];
		venueToFadeTo.initialize();
	}

	VenueFader.prototype.updateForTimerTick = function()
	{
		// do nothing
	}
}

function VenueLayout(venueParent, layout)
{
	this.venueParent = venueParent;
	this.layout = layout;
	this.layoutElementInProgress = null;
}
{
	VenueLayout.prototype.draw = function()
	{
		Globals.Instance.displayHelper.drawLayout(this.layout);
		this.venueControls.draw();
	}

	VenueLayout.prototype.initialize = function()
	{
		var controlRoot = this.controlBuild();
		this.venueControls = new VenueControls(controlRoot);
	}

	VenueLayout.prototype.updateForTimerTick = function()
	{
		this.venueControls.updateForTimerTick();

		var inputHelper = Globals.Instance.inputHelper;

		var layout = this.layout;
		var map = layout.map;
		var cursor = map.cursor;
		var cursorPos = cursor.pos;

		cursorPos.overwriteWith
		(
			inputHelper.mouseMovePos	
		).subtract
		(
			map.pos
		).divide
		(
			map.cellSizeInPixels
		).floor();

		if (cursorPos.isWithinRangeMax(map.sizeInCellsMinusOnes) == true)
		{
			if (inputHelper.isMouseLeftPressed == true)
			{
				inputHelper.isMouseLeftPressed = false;

				var cursorBodyDefn = cursor.bodyDefn;
				var cellAtCursor = map.cellAtPos(cursorPos);
	
				if (cursorBodyDefn == null)
				{
					var bodyToRemove = cellAtCursor.body;
					if (bodyToRemove != null)
					{
						layout.elementRemove(bodyToRemove);

						if (bodyToRemove == this.layoutElementInProgress)
						{
							this.layoutElementInProgress = null;
						}
					}
				}
				else
				{
					if (this.layoutElementInProgress != null)
					{
						layout.elementRemove
						(
							this.layoutElementInProgress	
						);	
					}

					this.layoutElementInProgress = new LayoutElement
					(
						cursorBodyDefn, 
						cursorPos.clone()
					);
					layout.elementAdd(this.layoutElementInProgress);
				}
			}
			else if (inputHelper.keyPressed == 13) // return
			{
				inputHelper.keyPressed = null;

				var cursorBodyDefnPrev = cursor.body;
				var bodyDefns = layout.bodyDefns;
				if (cursorBodyDefnPrev == null)
				{
					cursor.bodyDefn = bodyDefns[0];
				}
				else
				{
					var indexOfCursorBodyDefn = bodyDefns.indexOf
					(
						cursorBodyDefnPrev
					);
					indexOfCursorBodyDefn++;
					if (indexOfCursorBodyDefn < bodyDefns.length)
					{
						cursor.bodyDefn = bodyDefns[indexOfCursorBodyDefn];
					}
					else
					{
						cursor.bodyDefn = null;
					}
					
				}	
			}	
		}

		this.draw();
	}

	// controls

	VenueLayout.prototype.controlBuild = function()
	{
		var returnValue = null;

		var containerMainSize = Globals.Instance.displayHelper.viewSize.clone();
		var controlHeight = 16;
		var buttonWidth = 30;
		var margin = 10;

		var containerInnerSize = new Coords(100, 60);

		var faction = Globals.Instance.universe.world.factionCurrent();

		var returnValue = new ControlContainer
		(
			"containerMain",
			[ "Transparent", "Transparent" ], // colorsForeAndBack
			new Coords(0, 0), // pos
			containerMainSize,
			// children
			[
				new ControlButton
				(
					"buttonMenu",
					new Coords
					(
						(containerMainSize.x - buttonWidth) / 2, 
						containerMainSize.y - margin - controlHeight
					), // pos
					new Coords(buttonWidth, controlHeight), // size
					"Back",
					null, // dataBindingForIsEnabled
					// click
					function()
					{
						var universe = Globals.Instance.universe;
						var venueNext = universe.venueCurrent.venueParent;
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),

				faction.controlBuild
				(
					containerMainSize,
					containerInnerSize, 
					margin, 
					controlHeight,
					buttonWidth
				),

				this.controlBuild_Industry
				(
					containerMainSize,
					containerInnerSize, 
					margin, 
					controlHeight,
					buttonWidth
				),


				ControlBuilder.timeAndPlace
				(
					containerMainSize, 
					containerInnerSize, 
					margin,
					controlHeight
				),

				ControlBuilder.selection
				(
					new Coords
					(
						containerMainSize.x - margin - containerInnerSize.x,
						containerMainSize.y - margin - containerInnerSize.y
					),
					containerInnerSize,
					margin,
					controlHeight
				),

				
			]
		);

		return returnValue;
	}

	VenueLayout.prototype.controlBuild_Industry = function
	(
		containerMainSize,
		containerInnerSize, 
		margin, 
		controlHeight,
		buttonWidth
	)
	{
		var returnValue = new ControlContainer
		(
			"containerViewControls",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords
			(
				margin,
				containerMainSize.y 
					- margin
					- containerInnerSize.y
			),
			containerInnerSize,
			// children
			[
				new ControlLabel
				(
					"labelBuilding", 
					new Coords(margin, margin), // pos
					new Coords(0, 0), // size
					false, // isTextCentered
					new DataBinding("Building:")
				),

				new ControlButton
				(
					"buttonRemove",
					new Coords(containerInnerSize.x - margin - buttonWidth, margin / 2), // pos
					new Coords
					(
						buttonWidth, 
						controlHeight
					), // size
					"X",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var universe = Globals.Instance.universe;
						var venueCurrent = universe.venueCurrent;
						venueCurrent.layout.map.cursor.bodyDefn = null;
					}
				),
					
				new ControlSelect
				(
					"selectBuilding",
					new Coords(margin, controlHeight + margin), // pos
					new Coords(containerInnerSize.x - margin * 2, controlHeight), // size, 
					new DataBinding(Globals.Instance.universe.venueCurrent, "layout.map.cursor.bodyDefn" ), // dataBindingForValueSelected,
					new DataBinding(this.layout.bodyDefns), // dataBindingForOptions,
					null, // bindingExpressionForOptionValues,
					"name", // bindingExpressionForOptionText,
					new DataBinding(true), // dataBindingForIsEnabled,
					1 // numberOfItemsVisible
				),

				new ControlLabel
				(
					"labelProgress", 
					new Coords(margin, controlHeight * 2 + margin), // pos
					new Coords(50, controlHeight), // size
					false, // isTextCentered
					new DataBinding("[progress]") 
				),

				new ControlLabel
				(
					"labelRequired", 
					new Coords(margin + 50, controlHeight * 2 + margin), // pos
					new Coords(50, controlHeight), // size
					false, // isTextCentered
 					new DataBinding("[required]") 
				),
			]
		);


		return returnValue;
	}

}

function VenueStarsystem(starsystem)
{
	this.starsystem = starsystem;
	this.model = this.starsystem;

	this.venueControls = new VenueControls
	(
		this.controlBuild()
	);
}
{
	VenueStarsystem.prototype.cursorBuild = function()
	{
		var ship = this.selection;
		var cursor = new Cursor
		(
			ship,
			false,
			false
		);
		this.cursor = cursor;
		this.bodies.push(cursor);
		this.selection = cursor;
	}

	VenueStarsystem.prototype.cursorClear = function()
	{
		var bodyIndexOfCursor = this.bodies.indexOf(this.cursor);
		this.bodies.splice(bodyIndexOfCursor, 1);
		this.selection = this.cursor.bodyParent;
		this.cursor = null;
	}

	VenueStarsystem.prototype.draw = function()
	{
		var displayHelper = Globals.Instance.displayHelper;
		displayHelper.clear();
		displayHelper.drawStarsystemForCamera
		(
			this.starsystem,
			this.camera
		);

		if (this.cursor != null)
		{
			displayHelper.drawStarsystemForCamera_Body
			(
				this.camera,
				this.cursor
			);
		}

		this.venueControls.draw();
	}

	VenueStarsystem.prototype.finalize = function()
	{
		Globals.Instance.soundHelper.soundForMusic.pause();		
	}

	VenueStarsystem.prototype.initialize = function()
	{
		var starsystem = this.starsystem;

		var soundHelper = Globals.Instance.soundHelper;
		soundHelper.soundWithNamePlayAsMusic("Music");

		var viewSize = Globals.Instance.displayHelper.viewSize.clone();
		var focalLength = viewSize.y;
		viewSize.z = focalLength * 4;
			
		this.camera = new Camera
		(
			viewSize, 
			focalLength, 
			new Coords(0 - focalLength, 0, 0), //pos, 
			new Orientation
			(
				new Coords(1, 0, 0), // forward
				new Coords(0, 0, 1) // down
			)
		);
	
		var targetForCamera = new Coords(0, 0, 0);
	
		this.camera.constraints = [];

		this.camera.constraints.push
		(
			new Constraint_PositionOnCylinder
			(
				targetForCamera, // center
				new Orientation
				(
					new Coords(1, 0, 0), 
					new Coords(0, 0, 1) // axis
				),
				0, // yaw
				this.camera.focalLength, // radius
				0 - this.camera.focalLength / 2 // distanceFromCenterAlongAxisMax
			)
		);
	
		this.camera.constraints.push
		(
			new Constraint_LookAtBody
			(
				targetForCamera
			)
		);

		this.camera.constraints.addLookups("name");

		this.bodies = [];
		this.bodies.push(starsystem.star);
		this.bodies = this.bodies.concat(starsystem.linkPortals);
		this.bodies = this.bodies.concat(starsystem.planets);
		this.bodies = this.bodies.concat(starsystem.ships);
	}

	VenueStarsystem.prototype.updateForTimerTick = function()
	{
		this.venueControls.updateForTimerTick();

		var camera = this.camera;
		var cameraConstraints = camera.constraints;
		for (var i = 0; i < cameraConstraints.length; i++)
		{
			var constraint = cameraConstraints[i];
			constraint.applyToBody(camera);
		}

		if (this.cursor != null)
		{
			var constraints = this.cursor.constraints;
			for (var i = 0; i < constraints.length; i++)
			{
				var constraint = constraints[i];
				constraint.applyToBody(this.cursor);
			}
		}

		var bodies = this.starsystem.ships;
		for (var i = 0; i < bodies.length; i++)
		{
			var body = bodies[i];
			var bodyDefnName = body.defn.name;

			if (bodyDefnName == "Ship")
			{
				var ship = body;

				var shipOrder = ship.order;
				if (shipOrder != null)
				{
					shipOrder.obey(ship);
				}

				var shipActivity = ship.activity;
				if (shipActivity != null)
				{
					shipActivity.perform(ship);
				}				
			}
		}

		this.draw();

		var inputHelper = Globals.Instance.inputHelper;

		var keyCode = inputHelper.keyCodePressed;
		if (keyCode == 65) // A
		{
			new Action_CylinderMove_Yaw(-.01).perform(camera);
		}
		else if (keyCode == 68) // D
		{
			new Action_CylinderMove_Yaw(.01).perform(camera);
		}
		else if (keyCode == 70) // F
		{
			new Action_CylinderMove_DistanceAlongAxis(10).perform(camera);
		}
		else if (keyCode == 82) // R
		{
			new Action_CylinderMove_DistanceAlongAxis(-10).perform(camera);
		}
		else if (keyCode == 83) // S
		{
			new Action_CylinderMove_Radius(10).perform(camera);
		}
		else if (keyCode == 87) // W
		{
			new Action_CylinderMove_Radius(-10).perform(camera);
		}
		else if (inputHelper.isMouseLeftPressed == true)
		{
			inputHelper.isMouseLeftPressed = false;
			Globals.Instance.soundHelper.soundWithNamePlayAsEffect("Click");
			var mouseClickPos = inputHelper.mouseClickPos.clone().subtract
			(
				camera.viewSizeHalf
			);

			var rayFromCameraThroughClick = camera.rayToViewPos(mouseClickPos);

			var bodiesClickedAsCollisions = Collision.rayAndBodies
			(
				rayFromCameraThroughClick,
				this.bodies,
				10, // bodyRadius
				[]
			);
			
			var bodyClicked;

			if (bodiesClickedAsCollisions.length == 0)
			{				
				bodyClicked = null;
			}
			else
			{
				var bodiesClickedAsCollisionsSorted = [];

				for (var i = 0; i < bodiesClickedAsCollisions.length; i++)
				{
					var collisionToSort = bodiesClickedAsCollisions[i];

					var j = 0;
					for (j = 0; j < bodiesClickedAsCollisionsSorted.length; j++)
					{						
						var collisionSorted = bodiesClickedAsCollisionsSorted[j];

						if (collisionToSort.distance < collisionSorted.distance)
						{
							break;
						}
					}

					bodiesClickedAsCollisionsSorted.splice
					(
						j, 0, collisionToSort 
					);					
				}

				var numberOfCollisions = bodiesClickedAsCollisionsSorted.length;
				if (this.selection == null || numberOfCollisions == 1)
				{
					bodyClicked = bodiesClickedAsCollisionsSorted[0].colliders[0];
				}
				else
				{
					for (var c = 0; c < numberOfCollisions; c++)
					{
						var collision = bodiesClickedAsCollisionsSorted[c];
						bodyClicked = collision.colliders[0];
	
						if (bodyClicked == this.selection)
						{
							var cNext = c + 1;
							if (cNext >= numberOfCollisions)
							{
								cNext = 0;
							}
							collision = bodiesClickedAsCollisionsSorted[cNext];
							bodyClicked = collision.colliders[0];
							break;
						}	
					}
				}
			}

			if (this.selection == null)
			{
				this.selection = bodyClicked;
			}
			else
			{
				var selectionDefnName = this.selection.defn.name;
				if (selectionDefnName == "Cursor")
				{
					var cursor = this.selection;

					if (bodyClicked != null && bodyClicked.defn.name != "Cursor")
					{
						var targetBody = bodyClicked;

						var ship = cursor.bodyParent;

						ship.order = new Order
						(
							"Go",
							targetBody	
						);

						this.cursorClear();

						Globals.Instance.inputHelper.isEnabled = false;
					}
					else if (cursor.hasXYPositionBeenSpecified == false)
					{
						cursor.hasXYPositionBeenSpecified = true;
					}
					else if (cursor.hasZPositionBeenSpecified == false)
					{
						var targetBody = new Body
						(
							"Target", 
							new BodyDefn
							(
								"MoveTarget", 
								new Coords(0, 0, 0)
							), 
							cursor.loc.pos.clone()
						); 

						var ship = cursor.bodyParent;

						ship.order = new Order
						(
							"Go",
							targetBody	
						);

						this.cursorClear();

						Globals.Instance.inputHelper.isEnabled = false;
					}
	
				}
				else if (this.selection == bodyClicked)
				{
					if (selectionDefnName == "Planet")
					{
						var layout = bodyClicked.layout;
						var venueNext = new VenueLayout(this, layout);
						venueNext = new VenueFader(venueNext);
						Globals.Instance.universe.venueNext = venueNext;						
					}	
				}
				else
				{
					this.selection = bodyClicked;
				}
			}
		}
	}

	// controls

	VenueStarsystem.prototype.controlBuild = function()
	{
		var returnValue = null;

		var containerMainSize = Globals.Instance.displayHelper.viewSize.clone();
		var controlHeight = 16;
		var margin = 10;

		var containerInnerSize = new Coords(100, 60);

		var buttonWidth = (containerInnerSize.x - margin * 3) / 2;

		var returnValue = new ControlContainer
		(
			"containerMain",
			[ "Transparent", "Transparent" ], // colorsForeAndBack
			new Coords(0, 0), // pos
			containerMainSize,
			// children
			[
				new ControlButton
				(
					"buttonMenu",
					new Coords
					(
						(containerMainSize.x - buttonWidth) / 2, 
						containerMainSize.y - margin - controlHeight
					), // pos
					new Coords(buttonWidth, controlHeight), // size
					"Back",
					null, // dataBindingForIsEnabled
					// click
					function()
					{
						var universe = Globals.Instance.universe;
						var world = universe.world;
						var venueNext = new VenueWorld(world);
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),

				ControlBuilder.view
				(
					containerMainSize, 
					containerInnerSize, 
					margin,
					controlHeight
				),

				ControlBuilder.timeAndPlace
				(
					containerMainSize, 
					containerInnerSize, 
					margin,
					controlHeight
				),

				ControlBuilder.selection
				(
					new Coords
					(
						containerMainSize.x - margin - containerInnerSize.x,
						margin
					),
					new Coords
					(
						containerInnerSize.x, 
						containerMainSize.y - margin * 2
					), 
					margin,
					controlHeight
				),

			]
		);

		return returnValue;
	}
}

function VenueTalkSession(venueParent, talkSession)
{
	this.venueParent = venueParent;
	this.talkSession = talkSession;
}
{
	VenueTalkSession.prototype.draw = function()
	{
		this.venueControls.draw();
	}

	VenueTalkSession.prototype.initialize = function()
	{
		var controlRoot = this.controlBuild();
		this.venueControls = new VenueControls(controlRoot);
		this.talkSession.update();
	}

	VenueTalkSession.prototype.updateForTimerTick = function()
	{

		this.venueControls.updateForTimerTick();		
	}

	// controls

	VenueTalkSession.prototype.controlBuild = function()
	{
		var containerSize = Globals.Instance.displayHelper.viewSize.clone();
		var margin = 10;
		var controlHeight = 15;

		var returnValue = new ControlContainer
		(
			"containerConfigure",
			ControlBuilder.ColorsForeAndBackDefault,
			new Coords(0, 0), // pos
			containerSize,
			// children
			[
				new ControlButton
				(
					"buttonDone",
					new Coords
					(
						margin, margin	
					), // pos
					new Coords
					(
						controlHeight, controlHeight
					), // size
					"<",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var universe = Globals.Instance.universe;
						var venueNext = universe.venueCurrent.venueParent;
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),

				new ControlButton
				(
					"buttonLog",
					new Coords
					(
						containerSize.x - margin - controlHeight * 2, margin	
					), // pos
					new Coords
					(
						controlHeight * 2, controlHeight
					), // size
					"Log",
					null, // dataBindingForIsEnabled
					// click
					function ()
					{
						var talkSession = Globals.Instance.universe.venueCurrent.talkSession;
						alert(talkSession.log.join("\n"));
					}
				),

				new ControlLabel
				(
					"labelTalk",
					new Coords(margin, controlHeight + margin * 2), // pos
					new Coords
					(
						containerSize.x - margin * 2, 
						controlHeight
					), // size
					false, // isTextCentered
					new DataBinding(this.talkSession, "displayTextCurrent")
				),


				new ControlSelect
				(
					"listResponses",
					new Coords
					(
						margin, controlHeight * 2 + margin * 3
					), // pos
					new Coords
					(
						containerSize.x - margin * 2, 
						controlHeight * 4
					), // size
					// dataBindingForValueSelected
					new DataBinding(this.talkSession, "optionSelected"),
					// options
					new DataBinding
					(
						this.talkSession, "optionsAvailable"
					),
					null, // bindingExpressionForOptionValues
					"text", // bindingExpressionForOptionText
					new DataBinding(true), // isEnabled
					4 // numberOfItemsVisible
				),

				new ControlButton
				(
					"buttonContinue",
					new Coords
					(
						margin, controlHeight * 6 + margin * 4	
					), // pos
					new Coords
					(
						containerSize.x - margin * 2, 
						controlHeight
					), // size
					"Continue",
					// dataBindingForIsEnabled
					new DataBinding(this.talkSession, "hasResponseBeenSpecified"), 
					// click
					this.talkSession.respond.bind(this.talkSession)
				),	
			]
		);

		return returnValue;
	}
}

function VenueTechnologyResearchSession(researchSession)
{
	this.researchSession = researchSession;

	this.venueControls = new VenueControls
	(
		this.researchSession.controlBuild(Globals.Instance.displayHelper.viewSize)
	);
}
{
	VenueTechnologyResearchSession.prototype.draw = function()
	{
		this.venueControls.draw();
	}

	VenueTechnologyResearchSession.prototype.initialize = function()
	{
		// do nothing
	}

	VenueTechnologyResearchSession.prototype.updateForTimerTick = function()
	{
		this.venueControls.updateForTimerTick();
	}
}

function VenueVideo(videoName, venueNext)
{
	this.videoName = videoName;
	this.venueNext = venueNext;

	this.hasVideoBeenStarted = false;
}
{
	VenueVideo.prototype.draw = function()
	{
		// do nothing
	}

	VenueVideo.prototype.initialize = function()
	{
		// do nothing
	}

	VenueVideo.prototype.updateForTimerTick = function()
	{
		if (this.video == null)
		{
			Globals.Instance.displayHelper.hide();
			this.video = Globals.Instance.videoHelper.videos[this.videoName];
			this.video.play();
		}	

		var inputHelper = Globals.Instance.inputHelper;
		if (inputHelper.isMouseLeftPressed == true)
		{
			inputHelper.isMouseLeftPressed = false;
			this.video.stop();
		}

		if (this.video.isFinished == true)
		{
			var displayHelper = Globals.Instance.displayHelper;
			displayHelper.clear("Black");
			displayHelper.show();
			var universe = Globals.Instance.universe;
			universe.venueNext = this.venueNext;
		}
	}
}

function VenueWorld(world)
{
	this.world = world;
	this.model = this.world;

	this.camera = this.world.camera;

	this.venueControls = new VenueControls
	(
		this.controlBuild()
	);
}
{
	// controls

	VenueWorld.prototype.controlBuild = function()
	{
		var returnValue = null;

		var containerMainSize = Globals.Instance.displayHelper.viewSize.clone();
		var controlHeight = 16;

		var margin = 10;

		var containerInnerSize = new Coords(100, 60);

		var buttonWidth = (containerInnerSize.x - margin * 3) / 2;

		var faction = Globals.Instance.universe.world.factionCurrent();

		var returnValue = new ControlContainer
		(
			"containerMain",
			[ "Transparent", "Transparent" ], // colorsForeAndBack
			new Coords(0, 0), // pos
			containerMainSize,
			// children
			[
				new ControlButton
				(
					"buttonMenu",
					new Coords
					(
						(containerMainSize.x - buttonWidth) / 2, 
						containerMainSize.y - margin - controlHeight
					), // pos
					new Coords(buttonWidth, controlHeight), // size
					"Menu",
					null, // dataBindingForIsEnabled
					// click
					function()
					{
						var universe = Globals.Instance.universe;
						var venueNext = new VenueControls
						(
							ControlBuilder.configure()
						);
						venueNext = new VenueFader(venueNext);
						universe.venueNext = venueNext;
					}
				),

				faction.controlBuild
				(
					containerMainSize,
					containerInnerSize, 
					margin, 
					controlHeight,
					buttonWidth
				),

				ControlBuilder.view
				(
					containerMainSize, 
					containerInnerSize, 
					margin,
					controlHeight
				),

				ControlBuilder.timeAndPlace
				(
					containerMainSize, 
					containerInnerSize, 
					margin,
					controlHeight
				),

				ControlBuilder.selection
				(
					new Coords
					(
						containerMainSize.x - margin - containerInnerSize.x,
						containerMainSize.y - margin - containerInnerSize.y
					),					containerInnerSize, 
					margin,
					controlHeight
				),
			]
		);

		return returnValue;
	}

	// venue 

	VenueWorld.prototype.draw = function()
	{
		var displayHelper = Globals.Instance.displayHelper;

		displayHelper.clear();

		displayHelper.drawNetworkForCamera
		(
			this.world.network,
			this.world.camera
		);

		this.venueControls.draw();
	}

	VenueWorld.prototype.finalize = function()
	{
		Globals.Instance.soundHelper.soundForMusic.pause();		
	}

	VenueWorld.prototype.initialize = function()
	{
		var soundHelper = Globals.Instance.soundHelper;
		soundHelper.soundWithNamePlayAsMusic("Music");
	}

	VenueWorld.prototype.updateForTimerTick = function()
	{
		this.venueControls.updateForTimerTick();

		var world = this.world;				
		var camera = world.camera;

		var inputHelper = Globals.Instance.inputHelper;
		if (inputHelper.isMouseLeftPressed == true)
		{
			inputHelper.isMouseLeftPressed = false;
			Globals.Instance.soundHelper.soundWithNamePlayAsEffect("Click");
			var mouseClickPos = inputHelper.mouseClickPos.clone().subtract
			(
				camera.viewSizeHalf
			);

			var rayFromCameraThroughClick = camera.rayToViewPos(mouseClickPos);

			var bodiesClickedAsCollisions = Collision.rayAndBodies
			(
				rayFromCameraThroughClick,
				Globals.Instance.universe.world.network.nodes,
				NetworkNode.RadiusActual,
				[] // listToAddTo
			);	
			
			if (bodiesClickedAsCollisions.length > 0)
			{
				var collisionNearest = bodiesClickedAsCollisions[0];

				for (var i = 1; i < bodiesClickedAsCollisions.length; i++)
				{
					var collision = bodiesClickedAsCollisions[i];
					if (collision.distance < collisionNearest.distance)
					{
						collisionNearest = collision;
					}
				}

				var bodyClicked = collisionNearest.colliders[0]; // todo

				if (bodyClicked == this.selection)
				{				
					var venueNext = new VenueStarsystem(bodyClicked.starsystem);
					venueNext = new VenueFader(venueNext);
					Globals.Instance.universe.venueNext = venueNext;
				}

				this.selection = bodyClicked;
			}
		}

		var keyCode = inputHelper.keyCodePressed;

		if (keyCode == 27) // escape
		{
			var universe = Globals.Instance.universe;
			var venueNext = new VenueControls
			(
				ControlBuilder.configure()
			);
			venueNext = new VenueFader(venueNext);
			universe.venueNext = venueNext;
		}

		var cameraSpeed = 20;
		var displacementToMoveCamera = null;

		if (keyCode == 65) // A
		{
			displacementToMoveCamera = [cameraSpeed, 0];
		}
		else if (keyCode == 68) // D
		{
			displacementToMoveCamera = [0 - cameraSpeed, 0];
		}
		else if (keyCode == 83) // S
		{
			displacementToMoveCamera = [0, 0 - cameraSpeed];
		}
		else if (keyCode == 87) // W
		{
			displacementToMoveCamera = [0, cameraSpeed];
		}

		if (displacementToMoveCamera != null)
		{
			new Action_CameraMove(displacementToMoveCamera).perform(camera);
		}
	}
	
}

function Video(name, sourcePath)
{
	this.name = name;
	this.sourcePath = sourcePath;
}
{
	Video.prototype.domElementBuild = function()
	{
		this.domElement = document.createElement("video");
		this.domElement.src = this.sourcePath;
		this.domElement.video = this;
		this.domElement.autoplay = true;
		this.domElement.onended = this.stop.bind(this);

		var viewSize = Globals.Instance.displayHelper.viewSize;
		this.domElement.width = viewSize.x;
		this.domElement.height = viewSize.y;

		return this.domElement;
	}

	Video.prototype.play = function()
	{
		this.isFinished = false;
		Globals.Instance.divMain.appendChild(this.domElementBuild());
	}

	Video.prototype.stop = function(event)
	{
		var domElement = (event == null ? this.domElement : event.srcElement);
		Globals.Instance.divMain.removeChild(domElement);
		this.isFinished = true;
	}
}

function VideoHelper(videos)
{
	this.videos = videos;
	this.videos.addLookups("name");
}

function VisualGroup(visuals)
{
	this.visuals = visuals;	
}
{
	VisualGroup.prototype.draw = function(drawPos)
	{
		for (var i = 0; i < this.visuals.length; i++)
		{
			var visual = this.visuals[i];
			visual.draw(drawPos);
		}
	}
}

function VisualRectangle(color, size)
{
	this.color = color;
	this.size = size;
	this.sizeHalf = this.size.clone().divideScalar(2);
}
{
	VisualRectangle.prototype.draw = function(drawPos)
	{
		var graphics = Globals.Instance.displayHelper.graphics;
		graphics.strokeStyle = this.color.systemColor;

		graphics.beginPath();
		graphics.strokeRect
		(
			drawPos.x - this.sizeHalf.x, 
			drawPos.y - this.sizeHalf.y, 
			this.size.x, this.size.y
		);
		graphics.stroke();
	}
}

function VisualSphere(color, radius)
{
	this.color = color;
	this.radius = radius;
}
{
	VisualSphere.prototype.draw = function(drawPos)
	{
		var radius = this.radius;
		var graphics = Globals.Instance.displayHelper.graphics;
		graphics.strokeStyle = this.color.systemColor;

		graphics.beginPath();
		graphics.arc
		(
			drawPos.x, drawPos.y, 
			radius, 
			0, 2 * Math.PI, // start and stop angles 
			false // counterClockwise
		);
		graphics.stroke();
	}
}

function VisualText(text)
{
	this.text = text;
}
{
	VisualText.prototype.draw = function(drawPos)
	{
		var graphics = Globals.Instance.displayHelper.graphics;
		graphics.fillStyle = "LightGray";
		graphics.fillText
		(
			this.text,
			drawPos.x, drawPos.y
		);
	}
}

function World(name, network, factions, ships, camera)
{
	this.name = name;
	this.network = network;
	this.factions = factions;
	this.ships = ships;
	this.camera = camera;
	this.dateCreated = DateTime.now();
	this.dateSaved = this.dateCreated;

	this.factions.addLookups("name");
	this.ships.addLookups("name");

	this.turnsSoFar = 0;
	this.factionIndexCurrent = 0;
}
{
	// static methods

	World.new = function()
	{
		var worldName = NameGenerator.generateName() + " Cluster";

		var viewSize = Globals.Instance.displayHelper.viewSize.clone();
		var viewDimension = viewSize.y;

		var networkRadius = viewDimension * .35;
		var numberOfNetworkNodes = 6; // 128;
		var network = Network.generateRandom
		(
			"Test Network",
			NetworkNodeDefn.Instances._All,
			numberOfNetworkNodes,
			// minAndMaxDistanceOfNodesFromOrigin
			[ networkRadius / 2, networkRadius ],
			20 // distanceBetweenNodesMin
		);

		var focalLength = viewDimension;
		viewSize.z = focalLength;

		var numberOfFactions = 6;
		var factions = [];
		var ships = [];

		var colors = Color.Instances;
		var colorsForFactions = 
		[
			colors.Red,
			colors.Orange,
			colors.YellowDark,
			colors.Green,
			colors.Blue,
			colors.Violet,
		];

		for (var i = 0; i < numberOfFactions; i++)
		{
			var factionHomeStarsystem = null;

			var random = Math.random();
			var starsystemIndexStart = Math.floor
			(
				random * numberOfNetworkNodes
			);

			var starsystemIndex = starsystemIndexStart;

			while (factionHomeStarsystem == null)
			{
				factionHomeStarsystem = network.nodes[starsystemIndex].starsystem;
				if (factionHomeStarsystem.planets.length == 0)
				{
					factionHomestarsystem = null;
				}
				else if (factionHomeStarsystem.factionName != null)
				{
					factionHomeStarsystem = null;
				}

				starsystemIndex++;
				if (starsystemIndex >= numberOfNetworkNodes)
				{
					starsystemIndex = 0;
				}

				if (starsystemIndex == starsystemIndexStart)
				{
					throw "There are more factions than starsystems with planets.";
				}
			}

			var factionName = factionHomeStarsystem.name + "ians";
			factionHomeStarsystem.factionName = factionName;
			var factionColor = colorsForFactions[i];

			var ship = new Ship
			(
				factionName + " Ship0",
				Ship.bodyDefnBuild(factionColor),
				new Coords().randomize().multiply
				(
					factionHomeStarsystem.size
				).multiplyScalar
				(
					2
				).subtract
				(
					factionHomeStarsystem.size
				),
				factionName
			);
			ships.push(ship);
			factionHomeStarsystem.ships.push(ship);

			var faction = new Faction
			(
				factionName,
				factionColor,	
				[], // relationships 
				new TechnologyResearcher
				(
					factionName + "_Research",
					null, // nameOfTechnologyBeingResearched,
					0, // researchAccumulated
					// namesOfTechnologiesKnown
					[]					
				), 
				[
					factionHomeStarsystem.planets[0]
				], 
				[
					ship
				],
				new FactionKnowledge
				(
					[ factionHomeStarsystem ],
					[ factionHomeStarsystem.links() ]
				)
			);
			factions.push(faction);

		}	

		DiplomaticRelationship.initializeForFactions(factions);	

		var camera = new Camera
		(
			viewSize, 
			focalLength, 
			new Coords(-viewDimension, 0, 0), //pos, 
			new Orientation
			(
				new Coords(1, 0, 0), // forward
				new Coords(0, 0, 1) // down
			)
		);

		var returnValue = new World
		(
			worldName,
			network,
			factions,
			ships,
			camera
		);
		return returnValue;
	}

	// instance methods

	World.prototype.factionCurrent = function()
	{
		return this.factions[this.factionIndexCurrent];
	}

	World.prototype.factionsOtherThanCurrent = function()
	{
		var factionCurrent = this.factionCurrent();
		var returnValues = this.factions.slice();
		returnValues.splice
		(
			this.factionIndexCurrent, 1	
		);
		return returnValues;
	}

	World.prototype.updateForTurn = function()
	{
		this.network.updateForTurn();
	
		for (var i = 0; i < this.factions.length; i++)
		{
			var faction = this.factions[i];
			faction.updateForTurn();
		}

		this.turnsSoFar++;
	}
}

// run

main();

</script>
</body>
</html>

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

Connecting to %s