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-2014, 2016-2017, 2020-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.h"
50 
51 #include <assert.h>
52 #include <stdlib.h>
53 
54 #include "actor_placement.h"
55 #include "actors.h"
56 #include "ai_utils.h"
57 #include "collision/collision.h"
58 #include "config.h"
59 #include "defs.h"
60 #include "events.h"
61 #include "game_events.h"
62 #include "gamedata.h"
63 #include "handle_game_events.h"
64 #include "mission.h"
65 #include "net_util.h"
66 #include "sys_specifics.h"
67 #include "utils.h"
68 
69 #define AI_WAKE_SOUND_RANGE (8 * TILE_WIDTH)
70 #define AI_WAKE_SOUND_RANGE_INDIRECT (4 * TILE_WIDTH)
71 
72 static int gBaddieCount = 0;
73 static bool sAreGoodGuysPresent = false;
74 
IsFacingPlayer(TActor * actor,direction_e d)75 static bool IsFacingPlayer(TActor *actor, direction_e d)
76 {
77 	CA_FOREACH(const PlayerData, p, gPlayerDatas)
78 	if (!IsPlayerAlive(p))
79 	{
80 		continue;
81 	}
82 	const TActor *player = ActorGetByUID(p->ActorUID);
83 	if (AIIsFacing(actor, player->Pos, d))
84 	{
85 		return true;
86 	}
87 	CA_FOREACH_END()
88 	return false;
89 }
90 
91 #define Distance(a, b) CHEBYSHEV_DISTANCE(a->x, a->y, b->x, b->y)
92 
IsCloseToPlayer(const struct vec2 pos,const float distance)93 static bool IsCloseToPlayer(const struct vec2 pos, const float distance)
94 {
95 	const TActor *closestPlayer = AIGetClosestPlayer(pos);
96 	const float distance2 = distance * distance;
97 	return closestPlayer &&
98 		   svec2_distance_squared(pos, closestPlayer->Pos) < distance2;
99 }
100 
CanSeeActor(const TActor * a,const TActor * target)101 static bool CanSeeActor(const TActor *a, const TActor *target)
102 {
103 	// Can see if:
104 	// - They are close
105 	// - Or if they can see them with line of sight
106 	const float distance2 = svec2_distance_squared(a->Pos, target->Pos);
107 	const bool isClose = distance2 < SQUARED(16 * 2);
108 	return isClose || AICanSee(a, target->Pos, a->direction);
109 }
110 
CanSeeAPlayer(const TActor * a)111 static bool CanSeeAPlayer(const TActor *a)
112 {
113 	CA_FOREACH(const PlayerData, p, gPlayerDatas)
114 	if (!IsPlayerAlive(p))
115 	{
116 		continue;
117 	}
118 	const TActor *player = ActorGetByUID(p->ActorUID);
119 	if (CanSeeActor(a, player))
120 	{
121 		return true;
122 	}
123 	CA_FOREACH_END()
124 	return false;
125 }
126 
CanSeeActorBeingAttacked(const TActor * a)127 static bool CanSeeActorBeingAttacked(const TActor *a)
128 {
129 	CA_FOREACH(const TActor, target, gActors)
130 	if (!target->isInUse)
131 	{
132 		continue;
133 	}
134 	// Use grimacing as a proxy to being attacked / being aggressive
135 	if (!ActorIsGrimacing(target) && target->dead == 0)
136 	{
137 		continue;
138 	}
139 	if (CanSeeActor(a, target))
140 	{
141 		return true;
142 	}
143 	CA_FOREACH_END()
144 	return false;
145 }
146 
IsPosOK(const TActor * actor,const struct vec2 pos)147 static bool IsPosOK(const TActor *actor, const struct vec2 pos)
148 {
149 	if (IsCollisionDiamond(&gMap, pos, actor->thing.size))
150 	{
151 		return false;
152 	}
153 	const CollisionParams params = {
154 		THING_IMPASSABLE, CalcCollisionTeam(true, actor),
155 		IsPVP(gCampaign.Entry.Mode), false};
156 	if (OverlapGetFirstItem(
157 			&actor->thing, pos, actor->thing.size, actor->thing.Vel, params))
158 	{
159 		return false;
160 	}
161 	return true;
162 }
163 
164 #define STEPSIZE 4
165 
IsDirectionOK(const TActor * a,const int dir)166 static bool IsDirectionOK(const TActor *a, const int dir)
167 {
168 	switch (dir)
169 	{
170 	case DIRECTION_UP:
171 		return IsPosOK(a, svec2_add(a->Pos, svec2(0, -STEPSIZE)));
172 	case DIRECTION_UPLEFT:
173 		return IsPosOK(a, svec2_add(a->Pos, svec2(-STEPSIZE, -STEPSIZE))) ||
174 			   IsPosOK(a, svec2_add(a->Pos, svec2(-STEPSIZE, 0))) ||
175 			   IsPosOK(a, svec2_add(a->Pos, svec2(0, -STEPSIZE)));
176 	case DIRECTION_LEFT:
177 		return IsPosOK(a, svec2_add(a->Pos, svec2(-STEPSIZE, 0)));
178 	case DIRECTION_DOWNLEFT:
179 		return IsPosOK(a, svec2_add(a->Pos, svec2(-STEPSIZE, STEPSIZE))) ||
180 			   IsPosOK(a, svec2_add(a->Pos, svec2(-STEPSIZE, 0))) ||
181 			   IsPosOK(a, svec2_add(a->Pos, svec2(0, STEPSIZE)));
182 	case DIRECTION_DOWN:
183 		return IsPosOK(a, svec2_add(a->Pos, svec2(0, STEPSIZE)));
184 	case DIRECTION_DOWNRIGHT:
185 		return IsPosOK(a, svec2_add(a->Pos, svec2(STEPSIZE, STEPSIZE))) ||
186 			   IsPosOK(a, svec2_add(a->Pos, svec2(STEPSIZE, 0))) ||
187 			   IsPosOK(a, svec2_add(a->Pos, svec2(0, STEPSIZE)));
188 	case DIRECTION_RIGHT:
189 		return IsPosOK(a, svec2_add(a->Pos, svec2(STEPSIZE, 0)));
190 	case DIRECTION_UPRIGHT:
191 		return IsPosOK(a, svec2_add(a->Pos, svec2(STEPSIZE, -STEPSIZE))) ||
192 			   IsPosOK(a, svec2_add(a->Pos, svec2(STEPSIZE, 0))) ||
193 			   IsPosOK(a, svec2_add(a->Pos, svec2(0, -STEPSIZE)));
194 	}
195 	return 0;
196 }
197 
BrightWalk(TActor * actor,int roll)198 static int BrightWalk(TActor *actor, int roll)
199 {
200 	const CharBot *bot = ActorGetCharacter(actor)->bot;
201 	if (!!(actor->flags & FLAGS_VISIBLE) && roll < bot->probabilityToTrack)
202 	{
203 		actor->flags &= ~FLAGS_DETOURING;
204 		return AIHuntClosest(actor);
205 	}
206 
207 	if (actor->flags & FLAGS_TRYRIGHT)
208 	{
209 		if (IsDirectionOK(actor, (actor->direction + 7) % 8))
210 		{
211 			actor->direction = (actor->direction + 7) % 8;
212 			actor->turns--;
213 			if (actor->turns == 0)
214 			{
215 				actor->flags &= ~FLAGS_DETOURING;
216 			}
217 		}
218 		else if (!IsDirectionOK(actor, actor->direction))
219 		{
220 			actor->direction = (actor->direction + 1) % 8;
221 			actor->turns++;
222 			if (actor->turns == 4)
223 			{
224 				actor->flags &= ~(FLAGS_DETOURING | FLAGS_TRYRIGHT);
225 				actor->turns = 0;
226 			}
227 		}
228 	}
229 	else
230 	{
231 		if (IsDirectionOK(actor, (actor->direction + 1) % 8))
232 		{
233 			actor->direction = (actor->direction + 1) % 8;
234 			actor->turns--;
235 			if (actor->turns == 0)
236 				actor->flags &= ~FLAGS_DETOURING;
237 		}
238 		else if (!IsDirectionOK(actor, actor->direction))
239 		{
240 			actor->direction = (actor->direction + 7) % 8;
241 			actor->turns++;
242 			if (actor->turns == 4)
243 			{
244 				actor->flags &= ~(FLAGS_DETOURING | FLAGS_TRYRIGHT);
245 				actor->turns = 0;
246 			}
247 		}
248 	}
249 	return DirectionToCmd(actor->direction);
250 }
251 
WillFire(TActor * actor,int roll)252 static int WillFire(TActor *actor, int roll)
253 {
254 	const CharBot *bot = ActorGetCharacter(actor)->bot;
255 	if ((actor->flags & FLAGS_VISIBLE) != 0 &&
256 		roll < bot->probabilityToShoot &&
257 		ActorGetCanFireBarrel(actor, ACTOR_GET_WEAPON(actor)) >= 0)
258 	{
259 		if ((actor->flags & FLAGS_GOOD_GUY) != 0)
260 			return 1; //! FacingPlayer( actor);
261 		else if (sAreGoodGuysPresent)
262 		{
263 			return 1;
264 		}
265 		else
266 		{
267 			return IsFacingPlayer(actor, actor->direction);
268 		}
269 	}
270 	return 0;
271 }
272 
Detour(TActor * actor)273 void Detour(TActor *actor)
274 {
275 	actor->flags |= FLAGS_DETOURING;
276 	actor->turns = 1;
277 	if (actor->flags & FLAGS_TRYRIGHT)
278 		actor->direction = (CmdToDirection(actor->lastCmd) + 1) % 8;
279 	else
280 		actor->direction = (CmdToDirection(actor->lastCmd) + 7) % 8;
281 }
282 
DidPlayerShoot(void)283 static bool DidPlayerShoot(void)
284 {
285 	CA_FOREACH(const PlayerData, p, gPlayerDatas)
286 	if (!IsPlayerAlive(p))
287 	{
288 		continue;
289 	}
290 	const TActor *player = ActorGetByUID(p->ActorUID);
291 	if (player->lastCmd & CMD_BUTTON1)
292 	{
293 		return true;
294 	}
295 	CA_FOREACH_END()
296 	return false;
297 }
298 
IsAIEnabled(const TActor * a)299 static bool IsAIEnabled(const TActor *a)
300 {
301 	return a->isInUse && a->PlayerUID < 0 && !a->dead &&
302 		   !ActorGetCharacter(a)->Class->Vehicle;
303 }
304 
305 static int Follow(TActor *a);
306 static int GetCmd(TActor *actor, const int delayModifier, const int rollLimit);
AICommand(const int ticks)307 int AICommand(const int ticks)
308 {
309 	int count = 0;
310 	int delayModifier;
311 	int rollLimit;
312 
313 	switch (ConfigGetEnum(&gConfig, "Game.Difficulty"))
314 	{
315 	case DIFFICULTY_VERYEASY:
316 		delayModifier = 4;
317 		rollLimit = 300;
318 		break;
319 	case DIFFICULTY_EASY:
320 		delayModifier = 2;
321 		rollLimit = 200;
322 		break;
323 	case DIFFICULTY_HARD:
324 		delayModifier = 1;
325 		rollLimit = 75;
326 		break;
327 	case DIFFICULTY_VERYHARD:
328 		delayModifier = 1;
329 		rollLimit = 50;
330 		break;
331 	default:
332 		delayModifier = 1;
333 		rollLimit = 100;
334 		break;
335 	}
336 
337 	CA_FOREACH(TActor, actor, gActors)
338 	if (!IsAIEnabled(actor))
339 	{
340 		continue;
341 	}
342 	int cmd = 0;
343 	if (!(actor->flags & FLAGS_PRISONER))
344 	{
345 		if (actor->flags & (FLAGS_VICTIM | FLAGS_GOOD_GUY))
346 		{
347 			sAreGoodGuysPresent = true;
348 		}
349 		cmd = GetCmd(actor, delayModifier, rollLimit);
350 		actor->aiContext->Delay = MAX(0, actor->aiContext->Delay - ticks);
351 	}
352 	CommandActor(actor, cmd, ticks);
353 	actor->aiContext->LastCmd = cmd;
354 	count++;
355 	CA_FOREACH_END()
356 	return count;
357 }
GetCmd(TActor * actor,const int delayModifier,const int rollLimit)358 static int GetCmd(TActor *actor, const int delayModifier, const int rollLimit)
359 {
360 	const CharBot *bot = ActorGetCharacter(actor)->bot;
361 
362 	int cmd = 0;
363 
364 	// Wake up if it can see a player or someone dying
365 	if ((actor->flags & FLAGS_SLEEPING) && (actor->flags & FLAGS_VISIBLE) &&
366 		actor->aiContext->Delay == 0 &&
367 		(CanSeeAPlayer(actor) || CanSeeActorBeingAttacked(actor)))
368 	{
369 		AIWake(actor, delayModifier);
370 	}
371 	// Fully wake up
372 	if ((actor->flags & FLAGS_WAKING) && actor->aiContext->Delay == 0)
373 	{
374 		actor->flags &= ~FLAGS_WAKING;
375 	}
376 	// Go to sleep if the player's too far away
377 	if (!(actor->flags & FLAGS_SLEEPING) && actor->aiContext->Delay == 0 &&
378 		!(actor->flags & FLAGS_AWAKEALWAYS))
379 	{
380 		if (!IsCloseToPlayer(actor->Pos, 40 * 16))
381 		{
382 			actor->flags |= FLAGS_SLEEPING;
383 			actor->flags &= ~FLAGS_WAKING;
384 			ActorSetAIState(actor, AI_STATE_IDLE);
385 		}
386 	}
387 
388 	// Don't do anything if the AI is sleeping or waking
389 	if (actor->flags & (FLAGS_SLEEPING | FLAGS_WAKING))
390 	{
391 		return cmd;
392 	}
393 
394 	bool bypass = false;
395 	const int roll = rand() % rollLimit;
396 	if (actor->flags & FLAGS_FOLLOWER)
397 	{
398 		cmd = Follow(actor);
399 	}
400 	else if (
401 		!!(actor->flags & FLAGS_SNEAKY) && !!(actor->flags & FLAGS_VISIBLE) &&
402 		DidPlayerShoot())
403 	{
404 		cmd = AIHuntClosest(actor) | CMD_BUTTON1;
405 		if (actor->flags & FLAGS_RUNS_AWAY)
406 		{
407 			// Turn back and shoot for running away characters
408 			cmd = AIReverseDirection(cmd);
409 		}
410 		bypass = true;
411 		ActorSetAIState(actor, AI_STATE_HUNT);
412 	}
413 	else if (actor->flags & FLAGS_DETOURING)
414 	{
415 		cmd = BrightWalk(actor, roll);
416 		ActorSetAIState(actor, AI_STATE_TRACK);
417 	}
418 	else if (actor->flags & FLAGS_RESCUED)
419 	{
420 		// If we haven't completed all objectives, act as follower
421 		if (!CanCompleteMission(&gMission) || gMap.exits.size > 1)
422 		{
423 			cmd = Follow(actor);
424 		}
425 		else
426 		{
427 			// Run towards the exit if there is one
428 			if (gMap.exits.size == 1)
429 			{
430 				const struct vec2 exitPos = MapGetExitPos(&gMap, 0);
431 				cmd = AIGoto(actor, exitPos, false);
432 			}
433 		}
434 	}
435 	else if (actor->aiContext->Delay > 0)
436 	{
437 		cmd = actor->lastCmd & ~CMD_BUTTON1;
438 	}
439 	else
440 	{
441 		if (roll < bot->probabilityToTrack)
442 		{
443 			cmd = AIHuntClosest(actor);
444 			ActorSetAIState(actor, AI_STATE_HUNT);
445 		}
446 		else if (roll < bot->probabilityToMove)
447 		{
448 			cmd = DirectionToCmd(rand() & 7);
449 			ActorSetAIState(actor, AI_STATE_TRACK);
450 		}
451 		actor->aiContext->Delay = bot->actionDelay * delayModifier;
452 	}
453 	if (!bypass)
454 	{
455 		if (WillFire(actor, roll))
456 		{
457 			cmd |= CMD_BUTTON1;
458 			if (!!(actor->flags & FLAGS_FOLLOWER) &&
459 				(actor->flags & FLAGS_GOOD_GUY))
460 			{
461 				// Shoot in a random direction away
462 				for (int j = 0; j < 10; j++)
463 				{
464 					direction_e d = (direction_e)(rand() % DIRECTION_COUNT);
465 					if (!IsFacingPlayer(actor, d))
466 					{
467 						cmd = DirectionToCmd(d) | CMD_BUTTON1;
468 						break;
469 					}
470 				}
471 			}
472 			if (actor->flags & FLAGS_RUNS_AWAY)
473 			{
474 				// Turn back and shoot for running away characters
475 				cmd |= AIReverseDirection(AIHuntClosest(actor));
476 			}
477 			ActorSetAIState(actor, AI_STATE_HUNT);
478 		}
479 		else
480 		{
481 			if ((actor->flags & FLAGS_VISIBLE) == 0)
482 			{
483 				// I think this is some hack to make sure invisible enemies
484 				// don't fire so much
485 				Weapon *w = ACTOR_GET_WEAPON(actor);
486 				for (int i = 0; i < WeaponClassNumBarrels(w->Gun); i++)
487 				{
488 					w->barrels[i].lock = 40;
489 				}
490 			}
491 			if (cmd && !IsDirectionOK(actor, CmdToDirection(cmd)) &&
492 				(actor->flags & FLAGS_DETOURING) == 0)
493 			{
494 				Detour(actor);
495 				cmd = 0;
496 				ActorSetAIState(actor, AI_STATE_TRACK);
497 			}
498 		}
499 	}
500 	return cmd;
501 }
AIWake(TActor * a,const int delayModifier)502 void AIWake(TActor *a, const int delayModifier)
503 {
504 	if (!a->aiContext || !(a->flags & FLAGS_SLEEPING))
505 		return;
506 	a->flags &= ~FLAGS_SLEEPING;
507 	a->flags |= FLAGS_WAKING;
508 	ActorSetAIState(a, AI_STATE_NONE);
509 	const CharBot *bot = ActorGetCharacter(a)->bot;
510 	if (bot == NULL)
511 		return;
512 	a->aiContext->Delay = bot->actionDelay * delayModifier;
513 
514 	// Don't play alert sound for invisible enemies
515 	if (!(a->flags & FLAGS_SEETHROUGH))
516 	{
517 		GameEvent es = GameEventNew(GAME_EVENT_SOUND_AT);
518 		CharacterClassGetSound(
519 			ActorGetCharacter(a)->Class, es.u.SoundAt.Sound, "alert");
520 		es.u.SoundAt.Pos = Vec2ToNet(a->thing.Pos);
521 		GameEventsEnqueue(&gGameEvents, es);
522 	}
523 }
Follow(TActor * a)524 static int Follow(TActor *a)
525 {
526 	// If we are a rescue objective and we are in the same exit as another
527 	// player, stop following and stay in the exit
528 	const Character *ch = ActorGetCharacter(a);
529 	const CharacterStore *store = &gCampaign.Setting.characters;
530 	if (CharacterIsPrisoner(store, ch) && CanCompleteMission(&gMission))
531 	{
532 		const int exit = MapIsTileInExit(&gMap, &a->thing, -1);
533 		if (exit != -1)
534 		{
535 			CA_FOREACH(const PlayerData, pd, gPlayerDatas)
536 			if (!IsPlayerAlive(pd))
537 			{
538 				continue;
539 			}
540 			const TActor *p = ActorGetByUID(pd->ActorUID);
541 			const int playerExit = MapIsTileInExit(&gMap, &p->thing, -1);
542 			if (playerExit == exit)
543 			{
544 				a->flags &= ~FLAGS_FOLLOWER;
545 				a->flags |= FLAGS_RESCUED;
546 				return 0;
547 			}
548 			CA_FOREACH_END()
549 		}
550 	}
551 	if (IsCloseToPlayer(a->Pos, 32))
552 	{
553 		ActorSetAIState(a, AI_STATE_IDLE);
554 		return 0;
555 	}
556 	else
557 	{
558 		ActorSetAIState(a, AI_STATE_FOLLOW);
559 		return AIGoto(a, AIGetClosestPlayerPos(a->Pos), true);
560 	}
561 }
562 
AICommandLast(const int ticks)563 void AICommandLast(const int ticks)
564 {
565 	CA_FOREACH(TActor, actor, gActors)
566 	if (!IsAIEnabled(actor))
567 	{
568 		continue;
569 	}
570 	const int cmd = actor->aiContext->LastCmd;
571 	actor->aiContext->Delay = MAX(0, actor->aiContext->Delay - ticks);
572 	CommandActor(actor, cmd, ticks);
573 	CA_FOREACH_END()
574 }
575 
AIWakeOnSoundAt(const struct vec2 pos)576 void AIWakeOnSoundAt(const struct vec2 pos)
577 {
578 	CA_FOREACH(TActor, actor, gActors)
579 	if (!actor->isInUse || actor->PlayerUID >= 0 || actor->dead ||
580 		!(actor->flags & FLAGS_SLEEPING) || (actor->flags & FLAGS_DEAF))
581 	{
582 		continue;
583 	}
584 	const float d =
585 		CHEBYSHEV_DISTANCE(pos.x, pos.y, actor->Pos.x, actor->Pos.y);
586 	if (d > AI_WAKE_SOUND_RANGE)
587 	{
588 		continue;
589 	}
590 	if (d > AI_WAKE_SOUND_RANGE_INDIRECT &&
591 		!AIHasClearView(actor, pos, AI_WAKE_SOUND_RANGE_INDIRECT))
592 	{
593 		continue;
594 	}
595 	AIWake(actor, 1);
596 	CA_FOREACH_END()
597 }
598 
AIAddRandomEnemies(const int enemies,const Mission * m)599 void AIAddRandomEnemies(const int enemies, const Mission *m)
600 {
601 	if (m->Enemies.size > 0 && m->EnemyDensity > 0 &&
602 		enemies < MAX(1, (m->EnemyDensity *
603 						  ConfigGetInt(&gConfig, "Game.EnemyDensity")) /
604 							 100))
605 	{
606 		const int charId =
607 			CharacterStoreGetRandomBaddieId(&gCampaign.Setting.characters);
608 		const Character *c =
609 			CArrayGet(&gCampaign.Setting.characters.OtherChars, charId);
610 		GameEvent e = GameEventNewActorAdd(
611 			PlaceAwayFromPlayers(&gMap, true, PLACEMENT_ACCESS_ANY), c, true);
612 		e.u.ActorAdd.CharId = charId;
613 		GameEventsEnqueue(&gGameEvents, e);
614 		gBaddieCount++;
615 	}
616 }
617 
InitializeBadGuys(void)618 void InitializeBadGuys(void)
619 {
620 	CA_FOREACH(Objective, o, gMission.missionData->Objectives)
621 	const PlacementAccessFlags paFlags = ObjectiveGetPlacementAccessFlags(o);
622 	if (o->Type == OBJECTIVE_KILL &&
623 		gMission.missionData->SpecialChars.size > 0)
624 	{
625 		const int charId =
626 			CharacterStoreGetRandomSpecialId(&gCampaign.Setting.characters);
627 		const Character *c =
628 			CArrayGet(&gCampaign.Setting.characters.OtherChars, charId);
629 		for (; o->placed < o->Count; o->placed++)
630 		{
631 			GameEvent e = GameEventNewActorAdd(
632 				PlaceAwayFromPlayers(&gMap, false, paFlags), c, true);
633 			e.u.ActorAdd.CharId = charId;
634 			e.u.ActorAdd.ThingFlags = ObjectiveToThing(_ca_index);
635 			GameEventsEnqueue(&gGameEvents, e);
636 
637 			// Process the events that actually place the actors
638 			HandleGameEvents(&gGameEvents, NULL, NULL, NULL, NULL);
639 		}
640 	}
641 	else if (o->Type == OBJECTIVE_RESCUE)
642 	{
643 		const int charId =
644 			CharacterStoreGetPrisonerId(&gCampaign.Setting.characters, 0);
645 		const Character *c =
646 			CArrayGet(&gCampaign.Setting.characters.OtherChars, charId);
647 		for (; o->placed < o->Count; o->placed++)
648 		{
649 			GameEvent e = GameEventNewActorAdd(
650 				MapHasLockedRooms(&gMap)
651 					? PlacePrisoner(&gMap)
652 					: PlaceAwayFromPlayers(&gMap, false, paFlags),
653 				c, true);
654 			e.u.ActorAdd.CharId = charId;
655 			e.u.ActorAdd.ThingFlags = ObjectiveToThing(_ca_index);
656 			GameEventsEnqueue(&gGameEvents, e);
657 
658 			// Process the events that actually place the actors
659 			HandleGameEvents(&gGameEvents, NULL, NULL, NULL, NULL);
660 		}
661 	}
662 	CA_FOREACH_END()
663 
664 	gBaddieCount = gMission.index * 4;
665 	sAreGoodGuysPresent = false;
666 }
667 
CreateEnemies(void)668 void CreateEnemies(void)
669 {
670 	if (gCampaign.Setting.characters.baddieIds.size == 0)
671 	{
672 		return;
673 	}
674 
675 	const int density = gMission.missionData->EnemyDensity *
676 						ConfigGetInt(&gConfig, "Game.EnemyDensity");
677 	for (int i = 0; i < density / 100; i++)
678 	{
679 		const int charId =
680 			CharacterStoreGetRandomBaddieId(&gCampaign.Setting.characters);
681 		const Character *c =
682 			CArrayGet(&gCampaign.Setting.characters.OtherChars, charId);
683 		GameEvent e = GameEventNewActorAdd(
684 			PlaceAwayFromPlayers(&gMap, true, PLACEMENT_ACCESS_ANY), c, true);
685 		e.u.ActorAdd.CharId = charId;
686 		GameEventsEnqueue(&gGameEvents, e);
687 		gBaddieCount++;
688 
689 		// Process the events that actually place the actors
690 		HandleGameEvents(&gGameEvents, NULL, NULL, NULL, NULL);
691 	}
692 }
693