<!--

// JavaScript Document

/////////////////////////////////
// Game Object Functions

function AssignID(oObject)
{
	// give a new object a unique ID
	oObject._ID = nextID;
	nextID += 1;
}

/////////////////////////////////
// Visibility Functions

function Show(elementID)
{
	document.getElementById(elementID).style.visibility="visible";
}

function Hide(elementID)
{
	document.getElementById(elementID).style.visibility="hidden";
}

/////////////////////////////////
// Movement Functions

function RectangleAcceleration(oObject, direction)
{
	switch (direction)
	{
		case "left":
			if ((oObject._xSpeed) > oObject._maxSpeed * -1)
			{
				oObject._xSpeed = Math.max(oObject._xSpeed - bucketAcceleration, oObject._maxSpeed * -1);
			}
			break;
		case "right":
			if ((oObject._xSpeed) < oObject._maxSpeed)
			{
				oObject._xSpeed = Math.min(oObject._xSpeed + bucketAcceleration, oObject._maxSpeed);
			}
			break;
		case "up":
			if ((oObject._ySpeed) > oObject._maxSpeed * -1)
			{
				oObject._ySpeed = Math.max(oObject._ySpeed - bucketAcceleration, oObject._maxSpeed * -1);
			}
			break;
		case "down":
			if ((oObject._ySpeed) < oObject._maxSpeed)
			{
				oObject._ySpeed = Math.min(oObject._ySpeed + bucketAcceleration, oObject._maxSpeed);
			}
			break;
	}
}

function AccelerateUnderGravity(oBall)
{
	// change the velocity directly to accelerate under gravity
	// find actual velocity components
	var real_vx = oBall._vx * oBall._speed;
	var real_vy = oBall._vy * oBall._speed;
	
	if (real_vy < terminalVelocity)
	{
		real_vy += GravityAcceleration
	}
	
	// convert back to speed, unit vector
	oBall._speed = VectorMagnitude(real_vx, real_vy);
	
	// if speed very low, set default speed & direction to avoid dividing by 0
	if (oBall._speed < 0.001)
	{
		oBall._vx = 0;
		oBall._vy = 1;
		oBall._speed = 0;
	}
	else
	{
		oBall._vx = real_vx / oBall._speed;
		oBall._vy = real_vy / oBall._speed;
	}
	return;
}

function AbsoluteAcceleration(oBall, delta_vx, delta_vy)
{
	// change the velocity directly
	// find actual velocity components
	var real_vx = oBall._vx * oBall._speed;
	var real_vy = oBall._vy * oBall._speed;
	
	real_vx += delta_vx;
	real_vy += delta_vy;
	
	// convert back to speed, unit vector
	oBall._speed = Math.min(VectorMagnitude(real_vx, real_vy), ballMaxSpeed);
	
	// if speed very low, set default speed & direction to avoid dividing by 0
	if (oBall._speed < 0.001)
	{
		oBall._vx = 0;
		oBall._vy = 1;
		oBall._speed = 0;
	}
	else
	{
		oBall._vx = real_vx / oBall._speed;
		oBall._vy = real_vy / oBall._speed;
	}
	return;
}

function TeleportObject(oObject, x_pos, y_pos)
{
	// moves an object without changing its velocity
	// moves it to position x, y
	oObject._x = x_pos;
	oObject._y = y_pos;
	PlaceObject(oObject);
	return;
}

function TranslateObject(oObject, x_move, y_move)
{
	// moves an object without changing its velocity
	// moves it by a relative amount x, y
	oObject._x = oObject._x + x_move;
	oObject._y = oObject._y + y_move;
	
	PlaceObject(oObject);
	return;
}

function PlaceObject(oObject)
{
	// record the object's centre position for use in collision calculations
	// this saves recalculating it every time and potentially making a mistake
	oObject._centreX = CentreX(oObject);
	oObject._centreY = CentreY(oObject);
	
	// check whether the x, y values are valid
	if (isNaN(oObject._x))
	{
		alert("Place Object; ID: " + oObject._ID + ", type: " + oObject._objectType + ", ._x is NaN");
	}
	if (isNaN(oObject._y))
	{
		alert("Place Object; ID: " + oObject._ID + ", type: " + oObject._objectType + ", ._y is NaN");
	}
	
	// sets the object's left, top from its x, y properties
	oObject.style.left = Math.round(oObject._x) +'px';
	oObject.style.top = Math.round(oObject._y) +'px';
	return;
}

function CentreX(oObject)
{
	return oObject._x + (oObject._width / 2);
}

function CentreY(oObject)
{
	return oObject._y + (oObject._height / 2);
}

function MoveObject(oObject, topLine)
{
	oObject._speed = Math.min(oObject._speed, oObject._maxSpeed);
	
	// check whether the velocity values are valid
	if (isNaN(oObject._speed))
	{
		alert("MoveObject; ID: " + oObject._ID + ", type: " + oObject._objectType + ", ._speed is NaN");
	}
	if (isNaN(oObject._vx))
	{
		alert("MoveObject; ID: " + oObject._ID + ", type: " + oObject._objectType + ", ._vx is NaN");
	}
	if (isNaN(oObject._vy))
	{
		alert("MoveObject; ID: " + oObject._ID + ", type: " + oObject._objectType + ", ._vy is NaN");
	}
	// check (oObject._x))
	if (isNaN(oObject._x))
	{
		alert("MoveObject; ID: " + oObject._ID + ", type: " + oObject._objectType + ", ._x is NaN");
	}
	if (isNaN(oObject._y))
	{
		alert("MoveObject; ID: " + oObject._ID + ", type: " + oObject._objectType + ", ._y is NaN");
	}
	
	StayInMapArea(oObject, topLine);
	
	// updates the position of an object based on its velocity
	// x & y positions must be integers
	// but the positions are saved as floats so that changes of position < 1 will be logged
	// speed is pixels per seconds

	var distanceTravelled = oObject._speed * (gameTick / 1000);
	
	oObject._x += distanceTravelled * oObject._vx;
	oObject._y += distanceTravelled * oObject._vy;

	PlaceObject(oObject);

	if (oObject._rank)
	{
		// Draw bigger balls over the top of smaller ones
		// will probably want to change this when there are other types of objects than balls
		// only execute this if object has rank, otherwise error!	
		oObject.style.zIndex = 100 - oObject._rank;
	}
	return;
}

