· 8 years ago · Jan 13, 2018, 05:44 AM
1airID = 0
2goldID = 2
3doorID = 4 -- Xenocronium
4grassID = 128
5
6Obst = {R_LOW = 1, R_FRONT = 2, R_HIGH = 3, R_UP = 5, L_UP = 6, L_HIGH = 8, L_FRONT = 9, L_LOW = 10}
7WptType = {LAST = 0, UNKNOWN = 1, DROP = 2, AIR = 3}
8HumanAI = {}
9
10function Create(self)
11 ---------------- AI variables start ----------------
12 self.Ctrl = self:GetController()
13
14 self.lateralMoveState = Actor.LAT_STILL
15 self.jumpState = AHuman.NOTJUMPING
16
17 self.deviceState = AHuman.STILL
18 self.lastAIMode = Actor.AIMODE_NONE
19 self.teamBlockState = Actor.NOTBLOCKED
20 self.SentryFacing = self.HFlipped
21 self.fire = false
22
23 self.AirTimer = Timer()
24 self.PickUpTimer = Timer()
25 self.ReloadTimer = Timer()
26 self.BlockedTimer = Timer()
27 self.TargetLostTimer = Timer()
28
29 self.PlayerInterferedTimer = Timer()
30 self.PlayerInterferedTimer:SetSimTimeLimitMS(500)
31
32 self.AlarmTimer = Timer()
33 self.AlarmTimer:SetSimTimeLimitMS(500)
34
35 -- check if this team is controlled by a human
36 local Activ = ActivityMan:GetActivity()
37 for player = Activity.PLAYER_1, Activity.MAXPLAYERCOUNT - 1 do
38 if Activ:PlayerActive(player) and Activ:PlayerHuman(player) then
39 if self.Team == Activ:GetTeamOfPlayer(player) then
40 self.isPlayerOwned = true
41 break
42 end
43 end
44 end
45
46 function self:LookForTargets()
47 local Origin
48 local viewAng
49
50 if self.deviceState == AHuman.AIMING and self.FirearmIsReady then
51 Origin = ToHeldDevice(self.EquippedItem).MuzzlePos
52 viewAng = 0.1
53 elseif self.deviceState == AHuman.POINTING then
54 Origin = self.EyePos
55 viewAng = 0.3
56 elseif self.deviceState == AHuman.THROWING then
57 Origin = self.EyePos
58 viewAng = 0.4
59 else
60 Origin = self.EyePos
61 viewAng = 0.7
62 end
63
64 local viewLen = SceneMan:ShortestDistance(self.EyePos, self.ViewPoint, false).Magnitude + FrameMan.PlayerScreenWidth*0.5
65 local Trace = Vector(viewLen, 0):RadRotate(viewAng*NormalRand()+self:GetAimAngle(true))
66 local ID = SceneMan:CastMORay(Origin, Trace, self.ID, grassID, false, 5)
67
68 if ID < 255 then
69 return MovableMan:GetMOFromID(ID)
70 end
71
72 return nil
73 end
74
75 -- 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.
76 function self:CreateSentryBehavior()
77 if not self.FirearmIsReady and not self.ThrowableIsReady then
78 return
79 end
80
81 self.NextBehavior = coroutine.create(HumanAI.Sentry) -- replace "HumanAI.Sentry" with the function name of your own sentry behavior
82 self.NextCleanup = nil
83 self.NextBehaviorName = "Sentry"
84 end
85
86 function self:CreatePatrolBehavior()
87 self.NextBehavior = coroutine.create(HumanAI.Patrol)
88 self.NextCleanup = nil
89 self.NextBehaviorName = "Patrol"
90 end
91
92 function self:CreateGoldDigBehavior()
93 self.NextBehavior = coroutine.create(HumanAI.GoldDig)
94 self.NextCleanup = nil
95 self.NextBehaviorName = "GoldDig"
96 end
97
98 function self:CreateBrainSearchBehavior()
99 self.NextBehavior = coroutine.create(HumanAI.BrainSearch)
100 self.NextCleanup = nil
101 self.NextBehaviorName = "BrainSearch"
102 end
103
104 function self:CreateGetToolBehavior()
105 self.NextBehavior = coroutine.create(HumanAI.ToolSearch)
106 self.NextCleanup = nil
107 self.NextBehaviorName = "ToolSearch"
108 end
109
110 function self:CreateGetWeaponBehavior()
111 self.NextBehavior = coroutine.create(HumanAI.WeaponSearch)
112 self.NextCleanup = nil
113 self.NextBehaviorName = "WeaponSearch"
114 end
115
116 function self:CreateGoToBehavior()
117 self.NextGoTo = coroutine.create(HumanAI.GoToWpt)
118 self.NextGoToCleanup = function(self)
119 self.lateralMoveState = Actor.LAT_STILL
120 self.deviceState = AHuman.STILL
121 self.crawling = false
122 self.jump = false
123 self.fire = false
124 self:EquipFirearm(true)
125 self:UpdateMovePath()
126 end
127 self.NextGoToName = "GoToWpt"
128 end
129
130 function self:CreateMoveAroundBehavior()
131 self.NextGoTo = coroutine.create(HumanAI.MoveAroundActor)
132 self.NextGoToCleanup = function(self)
133 self.lateralMoveState = Actor.LAT_STILL
134 self.jump = false
135 end
136 self.NextGoToName = "MoveAroundActor"
137 end
138
139 function self:CreateAttackBehavior()
140 if self:EquipFirearm(true) then
141 self.NextBehavior = coroutine.create(HumanAI.ShootTarget)
142 self.NextBehaviorName = "ShootTarget"
143 elseif self:EquipThrowable(true) then
144 self.NextBehavior = coroutine.create(HumanAI.ThrowTarget)
145 self.NextBehaviorName = "ThrowTarget"
146 elseif self:EquipDiggingTool(true) then
147 self.NextBehavior = coroutine.create(HumanAI.DigTarget)
148 self.NextBehaviorName = "DigTarget"
149 else
150 return -- unarmed TODO: start run away behavior here?
151 end
152
153 self.NextCleanup = function(self)
154 self.fire = false
155 self.Target = nil
156 self.deviceState = AHuman.STILL
157 end
158 end
159
160 -- the controller class
161 PIDController = {}
162 PIDController.mt = {__index = PIDController}
163 function PIDController:New(p_in, i_in, d_in, last_in, integral_in)
164 return setmetatable(
165 {
166 p = p_in,
167 i = i_in,
168 d = d_in,
169 last = last_in,
170 integral = integral_in or 0
171 },
172 PIDController.mt)
173 end
174
175 function PIDController:Update(input, target)
176 local err = input - target
177 local change = input - self.last
178 self.last = input
179 self.integral = self.integral + err
180 self.integral = math.min(math.max(self.integral, -100), 100)
181 return self.p*err + self.i*self.integral + self.d*change
182 end
183
184 -- the controllers
185 self.XposPID = PIDController:New(0.04, 0.00001, 0.4, 0)
186 self.YposPID = PIDController:New(0.05, 0.0001, 0.2, 0)
187
188 ---------------- AI variables end ----------------
189
190 self.baseOffset = Vector(-2,8);
191 self.mountMobile = CreateACrab("Coabot Base");
192 self.mountMobile.Pos = self.Pos + self.baseOffset;
193 self.mountMobile.Team = self.Team;
194 self.mountMobile.HitsMOs = false;
195 self.mountMobile.AIMode = 0;
196 MovableMan:AddParticle(self.mountMobile);
197 self.HitsMOs = false;
198
199end
200
201function Update(self)
202
203 if MovableMan:IsParticle(self.mountMobile) then
204 -- self.mountMobile:SetWhichMOToNotHit(self,-1);
205 if self.HFlipped == false then
206 self.negativeNum = 1;
207 else
208 self.negativeNum = -1;
209 end
210 self.mountMobile.AIMode = 0;
211 self.Pos = self.mountMobile.Pos + Vector(self.baseOffset.X*self.negativeNum,self.baseOffset.Y*-1):RadRotate(self.RotAngle);
212 --self.mountMobile.Pos = self.Pos - Vector(self.baseOffset.X*self.negativeNum,self.baseOffset.Y*-1):RadRotate(self.mountMobile.RotAngle);
213 self.Vel = self.mountMobile.Vel;
214 --self.mountMobile.Vel = self.Vel;
215 self.RotAngle = self.mountMobile.RotAngle;
216 self.AngularVel = self.mountMobile.AngularVel;
217 if self:GetController():IsState(Controller.MOVE_LEFT) then
218 self.mountMobile:GetController():SetState(Controller.MOVE_LEFT,true);
219 self.mountMobile:GetController():SetState(Controller.MOVE_RIGHT,false);
220 elseif self:GetController():IsState(Controller.MOVE_RIGHT) then
221 self.mountMobile:GetController():SetState(Controller.MOVE_RIGHT,true);
222 self.mountMobile:GetController():SetState(Controller.MOVE_LEFT,false);
223 else
224 self.mountMobile:GetController():SetState(Controller.MOVE_LEFT,false);
225 self.mountMobile:GetController():SetState(Controller.MOVE_RIGHT,false);
226 end
227 end
228 self.lateralMoveState = Actor.LAT_STILL
229 self.lateralMoveState = Actor.LAT_STILL
230 self.mountMobile.lateralMoveState = Actor.LAT_STILL
231end
232
233function UpdateAI(self)
234
235 if self.PlayerInterferedTimer:IsPastSimTimeLimit() then
236 self.Behavior = nil -- remove the current behavior
237 if self.BehaviorCleanup then
238 self.BehaviorCleanup(self) -- clean up after the current behavior
239 self.BehaviorCleanup = nil
240 end
241
242 self.GoToBehavior = nil
243 if self.GoToCleanup then
244 self.GoToCleanup(self)
245 self.GoToCleanup = nil
246 end
247
248 self.Target = nil
249 self.PickupHD = nil
250 self.BlockingActor = nil
251 self.FollowingActor = nil
252
253 self.fire = false
254 self.jump = false
255 self.crawling = false
256
257 self.deviceState = AHuman.STILL
258 self.lastAIMode = Actor.AIMODE_NONE
259 self.teamBlockState = Actor.NOTBLOCKED
260 end
261
262 self.PlayerInterferedTimer:Reset()
263
264 -- switch to the next behavior, if avaliable
265 if self.NextBehavior then
266 if self.BehaviorCleanup then
267 self.BehaviorCleanup(self)
268 end
269
270 self.Behavior = self.NextBehavior
271 self.BehaviorCleanup = self.NextCleanup
272 self.BehaviorName = self.NextBehaviorName
273 self.NextBehavior = nil
274 end
275
276 -- switch to the next GoTo behavior, if avaliable
277 if self.NextGoTo then
278 if self.GoToCleanup then
279 self.GoToCleanup(self)
280 end
281
282 self.GoToBehavior = self.NextGoTo
283 self.GoToCleanup = self.NextGoToCleanup
284 self.NextGoTo = nil
285 end
286
287 -- check if the AI mode has changed or if we need a new behavior
288 if self.AIMode ~= self.lastAIMode or (not self.Behavior and not self.GoToBehavior) then
289 self.Behavior = nil
290 if self.BehaviorCleanup then
291 self.BehaviorCleanup(self) -- stop the current behavior
292 self.BehaviorCleanup = nil
293 end
294
295 self.GoToBehavior = nil
296 if self.GoToCleanup then
297 self.GoToCleanup(self)
298 self.GoToCleanup = nil
299 end
300
301 -- select a new behavior based on AI mode
302 if self.AIMode == Actor.AIMODE_GOTO then
303 self:CreateGoToBehavior()
304 elseif self.AIMode == Actor.AIMODE_BRAINHUNT then
305 self:CreateBrainSearchBehavior()
306 elseif self.AIMode == Actor.AIMODE_GOLDDIG then
307 self:CreateGoldDigBehavior()
308 elseif self.AIMode == Actor.AIMODE_PATROL then
309 self:CreatePatrolBehavior()
310 else
311 if self.AIMode ~= self.lastAIMode and self.AIMode == Actor.AIMODE_SENTRY then
312 self.SentryFacing = self.HFlipped -- store the direction in which we should be looking
313 end
314
315 self:CreateSentryBehavior()
316 end
317
318 self.lastAIMode = self.AIMode
319 end
320
321 -- check if the legs reach the ground
322 if self.AirTimer:IsPastSimMS(250) then
323 self.AirTimer:Reset()
324
325 if -1 < SceneMan:CastObstacleRay(self.Pos, Vector(0, self.Height/4), Vector(), Vector(), self.ID, grassID, 3) then
326 self.flying = false
327 else
328 self.flying = true
329 end
330 end
331
332 -- look for targets
333 local FoundMO = self:LookForTargets()
334 if FoundMO then
335 if self.Target and MovableMan:IsActor(self.Target) and FoundMO.RootID == self.Target.RootID then -- found the same target
336 self.TargetLostTimer:Reset()
337 self.ReloadTimer:Reset()
338 self.TargetOffset = (self.TargetOffset + SceneMan:ShortestDistance(self.Target.Pos, SceneMan:GetLastRayHitPos(), false))/2
339 else
340 local MO = MovableMan:GetMOFromID(FoundMO.RootID)
341 if MovableMan:IsActor(MO) then
342 if MO.Team == self.Team then
343 self.Target = nil -- stop shooting
344 if MO.ClassName ~= "ADoor" and
345 SceneMan:ShortestDistance(self.Pos, MO.Pos, false).Magnitude < self.Diameter + MO.Diameter
346 then
347 self.BlockingActor = ToActor(MO)
348 end
349 else
350 if MO.ClassName == "AHuman" then
351 MO = ToAHuman(MO)
352 elseif MO.ClassName == "ACrab" then
353 MO = ToACrab(MO)
354 elseif MO.ClassName == "ACRocket" then
355 MO = ToACRocket(MO)
356 elseif MO.ClassName == "ACDropShip" then
357 MO = ToACDropShip(MO)
358 elseif MO.ClassName == "ADoor" then
359 MO = ToADoor(MO)
360 elseif MO.ClassName == "Actor" then
361 MO = ToActor(MO)
362 else
363 MO = nil
364 end
365
366 if MO then
367 self.TargetLostTimer:Reset()
368 self.ReloadTimer:Reset()
369
370 if not self.Target then
371 self.OldTargetPos = nil
372 self.Target = MO
373 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
374 self.TargetLostTimer:SetSimTimeLimitMS(1000)
375 self:CreateAttackBehavior()
376 --else
377 -- TODO: check if this target have higher priority
378 end
379 end
380 end
381 end
382 end
383 else
384 if self.Target then
385 if MovableMan:IsActor(self.Target) then
386 if self.TargetLostTimer:IsPastSimTimeLimit() then
387 self.OldTargetPos = self.Target.Pos
388 self.Target = nil -- the target has been out of sight for a too long, ignore it
389 end
390 else
391 self.Target = nil
392 end
393 end
394
395 if self.ReloadTimer:IsPastSimMS(8000) then -- check if we need to reload
396 self.ReloadTimer:Reset()
397 if ToAHuman(self).FirearmNeedsReload then
398 self:ReloadFirearm()
399 end
400 end
401 end
402
403 -- run the move behavior and delete it if it returns true
404 if self.GoToBehavior then
405 local msg, done = coroutine.resume(self.GoToBehavior, self)
406 if not msg then
407 print(self.PresetName .. " GoToBehavior error:\n" .. done) -- print the error message
408 done = true
409 end
410
411 if done then
412 self.GoToBehavior = nil
413 if self.GoToCleanup then
414 self.GoToCleanup(self)
415 self.GoToCleanup = nil
416 end
417 end
418 elseif self.flying and not self.jump and self.Vel.Y > 7 then
419 self.Ctrl:SetState(Controller.BODY_JUMP, true)
420 end
421
422 -- run the selected behavior and delete it if it returns true
423 if self.Behavior then
424 local msg, done = coroutine.resume(self.Behavior, self)
425 if not msg then
426 print(self.PresetName .. " behavior " .. self.BehaviorName .. " error:\n" .. done) -- print the error message
427 done = true
428 end
429
430 if done then
431 self.Behavior = nil
432 if self.BehaviorCleanup then
433 self.BehaviorCleanup(self)
434 self.BehaviorCleanup = nil
435 end
436
437 if not self.PickupHD and not self.NextBehavior and self.PickUpTimer:IsPastSimMS(500) then
438 self.PickUpTimer:Reset()
439
440 if not self:EquipFirearm(false) then
441 self:CreateGetWeaponBehavior()
442 elseif self.AIMode ~= Actor.AIMODE_SENTRY and not self:EquipDiggingTool(false) then
443 self:CreateGetToolBehavior()
444 end
445 end
446 end
447 elseif self.Target and self.EquippedItem and ToHeldDevice(self.EquippedItem):IsTool() then -- attack with digger
448 -- TODO: move this to a separate behavior
449 if MovableMan:IsActor(self.Target) then
450 local Dist = SceneMan:ShortestDistance(ToHeldDevice(self.EquippedItem).MuzzlePos, self.Target.Pos, false)
451 if Dist.Magnitude < 50 then
452 self.Ctrl.AnalogAim = Dist.Normalized
453 self.Ctrl:SetState(Controller.WEAPON_FIRE, true) -- fire digger
454 end
455 else
456 self.Target = nil
457 end
458 elseif not self.PickupHD and not self.NextBehavior and self.PickUpTimer:IsPastSimMS(500) then
459 self.PickUpTimer:Reset()
460
461 if not self:EquipFirearm(false) then
462 self:CreateGetWeaponBehavior()
463 elseif self.AIMode ~= Actor.AIMODE_SENTRY and not self:EquipDiggingTool(false) then
464 self:CreateGetToolBehavior()
465 end
466 end
467
468 if self.PickupHD then -- there is a HeldDevice we want to pick up
469 if not MovableMan:IsDevice(self.PickupHD) or self.PickupHD.ID ~= self.PickupHD.RootID then
470 self.PickupHD = nil -- the HeldDevice has been destroyed or picked up
471 self:ClearAIWaypoints()
472
473 if self.PrevAIWaypoint or self.FollowingActor then
474 if self.FollowingActor and MovableMan:IsActor(self.FollowingActor) then -- what if the old destination was a moving actor?
475 self:AddAIMOWaypoint(self.FollowingActor)
476 self:CreateGoToBehavior() -- continue towards our old destination
477 else
478 self.FollowingActor = nil
479 if self.PrevAIWaypoint then
480 self:AddAISceneWaypoint(self.PrevAIWaypoint)
481 self:CreateGoToBehavior() -- continue towards our old destination
482 end
483 end
484 end
485 else
486 if SceneMan:ShortestDistance(self.Pos, self.PickupHD.Pos, false).Magnitude < self.Height then
487 self.Ctrl:SetState(Controller.WEAPON_PICKUP, true)
488 end
489 end
490 end
491
492 -- react to relevant AlarmEvents
493 if self.AlarmTimer:IsPastSimTimeLimit() then
494 self.AlarmPos = nil
495
496 for Event in MovableMan.AlarmEvents do
497 if Event.Team ~= self.Team then -- caused by some other team's activites - alarming!
498 local AlarmVec = SceneMan:ShortestDistance(self.EyePos, Event.ScenePos, false) -- see how far away the alarm situation is
499
500 -- only react if the alarm is within range and we are perceptive enough to hear it
501 if AlarmVec.Largest <= Event.Range then -- * self.Perceptiveness then
502 -- if this is the same alarm location as last, then don't repeat the signal
503 if not self.LastAlarmPos or SceneMan:ShortestDistance(self.LastAlarmPos, Event.ScenePos, false).Largest > 10 then
504 -- now check if we have line of sight to the alarm point
505 -- don't check all the way to the target, we are checking for no obstacles, and target will be an obstacle in itself
506 if SceneMan:CastObstacleRay(self.EyePos, AlarmVec*0.9, Vector(), Vector(), self.ID, grassID, 7) < 0 then
507 self.LastAlarmPos = self.AlarmPos
508 self.AlarmPos = Vector(Event.ScenePos.X, Event.ScenePos.Y)
509 self.AlarmTimer:Reset()
510 break
511 end
512 end
513 end
514 end
515 end
516 elseif self.AlarmPos then -- if alarmed, look at the alarming point until AlarmTimer has expired
517 -- point in the direciton we heard the alarm come from if we're not already engaging a target
518 if not (self.Target and
519 (self.deviceState == AHuman.AIMING or
520 self.deviceState == AHuman.FIRING or
521 self.deviceState == AHuman.THROWING))
522 then
523 -- look/point in the direction of alarm
524 self.deviceState = AHuman.POINTING
525 self.Ctrl.AnalogAim = SceneMan:ShortestDistance(self.EyePos, self.AlarmPos, false).Normalized
526 self.lateralMoveState = Actor.LAT_STILL
527 self.jump = false
528 end
529
530 -- if we're unarmed, hit the deck!
531 if not self:EquipFirearm(true) and not self:EquipThrowable(true) and not self:EquipDiggingTool(true) then
532 self.Ctrl:SetState(Controller.BODY_CROUCH, true)
533 -- TODO: run away behavior
534 end
535 end
536
537 if self.teamBlockState == Actor.IGNORINGBLOCK then
538 if self.BlockedTimer:IsPastSimMS(10000) then
539 self.teamBlockState = Actor.NOTBLOCKED
540 end
541 elseif self.teamBlockState == Actor.BLOCKED then -- we are blocked by a teammate, stop
542 self.lateralMoveState = Actor.LAT_STILL
543 self.jump = false
544 if self.BlockedTimer:IsPastSimMS(20000) then
545 self.BlockedTimer:Reset()
546 self.teamBlockState = Actor.IGNORINGBLOCK
547 end
548 else
549 self.BlockedTimer:Reset()
550 end
551
552 -- controller states
553 if self.fire then
554 self.Ctrl:SetState(Controller.WEAPON_FIRE, true)
555 end
556
557 if self.deviceState == AHuman.AIMING then
558 self.Ctrl:SetState(Controller.AIM_SHARP, true)
559 end
560
561 if self.crawling then
562 self.Ctrl:SetState(Controller.BODY_CROUCH, true)
563 end
564
565 if self.jump then
566 if self.jumpState == AHuman.PREJUMP then
567 self.jumpState = AHuman.UPJUMP
568 elseif self.jumpState ~= AHuman.UPJUMP then
569 self.jumpState = AHuman.PREJUMP
570 end
571
572 if self.JetTimeLeft < self.JetTimeTotal * 0.05 then -- TODO: move this to gotowpt?
573 self.refuel = true
574 end
575 else
576 self.jumpState = AHuman.NOTJUMPING
577 end
578
579 if self.jumpState == AHuman.PREJUMP then
580 self.Ctrl:SetState(Controller.BODY_JUMPSTART, true)
581 self.Ctrl:SetState(Controller.BODY_JUMP, false)
582 elseif self.jumpState == AHuman.UPJUMP then
583 self.Ctrl:SetState(Controller.BODY_JUMPSTART, false)
584 self.Ctrl:SetState(Controller.BODY_JUMP, true)
585 end
586
587 if self.lateralMoveState == Actor.LAT_LEFT then
588 self.Ctrl:SetState(Controller.MOVE_LEFT, true)
589 self.Ctrl:SetState(Controller.MOVE_RIGHT, false)
590 elseif self.lateralMoveState == Actor.LAT_RIGHT then
591 self.Ctrl:SetState(Controller.MOVE_LEFT, false)
592 self.Ctrl:SetState(Controller.MOVE_RIGHT, true)
593 end
594end
595
596
597-- finds an aming angle that compensate for projectile drop caused by gravity
598function HumanAI.GetHitAngle(AimPoint, TargetVel, StartPos, muzVel)
599 local Dist = SceneMan:ShortestDistance(StartPos, AimPoint, false)
600 local range = Dist.Magnitude
601
602 -- compensate for gravity if the point we are trying to hit is more than 2m away
603 if range > 40 then
604 local timeToTarget = range / muzVel
605
606 -- lead the target if target speed and projectile TTT is above the threshold
607 if timeToTarget * TargetVel.Magnitude > 0.5 then
608 AimPoint = AimPoint + TargetVel * timeToTarget
609 Dist = SceneMan:ShortestDistance(StartPos, AimPoint, false)
610 end
611
612 Dist = Dist / FrameMan.PPM -- convert from pixels to meters
613 local velSqr = muzVel*muzVel
614 local gravity = SceneMan.GlobalAcc.Y -- * self.Magazine.Ammo.Round.Particle.GlobalAccScalar
615 local root = math.sqrt(velSqr*velSqr - gravity*(gravity*Dist.X*Dist.X+2*-Dist.Y*velSqr))
616
617 if root ~= root then
618 return nil -- no solution exists if the root is NaN
619 end
620
621 return math.atan2(velSqr-root, gravity*Dist.X)
622 end
623
624 return Dist.AbsRadAngle
625end
626
627
628
629-----------------------
630------ Behaviors ------
631-----------------------
632
633function HumanAI.Sentry(self) -- in sentry behavior the agent only looks for new enemies, it sometimes sharp aims to increse spotting range
634 local sweepUp = true
635 local sweepDone = false
636 local maxAng = 1.4
637 local minAng = -1.4
638 local aim
639
640 if self.OldTargetPos then -- try to reaquire an old target
641 local Dist = SceneMan:ShortestDistance(self.EyePos, self.OldTargetPos, false)
642 self.OldTargetPos = nil
643 if (Dist.X < 0 and self.HFlipped) or (Dist.X > 0 and not self.HFlipped) then -- we are facing the target
644 self.deviceState = AHuman.AIMING
645 self.Ctrl.AnalogAim = Dist.Normalized
646
647 for i = 1, 30 do
648 coroutine.yield() -- aim here for ~0.5s
649 end
650 end
651 end
652
653 while true do -- start by looking forward
654 aim = self:GetAimAngle(false)
655
656 if sweepUp then
657 if aim < maxAng/3 then
658 self.Ctrl:SetState(Controller.AIM_UP, false)
659 coroutine.yield() -- wait until next frame
660 self.Ctrl:SetState(Controller.AIM_UP, true)
661 else
662 sweepUp = false
663 end
664 else
665 if aim > minAng/3 then
666 self.Ctrl:SetState(Controller.AIM_DOWN, false)
667 coroutine.yield() -- wait until next frame
668 self.Ctrl:SetState(Controller.AIM_DOWN, true)
669 else
670 sweepUp = true
671 if sweepDone then
672 break
673 else
674 sweepDone = true
675 end
676 end
677 end
678
679 coroutine.yield() -- wait until next frame
680 end
681
682 if self.HFlipped ~= self.SentryFacing then
683 self.HFlipped = self.SentryFacing -- turn to the direction we have been order to guard
684 return true -- restart this behavior
685 end
686
687 while true do -- look down
688 aim = self:GetAimAngle(false)
689 if aim > minAng then
690 self.Ctrl:SetState(Controller.AIM_DOWN, true)
691 else
692 break
693 end
694
695 coroutine.yield() -- wait until next frame
696 end
697
698 local Hit = Vector()
699 local NoObstacle = {}
700 local StartPos
701 self.deviceState = AHuman.AIMING
702
703 while true do -- scan the area for obstacles
704 aim = self:GetAimAngle(false)
705 if aim < maxAng then
706 self.Ctrl:SetState(Controller.AIM_UP, true)
707 else
708 break
709 end
710
711 if self:EquipFirearm(false) and self.EquippedItem then
712 StartPos = ToHeldDevice(self.EquippedItem).MuzzlePos
713 else
714 StartPos = self.EyePos
715 end
716
717 -- save the angle to a table if there is no obstacle
718 if not SceneMan:CastStrengthRay(StartPos, Vector(60, 0):RadRotate(self:GetAimAngle(true)), 5, Hit, 2, 0, true) then
719 table.insert(NoObstacle, aim) -- TODO: don't use a table for this
720 end
721
722 coroutine.yield() -- wait until next frame
723 end
724
725 local SharpTimer = Timer()
726 local aimTime = 2000
727 local angDiff = 1
728 self.deviceState = AHuman.POINTING
729
730 if #NoObstacle > 1 then -- only aim where we know there are no obstacles, e.g. out of a gun port
731 minAng = NoObstacle[1] * 0.95
732 maxAng = NoObstacle[#NoObstacle] * 0.95
733 angDiff = 1 / math.max(math.abs(maxAng - minAng), 0.1) -- sharp aim longer from a small aiming window
734 end
735
736 while true do
737 if not self:EquipFirearm(false) and not self:EquipThrowable(false) then
738 break
739 end
740
741 aim = self:GetAimAngle(false)
742
743 if sweepUp then
744 if aim < maxAng then
745 if aim < maxAng/5 and aim > minAng/5 and PosRand() > 0.3 then
746 self.Ctrl:SetState(Controller.AIM_UP, false)
747 else
748 self.Ctrl:SetState(Controller.AIM_UP, true)
749 end
750 else
751 sweepUp = false
752 end
753 else
754 if aim > minAng then
755 if aim < maxAng/5 and aim > minAng/5 and PosRand() > 0.3 then
756 self.Ctrl:SetState(Controller.AIM_DOWN, false)
757 else
758 self.Ctrl:SetState(Controller.AIM_DOWN, true)
759 end
760 else
761 sweepUp = true
762 end
763 end
764
765 if SharpTimer:IsPastSimMS(aimTime) then
766 SharpTimer:Reset()
767
768 if self.deviceState == AHuman.AIMING then
769 aimTime = RangeRand(1000, 3000)
770 self.deviceState = AHuman.POINTING
771 else
772 aimTime = RangeRand(6000, 12000) * angDiff
773 self.deviceState = AHuman.AIMING
774 end
775
776 if self.HFlipped ~= self.SentryFacing then
777 self.HFlipped = self.SentryFacing -- turn to the direction we have been order to guard
778 break -- restart this behavior
779 end
780 end
781
782 coroutine.yield() -- wait until next frame
783 end
784
785 return true
786end
787
788
789function HumanAI.Patrol(self) -- TODO: move around
790 local TurnTimer = Timer()
791 local turn = 3000
792
793 while true do
794 if TurnTimer:IsPastSimMS(turn) then
795 TurnTimer:Reset()
796 turn = RangeRand(2000, 6000)
797 self:SetAimAngle(RangeRand(-0.7, 0.7))
798
799 self.HFlipped = not self.HFlipped
800 end
801
802 coroutine.yield() -- wait until next frame
803 end
804
805 return true
806end
807
808
809function HumanAI.GoldDig(self)
810 if not self:EquipDiggingTool(true) then
811 self:CreateGetToolBehavior()
812 return true
813 end
814
815 local aimAngle = 0.57
816 local LookVec = Vector(200,0):RadRotate(aimAngle)
817 local GoldPos = Vector()
818 local GoldTable = {}
819
820 while true do
821 self.Ctrl.AnalogAim = LookVec.Normalized
822 if SceneMan:CastMaterialRay(self.EyePos, LookVec, goldID, GoldPos, 2, true) then
823 coroutine.yield() -- wait until next frame
824
825 local str = SceneMan:CastStrengthSumRay(self.EyePos, self.EyePos+LookVec, 4, goldID)/5 -- prioritise gold in soft ground
826 local Dist = SceneMan:ShortestDistance(self.Pos, GoldPos, false) -- prioritise gold close to us
827
828 if GoldPos.Y > SceneMan.SceneHeight - 40 then
829 str = str + 3000 -- avoid gold close to the lower edege of the scene
830 end
831
832 if not SceneMan.SceneWrapsX and GoldPos.X < 40 or GoldPos.X > SceneMan.SceneWidth - 40 then
833 str = str + 2000 -- avoid gold close to the edeges of a non-wrapping scene
834 end
835
836 if Dist.Magnitude < 30 then -- dig to a point behind the gold
837 GoldPos = self.Pos + Dist:SetMagnitude(40)
838 end
839
840 table.insert(GoldTable, {Pos=Vector(GoldPos.X, GoldPos.Y), prio=str+Dist.Magnitude})
841 end
842
843 if aimAngle < -3.71 then
844 break
845 else
846 aimAngle = aimAngle - 0.025
847 LookVec = Vector(200,0):RadRotate(aimAngle)
848 coroutine.yield() -- wait until next frame
849 end
850 end
851
852 if #GoldTable > 0 then
853 table.sort(GoldTable, function(A, B) return A.prio < B.prio end)
854 coroutine.yield() -- wait until next frame
855
856 self:ClearAIWaypoints()
857 self:AddAISceneWaypoint(GoldTable[1].Pos)
858 self:CreateGoToBehavior()
859 end
860
861 return true
862end
863
864
865-- find the closest enemy brain
866function HumanAI.BrainSearch(self)
867 local Activ = ActivityMan:GetActivity()
868 local Brains = {}
869
870 for Act in MovableMan.Actors do
871 if Act.Team ~= self.Team and Act:HasObjectInGroup("Brains") then
872 table.insert(Brains, Act)
873 end
874 end
875
876 coroutine.yield() -- wait until next frame
877
878 if #Brains < 1 then -- no bain actors found, check if some other actor is the brain
879 for player = Activity.PLAYER_1, Activity.MAXPLAYERCOUNT - 1 do
880 if Activ:PlayerActive(player) and Activ:GetTeamOfPlayer(player) ~= self.Team then
881 local Target = Activ:GetPlayerBrain(player)
882 if Target and MovableMan:IsActor(Target) then
883 table.insert(Brains, Target)
884 end
885 end
886 end
887
888 coroutine.yield() -- wait until next frame
889 end
890
891 if #Brains > 0 then
892 if #Brains == 1 then
893 if MovableMan:IsActor(Brains[1]) then
894 self:ClearAIWaypoints()
895 self.FollowingActor = Brains[1]
896 self:AddAIMOWaypoint(self.FollowingActor)
897 self:CreateGoToBehavior()
898 end
899 else
900 local ClosestBrain
901 local minDist = math.huge
902 for _, Act in pairs(Brains) do
903 if MovableMan:IsActor(Act) then
904 self:ClearAIWaypoints()
905 self:AddAISceneWaypoint(Act.Pos)
906 self:UpdateMovePath()
907
908 local waypoints = 0 -- estimate the walking distance to the target
909 for WptPos in self.MovePath do
910 waypoints = waypoints + 1
911 end
912
913 if waypoints < minDist then
914 minDist = waypoints
915 ClosestBrain = Act
916 end
917
918 coroutine.yield() -- wait until next frame
919 end
920 end
921
922 self:ClearAIWaypoints()
923
924 if MovableMan:IsActor(ClosestBrain) then
925 self.FollowingActor = ClosestBrain
926 self:AddAIMOWaypoint(self.FollowingActor)
927 self:CreateGoToBehavior()
928 else
929 return true -- the brain we found died while we where searching, restart this behavior next frame
930 end
931 end
932 else
933 self.AIMode = Actor.AIMODE_PATROL -- no enemy brains left
934 end
935
936 return true
937end
938
939
940-- find a weapon to pick up
941function HumanAI.WeaponSearch(self)
942 local range, minDist, HD
943 local Devices = {}
944
945 if self.isPlayerOwned then
946 minDist = 70 -- 3.5m
947 else
948 minDist = FrameMan.PlayerScreenWidth * 0.4
949 end
950
951 for Item in MovableMan.Items do -- store all HeldDevices of the correct type and within a cerain range in a table
952 if Item.ClassName ~= "TDExplosive" then
953 HD = ToHeldDevice(Item)
954 if HD:IsWeapon() and HD.Vel.Magnitude < 2 then
955 range = SceneMan:ShortestDistance(self.Pos, HD.Pos, false).Magnitude
956 if range < minDist then
957 table.insert(Devices, HD)
958 end
959 end
960 end
961 end
962
963 if #Devices > 0 then
964 coroutine.yield() -- wait until next frame
965
966 local PrevWpt
967 if not self.FollowingActor then
968 if self.AIMode == Actor.AIMODE_SENTRY and not self.flying then
969 PrevWpt = Vector(self.Pos.X, self.Pos.Y) -- return here after pick up
970 else
971 PrevWpt = self:GetLastAIWaypoint()
972 range = SceneMan:ShortestDistance(self.Pos, PrevWpt, false).Magnitude
973 if range < 1 then
974 PrevWpt = nil -- not a valid waypoint
975 end
976 end
977 end
978
979 if self.isPlayerOwned then
980 minDist = 5
981 else
982 minDist = 20
983 end
984
985 for _, Item in pairs(Devices) do
986 if MovableMan:IsDevice(Item) then
987 self:ClearAIWaypoints()
988 self:AddAISceneWaypoint(Item.Pos)
989 self:UpdateMovePath()
990
991 local waypoints = 0 -- estimate the walking distance to the item
992 for _ in self.MovePath do
993 waypoints = waypoints + 1
994 end
995
996 if waypoints < minDist then
997 minDist = waypoints
998 self.PickupHD = Item
999 end
1000
1001 coroutine.yield() -- wait until next frame
1002 end
1003 end
1004
1005 self:ClearAIWaypoints()
1006
1007 if MovableMan:IsDevice(self.PickupHD) then
1008 if PrevWpt then
1009 self.PrevAIWaypoint = PrevWpt
1010 end
1011
1012 self:AddAIMOWaypoint(self.PickupHD)
1013 self:CreateGoToBehavior()
1014 else
1015 self.PickupHD = nil -- the item became invalid while searching
1016
1017 if self.FollowingActor and MovableMan:IsActor(self.FollowingActor) then
1018 self:AddAIMOWaypoint(self.FollowingActor)
1019 elseif self.PrevAIWaypoint then
1020 self:AddAISceneWaypoint(self.PrevAIWaypoint)
1021 end
1022 end
1023 end
1024
1025 return true
1026end
1027
1028
1029-- find a tool to pick up
1030function HumanAI.ToolSearch(self)
1031 local range, minDist, HD
1032 local Devices = {}
1033
1034 if self.AIMode == Actor.AIMODE_GOLDDIG then
1035 minDist = FrameMan.PlayerScreenWidth * 0.5
1036 elseif self.isPlayerOwned then
1037 minDist = 70 -- 3.5m
1038 else
1039 minDist = FrameMan.PlayerScreenWidth * 0.3
1040 end
1041
1042 for Item in MovableMan.Items do -- store all HeldDevices of the correct type and within a cerain range in a table
1043 HD = ToHeldDevice(Item)
1044 if HD:IsTool() and HD.Vel.Magnitude < 2 then
1045 range = SceneMan:ShortestDistance(self.Pos, HD.Pos, false).Magnitude
1046 if range < minDist then
1047 table.insert(Devices, HD)
1048 end
1049 end
1050 end
1051
1052 if #Devices > 0 then
1053 coroutine.yield() -- wait until next frame
1054
1055 local PrevWpt
1056 if not self.FollowingActor then
1057 if self.AIMode == Actor.AIMODE_SENTRY and not self.flying then
1058 PrevWpt = Vector(self.Pos.X, self.Pos.Y) -- return here after pick up
1059 else
1060 PrevWpt = self:GetLastAIWaypoint()
1061 range = SceneMan:ShortestDistance(self.Pos, PrevWpt, false).Magnitude
1062 if range < 1 then
1063 PrevWpt = nil -- not a valid waypoint
1064 end
1065 end
1066 end
1067
1068 if self.AIMode == Actor.AIMODE_GOLDDIG then
1069 minDist = 30
1070 elseif self.isPlayerOwned then
1071 minDist = 5
1072 else
1073 minDist = 15
1074 end
1075
1076 for _, Item in pairs(Devices) do
1077 if MovableMan:IsDevice(Item) then
1078 self:ClearAIWaypoints()
1079 self:AddAISceneWaypoint(Item.Pos)
1080 self:UpdateMovePath()
1081
1082 local waypoints = 0 -- estimate the walking distance to the item
1083 for _ in self.MovePath do
1084 waypoints = waypoints + 1
1085 end
1086
1087 if waypoints < minDist then
1088 minDist = waypoints
1089 self.PickupHD = Item
1090 end
1091
1092 coroutine.yield() -- wait until next frame
1093 end
1094 end
1095
1096 self:ClearAIWaypoints()
1097
1098 if MovableMan:IsDevice(self.PickupHD) then
1099 if PrevWpt then
1100 self.PrevAIWaypoint = PrevWpt
1101 end
1102
1103 self:AddAIMOWaypoint(self.PickupHD)
1104 self:CreateGoToBehavior()
1105 else
1106 self.PickupHD = nil -- the item became invalid while searching
1107
1108 if self.FollowingActor and MovableMan:IsActor(self.FollowingActor) then
1109 self:AddAIMOWaypoint(self.FollowingActor)
1110 elseif self.PrevAIWaypoint then
1111 self:AddAISceneWaypoint(self.PrevAIWaypoint)
1112 end
1113 end
1114 end
1115
1116 return true
1117end
1118
1119
1120-- move to the next waypoint
1121function HumanAI.GoToWpt(self)
1122 local Destination = self:GetLastAIWaypoint()
1123 if SceneMan:ShortestDistance(Destination, self.Pos, false).Magnitude < 10 then
1124 self:ClearAIWaypoints()
1125 self.AIMode = Actor.AIMODE_SENTRY
1126 return true
1127 end
1128
1129 local Lower = function(Y1, Y2, margin)
1130 return Y1.Pos.Y - margin > Y2.Pos.Y
1131 end
1132
1133 local Higher = function(Y1, Y2, margin)
1134 return Y1.Pos.Y + margin < Y2.Pos.Y
1135 end
1136
1137 local AngleBetweenVectors = function(A, B) -- returns the smallest angle between two vectors
1138 local angle = math.atan2(A.Y, A.X) - math.atan2(B.Y, B.X)
1139
1140 if angle > 3.142 then
1141 angle = angle - 6.283
1142 elseif angle < -3.142 then
1143 angle = angle + 6.283
1144 end
1145
1146 return angle
1147 end
1148
1149 local UpdatePathTimer = Timer()
1150 local ArrivedTimer = Timer()
1151 local BurstTimer = Timer()
1152
1153 local StuckTimer = Timer()
1154 StuckTimer:SetSimTimeLimitMS(2000)
1155
1156 local groundDist = self.Height / 5
1157 local nextLatMove = self.lateralMoveState
1158 local nextAimAngle = self:GetAimAngle(false) * 0.8
1159 local scanMOs = 0
1160 local scanAng = 0 -- for obstacle detection
1161 local Obstacles = {}
1162 local PrevWptPos = self.Pos
1163 local sweepCW = true
1164 local sweepRange = 0
1165 local sweepMin = 0
1166 local sweepMax = 0
1167 local digState = AHuman.NOTDIGGING
1168 local obstacleState = Actor.PROCEEDING
1169 local WptList, Waypoint, Dist, CurrDist
1170
1171 while true do
1172 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
1173 coroutine.yield() -- wait until next frame
1174 end
1175
1176 if self.Vel.Magnitude > 2 then
1177 StuckTimer:Reset()
1178 end
1179
1180 if not self.flying and UpdatePathTimer:IsPastSimMS(5000) then
1181 if Waypoint and self.BlockingActor then
1182 if MovableMan:IsActor(self.BlockingActor) then
1183 CurrDist = SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false)
1184 if (self.Pos.X > self.BlockingActor.Pos.X and CurrDist.X < self.Pos.X) or
1185 (self.Pos.X < self.BlockingActor.Pos.X and CurrDist.X > self.Pos.X) or
1186 SceneMan:ShortestDistance(self.Pos, self.BlockingActor.Pos, false).Magnitude > self.Diameter + self.BlockingActor.Diameter
1187 then
1188 self.BlockingActor = nil -- the blocking actor is not in the way any longer
1189 self.teamBlockState = Actor.NOTBLOCKED
1190 else
1191 self.BlockedTimer:Reset()
1192 self.teamBlockState = Actor.IGNORINGBLOCK
1193 self:CreateMoveAroundBehavior()
1194 break -- end this behavior
1195 end
1196 else
1197 self.BlockingActor = nil
1198 end
1199 end
1200
1201 UpdatePathTimer:Reset()
1202 self.deviceState = AHuman.STILL
1203 self.crawling = false
1204 self.jump = false
1205 nextLatMove = Actor.LAT_STILL
1206 Waypoint = nil
1207 WptList = nil
1208 elseif StuckTimer:IsPastSimTimeLimit() then -- dislodge
1209 if self.jump then
1210 if self.JetTimeLeft < self.JetTimeTotal * 0.1 then -- out of fuel
1211 self.jump = false
1212 nextLatMove = Actor.LAT_STILL
1213 else
1214 local chance = PosRand()
1215 if chance < 0.1 then
1216 nextLatMove = Actor.LAT_LEFT
1217 elseif chance > 0.9 then
1218 nextLatMove = Actor.LAT_RIGHT
1219 else
1220 nextLatMove = Actor.LAT_STILL
1221 end
1222 end
1223 else
1224 nextLatMove = Actor.LAT_STILL
1225 if self.JetTimeLeft > self.JetTimeTotal * 0.9 then
1226 self.jump = true
1227 BurstTimer:SetSimTimeLimitMS(90) -- this burst last until the BurstTimer expire
1228 BurstTimer:Reset()
1229 end
1230 end
1231 elseif WptList then -- we have a list of waypoints, folow it
1232 if #WptList < 1 and not Waypoint then -- arrived
1233 break
1234 else
1235 if not Waypoint then -- get the next waypoint in the list
1236 -- if we are following someone we must update the path more often
1237 if self.MOMoveTarget and MovableMan:IsActor(self.MOMoveTarget) and
1238 SceneMan:ShortestDistance(self:GetLastAIWaypoint(), self.MOMoveTarget.Pos, false).Magnitude > 40
1239 then -- the target has moved
1240 WptList = nil
1241 Waypoint = nil
1242 else
1243 UpdatePathTimer:Reset()
1244 end
1245
1246 if WptList then
1247 local NextWptPos = WptList[1].Pos
1248 Dist = SceneMan:ShortestDistance(self.Pos, NextWptPos, false)
1249 if Dist.Y < -25 and math.abs(Dist.X) < 30 then -- avoid any corners if the next waypoint is above us
1250 local CornerPos = Vector(NextWptPos.X, NextWptPos.Y)
1251 if self.Pos.X > CornerPos.X then
1252 CornerPos = CornerPos + Vector(20, -50)
1253 else
1254 CornerPos = CornerPos + Vector(-20, -50)
1255 end
1256
1257 local Free = Vector()
1258 Dist = SceneMan:ShortestDistance(NextWptPos, CornerPos, false)
1259 -- make sure the corner waypoint is not inside terrain
1260 local pixels = SceneMan:CastObstacleRay(NextWptPos, Dist, Vector(), Free, self.ID, grassID, 3)
1261 if pixels == 0 then
1262 break -- the waypoint is inside terrain, plot a new path
1263 elseif pixels > 20 then
1264 CornerPos = NextWptPos + Dist:CapMagnitude(pixels-20) -- compensate for obstacles
1265 elseif pixels > 0 then
1266 CornerPos = Vector(NextWptPos.X, NextWptPos.Y) -- the corner is too close to an obstacle, don't move it
1267 end
1268
1269 coroutine.yield() -- wait until next frame
1270
1271 -- check if we have LOS
1272 Dist = SceneMan:ShortestDistance(self.Pos, CornerPos, false)
1273 if 0 <= SceneMan:CastObstacleRay(self.Pos, Dist, Vector(), Free, self.ID, grassID, 4) then
1274 -- we are blocked
1275 CornerPos.X = self.Pos.X
1276 coroutine.yield() -- wait until next frame
1277
1278 -- check if we have LOS again
1279 Dist = SceneMan:ShortestDistance(self.Pos, CornerPos, false)
1280 if 0 <= SceneMan:CastObstacleRay(self.Pos, Dist, Vector(), Free, self.ID, grassID, 4) then
1281 -- we are still blocked, make a guess
1282 CornerPos = NextWptPos + Vector(RangeRand(-20,20), RangeRand(-10,10))
1283 end
1284 end
1285
1286 coroutine.yield() -- wait until next frame
1287
1288 Waypoint = {Pos=CornerPos, Type=WptType.AIR}
1289 if WptList[1].Type == WptType.UNKNOWN then -- remove the waypoint after the corner if possible
1290 table.remove(WptList, 1)
1291 -- TODO: use self:RemoveMovePathBeginning() to clean up the graphical representation of the path
1292 end
1293
1294 self:AddToMovePathBeginning(Waypoint.Pos)
1295 else
1296 Waypoint = table.remove(WptList, 1)
1297 if #WptList > 0 then
1298 self:RemoveMovePathBeginning()
1299 end
1300
1301 if Waypoint.Type ~= WptType.AIR then
1302 local Free = Vector()
1303
1304 -- only if we have a digging tool
1305 if Waypoint.Type ~= WptType.DROP and self:EquipDiggingTool(false) then
1306 local PathSegRay = SceneMan:ShortestDistance(PrevWptPos, Waypoint.Pos, false) -- detect material blocking the path and start digging through it
1307 if self.teamBlockState ~= Actor.BLOCKED and SceneMan:CastStrengthRay(PrevWptPos, PathSegRay, 5, Free, 1, doorID, true) then
1308 if SceneMan:ShortestDistance(self.Pos, Free, false).Magnitude < self.Height*0.5 then -- check that we're close enough to start digging
1309 -- TODO: update the path and try again just to make sure we can't get through some where else
1310 digState = AHuman.STARTDIG
1311 self.deviceState = AHuman.DIGGING
1312 obstacleState = Actor.DIGPAUSING
1313 nextLatMove = Actor.LAT_STILL
1314 sweepRange = math.pi / 4
1315 StuckTimer:SetSimTimeLimitMS(5000)
1316 self.Ctrl.AnalogAim = SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false).Normalized -- aim in the direction of the next waypoint
1317 else
1318 digState = AHuman.NOTDIGGING
1319 obstacleState = Actor.PROCEEDING
1320 end
1321
1322 coroutine.yield() -- wait until next frame
1323 else
1324 digState = AHuman.NOTDIGGING
1325 obstacleState = Actor.PROCEEDING
1326 StuckTimer:SetSimTimeLimitMS(1500)
1327 end
1328 end
1329
1330 if digState == AHuman.NOTDIGGING and self.deviceState ~= AHuman.DIGGING then
1331 -- if our path isn't blocked enough to dig, but the headroom is too little, start crawling to get through
1332 local Heading = SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false):SetMagnitude(self.Height*0.5)
1333
1334 -- don't crawl if it's too steep, climb then instead
1335 if math.abs(Heading.X) > math.abs(Heading.Y) and self.Head and self.Head:IsAttached() then
1336 local TopHeadPos = self.Head.Pos - Vector(0, self.Head.Radius*0.7)
1337
1338 -- first check up to the top of the head, and then from there forward
1339 if SceneMan:CastStrengthRay(self.Pos, TopHeadPos - self.Pos, 5, Free, 4, doorID, true) or
1340 SceneMan:CastStrengthRay(TopHeadPos, Heading, 5, Free, 4, doorID, true)
1341 then
1342 self.crawling = true
1343 else
1344 self.crawling = false
1345 end
1346
1347 coroutine.yield() -- wait until next frame
1348 else
1349 self.crawling = false
1350 end
1351 end
1352 end
1353 end
1354
1355 if Waypoint.Type == WptType.AIR then
1356 ArrivedTimer:SetSimTimeLimitMS(25)
1357 else
1358 ArrivedTimer:SetSimTimeLimitMS(50)
1359 end
1360 end
1361 elseif #WptList > 1 then -- check if some other waypoint is closer
1362 local test = math.random(1, #WptList)
1363 local RandomWpt = WptList[test]
1364 if RandomWpt then
1365 Dist = SceneMan:ShortestDistance(self.Pos, RandomWpt.Pos, false)
1366 local mag = Dist.Magnitude
1367 if mag < 50 and mag*1.5 < SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false).Magnitude then
1368 -- this waypoint is closer, check LOS
1369 if -1 == SceneMan:CastObstacleRay(self.Pos, Dist, Vector(), Vector(), self.ID, grassID, 4) then
1370 Waypoint = RandomWpt -- go here instead
1371 if WptList[test-1] then
1372 PrevWptPos = WptList[test-1].Pos
1373 else
1374 PrevWptPos = self.Pos
1375 end
1376
1377 test = math.min(test, #WptList)
1378 for i = 1, test do -- delete the earlier waypoints
1379 table.remove(WptList, 1)
1380 if #WptList > 0 then
1381 self:RemoveMovePathBeginning()
1382 end
1383 end
1384 end
1385 end
1386 end
1387 end
1388
1389 if Waypoint then
1390 CurrDist = SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false)
1391
1392 -- digging
1393 if digState ~= AHuman.NOTDIGGING then
1394 if not self.Target and self:EquipDiggingTool(true) then -- switch to the digger if we have one
1395 if ToAHuman(self).FirearmIsEmpty then -- reload if it's empty
1396 self.fire = false
1397 self.Ctrl:SetState(Controller.WEAPON_RELOAD, true)
1398 else
1399 if self.teamBlockState == Actor.BLOCKED then
1400 self.fire = false
1401 nextLatMove = Actor.LAT_STILL
1402 else
1403 if obstacleState == Actor.PROCEEDING then
1404 if CurrDist.X < -1 then
1405 nextLatMove = Actor.LAT_LEFT
1406 elseif CurrDist.X > 1 then
1407 nextLatMove = Actor.LAT_RIGHT
1408 end
1409 else
1410 nextLatMove = Actor.LAT_STILL
1411 end
1412
1413 -- check if we are close enough to dig
1414 if SceneMan:ShortestDistance(PrevWptPos, self.Pos, false).Magnitude > self.Height*0.5 and
1415 SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false).Magnitude > self.Height*0.5
1416 then
1417 digState = AHuman.NOTDIGGING
1418 obstacleState = Actor.PROCEEDING
1419 self.deviceState = AHuman.STILL
1420 self.fire = false
1421 self:EquipFirearm(true)
1422 else
1423 local aimAngle = self:GetAimAngle(true)
1424 local AimVec = Vector(1, 0):RadRotate(aimAngle)
1425
1426 -- see if we have dug out all that we can in the sweep area without moving closer
1427 local centerAngle = CurrDist.AbsRadAngle
1428 local Ray = Vector(self.Height*0.3, 0):RadRotate(centerAngle) -- center
1429 if SceneMan:CastNotMaterialRay(self.Pos, Ray, 0, 3, false) < 0 then
1430 coroutine.yield() -- wait until next frame
1431
1432 -- now check the tunnel's thickness
1433 Ray = Vector(self.Height*0.3, 0):RadRotate(centerAngle + sweepRange) -- up
1434 if SceneMan:CastNotMaterialRay(self.Pos, Ray, airID, 3, false) < 0 then
1435 coroutine.yield() -- wait until next frame
1436
1437 Ray = Vector(self.Height*0.3, 0):RadRotate(centerAngle - sweepRange) -- down
1438 if SceneMan:CastNotMaterialRay(self.Pos, Ray, airID, 3, false) < 0 then
1439 obstacleState = Actor.PROCEEDING -- ok the tunnel section is clear, so start walking forward while still digging
1440 else
1441 obstacleState = Actor.DIGPAUSING -- tunnel cavity not clear yet, so stay put and dig some more
1442 end
1443 end
1444 else
1445 obstacleState = Actor.DIGPAUSING -- tunnel cavity not clear yet, so stay put and dig some more
1446 end
1447
1448 coroutine.yield() -- wait until next frame
1449
1450 local angDiff = AngleBetweenVectors(CurrDist, AimVec)
1451 if math.abs(angDiff) < sweepRange then
1452 self.fire = true -- only fire the digger at the obstacle
1453 else
1454 self.fire = false
1455 end
1456
1457 -- sweep the digger between the two endpoints of the obstacle
1458 local DigTarget
1459 if sweepCW then
1460 DigTarget = Vector(self.Height*0.4, 0):RadRotate(centerAngle + sweepRange)
1461 else
1462 DigTarget = Vector(self.Height*0.4, 0):RadRotate(centerAngle - sweepRange)
1463 end
1464
1465 angDiff = AngleBetweenVectors(DigTarget, AimVec)
1466 if math.abs(angDiff) < 0.1 then
1467 sweepCW = not sweepCW -- this is close enough, go in the other direction next frame
1468 else
1469 self.Ctrl.AnalogAim = Vector(AimVec.X, AimVec.Y):RadRotate(-angDiff*0.1)
1470 end
1471
1472 -- check if we are done when we get close enough to the waypoint
1473 if SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false).Magnitude < self.Height*0.25 then
1474 if not SceneMan:CastStrengthRay(PrevWptPos, SceneMan:ShortestDistance(PrevWptPos, Waypoint.Pos, false), 5, Vector(), 1, doorID, true) and
1475 not SceneMan:CastStrengthRay(self.EyePos, SceneMan:ShortestDistance(self.EyePos, Waypoint.Pos, false), 5, Vector(), 1, doorID, true)
1476 then
1477 -- advance to the next waypoint, if there are any
1478 if #WptList > 0 then
1479 UpdatePathTimer:Reset()
1480 PrevWptPos = Waypoint.Pos
1481 Waypoint = table.remove(WptList, 1)
1482 if #WptList > 0 then
1483 self:RemoveMovePathBeginning()
1484 end
1485 end
1486 end
1487
1488 coroutine.yield() -- wait until next frame
1489 end
1490 end
1491 end
1492 end
1493 else
1494 digState = AHuman.NOTDIGGING
1495 obstacleState = Actor.PROCEEDING
1496 self.deviceState = AHuman.STILL
1497 self.fire = false
1498 self:EquipFirearm(true)
1499 end
1500 else
1501 if not self.Target then
1502 self.fire = false
1503 end
1504
1505 -- Scan for obstacles
1506 local Trace = Vector(self.Diameter*0.85, 0):RadRotate(scanAng)
1507 local Free = Vector()
1508 local index = math.floor(scanAng*2.5+2.01)
1509 if -1 < SceneMan:CastObstacleRay(self.Pos, Trace, Vector(), Free, self.ID, grassID, 3) then
1510 Obstacles[index] = true
1511 else
1512 Obstacles[index] = false
1513 end
1514
1515 if scanAng < 1.57 then -- pi/2
1516 if scanAng > 1.2 then
1517 scanAng = 1.89
1518 else
1519 scanAng = scanAng + 0.55
1520 end
1521 else
1522 if scanAng > 3.5 then
1523 scanAng = -0.4
1524 else
1525 scanAng = scanAng + 0.55
1526 end
1527 end
1528
1529 coroutine.yield() -- wait until next frame
1530
1531 if CurrDist.Magnitude > 20 then
1532 ArrivedTimer:Reset() -- not close enough
1533 end
1534
1535 if ArrivedTimer:IsPastSimTimeLimit() then
1536 PrevWptPos = Waypoint.Pos
1537 Waypoint = nil -- only remove a waypoint if we have been close to it for a while
1538 elseif self.refuel and self.Vel.Y < 5 then
1539 if self.JetTimeLeft > self.JetTimeTotal * 0.95 then
1540 self.refuel = false
1541 else
1542 self.jump = false -- wait until the jetpack is full
1543 nextLatMove = Actor.LAT_STILL
1544 end
1545 else -- move towards the waypoint
1546 nextAimAngle = self:GetAimAngle(false) * 0.8 -- look straight ahead
1547
1548 -- control vertical movement
1549 local change
1550 if self.flying then
1551 change = self.YposPID:Update(CurrDist.Y+self.Vel.Y*3, 0)
1552 else
1553 change = self.YposPID:Update(CurrDist.Y, 0) -- ignore our velocity if on the ground
1554 end
1555
1556 if math.abs(self.RotAngle) < 0.5 and self.Vel.Y > -10 and
1557 (change < -1 or self.Vel.Y > 10)
1558 then
1559 if not self.jump then
1560 BurstTimer:Reset() -- this burst last until the BurstTimer expire
1561 end
1562
1563 if change < -6 then
1564 BurstTimer:SetSimTimeLimitMS(90)
1565 elseif change < -4 then
1566 BurstTimer:SetSimTimeLimitMS(120)
1567 elseif change < -2 or self.Vel.Y > 20 then
1568 BurstTimer:SetSimTimeLimitMS(150)
1569 else
1570 BurstTimer:SetSimTimeLimitMS(500)
1571 end
1572
1573 self.jump = true
1574 else
1575 self.jump = false
1576 end
1577
1578 -- control horisontal movement
1579 if self.jump then
1580 if self.flying then
1581 change = self.XposPID:Update(CurrDist.X+self.Vel.X*3, 0)
1582 else
1583 change = self.XposPID:Update(CurrDist.X, 0) -- ignore our velocity if on the ground
1584 end
1585
1586 if change < -0.5 then
1587 nextLatMove = Actor.LAT_LEFT
1588 elseif change > 0.5 then
1589 nextLatMove = Actor.LAT_RIGHT
1590 else
1591 nextAimAngle = 1.2 -- look up to aim jetpack down
1592 end
1593 elseif self.FGLeg or self.BGLeg then
1594 if CurrDist.X < -3 then
1595 nextLatMove = Actor.LAT_LEFT
1596 elseif CurrDist.X > 3 then
1597 nextLatMove = Actor.LAT_RIGHT
1598 else
1599 nextLatMove = Actor.LAT_STILL
1600 end
1601 elseif (CurrDist.X < -5 and self.HFlipped) or (CurrDist.X > 5 and not self.HFlipped) then
1602 -- no legs, jump forward
1603 BurstTimer:Reset()
1604 self.jump = true
1605 end
1606
1607 -- must fire thrusters to move sideways when in the air
1608 if not self.jump and self.flying and Waypoint.Type ~= WptType.DROP and
1609 self.Vel.Y > -5 and CurrDist.Y-self.Vel.Y*3 < 20 --TODO: use Ychange here?
1610 then
1611 change = math.abs(self.XposPID:Update(CurrDist.X+self.Vel.X*3, 0))
1612 if change > 0.5 then
1613 if not self.jump then
1614 BurstTimer:Reset() -- this burst last until the BurstTimer expire
1615 end
1616
1617 if change < -5 then
1618 BurstTimer:SetSimTimeLimitMS(90)
1619 elseif change < -2 then
1620 BurstTimer:SetSimTimeLimitMS(120)
1621 else
1622 BurstTimer:SetSimTimeLimitMS(300)
1623 end
1624
1625 self.jump = true
1626 end
1627 end
1628
1629 if self.jump then
1630 -- obstacle right
1631 for i = Obst.R_FRONT, Obst.R_UP do
1632 if Obstacles[i] then
1633 if Obstacles[Obst.R_UP] then
1634 self.jump = false
1635 else
1636 nextAimAngle = 1.2 -- look up
1637 end
1638
1639 break
1640 end
1641 end
1642
1643 -- obstacle left
1644 for i = Obst.L_UP, Obst.L_FRONT do
1645 if Obstacles[i] then
1646 if Obstacles[Obst.L_UP] then
1647 self.jump = false
1648 else
1649 nextAimAngle = 1.2 -- look up
1650 end
1651
1652 break
1653 end
1654 end
1655 elseif Waypoint.Type ~= WptType.DROP and not Lower(Waypoint, self, 20) then
1656 -- jump over low obstacles unless we want to jump off a ledge
1657 if nextLatMove == Actor.LAT_RIGHT and
1658 (Obstacles[Obst.R_LOW] or Obstacles[Obst.R_FRONT]) and not Obstacles[Obst.R_UP]
1659 then
1660 self.jump = true
1661 if Obstacles[Obst.R_HIGH] then
1662 nextLatMove = Actor.LAT_LEFT -- TODO: only when too close to the obstacle?
1663 end
1664 elseif nextLatMove == Actor.LAT_LEFT and
1665 (Obstacles[Obst.L_LOW] or Obstacles[Obst.L_FRONT]) and not Obstacles[Obst.L_UP]
1666 then
1667 self.jump = true
1668 if Obstacles[Obst.L_HIGH] then
1669 nextLatMove = Actor.LAT_RIGHT -- TODO: only when too close to the obstacle?
1670 end
1671 end
1672 end
1673
1674 if math.abs(self.RotAngle) > 0.5 then
1675 self.jump = false
1676 end
1677 end
1678
1679 if not self.Target then
1680 self:SetAimAngle(nextAimAngle)
1681 end
1682 end
1683 end
1684 end
1685 else -- no waypoint list, create one in several small steps to reduce lag
1686 local TmpList = {}
1687 table.insert(TmpList, {Pos=self.Pos, Type=WptType.UNKNOWN})
1688 self:UpdateMovePath()
1689 coroutine.yield() -- wait until next frame
1690
1691 for WptPos in self.MovePath do -- skip any waypoint too close to the previous one
1692 if SceneMan:ShortestDistance(TmpList[#TmpList].Pos, WptPos, false).Magnitude > 10 then
1693 table.insert(TmpList, {Pos=WptPos, Type=WptType.UNKNOWN})
1694 end
1695 end
1696
1697 if #TmpList < 3 then
1698 Dist = nil
1699 if TmpList[2] then
1700 Dist = SceneMan:ShortestDistance(TmpList[2].Pos, self.Pos, false)
1701 end
1702
1703 -- already at the target
1704 if not Dist or Dist.Magnitude < 25 then
1705 self:ClearMovePath()
1706 break
1707 end
1708 end
1709
1710 coroutine.yield() -- wait until next frame
1711
1712 -- add information about when to jump up and down
1713 local prune = {1} -- always remove the first waypoint (self.Pos)
1714 for i = 2, #TmpList do
1715 if Lower(TmpList[i], TmpList[i-1], 30) then -- scan for sharp drops
1716 TmpList[i].Type = WptType.DROP
1717
1718 for j = i+1, #TmpList do -- look for other side
1719 local gap = math.abs(SceneMan:ShortestDistance(TmpList[i-1].Pos, TmpList[j].Pos, false).X)
1720 if gap > 250 then
1721 break -- too far
1722 end
1723
1724 -- look for a landing point
1725 if not Higher(TmpList[i-1], TmpList[j], 10) and
1726 not Lower(TmpList[i-1], TmpList[j], 10)
1727 then
1728 -- check if we can jump
1729 local Trace = SceneMan:ShortestDistance(TmpList[i-1].Pos, TmpList[j].Pos, false)
1730 if -1 == SceneMan:CastObstacleRay(TmpList[i-1].Pos, Trace, Vector(), Vector(), self.ID, grassID, 4) then
1731 -- delete any waypoints in between, except one
1732 for k = i+1, j-1 do
1733 table.insert(prune, k)
1734 end
1735
1736 local TestPos = (TmpList[i-1].Pos + TmpList[j].Pos) / 2
1737 local Free = Vector()
1738 if 0 ~= SceneMan:CastObstacleRay(TestPos, Vector(0,-gap/3), Vector(), Free, self.ID, grassID, 2) then
1739 -- TODO: check LOS? what if 0?
1740 TmpList[i].Pos = Free
1741 TmpList[i].Type = WptType.AIR
1742 end
1743 end
1744
1745 coroutine.yield() -- wait until next frame
1746 break
1747 end
1748 end
1749 end
1750 end
1751
1752 coroutine.yield() -- wait until next frame
1753
1754 -- delete waypoints in reverse order, if marked for pruning
1755 table.sort(prune, function(a,b) return a > b end)
1756 for k = 1, #prune do
1757 table.remove(TmpList, prune[k])
1758 end
1759
1760 if #TmpList > 0 then
1761 TmpList[#TmpList].Type = WptType.LAST
1762 end
1763
1764 WptList = TmpList
1765
1766 -- create the move path seen on the screen
1767 self:ClearMovePath()
1768 for _, Wpt in pairs(TmpList) do
1769 self:AddToMovePathEnd(Wpt.Pos)
1770 end
1771 end
1772
1773 -- movement commands
1774 if self.Target and (self:EquipFirearm(false) or self:EquipThrowable(false)) then -- don't issue body commands if we have a target
1775 self.lateralMoveState = Actor.LAT_STILL
1776 else
1777 self.lateralMoveState = nextLatMove
1778 end
1779
1780 if self.jump and BurstTimer:IsPastSimTimeLimit() then -- trigger jetpack bursts
1781 BurstTimer:Reset()
1782 self.jump = false
1783 end
1784
1785 if self.BlockingActor then
1786 if not MovableMan:IsActor(self.BlockingActor) or
1787 SceneMan:ShortestDistance(self.Pos, self.BlockingActor.Pos, false).Magnitude > (self.Diameter + self.BlockingActor.Diameter)*1.2
1788 then
1789 self.BlockingActor = nil
1790 self.teamBlockState = Actor.NOTBLOCKED
1791
1792 if self.AIMode == Actor.AIMODE_BRAINHUNT and self.FollowingActor then
1793 self.FollowingActor = nil
1794 break -- end this behavior
1795 end
1796 elseif self.teamBlockState == Actor.NOTBLOCKED and Waypoint then
1797 if (Waypoint.Pos.X > self.Pos.X and self.BlockingActor.Pos.X > self.Pos.X) or
1798 (Waypoint.Pos.X < self.Pos.X and self.BlockingActor.Pos.X < self.Pos.X)
1799 then
1800 self.teamBlockState = Actor.BLOCKED
1801 if self.AIMode == Actor.AIMODE_BRAINHUNT and self.AIMode == self.BlockingActor.AIMode and
1802 (not self.BlockingActor.MOMoveTarget or self.BlockingActor.MOMoveTarget.ID ~= self.ID)
1803 then -- don't follow an actor that is following us
1804 self.FollowingActor = self.BlockingActor
1805 self:ClearAIWaypoints()
1806 self:AddAIMOWaypoint(self.FollowingActor)
1807 self:CreateGoToBehavior()
1808 end
1809 else
1810 self.BlockingActor = nil
1811 end
1812 end
1813 end
1814
1815 coroutine.yield() -- wait until next frame
1816 end
1817
1818 return true
1819end
1820
1821
1822-- move around another actor
1823function HumanAI.MoveAroundActor(self)
1824 if not MovableMan:IsActor(self.BlockingActor) then
1825 self.teamBlockState = Actor.NOTBLOCKED
1826 self.BlockingActor = nil
1827 return true
1828 end
1829
1830 local BurstTimer = Timer()
1831 local refuel = false
1832 local Dist
1833
1834 BurstTimer:SetSimTimeLimitMS(100) -- a burst last until the BurstTimer expire
1835 self.jump = true
1836
1837 -- look above the blocking actor
1838 Dist = SceneMan:ShortestDistance(self.Pos, self.BlockingActor.Pos, false)
1839 if Dist.X > 0 then
1840 self.Ctrl.AnalogAim = Vector(1,0):RadRotate(1.20)
1841 else
1842 self.Ctrl.AnalogAim = Vector(1,0):RadRotate(1.94)
1843 end
1844
1845 while true do
1846 if BurstTimer:IsPastSimTimeLimit() then -- trigger jetpack bursts
1847 BurstTimer:Reset()
1848 self.jump = false
1849
1850 Dist = SceneMan:ShortestDistance(self.Pos, self.BlockingActor.Pos, false)
1851 if Dist.Y + self.Vel.Y * 3 > (self.Diameter + self.BlockingActor.Diameter)*0.67 then
1852 self:SetAimAngle(-0.5)
1853
1854 if math.abs(Dist.X) > math.max(self.Diameter, self.BlockingActor.Diameter)/2 then
1855 return true
1856 end
1857 end
1858 else
1859 self.jump = true
1860 if self.Vel.Y < -9 then
1861 self.jump = false
1862 end
1863 end
1864
1865 if refuel then
1866 self.jump = false
1867 if self.JetTimeLeft > self.JetTimeTotal * 0.9 then
1868 refuel = false
1869 end
1870 elseif self.JetTimeLeft < self.JetTimeTotal * 0.1 then
1871 refuel = true
1872 end
1873
1874 coroutine.yield() -- wait until next frame
1875 if not MovableMan:IsActor(self.BlockingActor) then
1876 self.teamBlockState = Actor.NOTBLOCKED
1877 self.BlockingActor = nil
1878 return true
1879 end
1880 end
1881
1882 return true
1883end
1884
1885
1886-- open fire on the selected target
1887function HumanAI.ShootTarget(self)
1888 local aimBonus = 100
1889 local rateOfFire = 0
1890
1891 if self.EquippedItem and MovableMan:IsActor(self.Target) then
1892 rateOfFire = ToHDFirearm(self.EquippedItem).RateOfFire
1893 aimBonus = math.max(aimBonus, ToHeldDevice(self.EquippedItem).SharpLength) / 150 -- make the random error vary with the sharp aim length
1894 else
1895 return true
1896 end
1897
1898 local ShootTimer = Timer()
1899 local aimTime = RangeRand(600, 1000)
1900 local TargetAvgVel = self.Target.Vel
1901 local AimPoint
1902
1903 -- in air: shoot from the hip
1904 if self.flying then
1905 aimTime = aimTime * 0.5
1906 aimError = RangeRand(-0.4, 0.4)
1907 else
1908 aimError = RangeRand(-0.2, 0.2) / aimBonus
1909 end
1910
1911 while true do
1912 if not MovableMan:IsActor(self.Target) then
1913 break
1914 end
1915
1916 if self.FirearmIsReady then
1917 TargetAvgVel = TargetAvgVel * 0.4 + self.Target.Vel * 0.6 -- filter the target's velocity
1918
1919 if not self.flying then
1920 self.deviceState = AHuman.AIMING
1921 end
1922
1923 if SceneMan:ShortestDistance(self.Pos, AimPoint or self.Target.Pos, false).Magnitude < 40 then
1924 AimPoint = self.Target.Pos + self.TargetOffset * 0.9 -- move the aimpoint closer to the center of the target at close ranges
1925 else
1926 AimPoint = self.Target.Pos + self.TargetOffset
1927 end
1928
1929 aim = HumanAI.GetHitAngle(AimPoint, TargetAvgVel/RangeRand(0.2, 4.0), ToHeldDevice(self.EquippedItem).Pos, 90)
1930
1931 if aim then
1932 self.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim+aimError+RangeRand(-0.018, 0.018))
1933 else
1934 break -- target out of range
1935 end
1936
1937 if ShootTimer:IsPastSimMS(aimTime) then
1938 self.fire = true
1939
1940 if ToHDFirearm(self.EquippedItem).FullAuto then
1941 aimError = aimError * RangeRand(0.89, 0.95)
1942 else
1943 ShootTimer:Reset()
1944 aimTime = rateOfFire + RangeRand(25, 75)
1945 aimError = aimError * RangeRand(0.7, 0.85)
1946 end
1947 else
1948 self.fire = false
1949 end
1950 elseif self:EquipFirearm(true) then
1951 self.deviceState = AHuman.POINTING
1952 self.fire = false
1953
1954 ShootTimer:Reset()
1955 if ToAHuman(self).FirearmIsEmpty then
1956 self:ReloadFirearm()
1957 aimTime = RangeRand(400, 700)
1958 if self.flying then
1959 aimTime = aimTime * 0.5
1960 aimError = RangeRand(-0.28, 0.28)
1961 else
1962 aimError = RangeRand(-0.14, 0.14) / aimBonus
1963 end
1964 end
1965 else
1966 break -- no firearm avaliable
1967 end
1968
1969 coroutine.yield() -- wait until next frame
1970 end
1971
1972 return true
1973end
1974
1975-- throw a grenade at the selected target
1976function HumanAI.ThrowTarget(self)
1977 local ThrowTimer = Timer()
1978 local aimTime = RangeRand(1000, 1500)
1979 local scan = 0
1980 local miss = 0 -- stop scanning after a few missed atempts
1981 local AimPoint, Dist, MO, ID, rootID, aim, LOS
1982
1983 while true do
1984 if not MovableMan:IsActor(self.Target) then
1985 break
1986 end
1987
1988 if scan < 1 then
1989 if self.Target.Door then
1990 AimPoint = self.Target.Door.Pos
1991 else
1992 AimPoint = self.Target.Pos -- look for the center
1993 if self.Target.EyePos then
1994 AimPoint = (AimPoint + self.Target.EyePos) / 2
1995 end
1996 end
1997
1998 ID = 255
1999 if self:IsWithinRange(Vector(AimPoint.X, AimPoint.Y)) then -- TODO: use grenade properies to decide this
2000 Dist = SceneMan:ShortestDistance(self.EyePos, AimPoint, false)
2001 ID = SceneMan:CastMORay(self.EyePos, Dist, self.ID, grassID, false, 3)
2002 if ID < 1 or ID > 254 then -- not found, look for any head or legs
2003 AimPoint = self.Target.EyePos -- the head
2004 if AimPoint then
2005 coroutine.yield() -- wait until next frame
2006 if not MovableMan:IsActor(self.Target) then -- must verify that the target exist after a yield
2007 break
2008 end
2009
2010 Dist = SceneMan:ShortestDistance(self.EyePos, AimPoint, false)
2011 ID = SceneMan:CastMORay(self.EyePos, Dist, self.ID, grassID, false, 3)
2012 end
2013
2014 if ID < 1 or ID > 254 then
2015 local Legs = self.Target.FGLeg or self.Target.BGLeg -- the legs
2016 if Legs then
2017 coroutine.yield() -- wait until next frame
2018 if not MovableMan:IsActor(self.Target) then -- must verify that the target exist after a yield
2019 break
2020 end
2021
2022 AimPoint = Legs.Pos
2023 Dist = SceneMan:ShortestDistance(self.EyePos, AimPoint, false)
2024 ID = SceneMan:CastMORay(self.EyePos, Dist, self.ID, grassID, false, 3)
2025 end
2026 end
2027 end
2028 else
2029 break -- out of range
2030 end
2031
2032 if ID > 0 and ID < 255 then -- MO found
2033 scan = 6 -- skip the LOS check the next n frames
2034 miss = 0
2035 LOS = true -- we have line of sight to the target
2036
2037 -- check what target we will hit
2038 rootID = MovableMan:GetRootMOID(ID)
2039 if rootID ~= self.Target.ID then
2040 MO = MovableMan:GetMOFromID(rootID)
2041 if MovableMan:IsActor(MO) then
2042 if MO.Team ~= self.Team then
2043 if MO.ClassName == "AHuman" then
2044 self.Target = ToAHuman(MO)
2045 elseif MO.ClassName == "ACrab" then
2046 self.Target = ToACrab(MO)
2047 elseif MO.ClassName == "ACRocket" then
2048 self.Target = ToACRocket(MO)
2049 elseif MO.ClassName == "ACDropShip" then
2050 self.Target = ToACDropShip(MO)
2051 elseif MO.ClassName == "ADoor" then
2052 self.Target = ToADoor(MO)
2053 elseif MO.ClassName == "Actor" then
2054 self.Target = ToActor(MO)
2055 else
2056 break
2057 end
2058 else
2059 break -- don't shoot friendlies
2060 end
2061 end
2062 end
2063 else
2064 miss = miss + 1
2065 if miss > 4 then -- stop looking if we cannot find anything after n atempts
2066 break
2067 else
2068 scan = 3 -- check LOS a little bit more often if no MO was found
2069 end
2070 end
2071
2072 if LOS then -- don't sharp aim until LOS has been confirmed
2073 if self.ThrowableIsReady then
2074 aim = HumanAI.GetHitAngle(AimPoint, self.Target.Vel/RangeRand(0.5, 1.75), ToHeldDevice(self.EquippedItem).MuzzlePos, 18)
2075 if aim then
2076 self.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim+RangeRand(-0.1, 0.1))
2077 else
2078 break -- target out of range
2079 end
2080
2081 if not ThrowTimer:IsPastSimMS(aimTime) then
2082 self.fire = true
2083 else
2084 ThrowTimer:Reset()
2085 self.fire = false
2086 aimTime = RangeRand(1000, 1500)
2087 end
2088 else
2089 break -- no grenades left
2090 end
2091 end
2092 else
2093 scan = scan - 1
2094 end
2095
2096 coroutine.yield() -- wait until next frame
2097 end
2098
2099 return true
2100end
2101
2102-- move to digger range
2103function HumanAI.DigTarget(self)
2104 if MovableMan:IsActor(self.Target) then
2105 if not self.FollowingActor or (not MovableMan:IsActor(self.FollowingActor) or self.FollowingActor.ID ~= self.Target.ID) then
2106 self.PrevAIWaypoint = self:GetLastAIWaypoint()
2107 self:ClearAIWaypoints()
2108 self:AddAIMOWaypoint(self.Target)
2109 self.FollowingActor = self.Target
2110 self.Target = nil
2111 self:CreateGoToBehavior()
2112 end
2113 else
2114 self.Target = nil
2115 end
2116
2117 return true
2118end