function UnitAI() {}

UnitAI.prototype.Schema =
	"<a:help>Controls the unit's movement, attacks, etc, in response to commands from the player.</a:help>" +
	"<a:example/>" +
	"<element name='DefaultStance'>" +
		"<choice>" +
			"<value>violent</value>" +
			"<value>aggressive</value>" +
			"<value>defensive</value>" +
			"<value>passive</value>" +
			"<value>stand</value>" +
		"</choice>" +
	"</element>" +
	"<element name='FormationController'>" +
		"<data type='boolean'/>" +
	"</element>" +
	"<element name='FleeDistance'>" +
		"<ref name='positiveDecimal'/>" +
	"</element>" +
	"<optional>" +
		"<interleave>" +
			"<element name='NaturalBehaviour' a:help='Behaviour of the unit in the absence of player commands (intended for animals)'>" +
				"<choice>" +
					"<value a:help='Will actively attack any unit it encounters, even if not threatened'>violent</value>" +
					"<value a:help='Will attack nearby units if it feels threatened (if they linger within LOS for too long)'>aggressive</value>" +
					"<value a:help='Will attack nearby units if attacked'>defensive</value>" +
					"<value a:help='Will never attack units'>passive</value>" +
					"<value a:help='Will never attack units. Will typically attempt to flee for short distances when units approach'>skittish</value>" +
				"</choice>" +
			"</element>" +
			"<element name='RoamDistance'>" +
				"<ref name='positiveDecimal'/>" +
			"</element>" +
			"<element name='RoamTimeMin'>" +
				"<ref name='positiveDecimal'/>" +
			"</element>" +
			"<element name='RoamTimeMax'>" +
				"<ref name='positiveDecimal'/>" +
			"</element>" +
			"<element name='FeedTimeMin'>" +
				"<ref name='positiveDecimal'/>" +
			"</element>" +
			"<element name='FeedTimeMax'>" +
				"<ref name='positiveDecimal'/>" +
			"</element>"+
		"</interleave>" +
	"</optional>";

// Unit stances.
// There some targeting options:
//   targetVisibleEnemies: anything in vision range is a viable target
//   targetAttackers: anything that hurts us is a viable target
// There are some response options, triggered when targets are detected:
//   respondFlee: run away
//   respondChase: start chasing after the enemy
//   respondChaseBeyondVision: start chasing, and don't stop even if it's out
//     of this unit's vision range (though still visible to the player)
//   respondStandGround: attack enemy but don't move at all
//   respondHoldGround: attack enemy but don't move far from current position
// TODO: maybe add targetAggressiveEnemies (don't worry about lone scouts,
// do worry around armies slaughtering the guy standing next to you), etc.
var g_Stances = {
	"violent": {
		targetVisibleEnemies: true,
		targetAttackers: true,
		respondFlee: false,
		respondChase: true,
		respondChaseBeyondVision: true,
		respondStandGround: false,
		respondHoldGround: false,
	},
	"aggressive": {
		targetVisibleEnemies: true,
		targetAttackers: true,
		respondFlee: false,
		respondChase: true,
		respondChaseBeyondVision: false,
		respondStandGround: false,
		respondHoldGround: false,
	},
	"defensive": {
		targetVisibleEnemies: true,
		targetAttackers: true,
		respondFlee: false,
		respondChase: false,
		respondChaseBeyondVision: false,
		respondStandGround: false,
		respondHoldGround: true,
	},
	"passive": {
		targetVisibleEnemies: false,
		targetAttackers: true,
		respondFlee: true,
		respondChase: false,
		respondChaseBeyondVision: false,
		respondStandGround: false,
		respondHoldGround: false,
	},
	"stand": {
		targetVisibleEnemies: true,
		targetAttackers: true,
		respondFlee: false,
		respondChase: false,
		respondChaseBeyondVision: false,
		respondStandGround: true,
		respondHoldGround: false,
	},
};

