airID = 0 goldID = 2 doorID = 4 -- Xenocronium grassID = 128 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} WptType = {LAST = 0, UNKNOWN = 1, DROP = 2, AIR = 3} HumanAI = {} function Create(self) ---------------- AI variables start ---------------- self.Ctrl = self:GetController() self.lateralMoveState = Actor.LAT_STILL self.jumpState = AHuman.NOTJUMPING self.deviceState = AHuman.STILL self.lastAIMode = Actor.AIMODE_NONE self.teamBlockState = Actor.NOTBLOCKED self.SentryFacing = self.HFlipped self.fire = false self.HTimer = Timer() self.AirTimer = Timer() self.PickUpTimer = Timer() self.ReloadTimer = Timer() self.BlockedTimer = Timer() self.TargetLostTimer = Timer() self.PlayerInterferedTimer = Timer() self.PlayerInterferedTimer:SetSimTimeLimitMS(500) self.AlarmTimer = Timer() self.AlarmTimer:SetSimTimeLimitMS(500) -- check if this team is controlled by a human local Activ = ActivityMan:GetActivity() for player = Activity.PLAYER_1, Activity.MAXPLAYERCOUNT - 1 do if Activ:PlayerActive(player) and Activ:PlayerHuman(player) then if self.Team == Activ:GetTeamOfPlayer(player) then self.isPlayerOwned = true break end end end function self:LookForTargets() local Origin local viewAng if self.deviceState == AHuman.AIMING and self.FirearmIsReady then Origin = ToHeldDevice(self.EquippedItem).MuzzlePos viewAng = 0.1 elseif self.deviceState == AHuman.POINTING then Origin = self.EyePos viewAng = 0.3 elseif self.deviceState == AHuman.THROWING then Origin = self.EyePos viewAng = 0.4 else Origin = self.EyePos viewAng = 0.7 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, grassID, false, 5) if ID < 255 then return MovableMan:GetMOFromID(ID) end return nil end -- functions that create behaviors. the default behaviors are stored in the HumanAI table. store your custom behaviors in a table to avoid name conflicts between mods. function self:CreateSentryBehavior() if not self.FirearmIsReady and not self.ThrowableIsReady then return end self.NextBehavior = coroutine.create(HumanAI.Sentry) -- replace "HumanAI.Sentry" with the function name of your own sentry behavior self.NextCleanup = nil self.NextBehaviorName = "Sentry" end function self:CreatePatrolBehavior() self.NextBehavior = coroutine.create(HumanAI.Patrol) self.NextCleanup = nil self.NextBehaviorName = "Patrol" end function self:CreateGoldDigBehavior() self.NextBehavior = coroutine.create(HumanAI.GoldDig) self.NextCleanup = nil self.NextBehaviorName = "GoldDig" end function self:CreateBrainSearchBehavior() self.NextBehavior = coroutine.create(HumanAI.BrainSearch) self.NextCleanup = nil self.NextBehaviorName = "BrainSearch" end function self:CreateGetToolBehavior() self.NextBehavior = coroutine.create(HumanAI.ToolSearch) self.NextCleanup = nil self.NextBehaviorName = "ToolSearch" end function self:CreateGetWeaponBehavior() self.NextBehavior = coroutine.create(HumanAI.WeaponSearch) self.NextCleanup = nil self.NextBehaviorName = "WeaponSearch" end function self:CreateGoToBehavior() self.NextGoTo = coroutine.create(HumanAI.GoToWpt) self.NextGoToCleanup = function(self) self.lateralMoveState = Actor.LAT_STILL self.deviceState = AHuman.STILL self.crawling = false self.jump = false self.fire = false self:EquipFirearm(true) self:UpdateMovePath() end self.NextGoToName = "GoToWpt" end function self:CreateMoveAroundBehavior() self.NextGoTo = coroutine.create(HumanAI.MoveAroundActor) self.NextGoToCleanup = function(self) self.lateralMoveState = Actor.LAT_STILL self.jump = false end self.NextGoToName = "MoveAroundActor" end function self:CreateAttackBehavior() if self:EquipFirearm(true) then self.NextBehavior = coroutine.create(HumanAI.ShootTarget) self.NextBehaviorName = "ShootTarget" elseif self:EquipThrowable(true) then self.NextBehavior = coroutine.create(HumanAI.ThrowTarget) self.NextBehaviorName = "ThrowTarget" elseif self:EquipDiggingTool(true) then self.NextBehavior = coroutine.create(HumanAI.DigTarget) self.NextBehaviorName = "DigTarget" else return -- unarmed TODO: start run away behavior here? end self.NextCleanup = function(self) self.fire = false self.Target = nil self.deviceState = AHuman.STILL end end -- the controller class PIDController = {} PIDController.mt = {__index = PIDController} function PIDController:New(p_in, i_in, d_in, last_in, integral_in) return setmetatable( { p = p_in, i = i_in, d = d_in, last = last_in, integral = integral_in or 0 }, PIDController.mt) end function PIDController:Update(input, target) local err = input - target local change = input - self.last self.last = input self.integral = self.integral + err self.integral = math.min(math.max(self.integral, -100), 100) return self.p*err + self.i*self.integral + self.d*change end -- the controllers self.XposPID = PIDController:New(0.04, 0.00001, 0.4, 0) self.YposPID = PIDController:New(0.05, 0.0001, 0.2, 0) ---------------- AI variables end ---------------- end function Update(self) if self.Health < 100 and self.HTimer:IsPastSimMS(250) then self.Health = self.Health + 1; self.HTimer:Reset(); end end function UpdateAI(self) if self.PlayerInterferedTimer:IsPastSimTimeLimit() then self.Behavior = nil -- remove the current behavior if self.BehaviorCleanup then self.BehaviorCleanup(self) -- clean up after the current behavior self.BehaviorCleanup = nil end self.GoToBehavior = nil if self.GoToCleanup then self.GoToCleanup(self) self.GoToCleanup = nil end self.Target = nil self.PickupHD = nil self.BlockingActor = nil self.FollowingActor = nil self.fire = false self.jump = false self.crawling = false self.deviceState = AHuman.STILL self.lastAIMode = Actor.AIMODE_NONE self.teamBlockState = Actor.NOTBLOCKED end self.PlayerInterferedTimer:Reset() -- switch to the next behavior, if avaliable if self.NextBehavior then if self.BehaviorCleanup then self.BehaviorCleanup(self) end self.Behavior = self.NextBehavior self.BehaviorCleanup = self.NextCleanup self.BehaviorName = self.NextBehaviorName self.NextBehavior = nil end -- switch to the next GoTo behavior, if avaliable if self.NextGoTo then if self.GoToCleanup then self.GoToCleanup(self) end self.GoToBehavior = self.NextGoTo self.GoToCleanup = self.NextGoToCleanup self.NextGoTo = nil end -- check if the AI mode has changed or if we need a new behavior if self.AIMode ~= self.lastAIMode or (not self.Behavior and not self.GoToBehavior) then self.Behavior = nil if self.BehaviorCleanup then self.BehaviorCleanup(self) -- stop the current behavior self.BehaviorCleanup = nil end self.GoToBehavior = nil if self.GoToCleanup then self.GoToCleanup(self) self.GoToCleanup = nil end -- select a new behavior based on AI mode if self.AIMode == Actor.AIMODE_GOTO then self:CreateGoToBehavior() elseif self.AIMode == Actor.AIMODE_BRAINHUNT then self:CreateBrainSearchBehavior() elseif self.AIMode == Actor.AIMODE_GOLDDIG then self:CreateGoldDigBehavior() elseif self.AIMode == Actor.AIMODE_PATROL then self:CreatePatrolBehavior() else if self.AIMode ~= self.lastAIMode and self.AIMode == Actor.AIMODE_SENTRY then self.SentryFacing = self.HFlipped -- store the direction in which we should be looking end self:CreateSentryBehavior() end self.lastAIMode = self.AIMode end -- check if the legs reach the ground if self.AirTimer:IsPastSimMS(250) then self.AirTimer:Reset() if -1 < SceneMan:CastObstacleRay(self.Pos, Vector(0, self.Height/4), Vector(), Vector(), self.ID, grassID, 3) then self.flying = false else self.flying = true end end -- look for targets local FoundMO = self:LookForTargets() if FoundMO then if self.Target and MovableMan:IsActor(self.Target) and FoundMO.RootID == self.Target.RootID then -- found the same target self.TargetLostTimer:Reset() self.ReloadTimer:Reset() self.TargetOffset = (self.TargetOffset + SceneMan:ShortestDistance(self.Target.Pos, SceneMan:GetLastRayHitPos(), false))/2 else local MO = MovableMan:GetMOFromID(FoundMO.RootID) if MovableMan:IsActor(MO) then if MO.Team == self.Team then self.Target = nil -- stop shooting if MO.ClassName ~= "ADoor" and SceneMan:ShortestDistance(self.Pos, MO.Pos, false).Magnitude < self.Diameter + MO.Diameter then self.BlockingActor = ToActor(MO) end else if MO.ClassName == "AHuman" then MO = ToAHuman(MO) elseif MO.ClassName == "ACrab" then MO = ToACrab(MO) elseif MO.ClassName == "ACRocket" then MO = ToACRocket(MO) elseif MO.ClassName == "ACDropShip" then MO = ToACDropShip(MO) elseif MO.ClassName == "ADoor" then MO = ToADoor(MO) elseif MO.ClassName == "Actor" then MO = ToActor(MO) else MO = nil end if MO then self.TargetLostTimer:Reset() self.ReloadTimer:Reset() if not self.Target then self.OldTargetPos = nil self.Target = MO self.TargetOffset = SceneMan:ShortestDistance(self.Target.Pos, SceneMan:GetLastRayHitPos(), false) -- this it the distance vector from the target center to the point we hit with our ray self.TargetLostTimer:SetSimTimeLimitMS(1000) self:CreateAttackBehavior() --else -- TODO: check if this target have higher priority end end end end end else if self.Target then if MovableMan:IsActor(self.Target) then if self.TargetLostTimer:IsPastSimTimeLimit() then self.OldTargetPos = self.Target.Pos self.Target = nil -- the target has been out of sight for a too long, ignore it end else self.Target = nil end end if self.ReloadTimer:IsPastSimMS(8000) then -- check if we need to reload self.ReloadTimer:Reset() if ToAHuman(self).FirearmNeedsReload then self:ReloadFirearm() end end end -- run the move behavior and delete it if it returns true if self.GoToBehavior then local msg, done = coroutine.resume(self.GoToBehavior, self) if not msg then print(self.PresetName .. " GoToBehavior error:\n" .. done) -- print the error message done = true end if done then self.GoToBehavior = nil if self.GoToCleanup then self.GoToCleanup(self) self.GoToCleanup = nil end end elseif self.flying and not self.jump and self.Vel.Y > 7 then self.Ctrl:SetState(Controller.BODY_JUMP, true) end -- run the selected behavior and delete it if it returns true if self.Behavior then local msg, done = coroutine.resume(self.Behavior, self) if not msg then print(self.PresetName .. " behavior " .. self.BehaviorName .. " error:\n" .. done) -- print the error message done = true end if done then self.Behavior = nil if self.BehaviorCleanup then self.BehaviorCleanup(self) self.BehaviorCleanup = nil end if not self.PickupHD and not self.NextBehavior and self.PickUpTimer:IsPastSimMS(500) then self.PickUpTimer:Reset() if not self:EquipFirearm(false) then self:CreateGetWeaponBehavior() elseif self.AIMode ~= Actor.AIMODE_SENTRY and not self:EquipDiggingTool(false) then self:CreateGetToolBehavior() end end end elseif self.Target and self.EquippedItem and ToHeldDevice(self.EquippedItem):IsTool() then -- attack with digger -- TODO: move this to a separate behavior if MovableMan:IsActor(self.Target) then local Dist = SceneMan:ShortestDistance(ToHeldDevice(self.EquippedItem).MuzzlePos, self.Target.Pos, false) if Dist.Magnitude < 50 then self.Ctrl.AnalogAim = Dist.Normalized self.Ctrl:SetState(Controller.WEAPON_FIRE, true) -- fire digger end else self.Target = nil end elseif not self.PickupHD and not self.NextBehavior and self.PickUpTimer:IsPastSimMS(500) then self.PickUpTimer:Reset() if not self:EquipFirearm(false) then self:CreateGetWeaponBehavior() elseif self.AIMode ~= Actor.AIMODE_SENTRY and not self:EquipDiggingTool(false) then self:CreateGetToolBehavior() end end if self.PickupHD then -- there is a HeldDevice we want to pick up if not MovableMan:IsDevice(self.PickupHD) or self.PickupHD.ID ~= self.PickupHD.RootID then self.PickupHD = nil -- the HeldDevice has been destroyed or picked up self:ClearAIWaypoints() if self.PrevAIWaypoint or self.FollowingActor then if self.FollowingActor and MovableMan:IsActor(self.FollowingActor) then -- what if the old destination was a moving actor? self:AddAIMOWaypoint(self.FollowingActor) self:CreateGoToBehavior() -- continue towards our old destination else self.FollowingActor = nil if self.PrevAIWaypoint then self:AddAISceneWaypoint(self.PrevAIWaypoint) self:CreateGoToBehavior() -- continue towards our old destination end end end else if SceneMan:ShortestDistance(self.Pos, self.PickupHD.Pos, false).Magnitude < self.Height then self.Ctrl:SetState(Controller.WEAPON_PICKUP, true) end end end -- react to relevant AlarmEvents if self.AlarmTimer:IsPastSimTimeLimit() then self.AlarmPos = nil for Event in MovableMan.AlarmEvents do if Event.Team ~= self.Team then -- caused by some other team's activites - alarming! local AlarmVec = SceneMan:ShortestDistance(self.EyePos, Event.ScenePos, false) -- see how far away the alarm situation is -- only react if the alarm is within range and we are perceptive enough to hear it if AlarmVec.Largest <= Event.Range then -- * self.Perceptiveness then -- if this is the same alarm location as last, then don't repeat the signal if not self.LastAlarmPos or SceneMan:ShortestDistance(self.LastAlarmPos, Event.ScenePos, false).Largest > 10 then -- now check if we have line of sight to the alarm point -- don't check all the way to the target, we are checking for no obstacles, and target will be an obstacle in itself if SceneMan:CastObstacleRay(self.EyePos, AlarmVec*0.9, Vector(), Vector(), self.ID, grassID, 7) < 0 then self.LastAlarmPos = self.AlarmPos self.AlarmPos = Vector(Event.ScenePos.X, Event.ScenePos.Y) self.AlarmTimer:Reset() break end end end end end elseif self.AlarmPos then -- if alarmed, look at the alarming point until AlarmTimer has expired -- point in the direciton we heard the alarm come from if we're not already engaging a target if not (self.Target and (self.deviceState == AHuman.AIMING or self.deviceState == AHuman.FIRING or self.deviceState == AHuman.THROWING)) then -- look/point in the direction of alarm self.deviceState = AHuman.POINTING self.Ctrl.AnalogAim = SceneMan:ShortestDistance(self.EyePos, self.AlarmPos, false).Normalized self.lateralMoveState = Actor.LAT_STILL self.jump = false end -- if we're unarmed, hit the deck! if not self:EquipFirearm(true) and not self:EquipThrowable(true) and not self:EquipDiggingTool(true) then self.Ctrl:SetState(Controller.BODY_CROUCH, true) -- TODO: run away behavior end end if self.teamBlockState == Actor.IGNORINGBLOCK then if self.BlockedTimer:IsPastSimMS(10000) then self.teamBlockState = Actor.NOTBLOCKED end elseif self.teamBlockState == Actor.BLOCKED then -- we are blocked by a teammate, stop self.lateralMoveState = Actor.LAT_STILL self.jump = false if self.BlockedTimer:IsPastSimMS(20000) then self.BlockedTimer:Reset() self.teamBlockState = Actor.IGNORINGBLOCK end else self.BlockedTimer:Reset() end -- controller states if self.fire then self.Ctrl:SetState(Controller.WEAPON_FIRE, true) end if self.deviceState == AHuman.AIMING then self.Ctrl:SetState(Controller.AIM_SHARP, true) end if self.crawling then self.Ctrl:SetState(Controller.BODY_CROUCH, true) end if self.jump then if self.jumpState == AHuman.PREJUMP then self.jumpState = AHuman.UPJUMP elseif self.jumpState ~= AHuman.UPJUMP then self.jumpState = AHuman.PREJUMP end if self.JetTimeLeft < self.JetTimeTotal * 0.05 then -- TODO: move this to gotowpt? self.refuel = true end else self.jumpState = AHuman.NOTJUMPING end if self.jumpState == AHuman.PREJUMP then self.Ctrl:SetState(Controller.BODY_JUMPSTART, true) self.Ctrl:SetState(Controller.BODY_JUMP, false) elseif self.jumpState == AHuman.UPJUMP then self.Ctrl:SetState(Controller.BODY_JUMPSTART, false) self.Ctrl:SetState(Controller.BODY_JUMP, true) end if self.lateralMoveState == Actor.LAT_LEFT then self.Ctrl:SetState(Controller.MOVE_LEFT, true) self.Ctrl:SetState(Controller.MOVE_RIGHT, false) elseif self.lateralMoveState == Actor.LAT_RIGHT then self.Ctrl:SetState(Controller.MOVE_LEFT, false) self.Ctrl:SetState(Controller.MOVE_RIGHT, true) end end -- finds an aming angle that compensate for projectile drop caused by gravity function HumanAI.GetHitAngle(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 -- * self.Magazine.Ammo.Round.Particle.GlobalAccScalar 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 ----------------------- ------ Behaviors ------ ----------------------- function HumanAI.Sentry(self) -- in sentry behavior the agent only looks for new enemies, it sometimes sharp aims to increse spotting range local sweepUp = true local sweepDone = false local maxAng = 1.4 local minAng = -1.4 local aim 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 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 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 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 HumanAI.Patrol(self) -- TODO: move around local TurnTimer = Timer() local turn = 3000 while true do if TurnTimer:IsPastSimMS(turn) then TurnTimer:Reset() turn = RangeRand(2000, 6000) self:SetAimAngle(RangeRand(-0.7, 0.7)) self.HFlipped = not self.HFlipped end coroutine.yield() -- wait until next frame end return true end function HumanAI.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, goldID, GoldPos, 2, true) then coroutine.yield() -- wait until next frame local str = SceneMan:CastStrengthSumRay(self.EyePos, self.EyePos+LookVec, 4, 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() end return true end -- find the closest enemy brain function HumanAI.BrainSearch(self) local Activ = ActivityMan:GetActivity() 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 for player = Activity.PLAYER_1, Activity.MAXPLAYERCOUNT - 1 do if Activ:PlayerActive(player) and Activ:GetTeamOfPlayer(player) ~= self.Team then local Target = Activ: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 = 0 -- estimate the walking distance to the target for WptPos in self.MovePath do waypoints = waypoints + 1 end 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 HumanAI.WeaponSearch(self) local range, minDist, HD local Devices = {} if self.isPlayerOwned then minDist = 70 -- 3.5m else minDist = FrameMan.PlayerScreenWidth * 0.4 end for Item in MovableMan.Items do -- store all HeldDevices of the correct type and within a cerain range in a table if Item.ClassName ~= "TDExplosive" then HD = ToHeldDevice(Item) if HD:IsWeapon() and HD.Vel.Magnitude < 2 then range = SceneMan:ShortestDistance(self.Pos, HD.Pos, false).Magnitude if range < minDist then table.insert(Devices, HD) end 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() range = SceneMan:ShortestDistance(self.Pos, PrevWpt, false).Magnitude if range < 1 then PrevWpt = nil -- not a valid waypoint end end end if self.isPlayerOwned then minDist = 5 else minDist = 20 end for _, Item in pairs(Devices) do if MovableMan:IsDevice(Item) then self:ClearAIWaypoints() self:AddAISceneWaypoint(Item.Pos) self:UpdateMovePath() local waypoints = 0 -- estimate the walking distance to the item for _ in self.MovePath do waypoints = waypoints + 1 end 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 -- find a tool to pick up function HumanAI.ToolSearch(self) local range, minDist, HD local Devices = {} if self.AIMode == Actor.AIMODE_GOLDDIG then minDist = FrameMan.PlayerScreenWidth * 0.5 elseif self.isPlayerOwned then minDist = 70 -- 3.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:IsTool() and HD.Vel.Magnitude < 2 then range = SceneMan:ShortestDistance(self.Pos, HD.Pos, false).Magnitude if range < minDist 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() range = SceneMan:ShortestDistance(self.Pos, PrevWpt, false).Magnitude if range < 1 then PrevWpt = nil -- not a valid 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 = 0 -- estimate the walking distance to the item for _ in self.MovePath do waypoints = waypoints + 1 end 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 HumanAI.GoToWpt(self) local Destination = self:GetLastAIWaypoint() if SceneMan:ShortestDistance(Destination, self.Pos, false).Magnitude < 10 then self:ClearAIWaypoints() self.AIMode = Actor.AIMODE_SENTRY return true end local Lower = function(Y1, Y2, margin) return Y1.Pos.Y - margin > Y2.Pos.Y end 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 StuckTimer = Timer() StuckTimer:SetSimTimeLimitMS(2000) local groundDist = self.Height / 5 local nextLatMove = self.lateralMoveState local nextAimAngle = self:GetAimAngle(false) * 0.8 local scanMOs = 0 local scanAng = 0 -- for obstacle detection local Obstacles = {} local PrevWptPos = self.Pos local sweepCW = true local sweepRange = 0 local sweepMin = 0 local sweepMax = 0 local digState = AHuman.NOTDIGGING local obstacleState = Actor.PROCEEDING local WptList, Waypoint, Dist, CurrDist while true do while not self.flying and self.Target and (self:EquipFirearm(false) or self:EquipThrowable(false)) do -- don't move around if we have something to shoot at coroutine.yield() -- wait until next frame end if self.Vel.Magnitude > 2 then StuckTimer:Reset() end if not self.flying and UpdatePathTimer:IsPastSimMS(5000) 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 < self.JetTimeTotal * 0.1 then -- out of fuel self.jump = false 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 nextLatMove = Actor.LAT_STILL if self.JetTimeLeft > self.JetTimeTotal * 0.9 then self.jump = true BurstTimer:SetSimTimeLimitMS(90) -- 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 break else if not Waypoint then -- get the next waypoint in the list -- if we are following someone we must update the path more often if self.MOMoveTarget and MovableMan:IsActor(self.MOMoveTarget) and SceneMan:ShortestDistance(self:GetLastAIWaypoint(), self.MOMoveTarget.Pos, false).Magnitude > 40 then -- the target has moved WptList = nil Waypoint = nil 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(20, -50) else CornerPos = CornerPos + Vector(-20, -50) 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, grassID, 3) if pixels == 0 then break -- the waypoint is inside terrain, plot a new path elseif pixels > 20 then CornerPos = NextWptPos + Dist:CapMagnitude(pixels-20) -- compensate for obstacles elseif pixels > 0 then CornerPos = Vector(NextWptPos.X, NextWptPos.Y) -- the corner is too close to an obstacle, don't move it 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(), Free, self.ID, grassID, 4) then -- we are blocked CornerPos.X = self.Pos.X coroutine.yield() -- wait until next frame -- check if we have LOS again Dist = SceneMan:ShortestDistance(self.Pos, CornerPos, false) if 0 <= SceneMan:CastObstacleRay(self.Pos, Dist, Vector(), Free, self.ID, grassID, 4) then -- we are still blocked, make a guess CornerPos = NextWptPos + Vector(RangeRand(-20,20), RangeRand(-10,10)) end end coroutine.yield() -- wait until next frame Waypoint = {Pos=CornerPos, Type=WptType.AIR} if WptList[1].Type == WptType.UNKNOWN then -- remove the waypoint after the corner if possible table.remove(WptList, 1) -- TODO: use self:RemoveMovePathBeginning() to clean up the graphical representation of the path end self:AddToMovePathBeginning(Waypoint.Pos) else Waypoint = table.remove(WptList, 1) if #WptList > 0 then self:RemoveMovePathBeginning() end if Waypoint.Type ~= WptType.AIR then local Free = Vector() -- only if we have a digging tool if Waypoint.Type ~= WptType.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, 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, doorID, true) or SceneMan:CastStrengthRay(TopHeadPos, Heading, 5, Free, 4, 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 Waypoint.Type == WptType.AIR then ArrivedTimer:SetSimTimeLimitMS(25) else ArrivedTimer:SetSimTimeLimitMS(50) end end elseif #WptList > 1 then -- check if some other waypoint is closer local test = math.random(1, #WptList) local RandomWpt = WptList[test] if RandomWpt then Dist = SceneMan:ShortestDistance(self.Pos, RandomWpt.Pos, false) local mag = Dist.Magnitude if mag < 50 and mag*1.5 < SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false).Magnitude then -- this waypoint is closer, check LOS if -1 == SceneMan:CastObstacleRay(self.Pos, Dist, Vector(), Vector(), self.ID, 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) -- digging if digState ~= AHuman.NOTDIGGING then if not self.Target and self:EquipDiggingTool(true) then -- switch to the digger if we have one if ToAHuman(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, 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, 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, doorID, true) and not SceneMan:CastStrengthRay(self.EyePos, SceneMan:ShortestDistance(self.EyePos, Waypoint.Pos, false), 5, Vector(), 1, 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 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 -1 < SceneMan:CastObstacleRay(self.Pos, Trace, Vector(), Free, self.ID, grassID, 3) 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 coroutine.yield() -- wait until next frame if CurrDist.Magnitude > 20 then ArrivedTimer:Reset() -- not close enough end if ArrivedTimer:IsPastSimTimeLimit() then PrevWptPos = Waypoint.Pos Waypoint = nil -- only remove a waypoint if we have been close to it for a while elseif self.refuel and self.Vel.Y < 5 then if self.JetTimeLeft > self.JetTimeTotal * 0.95 then self.refuel = false else self.jump = false -- wait until the jetpack is full nextLatMove = Actor.LAT_STILL end else -- move towards the waypoint nextAimAngle = self:GetAimAngle(false) * 0.8 -- 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 BurstTimer:Reset() -- this burst last until the BurstTimer expire end if change < -6 then BurstTimer:SetSimTimeLimitMS(90) elseif change < -4 then BurstTimer:SetSimTimeLimitMS(120) elseif change < -2 or self.Vel.Y > 20 then BurstTimer:SetSimTimeLimitMS(150) else BurstTimer:SetSimTimeLimitMS(500) end self.jump = true 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 = 1.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 -- must fire thrusters to move sideways when in the air if not self.jump and self.flying and Waypoint.Type ~= WptType.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 if not self.jump then BurstTimer:Reset() -- this burst last until the BurstTimer expire end if change < -5 then BurstTimer:SetSimTimeLimitMS(90) elseif change < -2 then BurstTimer:SetSimTimeLimitMS(120) else BurstTimer:SetSimTimeLimitMS(300) end self.jump = true end end if self.jump then -- obstacle right for i = Obst.R_FRONT, Obst.R_UP do if Obstacles[i] then if Obstacles[Obst.R_UP] then self.jump = false else nextAimAngle = 1.2 -- look up 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 self.jump = false else nextAimAngle = 1.2 -- look up end break end end elseif Waypoint.Type ~= WptType.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[Obst.R_LOW] or Obstacles[Obst.R_FRONT]) and not Obstacles[Obst.R_UP] then self.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 self.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(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 local TmpList = {} table.insert(TmpList, {Pos=self.Pos, Type=WptType.UNKNOWN}) self:UpdateMovePath() coroutine.yield() -- wait until next frame for WptPos in self.MovePath do -- skip any waypoint too close to the previous one if SceneMan:ShortestDistance(TmpList[#TmpList].Pos, WptPos, false).Magnitude > 10 then table.insert(TmpList, {Pos=WptPos, Type=WptType.UNKNOWN}) end end if #TmpList < 3 then Dist = nil if TmpList[2] then Dist = SceneMan:ShortestDistance(TmpList[2].Pos, self.Pos, false) end -- already at the target if not Dist or Dist.Magnitude < 25 then self:ClearMovePath() break end end coroutine.yield() -- wait until next frame -- add information about when to jump up and down local prune = {1} -- always remove the first waypoint (self.Pos) for i = 2, #TmpList do if Lower(TmpList[i], TmpList[i-1], 30) then -- scan for sharp drops TmpList[i].Type = WptType.DROP for j = i+1, #TmpList do -- look for other side local gap = math.abs(SceneMan:ShortestDistance(TmpList[i-1].Pos, TmpList[j].Pos, false).X) if gap > 250 then break -- too far end -- look for a landing point if not Higher(TmpList[i-1], TmpList[j], 10) and not Lower(TmpList[i-1], TmpList[j], 10) then -- check if we can jump local Trace = SceneMan:ShortestDistance(TmpList[i-1].Pos, TmpList[j].Pos, false) if -1 == SceneMan:CastObstacleRay(TmpList[i-1].Pos, Trace, Vector(), Vector(), self.ID, grassID, 4) then -- delete any waypoints in between, except one for k = i+1, j-1 do table.insert(prune, k) end local TestPos = (TmpList[i-1].Pos + TmpList[j].Pos) / 2 local Free = Vector() if 0 ~= SceneMan:CastObstacleRay(TestPos, Vector(0,-gap/3), Vector(), Free, self.ID, grassID, 2) then -- TODO: check LOS? what if 0? TmpList[i].Pos = Free TmpList[i].Type = WptType.AIR end end coroutine.yield() -- wait until next frame break end end end end coroutine.yield() -- wait until next frame -- delete waypoints in reverse order, if marked for pruning table.sort(prune, function(a,b) return a > b end) for k = 1, #prune do table.remove(TmpList, prune[k]) end if #TmpList > 0 then TmpList[#TmpList].Type = WptType.LAST end WptList = TmpList -- create the move path seen on the screen self:ClearMovePath() for _, Wpt in pairs(TmpList) do self:AddToMovePathEnd(Wpt.Pos) end end -- movement commands if self.Target and (self:EquipFirearm(false) or self:EquipThrowable(false)) then -- don't issue body commands if we have a target self.lateralMoveState = Actor.LAT_STILL else self.lateralMoveState = nextLatMove end 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 -- move around another actor function HumanAI.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(100) -- 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 -- open fire on the selected target function HumanAI.ShootTarget(self) local aimBonus = 100 local rateOfFire = 0 if self.EquippedItem and MovableMan:IsActor(self.Target) then rateOfFire = ToHDFirearm(self.EquippedItem).RateOfFire aimBonus = math.max(aimBonus, ToHeldDevice(self.EquippedItem).SharpLength) / 150 -- make the random error vary with the sharp aim length else return true end local ShootTimer = Timer() local aimTime = RangeRand(600, 1000) local TargetAvgVel = self.Target.Vel local AimPoint -- in air: shoot from the hip if self.flying then aimTime = aimTime * 0.5 aimError = RangeRand(-0.4, 0.4) else aimError = RangeRand(-0.2, 0.2) / aimBonus end while true do if not MovableMan:IsActor(self.Target) then break end if self.FirearmIsReady then TargetAvgVel = TargetAvgVel * 0.4 + self.Target.Vel * 0.6 -- filter the target's velocity if not self.flying then self.deviceState = AHuman.AIMING end if SceneMan:ShortestDistance(self.Pos, AimPoint or self.Target.Pos, false).Magnitude < 40 then AimPoint = self.Target.Pos + self.TargetOffset * 0.9 -- move the aimpoint closer to the center of the target at close ranges else AimPoint = self.Target.Pos + self.TargetOffset end aim = HumanAI.GetHitAngle(AimPoint, TargetAvgVel/RangeRand(0.2, 4.0), ToHeldDevice(self.EquippedItem).Pos, 90) if aim then self.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim+aimError+RangeRand(-0.018, 0.018)) else break -- target out of range end if ShootTimer:IsPastSimMS(aimTime) then self.fire = true if ToHDFirearm(self.EquippedItem).FullAuto then aimError = aimError * RangeRand(0.89, 0.95) else ShootTimer:Reset() aimTime = rateOfFire + RangeRand(25, 75) aimError = aimError * RangeRand(0.7, 0.85) end else self.fire = false end elseif self:EquipFirearm(true) then self.deviceState = AHuman.POINTING self.fire = false ShootTimer:Reset() if ToAHuman(self).FirearmIsEmpty then self:ReloadFirearm() aimTime = RangeRand(400, 700) if self.flying then aimTime = aimTime * 0.5 aimError = RangeRand(-0.28, 0.28) else aimError = RangeRand(-0.14, 0.14) / aimBonus end end else break -- no firearm avaliable end coroutine.yield() -- wait until next frame end return true end -- throw a grenade at the selected target function HumanAI.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 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 = 255 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, 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, 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, grassID, false, 3) end end end else break -- out of range end if ID > 0 and ID < 255 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 = HumanAI.GetHitAngle(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 -- move to digger range function HumanAI.DigTarget(self) if MovableMan:IsActor(self.Target) then if not self.FollowingActor or (not MovableMan:IsActor(self.FollowingActor) or self.FollowingActor.ID ~= self.Target.ID) then self.PrevAIWaypoint = self:GetLastAIWaypoint() self:ClearAIWaypoints() self:AddAIMOWaypoint(self.Target) self.FollowingActor = self.Target self.Target = nil self:CreateGoToBehavior() end else self.Target = nil end return true end