function BallRectangleBounce(oBall, oRectangle, solid_top, solid_right, solid_bottom, solid_left)
{
	// designed to handle balls bouncing off solid rectangles or buckets with an open edge
	// "solid" parameters are flags, true indicates this is a solid edge
	// e.g. for bucket with open top, pass (oBall, oBucket, false, true, true, true)
	
	// find where the ball is colliding with the rectangle, if at all
	
	var outcome = BallRectangleCollision(oBall, oRectangle);
	var collisionLocation;
		
	if ((outcome._horizontal == "nocollision")) // only 1 needs to be checked
	{
		return "nocollision"; // do nothing to ball
	}
	if ((outcome._horizontal == "middle") && (outcome._vertical == "middle"))
	{
		return "inside"; // do nothing to ball
	}
	
	switch (outcome._horizontal + outcome._vertical)
	{
		case "middletop": // if top solid, bounce ball off top of rectangle
			if (solid_top || (oBall._centreX <= (oRectangle._x + oRectangle._thickness))
			 || (oBall._centreX >= (oRectangle._x + oRectangle._width - oRectangle._thickness)))
			// also bounce off if ball hits within wall thickness
			// should add this to the other 4 sides to cover bucket on its side
			{
				WallCollisionAcceleration(oBall, oRectangle, outcome);
				return "topbounce";
			}
			else { return "topentry"; } // ball is passing through top of rectangle
		case "rightmiddle": // if right side solid, bounce ball off right side of rectangle
			if (solid_right)
			{
				WallCollisionAcceleration(oBall, oRectangle, outcome);
				return "rightbounce";
			}
			else { return "rightentry"; } // ball is passing through right side of rectangle
		case "middlebottom": // if bottom solid, bounce ball off bottom of rectangle
			if (solid_bottom) 
			{
				WallCollisionAcceleration(oBall, oRectangle, outcome);
				return "bottombounce";
			}
			else { return "bottomentry"; } // ball is passing through bottom of rectangle	
		case "leftmiddle": // if left side solid, bounce ball off left side of rectangle
			if (solid_left)
			{
				WallCollisionAcceleration(oBall, oRectangle, outcome);
				return "leftbounce";
			}
			else { return "leftentry"; } // ball is passing through left side of rectangle
		case "lefttop":
			// top left corner collision
			if (solid_left || solid_top) // bounce if solid top or left side to rectangle
			{
				// corner collision only if ball close enough to corner
				{
					WallCollisionAcceleration(oBall, oRectangle, outcome);
				}
				return "topleftbounce";
			}
		case "righttop":
			// right top corner collision	
			if (solid_right || solid_top) // bounce if solid top or right side to rectangle
			{
				WallCollisionAcceleration(oBall, oRectangle, outcome);
				return "toprightbounce";
			}
		case "leftbottom":
			// bottom left corner collision
			if (solid_left || solid_bottom) // bounce if solid bottom or left side to rectangle
			{
				WallCollisionAcceleration(oBall, oRectangle, outcome);
				return "bottomleftbounce";
			}
		case "rightbottom":
			// right bottom corner collision
			if (solid_right || solid_bottom) // bounce if solid bottom or right side to rectangle
			{
				WallCollisionAcceleration(oBall, oRectangle, outcome);
				return "toprightbounce";
			}
	}
}

function WallCollisionAcceleration(oBall, oRectangle, outcome)
{
	// bucket uses ._xSpeed, but other objects use ._vx & ._speed, so pick the speed component that exists
	var rectangle_xSpeed = Math.abs(oRectangle._xSpeed != null)? oRectangle._xSpeed : (oRectangle._vx * oRectangle._speed);
	var rectangle_ySpeed = Math.abs(oRectangle._ySpeed != null)? oRectangle._ySpeed : (oRectangle._vy * oRectangle._speed);
	
	switch (outcome._horizontal)
	{
		case "left": x_factor = -1; break;
		case "right": x_factor = 1; break;
		case "middle": x_factor = 0; break;
	}
	switch (outcome._vertical)
	{
		case "top": y_factor = -1; break;
		case "bottom": y_factor = 1; break;
		case "middle": y_factor = 0; break;
	}
	if ((x_factor != 0) && (y_factor != 0)) // corner collision
	{
		// exchange x and y velocity components: sign change will be handled by top / side collisions
		var new_vx = oBall._vy;
		var new_vy = oBall._vx;
		oBall._vx = new_vx;
		oBall._vy = new_vy;
	}
	if (outcome._vertical != "middle") // a top or bottom collision
	{
		// ball velocity after collision must be upwards
		oBall._vy = y_factor * Math.abs(oBall._vy);
	}
	else if (outcome._horizontal != "middle") // a left or right collision
	{
		// ball velocity after collision is rightwards
		oBall._vx = x_factor * Math.abs(oBall._vx);
	}
	//AbsoluteAcceleration(oBall, (rectangle_xSpeed/5), (rectangle_xSpeed/5));
	
	if (outcome._horizontal == "left")
	{
		// push ball beyond left edge of rectangle
		oBall._x = oRectangle._x - (oBall._width/2) - Math.abs(outcome._x_offset) - 1 + (rectangle_xSpeed * (gameTick / 1000));
	}
	else if (outcome._horizontal == "right")
	{
		// push ball beyond right edge of rectangle
		oBall._x = oRectangle._x + oRectangle._width - (oBall._width/2) + Math.abs(outcome._x_offset) + 1 + (rectangle_xSpeed * (gameTick / 1000));
	}
	
	if (outcome._vertical == "top")
	{
		// push ball beyond top edge of rectangle
		oBall._y = oRectangle._y - (oBall._height/2) - Math.abs(outcome._y_offset) - 1 + (rectangle_ySpeed * (gameTick / 1000));
	}
	else if (outcome._vertical == "bottom")
	{
		// push ball beyond bottom edge of rectangle
		oBall._y = oRectangle._y + oRectangle._height - (oBall._height/2) + Math.abs(outcome._y_offset) + 1 + (rectangle_ySpeed * (gameTick / 1000));
	}

	PlaceObject(oBall);
}	 

function BallRectangleCollision(oBall, oRectangle)
{
	// detects collision between ball and rectangle, based on ball centre
	// space around the rectangle is divided into collision boxes
	// e.g. 'lefttop' means the ball is colliding with the left top corner of the rectangle. 'leftmiddle' means it is colliding with the left edge of the rectangle (from outside). 'middlemiddle' means the ball centre is inside the rectangle
	// corner collisions get extra consideration
	
	var radius; // collision radius of ball. Relevant for explosions where current size may be less than actual width
	var horizontal;
	var vertical;
	// for corner collisions, the offsets will correct the final ball position
	var x_offset = oBall._width/2; // default - works for edge collisions
	var y_offset = oBall._height/2; // default - works for edge collisions
	
	// if a collision radius hasn't been defined, use half the width
	if (!oBall._radius) { radius = oBall._width/2; }
	else { radius = oBall._radius; }
	
	// if the ball is outside the rectangle's collision zone, no collision
	// too far left
	if (oBall._centreX < oRectangle._x - radius)
	{
		return {_horizontal:"nocollision", _vertical:"nocollision"};
	}
	// too far right
	if (oBall._centreX > oRectangle._x + oRectangle._width + radius)
	{
		return {_horizontal:"nocollision", _vertical:"nocollision"};
	}
	// too far up
	if (oBall._centreY < oRectangle._y - radius)
	{
		return {_horizontal:"nocollision", _vertical:"nocollision"};
	}
	// too far down
	if (oBall._centreY > oRectangle._y + oRectangle._height + radius)
	{
		return {_horizontal:"nocollision", _vertical:"nocollision"};
	}
	
	// if there is a collision, find the horizontal collision region
	if (oBall._centreX <= oRectangle._x) { horizontal = "left"; }
	else if (oBall._centreX >= oRectangle._x + oRectangle._width) { horizontal = "right"; }
	else { horizontal = "middle"; }
	
	// and find the vertical colllision region
	if (oBall._centreY <= oRectangle._y) { vertical = "top"; }
	else if (oBall._centreY >= oRectangle._y + oRectangle._height) { vertical = "bottom" }
	else { vertical = "middle"; }
	
	if ((horizontal != "middle") && (vertical != "middle")) // corner collision
	{	
		// find distance from ball centre to corner
		var corner_x = oRectangle._x;
		var corner_y = oRectangle._y;
		if (vertical == "bottom")
		{
			corner_y += oRectangle._height;
		}
		if (horizontal == "right")
		{
			corner_x += oRectangle._width;
		}
		
		var x = corner_x - (oBall._x + (oBall._width/2));
		var y = corner_y - (oBall._y + (oBall._height/2));
		var distance = VectorMagnitude(x, y);
	
		if( distance >= radius) // ball too far from corner to hit it
		{
			return {_horizontal:"nocollision", _vertical:"nocollision"};
		}
		// find offset required to keep ball outside rectangle corner
		var sin_angle;
		if (distance <= 1) { sin_angle = 0.707} // if very close to corner, move out at 45 degrees
		else {sin_angle = y / distance; }
		x_offset = sin_angle * radius;
		y_offset = Math.cos(Math.asin(sin_angle)) * radius;
	}
	return {_horizontal:horizontal, _vertical:vertical, _x_offset:x_offset, _y_offset:y_offset};
}

