1 /*
2 C-Dogs SDL
3 A port of the legendary (and fun) action/arcade cdogs.
4 Copyright (C) 1995 Ronny Wester
5 Copyright (C) 2003 Jeremy Chin
6 Copyright (C) 2003-2007 Lucas Martin-King
7
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License, or
11 (at your option) any later version.
12
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
22 This file incorporates work covered by the following copyright and
23 permission notice:
24
25 Copyright (c) 2013-2015, 2017, 2021 Cong Xu
26 All rights reserved.
27
28 Redistribution and use in source and binary forms, with or without
29 modification, are permitted provided that the following conditions are met:
30
31 Redistributions of source code must retain the above copyright notice, this
32 list of conditions and the following disclaimer.
33 Redistributions in binary form must reproduce the above copyright notice,
34 this list of conditions and the following disclaimer in the documentation
35 and/or other materials provided with the distribution.
36
37 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
38 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
39 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
40 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
41 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
42 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
43 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
44 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
45 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
46 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
47 POSSIBILITY OF SUCH DAMAGE.
48 */
49 #include "ai_utils.h"
50
51 #include <assert.h>
52
53 #include "algorithms.h"
54 #include "collision/collision.h"
55 #include "gamedata.h"
56 #include "map.h"
57 #include "objs.h"
58 #include "path_cache.h"
59 #include "weapon.h"
60
AIGetClosestPlayer(const struct vec2 pos)61 TActor *AIGetClosestPlayer(const struct vec2 pos)
62 {
63 float minDistance2 = -1;
64 TActor *closestPlayer = NULL;
65 CA_FOREACH(const PlayerData, pd, gPlayerDatas)
66 if (!IsPlayerAlive(pd))
67 {
68 continue;
69 }
70 TActor *p = ActorGetByUID(pd->ActorUID);
71 const float distance2 = svec2_distance_squared(pos, p->Pos);
72 if (!closestPlayer || distance2 < minDistance2)
73 {
74 closestPlayer = p;
75 minDistance2 = distance2;
76 }
77 CA_FOREACH_END()
78 return closestPlayer;
79 }
80
AIGetClosestActor(const struct vec2 fromPos,const TActor * from,bool (* compFunc)(const TActor *,const TActor *))81 static TActor *AIGetClosestActor(
82 const struct vec2 fromPos, const TActor *from,
83 bool (*compFunc)(const TActor *, const TActor *))
84 {
85 // Search all the actors and find the closest one that
86 // satisfies the condition
87 TActor *closest = NULL;
88 float minDistance2 = -1;
89 CA_FOREACH(TActor, a, gActors)
90 if (!a->isInUse || a->dead)
91 {
92 continue;
93 }
94 // Never target invulnerables or civilians
95 if (a->flags & (FLAGS_INVULNERABLE | FLAGS_PENALTY))
96 {
97 continue;
98 }
99 if (compFunc(a, from))
100 {
101 const float distance2 = svec2_distance_squared(fromPos, a->Pos);
102 if (!closest || distance2 < minDistance2)
103 {
104 minDistance2 = distance2;
105 closest = a;
106 }
107 }
108 CA_FOREACH_END()
109 return closest;
110 }
111
IsGood(const TActor * a,const TActor * b)112 static bool IsGood(const TActor *a, const TActor *b)
113 {
114 UNUSED(b);
115 return a->PlayerUID >= 0 || (a->flags & FLAGS_GOOD_GUY);
116 }
IsBad(const TActor * a,const TActor * b)117 static bool IsBad(const TActor *a, const TActor *b)
118 {
119 return !IsGood(a, b);
120 }
IsDifferent(const TActor * a,const TActor * b)121 static bool IsDifferent(const TActor *a, const TActor *b)
122 {
123 return a != b;
124 }
AIGetClosestEnemy(const struct vec2 from,const TActor * a,const int flags)125 const TActor *AIGetClosestEnemy(
126 const struct vec2 from, const TActor *a, const int flags)
127 {
128 if (IsPVP(gCampaign.Entry.Mode))
129 {
130 // free for all; look for anybody else
131 return AIGetClosestActor(from, a, IsDifferent);
132 }
133 else if ((!a || a->PlayerUID < 0) && !(flags & FLAGS_GOOD_GUY))
134 {
135 // we are bad; look for good guys
136 return AIGetClosestActor(from, a, IsGood);
137 }
138 else
139 {
140 // we are good; look for bad guys
141 return AIGetClosestActor(from, a, IsBad);
142 }
143 }
144
IsGoodAndVisible(const TActor * a,const TActor * b)145 static bool IsGoodAndVisible(const TActor *a, const TActor *b)
146 {
147 return IsGood(a, b) && (a->flags & FLAGS_VISIBLE);
148 }
IsBadAndVisible(const TActor * a,const TActor * b)149 static bool IsBadAndVisible(const TActor *a, const TActor *b)
150 {
151 return IsBad(a, b) && (a->flags & FLAGS_VISIBLE);
152 }
AIGetClosestVisibleEnemy(const TActor * from,const bool isPlayer)153 const TActor *AIGetClosestVisibleEnemy(const TActor *from, const bool isPlayer)
154 {
155 if (IsPVP(gCampaign.Entry.Mode))
156 {
157 // free for all; look for anybody
158 return AIGetClosestActor(from->Pos, from, IsDifferent);
159 }
160 else if (!isPlayer && !(from->flags & FLAGS_GOOD_GUY))
161 {
162 // we are bad; look for good guys
163 return AIGetClosestActor(from->Pos, from, IsGoodAndVisible);
164 }
165 else
166 {
167 // we are good; look for bad guys
168 return AIGetClosestActor(from->Pos, from, IsBadAndVisible);
169 }
170 }
171
AIGetClosestPlayerPos(const struct vec2 pos)172 struct vec2 AIGetClosestPlayerPos(const struct vec2 pos)
173 {
174 TActor *closestPlayer = AIGetClosestPlayer(pos);
175 if (closestPlayer)
176 {
177 return closestPlayer->Pos;
178 }
179 else
180 {
181 return pos;
182 }
183 }
184
AIReverseDirection(int cmd)185 int AIReverseDirection(int cmd)
186 {
187 if (cmd & (CMD_LEFT | CMD_RIGHT))
188 {
189 cmd ^= CMD_LEFT | CMD_RIGHT;
190 }
191 if (cmd & (CMD_UP | CMD_DOWN))
192 {
193 cmd ^= CMD_UP | CMD_DOWN;
194 }
195 return cmd;
196 }
197
198 typedef bool (*IsBlockedFunc)(void *, const struct vec2i);
199 static bool AIHasClearLine(
200 struct vec2i from, struct vec2i to, IsBlockedFunc isBlockedFunc);
201 static bool IsTileNoWalk(void *data, const struct vec2i pos);
202 static bool IsTileNoWalkAroundObjects(void *data, const struct vec2i pos);
AIHasClearPath(const struct vec2 from,const struct vec2 to,const bool ignoreObjects)203 bool AIHasClearPath(
204 const struct vec2 from, const struct vec2 to, const bool ignoreObjects)
205 {
206 IsBlockedFunc f = ignoreObjects ? IsTileNoWalk : IsTileNoWalkAroundObjects;
207 return AIHasClearLine(Vec2ToTile(from), Vec2ToTile(to), f);
208 }
AIHasClearLine(struct vec2i from,struct vec2i to,IsBlockedFunc isBlockedFunc)209 static bool AIHasClearLine(
210 struct vec2i from, struct vec2i to, IsBlockedFunc isBlockedFunc)
211 {
212 // Find all tiles that overlap with the line (from, to)
213 HasClearLineData data;
214 data.IsBlocked = isBlockedFunc;
215 data.data = &gMap;
216
217 return HasClearLineJMRaytrace(from, to, &data);
218 }
219 static bool IsTileWalkableOrOpenable(Map *map, struct vec2i pos);
IsTileWalkable(Map * map,const struct vec2i pos)220 bool IsTileWalkable(Map *map, const struct vec2i pos)
221 {
222 if (!IsTileWalkableOrOpenable(map, pos))
223 {
224 return false;
225 }
226 // Check if tile has a dangerous (explosive) item on it
227 // For AI, we don't want to shoot it, so just walk around
228 Tile *t = MapGetTile(map, pos);
229 CA_FOREACH(ThingId, tid, t->things)
230 // Only look for explosive objects
231 if (tid->Kind != KIND_OBJECT)
232 {
233 continue;
234 }
235 const TObject *o = CArrayGet(&gObjs, tid->Id);
236 if (ObjIsDangerous(o))
237 {
238 return false;
239 }
240 CA_FOREACH_END()
241 return true;
242 }
IsTileNoWalk(void * data,const struct vec2i pos)243 static bool IsTileNoWalk(void *data, const struct vec2i pos)
244 {
245 return !IsTileWalkable(data, pos);
246 }
IsTileWalkableAroundObjects(Map * map,const struct vec2i pos)247 bool IsTileWalkableAroundObjects(Map *map, const struct vec2i pos)
248 {
249 if (!IsTileWalkableOrOpenable(map, pos))
250 {
251 return false;
252 }
253 // Check if tile has any item on it
254 Tile *t = MapGetTile(map, pos);
255 CA_FOREACH(ThingId, tid, t->things)
256 if (tid->Kind == KIND_OBJECT)
257 {
258 // Check that the object has hitbox - i.e. health > 0
259 const TObject *o = CArrayGet(&gObjs, tid->Id);
260 if (o->Health > 0)
261 {
262 return false;
263 }
264 }
265 else if (tid->Kind == KIND_CHARACTER)
266 {
267 switch (gCollisionSystem.allyCollision)
268 {
269 case ALLYCOLLISION_NORMAL:
270 return false;
271 case ALLYCOLLISION_REPEL:
272 // TODO: implement
273 // Need to know collision team of player
274 // to know if collision will result in repelling
275 break;
276 case ALLYCOLLISION_NONE:
277 continue;
278 default:
279 CASSERT(false, "unknown collision type");
280 break;
281 }
282 }
283 CA_FOREACH_END()
284 return true;
285 }
IsTileNoWalkAroundObjects(void * data,const struct vec2i pos)286 static bool IsTileNoWalkAroundObjects(void *data, const struct vec2i pos)
287 {
288 return !IsTileWalkableAroundObjects(data, pos);
289 }
IsTileWalkableOrOpenable(Map * map,struct vec2i pos)290 static bool IsTileWalkableOrOpenable(Map *map, struct vec2i pos)
291 {
292 const Tile *tile = MapGetTile(map, pos);
293 if (tile == NULL)
294 {
295 return false;
296 }
297 if (TileCanWalk(tile))
298 {
299 return true;
300 }
301 if (tile->Class->Type == TILE_CLASS_DOOR)
302 {
303 // A door; check if we can open it
304 int keycard = MapGetDoorKeycardFlag(map, pos);
305 if (!keycard)
306 {
307 // Unlocked door
308 return true;
309 }
310 return !!(keycard & gMission.KeyFlags);
311 }
312 // Otherwise, we cannot walk over this tile
313 return false;
314 }
315
IsPosNoSee(void * data,struct vec2i pos)316 static bool IsPosNoSee(void *data, struct vec2i pos)
317 {
318 return TileIsOpaque(MapGetTile(data, Vec2iToTile(pos)));
319 }
AIHasClearView(const TActor * a,const struct vec2 to,const int sightRange)320 bool AIHasClearView(
321 const TActor *a, const struct vec2 to, const int sightRange)
322 {
323 if (svec2_distance_squared(a->Pos, to) > SQUARED(sightRange))
324 {
325 return false;
326 }
327 return AIHasClearLine(
328 svec2i_assign_vec2(a->Pos), svec2i_assign_vec2(to), IsPosNoSee);
329 }
AICanSee(const TActor * a,const struct vec2 target,const direction_e d)330 bool AICanSee(const TActor *a, const struct vec2 target, const direction_e d)
331 {
332 const int sightRange =
333 ConfigGetInt(&gConfig, "Game.SightRange") * TILE_WIDTH;
334 if (AIIsFacing(a, target, d))
335 {
336 return AIHasClearView(a, target, sightRange * 2 / 3);
337 }
338 else if (
339 AIIsFacing(a, target, DirectionRotate(d, 1)) ||
340 AIIsFacing(a, target, DirectionRotate(d, -1)))
341 {
342 const int peripheralRange = sightRange / 3;
343 return AIHasClearView(a, target, peripheralRange);
344 }
345 return false;
346 }
347
IsPosShootable(void * data,const struct vec2i pos)348 static bool IsPosShootable(void *data, const struct vec2i pos)
349 {
350 return TileIsShootable(MapGetTile(data, Vec2iToTile(pos)));
351 }
AIHasClearShot(const struct vec2 from,const struct vec2 to)352 bool AIHasClearShot(const struct vec2 from, const struct vec2 to)
353 {
354 // Perform 4 line tests - above, below, left and right
355 // This is to account for possible positions for the muzzle
356 struct vec2 fromOffset = from;
357
358 const int pad = 2;
359 fromOffset.x = from.x - (ACTOR_W + pad) / 2;
360 if (Vec2ToTile(fromOffset).x >= 0 &&
361 !AIHasClearLine(
362 svec2i_assign_vec2(fromOffset), svec2i_assign_vec2(to),
363 IsPosShootable))
364 {
365 return false;
366 }
367 fromOffset.x = from.x + (ACTOR_W + pad) / 2;
368 if (Vec2ToTile(fromOffset).x < gMap.Size.x &&
369 !AIHasClearLine(
370 svec2i_assign_vec2(fromOffset), svec2i_assign_vec2(to),
371 IsPosShootable))
372 {
373 return false;
374 }
375 fromOffset.x = from.x;
376 fromOffset.y = from.y - (ACTOR_H + pad) / 2;
377 if (Vec2ToTile(fromOffset).y >= 0 &&
378 !AIHasClearLine(
379 svec2i_assign_vec2(fromOffset), svec2i_assign_vec2(to),
380 IsPosShootable))
381 {
382 return false;
383 }
384 fromOffset.y = from.y + (ACTOR_H + pad) / 2;
385 if (Vec2ToTile(fromOffset).y < gMap.Size.y &&
386 !AIHasClearLine(
387 svec2i_assign_vec2(fromOffset), svec2i_assign_vec2(to),
388 IsPosShootable))
389 {
390 return false;
391 }
392 return true;
393 }
394
AIGetObjectRunningInto(TActor * a,int cmd)395 TObject *AIGetObjectRunningInto(TActor *a, int cmd)
396 {
397 // Check the position just in front of the character;
398 // check if there's a (non-dangerous) object in front of it
399 struct vec2 frontPos = a->Pos;
400 Thing *item;
401 if (cmd & CMD_LEFT)
402 {
403 frontPos.x--;
404 }
405 else if (cmd & CMD_RIGHT)
406 {
407 frontPos.x++;
408 }
409 if (cmd & CMD_UP)
410 {
411 frontPos.y--;
412 }
413 else if (cmd & CMD_DOWN)
414 {
415 frontPos.y++;
416 }
417 const CollisionParams params = {
418 THING_IMPASSABLE, CalcCollisionTeam(true, a),
419 IsPVP(gCampaign.Entry.Mode), false};
420 item = OverlapGetFirstItem(
421 &a->thing, frontPos, a->thing.size, a->thing.Vel, params);
422 if (!item || item->kind != KIND_OBJECT)
423 {
424 return NULL;
425 }
426 return CArrayGet(&gObjs, item->id);
427 }
428
AIIsFacing(const TActor * a,const struct vec2 target,const direction_e d)429 bool AIIsFacing(const TActor *a, const struct vec2 target, const direction_e d)
430 {
431 const bool isUpperOrLowerOctants =
432 fabsf(a->Pos.x - target.x) < fabsf(a->Pos.y - target.y);
433 const bool isRight = a->Pos.x < target.x;
434 const bool isAbove = a->Pos.y > target.y;
435 switch (d)
436 {
437 case DIRECTION_UP:
438 return isAbove && isUpperOrLowerOctants;
439 case DIRECTION_UPLEFT:
440 return !isRight && isAbove;
441 case DIRECTION_LEFT:
442 return !isRight && !isUpperOrLowerOctants;
443 case DIRECTION_DOWNLEFT:
444 return !isRight && !isAbove;
445 case DIRECTION_DOWN:
446 return !isAbove && isUpperOrLowerOctants;
447 case DIRECTION_DOWNRIGHT:
448 return isRight && !isAbove;
449 case DIRECTION_RIGHT:
450 return isRight && !isUpperOrLowerOctants;
451 case DIRECTION_UPRIGHT:
452 return isRight && isAbove;
453 default:
454 CASSERT(false, "invalid direction");
455 break;
456 }
457 return false;
458 }
459
460 typedef struct
461 {
462 int ActorUID;
463 CollisionTeam CT;
464 bool HasFriendly;
465 } FindFriendliesInTileData;
466 static bool FindFriendliesInTile(void *data, const struct vec2i tile);
467 // Whether there are friendlies in the direct line of the gun's range
AIHasFriendliesInLine(const TActor * a,const direction_e dir)468 static bool AIHasFriendliesInLine(const TActor *a, const direction_e dir)
469 {
470 const struct vec2i tileStart = Vec2ToTile(a->Pos);
471 const struct vec2 d = Vec2FromRadians(dir2radians[dir]);
472 const WeaponClass *wc = ACTOR_GET_WEAPON(a)->Gun;
473 const float gunRange = WeaponClassGetRange(wc);
474 const struct vec2 dv = svec2_scale(d, gunRange);
475 const struct vec2 posEnd = svec2_add(a->Pos, dv);
476 const struct vec2i tileEnd = Vec2ToTile(posEnd);
477
478 HasClearLineData data;
479 data.IsBlocked = FindFriendliesInTile;
480 FindFriendliesInTileData tData;
481 tData.ActorUID = a->uid;
482 tData.CT = CalcCollisionTeam(true, a);
483 tData.CT = CalcCollisionTeam(true, a);
484 tData.HasFriendly = false;
485 data.data = &tData;
486 HasClearLineJMRaytrace(tileStart, tileEnd, &data);
487 return tData.HasFriendly;
488 }
FindFriendliesInTile(void * data,const struct vec2i tile)489 static bool FindFriendliesInTile(void *data, const struct vec2i tile)
490 {
491 const Tile *t = MapGetTile(&gMap, tile);
492 if (t == NULL)
493 return true;
494 FindFriendliesInTileData *tData = data;
495 CA_FOREACH(const ThingId, tid, t->things)
496 if (tid->Kind != KIND_CHARACTER)
497 continue;
498 const TActor *other = CArrayGet(&gActors, tid->Id);
499 // Don't worry about self
500 if (tData->ActorUID == other->uid)
501 continue;
502 // Never shoot prisoners/victims... it's not nice ;)
503 if (other->flags & (FLAGS_PRISONER | FLAGS_VICTIM))
504 {
505 tData->HasFriendly = true;
506 return true;
507 }
508 const CollisionTeam ctOther = CalcCollisionTeam(true, other);
509 if (tData->CT != COLLISIONTEAM_NONE && ctOther != COLLISIONTEAM_NONE &&
510 tData->CT == ctOther)
511 {
512 tData->HasFriendly = true;
513 return true;
514 }
515 // If it's an enemy, do shoot!
516 return true;
517 CA_FOREACH_END()
518 return false;
519 }
520
521 // Use pathfinding to check that there is a path between
522 // source and destination tiles
AIHasPath(const struct vec2 from,const struct vec2 to,const bool ignoreObjects)523 bool AIHasPath(
524 const struct vec2 from, const struct vec2 to, const bool ignoreObjects)
525 {
526 // Quick first test: check there is a clear path
527 if (AIHasClearPath(from, to, ignoreObjects))
528 {
529 return true;
530 }
531 // Pathfind
532 const struct vec2i fromTile = Vec2ToTile(from);
533 const struct vec2i toTile = MapSearchTileAround(
534 &gMap, Vec2ToTile(to),
535 ignoreObjects ? IsTileWalkable : IsTileWalkableAroundObjects);
536 CachedPath path =
537 PathCacheCreate(&gPathCache, fromTile, toTile, ignoreObjects, true);
538 const size_t pathCount = ASPathGetCount(path.Path);
539 CachedPathDestroy(&path);
540 return pathCount >= 1;
541 }
542
AIGotoDirect(const struct vec2 a,const struct vec2 p)543 int AIGotoDirect(const struct vec2 a, const struct vec2 p)
544 {
545 int cmd = 0;
546
547 if (a.x + 1 < p.x)
548 cmd |= CMD_RIGHT;
549 else if (a.x > p.x + 1)
550 cmd |= CMD_LEFT;
551
552 if (a.y + 1 < p.y)
553 cmd |= CMD_DOWN;
554 else if (a.y > p.y + 1)
555 cmd |= CMD_UP;
556
557 return cmd;
558 }
559
560 // Follow the current A* path
AStarFollow(AIGotoContext * c,const struct vec2i currentTile,const Thing * i,const struct vec2 a)561 static int AStarFollow(
562 AIGotoContext *c, const struct vec2i currentTile, const Thing *i,
563 const struct vec2 a)
564 {
565 struct vec2i *pathTile = ASPathGetNode(c->Path.Path, c->PathIndex);
566 c->IsFollowing = 1;
567 // Check if we need to follow the next step in the path
568 // Note: need to make sure the actor is fully within the current tile
569 // otherwise it may get stuck at corners
570 if (svec2i_is_equal(currentTile, *pathTile) &&
571 IsThingInsideTile(i, currentTile))
572 {
573 c->PathIndex++;
574 pathTile = ASPathGetNode(c->Path.Path, c->PathIndex);
575 }
576 // Go directly to the center of the next tile
577 return AIGotoDirect(a, Vec2CenterOfTile(*pathTile));
578 }
579 // Check that we are still close to the start of the A* path,
580 // and the end of the path is close to our goal
AStarCloseToPath(AIGotoContext * c,struct vec2i currentTile,struct vec2i goalTile)581 static int AStarCloseToPath(
582 AIGotoContext *c, struct vec2i currentTile, struct vec2i goalTile)
583 {
584 struct vec2i *pathTile;
585 struct vec2i *pathEnd;
586 if (!c || c->PathIndex >=
587 (int)ASPathGetCount(c->Path.Path) - 1) // at end of path
588 {
589 return 0;
590 }
591 // Check if we're too far from the current start of the path
592 pathTile = ASPathGetNode(c->Path.Path, c->PathIndex);
593 if ((int)svec2i_distance_squared(
594 currentTile, svec2i(pathTile->x, pathTile->y)) > 4)
595 {
596 return 0;
597 }
598 // Check if we're too far from the end of the path
599 pathEnd = ASPathGetNode(c->Path.Path, ASPathGetCount(c->Path.Path) - 1);
600 if ((int)svec2i_distance_squared(
601 goalTile, svec2i(pathEnd->x, pathEnd->y)) > 0)
602 {
603 return 0;
604 }
605 return 1;
606 }
AIGoto(const TActor * actor,const struct vec2 p,const bool ignoreObjects)607 int AIGoto(const TActor *actor, const struct vec2 p, const bool ignoreObjects)
608 {
609 const struct vec2i currentTile = Vec2ToTile(actor->Pos);
610 const struct vec2i goalTile = Vec2ToTile(p);
611 AIGotoContext *c = &actor->aiContext->Goto;
612
613 CASSERT(c != NULL, "no AI context");
614
615 // If we are already there, go directly to the goal
616 // This can happen if AI is trying to track the player,
617 // but the player has died, for example.
618 if (svec2i_is_equal(currentTile, goalTile))
619 {
620 return AIGotoDirect(actor->Pos, p);
621 }
622
623 // If we are currently following an A* path,
624 // and it is still valid, keep following it until
625 // we have reached a new tile
626 if (c && c->IsFollowing && AStarCloseToPath(c, currentTile, goalTile))
627 {
628 return AStarFollow(c, currentTile, &actor->thing, actor->Pos);
629 }
630 else if (AIHasClearPath(actor->Pos, p, ignoreObjects))
631 {
632 // Simple case: if there's a clear line between AI and target,
633 // walk straight towards it
634 return AIGotoDirect(actor->Pos, p);
635 }
636 else
637 {
638 // We need to recalculate A*
639
640 // First, if the goal tile is blocked itself,
641 // find a nearby tile that can be walked to
642 c->Goal = MapSearchTileAround(
643 &gMap, goalTile,
644 ignoreObjects ? IsTileWalkable : IsTileWalkableAroundObjects);
645
646 c->PathIndex = 1; // start navigating to the next path node
647 CachedPathDestroy(&c->Path);
648 c->Path = PathCacheCreate(
649 &gPathCache, currentTile, c->Goal, ignoreObjects, true);
650
651 // In case we can't calculate A* for some reason,
652 // try simple navigation again
653 if (ASPathGetCount(c->Path.Path) <= 1)
654 {
655 return AIGotoDirect(actor->Pos, p);
656 }
657
658 return AStarFollow(c, currentTile, &actor->thing, actor->Pos);
659 }
660 }
661
662 // Hunt moves an Actor towards a target, using the most efficient direction.
663 // That is, given the following octant:
664 // x A xxxx
665 // x xxxx
666 // x xxxx
667 // x xxxx B
668 // x xxx
669 // xxxxxxxxxxxxxxxxxxxxxxx
670 // Those in slice A will move down-left and those in slice B will move left.
AIHunt(const TActor * actor,const struct vec2 targetPos)671 int AIHunt(const TActor *actor, const struct vec2 targetPos)
672 {
673 const struct vec2 pos =
674 svec2_add(actor->Pos, ActorGetAverageWeaponMuzzleOffset(actor));
675 const float dx = fabsf(targetPos.x - pos.x);
676 const float dy = fabsf(targetPos.y - pos.y);
677
678 int cmd = 0;
679 if (2 * dx > dy)
680 {
681 if (pos.x < targetPos.x)
682 cmd |= CMD_RIGHT;
683 else if (pos.x > targetPos.x)
684 cmd |= CMD_LEFT;
685 }
686 if (2 * dy > dx)
687 {
688 if (pos.y < targetPos.y)
689 cmd |= CMD_DOWN;
690 else if (pos.y > targetPos.y)
691 cmd |= CMD_UP;
692 }
693 // If it's a coward, reverse directions...
694 if (actor->flags & FLAGS_RUNS_AWAY)
695 {
696 cmd = AIReverseDirection(cmd);
697 }
698
699 return cmd;
700 }
AIHuntClosest(TActor * actor)701 int AIHuntClosest(TActor *actor)
702 {
703 struct vec2 targetPos = actor->Pos;
704 if (!(actor->PlayerUID >= 0 || (actor->flags & FLAGS_GOOD_GUY)))
705 {
706 targetPos = AIGetClosestPlayerPos(actor->Pos);
707 }
708
709 if (actor->flags & FLAGS_VISIBLE)
710 {
711 const TActor *a =
712 AIGetClosestVisibleEnemy(actor, actor->PlayerUID >= 0);
713 if (a)
714 {
715 targetPos = a->Pos;
716 }
717 }
718 return AIHunt(actor, targetPos);
719 }
720
721 // Smarter attack routine:
722 // - Move/position towards target, keeping ideal distance from it
723 // - Fire if
724 // - has clear view to target, and
725 // - no friendlies in the way
AIAttack(const TActor * a,const struct vec2 targetPos)726 int AIAttack(const TActor *a, const struct vec2 targetPos)
727 {
728 // Move to the ideal distance for the weapon
729 int cmd = 0;
730 const Weapon *w = ACTOR_GET_WEAPON(a);
731 const WeaponClass *wc = w->Gun;
732 const float gunRange = WeaponClassGetRange(wc);
733 const float distanceSquared = svec2_distance_squared(a->Pos, targetPos);
734 const bool canFire =
735 WeaponClassCanShoot(wc) && WeaponGetUnlockedBarrel(w) >= 0;
736 if ((double)distanceSquared <
737 SQUARED(gunRange * 3) * a->aiContext->GunRangeScalar &&
738 !canFire)
739 {
740 // Move away from the enemy because we're too close
741 // Only move away if we can't fire; otherwise turn to fire
742 cmd = AIRetreatFrom(a, targetPos);
743 }
744 else
745 {
746 // Move towards the enemy, fire if able
747 // But don't bother firing if too far away
748
749 if ((double)distanceSquared > SQUARED(gunRange * 2))
750 {
751 // Too far away; approach using most efficient method
752 cmd = AIHunt(a, targetPos);
753 }
754 else
755 {
756 #define MINIMUM_GUN_DISTANCE 30
757 const bool willFire =
758 canFire && (distanceSquared < SQUARED(gunRange * 2) ||
759 distanceSquared > SQUARED(MINIMUM_GUN_DISTANCE));
760 if (willFire)
761 {
762 // Hunt; this is the best direction to attack in
763 cmd = AIHunt(a, targetPos);
764 }
765 else
766 {
767 // Track so that we end up in a favorable angle
768 cmd = AITrack(a, targetPos);
769 }
770 // Don't fire if there's a friendly in the way
771 const direction_e d = cmd2dir[cmd & CMD_DIRECTIONS];
772 if (willFire && !AIHasFriendliesInLine(a, d))
773 {
774 cmd |= CMD_BUTTON1;
775 }
776 }
777 }
778 return cmd;
779 }
780
781 // Move away from the target
782 // Usually used for a simple flee
AIRetreatFrom(const TActor * actor,const struct vec2 from)783 int AIRetreatFrom(const TActor *actor, const struct vec2 from)
784 {
785 return AIReverseDirection(AIHunt(actor, from));
786 }
787
788 // Track moves an Actor towards a target, but in such a fashion that the Actor
789 // will come into 8-axis alignment with the target soonest.
790 // That is, given the following octant:
791 // x A xxxx
792 // x xxxx
793 // x xxxx
794 // x xxxx B
795 // x xxx
796 // xxxxxxxxxxxxxxxxxxxxxxx
797 // Those in slice A will move left and those in slice B will move down-left.
AITrack(const TActor * actor,const struct vec2 targetPos)798 int AITrack(const TActor *actor, const struct vec2 targetPos)
799 {
800 const struct vec2 pos =
801 svec2_add(actor->Pos, ActorGetAverageWeaponMuzzleOffset(actor));
802 const float dx = fabsf(targetPos.x - pos.x);
803 const float dy = fabsf(targetPos.y - pos.y);
804
805 int cmd = 0;
806 // Terminology: imagine the compass directions sliced into 16 equal parts,
807 // and labelled in bearing/clock order. That is, the slices on the right
808 // half are labelled 1-8.
809 // In order to construct the movement, note that:
810 // - If the target is to our left, we need to move left...
811 // - Except if they are in slice 2 or 7
812 // Slice 2 and 7 (and 10 and 15) can be found by (dy < 2dx && dy > dx)
813 // - call this X-exception
814 // Repeat this for all 4 cardinal directions.
815 // Furthermore, give a bit of leeway for the 8 axes so we don't
816 // fluctuate between perpendicular movement vectors.
817 const bool xException = dy < 2 * dx && dy > dx * 1.1f;
818 if (!xException)
819 {
820 if (pos.x - targetPos.x < dy * 0.1f)
821 cmd |= CMD_RIGHT;
822 else if (pos.x - targetPos.x > -dy * 0.1f)
823 cmd |= CMD_LEFT;
824 }
825 const bool yException = dx < 2 * dy && dx > dy * 1.1f;
826 if (!yException)
827 {
828 if (pos.y - targetPos.y < dx * 0.1f)
829 cmd |= CMD_DOWN;
830 else if (pos.y - targetPos.y > -dx * 0.1f)
831 cmd |= CMD_UP;
832 }
833
834 return cmd;
835 }
836
AIMoveAwayFromLine(const struct vec2 pos,const struct vec2 lineStart,const direction_e lineD)837 int AIMoveAwayFromLine(
838 const struct vec2 pos, const struct vec2 lineStart,
839 const direction_e lineD)
840 {
841 const struct vec2 dv = svec2_subtract(pos, lineStart);
842 switch (lineD)
843 {
844 case DIRECTION_UP:
845 case DIRECTION_DOWN: // fallthrough
846 return dv.x < 0 ? CMD_LEFT : CMD_RIGHT;
847 case DIRECTION_UPLEFT:
848 case DIRECTION_DOWNRIGHT: // fallthrough
849 return dv.x > dv.y ? (CMD_RIGHT | CMD_UP) : (CMD_LEFT | CMD_DOWN);
850 case DIRECTION_LEFT:
851 case DIRECTION_RIGHT: // fallthrough
852 return dv.y < 0 ? CMD_UP : CMD_DOWN;
853 case DIRECTION_DOWNLEFT:
854 case DIRECTION_UPRIGHT: // fallthrough
855 return dv.x < -dv.y ? (CMD_LEFT | CMD_UP) : (CMD_RIGHT | CMD_DOWN);
856 default:
857 CASSERT(false, "unknown direction");
858 return 0;
859 }
860 }
861