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