// See ../helpers/FSM.js for some documentation of this FSM specification syntax
var UnitFsmSpec = {

	// Default event handlers:

	"MoveCompleted": function() {
		// ignore spurious movement messages
		// (these can happen when stopping moving at the same time
		// as switching states)
	},

	"MoveStarted": function() {
		// ignore spurious movement messages
	},

	"ConstructionFinished": function(msg) {
		// ignore uninteresting construction messages
	},

	"LosRangeUpdate": function(msg) {
		// ignore newly-seen units by default
	},

	"Attacked": function(msg) {
		// ignore attacker
	},

	"HealthChanged": function(msg) {
		// ignore
	},

	"EntityRenamed": function(msg) {
		// ignore
	},

	// Formation handlers:

	"FormationLeave": function(msg) {
		// ignore when we're not in FORMATIONMEMBER
	},

	// Called when being told to walk as part of a formation
	"Order.FormationWalk": function(msg) {
		if (this.IsAnimal())
		{
			// TODO: let players move captured animals around
			this.FinishOrder();
			return;
		}

		var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
		cmpUnitMotion.MoveToFormationOffset(msg.data.target, msg.data.x, msg.data.z);

		this.SetNextState("FORMATIONMEMBER.WALKING");
	},

	// Special orders:
	// (these will be overridden by various states)

	"Order.LeaveFoundation": function(msg) {
		// Default behaviour is to ignore the order since we're busy
		return { "discardOrder": true };
	},

	// Individual orders:
	// (these will switch the unit out of formation mode)

	"Order.Walk": function(msg) {
		if (this.IsAnimal())
		{
			// TODO: let players move captured animals around
			this.FinishOrder();
			return;
		}

		this.SetHeldPosition(this.order.data.x, this.order.data.z);
		this.MoveToPoint(this.order.data.x, this.order.data.z);
		this.SetNextState("INDIVIDUAL.WALKING");
	},

	"Order.WalkToTarget": function(msg) {
		if (this.IsAnimal())
		{
			// TODO: let players move captured animals around
			this.FinishOrder();
			return;
		}

		var ok = this.MoveToTarget(this.order.data.target);
		if (ok)
		{
			// We've started walking to the given point
			this.SetNextState("INDIVIDUAL.WALKING");
		}
		else
		{
			// We are already at the target, or can't move at all
			this.StopMoving();
			this.FinishOrder();
		}
	},

	"Order.Flee": function(msg) {
		// TODO: if we were attacked by a ranged unit, we need to flee much further away
		var ok = this.MoveToTargetRangeExplicit(this.order.data.target, +this.template.FleeDistance, -1);
		if (ok)
		{
			// We've started fleeing from the given target
			if (this.IsAnimal())
				this.SetNextState("ANIMAL.FLEEING");
			else
				this.SetNextState("INDIVIDUAL.FLEEING");
		}
		else
		{
			// We are already at the target, or can't move at all
			this.StopMoving();
			this.FinishOrder();
		}
	},

	"Order.Attack": function(msg) {
		// Check the target is alive
		if (!this.TargetIsAlive(this.order.data.target))
		{
			this.FinishOrder();
			return;
		}

		// Work out how to attack the given target
		var type = this.GetBestAttack();
		if (!type)
		{
			// Oops, we can't attack at all
			this.FinishOrder();
			return;
		}
		this.attackType = type;

		// If we are already at the target, try attacking it from here
		if (this.CheckTargetRange(this.order.data.target, IID_Attack, this.attackType))
		{
			this.StopMoving();
			if (this.IsAnimal())
				this.SetNextState("ANIMAL.COMBAT.ATTACKING");
			else
				this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
			return;
		}

		// If we can't reach the target, but are standing ground,
		// then abandon this attack order
		if (this.GetStance().respondStandGround && !this.order.data.force)
		{
			this.FinishOrder();
			return;
		}

		// Try to move within attack range
		if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType))
		{
			// We've started walking to the given point
			if (this.IsAnimal())
				this.SetNextState("ANIMAL.COMBAT.APPROACHING");
			else
				this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
			return;
		}

		// We can't reach the target, and can't move towards it,
		// so abandon this attack order
		this.FinishOrder();
	},

	"Order.Gather": function(msg) {
		// If the target is still alive, we need to kill it first
		if (this.MustKillGatherTarget(this.order.data.target) && this.CheckTargetVisible(this.order.data.target))
		{
			// Make sure we can attack the target, else we'll get very stuck
			if (!this.GetBestAttack())
			{
				// Oops, we can't attack at all - give up
				// TODO: should do something so the player knows why this failed
				this.FinishOrder();
				return;
			}

			this.PushOrderFront("Attack", { "target": this.order.data.target, "force": true });
			return;
		}

		// Try to move within range
		if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
		{
			// We've started walking to the given point
			this.SetNextState("INDIVIDUAL.GATHER.APPROACHING");
		}
		else
		{
			// We are already at the target, or can't move at all,
			// so try gathering it from here.
			// TODO: need better handling of the can't-reach-target case
			this.StopMoving();
			this.SetNextState("INDIVIDUAL.GATHER.GATHERING");
		}
	},

	"Order.ReturnResource": function(msg) {
		// Try to move to the dropsite
		if (this.MoveToTarget(this.order.data.target))
		{
			// We've started walking to the target
			this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING");
		}
		else
		{
			// Oops, we can't reach the dropsite.
			// Maybe we should try to pick another dropsite, to find an
			// accessible one?
			// For now, just give up.
			this.StopMoving();
			this.FinishOrder();
			return;
		}
	},
	
	"Order.Repair": function(msg) {
		// Try to move within range
		if (this.MoveToTargetRange(this.order.data.target, IID_Builder))
		{
			// We've started walking to the given point
			this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING");
		}
		else
		{
			// We are already at the target, or can't move at all,
			// so try repairing it from here.
			// TODO: need better handling of the can't-reach-target case
			this.StopMoving();
			this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING");
		}
	},
	
	"Order.Garrison": function(msg) {
		if (this.MoveToTarget(this.order.data.target))
		{
			this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING");
		}
		else
		{
			// TODO: this is probably bogus if the unit was
			// unable to move at all - we need to do some range checks
			// before actually garrisoning
			this.StopMoving();
			this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED");
		}
	},
	
	"Order.Cheering": function(msg) {
		this.SetNextState("INDIVIDUAL.CHEERING");
	},

	// States for the special entity representing a group of units moving in formation:
	"FORMATIONCONTROLLER": {

		"Order.Walk": function(msg) {
			var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
			cmpFormation.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);

			this.MoveToPoint(this.order.data.x, this.order.data.z);
			this.SetNextState("WALKING");
		},

		"Order.Attack": function(msg) {
			// TODO: we should move in formation towards the target,
			// then break up into individuals when close enough to it

			var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
			cmpFormation.CallMemberFunction("Attack", [msg.data.target, false]);

			// TODO: we should wait until the target is killed, then
			// move on to the next queued order.
			// Don't bother now, just disband the formation immediately.
			cmpFormation.Disband();
		},

		"Order.Repair": function(msg) {
			// TODO: see notes in Order.Attack
			var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
			cmpFormation.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]);
			cmpFormation.Disband();
		},

		"Order.Gather": function(msg) {
			// TODO: see notes in Order.Attack
			var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
			cmpFormation.CallMemberFunction("Gather", [msg.data.target, false]);
			cmpFormation.Disband();
		},

		"Order.ReturnResource": function(msg) {
			// TODO: see notes in Order.Attack
			var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
			cmpFormation.CallMemberFunction("ReturnResource", [msg.data.target, false]);
			cmpFormation.Disband();
		},

		"Order.Garrison": function(msg) {
			var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
			cmpFormation.CallMemberFunction("Garrison", [msg.data.target, false]);
			cmpFormation.Disband();
		},
		
		"IDLE": {
		},

		"WALKING": {
			"MoveStarted": function(msg) {
				var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
				cmpFormation.MoveMembersIntoFormation(true);
			},

			"MoveCompleted": function(msg) {
				if (this.FinishOrder())
					return;

				var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
				cmpFormation.Disband();
			},
		},
	},


	// States for entities moving as part of a formation:
	"FORMATIONMEMBER": {

		"FormationLeave": function(msg) {
			// Stop moving as soon as the formation disbands
			this.StopMoving();

			// We're leaving the formation, so stop our FormationWalk order
			if (this.FinishOrder())
				return;

			// No orders left, we're an individual now
			this.SetNextState("INDIVIDUAL.IDLE");
		},

		// Override the LeaveFoundation order since we're not doing
		// anything more important (and we might be stuck in the WALKING
		// state forever and need to get out of foundations in that case)
		"Order.LeaveFoundation": function(msg) {
			if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target))
			{
				this.FinishOrder();
				return;
			}
			// Move a tile outside the building
			var range = 4;
			var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range);
			if (ok)
			{
				// We've started walking to the given point
				this.SetNextState("WALKING");
			}
			else
			{
				// We are already at the target, or can't move at all
				this.FinishOrder();
			}
		},

		"IDLE": {
			"enter": function() {
				this.SelectAnimation("idle");
			},
		},

		"WALKING": {
			"enter": function () {
				this.SelectAnimation("move");
			},

			// (We stay in this state even if we're already in position
			// and no longer moving, because the formation controller might
			// move and we'll automatically start chasing after it again)
		},
	},


	// States for entities not part of a formation:
	"INDIVIDUAL": {

		"enter": function() {
			// Sanity-checking
			if (this.IsAnimal())
				error("Animal got moved into INDIVIDUAL.* state");
		},

		"Attacked": function(msg) {
			if (this.GetStance().targetAttackers)
			{
				this.RespondToTargetedEntities([msg.data.attacker]);
			}
		},

		"IDLE": {
			"enter": function() {
				// Switch back to idle animation to guarantee we won't
				// get stuck with an incorrect animation
				this.SelectAnimation("idle");

				// The GUI and AI want to know when a unit is idle, but we don't
				// want to send frequent spurious messages if the unit's only
				// idle for an instant and will quickly go off and do something else.
				// So we'll set a timer here and only report the idle event if we
				// remain idle
				this.StartTimer(1000);

				// If we entered the idle state we must have nothing better to do,
				// so immediately check whether there's anybody nearby to attack.
				// (If anyone approaches later, it'll be handled via LosRangeUpdate.)
				if (this.FindNewTargets())
					return true; // (abort the FSM transition since we may have already switched state)

				// Nobody to attack - stay in idle
				return false;
			},

			"leave": function() {
				var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
				rangeMan.DisableActiveQuery(this.losRangeQuery);

				this.StopTimer();

				if (this.isIdle)
				{
					this.isIdle = false;
					Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
				}
			},

			"LosRangeUpdate": function(msg) {
				if (this.GetStance().targetVisibleEnemies)
				{
					// Start attacking one of the newly-seen enemy (if any)
					this.RespondToTargetedEntities(msg.data.added);
				}
			},

			"Timer": function(msg) {
				if (!this.isIdle)
				{
					this.isIdle = true;
					Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
				}
			},

			// Override the LeaveFoundation order since we're not doing
			// anything more important
			"Order.LeaveFoundation": function(msg) {
				if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target))
				{
					this.FinishOrder();
					return;
				}
				// Move a tile outside the building
				var range = 4;
				var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range);
				if (ok)
				{
					// We've started walking to the given point
					this.SetNextState("INDIVIDUAL.WALKING");
				}
				else
				{
					// We are already at the target, or can't move at all
					this.FinishOrder();
				}
			},
		},

		"WALKING": {
			"enter": function () {
				this.SelectAnimation("move");
			},

			"MoveCompleted": function() {
				this.FinishOrder();
			},
		},

		"FLEEING": {
			"enter": function() {
				this.PlaySound("panic");

				// Run quickly
				var speed = this.GetRunSpeed();
				this.SelectAnimation("move");
				this.SetMoveSpeed(speed);
			},

			"leave": function() {
				// Reset normal speed
				this.SetMoveSpeed(this.GetWalkSpeed());
			},

			"MoveCompleted": function() {
				// When we've run far enough, stop fleeing
				this.FinishOrder();
			},

			// TODO: what if we run into more enemies while fleeing?
		},

		"COMBAT": {
			"EntityRenamed": function(msg) {
				if (this.order.data.target == msg.entity)
					this.order.data.target = msg.newentity;
			},

			"Attacked": function(msg) {
				// If we're already in combat mode, ignore anyone else
				// who's attacking us
			},

			"APPROACHING": {
				"enter": function () {
					this.SelectAnimation("move");
					this.StartTimer(1000, 1000);
				},

				"leave": function() {
					this.StopTimer();
				},

				"Timer": function(msg) {
					if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force))
					{
						this.StopMoving();
						this.FinishOrder();

						// Return to our original position
						if (this.GetStance().respondHoldGround)
							this.WalkToHeldPosition();
					}
				},

				"MoveCompleted": function() {
					this.SetNextState("ATTACKING");
				},

				"Attacked": function(msg) {
					// If we're attacked by a close enemy, we should try to defend ourself
					if (this.GetStance().targetAttackers && msg.data.type == "Melee")
					{
						this.RespondToTargetedEntities([msg.data.attacker]);
					}
				},
			},

			"ATTACKING": {
				"enter": function() {
					var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
					this.attackTimers = cmpAttack.GetTimers(this.attackType);

					this.SelectAnimation("melee", false, 1.0, "attack");
					this.SetAnimationSync(this.attackTimers.prepare, this.attackTimers.repeat);
					this.StartTimer(this.attackTimers.prepare, this.attackTimers.repeat);
					// TODO: we should probably only bother syncing projectile attacks, not melee

					// TODO: if .prepare is short, players can cheat by cycling attack/stop/attack
					// to beat the .repeat time; should enforce a minimum time

					this.FaceTowardsTarget(this.order.data.target);
				},

				"leave": function() {
					this.StopTimer();
				},

				"Timer": function(msg) {
					// Check the target is still alive
					if (this.TargetIsAlive(this.order.data.target))
					{
						// Check we can still reach the target
						if (this.CheckTargetRange(this.order.data.target, IID_Attack, this.attackType))
						{
							this.FaceTowardsTarget(this.order.data.target);
							var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
							cmpAttack.PerformAttack(this.attackType, this.order.data.target);
							return;
						}

						// Can't reach it - try to chase after it
						if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force))
						{
							if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType))
							{
								this.SetNextState("COMBAT.CHASING");
								return;
							}
						}

					}

					// Can't reach it, or it doesn't exist any more - give up
					if (this.FinishOrder())
						return;

					// See if we can switch to a new nearby enemy
					if (this.FindNewTargets())
						return;

					// Return to our original position
					if (this.GetStance().respondHoldGround)
						this.WalkToHeldPosition();
				},

				// TODO: respond to target deaths immediately, rather than waiting
				// until the next Timer event

				"Attacked": function(msg) {
					if (this.order.data.target != msg.data.attacker)
					{
						// If we're attacked by a close enemy stronger than our current target, we choose to attack him
						if (this.GetStance().targetAttackers && msg.data.type == "Melee")
						{
							var ents = [this.order.data.target, msg.data.attacker];
							SortEntitiesByPriority(ents);
							if (ents[0] != this.order.data.target)
							{
								this.RespondToTargetedEntities(ents);
							}
						}
					}
				},
			},

			"CHASING": {
				"enter": function () {
					this.SelectAnimation("move");
					this.StartTimer(1000, 1000);
				},

				"leave": function() {
					this.StopTimer();
				},

				"Timer": function(msg) {
					if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force))
					{
						this.StopMoving();
						this.FinishOrder();

						// Return to our original position
						if (this.GetStance().respondHoldGround)
							this.WalkToHeldPosition();
					}
				},

				"MoveCompleted": function() {
					this.SetNextState("ATTACKING");
				},
			},
		},

		"GATHER": {
			"APPROACHING": {
				"enter": function() {
					this.SelectAnimation("move");
				},

				"MoveCompleted": function(msg) {
					if (msg.data.error)
					{
						// We failed to reach the target

						// Save the current order's data in case we need it later
						var oldType = this.order.data.type;
						var oldTarget = this.order.data.target;

						// Try the next queued order if there is any
						if (this.FinishOrder())
							return;

						// Try to find another nearby target of the same specific type
						var nearby = this.FindNearbyResource(function (ent, type) {
							return (ent != oldTarget && type.specific == oldType.specific);
						});
						if (nearby)
						{
							this.Gather(nearby, true);
							return;
						}

						// Couldn't find anything else. Just try this one again,
						// maybe we'll succeed next time
						this.Gather(oldTarget, true);
						return;
					}

					// We reached the target - start gathering from it now
					this.SetNextState("GATHERING");
				},
			},

			"GATHERING": {
				"enter": function() {
					this.StartTimer(1000, 1000);

					// We want to start the gather animation as soon as possible,
					// but only if we're actually at the target and it's still alive
					// (else it'll look like we're chopping empty air).
					// (If it's not alive, the Timer handler will deal with sending us
					// off to a different target.)
					if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
					{
						var typename = "gather_" + this.order.data.type.specific;
						this.SelectAnimation(typename, false, 1.0, typename);
					}
				},

				"leave": function() {
					this.StopTimer();
				},

				"Timer": function(msg) {
					// Check we can still reach the target
					if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
					{
						// Gather the resources:

						var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);

						// Try to gather treasure
						if (cmpResourceGatherer.TryInstantGather(this.order.data.target)) 
							return;
						
						// If we've already got some resources but they're the wrong type,
						// drop them first to ensure we're only ever carrying one type
						if (cmpResourceGatherer.IsCarryingAnythingExcept(this.order.data.type.generic))
							cmpResourceGatherer.DropResources();

						// Collect from the target
						var status = cmpResourceGatherer.PerformGather(this.order.data.target);

						// TODO: if exhausted, we should probably stop immediately
						// and choose a new target

						// If we've collected as many resources as possible,
						// return to the nearest dropsite
						if (status.filled)
						{
							var nearby = this.FindNearestDropsite(this.order.data.type.generic);
							if (nearby)
							{
								// (Keep this Gather order on the stack so we'll
								// continue gathering after returning)
								this.PushOrderFront("ReturnResource", { "target": nearby });
								return;
							}

							// Oh no, couldn't find any drop sites. Give up on gathering.
							this.FinishOrder();
						}
					}
					else
					{
						// Try to follow the target
						if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
						{
							this.SetNextState("APPROACHING");
							return;
						}

						// Can't reach the target, or it doesn't exist any more

						// We want to carry on gathering resources in the same area as
						// the old one. So try to get close to the old resource's
						// last known position

						var maxRange = 8; // get close but not too close
						if (this.order.data.lastPos &&
							this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z,
								0, maxRange))
						{
							this.SetNextState("APPROACHING");
							return;
						}

						// We're already in range, or can't get anywhere near it.

						// Save the current order's type in case we need it later
						var oldType = this.order.data.type;

						// Give up on this order and try our next queued order
						if (this.FinishOrder())
							return;

						// No remaining orders - pick a useful default behaviour

						// Try to find a new resource of the same specific type near our current position:

						var nearby = this.FindNearbyResource(function (ent, type) {
							return (type.specific == oldType.specific);
						});
						if (nearby)
						{
							this.Gather(nearby, true);
							return;
						}

						// Nothing else to gather - if we're carrying anything then we should
						// drop it off, and if not then we might as well head to the dropsite
						// anyway because that's a nice enough place to congregate and idle

						var nearby = this.FindNearestDropsite(oldType.generic);
						if (nearby)
						{
							this.PushOrderFront("ReturnResource", { "target": nearby });
							return;
						}

						// No dropsites - just give up
					}
				},
			},
		},

		// Returning to dropsite
		"RETURNRESOURCE": {
			"APPROACHING": {
				"enter": function () {
					// Work out what we're carrying, in order to select an appropriate animation
					var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
					var type = cmpResourceGatherer.GetLastCarriedType();
					if (type)
					{
						var typename = "carry_" + type.generic;

						// Special case for meat
						if (type.specific == "meat")
							typename = "carry_" + type.specific;

						this.SelectAnimation(typename, false, this.GetWalkSpeed());
					}
					else
					{
						// We're returning empty-handed
						this.SelectAnimation("move");
					}
				},

				"MoveCompleted": function() {
					// Switch back to idle animation to guarantee we won't
					// get stuck with the carry animation after stopping moving
					this.SelectAnimation("idle");

					// Check the dropsite really is in range
					// (we didn't get stopped before reaching it)
					if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
					{
						var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
						if (cmpResourceDropsite)
						{
							// Dump any resources we can
							var dropsiteTypes = cmpResourceDropsite.GetTypes();

							var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
							cmpResourceGatherer.CommitResources(dropsiteTypes);

							// Our next order should always be a Gather,
							// so just switch back to that order
							this.FinishOrder();
							return;
						}
					}

					// The dropsite was destroyed, or we couldn't reach it.
					// Look for a new one.

					var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
					var genericType = cmpResourceGatherer.GetMainCarryingType();
					var nearby = this.FindNearestDropsite(genericType);
					if (nearby)
					{
						this.FinishOrder();
						this.PushOrderFront("ReturnResource", { "target": nearby });
						return;
					}

					// Oh no, couldn't find any drop sites. Give up on returning.
					this.FinishOrder();
				},
			},
		},

		"REPAIR": {
			"APPROACHING": {
				"enter": function () {
					this.SelectAnimation("move");
				},
			
				"MoveCompleted": function() {
					this.SetNextState("REPAIRING");
				},
			},

			"REPAIRING": {
				"enter": function() {
					this.SelectAnimation("build", false, 1.0, "build");
					this.StartTimer(1000, 1000);
				},

				"leave": function() {
					this.StopTimer();
				},

				"Timer": function(msg) {
					var target = this.order.data.target;
					// Check we can still reach the target
					if (!this.CheckTargetRange(target, IID_Builder))
					{
						// Can't reach it, or it doesn't exist any more
						this.FinishOrder();
						return;
					}
					
					var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
					cmpBuilder.PerformBuilding(target);
				},
			},

			"ConstructionFinished": function(msg) {
				if (msg.data.entity != this.order.data.target)
					return; // ignore other buildings

				// Save the current order's data in case we need it later
				var oldAutocontinue = this.order.data.autocontinue;

				// We finished building it.
				// Switch to the next order (if any)
				if (this.FinishOrder())
					return;

				// No remaining orders - pick a useful default behaviour

				// If autocontinue explicitly disabled (e.g. by AI) then
				// do nothing automatically
				if (!oldAutocontinue)
					return;

				// If this building was e.g. a farm, we should start gathering from it
				// if we are capable of doing so
				if (this.CanGather(msg.data.newentity))
				{
					this.Gather(msg.data.newentity, true);
					return;
				}

				// If this building was e.g. a farmstead, we should look for nearby
				// resources we can gather
				var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite);
				if (cmpResourceDropsite)
				{
					var types = cmpResourceDropsite.GetTypes();
					var nearby = this.FindNearbyResource(function (ent, type) {
						return (types.indexOf(type.generic) != -1);
					});
					if (nearby)
					{
						this.Gather(nearby, true);
						return;
					}
				}

				// Look for a nearby foundation to help with
				var nearbyFoundation = this.FindNearbyFoundation();
				if (nearbyFoundation)
				{
					this.Repair(nearbyFoundation, oldAutocontinue, true);
					return;
				}
			},

			// Override the LeaveFoundation order since we don't want to be
			// accidentally blocking our own building
			"Order.LeaveFoundation": function(msg) {
				if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target))
				{
					this.FinishOrder();
					return;
				}
				// Move a tile outside the building
				var range = 4;
				var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range);
				if (ok)
				{
					// We've started walking to the given point
					this.SetNextState("INDIVIDUAL.WALKING");
				}
				else
				{
					// We are already at the target, or can't move at all
					this.FinishOrder();
				}
			},

		},

		"GARRISON": {
			"APPROACHING": {
				"enter": function() {
					this.SelectAnimation("walk", false, this.GetWalkSpeed());
					this.PlaySound("walk");
				},

				"MoveCompleted": function() {
					this.SetNextState("GARRISONED");
				},
				
				"leave": function() {
					this.StopTimer();
				}
			},

			"GARRISONED": {
				"enter": function() {
					var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder);
					if (cmpGarrisonHolder && cmpGarrisonHolder.Garrison(this.entity))
					{
						this.isGarrisoned = true;
					}
					else
					{	// Garrisoning failed for some reason, so finish the order
						if (this.FinishOrder())
							return;
					}
				},
				
				"Order.Ungarrison": function() {
					if (this.FinishOrder())
						return;
				},

				"leave": function() {
					this.isGarrisoned = false;
				}
			},
		},

		"CHEERING": {
			"enter": function() {
				// Unit is invulnerable while cheering
				var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
				cmpDamageReceiver.SetInvulnerability(true); 
				this.SelectAnimation("promotion");
				this.StartTimer(4000, 4000);
				return false;
			},

			"leave": function() {
				this.StopTimer();
				var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
				cmpDamageReceiver.SetInvulnerability(false);
			},

			"Timer": function(msg) {
				this.FinishOrder();
			},
		},
	},

	"ANIMAL": {

		"HealthChanged": function(msg) {
			// If we died (got reduced to 0 hitpoints), stop the AI and act like a corpse
			if (msg.to == 0)
				this.SetNextState("CORPSE");
		},

		"Attacked": function(msg) {
			if (this.template.NaturalBehaviour == "skittish" ||
			    this.template.NaturalBehaviour == "passive")
			{
				this.Flee(msg.data.attacker, false);
			}
			else if (this.template.NaturalBehaviour == "violent" ||
			         this.template.NaturalBehaviour == "aggressive" ||
			         this.template.NaturalBehaviour == "defensive")
			{
				if (this.CanAttack(msg.data.attacker))
					this.Attack(msg.data.attacker, false);
			}
		},

		"Order.LeaveFoundation": function(msg) {
			// Run away from the foundation
			this.MoveToTargetRangeExplicit(msg.data.target, +this.template.FleeDistance, +this.template.FleeDistance);
			this.SetNextState("FLEEING");
		},

		"IDLE": {
			// (We need an IDLE state so that FinishOrder works)

			"enter": function() {
				// Start feeding immediately
				this.SetNextState("FEEDING");
				return true;
			},
		},

		"CORPSE": {
			"enter": function() {
				this.StopMoving();
			},

			// Ignore all orders that animals might otherwise respond to
			"Order.FormationWalk": function() { },
			"Order.Walk": function() { },
			"Order.WalkToTarget": function() { },
			"Order.Attack": function() { },

			"Attacked": function(msg) {
				// Do nothing, because we're dead already
			},

			"Order.LeaveFoundation": function(msg) {
				// We can't walk away from the foundation (since we're dead),
				// but we mustn't block its construction (since the builders would get stuck),
				// and we don't want to trick gatherers into trying to reach us when
				// we're stuck in the middle of a building, so just delete our corpse.
				Engine.DestroyEntity(this.entity);
			},
		},

		"ROAMING": {
			"enter": function() {
				// Walk in a random direction
				this.SelectAnimation("walk", false, this.GetWalkSpeed());
				this.MoveRandomly(+this.template.RoamDistance);
				// Set a random timer to switch to feeding state
				this.StartTimer(RandomInt(+this.template.RoamTimeMin, +this.template.RoamTimeMax));
			},

			"leave": function() {
				this.StopTimer();
			},

			"LosRangeUpdate": function(msg) {
				if (this.template.NaturalBehaviour == "skittish")
				{
					if (msg.data.added.length > 0)
					{
						this.Flee(msg.data.added[0], false);
						return;
					}
				}
				// Start attacking one of the newly-seen enemy (if any)
				else if (this.template.NaturalBehaviour == "violent" ||
				         this.template.NaturalBehaviour == "aggressive")
				{
					this.AttackVisibleEntity(msg.data.added);
				}

				// TODO: if two units enter our range together, we'll attack the
				// first and then the second won't trigger another LosRangeUpdate
				// so we won't notice it. Probably we should do something with
				// ResetActiveQuery in ROAMING.enter/FEEDING.enter in order to
				// find any units that are already in range.
			},

			"Timer": function(msg) {
				this.SetNextState("FEEDING");
			},

			"MoveCompleted": function() {
				this.MoveRandomly(+this.template.RoamDistance);
			},
		},

		"FEEDING": {
			"enter": function() {
				// Stop and eat for a while
				this.SelectAnimation("feeding");
				this.StopMoving();
				this.StartTimer(RandomInt(+this.template.FeedTimeMin, +this.template.FeedTimeMax));
			},

			"leave": function() {
				this.StopTimer();
			},

			"LosRangeUpdate": function(msg) {
				if (this.template.NaturalBehaviour == "skittish")
				{
					if (msg.data.added.length > 0)
					{
						this.Flee(msg.data.added[0], false);
						return;
					}
				}
				// Start attacking one of the newly-seen enemy (if any)
				else if (this.template.NaturalBehaviour == "violent")
				{
					this.AttackVisibleEntity(msg.data.added);
				}
			},

			"MoveCompleted": function() { },

			"Timer": function(msg) {
				this.SetNextState("ROAMING");
			},
		},

		"FLEEING": "INDIVIDUAL.FLEEING", // reuse the same fleeing behaviour for animals

		"COMBAT": "INDIVIDUAL.COMBAT", // reuse the same combat behaviour for animals
	},
};

