package.loaded.Constants = nil; require("Constants")
require("Actors/AI/PID")

HumanBehaviors = {}

function HumanBehaviors.LookForTargets(self)
	local Origin
	local viewAng
	
	if self.Target then	-- widen the view angle to increase the chance of spotting new targets
		Origin = self.EyePos
		viewAng = 0.7
	elseif self.deviceState == AHuman.AIMING then
		if self.EquippedItem then
			Origin = self.EquippedItem.Pos
		else
			Origin = self.EyePos
		end
		
		viewAng = 0.3
	elseif self.deviceState == AHuman.POINTING then
		Origin = self.EyePos
		viewAng = 0.5
	elseif self.deviceState == AHuman.THROWING then
		Origin = self.EyePos
		viewAng = 0.7
	else
		Origin = self.EyePos
		viewAng = RangeRand(0.5, 1.4)
	end
	
	local viewLen = SceneMan:ShortestDistance(self.EyePos, self.ViewPoint, false).Magnitude + FrameMan.PlayerScreenWidth*0.5
	local Trace = Vector(viewLen, 0):RadRotate(viewAng*NormalRand()+self:GetAimAngle(true))
	local ID = SceneMan:CastMORay(Origin, Trace, self.ID, self.IgnoresWhichTeam, rte.grassID, false, 5)
	
	if ID < rte.NoMOID then
		local HitPoint = SceneMan:GetLastRayHitPos()
		if not SceneMan:IsUnseen(HitPoint.X, HitPoint.Y, self.Team) then
			local MO = MovableMan:GetMOFromID(ID)
			if MO and ID ~= MO.RootID then
				MO = MovableMan:GetMOFromID(MO.RootID)
			end
			
			return MO, HitPoint
		end
	end
end

function HumanBehaviors.CalculateThreatLevel(self, MO)
	-- prioritize closer targets
	local priority = -SceneMan:ShortestDistance(self.Pos, MO.Pos, false).Largest / FrameMan.PlayerScreenWidth
	
	-- prioritize the weaker humans over crabs
	if MO.ClassName == "AHuman" then
		if MO.FirearmIsReady then	-- prioritize armed targets
			priority = priority + 3
		else
			priority = priority + 1
		end
	elseif MO.ClassName == "ACrab" then
		if MO.FirearmIsReady then	-- prioritize armed targets
			priority = priority + 2.5
		else
			priority = priority + 0.5
		end
	end
	
	return priority - MO.Health / 300	-- prioritize damaged targets
end

function HumanBehaviors.ProcessAlarmEvent(self)
	if self.AlarmPos then
		self.LastAlarmPos = self.AlarmPos
		self.AlarmPos = nil
	end
	
	local loudness, AlarmVec
	for Event in MovableMan.AlarmEvents do
		if Event.Team ~= self.Team then	-- caused by some other team's activites - alarming!
			loudness = FrameMan.PlayerScreenWidth * 0.5 * self.Perceptiveness * (Event.Range / 500)	-- adjust the audiable range to the screen resolution
			AlarmVec = SceneMan:ShortestDistance(self.EyePos, Event.ScenePos, false)	-- see how far away the alarm situation is
			if AlarmVec.Largest < loudness then	-- only react if the alarm is within hearing range
				-- if this is the same alarm location as last, then don't repeat the signal
				-- check if we have line of sight to the alarm point
				if (not self.LastAlarmPos or SceneMan:ShortestDistance(self.LastAlarmPos, Event.ScenePos, false).Largest > 20) and
					not SceneMan:CastStrengthRay(self.EyePos, AlarmVec, 6, Vector(), 7, rte.grassID, true)
				then
					self.AlarmPos = Vector(Event.ScenePos.X, Event.ScenePos.Y)
					return true
				end
			-- sometimes try to shoot back at enemies outside our view range (400 is the range of the brain alarm)
			elseif not self.flying and Event.Range > 400 and PosRand() < 0.5 and self.FirearmIsReady then
				-- only do this if we are facing the event
				if (AlarmVec.X < 0 and self.HFlipped) or (AlarmVec.X > 0 and not self.HFlipped) then
					if AlarmVec.Largest < FrameMan.PlayerScreenWidth * 1.5 * self.Perceptiveness and
						(not self.LastAlarmPos or SceneMan:ShortestDistance(self.LastAlarmPos, Event.ScenePos, false).Largest > 20)
					then
						-- check LOS
						local ID = SceneMan:CastMORay(self.EyePos, AlarmVec, self.ID, self.IgnoresWhichTeam, rte.grassID, false, 11)
						if ID < rte.NoMOID then
							local FoundMO = MovableMan:GetMOFromID(ID)
							if ID ~= FoundMO.RootID then
								FoundMO = MovableMan:GetMOFromID(FoundMO.RootID)
							end
							
							if FoundMO.ClassName == "AHuman" then
								FoundMO = ToAHuman(FoundMO)
							elseif FoundMO.ClassName == "ACrab" then
								FoundMO = ToACrab(FoundMO)
							else
								FoundMO = nil
							end
							
							if FoundMO and FoundMO:GetController():IsState(Controller.WEAPON_FIRE) and FoundMO.Vel.Largest < 20 then
								-- compare the enemy aim angle with the angle of the alarm vector
								local enemyAim = FoundMO:GetAimAngle(true) + math.pi	-- rotate 180 degrees
								if enemyAim > math.pi*2 then	-- make sure the angle is in the [0..2*pi) range
									enemyAim = enemyAim - math.pi*2
								elseif enemyAim < 0 then
									enemyAim = enemyAim + math.pi*2
								end
								
								local angDiff = AlarmVec.AbsRadAngle - enemyAim
								if angDiff > math.pi then	-- the difference between two angles can never be larger than pi
									angDiff = angDiff - math.pi*2
								elseif angDiff < -math.pi then
									angDiff = angDiff + math.pi*2
								end
								
								if math.abs(angDiff) < 0.8 then
									-- this actor is shooting in our direction
									self.ReloadTimer:Reset()
									self.TargetLostTimer:Reset()
									
									-- try to shoot back
									self.UnseenTarget = FoundMO
									self:CreateSuppressBehavior()
									
									self.AlarmPos = Event.ScenePos
									return true
								end
							else
								self.LastAlarmPos = Event.ScenePos
							end
						else
							self.LastAlarmPos = Event.ScenePos
						end
					end
				end
			end
		end
	end
end

function HumanBehaviors.GetGrenadeAngle(AimPoint, TargetVel, StartPos, muzVel)
	local Dist = SceneMan:ShortestDistance(StartPos, AimPoint, false)
	local range = Dist.Magnitude
	
	-- compensate for gravity if the point we are trying to hit is more than 2m away
	if range > 40 then
		local timeToTarget = range / muzVel
		
		-- lead the target if target speed and projectile TTT is above the threshold
		if timeToTarget * TargetVel.Magnitude > 0.5 then
			AimPoint = AimPoint + TargetVel * timeToTarget
			Dist = SceneMan:ShortestDistance(StartPos, AimPoint, false)
		end
		
		Dist = Dist / FrameMan.PPM	-- convert from pixels to meters
		local velSqr = muzVel*muzVel
		local gravity = SceneMan.GlobalAcc.Y
		local root = math.sqrt(velSqr*velSqr - gravity*(gravity*Dist.X*Dist.X+2*-Dist.Y*velSqr))
		
		if root ~= root then
			return nil	-- no solution exists if the root is NaN
		end
		
		return math.atan2(velSqr-root, gravity*Dist.X)
	end
	
	return Dist.AbsRadAngle
end

-- make sure we equipp a primary weapon if we have one. return true if we must run this function again to be sure
function HumanBehaviors.EquipPrimaryWeapon(self)
	if self.EquippedItem then
		if self.InventorySize > 0 and (not self.PlayerPreferredHD or self.EquippedItem.PresetName ~= self.PlayerPreferredHD) and
			not self.EquippedItem:HasObjectInGroup("Primary Weapons") and self:HasObjectInGroup("Primary Weapons")
		then
			-- the weapon equipped is not a primary weapon, but there is one in the inventory
			self.Ctrl:SetState(Controller.WEAPON_CHANGE_NEXT, true)
			return true
		end
	elseif self.ClassName == "AHuman" then
		return self:EquipFirearm(true)
	end
end

