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