var UnitFsm = new FSM(UnitFsmSpec);

UnitAI.prototype.Init = function()
{
	this.orderQueue = []; // current order is at the front of the list
	this.order = undefined; // always == this.orderQueue[0]
	this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to
	this.isGarrisoned = false;
	this.isIdle = false;
	this.lastFormationName = "Line Closed";

	this.SetStance(this.template.DefaultStance);
};

UnitAI.prototype.IsFormationController = function()
{
	return (this.template.FormationController == "true");
};

UnitAI.prototype.IsAnimal = function()
{
	return (this.template.NaturalBehaviour ? true : false);
};

/**
 * Returns whether this is an animal that is too difficult to hunt.
 * (Currently this just includes skittish animals, which are probably
 * too fast to chase.)
 */
UnitAI.prototype.IsUnhuntable = function()
{
	// return (this.template.NaturalBehaviour && this.template.NaturalBehaviour == "skittish");
	// Actually, since the AI is currently rubbish at hunting, skip all animals
	// that aren't really weak:
	return this.IsAnimal() && Engine.QueryInterface(this.entity, IID_Health).GetMaxHitpoints() >= 10;
};

UnitAI.prototype.IsIdle = function()
{
	return this.isIdle;
};

UnitAI.prototype.IsGarrisoned = function()
{
	return this.isGarrisoned;
};

