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