function StayInMapArea(oBall, topLine)
{
	// don't let objects go off the edge of the map
	if (oBall._x <= leftEdge)
	{
		oBall._vx *= -1;
		oBall._x = leftEdge + 1;
		oBall._centreX = CentreX(oBall);
		oBall.style.left = oBall._x+'px';
	}
	if (oBall._x + oBall._width >= rightEdge)
	{
		oBall._vx *= -1;
		oBall._x = rightEdge - 1 - oBall._width;
		oBall._centreX = CentreX(oBall);
		oBall.style.left = oBall._x+'px';
	}
	if (oBall._y <= topLine)
	{
		oBall._vy *= -1;
		oBall._y = topLine + 1;
		oBall._centreY = CentreY(oBall);
		oBall.style.top = oBall._y+'px';
	}
	if (oBall._y + (oBall._height)>= bottomEdge)
	{
		oBall._vy *= -1;
		oBall._y = bottomEdge - 1 - (oBall._height);
		oBall._centreY = CentreY(oBall);
		oBall.style.top = oBall._y+'px';
		
		// this is a hack to turn red balls blue on their first bounce
		if (oBall._colour == "red")
		{
			SetBallColour(oBall, "blue");
		}	 
	}
	return true;
}

function ForceIntoMap(oObject)
{
  // if an object is outside the map area, force it into the map
  // it is effectively 'bounced' off the walls
	if (oObject._x <= leftEdge)
	{
		oObject._x = leftEdge - oObject._x;
	}
	if (oObject._x >= rightEdge)
	{
		oObject._x = (2 * rightEdge) - oObject._x;
	}
	if (oObject._y <= gravityLine)
	{
		oObject._y = gravityLine - oObject._y;
	}
	if (oObject._y >= bottomEdge)
	{
		oObject._y = (2 * bottomEdge) - oObject._y;
	}
	PlaceObject(oObject);
	return true;
}

function Steer(dx, dy, object)
{
	// changes the object's direction by dx in the x direction, dy in the y direction
	// does not affect the object's speed
	// direction vector is always 1, 0 because it is a unit vector pointing along the x-axis
	var newVelocity = RotateVector(1 + dx, dy, (FindVectorAngle(object._vx, object._vy)));	
	var size = VectorMagnitude(newVelocity.x, newVelocity.y);
	return {x:newVelocity.x/size, y:newVelocity.y/size};
}

function Decelerate(oBall, amount)
{
	if (oBall._speed > amount)
	{
		oBall._speed -= amount;
	}
}

function Accelerate(oBall, amount)
{
	if (oBall._speed < oBall._maxSpeed - amount)
	{
		oBall._speed += amount;
	}
}

function Follow(oBall, oDestination)
{
	// follows a destination that is assumed to be moving
	// destination must have x, y, height, width
	// returns true if the ball is at the destination
	
	if (IsOverlapping(oBall, oDestination))
	{
		Decelerate(oBall, 1);
		return true;
	}
	
	else
	{
		// find the destination's position in local space
		var oDestinationLocalPos =	GetLocalCoordinates(oBall, oDestination);
		var local_x = oDestinationLocalPos.x;
		var local_y = oDestinationLocalPos.y;
		
		// if will collide with the destination in the next second, steer around it
		var avoidance_needed = AvoidObstacle(oBall, oDestination, local_x, local_y);

		// if didn't steer around destination, steer towards it
		if (avoidance_needed ==  false)
		{
			PursueDestination(oBall, local_y);
		}	
		return false;	
	}
}

function MoveTo(oBall, oDestination)
{
			
	// steers a ball towards a fixed destination
	// destination must have x, y, height, width
	// returns true if the ball is at the destination
	
	if (IsOverlapping(oBall, oDestination))
	{
		Decelerate(oBall, 1);

		return true;
	}
	
	else
	{
		// find the destination's position in local space
		var oDestinationLocalPos =	GetLocalCoordinates(oBall, oDestination);
		var local_x = oDestinationLocalPos.x;
		var local_y = oDestinationLocalPos.y;
		/*
// show where the destination is - temporary debug measure
	Destination0.style.left = oDestination._x;
	Destination0.style.top = oDestination._y; */

		// steer towards the destination
		PursueDestination(oBall, local_y);

		return false;	
	}
}

function PursueDestination(oBall, localY)
{
	// steer towards a destination
	// localX, localY are the local co-ordinates of the destination
	var steer_amount = (localY < 0) ? -0.1 : 0.1;
	var newVelocity = Steer(0, steer_amount, oBall);
	oBall._vx = newVelocity.x;
	oBall._vy = newVelocity.y;
	
	Accelerate(oBall, 1);
	
	return;
}

function AvoidObstacle(oBall, oDestination, localX, localY)
{
	// avoid running into an obstacle
	// localX, localY are the local co-ordinates of the destination
	if ((localX >= 0) && (localX <= (oBall._speed * 3) - (oBall._width)) 
	&& (localY >= (oDestination._height * -1)) && (localY < (oBall._height)))
	{
		var steer_amount = (localY < 0) ? 0.1 : -0.1;
		var newVelocity = Steer(0, steer_amount, oBall);
		oBall._vx = newVelocity.x;
		oBall._vy = newVelocity.y;

		Decelerate(oBall, 1);
		return true;
	}
	else
	{
		return false;
	}
}

function IsOverlapping(obj1, obj2)
{
	// finds whether obj1 & obj2 are overlapping based on actual width, height
	// compare distances between x midpoints of the two objects
	// and between y midpoints
	var overlapX = (Math.abs((obj1._centreX - obj2._centreX) <= ((obj1._width + obj2._width) / 2)));
	var overlapY = (Math.abs((obj1._centreY - obj2._centreY) <= ((obj1._height + obj2._height) / 2)));
	if (overlapX && overlapY)
	{
		return true;
	}
	else 
	{
		return false;
	}
}

function CircleCircleCollision(obj1, obj2)
{
	// finds whether two circles overlap based on collision radii and centres
	// returns outcome, distance, x and y components of vector between centres
	// the components are useful for example to find a force pushing the objects apart
	
	// objects should have collision radii defined, but use width/2 if not defined
	// this will work for square graphics that fill their total width & height
	if (!obj1._radius) { obj1._radius = obj1._width / 2; }
	if (!obj2._radius) { obj2._radius = obj2._width / 2; }
	
	// find vector connecting the object centres
	// still use actual width, height to find exact centre
	// assumes graphics are central within object
	var x_distance = obj2._centreX - obj1._centreX;
	var y_distance = obj2._centreY - obj1._centreY;
	var distance = VectorMagnitude(x_distance, y_distance);
	var outcome = (distance < (obj1._radius + obj2._radius)); // collision if distance between centres less than sum of radii
	return {_outcome:outcome, _distance:distance, _x_distance:x_distance, _y_distance:y_distance };
}

