
require("Actors/AI/PID")

HumanBehaviors = {}

function HumanBehaviors.LookForTargets(AI, Owner)
	local Origin
	local viewAng
	
	if AI.Target then	-- widen the view angle to increase the chance of spotting new targets
		Origin = Owner.EyePos
		viewAng = 0.7
	elseif AI.deviceState == AHuman.AIMING then
		if Owner.EquippedItem then
			Origin = Owner.EquippedItem.Pos
		else
			Origin = Owner.EyePos
		end
		
		viewAng = 0.3
	elseif AI.deviceState == AHuman.POINTING then
		Origin = Owner.EyePos
		viewAng = 0.5
	elseif AI.deviceState == AHuman.THROWING then
		Origin = Owner.EyePos
		viewAng = 0.7
	else
		Origin = Owner.EyePos
		viewAng = RangeRand(0.5, 1.4)
	end
	
	local viewLen = SceneMan:ShortestDistance(Owner.EyePos, Owner.ViewPoint, false).Magnitude + FrameMan.PlayerScreenWidth * 0.55
	local Trace = Vector(viewLen, 0):RadRotate(viewAng*NormalRand()+Owner:GetAimAngle(true))
	local ID = SceneMan:CastMORay(Origin, Trace, Owner.ID, Owner.IgnoresWhichTeam, rte.grassID, false, 5)
  
	if ID < rte.NoMOID then
		local HitPoint = SceneMan:GetLastRayHitPos()
		if not AI.isPlayerOwned or not SceneMan:IsUnseen(HitPoint.X, HitPoint.Y, Owner.Team) then	-- AI-teams ignore the fog
			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.CheckEnemyLOS(AI, Owner)
	if not AI.Enemies then	-- add all enemy actors on our screen to a table and check LOS to them, one per frame
		AI.Enemies = {}
		for Act in MovableMan.Actors do
			if Act.Team ~= Owner.Team then
				if not AI.isPlayerOwned or not SceneMan:IsUnseen(Act.Pos.X, Act.Pos.Y, Owner.Team) then	-- AI-teams ignore the fog
					local Dist = SceneMan:ShortestDistance(Owner.ViewPoint, Act.Pos, false)
					if (math.abs(Dist.X) - Act.Diameter < FrameMan.PlayerScreenWidth * 0.6) and
						(math.abs(Dist.Y) - Act.Diameter < FrameMan.PlayerScreenHeight * 0.6)
					then
						table.insert(AI.Enemies, Act)
					end
				end
			end
		end
		
		return HumanBehaviors.LookForTargets(AI, Owner)	-- cast rays like normal actors occasionally
	else
		local Enemy = table.remove(AI.Enemies)
		if Enemy then
			if MovableMan:ValidMO(Enemy) then
				local Origin
				if Owner.EquippedItem and AI.deviceState == AHuman.AIMING then
					Origin = Owner.EquippedItem.Pos
				else
					Origin = Owner.EyePos
				end
				
				local LookTarget
				if Enemy.ClassName == "ADoor" then
					local Door = ToADoor(Enemy).Door
					if Door and Door:IsAttached() then
						LookTarget = Door.Pos
					else
						return HumanBehaviors.LookForTargets(AI, Owner)		-- this door is destroyed, cast rays like normal actors
					end
				else
					LookTarget = Enemy.Pos
				end
				
				-- cast at body
				if not AI.isPlayerOwned or not SceneMan:IsUnseen(LookTarget.X, LookTarget.Y, Owner.Team) then	-- AI-teams ignore the fog
					local Dist = SceneMan:ShortestDistance(Owner.ViewPoint, LookTarget, false)
					if (math.abs(Dist.X) - Enemy.Radius < FrameMan.PlayerScreenWidth * 0.52) and
						(math.abs(Dist.Y) - Enemy.Radius < FrameMan.PlayerScreenHeight * 0.52)
					then
						local Trace = SceneMan:ShortestDistance(Origin, LookTarget, false)
						local ID = SceneMan:CastMORay(Origin, Trace, Owner.ID, Owner.IgnoresWhichTeam, rte.grassID, false, 5)
						if ID < rte.NoMOID then
							local MO = MovableMan:GetMOFromID(ID)
							if MO and ID ~= MO.RootID then
								MO = MovableMan:GetMOFromID(MO.RootID)
							end
							
							return MO, SceneMan:GetLastRayHitPos()
						end
					end
				end
				
				-- no LOS to the body, cast at head
				if Enemy.EyePos and (not AI.isPlayerOwned or not SceneMan:IsUnseen(Enemy.EyePos.X, Enemy.EyePos.Y, Owner.Team)) then	-- AI-teams ignore the fog
					local Dist = SceneMan:ShortestDistance(Owner.ViewPoint, Enemy.EyePos, false)
					if (math.abs(Dist.X) < FrameMan.PlayerScreenWidth * 0.52) and
						(math.abs(Dist.Y) < FrameMan.PlayerScreenHeight * 0.52)
					then
						local Trace = SceneMan:ShortestDistance(Origin, Enemy.EyePos, false)
						local ID = SceneMan:CastMORay(Origin, Trace, Owner.ID, Owner.IgnoresWhichTeam, rte.grassID, false, 5)
						if ID < rte.NoMOID then
							local MO = MovableMan:GetMOFromID(ID)
							if MO and ID ~= MO.RootID then
								MO = MovableMan:GetMOFromID(MO.RootID)
							end
							
							return MO, SceneMan:GetLastRayHitPos()
						end
					end
				end
			end
		else
			AI.Enemies = nil
			return HumanBehaviors.LookForTargets(AI, Owner)		-- cast rays like normal actors occasionally
		end
	end
end