-- in sentry behavior the agent only looks for new enemies, it sometimes sharp aims to increse spotting range
function HumanBehaviors.Sentry(self)
	local sweepUp = true
	local sweepDone = false
	local maxAng = 1.4
	local minAng = -1.4
	local aim
	
	if HumanBehaviors.EquipPrimaryWeapon(self) then
		local EquipTimer = Timer()
		while true do
			if EquipTimer:IsPastSimMS(500) then
				if not HumanBehaviors.EquipPrimaryWeapon(self) then
					break -- our current weapon is either a primary, we have no primary or we have no weapon
				end
			end
			
			coroutine.yield()
		end
	end
	
	if self.OldTargetPos then	-- try to reaquire an old target
		local Dist = SceneMan:ShortestDistance(self.EyePos, self.OldTargetPos, false)
		self.OldTargetPos = nil
		if (Dist.X < 0 and self.HFlipped) or (Dist.X > 0 and not self.HFlipped) then	-- we are facing the target	
			self.deviceState = AHuman.AIMING
			self.Ctrl.AnalogAim = Dist.Normalized
			
			for i = 1, 30 do
				coroutine.yield()	-- aim here for ~0.5s
			end
		end
	elseif not self.isPlayerOwned then -- face the most likely enemy approach direction
		for i = 1, math.random(5) do	-- wait for a while
			coroutine.yield()
		end
		
		self:AddAISceneWaypoint(Vector(self.Pos.X, 0))
		self:UpdateMovePath()
		coroutine.yield()	-- wait until next frame
		
		-- face the direction of the first waypoint
		for WptPos in self.MovePath do
			local Dist = SceneMan:ShortestDistance(self.Pos, WptPos, false)
			if Dist.X > 5 then
				self.SentryFacing = false
				self.Ctrl.AnalogAim = Dist.Normalized
			elseif Dist.X < -5 then
				self.SentryFacing = true
				self.Ctrl.AnalogAim = Dist.Normalized
			end
			
			break
		end
	end
	
	while true do	-- start by looking forward
		aim = self:GetAimAngle(false)
		
		if sweepUp then
			if aim < maxAng/3 then
				self.Ctrl:SetState(Controller.AIM_UP, false)
				coroutine.yield()	-- wait until next frame
				self.Ctrl:SetState(Controller.AIM_UP, true)
			else
				sweepUp = false
			end
		else
			if aim > minAng/3 then
				self.Ctrl:SetState(Controller.AIM_DOWN, false)
				coroutine.yield()	-- wait until next frame
				self.Ctrl:SetState(Controller.AIM_DOWN, true)
			else
				sweepUp = true
				if sweepDone then
					break
				else
					sweepDone = true
				end
			end
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	if self.HFlipped ~= self.SentryFacing then
		self.HFlipped = self.SentryFacing	-- turn to the direction we have been order to guard
		return true	-- restart this behavior
	end
	
	while true do	-- look down
		aim = self:GetAimAngle(false)
		if aim > minAng then
			self.Ctrl:SetState(Controller.AIM_DOWN, true)
		else
			break
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	local Hit = Vector()
	local NoObstacle = {}
	local StartPos
	self.deviceState = AHuman.AIMING
	
	while true do	-- scan the area for obstacles
		aim = self:GetAimAngle(false)
		if aim < maxAng then
			self.Ctrl:SetState(Controller.AIM_UP, true)
		else
			break
		end
		
		if self:EquipFirearm(false) and self.EquippedItem then
			StartPos = ToHeldDevice(self.EquippedItem).MuzzlePos
		else
			StartPos = self.EyePos
		end
		
		-- save the angle to a table if there is no obstacle
		if not SceneMan:CastStrengthRay(StartPos, Vector(60, 0):RadRotate(self:GetAimAngle(true)), 5, Hit, 2, 0, true) then
			table.insert(NoObstacle, aim)	-- TODO: don't use a table for this
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	local SharpTimer = Timer()
	local aimTime = 2000
	local angDiff = 1
	self.deviceState = AHuman.POINTING
	
	if #NoObstacle > 1 then	-- only aim where we know there are no obstacles, e.g. out of a gun port
		minAng = NoObstacle[1] * 0.95
		maxAng = NoObstacle[#NoObstacle] * 0.95
		angDiff = 1 / math.max(math.abs(maxAng - minAng), 0.1)	-- sharp aim longer from a small aiming window
	end
	
	while true do
		if not self:EquipFirearm(false) and not self:EquipThrowable(false) then
			break
		end
		
		aim = self:GetAimAngle(false)
		
		if sweepUp then
			if aim < maxAng then
				if aim < maxAng/5 and aim > minAng/5 and PosRand() > 0.3 then
					self.Ctrl:SetState(Controller.AIM_UP, false)
				else
					self.Ctrl:SetState(Controller.AIM_UP, true)
				end
			else
				sweepUp = false
			end
		else
			if aim > minAng then
				if aim < maxAng/5 and aim > minAng/5 and PosRand() > 0.3 then
					self.Ctrl:SetState(Controller.AIM_DOWN, false)
				else
					self.Ctrl:SetState(Controller.AIM_DOWN, true)
				end
			else
				sweepUp = true
			end
		end
		
		if SharpTimer:IsPastSimMS(aimTime) then
			SharpTimer:Reset()
			
			if HumanBehaviors.EquipPrimaryWeapon(self) then
				aimTime = 500
			elseif self.deviceState == AHuman.AIMING then
				aimTime = RangeRand(1000, 3000)
				self.deviceState = AHuman.POINTING
			else
				aimTime = RangeRand(6000, 12000) * angDiff
				self.deviceState = AHuman.AIMING
			end
			
			if SceneMan:ShortestDistance(self.Pos, self.SentryPos, false).Magnitude > self.Height*0.7 then
				self.SentryPos = SceneMan:MovePointToGround(self.SentryPos, self.Height*0.25, 2)
				self:ClearAIWaypoints()
				self:AddAISceneWaypoint(self.SentryPos)
				self:CreateGoToBehavior()	-- try to return to the sentry pos
				break
			elseif self.HFlipped ~= self.SentryFacing then
				self.HFlipped = self.SentryFacing	-- turn to the direction we have been order to guard
				break	-- restart this behavior
			end
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	return true
end


function HumanBehaviors.Patrol(self)
	while self.flying or self.Vel.Magnitude > 4 do	-- wait untill we are stationary
		return true
	end
	
	if HumanBehaviors.EquipPrimaryWeapon(self) then
		local EquipTimer = Timer()
		while true do
			if EquipTimer:IsPastSimMS(500) then
				if not HumanBehaviors.EquipPrimaryWeapon(self) then
					break -- our current weapon is either a primary, we have no primary or we have no weapon
				end
			end
			
			coroutine.yield()
		end
	end
	
	local Free = Vector()
	local WptA, WptB
	
	-- look for a path to the right
	SceneMan:CastObstacleRay(self.Pos, Vector(512, 0), Vector(), Free, self.ID, self.IgnoresWhichTeam, rte.grassID, 4)
	local Dist = SceneMan:ShortestDistance(self.Pos, Free, false)
	
	if Dist.Magnitude > 20 then
		self:ClearAIWaypoints()
		self:AddAISceneWaypoint(Free)
		coroutine.yield()	-- wait until next frame
		self:UpdateMovePath()
		coroutine.yield()	-- wait until next frame
		
		local PrevPos = Vector(self.Pos.X, self.Pos.Y)
		for WptPos in self.MovePath do
			if math.abs(PrevPos.Y - WptPos.Y) > 14 then
				break
			end
			
			WptA = Vector(PrevPos.X, PrevPos.Y)
			PrevPos:SetXY(WptPos.X, WptPos.Y)
		end
	end
	
	-- look for a path to the left
	SceneMan:CastObstacleRay(self.Pos, Vector(-512, 0), Vector(), Free, self.ID, self.IgnoresWhichTeam, rte.grassID, 4)
	Dist = SceneMan:ShortestDistance(self.Pos, Free, false)
	
	if Dist.Magnitude > 20 then
		self:ClearAIWaypoints()
		self:AddAISceneWaypoint(Free)
		coroutine.yield()	-- wait until next frame
		self:UpdateMovePath()
		coroutine.yield()	-- wait until next frame
		
		local PrevPos = Vector(self.Pos.X, self.Pos.Y)
		for WptPos in self.MovePath do
			if math.abs(PrevPos.Y - WptPos.Y) > 14 then
				break
			end
			
			WptB = Vector(PrevPos.X, PrevPos.Y)
			PrevPos:SetXY(WptPos.X, WptPos.Y)
		end
	end	
	
	self:ClearAIWaypoints()
	coroutine.yield()	-- wait until next frame
	
	if WptA then
		Dist = SceneMan:ShortestDistance(self.Pos, WptA, false)
		if Dist.Magnitude > 20 then
			self:AddAISceneWaypoint(WptA)
		else
			WptA = nil
		end
	end
	
	if WptB then
		Dist = SceneMan:ShortestDistance(self.Pos, WptB, false)
		if Dist.Magnitude > 20 then
			self:AddAISceneWaypoint(WptB)
		else
			WptB = nil
		end
	end
	
	if WptA or WptB then
		self:CreateGoToBehavior()
	else	-- no path was found
		local FlipTimer = Timer()
		FlipTimer:SetSimTimeLimitMS(3000)
		while true do
			coroutine.yield()	-- wait until next frame
			if FlipTimer:IsPastSimTimeLimit() then
				FlipTimer:Reset()
				FlipTimer:SetSimTimeLimitMS(RangeRand(2000, 5000))
				self.HFlipped = not self.HFlipped	-- turn around and try the other direction sometimes
				if PosRand() < 0.3 then
					break	-- end the behavior
				end
			end
		end
	end
	
	return true
end


function HumanBehaviors.GoldDig(self)
	if not self:EquipDiggingTool(true) then
		self:CreateGetToolBehavior()
		return true
	end
	
	local aimAngle = 0.57
	local LookVec = Vector(200,0):RadRotate(aimAngle)
	local GoldPos = Vector()
	local GoldTable = {}
	
	while true do
		self.Ctrl.AnalogAim = LookVec.Normalized
		if SceneMan:CastMaterialRay(self.EyePos, LookVec, rte.goldID, GoldPos, 2, true) then
			coroutine.yield()	-- wait until next frame
			
			local str = SceneMan:CastStrengthSumRay(self.EyePos, self.EyePos+LookVec, 4, rte.goldID)/5	-- prioritise gold in soft ground
			local Dist = SceneMan:ShortestDistance(self.Pos, GoldPos, false)	-- prioritise gold close to us
			
			if GoldPos.Y > SceneMan.SceneHeight - 40 then
				str = str + 3000	-- avoid gold close to the lower edege of the scene
			end
			
			if not SceneMan.SceneWrapsX and GoldPos.X < 40 or GoldPos.X > SceneMan.SceneWidth - 40 then
				str = str + 2000	-- avoid gold close to the edeges of a non-wrapping scene
			end
			
			if Dist.Magnitude < 30 then	-- dig to a point behind the gold
				GoldPos = self.Pos + Dist:SetMagnitude(40)
			end
			
			table.insert(GoldTable, {Pos=Vector(GoldPos.X, GoldPos.Y), prio=str+Dist.Magnitude})
		end
		
		if aimAngle < -3.71 then
			break
		else
			aimAngle = aimAngle - 0.025
			LookVec = Vector(200,0):RadRotate(aimAngle)
			coroutine.yield()	-- wait until next frame
		end
	end
	
	if #GoldTable > 0 then
		table.sort(GoldTable, function(A, B) return A.prio < B.prio end)
		coroutine.yield()	-- wait until next frame
		
		self:ClearAIWaypoints()
		self:AddAISceneWaypoint(GoldTable[1].Pos)
		self:CreateGoToBehavior()
	elseif self.Pos.Y + 200 < SceneMan.SceneHeight then	-- don't dig beyond the scene limit
		-- no gold found, so dig down and try again
		local str_r = SceneMan:CastStrengthSumRay(self.Pos, self.Pos+Vector( 100, 100), 5, rte.goldID)
		coroutine.yield()	-- wait until next frame
		local str_l = SceneMan:CastStrengthSumRay(self.Pos, self.Pos+Vector(-100, 100), 5, rte.goldID)
		coroutine.yield()	-- wait until next frame
		
		self:ClearAIWaypoints()
		if str_r < str_l then
			self:AddAISceneWaypoint(self.Pos+Vector( 100, 100))
		else
			self:AddAISceneWaypoint(self.Pos+Vector(-100, 100))
		end
		
		self:CreateGoToBehavior()
	end
	
	return true
end


-- find the closest enemy brain
function HumanBehaviors.BrainSearch(self)
	if HumanBehaviors.EquipPrimaryWeapon(self) then
		local EquipTimer = Timer()
		while true do
			if EquipTimer:IsPastSimMS(500) then
				if not HumanBehaviors.EquipPrimaryWeapon(self) then
					break -- our current weapon is either a primary, we have no primary or we have no weapon
				end
			end
			
			coroutine.yield()
		end
	end
	
	local Brains = {}
	
	for Act in MovableMan.Actors do
		if Act.Team ~= self.Team and Act:HasObjectInGroup("Brains") then
			table.insert(Brains, Act)
		end
	end
	
	coroutine.yield()	-- wait until next frame
	
	if #Brains < 1 then	-- no bain actors found, check if some other actor is the brain
		local GmActiv = ActivityMan:GetActivity()
		for player = Activity.PLAYER_1, Activity.MAXPLAYERCOUNT - 1 do
			if GmActiv:PlayerActive(player) and GmActiv:GetTeamOfPlayer(player) ~= self.Team then
				local Target = GmActiv:GetPlayerBrain(player)
				if Target and MovableMan:IsActor(Target) then
					table.insert(Brains, Target)
				end
			end
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	if #Brains > 0 then
		if #Brains == 1 then
			if MovableMan:IsActor(Brains[1]) then
				self:ClearAIWaypoints()
				self.FollowingActor = Brains[1]
				self:AddAIMOWaypoint(self.FollowingActor)
				self:CreateGoToBehavior()
			end
		else
			local ClosestBrain
			local minDist = math.huge
			for _, Act in pairs(Brains) do
				if MovableMan:IsActor(Act) then
					self:ClearAIWaypoints()
					self:AddAISceneWaypoint(Act.Pos)
					self:UpdateMovePath()
					
					local waypoints = self.MovePathSize
					if waypoints < minDist then
						minDist = waypoints
						ClosestBrain = Act
					end
					
					coroutine.yield()	-- wait until next frame
				end
			end
			
			self:ClearAIWaypoints()
			
			if MovableMan:IsActor(ClosestBrain) then
				self.FollowingActor = ClosestBrain
				self:AddAIMOWaypoint(self.FollowingActor)
				self:CreateGoToBehavior()
			else
				return true	-- the brain we found died while we where searching, restart this behavior next frame
			end
		end
	else
		self.AIMode = Actor.AIMODE_PATROL	-- no enemy brains left
	end
	
	return true
end


-- find a weapon to pick up
function HumanBehaviors.WeaponSearch(self)
	local range, minDist, HD
	local Devices = {}
	local pickupDiggers = not self:EquipDiggingTool(false)
	
	if self.isPlayerOwned then
		minDist = 70	-- don't move player actors more than 3.5m
	else
		minDist = FrameMan.PlayerScreenWidth * 0.45
	end
	
	for Item in MovableMan.Items do	-- store all HeldDevices of the correct type and within a cerain range in a table
		HD = ToHeldDevice(Item)
		if not HD:IsActivated() and HD.Vel.Magnitude < 2 then
			range = SceneMan:ShortestDistance(self.Pos, HD.Pos, false).Magnitude
			if range < minDist and not SceneMan:IsUnseen(HD.Pos.X, HD.Pos.Y, self.Team) then
				table.insert(Devices, HD)
			end
		end
	end
	
	if #Devices > 0 then
		coroutine.yield()	-- wait until next frame
		
		local PrevWpt
		if not self.FollowingActor then
			if self.AIMode == Actor.AIMODE_SENTRY and not self.flying then
				PrevWpt = Vector(self.Pos.X, self.Pos.Y)	-- return here after pick up
			else
				PrevWpt = self:GetLastAIWaypoint()
				if (PrevWpt-self.Pos).Largest < 1 then
					PrevWpt = nil	-- we have no waypoint
				end
			end
		end
		
		if self.isPlayerOwned then
			minDist = 10	-- # of waypoints
		else
			minDist = 48
		end
		
		local waypoints
		local DevicesToPickUp = {}
		for _, Item in pairs(Devices) do
			if MovableMan:IsDevice(Item) then
				self:ClearAIWaypoints()
				self:AddAISceneWaypoint(Item.Pos)
				self:UpdateMovePath()
				
				-- estimate the walking distance to the item				
				if Item.ClassName == "TDExplosive" then
					waypoints = self.MovePathSize * 1.4	-- prioritize non-grenades
				elseif Item:IsTool() then
					if pickupDiggers then
						waypoints = self.MovePathSize * 1.8	-- prioritize non-diggers
					else
						waypoints = minDist -- don't pick up
					end
				else
					waypoints = self.MovePathSize
				end
				
				if waypoints < minDist then
					table.insert(DevicesToPickUp, {HD=Item, range=waypoints})
				end
				
				coroutine.yield()	-- wait until next frame
			end
		end
		
		self:ClearAIWaypoints()
		table.sort(DevicesToPickUp, function(A,B) return A.range < B.range end)	-- sort the items in order of distance
		coroutine.yield()	-- wait until next frame
		
		self.PickupHD = nil
		for _, Data in pairs(DevicesToPickUp) do
			if MovableMan:IsDevice(Data.HD) then
				self.PickupHD = Data.HD
				break
			end
		end
		
		if self.PickupHD then
			if PrevWpt then
				self.PrevAIWaypoint = PrevWpt
			end
			
			self:AddAIMOWaypoint(self.PickupHD)
			self:CreateGoToBehavior()
		else	-- no items found
			if self.FollowingActor and MovableMan:IsActor(self.FollowingActor) then
				self:AddAIMOWaypoint(self.FollowingActor)
			elseif self.PrevAIWaypoint then
				self:AddAISceneWaypoint(self.PrevAIWaypoint)
			end
		end
	end
	
	return true
end


-- find a tool to pick up
function HumanBehaviors.ToolSearch(self)
	local range, minDist, HD
	local Devices = {}
	
	if self.AIMode == Actor.AIMODE_GOLDDIG then
		minDist = FrameMan.PlayerScreenWidth * 0.5	-- move up to half a screen when digging
	elseif self.isPlayerOwned then
		minDist = 50	-- don't move player actors more than 2.5m
	else
		minDist = FrameMan.PlayerScreenWidth * 0.3
	end
	
	for Item in MovableMan.Items do	-- store all HeldDevices of the correct type and within a cerain range in a table
		HD = ToHeldDevice(Item)
		if HD and HD:IsTool() and HD.Vel.Magnitude < 2 then
			range = SceneMan:ShortestDistance(self.Pos, HD.Pos, false).Magnitude
			if range < minDist and not SceneMan:IsUnseen(HD.Pos.X, HD.Pos.Y, self.Team) then
				table.insert(Devices, HD)
			end
		end
	end
	
	if #Devices > 0 then
		coroutine.yield()	-- wait until next frame
		
		local PrevWpt
		if not self.FollowingActor then
			if self.AIMode == Actor.AIMODE_SENTRY and not self.flying then
				PrevWpt = Vector(self.Pos.X, self.Pos.Y)	-- return here after pick up
			else
				PrevWpt = self:GetLastAIWaypoint()
				if (PrevWpt-self.Pos).Largest < 1 then
					PrevWpt = nil	-- we have no waypoint
				end
			end
		end
		
		if self.AIMode == Actor.AIMODE_GOLDDIG then
			minDist = 30
		elseif self.isPlayerOwned then
			minDist = 5
		else
			minDist = 15
		end
		
		for _, Item in pairs(Devices) do
			if MovableMan:IsDevice(Item) then
				self:ClearAIWaypoints()
				self:AddAISceneWaypoint(Item.Pos)
				self:UpdateMovePath()
				
				local waypoints = self.MovePathSize	-- estimate the walking distance to the item
				if waypoints < minDist then
					minDist = waypoints
					self.PickupHD = Item
				end
				
				coroutine.yield()	-- wait until next frame
			end
		end
		
		self:ClearAIWaypoints()
		
		if MovableMan:IsDevice(self.PickupHD) then		
			if PrevWpt then
				self.PrevAIWaypoint = PrevWpt
			end
			
			self:AddAIMOWaypoint(self.PickupHD)
			self:CreateGoToBehavior()
		else
			self.PickupHD = nil	-- the item became invalid while searching
			
			if self.FollowingActor and MovableMan:IsActor(self.FollowingActor) then
				self:AddAIMOWaypoint(self.FollowingActor)
			elseif self.PrevAIWaypoint then
				self:AddAISceneWaypoint(self.PrevAIWaypoint)
			end
		end
	end
	
	return true
end


-- move to the next waypoint
function HumanBehaviors.GoToWpt(self)
	if not self.MOMoveTarget then
		if SceneMan:ShortestDistance(self:GetLastAIWaypoint(), self.Pos, false).Magnitude < 10 then
			self:ClearAIWaypoints()
			if self.AIMode == Actor.AIMODE_GOTO then
				self.AIMode = Actor.AIMODE_SENTRY
			end
			return true
		end
	end
	
	-- is Y1 lower down in the scene, compared to Y2?
	local Lower = function(Y1, Y2, margin)
		return Y1.Pos.Y - margin > Y2.Pos.Y
	end
	
	-- is Y1 higher up in the scene, compared to Y2?
	local Higher = function(Y1, Y2, margin)
		return Y1.Pos.Y + margin < Y2.Pos.Y
	end
	
	local AngleBetweenVectors = function(A, B)	-- returns the smallest angle between two vectors
		local angle = math.atan2(A.Y, A.X) - math.atan2(B.Y, B.X)
		
		if angle > 3.142 then
		 angle = angle - 6.283
		elseif angle < -3.142 then
		 angle = angle + 6.283
		end
		
		return angle
	end
	
	local UpdatePathTimer = Timer()
	local ArrivedTimer = Timer()
	local BurstTimer = Timer()
	
	local NoLOSTimer = Timer()
	NoLOSTimer:SetSimTimeLimitMS(1000)
	
	local StuckTimer = Timer()
	StuckTimer:SetSimTimeLimitMS(2000)
	
	local nextLatMove = self.lateralMoveState
	local nextAimAngle = self:GetAimAngle(false) * 0.95
	local scanAng = 0	-- for obstacle detection
	local Obstacles = {}
	local PrevWptPos = self.Pos
	local sweepCW = true
	local sweepRange = 0
	local digState = AHuman.NOTDIGGING
	local obstacleState = Actor.PROCEEDING
	local WptList, Waypoint, Dist, CurrDist
	
	while true do
		if self.Vel.Magnitude > 2 then
			StuckTimer:Reset()
		end
		
		if self.MOMoveTarget then	-- make the last waypoint marker stick to the MO we are following
			if MovableMan:ValidMO(self.MOMoveTarget) then
				self:RemoveMovePathEnd()
				self:AddToMovePathEnd(self.MOMoveTarget.Pos)
				
				if self.Team == self.MOMoveTarget.Team and not self.flying then	-- we are following an ally, stop when close
					while SceneMan:ShortestDistance(self.Pos, self.MOMoveTarget.Pos, false).Magnitude < self.Radius+self.MOMoveTarget.Radius+10 do
						coroutine.yield()	-- wait until next frame
						
						if not MovableMan:ValidMO(self.MOMoveTarget) then
							self:ClearMovePath()
							break
						end
					end
				end
			else
				self:ClearMovePath()
			end
		end
		
		if not self.flying and UpdatePathTimer:IsPastSimMS(6000) then
			if Waypoint and self.BlockingActor then
				if MovableMan:IsActor(self.BlockingActor) then
					CurrDist = SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false)
					if (self.Pos.X > self.BlockingActor.Pos.X and CurrDist.X < self.Pos.X) or
						(self.Pos.X < self.BlockingActor.Pos.X and CurrDist.X > self.Pos.X) or
						SceneMan:ShortestDistance(self.Pos, self.BlockingActor.Pos, false).Magnitude > self.Diameter + self.BlockingActor.Diameter
					then
						self.BlockingActor = nil	-- the blocking actor is not in the way any longer
						self.teamBlockState = Actor.NOTBLOCKED
					else
						self.BlockedTimer:Reset()
						self.teamBlockState = Actor.IGNORINGBLOCK
						self:CreateMoveAroundBehavior()
						break	-- end this behavior
					end
				else
					self.BlockingActor = nil
				end
			end
			
			UpdatePathTimer:Reset()
			self.deviceState = AHuman.STILL
			self.crawling = false
			self.jump = false
			nextLatMove = Actor.LAT_STILL
			Waypoint = nil
			WptList = nil
		elseif StuckTimer:IsPastSimTimeLimit() then	-- dislodge
			if self.jump then
				if self.JetTimeLeft < TimerMan.DeltaTimeMS * 3 then	-- out of fuel
					self.jump = false
					self.refuel = true
					nextLatMove = Actor.LAT_STILL
				else
					local chance = PosRand()
					if chance < 0.1 then
						nextLatMove = Actor.LAT_LEFT
					elseif chance > 0.9 then
						nextLatMove = Actor.LAT_RIGHT
					else
						nextLatMove = Actor.LAT_STILL
					end
				end
			else
				if PosRand() < 0.2 then
					if self.lateralMoveState == Actor.LAT_LEFT then
						nextLatMove = Actor.LAT_RIGHT
					else
						nextLatMove = Actor.LAT_LEFT
					end
				end
				
				if self.refuel and self.JetTimeLeft > self.JetTimeTotal * 0.95 then
					self.jump = true
					BurstTimer:SetSimTimeLimitMS(500)	-- this burst last until the BurstTimer expire
					BurstTimer:Reset()
				end
			end
		elseif WptList then	-- we have a list of waypoints, folow it
			if #WptList < 1 and not Waypoint then	-- arrived
				if self.MOMoveTarget and MovableMan:ValidMO(self.MOMoveTarget) then
					if self.Team ~= self.MOMoveTarget.Team then
						Waypoint = {Pos=self.MOMoveTarget.Pos}
					else
						nextLatMove = Actor.LAT_STILL
						self.jump = false
						coroutine.yield()	-- wait until next frame
					end
				else
					break
				end
			else
				if not Waypoint then	-- get the next waypoint in the list
					if not self.Target and digState == AHuman.NOTDIGGING then
						if HumanBehaviors.EquipPrimaryWeapon(self) then
							local EquipTimer = Timer()
							while true do
								if EquipTimer:IsPastSimMS(500) then
									if not HumanBehaviors.EquipPrimaryWeapon(self) then
										break -- our current weapon is either a primary, we have no primary or we have no weapon
									end
								end
								
								coroutine.yield()
							end
						end
					end
					
					-- we must update the path more often if we are following someone
					if self.MOMoveTarget and MovableMan:ValidMO(self.MOMoveTarget) then
						if SceneMan:ShortestDistance(self:GetLastAIWaypoint(), self.MOMoveTarget.Pos, false).Magnitude > 100 then	-- the target has moved
							WptList = nil
							Waypoint = nil
						else
							UpdatePathTimer:Reset()
						end
					else
						UpdatePathTimer:Reset()
					end
					
					if WptList then						
						local NextWptPos = WptList[1].Pos
						Dist = SceneMan:ShortestDistance(self.Pos, NextWptPos, false)
						if Dist.Y < -25 and math.abs(Dist.X) < 30 then	-- avoid any corners if the next waypoint is above us
							local CornerPos = Vector(NextWptPos.X, NextWptPos.Y)
							if self.Pos.X > CornerPos.X then
								CornerPos = CornerPos + Vector(25, -40)
								cornerType = "right"
							else
								CornerPos = CornerPos + Vector(-25, -40)
								cornerType = "left"
							end
							
							local Free = Vector()
							Dist = SceneMan:ShortestDistance(NextWptPos, CornerPos, false)
							-- make sure the corner waypoint is not inside terrain
							local pixels = SceneMan:CastObstacleRay(NextWptPos, Dist, Vector(), Free, self.ID, self.IgnoresWhichTeam, rte.grassID, 3)
							if pixels == 0 then
								break	-- the waypoint is inside terrain, plot a new path
							elseif pixels > 0 then
								CornerPos = (NextWptPos + Free) / 2	-- compensate for obstacles
							end
							
							coroutine.yield()	-- wait until next frame
							
							-- check if we have LOS
							Dist = SceneMan:ShortestDistance(self.Pos, CornerPos, false)
							if 0 <= SceneMan:CastObstacleRay(self.Pos, Dist, Vector(), Vector(), self.ID, self.IgnoresWhichTeam, rte.grassID, 4) then
								-- CornerPos is blocked
								CornerPos.X = self.Pos.X	-- move CornerPos straight above us
								cornerType = "air"
							end
							
							coroutine.yield()	-- wait until next frame
							
							Waypoint = {Pos=CornerPos, Type=cornerType}
							if #WptList > 1 and not WptList[1].Type then	-- remove the waypoint after the corner if possible
								table.remove(WptList, 1)
								self:RemoveMovePathBeginning() -- clean up the graphical representation of the path
							end
							
							self:AddToMovePathBeginning(Waypoint.Pos)
						else
							Waypoint = table.remove(WptList, 1)							
							if Waypoint.Type ~= "air" then
								local Free = Vector()
								
								-- only if we have a digging tool
								if Waypoint.Type ~= "drop" and self:EquipDiggingTool(false) then
									local PathSegRay = SceneMan:ShortestDistance(PrevWptPos, Waypoint.Pos, false)	-- detect material blocking the path and start digging through it
									if self.teamBlockState ~= Actor.BLOCKED and SceneMan:CastStrengthRay(PrevWptPos, PathSegRay, 5, Free, 1, rte.doorID, true) then
										if SceneMan:ShortestDistance(self.Pos, Free, false).Magnitude < self.Height*0.5 then	-- check that we're close enough to start digging
											-- TODO: update the path and try again just to make sure we can't get through some where else
											digState = AHuman.STARTDIG
											self.deviceState = AHuman.DIGGING
											obstacleState = Actor.DIGPAUSING
											nextLatMove = Actor.LAT_STILL
											sweepRange = math.pi / 4
											StuckTimer:SetSimTimeLimitMS(5000)
											self.Ctrl.AnalogAim = SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false).Normalized	-- aim in the direction of the next waypoint
										else
											digState = AHuman.NOTDIGGING
											obstacleState = Actor.PROCEEDING
										end
										
										coroutine.yield()	-- wait until next frame
									else
										digState = AHuman.NOTDIGGING
										obstacleState = Actor.PROCEEDING
										StuckTimer:SetSimTimeLimitMS(1500)
									end
								end
								
								if digState == AHuman.NOTDIGGING and self.deviceState ~= AHuman.DIGGING then
									-- if our path isn't blocked enough to dig, but the headroom is too little, start crawling to get through
									local Heading = SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false):SetMagnitude(self.Height*0.5)
									
									-- don't crawl if it's too steep, climb then instead
									if math.abs(Heading.X) > math.abs(Heading.Y) and self.Head and self.Head:IsAttached() then
										local TopHeadPos = self.Head.Pos - Vector(0, self.Head.Radius*0.7)
										
										-- first check up to the top of the head, and then from there forward
										if SceneMan:CastStrengthRay(self.Pos, TopHeadPos - self.Pos, 5, Free, 4, rte.doorID, true) or
												SceneMan:CastStrengthRay(TopHeadPos, Heading, 5, Free, 4, rte.doorID, true)
										then
											self.crawling = true
										else
											self.crawling = false
										end
										
										coroutine.yield()	-- wait until next frame
									else
										self.crawling = false
									end
								end
							end
						end
						
						if not Waypoint.Type then
							ArrivedTimer:SetSimTimeLimitMS(100)
						elseif Waypoint.Type == "last" then
							ArrivedTimer:SetSimTimeLimitMS(500)
						else	-- air or corner wpt
							ArrivedTimer:SetSimTimeLimitMS(25)
						end
					end
				elseif #WptList > 1 then	-- check if some other waypoint is closer
					local test = math.random(1, 15)
					local RandomWpt = WptList[test]
					if RandomWpt then
						Dist = SceneMan:ShortestDistance(self.Pos, RandomWpt.Pos, false)
						local mag = Dist.Magnitude
						if mag < 50 and mag < SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false).Magnitude/3 then
							-- this waypoint is closer, check LOS
							if -1 == SceneMan:CastObstacleRay(self.Pos, Dist, Vector(), Vector(), self.ID, self.IgnoresWhichTeam, rte.grassID, 4) then
								Waypoint = RandomWpt	-- go here instead
								if WptList[test-1] then
									PrevWptPos = WptList[test-1].Pos
								else
									PrevWptPos = self.Pos
								end
								
								test = math.min(test, #WptList)
								for i = 1, test do	-- delete the earlier waypoints
									table.remove(WptList, 1)
									if #WptList > 0 then
										self:RemoveMovePathBeginning()
									end
								end
							end
						end
					end
				end
				
				if Waypoint then
					CurrDist = SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false)
					
					if self.Target then	-- don`t move away from a target
						while true do
							local TargetDist = SceneMan:ShortestDistance(self.Pos, self.Target.Pos, false)
							if (TargetDist.X > 0 and CurrDist.X > 0) or (TargetDist.X < 0 and CurrDist.X < 0) then
								break	-- ok to move towards it
							else
								self.jump = false
								coroutine.yield()	-- wait until next frame
								
								if not self.Target then
									break	-- target lost
								end
							end
						end
					end
					
					-- digging
					if digState ~= AHuman.NOTDIGGING then
						if not self.Target and self:EquipDiggingTool(true) then	-- switch to the digger if we have one
							if self.FirearmIsEmpty then	-- reload if it's empty
								self.fire = false
								self.Ctrl:SetState(Controller.WEAPON_RELOAD, true)
							else
								if self.teamBlockState == Actor.BLOCKED then
									self.fire = false
									nextLatMove = Actor.LAT_STILL
								else
									if obstacleState == Actor.PROCEEDING then
										if CurrDist.X < -1 then
											nextLatMove = Actor.LAT_LEFT
										elseif CurrDist.X > 1 then
											nextLatMove = Actor.LAT_RIGHT
										end
									else
										nextLatMove = Actor.LAT_STILL
									end
									
									-- check if we are close enough to dig
									if SceneMan:ShortestDistance(PrevWptPos, self.Pos, false).Magnitude > self.Height*0.5 and
										 SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false).Magnitude > self.Height*0.5
									then
										digState = AHuman.NOTDIGGING
										obstacleState = Actor.PROCEEDING
										self.deviceState = AHuman.STILL
										self.fire = false
										self:EquipFirearm(true)
									else
										local aimAngle = self:GetAimAngle(true)
										local AimVec = Vector(1, 0):RadRotate(aimAngle)
										
										-- see if we have dug out all that we can in the sweep area without moving closer
										local centerAngle = CurrDist.AbsRadAngle
										local Ray = Vector(self.Height*0.3, 0):RadRotate(centerAngle)	-- center
										if SceneMan:CastNotMaterialRay(self.Pos, Ray, 0, 3, false) < 0 then
											coroutine.yield()	-- wait until next frame
											
											-- now check the tunnel's thickness
											Ray = Vector(self.Height*0.3, 0):RadRotate(centerAngle + sweepRange)	-- up
											if SceneMan:CastNotMaterialRay(self.Pos, Ray, rte.airID, 3, false) < 0 then
												coroutine.yield()	-- wait until next frame
												
												Ray = Vector(self.Height*0.3, 0):RadRotate(centerAngle - sweepRange)	-- down
												if SceneMan:CastNotMaterialRay(self.Pos, Ray, rte.airID, 3, false) < 0 then
													obstacleState = Actor.PROCEEDING	-- ok the tunnel section is clear, so start walking forward while still digging
												else
													obstacleState = Actor.DIGPAUSING	-- tunnel cavity not clear yet, so stay put and dig some more
												end
											end
										else
											obstacleState = Actor.DIGPAUSING	-- tunnel cavity not clear yet, so stay put and dig some more
										end
										
										coroutine.yield()	-- wait until next frame
										
										local angDiff = AngleBetweenVectors(CurrDist, AimVec)
										if math.abs(angDiff) < sweepRange then
											self.fire = true	-- only fire the digger at the obstacle
										else
											self.fire = false
										end
										
										-- sweep the digger between the two endpoints of the obstacle
										local DigTarget
										if sweepCW then
											DigTarget = Vector(self.Height*0.4, 0):RadRotate(centerAngle + sweepRange)
										else
											DigTarget = Vector(self.Height*0.4, 0):RadRotate(centerAngle - sweepRange)
										end
										
										angDiff = AngleBetweenVectors(DigTarget, AimVec)
										if math.abs(angDiff) < 0.1 then
											sweepCW = not sweepCW	-- this is close enough, go in the other direction next frame
										else
											self.Ctrl.AnalogAim = Vector(AimVec.X, AimVec.Y):RadRotate(-angDiff*0.1)
										end
										
										-- check if we are done when we get close enough to the waypoint
										if SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false).Magnitude < self.Height*0.25 then
											if not SceneMan:CastStrengthRay(PrevWptPos, SceneMan:ShortestDistance(PrevWptPos, Waypoint.Pos, false), 5, Vector(), 1, rte.doorID, true) and
												 not SceneMan:CastStrengthRay(self.EyePos, SceneMan:ShortestDistance(self.EyePos, Waypoint.Pos, false), 5, Vector(), 1, rte.doorID, true)
											then
												-- advance to the next waypoint, if there are any
												if #WptList > 0 then
													UpdatePathTimer:Reset()
													PrevWptPos = Waypoint.Pos
													Waypoint = table.remove(WptList, 1)
													if #WptList > 0 then
														self:RemoveMovePathBeginning()
													end
												end
											end
											
											coroutine.yield()	-- wait until next frame
										end
									end
								end
							end
						else
							digState = AHuman.NOTDIGGING
							obstacleState = Actor.PROCEEDING
							self.deviceState = AHuman.STILL
							self.fire = false
							self:EquipFirearm(true)
						end
					else	-- not digging
						if not self.Target then
							self.fire = false
						end
						
						-- Scan for obstacles
						local Trace = Vector(self.Diameter*0.85, 0):RadRotate(scanAng)
						local Free = Vector()
						local index = math.floor(scanAng*2.5+2.01)
						if SceneMan:CastObstacleRay(self.Pos, Trace, Vector(), Free, self.ID, self.IgnoresWhichTeam, rte.grassID, 3) > -1 then
							Obstacles[index] = true
						else
							Obstacles[index] = false
						end
						
						if scanAng < 1.57 then	-- pi/2
							if scanAng > 1.2 then
								scanAng = 1.89
							else
								scanAng = scanAng + 0.55
							end
						else
							if scanAng > 3.5 then
								scanAng = -0.4
							else
								scanAng = scanAng + 0.55
							end
						end
						
						if not self.jump and not self.flying then
							coroutine.yield()	-- wait until next frame
						end
						
						if CurrDist.Magnitude > self.Height/3 then	-- not close enough to the waypoint
							ArrivedTimer:Reset()
							
							-- check if we have LOS to the waypoint
							if SceneMan:CastObstacleRay(self.Pos, CurrDist, Vector(), Vector(), self.ID, self.IgnoresWhichTeam, rte.grassID, 9) < 0 then
								NoLOSTimer:Reset()
							elseif NoLOSTimer:IsPastSimTimeLimit() then	-- calulate new path
								Waypoint = nil
								WptList = nil
							end							
							
							if not self.jump and not self.flying then
								coroutine.yield()	-- wait until next frame
							end
						elseif ArrivedTimer:IsPastSimTimeLimit() then	-- only remove a waypoint if we have been close to it for a while
							PrevWptPos = Waypoint.Pos
							self:RemoveMovePathBeginning()
							Waypoint = nil
						elseif self.refuel and self.Vel.Y < 5 then
							if self.JetTimeLeft > self.JetTimeTotal * 0.95 or
								(self.flying and self.Vel.Y < -3 and self.JetTimeLeft > 0.5)
							then
								self.refuel = false
							else
								self.jump = false	-- wait until the jetpack is full
								nextLatMove = Actor.LAT_STILL
							end
						end
						
						if Waypoint and not self.refuel then	-- move towards the waypoint
							nextAimAngle = self:GetAimAngle(false) * 0.95	-- look straight ahead
							
							-- control vertical movement
							local change
							if self.flying then
								change = self.YposPID:Update(CurrDist.Y+self.Vel.Y*3, 0)
							else
								change = self.YposPID:Update(CurrDist.Y, 0) -- ignore our velocity if on the ground
							end
							
							if math.abs(self.RotAngle) < 0.5 and self.Vel.Y > -10 and
								(change < -1 or self.Vel.Y > 10)
							then
								if not self.jump then
									self.jump = true
									BurstTimer:Reset()	-- this burst last until the BurstTimer expire
								end
								
								if change < -6 then
									BurstTimer:SetSimTimeLimitMS(250)
								elseif change < -3 then
									BurstTimer:SetSimTimeLimitMS(500)
								else
									BurstTimer:SetSimTimeLimitMS(1000)
								end
							else
								self.jump = false
							end
							
							-- control horisontal movement
							if self.jump then
								if self.flying then
									change = self.XposPID:Update(CurrDist.X+self.Vel.X*3, 0)
								else
									change = self.XposPID:Update(CurrDist.X, 0) -- ignore our velocity if on the ground
								end
								
								if change < -0.5 then
									nextLatMove = Actor.LAT_LEFT
								elseif change > 0.5 then
									nextLatMove = Actor.LAT_RIGHT
								else
									nextAimAngle = nextAimAngle - (nextAimAngle-1.2)*0.2	-- look up to aim jetpack down
								end
							elseif self.FGLeg or self.BGLeg then
								if CurrDist.X < -3 then
									nextLatMove = Actor.LAT_LEFT
								elseif CurrDist.X > 3 then
									nextLatMove = Actor.LAT_RIGHT
								else
									nextLatMove = Actor.LAT_STILL
								end
							elseif (CurrDist.X < -5 and self.HFlipped) or (CurrDist.X > 5 and not self.HFlipped) then
								-- no legs, jump forward
								BurstTimer:Reset()
								self.jump = true
							end
							
							if Waypoint.Type == "right" then
								if CurrDist.X > -3 then
									nextLatMove = Actor.LAT_RIGHT
								end
							elseif Waypoint.Type == "left" then
								if CurrDist.X < 3 then
									nextLatMove = Actor.LAT_LEFT
								end								
							end
							
							-- must fire thrusters to move sideways when in the air
							if not self.jump and self.flying and Waypoint.Type ~= "drop" and
								 self.Vel.Y > -5 and CurrDist.Y-self.Vel.Y*3 < 20	--TODO: use Ychange here?
							then
								change = math.abs(self.XposPID:Update(CurrDist.X+self.Vel.X*3, 0))
								if change > 0.5 then
									self.jump = true
								end
							end
							
							if self.jump then
								-- obstacle right
								for i = rte.Obst.R_FRONT, rte.Obst.R_UP do
									if Obstacles[i] then
										if Obstacles[rte.Obst.R_UP] then
											self.jump = false
										else
											nextAimAngle = nextAimAngle - (nextAimAngle-1.2)*0.2	-- look up to aim jetpack down
										end
										
										break
									end
								end
								
								-- obstacle left
								for i = rte.Obst.L_UP, rte.Obst.L_FRONT do
									if Obstacles[i] then
										if Obstacles[rte.Obst.L_UP] then
											self.jump = false
										else
											nextAimAngle = nextAimAngle - (nextAimAngle-1.2)*0.2	-- look up to aim jetpack down
										end
										
										break
									end
								end
							elseif Waypoint.Type ~= "drop" and not Lower(Waypoint, self, 20) then						
								-- jump over low obstacles unless we want to jump off a ledge
								if nextLatMove == Actor.LAT_RIGHT and
									(Obstacles[rte.Obst.R_LOW] or Obstacles[rte.Obst.R_FRONT]) and not Obstacles[rte.Obst.R_UP]
								then
									self.jump = true
									if Obstacles[rte.Obst.R_HIGH] then
										nextLatMove = Actor.LAT_LEFT -- TODO: only when too close to the obstacle?
									end
								elseif nextLatMove == Actor.LAT_LEFT and
									(Obstacles[rte.Obst.L_LOW] or Obstacles[rte.Obst.L_FRONT]) and not Obstacles[rte.Obst.L_UP]
								then
									self.jump = true
									if Obstacles[rte.Obst.L_HIGH] then
										nextLatMove = Actor.LAT_RIGHT -- TODO: only when too close to the obstacle?
									end
								end
							end
							
							if math.abs(self.RotAngle) > 0.5 then
								self.jump = false
							end
						end
						
						if not self.Target then
							self:SetAimAngle(nextAimAngle)
						end
					end
				end
			end
		else	-- no waypoint list, create one in several small steps to reduce lag
			self:DrawWaypoints(false)
			self:UpdateMovePath()
			coroutine.yield()	-- wait until next frame
			
			if self.MovePathSize < 1 then
				break
			end
			
			-- copy the MovePath to a temporary table so we can yield safely while working on the path
			local PathDump = {}
			for WptPos in self.MovePath do
				table.insert(PathDump, WptPos)
			end
			
			coroutine.yield()	-- wait until next frame
			
			-- copy useful waypoints to a temporary path
			local TmpWpts = {}
			table.insert(TmpWpts, {Pos=self.Pos})
			local Origin
			local LastPos = PathDump[1]
			for _, WptPos in pairs(PathDump) do
				Origin = TmpWpts[#TmpWpts].Pos
				WptPos = SceneMan:MovePointToGround(WptPos, self.Height/4, 4)
				if SceneMan:ShortestDistance(Origin, WptPos, false).Magnitude > 100 or	-- skip any waypoint too close to the previous one
					SceneMan:CastStrengthSumRay(Origin, WptPos, 3, rte.grassID) > 5
				then
					table.insert(TmpWpts, {Pos=LastPos})
				end
				
				LastPos = WptPos
				coroutine.yield()	-- wait until next frame
			end
			
			table.insert(TmpWpts, {Pos=PathDump[#PathDump]})	-- add the last waypoint in the MovePath
			
			coroutine.yield()	-- wait until next frame
			
			WptList = {}	-- store the waypoints we want in our path here
			local StartWpt = table.remove(TmpWpts, 1)
			while #TmpWpts > 0 do
				local NextWpt = table.remove(TmpWpts, 1)
				
				if Lower(NextWpt, StartWpt, 30) then	-- scan for sharp drops	TODO: check the slope instead?
					NextWpt.Type = "drop"
					
					local GapList = {}
					for j, JumpWpt in pairs(TmpWpts) do	-- look for the other side
						local Gap = SceneMan:ShortestDistance(StartWpt.Pos, JumpWpt.Pos, false)
						if Gap.Magnitude > 400 - Gap.Y then	-- TODO: use actor properties here
							break	-- too far
						end
						
						if Gap.Y > -40 then	-- no more than 2m above
							table.insert(GapList, {Wpt=JumpWpt, score=math.abs(Gap.X/Gap.Y), index=j})
						end
					end
					
					coroutine.yield()	-- wait until next frame
					
					table.sort(GapList, function(A, B) return A.score > B.score end)	-- sort largest first
					
					for _, LZ in pairs(GapList) do
						-- check if we can jump
						local Trace = SceneMan:ShortestDistance(StartWpt.Pos, LZ.Wpt.Pos, false)
						if -1 == SceneMan:CastObstacleRay(StartWpt.Pos, Trace, Vector(), Vector(), self.ID, self.IgnoresWhichTeam, rte.grassID, 4) then							
							-- find a point mid-air
							local TestPos = StartWpt.Pos + Trace / 2
							local Free = Vector()
							if 0 ~= SceneMan:CastObstacleRay(TestPos, Vector(0, -math.abs(Trace.X)/2), Vector(), Free, self.ID, self.IgnoresWhichTeam, rte.grassID, 2) then	-- TODO: check LOS? what if 0?
								table.insert(WptList, {Pos=Free+Vector(0,self.Height/4), Type="air"})	-- guide point in the air
								NextWpt = LZ.Wpt
								
								-- delete any waypoints between StartWpt and the LZ
								for i = LZ.index, 1, -1 do
									table.remove(TmpWpts, i)
								end
								
								break
							end
						end
						
						coroutine.yield()	-- wait until next frame
					end
				end
				
				table.insert(WptList, NextWpt)
				StartWpt = NextWpt
			end
			
			WptList[#WptList].Type = "last"
			
			coroutine.yield()	-- wait until next frame
			
			-- create the move path seen on the screen
			self:ClearMovePath()
			for _, Wpt in pairs(WptList) do
				self:AddToMovePathEnd(Wpt.Pos)
			end
			
			self:DrawWaypoints(true)
			NoLOSTimer:Reset()
		end
		
		-- movement commands
		self.lateralMoveState = nextLatMove
		if self.jump and BurstTimer:IsPastSimTimeLimit() then	-- trigger jetpack bursts
			BurstTimer:Reset()
			self.jump = false
		end
		
		if self.BlockingActor then
			if not MovableMan:IsActor(self.BlockingActor) or
				SceneMan:ShortestDistance(self.Pos, self.BlockingActor.Pos, false).Magnitude > (self.Diameter + self.BlockingActor.Diameter)*1.2
			then
				self.BlockingActor = nil
				self.teamBlockState = Actor.NOTBLOCKED
				
				if self.AIMode == Actor.AIMODE_BRAINHUNT and self.FollowingActor then
					self.FollowingActor = nil
					break	-- end this behavior
				end
			elseif self.teamBlockState == Actor.NOTBLOCKED and Waypoint then
				if (Waypoint.Pos.X > self.Pos.X and self.BlockingActor.Pos.X > self.Pos.X) or
					 (Waypoint.Pos.X < self.Pos.X and self.BlockingActor.Pos.X < self.Pos.X)
				then
					self.teamBlockState = Actor.BLOCKED
					if self.AIMode == Actor.AIMODE_BRAINHUNT and self.AIMode == self.BlockingActor.AIMode and
						(not self.BlockingActor.MOMoveTarget or self.BlockingActor.MOMoveTarget.ID ~= self.ID)
					then	-- don't follow an actor that is following us
						self.FollowingActor = self.BlockingActor
						self:ClearAIWaypoints()
						self:AddAIMOWaypoint(self.FollowingActor)
						self:CreateGoToBehavior()
					end
				else
					self.BlockingActor = nil
				end
			end
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	return true
end

-- go prone if we can shoot from the prone position and return the result
function HumanBehaviors.GoProne(self, AimPoint, targetID)
	if not self.Head then
		return false
	end
	
	local ground = self.Pos.Y + self.Height/5
	local Dist = SceneMan:ShortestDistance(self.Pos, AimPoint, false)
	local PronePos
	
	-- check if there we can lay down here
	if Dist.X > 100 then
		-- to the right
		PronePos = self.EyePos + Vector(self.Height*0.3, 0)
		
		local x_pos = self.Pos.X + 10
		for x = 1, math.floor(self.Height/14+0.5) do
			x_pos = x_pos + 7
			if SceneMan.SceneWrapsX and x_pos > SceneMan.SceneWidth then
				x_pos = SceneMan.SceneWidth - x_pos
			end
			
			if 0 == SceneMan:GetTerrMatter(x_pos, ground) then
				return false
			end
		end
	elseif Dist.X < -100 then
		-- to the left
		PronePos = self.EyePos + Vector(-self.Height*0.3, 0)
		
		local x_pos = self.Pos.X - 10
		for x = 1, math.floor(self.Height/14+0.5) do
			x_pos = x_pos - 7
			if SceneMan.SceneWrapsX and x_pos < 0 then
				x_pos = x_pos + SceneMan.SceneWidth
			end
			
			if 0 == SceneMan:GetTerrMatter(x_pos, ground) then
				return false
			end
		end
	else
		return false	-- target is too close
	end
	
	PronePos = SceneMan:MovePointToGround(PronePos, self.Head.Radius+3, 2)
	Dist = SceneMan:ShortestDistance(PronePos, AimPoint, false)
	
	-- check LOS from the prone position
	if not SceneMan:CastFindMORay(PronePos, Dist, targetID, Vector(), rte.grassID, false, 8) then
		return false
	else
		-- check for obstacles more more carefully
		Dist:CapMagnitude(60)
		if SceneMan:CastObstacleRay(PronePos, Dist, Vector(), Vector(), self.ID, self.IgnoresWhichTeam, rte.grassID, 1) > -1 then
			return false
		end
	end
	
	self.proneState = AHuman.PRONE
	if Dist.X > 0 then
		self.lateralMoveState = Actor.LAT_RIGHT
	else
		self.lateralMoveState = Actor.LAT_LEFT
	end
	
	return true
end

-- get the projectile properties from the magazine
-- in the future each magazine should have its own AI-script that explais to the AI how to use the item properly
function HumanBehaviors.GetProjectileData(self)
	local Weapon = ToHDFirearm(self.EquippedItem)
	local Round = Weapon.Magazine.NextRound
	local Projectile = Round.NextParticle
	local PrjDat = {MagazineName=Weapon.Magazine.PresetName}
	
	if Round.IsEmpty then	-- set default values if there is no particle
		PrjDat.g = 0
		PrjDat.vel = 100
		PrjDat.rng = math.huge
	else
		if Projectile.ClassName == "AEmitter" or Projectile.ClassName == "TDExplosive" then
			PrjDat.exp = true	-- assume that all Emitters and TDExplosives have a blast radius for now
			PrjDat.vel = math.max(Round.FireVel, Projectile.Sharpness)	-- use Sharpness instead of FireVel for exlosive projectiles
		else
			PrjDat.vel = Round.FireVel	-- muzzle velocity
			if Weapon.FullAuto and Projectile.ClassName == "MOPixel" then
				-- the theoretical upper limit for the total amount of material strength this weapon can destroy in half a second
				PrjDat.pen = Projectile.Mass * Projectile.Sharpness * PrjDat.vel * math.max((Weapon.RateOfFire / 120), 1)
			end
		end
		
		PrjDat.g = SceneMan.GlobalAcc.Y * 0.67 * Projectile.GlobalAccScalar	-- underestimate gravity
		PrjDat.vsq = PrjDat.vel^2	-- muzzle velocity squared
		PrjDat.vqu = PrjDat.vsq^2	-- muzzle velocity quadrat
		PrjDat.drg = 1 - Projectile.AirResistance * TimerMan.DeltaTimeSecs	-- AirResistance is stored as the ini-value times 60
		PrjDat.thr = math.min(Projectile.AirThreshold, PrjDat.vel)
		
		-- estimate theoretical max range with ...
		local lifeTime = Projectile.Lifetime
		if lifeTime < 1 then	-- infinite life time
			PrjDat.rng = math.huge
		elseif PrjDat.drg < 1 then	-- AirResistance
			PrjDat.rng = 0
			local threshold = PrjDat.thr * FrameMan.PPM * TimerMan.DeltaTimeSecs	-- AirThreshold in pixels/frame
			local vel = PrjDat.vel * FrameMan.PPM * TimerMan.DeltaTimeSecs	-- muzzle velocity in pixels/frame
			for t = 0, math.ceil(lifeTime/TimerMan.DeltaTimeMS) do
				PrjDat.rng = PrjDat.rng + vel
				if vel > threshold then
					vel = vel * PrjDat.drg
				end
			end
		else	-- no AirResistance
			PrjDat.rng = PrjDat.vel * FrameMan.PPM * TimerMan.DeltaTimeSecs * (lifeTime / TimerMan.DeltaTimeMS)
		end
		
		-- uncomment these to get the range of the weapon
		--print(Weapon.PresetName .. " range = " .. PrjDat.rng .. " px")
		--print(self.Target.PresetName .. " range = " .. SceneMan:ShortestDistance(self.Pos, self.Target.Pos, false).Magnitude .. " px")
	end
	
	return PrjDat
end

-- open fire on the selected target
function HumanBehaviors.ShootTarget(self)	
	if not MovableMan:IsActor(self.Target) then
		return true
	end
	
	self.TargetLostTimer:SetSimTimeLimitMS(1000)
	
	local LOSTimer = Timer()
	LOSTimer:SetSimTimeLimitMS(450)	
	
	local ShootTimer = Timer()
	local AimPoint = self.Target.Pos + self.TargetOffset
	if not self.flying and HumanBehaviors.GoProne(self, AimPoint, self.Target.ID) then
		ShootTimer:SetRealTimeLimitMS(RangeRand(800, 1400))
	else
		ShootTimer:SetRealTimeLimitMS(RangeRand(600, 900))
	end
	
	local PrjDat, OldWaypoint, canHit
	local openFire = 0
	local checkAim = true
	local canSwitchWeapon = self.InventorySize
	local TargetAvgVel = Vector(self.Target.Vel.X, self.Target.Vel.Y)
	local aimTarget = SceneMan:ShortestDistance(self.Pos, AimPoint, false).AbsRadAngle + RangeRand(-0.33, 0.33)
	
	coroutine.yield()	-- wait until next frame
	
	while true do
		if not self.Target or self.Target:IsDead() then
			if OldWaypoint then
				self:ClearAIWaypoints()
				self:AddAISceneWaypoint(OldWaypoint)
				self:CreateGoToBehavior()
			end
			
			break
		end
		
		if self.FirearmIsReady then
			-- it is now safe to get the ammo stats since FirearmIsReady
			local Weapon = ToHDFirearm(self.EquippedItem)
			if not PrjDat or PrjDat.MagazineName ~= Weapon.Magazine.PresetName then
				PrjDat = HumanBehaviors.GetProjectileData(self)
			else
				AimPoint = self.Target.Pos + self.TargetOffset
				local Dist = SceneMan:ShortestDistance(Weapon.Pos, AimPoint, false)
				local range = Dist.Magnitude
				TargetAvgVel = TargetAvgVel * 0.6 + self.Target.Vel * 0.4	-- smooth the target's velocity
				
				-- move the aimpoint towards the center of the target at close ranges
				if range < 40 then
					self.TargetOffset = self.TargetOffset * 0.95
				end
				
				if checkAim then
					checkAim = false	-- only check every second frame
					
					if range < 100 then	-- within 5m
						-- it is not safe to fire an explosive projectile at this distance
						aimTarget = Dist.AbsRadAngle
						canHit = true
						if PrjDat.exp and self.InventorySize > 0 then	-- we have more things in the inventory
							if self:EquipDiggingTool(false) then
								self:CreateHtHBehavior()
								break
							elseif canSwitchWeapon > 0 and self:HasObjectInGroup("Primary Weapons") or self:HasObjectInGroup("Secondary Weapons") then
								canSwitchWeapon = canSwitchWeapon - 1
								self.Ctrl:SetState(Controller.WEAPON_CHANGE_NEXT, true)
								PrjDat = nil	-- get the ammo info from the new weapon the next update
								canHit = false
							end
						end
					elseif range < PrjDat.rng then
						-- lead the target if target speed and projectile TTT is above the threshold
						local timeToTarget = range / PrjDat.vel
						if timeToTarget * self.Target.Vel.Magnitude > 2 then
							timeToTarget = timeToTarget * RangeRand(1.6, 2.2)	-- ~double this value since we only do this every second update
							Dist = SceneMan:ShortestDistance(Weapon.Pos, AimPoint+self.Target.Vel*timeToTarget, false)
						end
						
						aimTarget = HumanBehaviors.GetAimAngle(self, PrjDat, Dist)
						if aimTarget then
							canHit = true
						else
							canHit = false
							
							-- the target is too far away; switch weapon, move closer or run away
							if canSwitchWeapon > 0 and (self:HasObjectInGroup("Primary Weapons") or self:HasObjectInGroup("Secondary Weapons")) then
								canSwitchWeapon = canSwitchWeapon - 1
								self.Ctrl:SetState(Controller.WEAPON_CHANGE_NEXT, true)
								PrjDat = nil	-- get the ammo info from the new weapon the next update
							elseif not self.isPlayerOwned or self.AIMode ~= Actor.AIMODE_SENTRY then
								if not self.MOMoveTarget or 
									not MovableMan:ValidMO(self.MOMoveTarget) or
									self.MOMoveTarget.RootID ~= self.Target.RootID
								then	-- move towards the target
									OldWaypoint = self:GetLastAIWaypoint()
									if (OldWaypoint-self.Pos).Largest < 1 then
										OldWaypoint = Vector(self.Pos.X, self.Pos.Y)	-- move back here later
									end
									
									self:ClearAIWaypoints()
									self:AddAIMOWaypoint(self.Target)
									self:CreateGoToBehavior()
									self.proneState = AHuman.NOTPRONE
								end
							else
								-- TODO: run away?
								break
							end
						end
					elseif not self.isPlayerOwned or self.AIMode ~= Actor.AIMODE_SENTRY then	-- target out of reach; move towards it
						-- check if we are already moving towards an actor
						if not self.MOMoveTarget or 
							not MovableMan:ValidMO(self.MOMoveTarget) or
							self.MOMoveTarget.RootID ~= self.Target.RootID
						then	-- move towards the target
							OldWaypoint = self:GetLastAIWaypoint()
							if (OldWaypoint-self.Pos).Largest < 1 then
								OldWaypoint = Vector(self.Pos.X, self.Pos.Y)	-- move back here later
							end
							
							self:ClearAIWaypoints()
							self:AddAIMOWaypoint(self.Target)
							self:CreateGoToBehavior()
							self.proneState = AHuman.NOTPRONE
							canHit = false
						end
					end
				else
					checkAim = true
					if LOSTimer:IsPastSimTimeLimit() then
						LOSTimer:Reset()
						
						if range < 30 + Weapon.SharpLength + FrameMan.PlayerScreenWidth*0.5 and
							not SceneMan:IsUnseen(AimPoint.X, AimPoint.Y, self.Team)
						then
							if PrjDat.pen then
								if SceneMan:CastStrengthSumRay(Weapon.MuzzlePos, AimPoint, 7, rte.grassID) < PrjDat.pen / 7 then
									self.TargetLostTimer:Reset()	-- we can shoot the target
								end
							else
								-- periodically check that we can see the target
								local ID = SceneMan:CastMORay(Weapon.MuzzlePos, Dist, self.ID, self.IgnoresWhichTeam, rte.grassID, false, 9)
								if ID < rte.NoMOID and (ID == self.Target.ID or (MovableMan:GetMOFromID(ID)).RootID == self.Target.ID) then
									self.TargetLostTimer:Reset()	-- we can see the target
								end
							end
						end
					end
				end
				
				if canHit then
					self.lateralMoveState = Actor.LAT_STILL
					if not self.flying then
						self.deviceState = AHuman.AIMING
					end
				end
				
				local aim = self:GetAimAngle(true)			
				if self.flying then
					aimTarget = (aimTarget or aim) + RangeRand(-0.15, 0.15)
				else
					aimTarget = (aimTarget or aim) + math.min(math.max(RangeRand(-20, 20)/(range+60), -0.33), 0.33)
				end
				
				local angDiff = aim - aimTarget
				if angDiff > math.pi then
					angDiff = angDiff - math.pi*2
				elseif angDiff < -math.pi then
					angDiff = angDiff + math.pi*2
				end
				
				if PrjDat and ShootTimer:IsPastRealTimeLimit() then
					self.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim-angDiff*0.08)
					
					if canHit then 
						if Weapon.FullAuto then	-- open fire if our aim overlap the target
							if math.abs(angDiff) < math.tanh((1.7*self.Target.Diameter)/(range+0.1)) then
								openFire = 5	-- don't stop shooting just because we lose the target for a few frames
							else
								openFire = openFire - 1
							end
						elseif not self.fire then	-- open fire if our aim overlap the target
							if math.abs(angDiff) < math.tanh((1.1*self.Target.Diameter)/(range+0.1)) then
								openFire = 1
							else
								openFire = 0
							end
						else
							openFire = openFire - 1	-- release the trigger if semi auto
						end
						
						-- check for obstacles if the ammo have a blast radius
						if openFire > 0 and PrjDat.exp then
							if SceneMan:CastObstacleRay(Weapon.MuzzlePos, Weapon:RotateOffset(Vector(40, 0)), Vector(), Vector(), self.ID, self.IgnoresWhichTeam, rte.grassID, 2) > -1 then
								openFire = 0
							end
						end
					else
						openFire = openFire - 1
					end
				else
					self.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim-angDiff*0.05 + RangeRand(-0.04, 0.04))
					openFire = 0
				end
				
				if Weapon.FullAuto then
					if openFire > 0 then
						self.fire = true
					else
						self.fire = false
					end
				else
					if openFire > 0 then
						self.fire = true
					else
						self.fire = false
					end
				end
			end
		else
			if self.EquippedItem and ToHeldDevice(self.EquippedItem):IsReloading() then
				ShootTimer:Reset()
				self.Ctrl.AnalogAim = SceneMan:ShortestDistance(self.Pos, self.Target.Pos, false)
			elseif self:EquipFirearm(true) then
				ShootTimer:SetRealTimeLimitMS(RangeRand(500, 700))
				self.deviceState = AHuman.POINTING
				self.fire = false
				
				if self.FirearmIsEmpty then
					self:ReloadFirearm()
					-- TODO: check if ducking is appropriate while reloading (when we can make the actor stand up reliably)
				end
			else
				break -- no firearm avaliable
			end
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	return true
end

-- throw a grenade at the selected target
function HumanBehaviors.ThrowTarget(self)
	local ThrowTimer = Timer()
	local aimTime = RangeRand(1000, 1500)
	local scan = 0
	local miss = 0	-- stop scanning after a few missed atempts
	local AimPoint, Dist, MO, ID, rootID, aim, LOS
	
	self.TargetLostTimer:SetSimTimeLimitMS(1500)
	
	while true do
		if not MovableMan:IsActor(self.Target) then
			break
		end
		
		if scan < 1 then
			if self.Target.Door then
				AimPoint = self.Target.Door.Pos
			else
				AimPoint = self.Target.Pos	-- look for the center
				if self.Target.EyePos then
					AimPoint = (AimPoint + self.Target.EyePos) / 2
				end
			end
			
			ID = rte.NoMOID
			if self:IsWithinRange(Vector(AimPoint.X, AimPoint.Y)) then	-- TODO: use grenade properies to decide this
				Dist = SceneMan:ShortestDistance(self.EyePos, AimPoint, false)
				ID = SceneMan:CastMORay(self.EyePos, Dist, self.ID, self.IgnoresWhichTeam, rte.grassID, false, 3)
				if ID < 1 or ID > 254 then	-- not found, look for any head or legs
					AimPoint = self.Target.EyePos	-- the head
					if AimPoint then
						coroutine.yield()	-- wait until next frame
						if not MovableMan:IsActor(self.Target) then	-- must verify that the target exist after a yield
							break
						end
						
						Dist = SceneMan:ShortestDistance(self.EyePos, AimPoint, false)
						ID = SceneMan:CastMORay(self.EyePos, Dist, self.ID, self.IgnoresWhichTeam, rte.grassID, false, 3)
					end
					
					if ID < 1 or ID > 254 then
						local Legs = self.Target.FGLeg or self.Target.BGLeg	-- the legs
						if Legs then
							coroutine.yield()	-- wait until next frame
							if not MovableMan:IsActor(self.Target) then	-- must verify that the target exist after a yield
								break
							end
							
							AimPoint = Legs.Pos
							Dist = SceneMan:ShortestDistance(self.EyePos, AimPoint, false)
							ID = SceneMan:CastMORay(self.EyePos, Dist, self.ID, self.IgnoresWhichTeam, rte.grassID, false, 3)
						end
					end
				end
			else
				break	-- out of range
			end
			
			if ID > 0 and ID < rte.NoMOID then	-- MO found
				scan = 6	-- skip the LOS check the next n frames
				miss = 0
				LOS = true	-- we have line of sight to the target
				
				-- check what target we will hit
				rootID = MovableMan:GetRootMOID(ID)
				if rootID ~= self.Target.ID then
					MO = MovableMan:GetMOFromID(rootID)
					if MovableMan:IsActor(MO) then
						if MO.Team ~= self.Team then
							if MO.ClassName == "AHuman" then
								self.Target = ToAHuman(MO)
							elseif MO.ClassName == "ACrab" then
								self.Target = ToACrab(MO)
							elseif MO.ClassName == "ACRocket" then
								self.Target = ToACRocket(MO)
							elseif MO.ClassName == "ACDropShip" then
								self.Target = ToACDropShip(MO)
							elseif MO.ClassName == "ADoor" then
								self.Target = ToADoor(MO)
							elseif MO.ClassName == "Actor" then
								self.Target = ToActor(MO)
							else
								break
							end
						else
							break	-- don't shoot friendlies
						end
					end
				end
			else
				miss = miss + 1
				if miss > 4 then	-- stop looking if we cannot find anything after n atempts
					break
				else
					scan = 3	-- check LOS a little bit more often if no MO was found
				end
			end
			
			if LOS then	-- don't sharp aim until LOS has been confirmed
				if self.ThrowableIsReady then
					aim = HumanBehaviors.GetGrenadeAngle(AimPoint, self.Target.Vel/RangeRand(0.5, 1.75), ToHeldDevice(self.EquippedItem).MuzzlePos, 18)
					if aim then
						self.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim+RangeRand(-0.1, 0.1))
					else
						break	-- target out of range
					end
					
					if not ThrowTimer:IsPastSimMS(aimTime) then
						self.fire = true
					else
						ThrowTimer:Reset()
						self.fire = false
						aimTime = RangeRand(1000, 1500)
					end
				else
					break	-- no grenades left
				end
			end
		else
			scan = scan - 1
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	return true
end

-- attack the target in hand-to-hand
function HumanBehaviors.AttackTarget(self)
	if not self.Target or not MovableMan:IsActor(self.Target) then
		return true
	end
	
	self.TargetLostTimer:SetSimTimeLimitMS(5000)
	
	-- move towards the target
	local OldWaypoint
	if not self.MOMoveTarget or 
		not MovableMan:ValidMO(self.MOMoveTarget) or
		self.MOMoveTarget.RootID ~= self.Target.RootID
	then
		OldWaypoint = self:GetLastAIWaypoint()
		if (OldWaypoint-self.Pos).Largest < 1 then
			OldWaypoint = nil
		end
		
		self:ClearAIWaypoints()
		self:AddAIMOWaypoint(self.Target)
		self:CreateGoToBehavior()
	end
	
	local HD
	while true do
		coroutine.yield()	-- wait until next frame
		
		if not self.Target or not MovableMan:IsActor(self.Target) then
			self:ClearAIWaypoints()
			if OldWaypoint then	-- move towards the old wpt
				self:AddAISceneWaypoint(OldWaypoint)
				self:CreateGoToBehavior()
			end

			break
		end
		
		if self.EquippedItem then
			HD = ToHeldDevice(self.EquippedItem)
			if HD:IsTool() then	-- attack with digger
				local Dist = SceneMan:ShortestDistance(HD.MuzzlePos, self.Target.Pos, false)
				if Dist.Magnitude < 40 then
					self.Ctrl.AnalogAim = SceneMan:ShortestDistance(self.EyePos, self.Target.Pos, false).Normalized
					self.fire = true
				else
					self.fire = false
				end
			else
				break
			end
		-- else TODO: periodically look for weapons?
		end
	end
	
	return true
end


-- move around another actor
function HumanBehaviors.MoveAroundActor(self)
	if not MovableMan:IsActor(self.BlockingActor) then
		self.teamBlockState = Actor.NOTBLOCKED
		self.BlockingActor = nil
		return true
	end
	
	local BurstTimer = Timer()
	local refuel = false
	local Dist
	
	BurstTimer:SetSimTimeLimitMS(200)	-- a burst last until the BurstTimer expire
	self.jump = true
	
	-- look above the blocking actor
	Dist = SceneMan:ShortestDistance(self.Pos, self.BlockingActor.Pos, false)
	if Dist.X > 0 then
		self.Ctrl.AnalogAim = Vector(1,0):RadRotate(1.20)
	else
		self.Ctrl.AnalogAim = Vector(1,0):RadRotate(1.94)
	end
	
	while true do		
		if BurstTimer:IsPastSimTimeLimit() then	-- trigger jetpack bursts
			BurstTimer:Reset()
			self.jump = false
			
			Dist = SceneMan:ShortestDistance(self.Pos, self.BlockingActor.Pos, false)
			if Dist.Y + self.Vel.Y * 3 > (self.Diameter + self.BlockingActor.Diameter)*0.67 then
				self:SetAimAngle(-0.5)
				
				if math.abs(Dist.X) > math.max(self.Diameter, self.BlockingActor.Diameter)/2 then
					return true
				end
			end
		else
			self.jump = true
			if self.Vel.Y < -9 then
				self.jump = false
			end
		end
		
		if refuel then
			self.jump = false
			if self.JetTimeLeft > self.JetTimeTotal * 0.9 then
				refuel = false
			end
		elseif self.JetTimeLeft < self.JetTimeTotal * 0.1 then
			refuel = true
		end
		
		coroutine.yield()	-- wait until next frame
		if not MovableMan:IsActor(self.BlockingActor) then
			self.teamBlockState = Actor.NOTBLOCKED
			self.BlockingActor = nil
			return true
		end
	end
	
	return true
end


function HumanBehaviors.GetAimAngle(self, PrjDat, Dist)
	if PrjDat.g == 0 then	-- this projectile is not affected by gravity 
		return Dist.AbsRadAngle
	else	-- compensate for gravity
		local rootSq, muzVelSq
		local D = Dist / FrameMan.PPM	-- convert from pixels to meters
		if PrjDat.drg < 1 then	-- compensate for air resistance
			local rng = D.Magnitude
			local timeToTarget = math.floor((rng / math.max(PrjDat.vel*PrjDat.drg^math.floor(rng/(PrjDat.vel+1)+0.5), PrjDat.thr)) / TimerMan.DeltaTimeSecs)	-- estimate time of flight in frames
			
			if timeToTarget > 1 then
				local muzVel = 0.9*math.max(PrjDat.vel * PrjDat.drg^timeToTarget, PrjDat.thr) + 0.1*PrjDat.vel	-- compensate for velocity reduction during flight
				muzVelSq = muzVel * muzVel
				rootSq = muzVelSq*muzVelSq - PrjDat.g * (PrjDat.g*D.X*D.X + 2*-D.Y*muzVelSq)
			else
				muzVelSq = PrjDat.vsq
				rootSq = PrjDat.vqu - PrjDat.g * (PrjDat.g*D.X*D.X + 2*-D.Y*muzVelSq)
			end
		else
			muzVelSq = PrjDat.vsq
			rootSq = PrjDat.vqu - PrjDat.g * (PrjDat.g*D.X*D.X + 2*-D.Y*muzVelSq)
		end
		
		if rootSq >= 0 then	-- no solution exists if rootSq is below zero
			local ang1 = math.atan2(muzVelSq - math.sqrt(rootSq), PrjDat.g*D.X)
			local ang2 = math.atan2(muzVelSq + math.sqrt(rootSq), PrjDat.g*D.X)
			if ang1 + ang2 > math.pi then	-- both angles in the second or third quadrant
				if ang1 > math.pi or ang2 > math.pi then	-- one or more angle in the third quadrant
					return math.min(ang1, ang2)
				else
					return math.max(ang1, ang2)
				end
			else	-- both angles in the firs quadrant
				return math.min(ang1, ang2)
			end
		end
	end
end

-- open fire on the area around the selected target
function HumanBehaviors.ShootArea(self)
	if not MovableMan:IsActor(self.UnseenTarget) or not self.FirearmIsReady then
		return true
	end
	
	-- see if we can shoot from the prone position
	local ShootTimer = Timer()
	local aimTime = RangeRand(400, 700)
	if not self.flying and HumanBehaviors.GoProne(self, self.UnseenTarget.Pos, self.UnseenTarget.ID) then
		aimTime = aimTime + 500
	end
	
	local StartPos = Vector(self.UnseenTarget.Pos.X, self.UnseenTarget.Pos.Y)
	
	-- aim at the target in case we can see it when sharp aiming
	self:SetAimAngle(SceneMan:ShortestDistance(self.EyePos, StartPos, false).AbsRadAngle)
	self.deviceState = AHuman.AIMING
	
	-- aim for ~250ms
	for i = 1, 15 do
		coroutine.yield()	-- wait until next frame
	end
	
	if not self.FirearmIsReady then
		return true
	end
	
	local AimPoint
	for i = 1, 5 do	-- try up to five times to find a target area that is resonably close to the target
		AimPoint = StartPos + Vector(RangeRand(-100, 100), RangeRand(-100, 50))
		if AimPoint.X > SceneMan.SceneWidth then
			AimPoint.X = SceneMan.SceneWidth - AimPoint.X
		elseif AimPoint.X < 0 then
			AimPoint.X = AimPoint.X + SceneMan.SceneWidth
		end
		
		-- check if we can fire at the AimPoint
		local Trace = SceneMan:ShortestDistance(self.EyePos, AimPoint, false)
		local rayLenght = SceneMan:CastObstacleRay(self.EyePos, Trace, Vector(), Vector(), rte.NoMOID, self.IgnoresWhichTeam, rte.grassID, 11)
		if Trace.Magnitude * 0.67 < rayLenght then
			break	-- the AimPoint is close enough to the target, start shooting
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	if not self.FirearmIsReady then
		return true
	end
	
	local aim
	local PrjDat = HumanBehaviors.GetProjectileData(self)
	local Dist = SceneMan:ShortestDistance(self.EquippedItem.Pos, AimPoint, false)
	if Dist.Magnitude < PrjDat.rng then
		aim = HumanBehaviors.GetAimAngle(self, PrjDat, Dist)
	else
		return true	-- target out of range
	end
	
	local CheckTargetTimer = Timer()
	local aimError = RangeRand(-0.25, 0.25)
	
	while aim do
		if self.FirearmIsReady then
			self.deviceState = AHuman.AIMING
			self.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim+aimError+RangeRand(-0.02, 0.02))
			
			if ShootTimer:IsPastSimMS(aimTime) then
				self.fire = true
				aimError = aimError * 0.985
			else
				self.fire = false
			end
		else
			self.deviceState = AHuman.POINTING
			self.fire = false
			
			ShootTimer:Reset()
			if self.FirearmIsEmpty then
				self:ReloadFirearm()
			end
			
			break -- stop this behavior when the mag is empty
		end
		
		coroutine.yield()	-- wait until next frame
		
		if self.UnseenTarget and CheckTargetTimer:IsPastSimMS(400) then
			if MovableMan:IsActor(self.UnseenTarget) and (self.UnseenTarget.ClassName == "AHuman" or self.UnseenTarget.ClassName == "ACrab") then
				CheckTargetTimer:Reset()
				if self.UnseenTarget:GetController():IsState(Controller.WEAPON_FIRE) then
					-- compare the enemy aim angle with the angle of the alarm vector
					local enemyAim = self.UnseenTarget:GetAimAngle(true)
					if enemyAim > math.pi*2 then	-- make sure the angle is in the [0..2*pi] range
						enemyAim = enemyAim - math.pi*2
					elseif enemyAim < 0 then
						enemyAim = enemyAim + math.pi*2
					end
					
					local angDiff = SceneMan:ShortestDistance(self.UnseenTarget.Pos, self.Pos, false).AbsRadAngle - enemyAim
					if angDiff > math.pi then	-- the difference between two angles can never be larger than pi
						angDiff = angDiff - math.pi*2
					elseif angDiff < -math.pi then
						angDiff = angDiff + math.pi*2
					end
					
					if math.abs(angDiff) < 0.5 then
						-- this actor is shooting in our direction
						AimPoint = self.UnseenTarget.Pos + SceneMan:ShortestDistance(self.UnseenTarget.Pos, AimPoint, false) / 2 + Vector(RangeRand(-30, 30), RangeRand(-30, 30))
						aimError = RangeRand(-0.15, 0.15)
						
						Dist = SceneMan:ShortestDistance(self.EquippedItem.Pos, AimPoint, false)
						if Dist.Magnitude < PrjDat.rng then
							aim = HumanBehaviors.GetAimAngle(self, PrjDat, Dist)
						end
					end
				end
			else
				self.UnseenTarget = nil
			end
		end
	end
	
	return true
end


-- stop the user fom inadvertently modifying the storage table
local Proxy = {}
local Mt = {
	__index = HumanBehaviors,
	__newindex = function(Table, k, v)
		error("The HumanBehaviors table is read-only.", 2)
	end
}
setmetatable(Proxy, Mt)
HumanBehaviors = Proxy