/////////////////////////////////
// Vector Functions

// angles are measured in radians
// angles go anticlockwise from the x-axis
// it's a browser, so the y-axis points downward

function GetUnitVector(Angle)
{
	// returns x, y components for a unit-length vector pointing at the specified angle
	// note sine is negative because y-axis points downwards in browsers
	return {_x:Math.cos(Angle), _y:(Math.sin(Angle) * -1)};
}

function VectorMagnitude(x, y)
{
	// finds the magnitude of the vector
	return Math.sqrt((x * x) + (y * y)); 
}

function DistanceBetween(Obj1, Obj2)
{
	// finds the magnitude of the vector
	return Math.sqrt(((Obj1._x - Obj2._x) * (Obj1._x - Obj2._x)) + ((Obj1._y - Obj2._y) * (Obj1._y - Obj2._y))); 
}

function LineCircleIntersection(circleX, circleY, radius, p1X, p1Y, p2X, p2Y)
{
	// finds whether the line intersects the circle
	// also returns the square of the shortest distance from centre to line
	// usually squared distance can be used for comparisons: this saves a square root operation
	// circleX, circleY defines circle centre
	// radius is circle radius
	// p1X, p1Y defines start of line
	// p2X, p2Y defines end of line
	// note the two points must be different to avoid error (line must not have 0 length)
	
	var dirX = p2X - p1X; // find vector pointing along line
	var dirY = p2Y - p1Y;
	
	var diffX = circleX - p1X; // find vector pointing from line start to circle centre
	var diffY = circleY - p1Y;
	
	// find proportion of line length travelled to reach the point closest to the circle centre
	// (projection of diff onto dir)
	var t = DotProduct(diffX, diffY, dirX, dirY) / DotProduct(dirX, dirY, dirX, dirY);
	if (t < 0) { t = 0; } // if closest point before start of line, find distance to line start
	if (t > 1) { t = 1; } // if closest point after end of line, find distance to line end
	
	var nearestX = p1X + (t * dirX); // find coordinates of point on line nearest circle centre
	var nearestY = p1Y + (t * dirY);
	
	var distanceX = circleX - nearestX; // find vector from circle centre to closest point on line
	var distanceY = circleY - nearestY;
	
	// find square of distance from circle centre to line
	var distance_squared = DotProduct(distanceX, distanceY, distanceX, distanceY);
	
	// return true if distance to line less than radius
	return {_collision: (distance_squared <= radius * radius), _distance_squared:distance_squared};
}
	

function DotProduct(x_1, y_1, x_2, y_2)
{
	// finds the dot product of two vectors
	return ((x_1 * x_2) + (y_1 * y_2));
}

function AngleBetweenVectors(x_1, y_1, x_2, y_2)
{
	// finds the angle between two vectors
	// if magnitude of either vector = 0, return an angle of 0
	var magnitude1 = VectorMagnitude(x_1, y_1);
	var magnitude2 = VectorMagnitude(x_2, y_2);
	if ((magnitude1 == 0 ) || (magnitude2 == 0))
	{
		return 0;
	}
	else
	{
		// make sure that the cos is between 1 & -1
		var cosine = DotProduct(x_1, y_1, x_2, y_2)/(magnitude1 * magnitude2);
		var cosine = Math.min(1, cosine);
		var cosine = Math.max(-1, cosine);
		return Math.acos(cosine);
	}
}

function RelativePosition(obj1, obj2)
{
	// finds the vector pointing from obj1 to obj2
	return {_x: (obj2._x - obj1._x), _y: (obj2._y - obj1._y)};
}


function RotateVector(X, Y, angle)
{
	// rotates a vector by the angle
	var new_x = (X * Math.cos(angle)) + (Y * Math.sin(angle));
	var new_y = -1 * (X * Math.sin(angle)) + (Y * Math.cos(angle));
	return {x:new_x, y:new_y};
}

function GetLocalCoordinates(obj, target)
{
	// finds the target's position in the object's local space
	// works on object centres
	// translate the axes to the origin
	// then rotate so object is pointing along the x-axis
	var rotation_angle = -1 * FindVectorAngle(obj._vx, obj._vy);
	var local_pos = RotateVector(((target._x + (target._width / 2)) - (obj._x + (obj._width / 2))), 
	((target._y + (target._height / 2)) - (obj._y + (obj._height / 2))), rotation_angle);
	return {x: local_pos.x, y: local_pos.y};
}

function FindVectorAngle(x, y)
{
	// find the angle of the vector
	// if x = 0, vector is pointing straight down or straight up
	if (x == 0)
	{
		if (y >= 0) { vec_angle = (3 / 2) * Math.PI; }
		else { vec_angle = Math.PI/2; }
	}
	else
	{
		vec_angle = Math.atan(y / x);
		// quadrants with negative x needs to have pi added
		if (x > 0)
		{
			
			if (y > 0) // quadrant 4
			{
				vec_angle = (2 * Math.PI) - vec_angle;
			}
			else // quadrant 1
			{
				vec_angle = vec_angle * -1;
			}
		}
		else // x < 0
		{
			if (y > 0) // quadrant 3
			{
				vec_angle = Math.PI - vec_angle;
			}
			else // quadrant 2
			{
				vec_angle = Math.PI - vec_angle;
			}
		}
	}
	return vec_angle;
}

function GetDestination(oBall, angle, distance)
{
  // find the x, y co-ordinates of a point at that angle and distance from the ball
  // give it a 2-pixel height and width as a default
  // therefore the top left edge (which is what we store) is offset by -1, -1
  return {_x:oBall._centreX + (Math.cos(angle) * distance) - 1,
	 _y:oBall._centreY - (Math.sin(angle) * distance) - 1,
	_width:2, _height:2 }
}

/////////////////////////////////
// Mathematical Functions

function IsInt(num)
{
	var a = parseInt(num);
	var b = parseFloat(num);
	return (a == b);
}

function FindDistance(obj1, obj2)
{
	// finds the distance in pixels between two objects
	var side1 = obj1._x - obj2._x;
	var side2 = obj1._y - obj2._y;
	var hypotenuse = (side1 * side1) + (side2 * side2);
	return Math.sqrt(hypotenuse);
}

function findInArray(array, element)
{
	// returns the position of the element in the array (first instance only)
	// if not present, returns -1
	var i;
	for (i = 0; i <= (array.length-1); i++)
	{
		if (array[i] == element)
		return i;
	}
	return -1;
}

/////////////////////////////////
// Random Functions
function RandomInteger(lim) 
{
 	// generates a random integer between 1 and the limit
	 randomSeed = (randomSeed * 125) % 2796203;
  return (randomSeed % lim) + 1;
}

function RandomNumber(lim) 
{
 	// generates a random number between 0 and the limit
 	// it has 5 decimal places
	randomSeed = (randomSeed * 125) % 2796203;
  return ((randomSeed % (lim * 100000)) + 1)/100000;
}

function RandomAngle()
{
	// generates a random angle in radians
	return (RandomNumber(2) * Math.PI);
}