UnitAI.prototype.OnCreate = function()
{
	if (this.IsAnimal())
		UnitFsm.Init(this, "ANIMAL.FEEDING");
	else if (this.IsFormationController())
		UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
	else
		UnitFsm.Init(this, "INDIVIDUAL.IDLE");
};

UnitAI.prototype.OnOwnershipChanged = function(msg)
{
	this.SetupRangeQuery();
};

UnitAI.prototype.OnDestroy = function()
{
	// Clean up any timers that are now obsolete
	this.StopTimer();

	// Clean up range queries
	var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
	if (this.losRangeQuery)
		rangeMan.DestroyActiveQuery(this.losRangeQuery);
};

// Set up a range query for all enemy units within LOS range
// which can be attacked.
// This should be called whenever our ownership changes.
UnitAI.prototype.SetupRangeQuery = function()
{
	var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
	var owner = cmpOwnership.GetOwner();

	var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
	var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);

	if (this.losRangeQuery)
		rangeMan.DestroyActiveQuery(this.losRangeQuery);

	var players = [];

	if (owner != -1)
	{
		// If unit not just killed, get enemy players via diplomacy
		var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
		var numPlayers = playerMan.GetNumPlayers();
		for (var i = 1; i < numPlayers; ++i)
		{
			// Exclude gaia, allies, and self
			// TODO: How to handle neutral players - Special query to attack military only?
			if (cmpPlayer.IsEnemy(i))
				players.push(i);
		}
	}

	var range = this.GetQueryRange();

	this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver);
	rangeMan.EnableActiveQuery(this.losRangeQuery);
};

