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-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 "actors.h"
50
51 #include <assert.h>
52 #include <float.h>
53 #include <math.h>
54 #include <stdlib.h>
55 #include <string.h>
56
57 #include "actor_fire.h"
58 #include "actor_placement.h"
59 #include "ai.h"
60 #include "ai_coop.h"
61 #include "ai_utils.h"
62 #include "ammo.h"
63 #include "character.h"
64 #include "collision/collision.h"
65 #include "config.h"
66 #include "damage.h"
67 #include "defs.h"
68 #include "draw/drawtools.h"
69 #include "events.h"
70 #include "game.h"
71 #include "game_events.h"
72 #include "gamedata.h"
73 #include "log.h"
74 #include "mission.h"
75 #include "pic_manager.h"
76 #include "pickup.h"
77 #include "sounds.h"
78 #include "thing.h"
79 #include "triggers.h"
80 #include "utils.h"
81
82 #define FOOTSTEP_MAX_ANIM_SPEED 2
83 #define REPEL_STRENGTH 0.06f
84 #define SLIDE_LOCK 50
85 #define SLIDE_X (TILE_WIDTH / 3)
86 #define SLIDE_Y (TILE_HEIGHT / 3)
87 #define VEL_DECAY_X (TILE_WIDTH * 2 / 256.0f)
88 #define VEL_DECAY_Y (TILE_WIDTH * 2 / 256.0f) // Note: deliberately tile width
89 #define SOUND_LOCK_WEAPON_CLICK 20
90 #define DRAW_RADIAN_SPEED (MPI / 16)
91 // Percent of health considered low; bleed and flash HUD if low
92 #define LOW_HEALTH_PERCENTAGE 25
93 #define GORE_EMITTER_MAX_SPEED 0.25f
94 #define CHATTER_SHOW_SECONDS 2
95 #define CHATTER_SWITCH_GUN \
96 45 // TODO: based on clock time instead of game ticks
97 #define GRIMACE_PERIOD 20
98 #define GRIMACE_HIT_TICKS 39
99 #define GRIMACE_MELEE_TICKS 19
100 #define DAMAGE_TEXT_DISTANCE_RESET_THRESHOLD (ACTOR_W / 2)
101
102 CArray gPlayerIds;
103
104 CArray gActors;
105 static unsigned int sActorUIDs = 0;
106
ActorSetState(TActor * actor,const ActorAnimation state)107 void ActorSetState(TActor *actor, const ActorAnimation state)
108 {
109 actor->anim = AnimationGetActorAnimation(state);
110 }
111
112 static void ActorUpdateWeapon(TActor *a, Weapon *w, const int ticks);
113 static void CheckPickups(TActor *actor);
UpdateActorState(TActor * actor,int ticks)114 void UpdateActorState(TActor *actor, int ticks)
115 {
116 ActorUpdateWeapon(actor, ACTOR_GET_GUN(actor), ticks);
117 ActorUpdateWeapon(actor, ACTOR_GET_GRENADE(actor), ticks);
118
119 // If we're ready to pick up, always check the pickups
120 if (actor->PickupAll && !gCampaign.IsClient)
121 {
122 CheckPickups(actor);
123 }
124 // Stop picking up to prevent multiple pickups
125 // (require repeated key presses)
126 actor->PickupAll = false;
127
128 if (actor->health > 0)
129 {
130 actor->flamed = MAX(0, actor->flamed - ticks);
131 if (actor->poisoned)
132 {
133 if ((actor->poisoned & 7) == 0)
134 {
135 InjureActor(actor, 1);
136 }
137 actor->poisoned = MAX(0, actor->poisoned - ticks);
138 }
139 actor->petrified = MAX(0, actor->petrified - ticks);
140 actor->confused = MAX(0, actor->confused - ticks);
141 }
142
143 actor->slideLock = MAX(0, actor->slideLock - ticks);
144
145 ThingUpdate(&actor->thing, ticks);
146
147 actor->stateCounter = MAX(0, actor->stateCounter - ticks);
148 if (actor->stateCounter > 0)
149 {
150 return;
151 }
152
153 if (actor->health <= 0)
154 {
155 actor->dead++;
156 actor->MoveVel = svec2_zero();
157 actor->stateCounter = 4;
158 actor->thing.flags = 0;
159 return;
160 }
161
162 // Draw rotation interpolation
163 const float targetRadians = (float)dir2radians[actor->direction];
164 if (actor->DrawRadians - targetRadians > MPI)
165 {
166 actor->DrawRadians -= 2 * MPI;
167 }
168 if (actor->DrawRadians - targetRadians < -MPI)
169 {
170 actor->DrawRadians += 2 * MPI;
171 }
172 const float dr = actor->DrawRadians - targetRadians;
173 if (dr < 0)
174 {
175 actor->DrawRadians += (float)MIN(DRAW_RADIAN_SPEED * ticks, -dr);
176 }
177 else if (dr > 0)
178 {
179 actor->DrawRadians -= (float)MIN(DRAW_RADIAN_SPEED * ticks, dr);
180 }
181
182 // Footstep sounds
183 // Step on 2 and 6
184 // TODO: custom animation and footstep frames
185 if (ConfigGetBool(&gConfig, "Sound.Footsteps") &&
186 actor->anim.Type == ACTORANIMATION_WALKING &&
187 (AnimationGetFrame(&actor->anim) == 2 ||
188 AnimationGetFrame(&actor->anim) == 6) &&
189 actor->anim.newFrame)
190 {
191 GameEvent es = GameEventNew(GAME_EVENT_SOUND_AT);
192 const CharacterClass *cc = ActorGetCharacter(actor)->Class;
193 sprintf(es.u.SoundAt.Sound, "footsteps/%s", cc->Footsteps);
194 es.u.SoundAt.Pos = Vec2ToNet(actor->thing.Pos);
195 es.u.SoundAt.Distance = cc->FootstepsDistancePlus;
196 GameEventsEnqueue(&gGameEvents, es);
197 }
198
199 // Animation
200 float animTicks = 1;
201 if (actor->anim.Type == ACTORANIMATION_WALKING)
202 {
203 // Update walk animation based on actor speed
204 animTicks =
205 MIN(svec2_length(svec2_add(actor->MoveVel, actor->thing.Vel)),
206 FOOTSTEP_MAX_ANIM_SPEED);
207 }
208 animTicks *= ticks;
209 AnimationUpdate(&actor->anim, animTicks);
210
211 // Chatting
212 actor->ChatterCounter = MAX(0, actor->ChatterCounter - ticks);
213 if (actor->ChatterCounter == 0)
214 {
215 // Stop chatting
216 strcpy(actor->Chatter, "");
217 }
218
219 actor->grimaceCounter = MAX(0, actor->grimaceCounter - ticks);
220 }
ActorUpdateWeapon(TActor * a,Weapon * w,const int ticks)221 static void ActorUpdateWeapon(TActor *a, Weapon *w, const int ticks)
222 {
223 if (w->Gun == NULL)
224 {
225 return;
226 }
227 WeaponUpdate(w, ticks);
228 ActorFireUpdate(w, a, ticks);
229 for (int i = 0; i < WeaponClassNumBarrels(w->Gun); i++)
230 {
231 if (WeaponBarrelIsOverheating(w, i))
232 {
233 AddParticle ap;
234 memset(&ap, 0, sizeof ap);
235 ap.Pos = svec2_add(a->Pos, ActorGetMuzzleOffset(a, w, i));
236 ap.Z = WeaponClassGetMuzzleHeight(w->Gun, w->barrels[i].state, i) /
237 Z_FACTOR;
238 ap.Mask = colorWhite;
239 EmitterUpdate(&a->barrelSmoke, &ap, ticks);
240 }
241 }
242 }
243
244 static struct vec2 GetConstrainedPos(
245 const Map *map, const struct vec2 from, const struct vec2 to,
246 const struct vec2i size);
247 static void OnMove(TActor *a);
TryMoveActor(TActor * actor,struct vec2 pos)248 bool TryMoveActor(TActor *actor, struct vec2 pos)
249 {
250 CASSERT(
251 !svec2_is_nearly_equal(actor->Pos, pos, EPSILON_POS),
252 "trying to move to same position");
253
254 // Don't check collisions for pilots
255 if (actor->vehicleUID == -1)
256 {
257 actor->hasCollided = true;
258 actor->CanPickupSpecial = false;
259
260 const struct vec2 oldPos = actor->Pos;
261 pos = GetConstrainedPos(&gMap, actor->Pos, pos, actor->thing.size);
262 if (svec2_is_nearly_equal(oldPos, pos, EPSILON_POS))
263 {
264 return false;
265 }
266
267 // Check for object collisions
268 const CollisionParams params = {
269 THING_IMPASSABLE, CalcCollisionTeam(true, actor),
270 IsPVP(gCampaign.Entry.Mode), false};
271 Thing *target = OverlapGetFirstItem(
272 &actor->thing, pos, actor->thing.size, actor->thing.Vel, params);
273 if (target)
274 {
275 Weapon *gun = ACTOR_GET_WEAPON(actor);
276 const TObject *object = target->kind == KIND_OBJECT
277 ? CArrayGet(&gObjs, target->id)
278 : NULL;
279 const int barrel = ActorGetCanFireBarrel(actor, gun);
280 // Check for melee damage if we are the owner of the actor
281 const bool checkMelee =
282 (!gCampaign.IsClient && actor->PlayerUID < 0) ||
283 ActorIsLocalPlayer(actor->uid);
284 // TODO: support melee weapons on multi guns
285 const BulletClass *b = WeaponClassGetBullet(gun->Gun, barrel);
286 if (checkMelee && barrel >= 0 && !WeaponClassCanShoot(gun->Gun) &&
287 actor->health > 0 && b &&
288 (!object ||
289 (((b->Hit.Object.Hit && target->kind == KIND_OBJECT) ||
290 (b->Hit.Flesh.Hit && target->kind == KIND_CHARACTER)) &&
291 !ObjIsDangerous(object))))
292 {
293 if (CanHit(b, actor->flags, actor->uid, target))
294 {
295 // Tell the server that we want to melee something
296 GameEvent e = GameEventNew(GAME_EVENT_ACTOR_MELEE);
297 e.u.Melee.UID = actor->uid;
298 strcpy(e.u.Melee.BulletClass, b->Name);
299 e.u.Melee.TargetKind = target->kind;
300 switch (target->kind)
301 {
302 case KIND_CHARACTER:
303 e.u.Melee.TargetUID =
304 ((const TActor *)CArrayGet(&gActors, target->id))
305 ->uid;
306 e.u.Melee.HitType = HIT_FLESH;
307 break;
308 case KIND_OBJECT:
309 e.u.Melee.TargetUID =
310 ((const TObject *)CArrayGet(&gObjs, target->id))
311 ->uid;
312 e.u.Melee.HitType = HIT_OBJECT;
313 break;
314 default:
315 CASSERT(false, "cannot damage target kind");
316 break;
317 }
318 if (gun->barrels[barrel].soundLock > 0)
319 {
320 e.u.Melee.HitType = (int)HIT_NONE;
321 }
322 GameEventsEnqueue(&gGameEvents, e);
323 WeaponBarrelOnFire(gun, barrel);
324
325 // Only set grimace when counter 0 so that the actor
326 // alternates their grimace
327 if (actor->grimaceCounter == 0)
328 {
329 actor->grimaceCounter = GRIMACE_MELEE_TICKS;
330 }
331 }
332 return false;
333 }
334
335 const struct vec2 yPos = svec2(actor->Pos.x, pos.y);
336 if (OverlapGetFirstItem(
337 &actor->thing, yPos, actor->thing.size, svec2_zero(),
338 params))
339 {
340 pos.y = actor->Pos.y;
341 }
342 const struct vec2 xPos = svec2(pos.x, actor->Pos.y);
343 if (OverlapGetFirstItem(
344 &actor->thing, xPos, actor->thing.size, svec2_zero(),
345 params))
346 {
347 pos.x = actor->Pos.x;
348 }
349 if (pos.x != actor->Pos.x && pos.y != actor->Pos.y)
350 {
351 // Both x-only or y-only movement are viable,
352 // i.e. we are colliding corner vs corner
353 // Arbitrarily choose x-only movement
354 pos.y = actor->Pos.y;
355 }
356 if ((pos.x == actor->Pos.x && pos.y == actor->Pos.y) ||
357 IsCollisionWithWall(pos, actor->thing.size))
358 {
359 return false;
360 }
361 }
362 }
363
364 actor->Pos = pos;
365 OnMove(actor);
366
367 actor->hasCollided = false;
368 return true;
369 }
370 // Get a movement position that is constrained by collisions
371 // May return a position that is the same as the 'from', that is, we cannot
372 // move in the direction specified.
GetConstrainedPos(const Map * map,const struct vec2 from,const struct vec2 to,const struct vec2i size)373 static struct vec2 GetConstrainedPos(
374 const Map *map, const struct vec2 from, const struct vec2 to,
375 const struct vec2i size)
376 {
377 // Check collision with wall
378 if (!IsCollisionWithWall(to, size))
379 {
380 // Not in collision; just return where we wanted to go
381 return to;
382 }
383
384 CASSERT(size.x >= size.y, "tall collision not supported");
385 const struct vec2 dv = svec2_subtract(to, from);
386
387 // If moving diagonally, use rectangular bounds and
388 // try to move in only x or y directions
389 if (!nearly_equal(dv.x, 0.0f, EPSILON_POS) &&
390 !nearly_equal(dv.y, 0.0f, EPSILON_POS))
391 {
392 // X-only movement
393 const struct vec2 xVec = svec2(to.x, from.y);
394 if (!IsCollisionWithWall(xVec, size))
395 {
396 return xVec;
397 }
398 // Y-only movement
399 const struct vec2 yVec = svec2(from.x, to.y);
400 if (!IsCollisionWithWall(yVec, size))
401 {
402 return yVec;
403 }
404 // If we're still stuck, we're possibly stuck on a corner which is not
405 // in collision with a diamond but is colliding with the box.
406 // If so try x- or y-only movement, but with the benefit of diamond
407 // slipping.
408 const struct vec2 xPos = GetConstrainedPos(map, from, xVec, size);
409 if (!svec2_is_nearly_equal(xPos, from, EPSILON_POS))
410 {
411 return xPos;
412 }
413 const struct vec2 yPos = GetConstrainedPos(map, from, yVec, size);
414 if (!svec2_is_nearly_equal(yPos, from, EPSILON_POS))
415 {
416 return yPos;
417 }
418 }
419
420 // Now check diagonal movement, if we were moving in an x- or y-
421 // only direction
422 // Note: we're moving at extra speed because dx/dy are only magnitude 1;
423 // if we divide then we get 0 which ruins the logic
424 if (nearly_equal(dv.x, 0.0f, EPSILON_POS))
425 {
426 // Moving up or down; try moving to the left or right diagonally
427 // Scale X movement because our diamond is wider than tall, so we
428 // may need to scale the diamond wider.
429 const int xScale =
430 size.x > size.y ? (int)ceil((double)size.x / size.y) : 1;
431 const struct vec2 diag1Vec =
432 svec2_add(from, svec2(-dv.y * xScale, dv.y));
433 if (!IsCollisionDiamond(map, diag1Vec, size))
434 {
435 return diag1Vec;
436 }
437 const struct vec2 diag2Vec =
438 svec2_add(from, svec2(dv.y * xScale, dv.y));
439 if (!IsCollisionDiamond(map, diag2Vec, size))
440 {
441 return diag2Vec;
442 }
443 }
444 else if (nearly_equal(dv.y, 0.0f, EPSILON_POS))
445 {
446 // Moving left or right; try moving up or down diagonally
447 const struct vec2 diag1Vec = svec2_add(from, svec2(dv.x, -dv.x));
448 if (!IsCollisionDiamond(map, diag1Vec, size))
449 {
450 return diag1Vec;
451 }
452 const struct vec2 diag2Vec = svec2_add(from, svec2(dv.x, dv.x));
453 if (!IsCollisionDiamond(map, diag2Vec, size))
454 {
455 return diag2Vec;
456 }
457 }
458
459 // All alternative movements are in collision; don't move
460 return from;
461 }
462
ActorMove(const NActorMove am)463 void ActorMove(const NActorMove am)
464 {
465 TActor *a = ActorGetByUID(am.UID);
466 if (a == NULL || !a->isInUse)
467 return;
468 a->Pos = NetToVec2(am.Pos);
469 a->MoveVel = NetToVec2(am.MoveVel);
470 OnMove(a);
471 }
472 static void CheckTrigger(const TActor *a, const Map *map);
473 static void CheckRescue(const TActor *a);
OnMove(TActor * a)474 static void OnMove(TActor *a)
475 {
476 MapTryMoveThing(&gMap, &a->thing, a->Pos);
477 if (MapIsTileInExit(&gMap, &a->thing, -1) != -1)
478 {
479 a->action = ACTORACTION_EXITING;
480 }
481 else
482 {
483 a->action = ACTORACTION_MOVING;
484 }
485
486 if (!gCampaign.IsClient)
487 {
488 CheckTrigger(a, &gMap);
489
490 CheckPickups(a);
491
492 CheckRescue(a);
493 }
494 }
CheckTrigger(const TActor * a,const Map * map)495 static void CheckTrigger(const TActor *a, const Map *map)
496 {
497 // Don't let sleeping AI actors trigger tiles
498 if (a->PlayerUID < 0 && a->flags & FLAGS_SLEEPING)
499 {
500 return;
501 }
502 const struct vec2i tilePos = Vec2ToTile(a->Pos);
503 const bool showLocked = ActorIsLocalPlayer(a->uid);
504 const Tile *t = MapGetTile(map, tilePos);
505 CA_FOREACH(Trigger *, tp, t->triggers)
506 if (!TriggerTryActivate(*tp, gMission.KeyFlags, tilePos) &&
507 (*tp)->isActive && TriggerCannotActivate(*tp) && showLocked)
508 {
509 TriggerSetCannotActivate(*tp);
510 GameEvent s = GameEventNew(GAME_EVENT_ADD_PARTICLE);
511 s.u.AddParticle.Class =
512 StrParticleClass(&gParticleClasses, "locked_text");
513 s.u.AddParticle.Pos = Vec2CenterOfTile(tilePos);
514 s.u.AddParticle.Z = (BULLET_Z * 2) * Z_FACTOR;
515 sprintf(s.u.AddParticle.Text, "locked");
516 GameEventsEnqueue(&gGameEvents, s);
517 }
518 CA_FOREACH_END()
519 }
520 // Check if the player can pickup any item
521 static bool CheckPickupFunc(
522 Thing *ti, void *data, const struct vec2 colA, const struct vec2 colB,
523 const struct vec2 normal);
524 static void CheckPilot(const TActor *a, const CollisionParams params);
CheckPickups(TActor * actor)525 static void CheckPickups(TActor *actor)
526 {
527 // NPCs can't pickup
528 if (actor->PlayerUID < 0)
529 {
530 return;
531 }
532 const CollisionParams params = {
533 0, CalcCollisionTeam(true, actor), IsPVP(gCampaign.Entry.Mode), false};
534 OverlapThings(
535 &actor->thing, actor->Pos, actor->thing.Vel, actor->thing.size, params,
536 CheckPickupFunc, actor, NULL, NULL, NULL);
537 if (actor->PickupAll)
538 {
539 const CollisionParams paramsPilot = {
540 THING_IMPASSABLE | THING_CAN_BE_SHOT, params.Team, params.IsPVP,
541 true};
542 CheckPilot(actor, paramsPilot);
543 }
544 }
CheckPickupFunc(Thing * ti,void * data,const struct vec2 colA,const struct vec2 colB,const struct vec2 normal)545 static bool CheckPickupFunc(
546 Thing *ti, void *data, const struct vec2 colA, const struct vec2 colB,
547 const struct vec2 normal)
548 {
549 UNUSED(colA);
550 UNUSED(colB);
551 UNUSED(normal);
552 // Always return true, as we can pickup multiple items in one go
553 if (ti->kind != KIND_PICKUP)
554 return true;
555 TActor *a = data;
556 PickupPickup(a, CArrayGet(&gPickups, ti->id), a->PickupAll);
557 return true;
558 }
CheckPilot(const TActor * a,const CollisionParams params)559 static void CheckPilot(const TActor *a, const CollisionParams params)
560 {
561 const Thing *ti = OverlapGetFirstItem(
562 &a->thing, a->Pos, a->thing.size, a->thing.Vel, params);
563 if (ti == NULL || ti->kind != KIND_CHARACTER)
564 return;
565 const TActor *vehicle = CArrayGet(&gActors, ti->id);
566 if (vehicle->pilotUID >= 0)
567 return;
568
569 GameEvent e = GameEventNew(GAME_EVENT_ACTOR_PILOT);
570 e.u.Pilot.On = true;
571 e.u.Pilot.UID = a->uid;
572 e.u.Pilot.VehicleUID = vehicle->uid;
573 GameEventsEnqueue(&gGameEvents, e);
574 }
CheckRescue(const TActor * a)575 static void CheckRescue(const TActor *a)
576 {
577 // NPCs can't rescue
578 if (a->PlayerUID < 0)
579 return;
580
581 // Check an area slightly bigger than the actor's size for rescue
582 // objectives
583 #define RESCUE_CHECK_PAD 2
584 const CollisionParams params = {
585 THING_IMPASSABLE, CalcCollisionTeam(true, a),
586 IsPVP(gCampaign.Entry.Mode), false};
587 const Thing *target = OverlapGetFirstItem(
588 &a->thing, a->Pos,
589 svec2i_add(a->thing.size, svec2i(RESCUE_CHECK_PAD, RESCUE_CHECK_PAD)),
590 a->thing.Vel, params);
591 if (target != NULL && target->kind == KIND_CHARACTER)
592 {
593 TActor *other = CArrayGet(&gActors, target->id);
594 CASSERT(other->isInUse, "Cannot find nonexistent player");
595 if (other->flags & FLAGS_PRISONER)
596 {
597 other->flags &= ~FLAGS_PRISONER;
598 GameEvent e = GameEventNew(GAME_EVENT_RESCUE_CHARACTER);
599 e.u.Rescue.UID = other->uid;
600 GameEventsEnqueue(&gGameEvents, e);
601 UpdateMissionObjective(
602 &gMission, other->thing.flags, OBJECTIVE_RESCUE, 1);
603 }
604 }
605 }
606
ActorHeal(TActor * actor,int health)607 void ActorHeal(TActor *actor, int health)
608 {
609 actor->health += health;
610 actor->health = MIN(actor->health, ActorGetCharacter(actor)->maxHealth);
611 AddParticle ap;
612 memset(&ap, 0, sizeof ap);
613 ap.Pos = actor->Pos;
614 ap.Z = 10;
615 for (int i = 0; i < MAX(health / 20, 1); i++)
616 {
617 ap.Vel = svec2(RAND_FLOAT(-0.2f, 0.2f), RAND_FLOAT(-0.2f, 0.2f));
618 EmitterStart(&actor->healEffect, &ap);
619 }
620 }
621
InjureActor(TActor * actor,int injury)622 void InjureActor(TActor *actor, int injury)
623 {
624 const int lastHealth = actor->health;
625 actor->health -= injury;
626 LOG(LM_ACTOR, LL_DEBUG, "actor uid(%d) injured %d -(%d)-> %d", actor->uid,
627 lastHealth, injury, actor->health);
628 if (lastHealth > 0 && actor->health <= 0)
629 {
630 actor->stateCounter = 0;
631 GameEvent es = GameEventNew(GAME_EVENT_SOUND_AT);
632 CharacterClassGetSound(
633 ActorGetCharacter(actor)->Class, es.u.SoundAt.Sound, "die");
634 es.u.SoundAt.Pos = Vec2ToNet(actor->thing.Pos);
635 GameEventsEnqueue(&gGameEvents, es);
636 if (actor->PlayerUID >= 0)
637 {
638 es = GameEventNew(GAME_EVENT_SOUND_AT);
639 strcpy(es.u.SoundAt.Sound, "hahaha");
640 es.u.SoundAt.Pos = Vec2ToNet(actor->thing.Pos);
641 GameEventsEnqueue(&gGameEvents, es);
642 }
643 if (actor->thing.flags & THING_OBJECTIVE)
644 {
645 UpdateMissionObjective(
646 &gMission, actor->thing.flags, OBJECTIVE_KILL, 1);
647 // If we've killed someone we have rescued, deduct from the
648 // rescue objective
649 if (!(actor->flags & FLAGS_PRISONER))
650 {
651 UpdateMissionObjective(
652 &gMission, actor->thing.flags, OBJECTIVE_RESCUE, -1);
653 }
654 }
655 }
656 }
657
ActorAddAmmo(TActor * actor,const int ammoId,const int amount)658 void ActorAddAmmo(TActor *actor, const int ammoId, const int amount)
659 {
660 int *ammo = CArrayGet(&actor->ammo, ammoId);
661 *ammo += amount;
662 const int ammoMax = AmmoGetById(&gAmmo, ammoId)->Max;
663 *ammo = CLAMP(*ammo, 0, ammoMax);
664 }
665
ActorUsesAmmo(const TActor * actor,const int ammoId)666 bool ActorUsesAmmo(const TActor *actor, const int ammoId)
667 {
668 for (int i = 0; i < MAX_WEAPONS; i++)
669 {
670 const WeaponClass *wc = actor->guns[i].Gun;
671 if (wc == NULL)
672 {
673 continue;
674 }
675 for (int j = 0; j < WeaponClassNumBarrels(wc); j++)
676 {
677 if (WC_BARREL_ATTR(*wc, AmmoId, j) == ammoId)
678 {
679 return true;
680 }
681 }
682 }
683 return false;
684 }
685
ActorReplaceGun(const NActorReplaceGun rg)686 void ActorReplaceGun(const NActorReplaceGun rg)
687 {
688 TActor *a = ActorGetByUID(rg.UID);
689 if (a == NULL || !a->isInUse)
690 return;
691 const WeaponClass *wc = StrWeaponClass(rg.Gun);
692 CASSERT(wc != NULL, "cannot find gun");
693 // If player already has gun, don't do anything
694 if (ActorFindGun(a, wc) >= 0)
695 {
696 return;
697 }
698 LOG(LM_ACTOR, LL_DEBUG, "actor uid(%d) replacing gun(%s) idx(%d)",
699 (int)rg.UID, rg.Gun, rg.GunIdx);
700 Weapon w = WeaponCreate(wc);
701 memcpy(&a->guns[rg.GunIdx], &w, sizeof w);
702 // Switch immediately to picked up gun
703 const PlayerData *p = PlayerDataGetByUID(a->PlayerUID);
704 if (wc->Type == GUNTYPE_GRENADE && PlayerHasGrenadeButton(p))
705 {
706 a->grenadeIndex = rg.GunIdx - MAX_GUNS;
707 }
708 else
709 {
710 a->gunIndex = rg.GunIdx;
711 }
712
713 SoundPlayAt(&gSoundDevice, wc->SwitchSound, a->Pos);
714 }
715
ActorFindGun(const TActor * a,const WeaponClass * wc)716 int ActorFindGun(const TActor *a, const WeaponClass *wc)
717 {
718 for (int i = 0; i < MAX_WEAPONS; i++)
719 {
720 if (a->guns[i].Gun == wc)
721 {
722 return i;
723 }
724 }
725 return -1;
726 }
727
ActorGetNumWeapons(const TActor * a)728 int ActorGetNumWeapons(const TActor *a)
729 {
730 int count = 0;
731 for (int i = 0; i < MAX_WEAPONS; i++)
732 {
733 if (a->guns[i].Gun != NULL)
734 {
735 count++;
736 }
737 }
738 return count;
739 }
ActorGetNumGuns(const TActor * a)740 int ActorGetNumGuns(const TActor *a)
741 {
742 int count = 0;
743 for (int i = 0; i < MAX_GUNS; i++)
744 {
745 if (a->guns[i].Gun != NULL)
746 {
747 count++;
748 }
749 }
750 return count;
751 }
ActorGetNumGrenades(const TActor * a)752 int ActorGetNumGrenades(const TActor *a)
753 {
754 int count = 0;
755 for (int i = MAX_GUNS; i < MAX_WEAPONS; i++)
756 {
757 if (a->guns[i].Gun != NULL)
758 {
759 count++;
760 }
761 }
762 return count;
763 }
764
ActorSetChatter(TActor * a,const char * text,const int count)765 static void ActorSetChatter(TActor *a, const char *text, const int count)
766 {
767 strcpy(a->Chatter, text);
768 a->ChatterCounter = count;
769 }
770
771 // Set AI state and possibly say something based on the state
ActorSetAIState(TActor * actor,const AIState s)772 void ActorSetAIState(TActor *actor, const AIState s)
773 {
774 if (AIContextSetState(actor->aiContext, s) &&
775 AIContextShowChatter(ConfigGetEnum(&gConfig, "Interface.AIChatter")))
776 {
777 ActorSetChatter(
778 actor, AIStateGetChatterText(actor->aiContext->State),
779 CHATTER_SHOW_SECONDS * ConfigGetInt(&gConfig, "Game.FPS"));
780 }
781 }
782
ActorPilot(const NActorPilot ap)783 void ActorPilot(const NActorPilot ap)
784 {
785 TActor *pilot = ActorGetByUID(ap.UID);
786 TActor *vehicle = ActorGetByUID(ap.VehicleUID);
787 if (ap.On)
788 {
789 CASSERT(pilot->vehicleUID == -1, "already piloting");
790 pilot->vehicleUID = vehicle->uid;
791 CASSERT(vehicle->pilotUID == -1, "already has pilot");
792 vehicle->pilotUID = pilot->uid;
793 char buf[256];
794 CharacterClassGetSound(
795 ActorGetCharacter(vehicle)->Class, buf, "alert");
796 SoundPlayAt(&gSoundDevice, StrSound(buf), vehicle->Pos);
797 }
798 else
799 {
800 CASSERT(pilot->vehicleUID != -1, "not piloting");
801 pilot->vehicleUID = -1;
802 CASSERT(vehicle->pilotUID != -1, "doesn't have pilot");
803 vehicle->pilotUID = -1;
804 char buf[256];
805 sprintf(
806 buf, "footsteps/%s", ActorGetCharacter(pilot)->Class->Footsteps);
807 SoundPlayAt(&gSoundDevice, StrSound(buf), vehicle->Pos);
808 }
809 }
810
FireWeapon(TActor * a,Weapon * w)811 static void FireWeapon(TActor *a, Weapon *w)
812 {
813 if (w->Gun == NULL)
814 {
815 return;
816 }
817 const int barrel = ActorGetCanFireBarrel(a, w);
818 if (barrel == -1)
819 {
820 if (WeaponGetUnlockedBarrel(w) >= 0 && gCampaign.Setting.Ammo)
821 {
822 CASSERT(
823 ActorWeaponGetAmmo(a, w->Gun, barrel) == 0,
824 "should be out of ammo");
825 // Play a clicking sound if this weapon is out of ammo
826 if (w->clickLock <= 0)
827 {
828 GameEvent es = GameEventNew(GAME_EVENT_SOUND_AT);
829 strcpy(es.u.SoundAt.Sound, "click");
830 es.u.SoundAt.Pos = Vec2ToNet(a->Pos);
831 GameEventsEnqueue(&gGameEvents, es);
832 w->clickLock = SOUND_LOCK_WEAPON_CLICK;
833 }
834 }
835 return;
836 }
837 ActorFireBarrel(w, a, barrel);
838 const int ammoId = WC_BARREL_ATTR(*(w->Gun), AmmoId, barrel);
839 if (a->PlayerUID >= 0 && gCampaign.Setting.Ammo && ammoId >= 0)
840 {
841 GameEvent e = GameEventNew(GAME_EVENT_ACTOR_USE_AMMO);
842 e.u.UseAmmo.UID = a->uid;
843 e.u.UseAmmo.PlayerUID = a->PlayerUID;
844 e.u.UseAmmo.Ammo.Id = ammoId;
845 e.u.UseAmmo.Ammo.Amount = 1;
846 GameEventsEnqueue(&gGameEvents, e);
847 }
848 const TActor *firingActor = ActorGetByUID(a->pilotUID);
849 const int cost = WC_BARREL_ATTR(*(w->Gun), Cost, barrel);
850 if (firingActor->PlayerUID >= 0 && cost != 0)
851 {
852 // Classic C-Dogs score consumption
853 GameEvent e = GameEventNew(GAME_EVENT_SCORE);
854 e.u.Score.PlayerUID = firingActor->PlayerUID;
855 e.u.Score.Score = -cost;
856 GameEventsEnqueue(&gGameEvents, e);
857 }
858 }
859
ActorTryChangeDirection(TActor * actor,const int cmd,const int prevCmd)860 static bool ActorTryChangeDirection(
861 TActor *actor, const int cmd, const int prevCmd)
862 {
863 const bool willChangeDirecton =
864 !actor->petrified && CMD_HAS_DIRECTION(cmd) &&
865 (!(cmd & CMD_BUTTON2) ||
866 ConfigGetEnum(&gConfig, "Game.SwitchMoveStyle") !=
867 SWITCHMOVE_STRAFE) &&
868 (!(prevCmd & CMD_BUTTON1) ||
869 ConfigGetEnum(&gConfig, "Game.FireMoveStyle") != FIREMOVE_STRAFE);
870 const direction_e dir = CmdToDirection(cmd);
871 if (willChangeDirecton && dir != actor->direction)
872 {
873 GameEvent e = GameEventNew(GAME_EVENT_ACTOR_DIR);
874 e.u.ActorDir.UID = actor->uid;
875 e.u.ActorDir.Dir = (int32_t)dir;
876 GameEventsEnqueue(&gGameEvents, e);
877 // Change direction immediately because this affects shooting
878 actor->direction = dir;
879 }
880 return willChangeDirecton;
881 }
882
ActorTryShoot(TActor * actor,const int cmd)883 static bool ActorTryShoot(TActor *actor, const int cmd)
884 {
885 const bool willShoot = !actor->petrified && (cmd & CMD_BUTTON1);
886 if (willShoot)
887 {
888 FireWeapon(actor, ACTOR_GET_GUN(actor));
889 }
890 else
891 {
892 // Stop firing barrel and restore ready state
893 const Weapon *w = ACTOR_GET_GUN(actor);
894 for (int i = 0; i < WeaponClassNumBarrels(w->Gun); i++)
895 {
896 if (w->barrels[i].state == GUNSTATE_FIRING)
897 {
898 GameEvent e = GameEventNew(GAME_EVENT_GUN_STATE);
899 e.u.GunState.ActorUID = actor->uid;
900 e.u.GunState.Barrel = i;
901 e.u.GunState.State = GUNSTATE_READY;
902 GameEventsEnqueue(&gGameEvents, e);
903 }
904 }
905 }
906 return willShoot;
907 }
908
TryGrenade(TActor * a,const int cmd)909 static bool TryGrenade(TActor *a, const int cmd)
910 {
911 const bool willGrenade = !a->petrified && (cmd & CMD_GRENADE);
912 if (willGrenade)
913 {
914 FireWeapon(a, ACTOR_GET_GRENADE(a));
915 }
916 return willGrenade;
917 }
918
919 static bool ActorTryMove(TActor *actor, int cmd, int hasShot, int ticks);
CommandActor(TActor * actor,int cmd,int ticks)920 void CommandActor(TActor *actor, int cmd, int ticks)
921 {
922 // If this is a pilot, command the vehicle instead
923 if (actor->vehicleUID != -1)
924 {
925 TActor *vehicle = ActorGetByUID(actor->vehicleUID);
926 CommandActor(vehicle, cmd, ticks);
927 }
928 else if (actor->pilotUID == -1)
929 {
930 // If this is a vehicle and there's no pilot, do nothing
931 }
932 else
933 {
934
935 if (actor->confused)
936 {
937 cmd = CmdGetReverse(cmd);
938 }
939
940 if (actor->health > 0)
941 {
942 const bool hasChangedDirection =
943 ActorTryChangeDirection(actor, cmd, actor->lastCmd);
944 const bool hasShot = ActorTryShoot(actor, cmd);
945 const bool hasGrenaded = TryGrenade(actor, cmd);
946 const bool hasMoved = ActorTryMove(actor, cmd, hasShot, ticks);
947 ActorAnimation anim = actor->anim.Type;
948 // Idle if player hasn't done anything
949 if (!(hasChangedDirection || hasShot || hasGrenaded || hasMoved))
950 {
951 anim = ACTORANIMATION_IDLE;
952 }
953 else if (hasMoved)
954 {
955 anim = ACTORANIMATION_WALKING;
956 }
957 else
958 {
959 anim = ACTORANIMATION_STAND;
960 }
961 if (actor->anim.Type != anim)
962 {
963 GameEvent e = GameEventNew(GAME_EVENT_ACTOR_STATE);
964 e.u.ActorState.UID = actor->uid;
965 e.u.ActorState.State = (int32_t)anim;
966 GameEventsEnqueue(&gGameEvents, e);
967 }
968 }
969 }
970
971 actor->specialCmdDir = CMD_HAS_DIRECTION(cmd);
972 if ((cmd & CMD_BUTTON2) && !actor->specialCmdDir)
973 {
974 // Special: pick up things that can only be picked up on demand
975 if (!actor->PickupAll && !(actor->lastCmd & CMD_BUTTON2) &&
976 actor->vehicleUID == -1)
977 {
978 GameEvent e = GameEventNew(GAME_EVENT_ACTOR_PICKUP_ALL);
979 e.u.ActorPickupAll.UID = actor->uid;
980 e.u.ActorPickupAll.PickupAll = true;
981 GameEventsEnqueue(&gGameEvents, e);
982 }
983 }
984
985 actor->lastCmd = cmd;
986 }
ActorTryMove(TActor * actor,int cmd,int hasShot,int ticks)987 static bool ActorTryMove(TActor *actor, int cmd, int hasShot, int ticks)
988 {
989 const bool canMoveWhenShooting =
990 ConfigGetEnum(&gConfig, "Game.FireMoveStyle") != FIREMOVE_STOP ||
991 !hasShot ||
992 (ConfigGetEnum(&gConfig, "Game.SwitchMoveStyle") ==
993 SWITCHMOVE_STRAFE &&
994 (cmd & CMD_BUTTON2));
995 const bool willMove =
996 !actor->petrified && CMD_HAS_DIRECTION(cmd) && canMoveWhenShooting;
997 actor->MoveVel = svec2_zero();
998 if (willMove)
999 {
1000 const float moveAmount = ActorGetCharacter(actor)->speed * ticks;
1001 struct vec2 moveVel = svec2_zero();
1002 if (cmd & CMD_LEFT)
1003 moveVel.x--;
1004 else if (cmd & CMD_RIGHT)
1005 moveVel.x++;
1006 if (cmd & CMD_UP)
1007 moveVel.y--;
1008 else if (cmd & CMD_DOWN)
1009 moveVel.y++;
1010 if (!svec2_is_zero(moveVel))
1011 {
1012 actor->MoveVel = svec2_scale(svec2_normalize(moveVel), moveAmount);
1013 }
1014 }
1015
1016 // If we have changed our move commands, send the move event
1017 if (cmd != actor->lastCmd || actor->hasCollided)
1018 {
1019 GameEvent e = GameEventNew(GAME_EVENT_ACTOR_MOVE);
1020 e.u.ActorMove.UID = actor->uid;
1021 e.u.ActorMove.Pos = Vec2ToNet(actor->Pos);
1022 e.u.ActorMove.MoveVel = Vec2ToNet(actor->MoveVel);
1023 GameEventsEnqueue(&gGameEvents, e);
1024 }
1025
1026 return willMove || !svec2_is_zero(actor->thing.Vel);
1027 }
1028
SlideActor(TActor * actor,int cmd)1029 void SlideActor(TActor *actor, int cmd)
1030 {
1031 // Check that actor can slide
1032 if (actor->slideLock > 0)
1033 {
1034 return;
1035 }
1036
1037 if (actor->petrified)
1038 return;
1039
1040 if (actor->confused)
1041 {
1042 cmd = CmdGetReverse(cmd);
1043 }
1044
1045 GameEvent e = GameEventNew(GAME_EVENT_ACTOR_SLIDE);
1046 e.u.ActorSlide.UID = actor->uid;
1047 struct vec2 vel = svec2_zero();
1048 if (cmd & CMD_LEFT)
1049 vel.x = -SLIDE_X;
1050 else if (cmd & CMD_RIGHT)
1051 vel.x = SLIDE_X;
1052 if (cmd & CMD_UP)
1053 vel.y = -SLIDE_Y;
1054 else if (cmd & CMD_DOWN)
1055 vel.y = SLIDE_Y;
1056 e.u.ActorSlide.Vel = Vec2ToNet(vel);
1057 GameEventsEnqueue(&gGameEvents, e);
1058
1059 actor->slideLock = SLIDE_LOCK;
1060 }
1061
1062 static void ActorAddBloodSplatters(
1063 TActor *a, const int power, const float mass, const struct vec2 hitVector);
1064
1065 static void ActorUpdatePosition(TActor *actor, int ticks);
1066 static void ActorDie(TActor *actor);
UpdateAllActors(const int ticks)1067 void UpdateAllActors(const int ticks)
1068 {
1069 CA_FOREACH(TActor, actor, gActors)
1070 if (!actor->isInUse)
1071 {
1072 continue;
1073 }
1074 // Update pilot/vehicle statuses
1075 if (actor->vehicleUID != -1)
1076 {
1077 const TActor *vehicle = ActorGetByUID(actor->vehicleUID);
1078 if (vehicle->dead)
1079 {
1080 actor->vehicleUID = -1;
1081 }
1082 }
1083 if (actor->pilotUID != -1 && actor->pilotUID != actor->uid)
1084 {
1085 const TActor *pilot = ActorGetByUID(actor->pilotUID);
1086 if (pilot->dead)
1087 {
1088 actor->pilotUID = -1;
1089 }
1090 }
1091 ActorUpdatePosition(actor, ticks);
1092 UpdateActorState(actor, ticks);
1093 const NamedSprites *deathSprites = CharacterClassGetDeathSprites(
1094 ActorGetCharacter(actor)->Class, &gPicManager);
1095 if (actor->dead && (deathSprites == NULL || actor->dead - 1 > (int)deathSprites->pics.size))
1096 {
1097 if (!gCampaign.IsClient)
1098 {
1099 ActorDie(actor);
1100 }
1101 continue;
1102 }
1103 // Find actors that are on the same team and colliding,
1104 // and repel them
1105 if (!gCampaign.IsClient &&
1106 gCollisionSystem.allyCollision == ALLYCOLLISION_REPEL)
1107 {
1108 const CollisionParams params = {
1109 THING_IMPASSABLE, COLLISIONTEAM_NONE, IsPVP(gCampaign.Entry.Mode),
1110 false};
1111 const Thing *collidingItem = OverlapGetFirstItem(
1112 &actor->thing, actor->Pos, actor->thing.size, actor->thing.Vel,
1113 params);
1114 if (collidingItem && collidingItem->kind == KIND_CHARACTER)
1115 {
1116 TActor *collidingActor = CArrayGet(&gActors, collidingItem->id);
1117 if (CalcCollisionTeam(1, collidingActor) ==
1118 CalcCollisionTeam(1, actor))
1119 {
1120 struct vec2 v =
1121 svec2_subtract(actor->Pos, collidingActor->Pos);
1122 if (svec2_is_zero(v))
1123 {
1124 v = svec2(1, 0);
1125 }
1126 v = svec2_scale(svec2_normalize(v), REPEL_STRENGTH);
1127 GameEvent e = GameEventNew(GAME_EVENT_ACTOR_IMPULSE);
1128 e.u.ActorImpulse.UID = actor->uid;
1129 e.u.ActorImpulse.Vel = Vec2ToNet(v);
1130 e.u.ActorImpulse.Pos = Vec2ToNet(actor->Pos);
1131 GameEventsEnqueue(&gGameEvents, e);
1132 e.u.ActorImpulse.UID = collidingActor->uid;
1133 e.u.ActorImpulse.Vel = Vec2ToNet(svec2_scale(v, -1));
1134 e.u.ActorImpulse.Pos = Vec2ToNet(collidingActor->Pos);
1135 GameEventsEnqueue(&gGameEvents, e);
1136 }
1137 }
1138 }
1139 // If low on health, bleed
1140 if (ActorIsLowHealth(actor))
1141 {
1142 actor->bleedCounter -= ticks;
1143 if (actor->bleedCounter <= 0)
1144 {
1145 ActorAddBloodSplatters(actor, 1, 1.0f, svec2_zero());
1146 actor->bleedCounter += ActorGetHealthPercent(actor);
1147 }
1148 }
1149 CA_FOREACH_END()
1150 }
1151 static void CheckManualPickups(TActor *a);
ActorUpdatePosition(TActor * actor,int ticks)1152 static void ActorUpdatePosition(TActor *actor, int ticks)
1153 {
1154 struct vec2 newPos;
1155 // If piloting a vehicle, set position to that of vehicle
1156 if (actor->vehicleUID != -1)
1157 {
1158 const TActor *vehicle = ActorGetByUID(actor->vehicleUID);
1159 newPos = vehicle->Pos;
1160 }
1161 else
1162 {
1163 newPos = svec2_add(actor->Pos, actor->MoveVel);
1164 if (!svec2_is_zero(actor->thing.Vel))
1165 {
1166 newPos =
1167 svec2_add(newPos, svec2_scale(actor->thing.Vel, (float)ticks));
1168
1169 for (int i = 0; i < ticks; i++)
1170 {
1171 if (actor->thing.Vel.x > FLT_EPSILON)
1172 {
1173 actor->thing.Vel.x =
1174 MAX(0, actor->thing.Vel.x - VEL_DECAY_X);
1175 }
1176 else if (actor->thing.Vel.x < -FLT_EPSILON)
1177 {
1178 actor->thing.Vel.x =
1179 MIN(0, actor->thing.Vel.x + VEL_DECAY_X);
1180 }
1181 if (actor->thing.Vel.y > FLT_EPSILON)
1182 {
1183 actor->thing.Vel.y =
1184 MAX(0, actor->thing.Vel.y - VEL_DECAY_Y);
1185 }
1186 else if (actor->thing.Vel.y < FLT_EPSILON)
1187 {
1188 actor->thing.Vel.y =
1189 MIN(0, actor->thing.Vel.y + VEL_DECAY_Y);
1190 }
1191 }
1192 }
1193 }
1194
1195 if (!svec2_is_nearly_equal(actor->Pos, newPos, EPSILON_POS))
1196 {
1197 TryMoveActor(actor, newPos);
1198 }
1199 // Check if we're standing over any manual pickups
1200 CheckManualPickups(actor);
1201 }
1202 // Check if the actor is over any manual pickups
1203 static bool CheckManualPickupFunc(
1204 Thing *ti, void *data, const struct vec2 colA, const struct vec2 colB,
1205 const struct vec2 normal);
1206 // Check if the actor is over any unpiloted vehicles
1207 static void CheckManualPilot(TActor *a, const CollisionParams params);
CheckManualPickups(TActor * a)1208 static void CheckManualPickups(TActor *a)
1209 {
1210 // NPCs can't pickup
1211 if (a->PlayerUID < 0)
1212 return;
1213 const CollisionParams params = {
1214 0, CalcCollisionTeam(true, a), IsPVP(gCampaign.Entry.Mode), false};
1215 OverlapThings(
1216 &a->thing, a->Pos, a->thing.Vel, a->thing.size, params,
1217 CheckManualPickupFunc, a, NULL, NULL, NULL);
1218 const CollisionParams paramsPilot = {
1219 THING_IMPASSABLE | THING_CAN_BE_SHOT, params.Team, params.IsPVP, true};
1220 CheckManualPilot(a, paramsPilot);
1221 }
CheckManualPickupFunc(Thing * ti,void * data,const struct vec2 colA,const struct vec2 colB,const struct vec2 normal)1222 static bool CheckManualPickupFunc(
1223 Thing *ti, void *data, const struct vec2 colA, const struct vec2 colB,
1224 const struct vec2 normal)
1225 {
1226 UNUSED(colA);
1227 UNUSED(colB);
1228 UNUSED(normal);
1229 TActor *a = data;
1230 if (ti->kind != KIND_PICKUP)
1231 return true;
1232 const Pickup *p = CArrayGet(&gPickups, ti->id);
1233 if (!PickupIsManual(p))
1234 return true;
1235 // "Say" that the weapon must be picked up using a command
1236 const PlayerData *pData = PlayerDataGetByUID(a->PlayerUID);
1237 if (pData->IsLocal && IsPlayerHuman(pData))
1238 {
1239 char buttonName[64];
1240 strcpy(buttonName, "");
1241 InputGetButtonName(
1242 pData->inputDevice, pData->deviceIndex, CMD_BUTTON2, buttonName);
1243 // TODO: PickupGetName
1244 const char *pickupName;
1245 switch (p->class->Type)
1246 {
1247 case PICKUP_AMMO:
1248 pickupName = AmmoGetById(&gAmmo, p->class->u.Ammo.Id)->Name;
1249 break;
1250 case PICKUP_GUN:
1251 pickupName = IdWeaponClass(p->class->u.GunId)->name;
1252 break;
1253 default:
1254 CASSERT(false, "unknown pickup type");
1255 pickupName = "???";
1256 break;
1257 }
1258 char buf[256];
1259 sprintf(buf, "%s to pick up\n%s", buttonName, pickupName);
1260 ActorSetChatter(a, buf, 2);
1261 }
1262 // If co-op AI, alert it so it can try to pick the gun up
1263 if (a->aiContext != NULL)
1264 {
1265 AICoopOnPickupGun(a, p->class->u.GunId);
1266 }
1267 a->CanPickupSpecial = true;
1268 return false;
1269 }
CheckManualPilot(TActor * a,const CollisionParams params)1270 static void CheckManualPilot(TActor *a, const CollisionParams params)
1271 {
1272 if (a->vehicleUID >= 0)
1273 {
1274 return;
1275 }
1276 const Thing *ti = OverlapGetFirstItem(
1277 &a->thing, a->Pos, a->thing.size, a->thing.Vel, params);
1278 if (ti == NULL || ti->kind != KIND_CHARACTER)
1279 return;
1280 const TActor *vehicle = CArrayGet(&gActors, ti->id);
1281 if (vehicle->pilotUID >= 0)
1282 return;
1283 // "Say" that we can pilot using a command
1284 const PlayerData *pData = PlayerDataGetByUID(a->PlayerUID);
1285 if (pData->IsLocal && IsPlayerHuman(pData))
1286 {
1287 char buttonName[64];
1288 strcpy(buttonName, "");
1289 InputGetButtonName(
1290 pData->inputDevice, pData->deviceIndex, CMD_BUTTON2, buttonName);
1291 const char *vehicleName = ActorGetCharacter(vehicle)->Class->Name;
1292 char buf[256];
1293 sprintf(buf, "%s to pilot\n%s", buttonName, vehicleName);
1294 ActorSetChatter(a, buf, 2);
1295 }
1296 // TODO: co-op AI pilot
1297 a->CanPickupSpecial = true;
1298 }
1299 static void ActorAddAmmoPickup(const TActor *actor);
1300 static void ActorAddGunPickup(const TActor *actor);
ActorDie(TActor * actor)1301 static void ActorDie(TActor *actor)
1302 {
1303 const Character *c = ActorGetCharacter(actor);
1304 GameEvent e;
1305 if (!gCampaign.IsClient)
1306 {
1307 if (c->Drop)
1308 {
1309 e = GameEventNew(GAME_EVENT_ADD_PICKUP);
1310 strcpy(e.u.AddPickup.PickupClass, c->Drop->Name);
1311 e.u.AddPickup.Pos = Vec2ToNet(actor->Pos);
1312 GameEventsEnqueue(&gGameEvents, e);
1313 }
1314 else
1315 {
1316 // Add an ammo pickup of the actor's gun
1317 if (gCampaign.Setting.Ammo)
1318 {
1319 ActorAddAmmoPickup(actor);
1320 }
1321
1322 ActorAddGunPickup(actor);
1323 }
1324 }
1325
1326 // Add corpse
1327 if (ConfigGetEnum(&gConfig, "Graphics.Gore") != GORE_NONE)
1328 {
1329 GameEvent ea = GameEventNew(GAME_EVENT_MAP_OBJECT_ADD);
1330 ea.u.MapObjectAdd.UID = ObjsGetNextUID();
1331 const MapObject *corpse = StrMapObject(c->Class->Corpse);
1332 if (!corpse)
1333 {
1334 corpse = GetRandomBloodPool();
1335 ea.u.MapObjectAdd.Mask = Color2Net(c->Class->BloodColor);
1336 }
1337 strcpy(ea.u.MapObjectAdd.MapObjectClass, corpse->Name);
1338 ea.u.MapObjectAdd.Pos = Vec2ToNet(actor->Pos);
1339 ea.u.MapObjectAdd.ThingFlags = MapObjectGetFlags(corpse);
1340 ea.u.MapObjectAdd.Health = corpse->Health;
1341 GameEventsEnqueue(&gGameEvents, ea);
1342 }
1343
1344 e = GameEventNew(GAME_EVENT_ACTOR_DIE);
1345 e.u.ActorDie.UID = actor->uid;
1346 GameEventsEnqueue(&gGameEvents, e);
1347 }
1348 static bool IsUnarmedBot(const TActor *actor);
ActorAddAmmoPickup(const TActor * actor)1349 static void ActorAddAmmoPickup(const TActor *actor)
1350 {
1351 if (IsUnarmedBot(actor))
1352 {
1353 return;
1354 }
1355
1356 // Add ammo pickups for each of the actor's guns
1357 for (int i = 0; i < MAX_WEAPONS; i++)
1358 {
1359 const Weapon *w = &actor->guns[i];
1360 if (w->Gun == NULL)
1361 {
1362 continue;
1363 }
1364
1365 for (int j = 0; j < WeaponClassNumBarrels(w->Gun); j++)
1366 {
1367 const int ammoId = WC_BARREL_ATTR(*(w->Gun), AmmoId, j);
1368 // Check if the actor's gun has ammo at all
1369 if (ammoId < 0)
1370 {
1371 continue;
1372 }
1373 // Don't spawn ammo if no players use it
1374 if (PlayersNumUseAmmo(ammoId) == 0)
1375 {
1376 continue;
1377 }
1378
1379 GameEvent e = GameEventNew(GAME_EVENT_ADD_PICKUP);
1380 const Ammo *a = AmmoGetById(&gAmmo, ammoId);
1381 sprintf(e.u.AddPickup.PickupClass, "ammo_%s", a->Name);
1382 // Add a little random offset so the pickups aren't all together
1383 const struct vec2 offset = svec2(
1384 (float)RAND_INT(-TILE_WIDTH, TILE_WIDTH) / 2,
1385 (float)RAND_INT(-TILE_HEIGHT, TILE_HEIGHT) / 2);
1386 e.u.AddPickup.Pos = Vec2ToNet(svec2_add(actor->Pos, offset));
1387 GameEventsEnqueue(&gGameEvents, e);
1388 }
1389 }
1390 }
1391 static bool HasGunPickups(const WeaponClass *wc, const int n);
ActorAddGunPickup(const TActor * actor)1392 static void ActorAddGunPickup(const TActor *actor)
1393 {
1394 if (IsUnarmedBot(actor))
1395 {
1396 return;
1397 }
1398
1399 // Select a valid gun to drop
1400 for (int i = 0; i < MAX_WEAPONS; i++)
1401 {
1402 const WeaponClass *wc = actor->guns[i].Gun;
1403 if (wc == NULL)
1404 continue;
1405 if (!wc->CanDrop)
1406 continue;
1407 if (wc->DropGun)
1408 {
1409 wc = StrWeaponClass(wc->DropGun);
1410 CASSERT(wc != NULL, "Cannot find gun to drop");
1411 }
1412 // Don't drop gun if there's gun pickups for this already
1413 if (HasGunPickups(wc, 2))
1414 continue;
1415 PickupAddGun(wc, actor->Pos);
1416 break;
1417 }
1418 }
HasGunPickups(const WeaponClass * wc,const int n)1419 static bool HasGunPickups(const WeaponClass *wc, const int n)
1420 {
1421 const int wcId = WeaponClassId(wc);
1422 int count = 0;
1423 CA_FOREACH(const Pickup, p, gPickups)
1424 if (!p->isInUse)
1425 {
1426 continue;
1427 }
1428 if (p->class->Type == PICKUP_GUN && p->class->u.GunId == wcId)
1429 {
1430 count++;
1431 if (count >= n)
1432 {
1433 return true;
1434 }
1435 }
1436 CA_FOREACH_END()
1437 return false;
1438 }
IsUnarmedBot(const TActor * actor)1439 static bool IsUnarmedBot(const TActor *actor)
1440 {
1441 // Note: if the actor is AI with no shooting time,
1442 // then it's an unarmed actor
1443 const Character *c = ActorGetCharacter(actor);
1444 return c->bot != NULL && c->bot->probabilityToShoot == 0;
1445 }
1446
1447 static void VehicleTakePilot(const TActor *vehicle);
1448 // Check whether actors are overlapping with unpiloted vehicles,
1449 // and automatically pilot them
1450 // Only called at start of mission
ActorsPilotVehicles(void)1451 void ActorsPilotVehicles(void)
1452 {
1453 CA_FOREACH(const TActor, vehicle, gActors)
1454 if (!vehicle->isInUse)
1455 {
1456 continue;
1457 }
1458 if (vehicle->pilotUID == -1)
1459 {
1460 VehicleTakePilot(vehicle);
1461 }
1462 CA_FOREACH_END()
1463 }
VehicleTakePilot(const TActor * vehicle)1464 static void VehicleTakePilot(const TActor *vehicle)
1465 {
1466 CA_FOREACH(const TActor, pilot, gActors)
1467 if (!pilot->isInUse)
1468 {
1469 continue;
1470 }
1471 if (pilot->pilotUID == pilot->uid &&
1472 AABBOverlap(
1473 vehicle->Pos, pilot->Pos, vehicle->thing.size, pilot->thing.size))
1474 {
1475 GameEvent e = GameEventNew(GAME_EVENT_ACTOR_PILOT);
1476 e.u.Pilot.On = true;
1477 e.u.Pilot.UID = pilot->uid;
1478 e.u.Pilot.VehicleUID = vehicle->uid;
1479 GameEventsEnqueue(&gGameEvents, e);
1480 break;
1481 }
1482 CA_FOREACH_END()
1483 }
1484
ActorsInit(void)1485 void ActorsInit(void)
1486 {
1487 CArrayInit(&gActors, sizeof(TActor));
1488 CArrayReserve(&gActors, 64);
1489 sActorUIDs = 0;
1490 }
ActorsTerminate(void)1491 void ActorsTerminate(void)
1492 {
1493 CA_FOREACH(TActor, a, gActors)
1494 if (!a->isInUse)
1495 continue;
1496 ActorDestroy(a);
1497 CA_FOREACH_END()
1498 CArrayTerminate(&gActors);
1499 }
ActorsGetNextUID(void)1500 int ActorsGetNextUID(void)
1501 {
1502 return sActorUIDs++;
1503 }
ActorsGetFreeIndex(void)1504 int ActorsGetFreeIndex(void)
1505 {
1506 // Find an empty slot in actor list
1507 // actors.size if no slot found (i.e. add to end)
1508 CA_FOREACH(const TActor, a, gActors)
1509 if (!a->isInUse)
1510 {
1511 return _ca_index;
1512 }
1513 CA_FOREACH_END()
1514 return (int)gActors.size;
1515 }
1516
1517 static void GoreEmitterInit(Emitter *em, const char *particleClassName);
ActorAdd(NActorAdd aa)1518 TActor *ActorAdd(NActorAdd aa)
1519 {
1520 // Don't add if UID exists
1521 if (ActorGetByUID(aa.UID) != NULL)
1522 {
1523 LOG(LM_ACTOR, LL_DEBUG, "actor uid(%d) already exists; not adding",
1524 (int)aa.UID);
1525 return NULL;
1526 }
1527 const int id = ActorsGetFreeIndex();
1528 while (id >= (int)gActors.size)
1529 {
1530 TActor a;
1531 memset(&a, 0, sizeof a);
1532 CArrayPushBack(&gActors, &a);
1533 }
1534 TActor *actor = CArrayGet(&gActors, id);
1535 memset(actor, 0, sizeof *actor);
1536 actor->uid = aa.UID;
1537 LOG(LM_ACTOR, LL_DEBUG, "add actor uid(%d) playerUID(%d)", actor->uid,
1538 aa.PlayerUID);
1539 actor->pilotUID = aa.PilotUID;
1540 actor->vehicleUID = aa.VehicleUID;
1541 CArrayInit(&actor->ammo, sizeof(int));
1542 for (int i = 0; i < AmmoGetNumClasses(&gAmmo); i++)
1543 {
1544 // Initialise with twice the standard ammo amount
1545 const int amount =
1546 AmmoGetById(&gAmmo, i)->Amount * AMMO_STARTING_MULTIPLE;
1547 CArrayPushBack(&actor->ammo, &amount);
1548 }
1549 if (gMission.missionData->WeaponPersist)
1550 {
1551 for (int i = 0; i < aa.Ammo_count; i++)
1552 {
1553 // Use persisted ammo amount if it is greater
1554 if ((int)aa.Ammo[i].Amount >
1555 AmmoGetById(&gAmmo, aa.Ammo[i].Id)->Amount *
1556 AMMO_STARTING_MULTIPLE)
1557 {
1558 CArraySet(&actor->ammo, aa.Ammo[i].Id, &aa.Ammo[i].Amount);
1559 }
1560 }
1561 }
1562 actor->PlayerUID = aa.PlayerUID;
1563 actor->charId = aa.CharId;
1564 const Character *c = ActorGetCharacter(actor);
1565 if (aa.PlayerUID >= 0)
1566 {
1567 // Add all player weapons
1568 PlayerData *p = PlayerDataGetByUID(aa.PlayerUID);
1569 for (int i = 0; i < MAX_WEAPONS; i++)
1570 {
1571 Weapon gun = WeaponCreate(p->guns[i]);
1572 actor->guns[i] = gun;
1573 if (i < MAX_GUNS && ACTOR_GET_GUN(actor)->Gun == NULL)
1574 {
1575 actor->gunIndex = i;
1576 }
1577 if (i >= MAX_GUNS && ACTOR_GET_GRENADE(actor)->Gun == NULL)
1578 {
1579 actor->grenadeIndex = i - MAX_GUNS;
1580 }
1581 }
1582 p->ActorUID = aa.UID;
1583 }
1584 else
1585 {
1586 // Add sole weapon from character type
1587 Weapon gun = WeaponCreate(c->Gun);
1588 actor->guns[0] = gun;
1589 actor->gunIndex = 0;
1590 }
1591 actor->health = aa.Health;
1592 actor->action = ACTORACTION_MOVING;
1593 actor->thing.Pos.x = actor->thing.Pos.y = -1;
1594 actor->thing.kind = KIND_CHARACTER;
1595 actor->thing.drawFunc = NULL;
1596 actor->thing.size = svec2i(ACTOR_W, ACTOR_H);
1597 actor->thing.flags = THING_IMPASSABLE | THING_CAN_BE_SHOT | aa.ThingFlags;
1598 actor->thing.id = id;
1599 actor->isInUse = true;
1600
1601 actor->flags = FLAGS_SLEEPING | c->flags;
1602 // Flag corrections
1603 if (actor->flags & FLAGS_AWAKEALWAYS)
1604 {
1605 actor->flags &= ~FLAGS_SLEEPING;
1606 }
1607 // Rescue objectives always have follower flag on
1608 if (actor->thing.flags & THING_OBJECTIVE)
1609 {
1610 const Objective *o = CArrayGet(
1611 &gMission.missionData->Objectives,
1612 ObjectiveFromThing(actor->thing.flags));
1613 if (o->Type == OBJECTIVE_RESCUE)
1614 {
1615 // If they don't have prisoner flag set, automatically rescue
1616 // them
1617 if (!(actor->flags & FLAGS_PRISONER) && !gCampaign.IsClient)
1618 {
1619 GameEvent e = GameEventNew(GAME_EVENT_RESCUE_CHARACTER);
1620 e.u.Rescue.UID = aa.UID;
1621 GameEventsEnqueue(&gGameEvents, e);
1622 UpdateMissionObjective(
1623 &gMission, actor->thing.flags, OBJECTIVE_RESCUE, 1);
1624 }
1625 }
1626 }
1627
1628 actor->direction = aa.Direction;
1629 actor->DrawRadians = (float)dir2radians[actor->direction];
1630 actor->anim = AnimationGetActorAnimation(ACTORANIMATION_IDLE);
1631 actor->slideLock = 0;
1632 if (c->bot)
1633 {
1634 actor->aiContext = AIContextNew();
1635 ActorSetAIState(actor, AI_STATE_IDLE);
1636 }
1637
1638 EmitterInit(
1639 &actor->barrelSmoke, StrParticleClass(&gParticleClasses, "smoke"),
1640 svec2_zero(), -0.05f, 0.05f, 3, 3, 0, 0, 10);
1641 EmitterInit(
1642 &actor->healEffect, StrParticleClass(&gParticleClasses, "health_plus"),
1643 svec2_zero(), -0.1f, 0.1f, 0, 0, 0, 0, 0);
1644 GoreEmitterInit(&actor->blood1, "blood1");
1645 GoreEmitterInit(&actor->blood2, "blood2");
1646 GoreEmitterInit(&actor->blood3, "blood3");
1647
1648 TryMoveActor(actor, NetToVec2(aa.Pos));
1649
1650 return actor;
1651 }
GoreEmitterInit(Emitter * em,const char * particleClassName)1652 static void GoreEmitterInit(Emitter *em, const char *particleClassName)
1653 {
1654 EmitterInit(
1655 em, StrParticleClass(&gParticleClasses, particleClassName),
1656 svec2_zero(), 0, GORE_EMITTER_MAX_SPEED, 6, 12, -0.1, 0.1, 0);
1657 }
1658
ActorDestroy(TActor * a)1659 void ActorDestroy(TActor *a)
1660 {
1661 CASSERT(a->isInUse, "Destroying in-use actor");
1662 CArrayTerminate(&a->ammo);
1663 MapRemoveThing(&gMap, &a->thing);
1664 // Set PlayerData's ActorUID to -1 to signify actor destruction
1665 PlayerData *p = PlayerDataGetByUID(a->PlayerUID);
1666 if (p != NULL)
1667 p->ActorUID = -1;
1668 AIContextDestroy(a->aiContext);
1669 a->isInUse = false;
1670 }
1671
ActorGetByUID(const int uid)1672 TActor *ActorGetByUID(const int uid)
1673 {
1674 CA_FOREACH(TActor, a, gActors)
1675 if (a->uid == uid)
1676 {
1677 return a;
1678 }
1679 CA_FOREACH_END()
1680 return NULL;
1681 }
1682
ActorGetCharacter(const TActor * a)1683 const Character *ActorGetCharacter(const TActor *a)
1684 {
1685 if (a->PlayerUID >= 0)
1686 {
1687 return &PlayerDataGetByUID(a->PlayerUID)->Char;
1688 }
1689 return CArrayGet(&gCampaign.Setting.characters.OtherChars, a->charId);
1690 }
1691
ActorGetAverageWeaponMuzzleOffset(const TActor * a)1692 struct vec2 ActorGetAverageWeaponMuzzleOffset(const TActor *a)
1693 {
1694 const Weapon *w = ACTOR_GET_WEAPON(a);
1695 struct vec2 offset = svec2_zero();
1696 for (int i = 0; i < WeaponClassNumBarrels(w->Gun); i++)
1697 {
1698 offset = svec2_add(offset, ActorGetMuzzleOffset(a, w, i));
1699 }
1700 return svec2_scale(offset, 1.0f / (float)WeaponClassNumBarrels(w->Gun));
1701 }
ActorGetMuzzleOffset(const TActor * a,const Weapon * w,const int barrel)1702 struct vec2 ActorGetMuzzleOffset(
1703 const TActor *a, const Weapon *w, const int barrel)
1704 {
1705 const Character *c = ActorGetCharacter(a);
1706 const CharSprites *cs = c->Class->Sprites;
1707 return WeaponClassGetBarrelMuzzleOffset(
1708 w->Gun, cs, barrel, a->direction, w->barrels[barrel].state);
1709 }
ActorWeaponGetAmmo(const TActor * a,const WeaponClass * wc,const int barrel)1710 int ActorWeaponGetAmmo(
1711 const TActor *a, const WeaponClass *wc, const int barrel)
1712 {
1713 const int ammoId = WC_BARREL_ATTR(*wc, AmmoId, barrel);
1714 if (ammoId == -1)
1715 {
1716 return -1;
1717 }
1718 return *(int *)CArrayGet(&a->ammo, ammoId);
1719 }
ActorGetCanFireBarrel(const TActor * a,const Weapon * w)1720 int ActorGetCanFireBarrel(const TActor *a, const Weapon *w)
1721 {
1722 if (w->Gun == NULL)
1723 {
1724 return -1;
1725 }
1726 const int barrel = WeaponGetUnlockedBarrel(w);
1727 if (barrel == -1)
1728 {
1729 return -1;
1730 }
1731 const bool hasAmmo = ActorWeaponGetAmmo(a, w->Gun, barrel) != 0;
1732 if (gCampaign.Setting.Ammo && !hasAmmo)
1733 {
1734 // TODO: multi guns not firing if the first gun is out of ammo
1735 return -1;
1736 }
1737 return barrel;
1738 }
ActorTrySwitchWeapon(const TActor * a,const bool allGuns)1739 bool ActorTrySwitchWeapon(const TActor *a, const bool allGuns)
1740 {
1741 // Find the next weapon to switch to
1742 // If the player does not have a grenade key set, allow switching to
1743 // grenades (classic style)
1744 const int switchCount = allGuns ? MAX_WEAPONS : MAX_GUNS;
1745 const int startIndex =
1746 ActorGetNumGuns(a) > 0 ? a->gunIndex : a->grenadeIndex + MAX_GUNS;
1747 int weaponIndex = startIndex;
1748 do
1749 {
1750 weaponIndex = (weaponIndex + 1) % switchCount;
1751 } while (a->guns[weaponIndex].Gun == NULL);
1752 if (weaponIndex == startIndex)
1753 {
1754 // No other weapon to switch to
1755 return false;
1756 }
1757
1758 GameEvent e = GameEventNew(GAME_EVENT_ACTOR_SWITCH_GUN);
1759 e.u.ActorSwitchGun.UID = a->uid;
1760 e.u.ActorSwitchGun.GunIdx = weaponIndex;
1761 GameEventsEnqueue(&gGameEvents, e);
1762 return true;
1763 }
ActorSwitchGun(const NActorSwitchGun sg)1764 void ActorSwitchGun(const NActorSwitchGun sg)
1765 {
1766 TActor *a = ActorGetByUID(sg.UID);
1767 if (a == NULL || !a->isInUse)
1768 return;
1769 a->gunIndex = sg.GunIdx;
1770 const WeaponClass *gun = ACTOR_GET_WEAPON(a)->Gun;
1771 SoundPlayAt(&gSoundDevice, gun->SwitchSound, a->thing.Pos);
1772 ActorSetChatter(a, gun->name, CHATTER_SWITCH_GUN);
1773 }
1774
ActorIsImmune(const TActor * actor,const special_damage_e damage)1775 bool ActorIsImmune(const TActor *actor, const special_damage_e damage)
1776 {
1777 switch (damage)
1778 {
1779 case SPECIAL_FLAME:
1780 return actor->flags & FLAGS_ASBESTOS;
1781 case SPECIAL_POISON:
1782 case SPECIAL_PETRIFY: // fallthrough
1783 return actor->flags & FLAGS_IMMUNITY;
1784 case SPECIAL_CONFUSE:
1785 return actor->flags & FLAGS_IMMUNITY;
1786 default:
1787 break;
1788 }
1789 // Don't bother if health already 0 or less
1790 if (actor->health <= 0)
1791 {
1792 return 1;
1793 }
1794 return 0;
1795 }
1796
ActorTakesDamage(const TActor * actor,const special_damage_e damage)1797 bool ActorTakesDamage(const TActor *actor, const special_damage_e damage)
1798 {
1799 switch (damage)
1800 {
1801 case SPECIAL_FLAME:
1802 return !(actor->flags & FLAGS_ASBESTOS);
1803 default:
1804 return true;
1805 }
1806 }
1807
1808 #define MAX_POISONED_COUNT 140
1809
ActorTakeSpecialDamage(TActor * actor,const special_damage_e damage,const int ticks)1810 static void ActorTakeSpecialDamage(
1811 TActor *actor, const special_damage_e damage, const int ticks)
1812 {
1813 if (ActorIsImmune(actor, damage))
1814 {
1815 return;
1816 }
1817 switch (damage)
1818 {
1819 case SPECIAL_FLAME:
1820 actor->flamed = ticks;
1821 break;
1822 case SPECIAL_POISON:
1823 actor->poisoned = MAX(actor->poisoned + ticks, MAX_POISONED_COUNT);
1824 break;
1825 case SPECIAL_PETRIFY:
1826 if (!actor->petrified)
1827 {
1828 actor->petrified = ticks;
1829 }
1830 break;
1831 case SPECIAL_CONFUSE:
1832 actor->confused = ticks;
1833 break;
1834 default:
1835 // do nothing
1836 break;
1837 }
1838 }
1839
1840 static void ActorTakeHit(
1841 TActor *actor, const int flags, const int sourceUID,
1842 const special_damage_e damage, const int specialTicks);
ActorHit(const NThingDamage d)1843 void ActorHit(const NThingDamage d)
1844 {
1845 TActor *a = ActorGetByUID(d.UID);
1846 if (!a->isInUse)
1847 return;
1848
1849 ActorTakeHit(a, d.Flags, d.SourceActorUID, d.Special, d.SpecialTicks);
1850 if (d.Power > 0)
1851 {
1852 DamageActor(a, d.Power, d.SourceActorUID);
1853
1854 // Add damage text
1855 // See if there is one already; if so remove it and add a new one,
1856 // combining the damage numbers
1857 int damage = (int)d.Power;
1858 struct vec2 pos =
1859 svec2_add(a->Pos, svec2(RAND_FLOAT(-3, 3), RAND_FLOAT(-3, 3)));
1860 CA_FOREACH(const Particle, p, gParticles)
1861 if (p->isInUse && p->ActorUID == a->uid)
1862 {
1863 damage += a->accumulatedDamage;
1864 if (svec2_distance(pos, p->Pos) <
1865 DAMAGE_TEXT_DISTANCE_RESET_THRESHOLD)
1866 {
1867 pos = p->Pos;
1868 }
1869 GameEvent e = GameEventNew(GAME_EVENT_PARTICLE_REMOVE);
1870 e.u.ParticleRemoveId = _ca_index;
1871 GameEventsEnqueue(&gGameEvents, e);
1872 break;
1873 }
1874 CA_FOREACH_END()
1875 a->accumulatedDamage = damage;
1876
1877 GameEvent s = GameEventNew(GAME_EVENT_ADD_PARTICLE);
1878 s.u.AddParticle.Class =
1879 StrParticleClass(&gParticleClasses, "damage_text");
1880 s.u.AddParticle.ActorUID = a->uid;
1881 s.u.AddParticle.Pos = pos;
1882 s.u.AddParticle.Z = BULLET_Z * Z_FACTOR;
1883 s.u.AddParticle.DZ = 3;
1884 sprintf(s.u.AddParticle.Text, "-%d", damage);
1885 GameEventsEnqueue(&gGameEvents, s);
1886
1887 ActorAddBloodSplatters(a, d.Power, d.Mass, NetToVec2(d.Vel));
1888
1889 // Rumble if taking hit
1890 if (a->PlayerUID >= 0)
1891 {
1892 const PlayerData *p = PlayerDataGetByUID(a->PlayerUID);
1893 if (p->inputDevice == INPUT_DEVICE_JOYSTICK)
1894 {
1895 JoyImpact(p->deviceIndex);
1896 }
1897 }
1898
1899 a->grimaceCounter = GRIMACE_HIT_TICKS;
1900 }
1901 }
1902
ActorTakeHit(TActor * actor,const int flags,const int sourceUID,const special_damage_e damage,const int specialTicks)1903 static void ActorTakeHit(
1904 TActor *actor, const int flags, const int sourceUID,
1905 const special_damage_e damage, const int specialTicks)
1906 {
1907 // Wake up if this is an AI
1908 if (!gCampaign.IsClient)
1909 {
1910 AIWake(actor, 1);
1911 }
1912 const TActor *source = ActorGetByUID(sourceUID);
1913 const int playerUID = source != NULL ? source->PlayerUID : -1;
1914 if (ActorIsInvulnerable(
1915 actor, flags, playerUID, gCampaign.Entry.Mode, damage))
1916 {
1917 return;
1918 }
1919 ActorTakeSpecialDamage(actor, damage, specialTicks);
1920 }
1921
ActorIsInvulnerable(const TActor * actor,const int flags,const int playerUID,const GameMode mode,const special_damage_e special)1922 bool ActorIsInvulnerable(
1923 const TActor *actor, const int flags, const int playerUID,
1924 const GameMode mode, const special_damage_e special)
1925 {
1926 if (actor->flags & FLAGS_INVULNERABLE)
1927 {
1928 return true;
1929 }
1930
1931 if (!(flags & FLAGS_HURTALWAYS) && !(actor->flags & FLAGS_VICTIM))
1932 {
1933 // Same player hits
1934 if (playerUID >= 0 && playerUID == actor->PlayerUID)
1935 {
1936 return true;
1937 }
1938 const bool isGood = playerUID >= 0 || (flags & FLAGS_GOOD_GUY);
1939 const bool isTargetGood =
1940 actor->PlayerUID >= 0 || (actor->flags & FLAGS_GOOD_GUY);
1941 // Friendly fire (NPCs)
1942 if (!IsPVP(mode) && !ConfigGetBool(&gConfig, "Game.FriendlyFire") &&
1943 isGood && isTargetGood)
1944 {
1945 return true;
1946 }
1947 // Enemies don't hurt each other
1948 if (!isGood && !isTargetGood)
1949 {
1950 return true;
1951 }
1952 if (!ActorTakesDamage(actor, special))
1953 {
1954 return true;
1955 }
1956 }
1957
1958 return false;
1959 }
1960
ActorAddBloodSplatters(TActor * a,const int power,const float mass,const struct vec2 hitVector)1961 static void ActorAddBloodSplatters(
1962 TActor *a, const int power, const float mass, const struct vec2 hitVector)
1963 {
1964 const GoreAmount ga = ConfigGetEnum(&gConfig, "Graphics.Gore");
1965 if (ga == GORE_NONE)
1966 return;
1967
1968 // Emit blood based on power and gore setting
1969 int bloodPower = power * 2;
1970 // Randomly cycle through the blood types
1971 int bloodSize = 1;
1972 const struct vec2 hitVNorm = svec2_normalize(hitVector);
1973 const float speedBase = MAX(1.0f, mass) * SHOT_IMPULSE_FACTOR;
1974 while (bloodPower > 0)
1975 {
1976 Emitter *em = NULL;
1977 switch (bloodSize)
1978 {
1979 case 1:
1980 em = &a->blood1;
1981 break;
1982 case 2:
1983 em = &a->blood2;
1984 break;
1985 default:
1986 em = &a->blood3;
1987 break;
1988 }
1989 bloodSize++;
1990 if (bloodSize > 3)
1991 {
1992 bloodSize = 1;
1993 }
1994 const struct vec2 vel =
1995 svec2_scale(hitVNorm, speedBase * RAND_FLOAT(0.5f, 1));
1996 AddParticle ap;
1997 memset(&ap, 0, sizeof ap);
1998 ap.Pos = a->Pos;
1999 ap.Angle = NAN;
2000 ap.Z = 10;
2001 ap.Vel = vel;
2002 ap.Mask = ActorGetCharacter(a)->Class->BloodColor;
2003 EmitterStart(em, &ap);
2004 switch (ga)
2005 {
2006 case GORE_LOW:
2007 bloodPower /= 8;
2008 break;
2009 case GORE_MEDIUM:
2010 bloodPower /= 2;
2011 break;
2012 default:
2013 bloodPower = bloodPower * 7 / 8;
2014 break;
2015 }
2016 }
2017 }
2018
ActorGetHealthPercent(const TActor * a)2019 int ActorGetHealthPercent(const TActor *a)
2020 {
2021 const int maxHealth = ActorGetCharacter(a)->maxHealth;
2022 return a->health * 100 / maxHealth;
2023 }
2024
ActorIsLowHealth(const TActor * a)2025 bool ActorIsLowHealth(const TActor *a)
2026 {
2027 return ActorGetHealthPercent(a) < LOW_HEALTH_PERCENTAGE;
2028 }
2029
ActorIsGrimacing(const TActor * a)2030 bool ActorIsGrimacing(const TActor *a)
2031 {
2032 const Weapon *gun = ACTOR_GET_WEAPON(a);
2033 if (gun->Gun)
2034 {
2035 for (int i = 0; i < WeaponClassNumBarrels(gun->Gun); i++)
2036 {
2037 if (gun->barrels[i].state == GUNSTATE_FIRING ||
2038 gun->barrels[i].state == GUNSTATE_RECOIL)
2039 {
2040 return true;
2041 }
2042 }
2043 }
2044 return (a->grimaceCounter % GRIMACE_PERIOD) > GRIMACE_PERIOD / 2;
2045 }
2046
ActorIsLocalPlayer(const int uid)2047 bool ActorIsLocalPlayer(const int uid)
2048 {
2049 const TActor *a = ActorGetByUID(uid);
2050 // Don't accept updates if actor doesn't exist
2051 // This can happen in the very first frame, where we haven't yet
2052 // processed an actor add message
2053 // Otherwise this shouldn't happen
2054 if (a == NULL)
2055 return true;
2056
2057 return PlayerIsLocal(a->PlayerUID);
2058 }
2059