function RandomNormal(mean,standard_deviation)
{
	// returns a number distributed normally with the given mean and standard deviation
	var S = 1;
	var U1;
	var U2;
	var V1;
	var V2;
	var X;
   
	while (S >= 1 || S == 0)
	{
	   	U1 = RandomNumber(0);		// U1=[0,1]
	   	U2 = RandomNumber(0);		// U2=[0,1]
	   	
	   	V1 = 2*U1 - 1;				// V1=[-1,1]
	   	V2 = 2*U2 - 1;				// V2=[-1,1]

	   	S = V1 * V1 + V2 * V2;
	}
		
	X = Math.sqrt(-2 * Math.log(S) / S) * V1;
//	Y=Math.sqrt(-2 * Math.log(S) / S) * V2		// could calculate another value if desired

//	The above is called the Polar Method and is fully described in the Ross book [Ros88].
//	X and Y will be unit normal random variables (mean = 0 and variance = 1), and can be easilly modified for different mean and variance.
	X = mean + standard_deviation * X; //  convert to a normal variable with correct mean and variance

	return X;
}

/////////////////////////////////
// UI Functions

function InitGameStatus(info_screen, header, hint, countdown)
{
	// specifies the text for whichever info screen is selected
	// this is effectively a modal dialog like Settings and uses much of the same code
	// however the player cannot close it: it closes itself after a set amount of time
	
	// if the game isn't paused, pause it
	if (isPaused == 0)
	{
		previousPauseStatus = 0;
		PauseRun();
	}
	else
	{
		previousPauseStatus = 1;
	}
	StopKeys();
	
	switch(info_screen)
	{
		case "NewLevel":
			WriteText("new_level_header", header);
			WriteText("new_level_hint", hint);
			WriteText("new_level_countdown", countdown);
			Show("NewLevel");
			break;
		case "LevelComplete":
			WriteText("level_complete_header", header);
			WriteText("level_complete_hint", hint);
			WriteText("level_complete_countdown", countdown);
			Show("LevelComplete");
			break;
		case "LevelFail":
			WriteText("level_fail_header", header);
			WriteText("level_fail_hint", hint);
			WriteText("level_fail_countdown", countdown);
			Show("LevelFail");
			break;
		case "GameWin":
			WriteText("game_win_header", header);
			WriteText("game_win_hint", hint);
			WriteText("game_win_countdown", countdown);
			Show("GameWin");
			break;
		case "GameLose":
			WriteText("game_lose_header", header);
			WriteText("game_lose_hint", hint);
			WriteText("game_lose_countdown", countdown);
			Show("GameLose");
			break;
	}
	modalDialogOpen = 1;
}
	

function NewLevelInfo()
{
	InitGameStatus("NewLevel", ("Level " + currentLevel), hintText[currentLevel], "", false)
	
	gameStatusCounter = gameStatusTime;
	NewLevelCountdown();
}

function NewLevelCountdown()
{
	if ( gameStatusCounter > 0)
	{
		WriteText("new_level_countdown", Math.round(gameStatusCounter/1000));
		gameStatusCounter -= 1000;
		timerCountdown = setTimeout("NewLevelCountdown()",1000);
	}
	else
	{
		clearTimeout(timerCountdown);
		GameStatusHide("NewLevel");
	}
}

function LevelOver()
{
	InitGameStatus("LevelComplete", ("Level " + currentLevel + " Complete"), ("Current score: " + gameScore), "", false);
		
	gameStatusCounter = gameStatusTime;
	LevelOverCountdown();
}

function LevelOverCountdown()
{
	if ( gameStatusCounter > 0)
	{
		WriteText("level_complete_countdown", "");
		gameStatusCounter -= 1000;
		timerCountdown = setTimeout("LevelOverCountdown()",1000);
	}
	else
	{
		DeleteGameObjects();
		GameStatusHide("LevelComplete");
		clearTimeout(timerCountdown);
		if (currentLevel == numberOfLevels)
		{
			GameOver("win");
			return;
		}
		else
		{
			currentLevel += 1;
			levelOver = true;
			InitGame();
		}
	}
	return;
}

function LevelFail()
{
	DeleteGameObjects();
	InitGameStatus("LevelFail", ("Lose a bucket"), ("You caught a yellow ball!"), "", false);
		
	gameStatusCounter = gameStatusTime;
	LevelFailCountdown();
}

function LevelFailCountdown()
{
	if ( gameStatusCounter > 0)
	{
		WriteText("level_fail_countdown", "");
		gameStatusCounter -= 1000;
		timerCountdown = setTimeout("LevelFailCountdown()",1000);
	}
	else
	{
		GameStatusHide("LevelFail");
		NewGame(true); //retry the level
	}
	return;
}

function GameWin()
{
	InitGameStatus("GameWin", "You have won the game", ("Score: " + gameScore), "Congratulations!", true);
}

function GameLose()
{
	DeleteGameObjects();
	InitGameStatus("GameLose", "Game Over!", ("Score: " + gameScore), "", true);
}

function GameStatusHide(info_screen)
{
	Hide(info_screen);
		
	modalDialogOpen = 0;
	// unpause the game, regardless of previous status
	if (isPaused==1)
	{
		PauseRun();
	}
}

function PlayAgain(info_screen)
{
	GameStatusHide(info_screen);
	NewGame(false);
}

function NotAgain(info_screen)
{
	GameStatusHide(info_screen);
}

function PauseRun()
{
	// if there is a modal dialog open, ignore a button click
	if (modalDialogOpen==1)
	{
		return;
	}
	// pauses the game if it's running
	// or restarts if it's paused
	switch (isPaused)
	{case 0:
		WriteText("PauseButton", "<img src=\"icons/ui/run.gif\" alt=\"Run the game\">&nbsp;Run");
		PauseGame();
		break;

	case 1:
		WriteText("PauseButton", "<img src=\"icons/ui/pause.gif\" alt=\"Pause the game\">&nbsp;Pause");
		RunGame();
		break;
	}
}

function PauseGame()
{
	// stop the game timer
	clearTimeout(gameInterval);
	isPaused=1;
}

function RunGame()
{
	// start the game timer
	gameInterval = setInterval("GameUpdate();", gameTick);
	isPaused=0;
}

function SettingsDialogShow()
{
	// if there is a modal dialog open, ignore a button click
	if (modalDialogOpen==1)
	{
		return;
	}
	
	// if the game isn't paused, pause it
	if (isPaused == 0)
	{
		previousPauseStatus = 0;
		PauseRun();
	}
	else
	{
		previousPauseStatus = 1;
	}
	
	// set the UI to match current values
	document.getElementById("number_of_balls").value=startLevel;
	document.getElementById("seed").value=startSeed;
	document.getElementById("set_seed").checked=setSeed;
	SettingsDialogEnableSeed();
	
	document.getElementById("dialog_01").style.left = settingsDialogX;
	document.getElementById("dialog_01").style.top = settingsDialogY;
	// each div of the dialog must be shown
	Show("dialog_01");
	Show("DialogTitle");
	Show("dialog_01_pane_01");

	modalDialogOpen = 1;
}

function SettingsDialogOK()
{
	
	var validSettings;
	validSettings = ValidateSettingsDialog();
	if (validSettings==1) {
	SettingsDialogHide();
	}
}

function SettingsDialogCancel()
{
	SettingsDialogHide();
}

function SettingsDialogHide()
{
	// each div of the dialog must be hidden
	Hide("dialog_01");
	Hide("DialogTitle");
	Hide("dialog_01_pane_01");
		
	modalDialogOpen = 0;
	// unpause the game
	if (isPaused==1)
	{
		// but only if it was running before the dialog was opened
		if (previousPauseStatus == 0)
		{
			PauseRun();
		}
	}
}