//// FSM linkage functions ////

UnitAI.prototype.SetNextState = function(state)
{
	UnitFsm.SetNextState(this, state);
};

UnitAI.prototype.DeferMessage = function(msg)
{
	UnitFsm.DeferMessage(this, msg);
};

/**
 * Call when the current order has been completed (or failed).
 * Removes the current order from the queue, and processes the
 * next one (if any). Returns false and defaults to IDLE
 * if there are no remaining orders.
 */
UnitAI.prototype.FinishOrder = function()
{
	if (!this.orderQueue.length)
		error("FinishOrder called when order queue is empty");

	this.orderQueue.shift();
	this.order = this.orderQueue[0];

	if (this.orderQueue.length)
	{
		var ret = UnitFsm.ProcessMessage(this,
			{"type": "Order."+this.order.type, "data": this.order.data}
		);

		// If the order was rejected then immediately take it off
		// and process the remaining queue
		if (ret && ret.discardOrder)
		{
			return this.FinishOrder();
		}

		// Otherwise we've successfully processed a new order
		return true;
	}
	else
	{
		this.SetNextState("IDLE");
		return false;
	}
};

/**
 * Add an order onto the back of the queue,
 * and execute it if we didn't already have an order.
 */
UnitAI.prototype.PushOrder = function(type, data)
{
	var order = { "type": type, "data": data };
	this.orderQueue.push(order);

	// If we didn't already have an order, then process this new one
	if (this.orderQueue.length == 1)
	{
		this.order = order;
		var ret = UnitFsm.ProcessMessage(this,
			{"type": "Order."+this.order.type, "data": this.order.data}
		);

		// If the order was rejected then immediately take it off
		// and process the remaining queue
		if (ret && ret.discardOrder)
		{
			this.FinishOrder();
		}
	}
};

/**
 * Add an order onto the front of the queue,
 * and execute it immediately.
 */
UnitAI.prototype.PushOrderFront = function(type, data)
{
	var order = { "type": type, "data": data };
	// If current order is cheering then add new order after it 
	if (this.order && this.order.type == "Cheering")
	{
		var cheeringOrder = this.orderQueue.shift();
		this.orderQueue.unshift(cheeringOrder, order);
	}
	else
	{
		this.orderQueue.unshift(order);
		this.order = order;
		var ret = UnitFsm.ProcessMessage(this,
			{"type": "Order."+this.order.type, "data": this.order.data}
		);

		// If the order was rejected then immediately take it off again;
		// assume the previous active order is still valid (the short-lived
		// new order hasn't changed state or anything) so we can carry on
		// as if nothing had happened
		if (ret && ret.discardOrder)
		{
			this.orderQueue.shift();
			this.order = this.orderQueue[0];
		}
	}
};

UnitAI.prototype.ReplaceOrder = function(type, data)
{
	// If current order is cheering then add new order after it
	if (this.order && this.order.type == "Cheering")
	{
		var order = { "type": type, "data": data };
		var cheeringOrder = this.orderQueue.shift();
		this.orderQueue = [ cheeringOrder, order ];
	}
	else
	{
		this.orderQueue = [];
		this.PushOrder(type, data);
	}
};

UnitAI.prototype.GetOrders = function()
{
	return this.orderQueue.slice();
}

UnitAI.prototype.AddOrders = function(orders)
{
	for each (var order in orders)
	{
		this.PushOrder(order.type, order.data);
	}
}