function HumanBehaviors.CalculateThreatLevel(MO, Owner)
	-- prioritize closer targets
	local priority = -SceneMan:ShortestDistance(Owner.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 + 1.0
		else
			priority = priority + 0.5
		end
	elseif MO.ClassName == "ACrab" then
		if MO.FirearmIsReady then	-- prioritize armed targets
			priority = priority + 0.7
		else
			priority = priority + 0.3
		end
	end
	
	return priority - MO.Health / 500	-- prioritize damaged targets
end

function HumanBehaviors.ProcessAlarmEvent(AI, Owner)
	AI.AlarmPos = nil
	
	local loudness, AlarmVec
	for Event in MovableMan.AlarmEvents do
		if Event.Team ~= Owner.Team then	-- caused by some other team's activites - alarming!
			loudness = 30 + FrameMan.PlayerScreenWidth * 0.5 * Owner.Perceptiveness * (Event.Range / 500)	-- adjust the audiable range to the screen resolution
			AlarmVec = SceneMan:ShortestDistance(Owner.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 our relative position to the alarm location is the same, don't repeat the signal
				-- check if we have line of sight to the alarm point
				if (not AI.LastAlarmVec or (AI.LastAlarmVec-AlarmVec).Largest > 10) then
					AI.LastAlarmVec = AlarmVec
					
					if AlarmVec.Largest < 100 then
						-- check more carfully at close range, and allow hearing of partially blocked alarm events
						if SceneMan:CastStrengthSumRay(Owner.EyePos, Event.ScenePos, 4, rte.grassID) < 100 then
							AI.AlarmPos = Vector(Event.ScenePos.X, Event.ScenePos.Y)
						end
					elseif not SceneMan:CastStrengthRay(Owner.EyePos, AlarmVec, 6, Vector(), 8, rte.grassID, true)	then
						AI.AlarmPos = Vector(Event.ScenePos.X, Event.ScenePos.Y)
					end
					
					if AI.AlarmPos then
						AI:CreateFaceAlarmBehavior(Owner)
						return true
					end
				end
			-- sometimes try to shoot back at enemies outside our view range (400 is the range of the brain alarm)
			elseif not AI.flying and Event.Range > 400 and PosRand() < 0.6 and Owner.FirearmIsReady then
				-- only do this if we are facing the event and we or the target has changed position since the last check
				if (AlarmVec.X < 0 and Owner.HFlipped) or (AlarmVec.X > 0 and not Owner.HFlipped) then
					if AlarmVec.Largest < FrameMan.PlayerScreenWidth * 1.8 and
						(not AI.LastAlarmVec or (AI.LastAlarmVec-AlarmVec).Largest > 20)
					then
						-- check LOS
						local ID = SceneMan:CastMORay(Owner.EyePos, AlarmVec, Owner.ID, Owner.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)
								if FoundMO.EquippedItem and not ToHeldDevice(FoundMO.EquippedItem):IsWeapon() then
									FoundMO = nil	-- don't shoot at actors using tools
								end
							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.7 then
									-- this actor is shooting in our direction
									AI.ReloadTimer:Reset()
									AI.TargetLostTimer:Reset()
									
									-- try to shoot back
									AI.UnseenTarget = FoundMO
									AI:CreateSuppressBehavior(Owner)
									
									AI.AlarmPos = Event.ScenePos
									return true
								end
							end
						else
							AI.LastAlarmVec = AlarmVec	-- don't look here again if the raycast failed
						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 equip a primary weapon if we have one. return true if we must run this function again to be sure
function HumanBehaviors.EquipPrimaryWeapon(AI, Owner)
	if Owner.EquippedItem then
		if Owner.InventorySize > 0 and (not AI.PlayerPreferredHD or Owner.EquippedItem.PresetName ~= AI.PlayerPreferredHD) and
			not Owner.EquippedItem:HasObjectInGroup("Primary Weapons") and Owner:HasObjectInGroup("Primary Weapons")
		then
			-- the weapon equipped is not a primary weapon, but there is one in the inventory
			AI.Ctrl:SetState(Controller.WEAPON_CHANGE_NEXT, true)
			return true
		end
	elseif Owner.ClassName == "AHuman" then
		return Owner: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(AI, Owner)
	local sweepUp = true
	local sweepDone = false
	local maxAng = 1.4
	local minAng = -1.4
	local aim
	
	if HumanBehaviors.EquipPrimaryWeapon(AI, Owner) then
		local EquipTimer = Timer()
		while true do
			if EquipTimer:IsPastSimMS(500) then
				if not HumanBehaviors.EquipPrimaryWeapon(AI, Owner) 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 AI.OldTargetPos then	-- try to reaquire an old target
		local Dist = SceneMan:ShortestDistance(Owner.EyePos, AI.OldTargetPos, false)
		AI.OldTargetPos = nil
		if (Dist.X < 0 and Owner.HFlipped) or (Dist.X > 0 and not Owner.HFlipped) then	-- we are facing the target	
			AI.deviceState = AHuman.AIMING
			AI.Ctrl.AnalogAim = Dist.Normalized
			
			for _ = 1, math.random(20, 30) do
				coroutine.yield()	-- aim here for ~0.25s
			end
		end
	elseif not AI.isPlayerOwned then -- face the most likely enemy approach direction
		for _ = 1, math.random(5) do	-- wait for a while
			coroutine.yield()
		end
		
		Owner:AddAISceneWaypoint(Vector(Owner.Pos.X, 0))
		Owner:UpdateMovePath()
		coroutine.yield()	-- wait until next frame
		
		-- face the direction of the first waypoint
		for WptPos in Owner.MovePath do
			local Dist = SceneMan:ShortestDistance(Owner.Pos, WptPos, false)
			if Dist.X > 5 then
				AI.SentryFacing = false
				AI.Ctrl.AnalogAim = Dist.Normalized
			elseif Dist.X < -5 then
				AI.SentryFacing = true
				AI.Ctrl.AnalogAim = Dist.Normalized
			end
			
			break
		end
	end
	
	while true do	-- start by looking forward
		aim = Owner:GetAimAngle(false)
		
		if sweepUp then
			if aim < maxAng/3 then
				AI.Ctrl:SetState(Controller.AIM_UP, false)
				coroutine.yield()	-- wait until next frame
				AI.Ctrl:SetState(Controller.AIM_UP, true)
			else
				sweepUp = false
			end
		else
			if aim > minAng/3 then
				AI.Ctrl:SetState(Controller.AIM_DOWN, false)
				coroutine.yield()	-- wait until next frame
				AI.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 Owner.HFlipped ~= AI.SentryFacing then
		Owner.HFlipped = AI.SentryFacing	-- turn to the direction we have been order to guard
		return true	-- restart this behavior
	end
	
	while true do	-- look down
		aim = Owner:GetAimAngle(false)
		if aim > minAng then
			AI.Ctrl:SetState(Controller.AIM_DOWN, true)
		else
			break
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	local Hit = Vector()
	local NoObstacle = {}
	local StartPos
	AI.deviceState = AHuman.AIMING
	
	while true do	-- scan the area for obstacles
		aim = Owner:GetAimAngle(false)
		if aim < maxAng then
			AI.Ctrl:SetState(Controller.AIM_UP, true)
		else
			break
		end
		
		if Owner:EquipFirearm(false) and Owner.EquippedItem then
			StartPos = ToHeldDevice(Owner.EquippedItem).MuzzlePos
		else
			StartPos = Owner.EyePos
		end
		
		-- save the angle to a table if there is no obstacle
		if not SceneMan:CastStrengthRay(StartPos, Vector(60, 0):RadRotate(Owner: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
	AI.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 Owner:EquipFirearm(false) and not Owner:EquipThrowable(false) then
			break
		end
		
		aim = Owner:GetAimAngle(false)
		
		if sweepUp then
			if aim < maxAng then
				if aim < maxAng/5 and aim > minAng/5 and PosRand() > 0.3 then
					AI.Ctrl:SetState(Controller.AIM_UP, false)
				else
					AI.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
					AI.Ctrl:SetState(Controller.AIM_DOWN, false)
				else
					AI.Ctrl:SetState(Controller.AIM_DOWN, true)
				end
			else
				sweepUp = true
			end
		end
		
		if SharpTimer:IsPastSimMS(aimTime) then
			SharpTimer:Reset()
			
			if HumanBehaviors.EquipPrimaryWeapon(AI, Owner) then
				aimTime = 500
			elseif AI.deviceState == AHuman.AIMING then
				aimTime = RangeRand(1000, 3000)
				AI.deviceState = AHuman.POINTING
			else
				aimTime = RangeRand(6000, 12000) * angDiff
				AI.deviceState = AHuman.AIMING
			end
			
			if SceneMan:ShortestDistance(Owner.Pos, AI.SentryPos, false).Magnitude > Owner.Height*0.7 then
				AI.SentryPos = SceneMan:MovePointToGround(AI.SentryPos, Owner.Height*0.25, 2)
				Owner:ClearAIWaypoints()
				Owner:AddAISceneWaypoint(AI.SentryPos)
				AI:CreateGoToBehavior(Owner)	-- try to return to the sentry pos
				break
			elseif Owner.HFlipped ~= AI.SentryFacing then
				Owner.HFlipped = AI.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(AI, Owner)
	while AI.flying or Owner.Vel.Magnitude > 4 do	-- wait untill we are stationary
		return true
	end
	
	if HumanBehaviors.EquipPrimaryWeapon(AI, Owner) then
		local EquipTimer = Timer()
		while true do
			if EquipTimer:IsPastSimMS(500) then
				if not HumanBehaviors.EquipPrimaryWeapon(AI, Owner) 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(Owner.Pos, Vector(512, 0), Vector(), Free, Owner.ID, Owner.IgnoresWhichTeam, rte.grassID, 4)
	local Dist = SceneMan:ShortestDistance(Owner.Pos, Free, false)
	
	if Dist.Magnitude > 20 then
		Owner:ClearAIWaypoints()
		Owner:AddAISceneWaypoint(Free)
		coroutine.yield()	-- wait until next frame
		Owner:UpdateMovePath()
		coroutine.yield()	-- wait until next frame
		
		local PrevPos = Vector(Owner.Pos.X, Owner.Pos.Y)
		for WptPos in Owner.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(Owner.Pos, Vector(-512, 0), Vector(), Free, Owner.ID, Owner.IgnoresWhichTeam, rte.grassID, 4)
	Dist = SceneMan:ShortestDistance(Owner.Pos, Free, false)
	
	if Dist.Magnitude > 20 then
		Owner:ClearAIWaypoints()
		Owner:AddAISceneWaypoint(Free)
		coroutine.yield()	-- wait until next frame
		Owner:UpdateMovePath()
		coroutine.yield()	-- wait until next frame
		
		local PrevPos = Vector(Owner.Pos.X, Owner.Pos.Y)
		for WptPos in Owner.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	
	
	Owner:ClearAIWaypoints()
	coroutine.yield()	-- wait until next frame
	
	if WptA then
		Dist = SceneMan:ShortestDistance(Owner.Pos, WptA, false)
		if Dist.Magnitude > 20 then
			Owner:AddAISceneWaypoint(WptA)
		else
			WptA = nil
		end
	end
	
	if WptB then
		Dist = SceneMan:ShortestDistance(Owner.Pos, WptB, false)
		if Dist.Magnitude > 20 then
			Owner:AddAISceneWaypoint(WptB)
		else
			WptB = nil
		end
	end
	
	if WptA or WptB then
		AI:CreateGoToBehavior(Owner)
	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))
				Owner.HFlipped = not Owner.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(AI, Owner)	
	if not Owner:EquipDiggingTool(false) then
		AI:CreateGetToolBehavior(Owner)
		return true
	end
	
	-- make sure our weapon have ammo before we start to dig, just in case we encounter an enemy while digging
	if Owner.EquippedItem and (Owner.FirearmNeedsReload or Owner.FirearmIsEmpty) and Owner.EquippedItem:HasObjectInGroup("Weapons") then
		Owner:ReloadFirearm()
		
		repeat
			coroutine.yield()	-- wait until next frame
		until not Owner.FirearmIsEmpty
	end
	
	Owner:EquipDiggingTool(true)
	
	local aimAngle = 0.45
	local LookVec = Vector(180, 0):RadRotate(aimAngle)
	local GoldPos = Vector()
	local BestGoldPos = Vector()
	local smallestPenalty = math.huge
	
	while true do
		AI.Ctrl.AnalogAim = LookVec.Normalized
		if SceneMan:CastMaterialRay(Owner.EyePos, LookVec, rte.goldID, GoldPos, 1, true) then
			-- avoid gold close to the edeges of the scene
			if GoldPos.Y < SceneMan.SceneHeight - 25 and
				(SceneMan.SceneWrapsX or (GoldPos.X > 50 and GoldPos.X < SceneMan.SceneWidth - 50))
			then
				local Dist = SceneMan:ShortestDistance(Owner.Pos, GoldPos, false)	-- prioritize gold close to us
				local str = SceneMan:CastStrengthSumRay(Owner.EyePos, GoldPos, 3, rte.goldID) / 30	-- prioritize gold in soft ground
				local penalty = str + Dist.Magnitude + Dist.Y^2
				
				-- prioritize gold located horizontaly relative to us
				if math.abs(Dist.X) > math.abs(Dist.Y) then
					penalty = penalty - 40
				end
				
				-- prioritize gold below us
				if Dist.Y > 0 then
					penalty = penalty - 5
				end
				
				if penalty < smallestPenalty then
					if Dist.Magnitude < 50 then	-- dig to a point behind the gold
						GoldPos = Owner.Pos + Dist:SetMagnitude(55)
					end
					
					-- make sure there is no metal in our path
					if not SceneMan:CastStrengthRay(Owner.Pos, Dist:SetMagnitude(60), 95, Vector(), 2, rte.grassID, SceneMan.SceneWrapsX) then
						smallestPenalty = penalty + RangeRand(-10, 10)
						BestGoldPos:SetXY(GoldPos.X, GoldPos.Y)
					end
				end
			end
		end
		
		if aimAngle < -3.6 then
			break
		else
			aimAngle = aimAngle - 0.033
			
			-- search further away horizontaly
			if aimAngle < -0.8 and aimAngle > -2.4 then
				LookVec = Vector(120,0):RadRotate(aimAngle)
			else
				LookVec = Vector(180,0):RadRotate(aimAngle)
			end
			
			coroutine.yield()	-- wait until next frame
		end
	end
	
	if BestGoldPos.Largest == 0 then
		if Owner.Pos.Y < SceneMan.SceneHeight - 50 then	-- don't dig beyond the scene limit
			-- no gold found, so dig down and try again
			local rayLenght = 60
			local Target = Owner.Pos + Vector(rayLenght, rayLenght)
			local str_r = SceneMan:CastStrengthSumRay(Owner.Pos, Target, 6, rte.goldID)
			coroutine.yield()	-- wait until next frame
			
			Target = Owner.Pos + Vector(-rayLenght, rayLenght)
			local str_l = SceneMan:CastStrengthSumRay(Owner.Pos, Target, 6, rte.goldID)
			coroutine.yield()	-- wait until next frame
			
			if str_r < str_l then
				BestGoldPos = Owner.Pos + Vector(rayLenght, rayLenght)
			else
				BestGoldPos = Owner.Pos + Vector(-rayLenght, rayLenght)
			end
		else
			-- no gold here, and we cannot dig deeper, calculate average horizontal strength
			local rayLenght = 80
			local Target = Owner.Pos + Vector(rayLenght, -5)
			local Trace = SceneMan:ShortestDistance(Owner.Pos, Target, false)
			local str_r = SceneMan:CastStrengthSumRay(Owner.Pos, Target, 5, rte.goldID)
			local obst_r = SceneMan:CastStrengthRay(Owner.Pos, Trace, 95, Vector(), 2, rte.grassID, SceneMan.SceneWrapsX)
			coroutine.yield()	-- wait until next frame
			
			Target = Owner.Pos + Vector(-rayLenght, -5)
			Trace = SceneMan:ShortestDistance(Owner.Pos, Target, false)
			local str_l = SceneMan:CastStrengthSumRay(Owner.Pos, Target, 5, rte.goldID)
			local obst_l = SceneMan:CastStrengthRay(Owner.Pos, Trace, 95, Vector(), 2, rte.grassID, SceneMan.SceneWrapsX)
			coroutine.yield()	-- wait until next frame
			
			local goLeft
			if obst_l then
				goLeft = false
			elseif obst_r then
				goLeft = true
			else
				goLeft = math.random() > 0.5
				
				-- go towards the larger obstacle, unless metal
				if math.abs(str_l - str_r) > 200 then
					if str_r > str_l and not obst_r then
						goLeft = false
					elseif str_r < str_l and not obst_l then
						goLeft = true
					end
				end
			end
		
			if goLeft then
				BestGoldPos = Owner.Pos + Vector(-rayLenght, -5)
			else
				BestGoldPos = Owner.Pos + Vector(rayLenght, -5)
			end
		end
	end
	
	BestGoldPos.Y = math.min(BestGoldPos.Y, SceneMan.SceneHeight-30)
	Owner:ClearAIWaypoints()
	Owner:AddAISceneWaypoint(BestGoldPos)
	AI:CreateGoToBehavior(Owner)
	
	return true
end


-- find the closest enemy brain
function HumanBehaviors.BrainSearch(AI, Owner)
	if HumanBehaviors.EquipPrimaryWeapon(AI, Owner) then
		local EquipTimer = Timer()
		while true do
			if EquipTimer:IsPastSimMS(500) then
				if not HumanBehaviors.EquipPrimaryWeapon(AI, Owner) 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 ~= Owner.Team and Act:HasObjectInGroup("Brains") then
			table.insert(Brains, Act)
		end
	end
	
	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) ~= Owner.Team then
				local Act = GmActiv:GetPlayerBrain(player)
				if Act and MovableMan:IsActor(Act) then
					table.insert(Brains, Act)
				end
			end
		end
	end
	
	if #Brains > 0 then
		coroutine.yield()	-- wait until next frame
		
		if #Brains == 1 then
			if MovableMan:IsActor(Brains[1]) then
				Owner:ClearAIWaypoints()
				AI.FollowingActor = Brains[1]
				Owner:AddAIMOWaypoint(AI.FollowingActor)
				AI:CreateGoToBehavior(Owner)
			end
		else
			local ClosestBrain
			local minDist = math.huge
			for _, Act in pairs(Brains) do
				-- measure how easy the path to the destination is to traverse
				if MovableMan:IsActor(Act) then
					Owner:ClearAIWaypoints()
					Owner:AddAISceneWaypoint(Act.Pos)
					Owner:UpdateMovePath()
					
					local OldWpt, deltaY
					local index = 0
					local height = 0
					local pathLength = 0
					local pathObstMaxHeight = 0
					for Wpt in Owner.MovePath do
						pathLength = pathLength + 1
						Wpt = SceneMan:MovePointToGround(Wpt, 15, 6)
						
						if OldWpt then
							deltaY = OldWpt.Y - Wpt.Y
							if deltaY > 20 then	-- Wpt is more than n pixels above OldWpt in the scene
								if deltaY / math.abs(SceneMan:ShortestDistance(OldWpt, Wpt, false).X) > 1 then	-- the slope is more than 45 degrees
									height = height + (OldWpt.Y - Wpt.Y)
									pathObstMaxHeight = math.max(pathObstMaxHeight, height)
								else
									height = 0
								end
							else
								height = 0
							end
						end
						
						OldWpt = Wpt
						
						if index > 30 then
							index = 0
							coroutine.yield()	-- wait until the next frame
						else
							index = index + 1
						end
					end
					
					local score = pathLength * 0.55 + math.floor(pathObstMaxHeight/27) * 8
					if score < minDist then
						minDist = score
						ClosestBrain = Act
					end
					
					coroutine.yield()	-- wait until next frame
				end
			end
			
			Owner:ClearAIWaypoints()
			
			if MovableMan:IsActor(ClosestBrain) then
				AI.FollowingActor = ClosestBrain
				Owner:AddAIMOWaypoint(AI.FollowingActor)
				AI:CreateGoToBehavior(Owner)
			else
				return true	-- the brain we found died while we where searching, restart this behavior next frame
			end
		end
	else	-- no enemy brains left
		if AI.isPlayerOwned then
			Owner.AIMode = Actor.AIMODE_SENTRY
		else
			Owner.AIMode = Actor.AIMODE_PATROL
		end
	end
	
	return true
end


-- find a weapon to pick up
function HumanBehaviors.WeaponSearch(AI, Owner)
	local range, minDist, HD
	local Devices = {}
	local pickupDiggers = not Owner:EquipDiggingTool(false)
	
	if AI.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(Owner.Pos, HD.Pos, false).Magnitude
			if range < minDist and not SceneMan:IsUnseen(HD.Pos.X, HD.Pos.Y, Owner.Team) then
				table.insert(Devices, HD)
			end
		end
	end
	
	if #Devices > 0 then
		coroutine.yield()	-- wait until next frame
		
		local PrevWpt
		if not AI.FollowingActor then
			if Owner.AIMode == Actor.AIMODE_SENTRY and not AI.flying then
				PrevWpt = Vector(Owner.Pos.X, Owner.Pos.Y)	-- return here after pick up
			else
				PrevWpt = Owner:GetLastAIWaypoint()
				if (PrevWpt-Owner.Pos).Largest < 1 then
					PrevWpt = nil	-- we have no waypoint
				end
			end
		end
		
		if AI.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
				Owner:ClearAIWaypoints()
				Owner:AddAISceneWaypoint(Item.Pos)
				Owner:UpdateMovePath()
				
				-- estimate the walking distance to the item				
				if Item.ClassName == "TDExplosive" then
					waypoints = Owner.MovePathSize * 1.4	-- prioritize non-grenades
				elseif Item:IsTool() then
					if pickupDiggers then
						waypoints = Owner.MovePathSize * 1.8	-- prioritize non-diggers
					else
						waypoints = minDist -- don't pick up
					end
				else
					waypoints = Owner.MovePathSize
				end
				
				if waypoints < minDist then
					table.insert(DevicesToPickUp, {HD=Item, range=waypoints})
				end
				
				coroutine.yield()	-- wait until next frame
			end
		end
		
		Owner: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
		
		AI.PickupHD = nil
		for _, Data in pairs(DevicesToPickUp) do
			if MovableMan:IsDevice(Data.HD) then
				AI.PickupHD = Data.HD
				break
			end
		end
		
		if AI.PickupHD then
			if PrevWpt then
				AI.PrevAIWaypoint = PrevWpt
			end
			
			Owner:AddAIMOWaypoint(AI.PickupHD)
			AI:CreateGoToBehavior(Owner)
		else	-- no items found
			if AI.FollowingActor and MovableMan:IsActor(AI.FollowingActor) then
				Owner:AddAIMOWaypoint(AI.FollowingActor)
			elseif AI.PrevAIWaypoint then
				Owner:AddAISceneWaypoint(AI.PrevAIWaypoint)
			end
		end
	end
	
	return true
end


-- find a tool to pick up
function HumanBehaviors.ToolSearch(AI, Owner)
	local range, minDist, HD
	local Devices = {}
	
	if Owner.AIMode == Actor.AIMODE_GOLDDIG then
		minDist = FrameMan.PlayerScreenWidth * 0.5	-- move up to half a screen when digging
	elseif AI.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(Owner.Pos, HD.Pos, false).Magnitude
			if range < minDist and not SceneMan:IsUnseen(HD.Pos.X, HD.Pos.Y, Owner.Team) then
				table.insert(Devices, HD)
			end
		end
	end
	
	if #Devices > 0 then
		coroutine.yield()	-- wait until next frame
		
		local PrevWpt
		if not AI.FollowingActor then
			if Owner.AIMode == Actor.AIMODE_SENTRY and not AI.flying then
				PrevWpt = Vector(Owner.Pos.X, Owner.Pos.Y)	-- return here after pick up
			else
				PrevWpt = Owner:GetLastAIWaypoint()
				if (PrevWpt-Owner.Pos).Largest < 1 then
					PrevWpt = nil	-- we have no waypoint
				end
			end
		end
		
		if Owner.AIMode == Actor.AIMODE_GOLDDIG then
			minDist = 30
		elseif AI.isPlayerOwned then
			minDist = 5
		else
			minDist = 15
		end
		
		for _, Item in pairs(Devices) do
			if MovableMan:IsDevice(Item) then
				Owner:ClearAIWaypoints()
				Owner:AddAISceneWaypoint(Item.Pos)
				Owner:UpdateMovePath()
				
				local waypoints = Owner.MovePathSize	-- estimate the walking distance to the item
				if waypoints < minDist then
					minDist = waypoints
					AI.PickupHD = Item
				end
				
				coroutine.yield()	-- wait until next frame
			end
		end
		
		Owner:ClearAIWaypoints()
		
		if MovableMan:IsDevice(AI.PickupHD) then		
			if PrevWpt then
				AI.PrevAIWaypoint = PrevWpt
			end
			
			Owner:AddAIMOWaypoint(AI.PickupHD)
			AI:CreateGoToBehavior(Owner)
		else
			AI.PickupHD = nil	-- the item became invalid while searching
			
			if AI.FollowingActor and MovableMan:IsActor(AI.FollowingActor) then
				Owner:AddAIMOWaypoint(AI.FollowingActor)
			elseif AI.PrevAIWaypoint then
				Owner:AddAISceneWaypoint(AI.PrevAIWaypoint)
			end
		end
	end
	
	return true
end


-- move to the next waypoint
function HumanBehaviors.GoToWpt(AI, Owner)
	if not Owner.MOMoveTarget then
		if SceneMan:ShortestDistance(Owner:GetLastAIWaypoint(), Owner.Pos, false).Largest < 10 then
			Owner:ClearAIWaypoints()
			if Owner.AIMode == Actor.AIMODE_GOTO then
				Owner.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
	
	local ArrivedTimer = Timer()
	local BurstTimer = Timer()
	local UpdatePathTimer = Timer()
	
	if Owner.MOMoveTarget then
		UpdatePathTimer:SetSimTimeLimitMS(6000)
	else
		UpdatePathTimer:SetSimTimeLimitMS(15000)
	end
	
	local NoLOSTimer = Timer()
	NoLOSTimer:SetSimTimeLimitMS(1000)
	
	local StuckTimer = Timer()
	StuckTimer:SetSimTimeLimitMS(2000)
	
	local nextLatMove = AI.lateralMoveState
	local nextAimAngle = Owner:GetAimAngle(false) * 0.95
	local scanAng = 0	-- for obstacle detection
	local Obstacles = {}
	local PrevWptPos = Owner.Pos
	local sweepCW = true
	local sweepRange = 0
	local digState = AHuman.NOTDIGGING
	local obstacleState = Actor.PROCEEDING
	local WptList, Waypoint, Dist, CurrDist
	local Obst = {R_LOW = 1, R_FRONT = 2, R_HIGH = 3, R_UP = 5, L_UP = 6, L_HIGH = 8, L_FRONT = 9, L_LOW = 10}
	
	while true do
		if Owner.Vel.Largest > 2 then
			StuckTimer:Reset()
		end
		
		if Owner.MOMoveTarget then	-- make the last waypoint marker stick to the MO we are following
			if MovableMan:ValidMO(Owner.MOMoveTarget) then
				Owner:RemoveMovePathEnd()
				Owner:AddToMovePathEnd(Owner.MOMoveTarget.Pos)
				
				if Owner.Team == Owner.MOMoveTarget.Team and not AI.flying then	-- we are following an ally, stop when close
					while SceneMan:ShortestDistance(Owner.Pos, Owner.MOMoveTarget.Pos, false).Magnitude < Owner.Radius+Owner.MOMoveTarget.Radius+10 do
						coroutine.yield()	-- wait until next frame
						
						if not MovableMan:ValidMO(Owner.MOMoveTarget) then
							Owner:ClearMovePath()
							break
						end
					end
				end
			else
				Owner:ClearMovePath()
			end
		end
		
		if not AI.flying and UpdatePathTimer:IsPastSimTimeLimit() then
			UpdatePathTimer:Reset()
			
			if Waypoint and AI.BlockingActor then
				if MovableMan:IsActor(AI.BlockingActor) then
					CurrDist = SceneMan:ShortestDistance(Owner.Pos, Waypoint.Pos, false)
					if (Owner.Pos.X > AI.BlockingActor.Pos.X and CurrDist.X < Owner.Pos.X) or
						(Owner.Pos.X < AI.BlockingActor.Pos.X and CurrDist.X > Owner.Pos.X) or
						SceneMan:ShortestDistance(Owner.Pos, AI.BlockingActor.Pos, false).Magnitude > Owner.Diameter + AI.BlockingActor.Diameter
					then
						AI.BlockingActor = nil	-- the blocking actor is not in the way any longer
						AI.teamBlockState = Actor.NOTBLOCKED
					else
						AI.BlockedTimer:Reset()
						AI.teamBlockState = Actor.IGNORINGBLOCK
						AI:CreateMoveAroundBehavior(Owner)
						break	-- end this behavior
					end
				else
					AI.BlockingActor = nil
				end
			end
			
			AI.deviceState = AHuman.STILL
			AI.proneState = AHuman.NOTPRONE
			AI.jump = false
			nextLatMove = Actor.LAT_STILL
			digState = AHuman.NOTDIGGING
			Waypoint = nil
			WptList = nil
		elseif StuckTimer:IsPastSimTimeLimit() then	-- dislodge
			if AI.jump then
				if Owner.JetTimeLeft < TimerMan.DeltaTimeMS * 3 then	-- out of fuel
					AI.jump = false
					AI.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 AI.lateralMoveState == Actor.LAT_LEFT then
						nextLatMove = Actor.LAT_RIGHT
					else
						nextLatMove = Actor.LAT_LEFT
					end
				end
				
				if AI.refuel and Owner.JetTimeLeft > Owner.JetTimeTotal * 0.95 then
					AI.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 Owner.MOMoveTarget and MovableMan:ValidMO(Owner.MOMoveTarget) then
					if Owner.Team ~= Owner.MOMoveTarget.Team then
						Waypoint = {Pos=Owner.MOMoveTarget.Pos}
					else
						nextLatMove = Actor.LAT_STILL
						AI.jump = false
						coroutine.yield()	-- wait until next frame
					end
				else
					break
				end
			else
				if not Waypoint then	-- get the next waypoint in the list
					-- we must update the path more often if we are following someone
					if Owner.MOMoveTarget and MovableMan:ValidMO(Owner.MOMoveTarget) then
						if SceneMan:ShortestDistance(Owner:GetLastAIWaypoint(), Owner.MOMoveTarget.Pos, false).Largest > 80 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(Owner.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 cornerType
							local CornerPos = Vector(NextWptPos.X, NextWptPos.Y)
							if Owner.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, Owner.ID, Owner.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(Owner.Pos, CornerPos, false)
							if 0 <= SceneMan:CastObstacleRay(Owner.Pos, Dist, Vector(), Vector(), Owner.ID, Owner.IgnoresWhichTeam, rte.grassID, 4) then
								-- CornerPos is blocked
								CornerPos.X = Owner.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)
								Owner:RemoveMovePathBeginning() -- clean up the graphical representation of the path
							end
							
							Owner: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 (Owner.AIMode == Actor.AIMODE_GOLDDIG or Waypoint.Type ~= "drop") and Owner:EquipDiggingTool(false) then
									local PathSegRay = SceneMan:ShortestDistance(PrevWptPos, Waypoint.Pos, false)	-- detect material blocking the path and start digging through it
									if AI.teamBlockState ~= Actor.BLOCKED and SceneMan:CastStrengthRay(PrevWptPos, PathSegRay, 4, Free, 2, rte.doorID, true) then
										if SceneMan:ShortestDistance(Owner.Pos, Free, false).Magnitude < Owner.Height*0.5 then	-- check that we're close enough to start digging
											digState = AHuman.STARTDIG
											AI.deviceState = AHuman.DIGGING
											obstacleState = Actor.DIGPAUSING
											nextLatMove = Actor.LAT_STILL
											sweepRange = math.pi / 4
											StuckTimer:SetSimTimeLimitMS(6000)
											AI.Ctrl.AnalogAim = SceneMan:ShortestDistance(Owner.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 AI.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(Owner.Pos, Waypoint.Pos, false):SetMagnitude(Owner.Height*0.5)
									
									-- don't crawl if it's too steep, climb then instead
									if math.abs(Heading.X) > math.abs(Heading.Y) and Owner.Head and Owner.Head:IsAttached() then
										local TopHeadPos = Owner.Head.Pos - Vector(0, Owner.Head.Radius*0.7)
										
										-- first check up to the top of the head, and then from there forward
										if SceneMan:CastStrengthRay(Owner.Pos, TopHeadPos - Owner.Pos, 5, Free, 4, rte.doorID, true) or
												SceneMan:CastStrengthRay(TopHeadPos, Heading, 5, Free, 4, rte.doorID, true)
										then
											AI.proneState = AHuman.PRONE
										else
											AI.proneState = AHuman.NOTPRONE
										end
										
										coroutine.yield()	-- wait until next frame
									else
										AI.proneState = AHuman.NOTPRONE
									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(Owner.Pos, RandomWpt.Pos, false)
						local mag = Dist.Magnitude
						if mag < 50 and mag < SceneMan:ShortestDistance(Owner.Pos, Waypoint.Pos, false).Magnitude/3 then
							-- this waypoint is closer, check LOS
							if -1 == SceneMan:CastObstacleRay(Owner.Pos, Dist, Vector(), Vector(), Owner.ID, Owner.IgnoresWhichTeam, rte.grassID, 4) then
								Waypoint = RandomWpt	-- go here instead
								if WptList[test-1] then
									PrevWptPos = WptList[test-1].Pos
								else
									PrevWptPos = Owner.Pos
								end
								
								test = math.min(test, #WptList)
								for _ = 1, test do	-- delete the earlier waypoints
									table.remove(WptList, 1)
									if #WptList > 0 then
										Owner:RemoveMovePathBeginning()
									end
								end
							end
						end
					end
				end
				
				if Waypoint then
					CurrDist = SceneMan:ShortestDistance(Owner.Pos, Waypoint.Pos, false)
					
					-- digging
					if digState ~= AHuman.NOTDIGGING then
						if not AI.Target and Owner:EquipDiggingTool(true) then	-- switch to the digger if we have one
							if Owner.FirearmIsEmpty then	-- reload if it's empty
								AI.fire = false
								AI.Ctrl:SetState(Controller.WEAPON_RELOAD, true)
							else
								if AI.teamBlockState == Actor.BLOCKED then
									AI.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, Owner.Pos, false).Magnitude > Owner.Height*0.5 and
										 SceneMan:ShortestDistance(Owner.Pos, Waypoint.Pos, false).Magnitude > Owner.Height*0.5
									then
										digState = AHuman.NOTDIGGING
										obstacleState = Actor.PROCEEDING
										AI.deviceState = AHuman.STILL
										AI.fire = false
										Owner:EquipFirearm(true)
									else
										-- see if we have dug out all that we can in the sweep area without moving closer
										local centerAngle = CurrDist.AbsRadAngle
										local Ray = Vector(Owner.Height*0.3, 0):RadRotate(centerAngle)	-- center
										if SceneMan:CastNotMaterialRay(Owner.Pos, Ray, 0, 3, false) < 0 then
											coroutine.yield()	-- wait until next frame
											
											-- now check the tunnel's thickness
											Ray = Vector(Owner.Height*0.3, 0):RadRotate(centerAngle + sweepRange)	-- up
											if SceneMan:CastNotMaterialRay(Owner.Pos, Ray, rte.airID, 3, false) < 0 then
												coroutine.yield()	-- wait until next frame
												
												Ray = Vector(Owner.Height*0.3, 0):RadRotate(centerAngle - sweepRange)	-- down
												if SceneMan:CastNotMaterialRay(Owner.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 aimAngle = Owner:GetAimAngle(true)
										local AimVec = Vector(1, 0):RadRotate(aimAngle)
										
										local angDiff = math.asin(AimVec:Cross(CurrDist.Normalized))	-- the angle between CurrDist and AimVec
										if math.abs(angDiff) < sweepRange then
											AI.fire = true	-- only fire the digger at the obstacle
										else
											AI.fire = false
										end
										
										-- sweep the digger between the two endpoints of the obstacle
										local DigTarget
										if sweepCW then
											DigTarget = Vector(Owner.Height*0.4, 0):RadRotate(centerAngle + sweepRange)
										else
											DigTarget = Vector(Owner.Height*0.4, 0):RadRotate(centerAngle - sweepRange)
										end
										
										angDiff = math.asin(AimVec:Cross(DigTarget.Normalized))	-- The angle between DigTarget and AimVec
										if math.abs(angDiff) < 0.1 then
											sweepCW = not sweepCW	-- this is close enough, go in the other direction next frame
										else
											AI.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(Owner.Pos, Waypoint.Pos, false).Magnitude < Owner.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(Owner.EyePos, SceneMan:ShortestDistance(Owner.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
														Owner:RemoveMovePathBeginning()
													end
												end
											end
											
											coroutine.yield()	-- wait until next frame
										end
									end
								end
							end
						else
							digState = AHuman.NOTDIGGING
							obstacleState = Actor.PROCEEDING
							AI.deviceState = AHuman.STILL
							AI.fire = false
							Owner:EquipFirearm(true)
						end
					else	-- not digging
						if not AI.Target then
							AI.fire = false
						end
						
						-- Scan for obstacles
						local Trace = Vector(Owner.Diameter*0.85, 0):RadRotate(scanAng)
						local Free = Vector()
						local index = math.floor(scanAng*2.5+2.01)
						if SceneMan:CastObstacleRay(Owner.Pos, Trace, Vector(), Free, Owner.ID, Owner.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 AI.jump and not AI.flying then
							coroutine.yield()	-- wait until next frame
						end
						
						if CurrDist.Magnitude > Owner.Height/3 then	-- not close enough to the waypoint
							ArrivedTimer:Reset()
							
							-- check if we have LOS to the waypoint
							if SceneMan:CastObstacleRay(Owner.Pos, CurrDist, Vector(), Vector(), Owner.ID, Owner.IgnoresWhichTeam, rte.grassID, 9) < 0 then
								NoLOSTimer:Reset()
							elseif NoLOSTimer:IsPastSimTimeLimit() then	-- calulate new path
								Waypoint = nil
								WptList = nil
								
								if Owner.AIMode == Actor.AIMODE_GOLDDIG and digState == AHuman.NOTDIGGING and math.random() < 0.5 then
									return true	-- end this behavior and look for gold again
								end
							end
							
							if not AI.jump and not AI.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
							Owner:RemoveMovePathBeginning()
							Waypoint = nil
						elseif AI.refuel and Owner.Vel.Y < 5 then
							if Owner.JetTimeLeft > Owner.JetTimeTotal * 0.95 or
								(AI.flying and Owner.Vel.Y < -3 and Owner.JetTimeLeft > 0.5)
							then
								AI.refuel = false
							else
								AI.jump = false	-- wait until the jetpack is full
								nextLatMove = Actor.LAT_STILL
							end
						end
						
						if Waypoint and not AI.refuel then	-- move towards the waypoint
							nextAimAngle = Owner:GetAimAngle(false) * 0.95	-- look straight ahead
							
							-- control vertical movement
							local change
							if AI.flying then
								change = AI.YposPID:Update(CurrDist.Y+Owner.Vel.Y*3, 0)
							else
								change = AI.YposPID:Update(CurrDist.Y, 0) -- ignore our velocity if on the ground
							end
							
							if math.abs(Owner.RotAngle) < 0.5 and Owner.Vel.Y > -10 and
								(change < -1 or Owner.Vel.Y > 10)
							then
								if not AI.jump then
									AI.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
								AI.jump = false
							end
							
							-- control horisontal movement
							if AI.jump then
								if AI.flying then
									change = AI.XposPID:Update(CurrDist.X+Owner.Vel.X*3, 0)
								else
									change = AI.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 Owner.FGLeg or Owner.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 Owner.HFlipped) or (CurrDist.X > 5 and not Owner.HFlipped) then
								-- no legs, jump forward
								BurstTimer:Reset()
								AI.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 AI.jump and AI.flying and Waypoint.Type ~= "drop" and
								 Owner.Vel.Y > -5 and CurrDist.Y-Owner.Vel.Y*3 < 20	--TODO: use Ychange here?
							then
								change = math.abs(AI.XposPID:Update(CurrDist.X+Owner.Vel.X*3, 0))
								if change > 0.5 then
									AI.jump = true
								end
							end
							
							if AI.jump then
								-- obstacle right
								for i = Obst.R_FRONT, Obst.R_UP do
									if Obstacles[i] then
										if Obstacles[Obst.R_UP] then
											AI.jump = false
										else
											nextAimAngle = nextAimAngle - (nextAimAngle-1.2)*0.2	-- look up to aim jetpack down
										end
										
										break
									end
								end
								
								-- obstacle left
								for i = Obst.L_UP, Obst.L_FRONT do
									if Obstacles[i] then
										if Obstacles[Obst.L_UP] then
											AI.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, Owner, 20) then						
								-- jump over low obstacles unless we want to jump off a ledge
								if nextLatMove == Actor.LAT_RIGHT and
									(Obstacles[Obst.R_LOW] or Obstacles[Obst.R_FRONT]) and not Obstacles[Obst.R_UP]
								then
									AI.jump = true
									if Obstacles[Obst.R_HIGH] then
										nextLatMove = Actor.LAT_LEFT -- TODO: only when too close to the obstacle?
									end
								elseif nextLatMove == Actor.LAT_LEFT and
									(Obstacles[Obst.L_LOW] or Obstacles[Obst.L_FRONT]) and not Obstacles[Obst.L_UP]
								then
									AI.jump = true
									if Obstacles[Obst.L_HIGH] then
										nextLatMove = Actor.LAT_RIGHT -- TODO: only when too close to the obstacle?
									end
								end
							end
							
							if math.abs(Owner.RotAngle) > 0.5 then
								AI.jump = false
							end
						end
						
						if not AI.Target then
							Owner:SetAimAngle(nextAimAngle)
						end
					end
				end
			end
		else
			-- ignore the path-finding and plot waypoints in a straight line to the target, if there is no metal in the way
			local Trace = SceneMan:ShortestDistance(Owner.Pos, Owner:GetLastAIWaypoint(), false)
			if Owner.AIMode == Actor.AIMODE_GOLDDIG and not AI.Target and not SceneMan:CastStrengthRay(Owner.Pos, Trace, 105, Vector(), 2, 0, true) then
				WptList = {}	-- store the waypoints we want in our path here
				
				local wpts = math.ceil(Trace.Magnitude/60)
				Trace:CapMagnitude(60)
				for i = 1, wpts do
					local TmpPos = Owner.Pos + Trace * i
					table.insert(WptList, {Pos=SceneMan:MovePointToGround(TmpPos, Owner.Height/4, 4)})
				end
				
				-- create the move path seen on the screen
				for _, Wpt in pairs(WptList) do
					Owner:AddToMovePathEnd(Wpt.Pos)
				end
				
				Owner:DrawWaypoints(true)
				NoLOSTimer:Reset()
			else	-- no waypoint list, create one in several small steps to reduce lag
				Owner:DrawWaypoints(false)
				Owner:UpdateMovePath()
				coroutine.yield()	-- wait until next frame
				
				if Owner.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 Owner.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=Owner.Pos})
				local Origin
				local LastPos = PathDump[1]
				for _, WptPos in pairs(PathDump) do
					Origin = TmpWpts[#TmpWpts].Pos
					WptPos = SceneMan:MovePointToGround(WptPos, Owner.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(), Owner.ID, Owner.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, Owner.ID, Owner.IgnoresWhichTeam, rte.grassID, 2) then	-- TODO: check LOS? what if 0?
									table.insert(WptList, {Pos=Free+Vector(0,Owner.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
				Owner:ClearMovePath()
				for _, Wpt in pairs(WptList) do
					Owner:AddToMovePathEnd(Wpt.Pos)
				end
				
				Owner:DrawWaypoints(true)
				NoLOSTimer:Reset()
			end
		end
		
		-- movement commands
		if AI.Target and AI.BehaviorName ~= "AttackTarget" then
			AI.lateralMoveState = Actor.LAT_STILL
			
			if AI.flying then
				if AI.jump and BurstTimer:IsPastSimTimeLimit() then	-- trigger jetpack bursts
					BurstTimer:Reset()
					AI.jump = false
				end
			else
				AI.jump = false
			end
		else
			AI.lateralMoveState = nextLatMove
			if AI.jump and BurstTimer:IsPastSimTimeLimit() then	-- trigger jetpack bursts
				BurstTimer:Reset()
				AI.jump = false
			end
		end
		
		if AI.BlockingActor then
			if not MovableMan:IsActor(AI.BlockingActor) or
				SceneMan:ShortestDistance(Owner.Pos, AI.BlockingActor.Pos, false).Magnitude > (Owner.Diameter + AI.BlockingActor.Diameter)*1.2
			then
				AI.BlockingActor = nil
				AI.teamBlockState = Actor.NOTBLOCKED
				
				if Owner.AIMode == Actor.AIMODE_BRAINHUNT and AI.FollowingActor then
					AI.FollowingActor = nil
					break	-- end this behavior
				end
			elseif AI.teamBlockState == Actor.NOTBLOCKED and Waypoint then
				if (Waypoint.Pos.X > Owner.Pos.X and AI.BlockingActor.Pos.X > Owner.Pos.X) or
					 (Waypoint.Pos.X < Owner.Pos.X and AI.BlockingActor.Pos.X < Owner.Pos.X)
				then
					AI.teamBlockState = Actor.BLOCKED
					if Owner.AIMode == Actor.AIMODE_BRAINHUNT and Owner.AIMode == AI.BlockingActor.AIMode and
						(not AI.BlockingActor.MOMoveTarget or AI.BlockingActor.MOMoveTarget.ID ~= Owner.ID)
					then	-- don't follow an actor that is following us
						AI.FollowingActor = AI.BlockingActor
						Owner:ClearAIWaypoints()
						Owner:AddAIMOWaypoint(AI.FollowingActor)
						AI:CreateGoToBehavior(Owner)
					end
				else
					AI.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(AI, Owner, TargetPos, targetID)
	if not Owner.Head or AI.proneState == AHuman.PRONE then
		return false
	end
	
	-- only go prone if we can see the ground near the target
	local AimPoint = SceneMan:MovePointToGround(TargetPos, 10, 3)
	local ground = Owner.Pos.Y + Owner.Height * 0.25
	local Dist = SceneMan:ShortestDistance(Owner.Pos, AimPoint, false)
	local PronePos
	
	-- check if there is room to go prone here
	if Dist.X > 100 then
		-- to the right
		PronePos = Owner.EyePos + Vector(Owner.Height*0.3, 0)
		
		local x_pos = Owner.Pos.X + 10
		for _ = 1, math.ceil(Owner.Height/16) 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 = Owner.EyePos + Vector(-Owner.Height*0.3, 0)
		
		local x_pos = Owner.Pos.X - 10
		for _ = 1, math.ceil(Owner.Height/16) 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, Owner.Head.Radius+3, 2)
	Dist = SceneMan:ShortestDistance(PronePos, AimPoint, false)
	
	-- check LOS from the prone position
	--if not SceneMan:CastFindMORay(PronePos, Dist, targetID, Hit, rte.grassID, false, 8) then
	if SceneMan:CastObstacleRay(PronePos, Dist, Vector(), Vector(), targetID, Owner.IgnoresWhichTeam, rte.grassID, 9) > -1 then
		return false
	else
		-- check for obstacles more more carefully
		Dist:CapMagnitude(60)
		if SceneMan:CastObstacleRay(PronePos, Dist, Vector(), Vector(), 0, Owner.IgnoresWhichTeam, rte.grassID, 1) > -1 then
			return false
		end
	end
	
	AI.proneState = AHuman.PRONE
	if Dist.X > 0 then
		AI.lateralMoveState = Actor.LAT_RIGHT
	else
		AI.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(Owner)
	local Weapon = ToHDFirearm(Owner.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 Weapon:HasObjectInGroup("Explosive Weapons") then
			PrjDat.exp = true	-- this weapon have a blast radius
		end
		
		PrjDat.vel = Round.FireVel	-- muzzle velocity
		if Projectile.ClassName == "MOPixel" then
			-- half of the theoretical upper limit for the total amount of material strength this weapon can destroy in 250ms
			PrjDat.pen = 0.5 * Projectile.Mass * Projectile.Sharpness * PrjDat.vel * math.max((Weapon.RateOfFire / 240), 1)
		elseif Projectile.ClassName == "AEmitter" then
			PrjDat.vel = math.max(PrjDat.vel, Projectile.Sharpness)	-- AEmitters can have FireVel overriden by Sharpness
		end
		
		PrjDat.g = SceneMan.GlobalAcc.Y * 0.67 * math.max(Projectile.GlobalAccScalar, 0)	-- 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 _ = 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
	end
	
	return PrjDat
end

-- open fire on the selected target
function HumanBehaviors.ShootTarget(AI, Owner)
	if not MovableMan:IsActor(AI.Target) then
		return true
	end
	
	AI.canHitTarget = false
	AI.TargetLostTimer:SetSimTimeLimitMS(700)
	
	local LOSTimer = Timer()
	LOSTimer:SetSimTimeLimitMS(170)
	
	local ShootTimer = Timer()
	local shootDelay = RangeRand(400, 600)
	local AimPoint = AI.Target.Pos + AI.TargetOffset
	if not AI.flying and AI.Target.Vel.Largest < 4 and HumanBehaviors.GoProne(AI, Owner, AimPoint, AI.Target.ID) then
		shootDelay = shootDelay + 250
	end
	
	local PrjDat, OldWaypoint
	local openFire = 0
	local checkAim = true
	local canSwitchWeapon = Owner.InventorySize
	local TargetAvgVel = Vector(AI.Target.Vel.X, AI.Target.Vel.Y)
	local Dist = SceneMan:ShortestDistance(Owner.Pos, AimPoint, false)
	
	-- make sure we are facing the right direction
	if Owner.HFlipped then
		if Dist.X > 0 then
			Owner.HFlipped = false
		end
	elseif Dist.X < 0 then
		Owner.HFlipped = true
	end
	
	coroutine.yield()	-- wait until next frame
	
	local ErrorOffset = Vector(RangeRand(40, 80), 0):RadRotate(RangeRand(1, 6))
	if Dist.Largest < 150 then
		ErrorOffset = ErrorOffset * 0.5
	end
	
	local aimTarget = SceneMan:ShortestDistance(Owner.Pos, AimPoint+ErrorOffset, false).AbsRadAngle
	
	while true do
		if not AI.Target or AI.Target:IsDead() then
			AI.Target = nil
			
			-- the target is gone, try to find another right away
			local ClosestEnemy
			local best = math.huge
			for Act in MovableMan.Actors do
				if Act.Team ~= Owner.Team then
					local distance = SceneMan:ShortestDistance(Act.Pos, AimPoint, false).Largest
					if distance < best then
						best = distance
						ClosestEnemy = Act
					end
				end
			end
			
			if best < 200 then
				-- check if the target is inside our "screen"
				local ViewDist = SceneMan:ShortestDistance(Owner.ViewPoint, ClosestEnemy.Pos, false)
				if (math.abs(ViewDist.X) - ClosestEnemy.Radius < FrameMan.PlayerScreenWidth * 0.5) and
					(math.abs(ViewDist.Y) - ClosestEnemy.Radius < FrameMan.PlayerScreenHeight * 0.5)
				then
					if not AI.isPlayerOwned or not SceneMan:IsUnseen(ClosestEnemy.Pos.X, ClosestEnemy.Pos.Y, Owner.Team) then	-- AI-teams ignore the fog
						if SceneMan:CastStrengthSumRay(Owner.EyePos, ClosestEnemy.Pos, 6, rte.grassID) < 120 then
							AI.Target = ClosestEnemy
							AI.TargetOffset = Vector()
						end
					end
				end
			end
			
			-- no new target found
			if not AI.Target then
				if OldWaypoint then
					Owner:ClearAIWaypoints()
					Owner:AddAISceneWaypoint(OldWaypoint)
					AI:CreateGoToBehavior(Owner)
				end
				
				break
			end
		end
		
		if Owner.FirearmIsReady then
			-- it is now safe to get the ammo stats since FirearmIsReady
			local Weapon = ToHDFirearm(Owner.EquippedItem)
			if not PrjDat or PrjDat.MagazineName ~= Weapon.Magazine.PresetName then
				PrjDat = HumanBehaviors.GetProjectileData(Owner)
				
				-- uncomment these to get the range of the weapon
				--ConsoleMan:PrintString(Weapon.PresetName .. " range = " .. PrjDat.rng .. " px")
				--ConsoleMan:PrintString(AI.Target.PresetName .. " range = " .. SceneMan:ShortestDistance(Owner.Pos, AI.Target.Pos, false).Magnitude .. " px")
				
				-- Aim longer with lo-cap weapons
				if Weapon.Magazine.Capacity > -1 and Weapon.Magazine.Capacity < 7 and Dist.Largest > 150 then
					ErrorOffset = ErrorOffset * 0.6
					shootDelay = shootDelay + 500
				end
			else
				TargetAvgVel = TargetAvgVel * 0.6 + AI.Target.Vel * 0.4	-- smooth the target's velocity
				AimPoint = AI.Target.Pos + AI.TargetOffset + ErrorOffset
				
				Dist = SceneMan:ShortestDistance(Weapon.Pos, AimPoint, false)
				local range = Dist.Magnitude
				
				-- move the aimpoint towards the center of the target at close ranges
				if range < 40 then
					AI.TargetOffset = AI.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
						AI.canHitTarget = true
						if PrjDat.exp and Owner.InventorySize > 0 then	-- we have more things in the inventory
							if Owner:EquipDiggingTool(false) then
								AI:CreateHtHBehavior(Owner)
								break
							elseif canSwitchWeapon > 0 and Owner:HasObjectInGroup("Primary Weapons") or Owner:HasObjectInGroup("Secondary Weapons") then
								canSwitchWeapon = canSwitchWeapon - 1
								AI.Ctrl:SetState(Controller.WEAPON_CHANGE_NEXT, true)
								PrjDat = nil	-- get the ammo info from the new weapon the next update
								AI.canHitTarget = 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 * AI.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+(Owner.Vel*0.5+AI.Target.Vel)*timeToTarget, false)
						end
						
						aimTarget = HumanBehaviors.GetAngleToHit(PrjDat, Dist)
						if aimTarget then
							AI.canHitTarget = true
						else
							AI.canHitTarget = false
							
							-- the target is too far away; switch weapon, move closer or run away
							if canSwitchWeapon > 0 and (Owner:HasObjectInGroup("Primary Weapons") or Owner:HasObjectInGroup("Secondary Weapons")) then
								canSwitchWeapon = canSwitchWeapon - 1
								AI.Ctrl:SetState(Controller.WEAPON_CHANGE_NEXT, true)
								PrjDat = nil	-- get the ammo info from the new weapon the next update
							elseif not AI.isPlayerOwned or Owner.AIMode ~= Actor.AIMODE_SENTRY then
								if not Owner.MOMoveTarget or 
									not MovableMan:ValidMO(Owner.MOMoveTarget) or
									Owner.MOMoveTarget.RootID ~= AI.Target.RootID
								then	-- move towards the target
									OldWaypoint = Owner:GetLastAIWaypoint()
									if (OldWaypoint-Owner.Pos).Largest < 1 then
										OldWaypoint = Vector(Owner.Pos.X, Owner.Pos.Y)	-- move back here later
									end
									
									Owner:ClearAIWaypoints()
									Owner:AddAIMOWaypoint(AI.Target)
									AI:CreateGoToBehavior(Owner)
									AI.proneState = AHuman.NOTPRONE
								end
							else
								-- TODO: run away or duck?
								break
							end
						end
					elseif not AI.isPlayerOwned or Owner.AIMode ~= Actor.AIMODE_SENTRY then	-- target out of reach; move towards it
						-- check if we are already moving towards an actor
						if not Owner.MOMoveTarget or 
							not MovableMan:ValidMO(Owner.MOMoveTarget) or
							Owner.MOMoveTarget.RootID ~= AI.Target.RootID
						then	-- move towards the target
							OldWaypoint = Owner:GetLastAIWaypoint()
							if (OldWaypoint-Owner.Pos).Largest < 1 then
								OldWaypoint = Vector(Owner.Pos.X, Owner.Pos.Y)	-- move back here later
							end
							
							Owner:ClearAIWaypoints()
							Owner:AddAIMOWaypoint(AI.Target)
							AI:CreateGoToBehavior(Owner)
							AI.proneState = AHuman.NOTPRONE
							AI.canHitTarget = false
						end
					end
				else
					checkAim = true
					
					-- periodically check that we have LOS to the target
					if LOSTimer:IsPastSimTimeLimit() then
						LOSTimer:Reset()
						local TargetPoint = AI.Target.Pos + AI.TargetOffset
						
						if (range < 30 + Weapon.SharpLength + FrameMan.PlayerScreenWidth*0.5) and
							(not AI.isPlayerOwned or not SceneMan:IsUnseen(TargetPoint.X, TargetPoint.Y, Owner.Team))
						then
							if PrjDat.pen then
								if SceneMan:CastStrengthSumRay(Weapon.Pos, TargetPoint, 6, rte.grassID) * 5 < PrjDat.pen then
									AI.TargetLostTimer:Reset()	-- we can shoot at the target
								end
							else
								if SceneMan:CastStrengthSumRay(Weapon.Pos, TargetPoint, 6, rte.grassID) < 120 then
									AI.TargetLostTimer:Reset()	-- we can shoot at the target
								end
							end
						end
					end
				end
				
				if AI.canHitTarget then
					AI.lateralMoveState = Actor.LAT_STILL
					if not AI.flying then
						AI.deviceState = AHuman.AIMING
					end
				end
				
				local aim = Owner:GetAimAngle(true)			
				if AI.flying then
					aimTarget = (aimTarget or aim) + RangeRand(-0.05, 0.05)
				else
					aimTarget = (aimTarget or aim) + math.min(math.max(RangeRand(-15, 15)/(range+50), -0.1), 0.1)
				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:IsPastRealMS(shootDelay) then
					ErrorOffset = ErrorOffset * 0.97	-- reduce the aim point error
					AI.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim-math.max(math.min(angDiff*0.1, 0.25), -0.25))
					
					if AI.canHitTarget and angDiff < 0.7 then 
						if Weapon.FullAuto then	-- open fire if our aim overlap the target
							if math.abs(angDiff) < math.tanh((2*AI.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 AI.fire then	-- open fire if our aim overlap the target
							if math.abs(angDiff) < math.tanh((1.5*AI.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(), Owner.ID, Owner.IgnoresWhichTeam, rte.grassID, 2) > -1 then
								openFire = 0
							end
						end
					else
						openFire = openFire - 1
					end
				else
					ErrorOffset = ErrorOffset * 0.985	-- reduce the aim point error
					AI.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim-math.max(math.min(angDiff*0.065, 0.07), -0.07) + RangeRand(-0.04, 0.04))
					openFire = 0
				end
				
				if openFire > 0 then
					AI.fire = true
				else
					AI.fire = false
				end
			end
		else
			if Owner.EquippedItem and ToHeldDevice(Owner.EquippedItem):IsReloading() then
				ShootTimer:Reset()
				AI.Ctrl.AnalogAim = SceneMan:ShortestDistance(Owner.Pos, AI.Target.Pos, false)
			elseif Owner:EquipFirearm(true) then
				shootDelay = RangeRand(300, 400)
				AI.deviceState = AHuman.POINTING
				AI.fire = false
				
				if Owner.FirearmIsEmpty then
					Owner:ReloadFirearm()
					-- TODO: check if ducking is appropriate while reloading (when we can make the actor stand up reliably)
				end
			else
				AI:CreateGetWeaponBehavior(Owner)
				break -- no firearm avaliable
			end
		end
		
		-- make sure we are facing the right direction
		if Owner.HFlipped then
			if Dist.X > 0 then
				Owner.HFlipped = false
			end
		elseif Dist.X < 0 then
			Owner.HFlipped = true
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	return true
end

-- throw a grenade at the selected target
function HumanBehaviors.ThrowTarget(AI, Owner)
	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
	
	AI.TargetLostTimer:SetSimTimeLimitMS(1500)
	
	while true do
		if not MovableMan:IsActor(AI.Target) then
			break
		end
		
		if scan < 1 then
			if AI.Target.Door then
				AimPoint = AI.Target.Door.Pos
			else
				AimPoint = AI.Target.Pos	-- look for the center
				if AI.Target.EyePos then
					AimPoint = (AimPoint + AI.Target.EyePos) / 2
				end
			end
			
			ID = rte.NoMOID
			if Owner:IsWithinRange(Vector(AimPoint.X, AimPoint.Y)) then	-- TODO: use grenade properies to decide this
				Dist = SceneMan:ShortestDistance(Owner.EyePos, AimPoint, false)
				ID = SceneMan:CastMORay(Owner.EyePos, Dist, Owner.ID, Owner.IgnoresWhichTeam, rte.grassID, false, 3)
				if ID < 1 or ID > 254 then	-- not found, look for any head or legs
					AimPoint = AI.Target.EyePos	-- the head
					if AimPoint then
						coroutine.yield()	-- wait until next frame
						if not MovableMan:IsActor(AI.Target) then	-- must verify that the target exist after a yield
							break
						end
						
						Dist = SceneMan:ShortestDistance(Owner.EyePos, AimPoint, false)
						ID = SceneMan:CastMORay(Owner.EyePos, Dist, Owner.ID, Owner.IgnoresWhichTeam, rte.grassID, false, 3)
					end
					
					if ID < 1 or ID > 254 then
						local Legs = AI.Target.FGLeg or AI.Target.BGLeg	-- the legs
						if Legs then
							coroutine.yield()	-- wait until next frame
							if not MovableMan:IsActor(AI.Target) then	-- must verify that the target exist after a yield
								break
							end
							
							AimPoint = Legs.Pos
							Dist = SceneMan:ShortestDistance(Owner.EyePos, AimPoint, false)
							ID = SceneMan:CastMORay(Owner.EyePos, Dist, Owner.ID, Owner.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 ~= AI.Target.ID then
					MO = MovableMan:GetMOFromID(rootID)
					if MovableMan:IsActor(MO) then
						if MO.Team ~= Owner.Team then
							if MO.ClassName == "AHuman" then
								AI.Target = ToAHuman(MO)
							elseif MO.ClassName == "ACrab" then
								AI.Target = ToACrab(MO)
							elseif MO.ClassName == "ACRocket" then
								AI.Target = ToACRocket(MO)
							elseif MO.ClassName == "ACDropShip" then
								AI.Target = ToACDropShip(MO)
							elseif MO.ClassName == "ADoor" then
								AI.Target = ToADoor(MO)
							elseif MO.ClassName == "Actor" then
								AI.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 Owner.ThrowableIsReady then
					aim = HumanBehaviors.GetGrenadeAngle(AimPoint, AI.Target.Vel/RangeRand(0.5, 1.75), ToHeldDevice(Owner.EquippedItem).MuzzlePos, 18)
					if aim then
						AI.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
						AI.fire = true
					else
						ThrowTimer:Reset()
						AI.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(AI, Owner)
	if not AI.Target or not MovableMan:IsActor(AI.Target) then
		return true
	end
	
	AI.TargetLostTimer:SetSimTimeLimitMS(5000)
	
	-- move towards the target
	local OldWaypoint
	if not Owner.MOMoveTarget or 
		not MovableMan:ValidMO(Owner.MOMoveTarget) or
		Owner.MOMoveTarget.RootID ~= AI.Target.RootID
	then
		OldWaypoint = Owner:GetLastAIWaypoint()
		if (OldWaypoint-Owner.Pos).Largest < 1 then
			OldWaypoint = nil
		end
		
		Owner:ClearAIWaypoints()
		Owner:AddAIMOWaypoint(AI.Target)
		AI:CreateGoToBehavior(Owner)
	end
	
	local HD
	while true do
		coroutine.yield()	-- wait until next frame
		
		if not AI.Target or not MovableMan:IsActor(AI.Target) then
			Owner:ClearAIWaypoints()
			if OldWaypoint then	-- move towards the old wpt
				Owner:AddAISceneWaypoint(OldWaypoint)
				AI:CreateGoToBehavior(Owner)
			end

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


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


function HumanBehaviors.GetAngleToHit(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(AI, Owner)
	if not MovableMan:IsActor(AI.UnseenTarget) or not Owner.FirearmIsReady then
		return true
	end
	
	-- see if we can shoot from the prone position
	local ShootTimer = Timer()
	local aimTime = RangeRand(100, 300)
	if not AI.flying and AI.UnseenTarget.Vel.Largest < 12 and HumanBehaviors.GoProne(AI, Owner, AI.UnseenTarget.Pos, AI.UnseenTarget.ID) then
		aimTime = aimTime + 500
	end
	
	local StartPos = Vector(AI.UnseenTarget.Pos.X, AI.UnseenTarget.Pos.Y)
	
	-- aim at the target in case we can see it when sharp aiming
	Owner:SetAimAngle(SceneMan:ShortestDistance(Owner.EyePos, StartPos, false).AbsRadAngle)
	AI.deviceState = AHuman.AIMING
	
	-- aim for ~160ms
	for _ = 1, 10 do
		coroutine.yield()	-- wait until next frame
	end
	
	if not Owner.FirearmIsReady then
		return true
	end
	
	local AimPoint
	for _ = 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(Owner.EyePos, AimPoint, false)
		local rayLenght = SceneMan:CastObstacleRay(Owner.EyePos, Trace, Vector(), Vector(), rte.NoMOID, Owner.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 Owner.FirearmIsReady then
		return true
	end
	
	local aim
	local PrjDat = HumanBehaviors.GetProjectileData(Owner)
	local Dist = SceneMan:ShortestDistance(Owner.EquippedItem.Pos, AimPoint, false)
	if Dist.Magnitude < PrjDat.rng then
		aim = HumanBehaviors.GetAngleToHit(PrjDat, Dist)
	else
		return true	-- target out of range
	end
	
	local CheckTargetTimer = Timer()
	local aimError = RangeRand(-0.25, 0.25)
	
	while aim do
		if Owner.FirearmIsReady then
			AI.deviceState = AHuman.AIMING
			AI.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim+aimError+RangeRand(-0.02, 0.02))
			
			if ShootTimer:IsPastSimMS(aimTime) then
				AI.fire = true
				aimError = aimError * 0.985
			else
				AI.fire = false
			end
		else
			AI.deviceState = AHuman.POINTING
			AI.fire = false
			
			ShootTimer:Reset()
			if Owner.FirearmIsEmpty then
				Owner:ReloadFirearm()
			end
			
			break -- stop this behavior when the mag is empty
		end
		
		coroutine.yield()	-- wait until next frame
		
		if AI.UnseenTarget and CheckTargetTimer:IsPastSimMS(400) then
			if MovableMan:IsActor(AI.UnseenTarget) and (AI.UnseenTarget.ClassName == "AHuman" or AI.UnseenTarget.ClassName == "ACrab") then
				CheckTargetTimer:Reset()
				if AI.UnseenTarget:GetController():IsState(Controller.WEAPON_FIRE) then
					-- compare the enemy aim angle with the angle of the alarm vector
					local enemyAim = AI.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(AI.UnseenTarget.Pos, Owner.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 = AI.UnseenTarget.Pos + SceneMan:ShortestDistance(AI.UnseenTarget.Pos, AimPoint, false) / 2 + Vector(RangeRand(-30, 30), RangeRand(-30, 30))
						aimError = RangeRand(-0.15, 0.15)
						
						Dist = SceneMan:ShortestDistance(Owner.EquippedItem.Pos, AimPoint, false)
						if Dist.Magnitude < PrjDat.rng then
							aim = HumanBehaviors.GetAngleToHit(PrjDat, Dist)
						end
					end
				end
			else
				AI.UnseenTarget = nil
			end
		end
	end
	
	return true
end

-- look at the alarm event
function HumanBehaviors.FaceAlarm(AI, Owner)
	local TargetPoint = AI.AlarmPos or AI.OldTargetPos
	if TargetPoint then
		local AlarmDist = SceneMan:ShortestDistance(Owner.EyePos, TargetPoint, false)
		for _ = 1, math.ceil(400/TimerMan.DeltaTimeMS) do
			AI.deviceState = AHuman.AIMING
			AI.lateralMoveState = Actor.LAT_STILL
			AI.Ctrl.AnalogAim = AlarmDist
			coroutine.yield()	-- wait until next frame
		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