function ValidateSettingsDialog()
{
	// get the UI components and their current values
	var object_flatlanders = document.getElementById("number_of_balls");
	var num_flatlanders = object_flatlanders.value;
	var object_seed = document.getElementById("seed");
	var current_seed = object_seed.value;

	// check the number of flatlanders is valid
	// cancel if there is any error and let the player know what's wrong
	if(isNaN(num_flatlanders))
	{
		(level_number_string)
		object_flatlanders.focus();
		return 0;
	}
	else if (!IsInt(num_flatlanders))
	{
		alert(level_number_string)
		object_flatlanders.focus();
		return 0;		
	}
	else if (num_flatlanders<minStartLevel)
	{
		alert(level_number_string)
		object_flatlanders.focus();
		return 0;		
	}
		else if (num_flatlanders>maxStartLevel)
	{
		alert(level_number_string)
		object_flatlanders.focus();
		return 0;		
	}
	
	// check the random seed is valid
	// cancel if there is any error and let the player know what's wrong
	if(isNaN(current_seed))
	{
		alert(seed_string)
		object_seed.focus();
		return 0;
	}
	else if (!IsInt(current_seed))
	{
		alert(seed_string)
		object_seed.focus();
		return 0;		
	}
	else if (current_seed<minSeed)
	{
		alert(seed_string)
		object_seed.focus();
		return 0;		
	}
	
	// if the numbers are ok, commit the UI values to the game
	else
	{
		startLevel=Math.round(document.getElementById("number_of_balls").value);
		startSeed=document.getElementById("seed").value;
		setSeed=document.getElementById("set_seed").checked;
		return 1;
	}
}

function SettingsDialogEnableSeed()
{
	if (document.getElementById('set_seed').checked==true)
	{
		document.getElementById('seed').disabled=false;
		document.getElementById('seed_text').disabled=false;
	}
	else
	{
		document.getElementById('seed').disabled=true;
		document.getElementById('seed_text').disabled=true;
	}	
}


function NewGame(retry)
{
	Hide('PlayGame'); // hide the Play Game button
	Show('PauseButton');
	// set the game time to 0
	if (retry == false)
	{
		timeElapsed = 0;
		gameScore = 0;
		currentLevel = startLevel;
		currentNumberLives = defaultNumberLives;
	}
	ShowGameScore();
	gameOver = false;
	DeleteGameObjects();
	InitGame();
}

function DeleteGameObjects()
{
		// delete all balls except the mystery last/first ball
	var num = balls.length-2;
	var i;

	for (i = 0; i <= (num); i++)
	{
		DeleteBall(balls[1]);
	}
	
	// delete all sources except the mystery last/first source
	num = sources.length-2;

	for (i = 0; i <= (num); i++)
	{
		DeleteSource(sources[1]);
	}
	
	// delete all buckets except the mystery last/first bucket
	num = buckets.length-2;

	for (i = 0; i <= (num); i++)
	{
		DeleteBucket(buckets[1]);
	}
	
	// delete all score cards except the mystery last/first score card
	num = scoreCards.length-2;

	for (i = 0; i <= (num); i++)
	{
		DeleteScoreCard(scoreCards[1]);
	}
	
	// delete all powerups except the template
	num = powerups.length-2;

	for (i = 0; i <= (num); i++)
	{
		DeletePowerup(powerups[1]);
	}
}

function SelectObject(oObject)
{
	WriteDebug1("ID: " + oObject._ID);
}

/////////////////////////////////
// Dragging Functions

// drag items are identified by their class
// class "dragdirectly" is for game objects like flatlanders that you just grab anywhere
// you can't drag game objects when the Settings dialog is open
// class "dialog_title_384" is for a dialog where you can only grab it by the title bar

var ie=document.all;
var nn6=document.getElementById&&!document.all;

var isdrag=false;
var MouseX,MouseY;
var dobj;

function movemouse(e)
{
  if (isdrag)
  {
    var newLeft = nn6 ? tx + e.clientX - MouseX : tx + event.clientX - MouseX;
    var newTop = nn6 ? ty + e.clientY - MouseY : ty + event.clientY - MouseY;
	
	dobj.style.left = newLeft + 'px';
    dobj.style.top  = newTop + 'px';
    
    if (dobj.className=="dragme")
    // if this is a game object with a strategy, update its position and interrupt the strategy
    {
    	dobj._x = newLeft;
    	dobj._y = newTop;
   		dobj._centreX = CentreX(dobj);
    	dobj._centreY = CentreY(dobj);

    	if (dobj._strategy != "drag")
    	{
    		SetStrategy(dobj, "drag");
		}		
	}
  return false;
  }
}

function selectmouse(e) 
{
  var fobj       = nn6 ? e.target : event.srcElement;
  var topelement = nn6 ? "HTML" : "BODY";

  while (fobj.tagName != topelement && fobj.className != "dragme" && fobj.className != "dialog_title_384")
  {
    fobj = nn6 ? fobj.parentNode : fobj.parentElement;
  }

  if ((fobj.className=="dragme" && modalDialogOpen==0) || (fobj.className=="dialog_title_384"))
  {
		isdrag = true;
    // when dragging a dialog by its title bar, move the whole dialog
    // when dragging a game object, move just the game object
    dobj = (fobj.className=="dialog_title_384")? fobj.parentNode : fobj;
    
    tx = parseInt(dobj.style.left+0);
    ty = parseInt(dobj.style.top+0);
    MouseX = nn6 ? e.clientX : event.clientX;
    MouseY = nn6 ? e.clientY : event.clientY;
    	
		document.onmousemove=movemouse;
    return false;
  }
}

document.onmousedown=selectmouse;
document.onmouseup=MouseUp;

function MouseUp()
{
	// if the mouse is released, stop any dragging behaviour and reset strategy of any game object that was being dragged
	isdrag=false;

	if (dobj != null)
	{
		if (dobj._strategy == "drag")
		{
			dobj._strategy = "evaluate";
			dobj._counter = 0;
		}
	}
}

/////////////////////////////////
// Ball Functions

function FindBallByID(id)
{
	// returns the ball object
	var i;
	for (i = 0; i <= (balls.length-1); i++)
	{
		if (id == balls[i]._ID)
		{
			return balls[i];
		}
	}
	return -1;
}

function FindHighestRank(arr)
{
	// returns the highest ranked ball in the array - returns the actual ball NOT the id
	// this is because you have to find the actual ball to get their rank
	// don't pass an array with 0 elements!
	var highestRank = 0;
	var highestRanker;
	var aBall;
	
	var i;
	for (i = 0; i <= (arr.length-1); i++)
	{
		aBall = FindBallByID(arr[i]);
		
		if (aBall._rank > highestRank)
		{
			highestRanker = aBall;
			highestRank = aBall._rank;
		}
	}
	return highestRanker;
}

function FindNeighbors(oBall)
{
	var neighborsID=new Array();
	var neighborsDistance=new Array();
	var distance;
	
	// find the balls this ball can detect
	var i;
	for (i = 0; i <= (balls.length-1); i++)
	{
		oNeighbor=balls[i];
		if (oBall._ID!=oNeighbor._ID)
		{
			// don't count yourself!
			// find how far away the neighbor is
			distance = FindDistance(oBall, oNeighbor);
			if (distance < sightDistance)
			{
				// add the ball to the list of neighbors
				neighborsID[neighborsID.length] = oNeighbor._ID;
				neighborsDistance[neighborsID.length] = distance;
			}
		}
	}
	return {_id:neighborsID, _distance:neighborsDistance};
}