UnitAI.prototype.TimerHandler = function(data, lateness)
{
	// Reset the timer
	if (data.timerRepeat === undefined)
	{
		this.timer = undefined;
	}
	else
	{
		var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
		this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", data.timerRepeat - lateness, data);
	}

	UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
};

/**
 * Set up the UnitAI timer to run after 'offset' msecs, and then
 * every 'repeat' msecs until StopTimer is called. A "Timer" message
 * will be sent each time the timer runs.
 */
UnitAI.prototype.StartTimer = function(offset, repeat)
{
	if (this.timer)
		error("Called StartTimer when there's already an active timer");

	var data = { "timerRepeat": repeat };

	var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
	this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, data);
};

/**
 * Stop the current UnitAI timer.
 */
UnitAI.prototype.StopTimer = function()
{
	if (!this.timer)
		return;

	var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
	cmpTimer.CancelTimer(this.timer);
	this.timer = undefined;
};

//// Message handlers /////

UnitAI.prototype.OnMotionChanged = function(msg)
{
	if (msg.starting && !msg.error)
	{
		UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg});
	}
	else if (!msg.starting || msg.error)
	{
		UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg});
	}
};

UnitAI.prototype.OnGlobalConstructionFinished = function(msg)
{
	// TODO: This is a bit inefficient since every unit listens to every
	// construction message - ideally we could scope it to only the one we're building

	UnitFsm.ProcessMessage(this, {"type": "ConstructionFinished", "data": msg});
};

UnitAI.prototype.OnGlobalEntityRenamed = function(msg)
{
	UnitFsm.ProcessMessage(this, {"type": "EntityRenamed", "entity": msg.entity, "newentity": msg.newentity});
}

UnitAI.prototype.OnAttacked = function(msg)
{
	UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
};

UnitAI.prototype.OnHealthChanged = function(msg)
{
	UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to});
};

UnitAI.prototype.OnRangeUpdate = function(msg)
{
	if (msg.tag == this.losRangeQuery)
		UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg});
};

//// Helper functions to be called by the FSM ////

UnitAI.prototype.GetWalkSpeed = function()
{
	var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
	return cmpUnitMotion.GetWalkSpeed();
};

UnitAI.prototype.GetRunSpeed = function()
{
	var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
	return cmpUnitMotion.GetRunSpeed();
};

/**
 * Returns true if the target exists and has non-zero hitpoints.
 */
UnitAI.prototype.TargetIsAlive = function(ent)
{
	var cmpHealth = Engine.QueryInterface(ent, IID_Health);
	if (!cmpHealth)
		return false;

	return (cmpHealth.GetHitpoints() != 0);
};

/**
 * Returns true if the target exists and needs to be killed before
 * beginning to gather resources from it.
 */
UnitAI.prototype.MustKillGatherTarget = function(ent)
{
	var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
	if (!cmpResourceSupply)
		return false;

	if (!cmpResourceSupply.GetKillBeforeGather())
		return false;

	return this.TargetIsAlive(ent);
};

/**
 * Returns the entity ID of the nearest resource supply where the given
 * filter returns true, or undefined if none can be found.
 * TODO: extend this to exclude resources that already have lots of
 * gatherers.
 */
UnitAI.prototype.FindNearbyResource = function(filter)
{
	var range = 64; // TODO: what's a sensible number?

	// Accept any resources owned by Gaia
	var players = [0];
	// Also accept resources owned by this unit's player:
	var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
	if (cmpOwnership)
		players.push(cmpOwnership.GetOwner());

	var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
	var nearby = rangeMan.ExecuteQuery(this.entity, 0, range, players, IID_ResourceSupply);
	for each (var ent in nearby)
	{
		// Never automatically pick resources that are animals that are too hard to hunt
		var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
		if (cmpUnitAI && cmpUnitAI.IsUnhuntable())
			continue;

		var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
		var type = cmpResourceSupply.GetType();
		if (filter(ent, type))
			return ent;
	}

	return undefined;
};

/**
 * Returns the entity ID of the nearest resource dropsite that accepts
 * the given type, or undefined if none can be found.
 */
UnitAI.prototype.FindNearestDropsite = function(genericType)
{
	// Find dropsites owned by this unit's player
	var players = [];
	var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
	if (cmpOwnership)
		players.push(cmpOwnership.GetOwner());

	var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
	var nearby = rangeMan.ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite);
	for each (var ent in nearby)
	{
		var cmpDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
		if (!cmpDropsite.AcceptsType(genericType))
			continue;

		return ent;
	}

	return undefined;
};

/**
 * Returns the entity ID of the nearest building that needs to be constructed,
 * or undefined if none can be found close enough.
 */
UnitAI.prototype.FindNearbyFoundation = function()
{
	var range = 64; // TODO: what's a sensible number?

	// Find buildings owned by this unit's player
	var players = [];
	var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
	if (cmpOwnership)
		players.push(cmpOwnership.GetOwner());

	var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
	var nearby = rangeMan.ExecuteQuery(this.entity, 0, range, players, IID_Foundation);
	for each (var ent in nearby)
	{
		// Skip foundations that are already complete. (This matters since
		// we process the ConstructionFinished message before the foundation
		// we're working on has been deleted.)
		var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
		if (cmpFoundation.IsFinished())
			continue;

		return ent;
	}

	return undefined;
};

/**
 * Play a sound appropriate to the current entity.
 */
UnitAI.prototype.PlaySound = function(name)
{
	// If we're a formation controller, use the sounds from our first member
	if (this.IsFormationController())
	{
		var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
		var member = cmpFormation.GetPrimaryMember();
		if (member)
			PlaySound(name, member);
	}
	else
	{
		// Otherwise use our own sounds
		PlaySound(name, this.entity);
	}
};

UnitAI.prototype.SelectAnimation = function(name, once, speed, sound)
{
	var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
	if (!cmpVisual)
		return;

	// Special case: the "move" animation gets turned into a special
	// movement mode that deals with speeds and walk/run automatically
	if (name == "move")
	{
		// Speed to switch from walking to running animations
		var runThreshold = (this.GetWalkSpeed() + this.GetRunSpeed()) / 2;

		cmpVisual.SelectMovementAnimation(runThreshold);
		return;
	}

	var soundgroup;
	if (sound)
	{
		var cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
		if (cmpSound)
			soundgroup = cmpSound.GetSoundGroup(sound);
	}

	// Set default values if unspecified
	if (typeof once == "undefined")
		once = false;
	if (typeof speed == "undefined")
		speed = 1.0;
	if (typeof soundgroup == "undefined")
		soundgroup = "";

	cmpVisual.SelectAnimation(name, once, speed, soundgroup);
};

UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime)
{
	var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
	if (!cmpVisual)
		return;

	cmpVisual.SetAnimationSyncRepeat(repeattime);
	cmpVisual.SetAnimationSyncOffset(actiontime);
};

UnitAI.prototype.StopMoving = function()
{
	var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
	cmpUnitMotion.StopMoving();
};

UnitAI.prototype.MoveToPoint = function(x, z)
{
	var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
	return cmpUnitMotion.MoveToPointRange(x, z, 0, 0);
};

UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax)
{
	var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
	return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax);
};

UnitAI.prototype.MoveToTarget = function(target)
{
	if (!this.CheckTargetVisible(target))
		return false;

	var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
	return cmpUnitMotion.MoveToTargetRange(target, 0, 0);
};

UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
{
	if (!this.CheckTargetVisible(target))
		return false;

	var cmpRanged = Engine.QueryInterface(this.entity, iid);
	var range = cmpRanged.GetRange(type);

	var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
	return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};

UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
{
	if (!this.CheckTargetVisible(target))
		return false;

	var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
	return cmpUnitMotion.MoveToTargetRange(target, min, max);
};

UnitAI.prototype.CheckTargetRange = function(target, iid, type)
{
	var cmpRanged = Engine.QueryInterface(this.entity, iid);
	var range = cmpRanged.GetRange(type);

	var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
	return cmpUnitMotion.IsInTargetRange(target, range.min, range.max);
};

/**
 * Returns true if the target entity is visible through the FoW/SoD.
 */
UnitAI.prototype.CheckTargetVisible = function(target)
{
	var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
	if (!cmpOwnership)
		return false;

	var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
	if (!cmpRangeManager)
		return false;

	if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) == "hidden")
		return false;

	// Either visible directly, or visible in fog
	return true;
};

UnitAI.prototype.FaceTowardsTarget = function(target)
{
	var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
	if (!cmpPosition || !cmpPosition.IsInWorld())
			return;
	var cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
	if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
			return;
	var pos = cmpPosition.GetPosition();
	var targetpos = cmpTargetPosition.GetPosition();
	var angle = Math.atan2(targetpos.x - pos.x, targetpos.z - pos.z);
	var rot = cmpPosition.GetRotation();
	var delta = (rot.y - angle + Math.PI) % (2 * Math.PI) - Math.PI;
	if (Math.abs(delta) > 0.2)
	{
		var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
		if (cmpUnitMotion)
			cmpUnitMotion.FaceTowardsPoint(targetpos.x, targetpos.z);
	}
}

UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, type)
{
	var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
	var range = cmpRanged.GetRange(type);

	var cmpPosition = Engine.QueryInterface(target, IID_Position);
	if (!cmpPosition || !cmpPosition.IsInWorld())
		return false;

	var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
	if (!cmpVision)
		return false;
	var halfvision = cmpVision.GetRange() / 2;

	var pos = cmpPosition.GetPosition();
	var dx = this.heldPosition.x - pos.x;
	var dz = this.heldPosition.z - pos.z;
	var dist = Math.sqrt(dx*dx + dz*dz);

	return dist < halfvision + range.max;
};

UnitAI.prototype.CheckTargetIsInVisionRange = function(target)
{
	var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
	if (!cmpVision)
		return false;
	var range = cmpVision.GetRange();

	var distance = DistanceBetweenEntities(this.entity,target);

	return distance < range;
};

UnitAI.prototype.GetBestAttack = function()
{
	var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
	if (!cmpAttack)
		return undefined;
	return cmpAttack.GetBestAttack();
};

/**
 * Try to find one of the given entities which can be attacked,
 * and start attacking it.
 * Returns true if it found something to attack.
 */
UnitAI.prototype.AttackVisibleEntity = function(ents)
{
	for each (var target in ents)
	{
		if (this.CanAttack(target))
		{
			this.PushOrderFront("Attack", { "target": target, "force": false });
			return true;
		}
	}
	return false;
};

/**
 * Try to find one of the given entities which can be attacked
 * and which is close to the hold position, and start attacking it.
 * Returns true if it found something to attack.
 */
UnitAI.prototype.AttackEntityInZone = function(ents)
{
	var type = this.GetBestAttack();
	for each (var target in ents)
	{
		if (this.CanAttack(target) && this.CheckTargetDistanceFromHeldPosition(target, type))
		{
			this.PushOrderFront("Attack", { "target": target, "force": false });
			return true;
		}
	}
	return false;
};

/**
 * Try to respond appropriately given our current stance,
 * given a list of entities that match our stance's target criteria.
 * Returns true if it responded.
 */
UnitAI.prototype.RespondToTargetedEntities = function(ents)
{
	if (!ents.length)
		return false;

	if (this.GetStance().respondChase)
		return this.AttackVisibleEntity(ents);

	if (this.GetStance().respondStandGround)
		return this.AttackVisibleEntity(ents);

	if (this.GetStance().respondHoldGround)
		return this.AttackEntityInZone(ents);

	if (this.GetStance().respondFlee)
	{
		this.PushOrderFront("Flee", { "target": ents[0] });
		return true;
	}

	return false;
};

/**
 * Returns true if we should stop following the target entity.
 */
UnitAI.prototype.ShouldAbandonChase = function(target, force)
{
	// Stop if we're in hold-ground mode and it's too far from the holding point
	if (this.GetStance().respondHoldGround)
	{
		if (!this.CheckTargetDistanceFromHeldPosition(target, this.attackType))
			return true;
	}

	// Stop if it's left our vision range, unless we're especially persistent
	if (!force && !this.GetStance().respondChaseBeyondVision)
	{
		if (!this.CheckTargetIsInVisionRange(target))
			return true;
	}

	// (Note that CCmpUnitMotion will detect if the target is lost in FoW,
	// and will continue moving to its last seen position and then stop)

	return false;
};

/*
 * Returns whether we should chase the targeted entity,
 * given our current stance.
 */
UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
{
	if (this.GetStance().respondChase)
		return true;

	if (force)
		return true;

	return false;
};

//// External interface functions ////

UnitAI.prototype.SetFormationController = function(ent)
{
	this.formationController = ent;

	// Set obstruction group, so we can walk through members
	// of our own formation (or ourself if not in formation)
	var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
	if (cmpObstruction)
	{
		if (ent == INVALID_ENTITY)
			cmpObstruction.SetControlGroup(this.entity);
		else
			cmpObstruction.SetControlGroup(ent);
	}

	// If we were removed from a formation, let the FSM switch back to INDIVIDUAL
	if (ent == INVALID_ENTITY)
		UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });
};

UnitAI.prototype.GetFormationController = function()
{
	return this.formationController;
};

UnitAI.prototype.SetLastFormationName = function(name)
{
	this.lastFormationName = name;
};

UnitAI.prototype.GetLastFormationName = function()
{
	return this.lastFormationName;
};

/**
 * Returns the estimated distance that this unit will travel before either
 * finishing all of its orders, or reaching a non-walk target (attack, gather, etc).
 * Intended for Formation to switch to column layout on long walks.
 */
UnitAI.prototype.ComputeWalkingDistance = function()
{
	var distance = 0;

	var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
	if (!cmpPosition || !cmpPosition.IsInWorld())
		return 0;

	// Keep track of the position at the start of each order
	var pos = cmpPosition.GetPosition();

	for (var i = 0; i < this.orderQueue.length; ++i)
	{
		var order = this.orderQueue[i];
		switch (order.type)
		{
		case "Walk":
			// Add the distance to the target point
			var dx = order.data.x - pos.x;
			var dz = order.data.z - pos.z;
			var d = Math.sqrt(dx*dx + dz*dz);
			distance += d;
			
			// Remember this as the start position for the next order
			pos = order.data;

			break; // and continue the loop

		case "WalkToTarget":
		case "Flee":
		case "LeaveFoundation":
		case "Attack":
		case "Gather":
		case "ReturnResource":
		case "Repair":
		case "Garrison":
			// Find the target unit's position
			var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position);
			if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
				return distance;
			var targetPos = cmpTargetPosition.GetPosition();

			// Add the distance to the target unit
			var dx = targetPos.x - pos.x;
			var dz = targetPos.z - pos.z;
			var d = Math.sqrt(dx*dx + dz*dz);
			distance += d;

			// Return the total distance to the target
			return distance;

		default:
			error("ComputeWalkingDistance: Unrecognised order type '"+order.type+"'");
			return distance;
		}
	}

	// Return the total distance to the end of the order queue
	return distance;
};

UnitAI.prototype.AddOrder = function(type, data, queued)
{
	if (queued)
		this.PushOrder(type, data);
	else
		this.ReplaceOrder(type, data);
};

UnitAI.prototype.Walk = function(x, z, queued)
{
	this.AddOrder("Walk", { "x": x, "z": z }, queued);
};

UnitAI.prototype.WalkToTarget = function(target, queued)
{
	this.AddOrder("WalkToTarget", { "target": target }, queued);
};

UnitAI.prototype.LeaveFoundation = function(target)
{
	// If we're already being told to leave a foundation, then
	// ignore this new request so we don't end up being too indecisive
	// to ever actually move anywhere
	if (this.order && this.order.type == "LeaveFoundation")
		return;

	this.PushOrderFront("LeaveFoundation", { "target": target });
};

UnitAI.prototype.Attack = function(target, queued)
{
	if (!this.CanAttack(target))
	{
		this.WalkToTarget(target, queued);
		return;
	}

	this.AddOrder("Attack", { "target": target, "force": true }, queued);
};