function GetLeader(oBall)
{
	// returns the id of a new leader for the ball
	// find the balls within sight
	var myNeighbors=new Array();
	var myNeighbors=FindNeighbors(oBall)._id;
	var newLeader;
	var newLeader_id;
	var str = "";

	if (myNeighbors.length==0)
	{
		// if no neighbors, make the ball their own leader
		newLeader_id = oBall._ID;
	}
	else
	{
		newLeader = FindHighestRank(myNeighbors);

		if (newLeader._rank > oBall._rank)
		{
			// follow somebody with higher rank than yourself
			newLeader_id = newLeader._ID;		
		}
		else
		{
			// make the ball their own leader
			newLeader_id = oBall._ID;	
		}
	}
		return newLeader_id;
}

function DeleteBall(oBall)
{
	try
	{
		var index = oBall._index;
		var elm = document.getElementById('TheBalls')
		elm.removeChild(balls[oBall._index]);

		// make sure all the remaining balls have the right index
		var i;
		for (i=0; i<balls.length; i++)
		{
			balls[i]._index = i;
		}
	}
	catch(err)
	{
		// the last ball doesn't seem to be deletable
		alert("can't delete this ball")
	}
}

function DeleteSource(oSource)
{
	try
	{
		var index = oSource._index;
		var elm = document.getElementById('Sources')
		elm.removeChild(sources[oSource._index]);
		//alert("here");

		// make sure all the remaining sources have the right index
		var i;
		for (i=0; i<sources.length; i++)
		{
			sources[i]._index = i;
		}
	}
	catch(err)
	{
		// the last source doesn't seem to be deletable
		alert("can't delete this source")
	}
}

function DeleteBucket(oBucket)
{
	try
	{
		var index = oBucket._index;
		var elm = document.getElementById('Bucket')
		elm.removeChild(buckets[oBucket._index]);

		// make sure all the remaining balls have the right index
		var i;
		for (i=0; i<buckets.length; i++)
		{
			buckets[i]._index = i;
		}
	}
	catch(err)
	{
		// the last bucket doesn't seem to be deletable
		alert("can't delete this bucket")
	}
}

function DeleteScoreCard(oScoreCard)
{
	try
	{
		var index = oScoreCard._index;
		var elm = document.getElementById('ScoreCards')
		elm.removeChild(scoreCards[oScoreCard._index]);

	// make sure all the remaining score cards have the right index
		var i;
		for (i=0; i<scoreCards.length; i++)
		{
			scoreCards[i]._index = i;
		}
	}
	catch(err)
	{
		// the last score card doesn't seem to be deletable
		alert("can't delete this score card")
	}
}

function DeletePowerup(oPowerup)
{
	try
	{
		var index = oPowerup._index;
		var elm = document.getElementById('Powerups')
		elm.removeChild(powerups[oPowerup._index]);

	// make sure all the remaining score cards have the right index
		var i;
		for (i=0; i<powerups.length; i++)
		{
			powerups[i]._index = i;
		}
	}
	catch(err)
	{
		// the last score card doesn't seem to be deletable
		alert("can't delete this powerup")
	}
}

/////////////////////////////////
// Info Functions

function WriteText(elementID, text)
{
	// writes text to any element that supports innerHTML and has an id
	document.getElementById(elementID).innerHTML = text;
}

function WriteDebug1(txt)
{
	document.getElementById("debug_01").innerHTML=txt;
}

function WriteDebug2(txt)
{
	document.getElementById("debug_02").innerHTML=txt;
}

function WriteGameInfo(txt)
{
	document.getElementById("game_info").innerHTML=txt;
}

function WriteInfo(object)
{
	var txt = "ID: " + object._ID;
	var txt = txt + ", Strategy: " + object._strategy;
	var txt = txt + ", Type: " + object._objectType;
	var txt = txt + ", x: " + object._x;
	WriteDebug2(txt);
}

function ClearInfo()
{
	document.getElementById("debug_02").innerHTML="&nbsp;";
}

/////////////////////////////////////
// Key Press Functions

document.onkeydown = KeyDown;
document.onkeyup = KeyUp;

function KeyDown(e)
{
	e = e || window.event;
	var keyDown = e.keyCode || e.which;
	// detect whether the player is pressing movement keys
	{
		if ((keyDown == leftKey) || (keyDown == altLeftKey))
		{
			moveLeft = true;
		}
		if ((keyDown == rightKey) || (keyDown == altRightKey))
		{
			moveRight = true;
		}
		if ((keyDown == upKey) || (keyDown == altUpKey))
		{
			moveUp = true;
		}
		if ((keyDown == downKey) || (keyDown == altDownKey))
		{
			moveDown = true;
		}
	}
	return;
}

function KeyUp(e)
{
	e = e || window.event;
	var keyUp = e.keyCode || e.which;
	
	if ((keyUp == leftKey) || (keyUp == altLeftKey))
	{
		buckets[0]._xSpeed = 0;
		moveLeft = false;
	}
	if ((keyUp == rightKey) || (keyUp == altRightKey))
	{
		buckets[0]._xSpeed = 0;
		moveRight = false;
	}
	if ((keyUp == upKey) || (keyUp == altUpKey))
	{
		buckets[0]._ySpeed = 0;
		moveUp = false;
	}
	if ((keyUp == downKey) || (keyUp == altDownKey))
	{
		buckets[0]._ySpeed = 0;
		moveDown = false;
	}
	return;
}

function StopKeys()
{
	moveLeft = false;
	moveRight = false;
	moveUp = false;
	moveDown = false;
}

function RectangleVLineCollision(oObject, line_x, line_y1, line_y2)
{
	// tests whether a vertical line intersects a rectangle
	// y1: upper end of the line
	// y2: lower end of the line
	if (line_x < oObject._x) { ;return false; } // test against left side of rectangle
	if (line_x > oObject._x + oObject._width) { return false; } // test against right side of rectangle
	if (line_y2 < oObject._y) { return false; } // test against top of rectangle
	if (line_y1 > oObject._y + oObject._height) { return false; } // test against bottom of rectangle
	return true;
}

function MovingVFixedRectangleCollision(oMoving, oFixed, direction)
{
	// tests a rectangle moving along a known orthogonal to see if it will collide with a fixed rectangle
	// find where the moving rectangle will be next tick
	var new_pos = RectangleNewPosition(oMoving);
	
	switch (direction)
	{
	case "left": // does moving left edge intersect fixed?
	// alert(new_pos._x + " " + oFixed._x + " " + oFixed._width);
		if (new_pos._x <= oFixed._x) { return false; } // test against left side of rectangle
		if (new_pos._x >= oFixed._x + oFixed._width) { return false; } // test against right side of rectangle
		if (new_pos._y + oMoving._height <= oFixed._y) { return false; } // test against top of rectangle
		if (new_pos._y >= oFixed._y + oFixed._height) { return false; } // test against bottom of rectangle
		return true;
	case "right": // does moving right edge intersect fixed?
		if (new_pos._x + oMoving._width <= oFixed._x) { return false; } // test against left side of rectangle
		if (new_pos._x + oMoving._width >= oFixed._x + oFixed._width) { return false; } // test against right side of rectangle
		if (new_pos._y + oMoving._height <= oFixed._y) { return false; } // test against top of rectangle
		if (new_pos._y >= oFixed._y + oFixed._height) { return false; } // test against bottom of rectangle
		return true;
	case "down": // does moving bottom edge intersect fixed?
		if (new_pos._x + oMoving._width <= oFixed._x) { return false; } // test against left side of rectangle
		if (new_pos._x >= oFixed._x + oFixed._width) { return false; } // test against right side of rectangle
		if (new_pos._y + oMoving._height <= oFixed._y) { return false; } // test against top of rectangle
		if (new_pos._y + oMoving._height >= oFixed._y + oFixed._height) { return false; } // test against bottom of rectangle
		return true;
	case "up": // does moving top edge intersect fixed?
		if (new_pos._x + oMoving._width <= oFixed._x) { return false; } // test against left side of rectangle
		if (new_pos._x >= oFixed._x + oFixed._width) { return false; } // test against right side of rectangle
		if (new_pos._y <= oFixed._y) { return false; } // test against top of rectangle
		if (new_pos._y >= oFixed._y + oFixed._height) { return false; } // test against bottom of rectangle
	}
	return true;
}