UnitAI.prototype.Garrison = function(target, queued)
{
	if (!this.CanGarrison(target))
	{
		this.WalkToTarget(target, queued);
		return;
	}
	this.AddOrder("Garrison", { "target": target }, queued);
};

UnitAI.prototype.Ungarrison = function()
{
	if (this.IsGarrisoned())
	{
		this.AddOrder("Ungarrison", null, false);
	}
};

UnitAI.prototype.Gather = function(target, queued)
{
	if (!this.CanGather(target))
	{
		this.WalkToTarget(target, queued);
		return;
	}

	// Save the resource type now, so if the resource gets destroyed
	// before we process the order then we still know what resource
	// type to look for more of
	var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
	var type = cmpResourceSupply.GetType();

	// Remember the position of our target, if any, in case it disappears
	// later and we want to head to its last known position
	// (TODO: if the target moves a lot (e.g. it's an animal), maybe we
	// need to update this lastPos regularly rather than just here?)
	var lastPos = undefined;
	var cmpPosition = Engine.QueryInterface(target, IID_Position);
	if (cmpPosition && cmpPosition.IsInWorld())
		lastPos = cmpPosition.GetPosition();

	this.AddOrder("Gather", { "target": target, "type": type, "lastPos": lastPos }, queued);
};

UnitAI.prototype.ReturnResource = function(target, queued)
{
	if (!this.CanReturnResource(target))
	{
		this.WalkToTarget(target, queued);
		return;
	}

	this.AddOrder("ReturnResource", { "target": target }, queued);
};

UnitAI.prototype.Repair = function(target, autocontinue, queued)
{
	if (!this.CanRepair(target))
	{
		this.WalkToTarget(target, queued);
		return;
	}

	this.AddOrder("Repair", { "target": target, "autocontinue": autocontinue }, queued);
};

UnitAI.prototype.Flee = function(target, queued)
{
	this.AddOrder("Flee", { "target": target }, queued);
};

UnitAI.prototype.Cheer = function()
{
	this.AddOrder("Cheering", null, false);
};

UnitAI.prototype.SetStance = function(stance)
{
	if (g_Stances[stance])
		this.stance = stance;
	else
		error("UnitAI: Setting to invalid stance '"+stance+"'");
}

UnitAI.prototype.SwitchToStance = function(stance)
{
	var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
	if (!cmpPosition || !cmpPosition.IsInWorld())
		return;
	var pos = cmpPosition.GetPosition();
	this.SetHeldPosition(pos.x, pos.z);

	this.SetStance(stance);
	if (stance == "stand" || stance == "defensive" || stance == "passive")
		this.StopMoving();

	// Reset the range query, since the range depends on stance
	this.SetupRangeQuery();
}

/**
 * Resets losRangeQuery, and if there are some targets in range that we can
 * attack then we start attacking and this returns true; otherwise, returns false.
 */
UnitAI.prototype.FindNewTargets = function()
{
	if (!this.losRangeQuery)
		return false;

	var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
	var ents = rangeMan.ResetActiveQuery(this.losRangeQuery);

	if (!this.GetStance().targetVisibleEnemies)
		return false;

	SortEntitiesByPriority(ents);
	return this.RespondToTargetedEntities(ents);
};

UnitAI.prototype.GetQueryRange = function()
{
	var ret = { "min": 0, "max": 0 };
	if (this.GetStance().respondStandGround)
	{
		var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
		if (!cmpRanged)
			return ret;
		var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
		ret.min = range.min;
		ret.max = range.max;
	}
	else if (this.GetStance().respondChase)
	{
		var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
		if (!cmpVision)
			return ret;
		var range = cmpVision.GetRange();
		ret.max = range;
	}
	else if (this.GetStance().respondHoldGround)
	{
		var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);
		if (!cmpRanged)
			return ret;
		var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
		var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
		if (!cmpVision)
			return ret;
		var halfvision = cmpVision.GetRange() / 2;
		ret.max = range.max + halfvision;
	}
	return ret;
};

UnitAI.prototype.GetStance = function()
{
	return g_Stances[this.stance];
};

UnitAI.prototype.GetStanceName = function()
{
	return this.stance;
};

//// Helper functions ////

UnitAI.prototype.CanAttack = function(target)
{
	// Formation controllers should always respond to commands
	// (then the individual units can make up their own minds)
	if (this.IsFormationController())
		return true;

	// Verify that we're able to respond to Attack commands
	var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
	if (!cmpAttack)
		return false;

	// TODO: verify that this is a valid target

	return true;
};

UnitAI.prototype.CanGarrison = function(target)
{
	var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
	if (!cmpGarrisonHolder)
		return false;

	// Don't let animals garrison for now
	// (If we want to support that, we'll need to change Order.Garrison so it
	// doesn't move the animal into an INVIDIDUAL.* state)
	if (this.IsAnimal())
		return false;

	return true;
};

UnitAI.prototype.CanGather = function(target)
{
	// Formation controllers should always respond to commands
	// (then the individual units can make up their own minds)
	if (this.IsFormationController())
		return true;

	// Verify that we're able to respond to Gather commands
	var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
	if (!cmpResourceGatherer)
		return false;

	// Verify that we can gather from this target
	if (!cmpResourceGatherer.GetTargetGatherRate(target))
		return false;

	// TODO: should verify it's owned by the correct player, etc

	return true;
};

UnitAI.prototype.CanReturnResource = function(target)
{
	// Formation controllers should always respond to commands
	// (then the individual units can make up their own minds)
	if (this.IsFormationController())
		return true;

	// Verify that we're able to respond to ReturnResource commands
	var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
	if (!cmpResourceGatherer)
		return false;

	// Verify that the target is a dropsite
	var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
	if (!cmpResourceDropsite)
		return false;

	// Verify that we are carrying some resources,
	// and can return our current resource to this target
	var type = cmpResourceGatherer.GetMainCarryingType();
	if (!type || !cmpResourceDropsite.AcceptsType(type))
		return false;

	// TODO: should verify it's owned by the correct player, etc

	return true;
};

UnitAI.prototype.CanRepair = function(target)
{
	// Formation controllers should always respond to commands
	// (then the individual units can make up their own minds)
	if (this.IsFormationController())
		return true;

	// Verify that we're able to respond to Repair (Builder) commands
	var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
	if (!cmpBuilder)
		return false;

	// TODO: verify that this is a valid target

	return true;
};

//// Animal specific functions ////

UnitAI.prototype.MoveRandomly = function(distance)
{
	// We want to walk in a random direction, but avoid getting stuck
	// in obstacles or narrow spaces.
	// So pick a circular range from approximately our current position,
	// and move outwards to the nearest point on that circle, which will
	// lead to us avoiding obstacles and moving towards free space.

	// TODO: we probably ought to have a 'home' point, and drift towards
	// that, so we don't spread out all across the whole map

	var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
	if (!cmpPosition)
		return;

	if (!cmpPosition.IsInWorld())
		return;

	var pos = cmpPosition.GetPosition();

	var jitter = 0.5;

	// Randomly adjust the range's center a bit, so we tend to prefer
	// moving in random directions (if there's nothing in the way)
	var tx = pos.x + (2*Math.random()-1)*jitter;
	var tz = pos.z + (2*Math.random()-1)*jitter;

	var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
	cmpMotion.MoveToPointRange(tx, tz, distance, distance);
};

UnitAI.prototype.SetMoveSpeed = function(speed)
{
	var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
	cmpMotion.SetSpeed(speed);
};

UnitAI.prototype.SetHeldPosition = function(x, z)
{
	this.heldPosition = {"x": x, "z": z};
};

UnitAI.prototype.GetHeldPosition = function(pos)
{
	return this.heldPosition;
};

UnitAI.prototype.WalkToHeldPosition = function()
{
	if (this.heldPosition)
	{
		this.Walk(this.heldPosition.x, this.heldPosition.z, false);
		return true;
	}
	return false;
};

Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