function RectangleVPowerupCollision(oObject, direction)
{
	// will a moving rectangle collide with a powerup?
	// direction is the direction the object is moving in
	for (i = 1; i <= (powerups.length-1); i++)
	{
		// will the bucket hit a powerup?
		if (MovingVFixedRectangleCollision(oObject, powerups[i], direction))
		{
			return true; // don't check any more powerups
		}
	}
	return false; // no powerups found in that direction
}

function RectangleNewPosition(oObject)
{
	// projects object's velocity to find future position
	// need to use the correct movement model, orthogonal or vector
	if (isNaN(oObject._xSpeed)  || (oObject._xSpeed === undefined))
	{
		var distanceTravelled = oObject._speed * (gameTick / 1000);
		return {_x: oObject._x + distanceTravelled * oObject._vx, _y:oObject._y + distanceTravelled * oObject._vy};
	}
	else
	{
		return {_x: oObject._x + (oObject._xSpeed * (gameTick / 1000)),
	_y:  oObject._y + (oObject._ySpeed * (gameTick / 1000))};
	}
}


function moveBucket(oObject)
{
	// if game is running, move bucket left/right
	if (isPaused == 1) { return false; }

	var x_pos = oObject._x;
	var y_pos = oObject._y;
	var new_pos;
	var collision = false;

	var i;
	
	if ((moveLeft == true) && (moveRight == false))
	{
		RectangleAcceleration(oObject, "left");

		collision = RectangleVPowerupCollision(oObject, "left"); // check for a powerup to the left
		if (collision == true) { oObject._xSpeed = 0; } // don't move into the powerup
		
		// avoid squashing balls through the bucket wall
		new_pos = RectangleNewPosition(oObject); // check future pos
		if ((new_pos._x < leftEdge + ball_width[0] + 1) && (collision == false)) // assumes all balls have same width!
		{
			for (i = 1; i <= (balls.length-1); i++) //don't bother with ball[0]
			{
				if ((balls[i]._x <= new_pos._x) &&
				(balls[i]._y + balls[i]._height >= new_pos._y) &&
				(balls[i]._y <= new_pos._y + oObject._height)) // ball to left of bucket
				{
					collision = true; // noted in case of more tests later
					balls[i]._vx = 0; // kill the ball's horizontal velocity
					balls[i]._vy = (balls[i]._vy > 0)? 1 : -1; // normalise the vertical velocity
					oObject._xSpeed = 0;
					break;
				}
			}
		}
		// move the bucket, but not beyond the left edge
		new_pos = RectangleNewPosition(oObject);
		x_pos = Math.max(new_pos._x, leftEdge);
	}
	else if ((moveRight == true) && (moveLeft == false))
	{
		RectangleAcceleration(oObject, "right");
		
		collision = RectangleVPowerupCollision(oObject, "right"); // check for a powerup to the right
		if (collision == true) { oObject._xSpeed = 0; } // don't move into the powerup
		
		// avoid squashing balls through the bucket wall
		new_pos = RectangleNewPosition(oObject); // check future pos
		if ((new_pos._x + oObject._width > rightEdge - ball_width[0] - 1) && (collision == false)) // assumes all balls have same width!
		{
			for (i = 1; i <= (balls.length-1); i++) //don't bother with ball[0]
			{
				if ((balls[i]._x >= new_pos._x + oObject._width) &&
				(balls[i]._y + balls[i]._height >= new_pos._y) &&
				(balls[i]._y <= new_pos._y + oObject._height)) // ball to right of bucket
				{
					collision = true; // noted in case of more tests later
					balls[i]._vx = 0; // kill the ball's horizontal velocity
					balls[i]._vy = (balls[i]._vy > 0)? 1 : -1; // normalise the vertical velocity
					oObject._xSpeed = 0;
					break;
				}
			}
		}
		// move the bucket, but not beyond the right edge
		new_pos = RectangleNewPosition(oObject);
 		x_pos = Math.min(new_pos._x, (rightEdge - oObject._width));
	}
	if ((moveUp == true) && (moveDown == false) && (oObject._fly == "fly"))
	{
		RectangleAcceleration(oObject, "up");

		collision = RectangleVPowerupCollision(oObject, "up"); // check for a powerup above
		if (collision == true) { oObject._ySpeed = 0; } // don't move into the powerup
		
		// avoid squashing balls through the bucket wall
		new_pos = RectangleNewPosition(oObject); // check future pos
		if ((new_pos._y < gravityLine + ball_height[0] + 1)&& (collision == false)) // assumes all balls have same height!
		{
			for (i = 1; i <= (balls.length-1); i++) //don't bother with ball[0]
			{
				if ((balls[i]._x <= new_pos._x + oObject._width) &&
				(balls[i]._x + balls[i]._width >= new_pos._x) &&
				(balls[i]._y + balls[i]._height <= new_pos._y)) // ball above bucket
				{
					collision = true; // noted in case of more tests later
					balls[i]._vy = 0; // kill the ball's vertical velocity
					balls[i]._vx = (balls[i]._vx > 0)? 1 : -1; // normalise the horizontal velocity
					oObject._ySpeed = 0;
					break;
				}
			}
		}
		// move the bucket, but not above the permitted height
		new_pos = RectangleNewPosition(oObject);
		y_pos = Math.max(new_pos._y, bucketMinY);
	}
	else if ((moveDown == true) && (moveUp == false) && (oObject._fly == "fly"))
	{
		RectangleAcceleration(oObject, "down");
		
		collision = RectangleVPowerupCollision(oObject, "down"); // check for a powerup above
		if (collision == true) { oObject._ySpeed = 0; } // don't move into the powerup
		
		// avoid squashing balls through the bucket wall
		new_pos = RectangleNewPosition(oObject); // check future pos
		if ((new_pos._y + oObject._height > bottomEdge - ball_height[0] - 1)&& (collision == false)) // assumes all balls have same height!
		{
			for (i = 1; i <= (balls.length-1); i++) //don't bother with ball[0]
			{
				if ((balls[i]._x <= new_pos._x + oObject._width) &&
				(balls[i]._x + balls[i]._width >= new_pos._x) &&
				(balls[i]._y >= new_pos._y + oObject._height)) // ball below bucket
				{
					collision = true; // bucket shouldn't be allowed to move right
					balls[i]._vy = 0; // kill the ball's vertical velocity
					balls[i]._vx = (balls[i]._vx > 0)? 1 : -1; // normalise the horizontal velocity
					oObject._ySpeed = 0;
					break;
				}
			}
		}
		// move the bucket, but not beyond the bottom edge
		new_pos = RectangleNewPosition(oObject);
 		y_pos = Math.min(new_pos._y, (bottomEdge - oObject._height));
	}
	TeleportObject(oObject, x_pos, y_pos);
	return true;
}

//-->


