1 /*
2 	C-Dogs SDL
3 	A port of the legendary (and fun) action/arcade cdogs.
4 	Copyright (c) 2014-2015, 2017-2020 Cong Xu
5 	All rights reserved.
6 
7 	Redistribution and use in source and binary forms, with or without
8 	modification, are permitted provided that the following conditions are met:
9 
10 	Redistributions of source code must retain the above copyright notice, this
11 	list of conditions and the following disclaimer.
12 	Redistributions in binary form must reproduce the above copyright notice,
13 	this list of conditions and the following disclaimer in the documentation
14 	and/or other materials provided with the distribution.
15 
16 	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 	AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 	IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 	ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20 	LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 	CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 	SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 	INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 	CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 	ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 	POSSIBILITY OF SUCH DAMAGE.
27 */
28 #include "pickup.h"
29 
30 #include "ammo.h"
31 #include "game_events.h"
32 #include "gamedata.h"
33 #include "json_utils.h"
34 #include "map.h"
35 #include "net_util.h"
36 
37 CArray gPickups;
38 static unsigned int sPickupUIDs;
39 #define PICKUP_SIZE svec2i(8, 8)
40 
PickupsInit(void)41 void PickupsInit(void)
42 {
43 	CArrayInit(&gPickups, sizeof(Pickup));
44 	CArrayReserve(&gPickups, 128);
45 	sPickupUIDs = 0;
46 }
PickupsTerminate(void)47 void PickupsTerminate(void)
48 {
49 	CA_FOREACH(const Pickup, p, gPickups)
50 	if (p->isInUse)
51 	{
52 		PickupDestroy(p->UID);
53 	}
54 	CA_FOREACH_END()
55 	CArrayTerminate(&gPickups);
56 }
PickupsGetNextUID(void)57 int PickupsGetNextUID(void)
58 {
59 	return sPickupUIDs++;
60 }
61 static void PickupDraw(
62 	GraphicsDevice *g, const int id, const struct vec2i pos);
PickupAdd(const NAddPickup ap)63 void PickupAdd(const NAddPickup ap)
64 {
65 	// Check if existing pickup
66 	Pickup *p = PickupGetByUID(ap.UID);
67 	if (p != NULL && p->isInUse)
68 	{
69 		PickupDestroy(ap.UID);
70 	}
71 	// Find an empty slot in pickup list
72 	p = NULL;
73 	int i;
74 	for (i = 0; i < (int)gPickups.size; i++)
75 	{
76 		Pickup *pu = CArrayGet(&gPickups, i);
77 		if (!pu->isInUse)
78 		{
79 			p = pu;
80 			break;
81 		}
82 	}
83 	if (p == NULL)
84 	{
85 		Pickup pu;
86 		memset(&pu, 0, sizeof pu);
87 		CArrayPushBack(&gPickups, &pu);
88 		i = (int)gPickups.size - 1;
89 		p = CArrayGet(&gPickups, i);
90 	}
91 	memset(p, 0, sizeof *p);
92 	p->UID = ap.UID;
93 	p->class = StrPickupClass(ap.PickupClass);
94 	ThingInit(&p->thing, i, KIND_PICKUP, PICKUP_SIZE, ap.ThingFlags);
95 	p->thing.CPic = p->class->Pic;
96 	p->thing.CPicFunc = PickupDraw;
97 	MapTryMoveThing(&gMap, &p->thing, NetToVec2(ap.Pos));
98 	p->IsRandomSpawned = ap.IsRandomSpawned;
99 	p->PickedUp = false;
100 	p->SpawnerUID = ap.SpawnerUID;
101 	p->isInUse = true;
102 }
PickupAddGun(const WeaponClass * w,const struct vec2 pos)103 void PickupAddGun(const WeaponClass *w, const struct vec2 pos)
104 {
105 	if (!w->CanDrop)
106 	{
107 		return;
108 	}
109 	GameEvent e = GameEventNew(GAME_EVENT_ADD_PICKUP);
110 	sprintf(e.u.AddPickup.PickupClass, "gun_%s", w->name);
111 	e.u.AddPickup.Pos = Vec2ToNet(pos);
112 	GameEventsEnqueue(&gGameEvents, e);
113 }
PickupDestroy(const int uid)114 void PickupDestroy(const int uid)
115 {
116 	Pickup *p = PickupGetByUID(uid);
117 	CASSERT(p->isInUse, "Destroying not-in-use pickup");
118 	MapRemoveThing(&gMap, &p->thing);
119 	p->isInUse = false;
120 }
121 
PickupsUpdate(CArray * pickups,const int ticks)122 void PickupsUpdate(CArray *pickups, const int ticks)
123 {
124 	CA_FOREACH(Pickup, p, *pickups)
125 	if (!p->isInUse)
126 	{
127 		continue;
128 	}
129 	ThingUpdate(&p->thing, ticks);
130 	CA_FOREACH_END()
131 }
132 
133 static bool TreatAsGunPickup(const Pickup *p, const TActor *a);
134 static bool TryPickupAmmo(TActor *a, const Pickup *p);
135 static bool TryPickupGun(
136 	TActor *a, const Pickup *p, const bool pickupAll, const char **sound);
PickupPickup(TActor * a,Pickup * p,const bool pickupAll)137 void PickupPickup(TActor *a, Pickup *p, const bool pickupAll)
138 {
139 	if (p->PickedUp)
140 		return;
141 	CASSERT(a->PlayerUID >= 0, "NPCs cannot pickup");
142 	bool canPickup = true;
143 	const char *sound = p->class->Sound;
144 	const struct vec2 actorPos = a->thing.Pos;
145 	switch (p->class->Type)
146 	{
147 	case PICKUP_JEWEL: {
148 		GameEvent e = GameEventNew(GAME_EVENT_SCORE);
149 		e.u.Score.PlayerUID = a->PlayerUID;
150 		e.u.Score.Score = p->class->u.Score;
151 		GameEventsEnqueue(&gGameEvents, e);
152 
153 		e = GameEventNew(GAME_EVENT_ADD_PARTICLE);
154 		e.u.AddParticle.Class =
155 			StrParticleClass(&gParticleClasses, "score_text");
156 		e.u.AddParticle.ActorUID = a->uid;
157 		e.u.AddParticle.Pos = p->thing.Pos;
158 		e.u.AddParticle.DZ = 3;
159 		if (gCampaign.Setting.Ammo)
160 		{
161 			sprintf(e.u.AddParticle.Text, "$%d", p->class->u.Score);
162 		}
163 		else
164 		{
165 			sprintf(e.u.AddParticle.Text, "+%d", p->class->u.Score);
166 		}
167 		GameEventsEnqueue(&gGameEvents, e);
168 
169 		UpdateMissionObjective(
170 			&gMission, p->thing.flags, OBJECTIVE_COLLECT, 1);
171 	}
172 	break;
173 
174 	case PICKUP_HEALTH:
175 		// Don't pick up unless taken damage
176 		canPickup = false;
177 		if (a->health < ActorGetCharacter(a)->maxHealth)
178 		{
179 			canPickup = true;
180 			GameEvent e = GameEventNew(GAME_EVENT_ACTOR_HEAL);
181 			e.u.Heal.UID = a->uid;
182 			e.u.Heal.PlayerUID = a->PlayerUID;
183 			e.u.Heal.Amount = p->class->u.Health;
184 			e.u.Heal.IsRandomSpawned = p->IsRandomSpawned;
185 			GameEventsEnqueue(&gGameEvents, e);
186 		}
187 		break;
188 
189 	case PICKUP_AMMO: // fallthrough
190 	case PICKUP_GUN:
191 		if (TreatAsGunPickup(p, a))
192 		{
193 			canPickup = TryPickupGun(a, p, pickupAll, &sound);
194 		}
195 		else
196 		{
197 			canPickup = TryPickupAmmo(a, p);
198 		}
199 		break;
200 
201 	case PICKUP_KEYCARD: {
202 		GameEvent e = GameEventNew(GAME_EVENT_ADD_KEYS);
203 		e.u.AddKeys.KeyFlags = p->class->u.Keys;
204 		e.u.AddKeys.Pos = Vec2ToNet(actorPos);
205 		GameEventsEnqueue(&gGameEvents, e);
206 		sound = "key";
207 	}
208 	break;
209 
210 	case PICKUP_SHOW_MAP: {
211 		GameEvent e = GameEventNew(GAME_EVENT_EXPLORE_TILES);
212 		e.u.ExploreTiles.Runs_count = 1;
213 		e.u.ExploreTiles.Runs[0].Run = gMap.Size.x * gMap.Size.y;
214 		GameEventsEnqueue(&gGameEvents, e);
215 	}
216 	break;
217 
218 	default:
219 		CASSERT(false, "unexpected pickup type");
220 		break;
221 	}
222 	if (canPickup)
223 	{
224 		if (sound != NULL)
225 		{
226 			GameEvent es = GameEventNew(GAME_EVENT_SOUND_AT);
227 			strcpy(es.u.SoundAt.Sound, sound);
228 			es.u.SoundAt.Pos = Vec2ToNet(actorPos);
229 			GameEventsEnqueue(&gGameEvents, es);
230 		}
231 		GameEvent e = GameEventNew(GAME_EVENT_REMOVE_PICKUP);
232 		e.u.RemovePickup.UID = p->UID;
233 		e.u.RemovePickup.SpawnerUID = p->SpawnerUID;
234 		GameEventsEnqueue(&gGameEvents, e);
235 		// Prevent multiple pickups by marking
236 		p->PickedUp = true;
237 		a->PickupAll = false;
238 		a->CanPickupSpecial = false;
239 	}
240 }
241 
242 static bool HasGunUsingAmmo(const TActor *a, const int ammoId);
TreatAsGunPickup(const Pickup * p,const TActor * a)243 static bool TreatAsGunPickup(const Pickup *p, const TActor *a)
244 {
245 	// Grenades can also be gun pickups; treat as gun pickup if the player
246 	// doesn't have its ammo
247 	switch (p->class->Type)
248 	{
249 	case PICKUP_AMMO:
250 		if (!HasGunUsingAmmo(a, p->class->u.Ammo.Id))
251 		{
252 			const Ammo *ammo = AmmoGetById(&gAmmo, p->class->u.Ammo.Id);
253 			if (ammo->DefaultGun)
254 			{
255 				return true;
256 			}
257 		}
258 		return false;
259 	case PICKUP_GUN: {
260 		const WeaponClass *wc = IdWeaponClass(p->class->u.GunId);
261 		// TODO: support picking up multi guns?
262 		return wc->Type == GUNTYPE_NORMAL ||
263 			   (wc->Type == GUNTYPE_GRENADE &&
264 				!HasGunUsingAmmo(a, wc->u.Normal.AmmoId));
265 	}
266 	default:
267 		CASSERT(false, "unexpected pickup type");
268 		return false;
269 	}
270 }
HasGunUsingAmmo(const TActor * a,const int ammoId)271 static bool HasGunUsingAmmo(const TActor *a, const int ammoId)
272 {
273 	for (int i = 0; i < MAX_WEAPONS; i++)
274 	{
275 		const WeaponClass *wc = a->guns[i].Gun;
276 		if (wc == NULL)
277 			continue;
278 		for (int j = 0; j < WeaponClassNumBarrels(wc); j++)
279 		{
280 			if (WC_BARREL_ATTR(*wc, AmmoId, j) == ammoId)
281 			{
282 				return true;
283 			}
284 		}
285 	}
286 	return false;
287 }
288 
TryPickupAmmo(TActor * a,const Pickup * p)289 static bool TryPickupAmmo(TActor *a, const Pickup *p)
290 {
291 	// Don't pickup if not using ammo
292 	if (!gCampaign.Setting.Ammo)
293 	{
294 		return false;
295 	}
296 	// Don't pickup if ammo full
297 	const Ammo *ammo = AmmoGetById(
298 		&gAmmo, p->class->Type == PICKUP_AMMO
299 					? (int)p->class->u.Ammo.Id
300 					: IdWeaponClass(p->class->u.GunId)->u.Normal.AmmoId);
301 	const int current = *(int *)CArrayGet(&a->ammo, p->class->u.Ammo.Id);
302 	if (current >= ammo->Max)
303 	{
304 		return false;
305 	}
306 
307 	// Take ammo
308 	GameEvent e = GameEventNew(GAME_EVENT_ACTOR_ADD_AMMO);
309 	e.u.AddAmmo.UID = a->uid;
310 	e.u.AddAmmo.PlayerUID = a->PlayerUID;
311 	e.u.AddAmmo.Ammo.Id = p->class->u.Ammo.Id;
312 	e.u.AddAmmo.Ammo.Amount = p->class->u.Ammo.Amount;
313 	e.u.AddAmmo.IsRandomSpawned = p->IsRandomSpawned;
314 	// Note: receiving end will prevent ammo from exceeding max
315 	GameEventsEnqueue(&gGameEvents, e);
316 	return true;
317 }
TryPickupGun(TActor * a,const Pickup * p,const bool pickupAll,const char ** sound)318 static bool TryPickupGun(
319 	TActor *a, const Pickup *p, const bool pickupAll, const char **sound)
320 {
321 	// Guns can only be picked up manually
322 	if (!pickupAll)
323 	{
324 		return false;
325 	}
326 
327 	// When picking up a gun, the actor always ends up with it equipped, but:
328 	// - If the player already has the gun:
329 	//   - Switch to the same gun and drop the same gun
330 	// - If the player doesn't have the gun:
331 	//   - If the player has an empty slot, pickup the gun into that slot
332 	//   - If the player doesn't have an empty slot, replace the current gun,
333 	//     dropping it in the process
334 
335 	const WeaponClass *wc =
336 		p->class->Type == PICKUP_GUN
337 			? IdWeaponClass(p->class->u.GunId)
338 			: StrWeaponClass(
339 				  AmmoGetById(&gAmmo, p->class->u.Ammo.Id)->DefaultGun);
340 	const int actorsGunIdx = ActorFindGun(a, wc);
341 
342 	if (actorsGunIdx >= 0)
343 	{
344 		// Actor already has gun
345 
346 		// Switch to the same gun
347 		GameEvent e = GameEventNew(GAME_EVENT_ACTOR_SWITCH_GUN);
348 		e.u.ActorSwitchGun.UID = a->uid;
349 		e.u.ActorSwitchGun.GunIdx = actorsGunIdx;
350 		GameEventsEnqueue(&gGameEvents, e);
351 
352 		// Drop the same gun
353 		PickupAddGun(wc, a->Pos);
354 	}
355 	else
356 	{
357 		// Pickup gun
358 		// Replace the current gun, unless there's a free slot, in which case
359 		// pick up into the free spot
360 		const int weaponIndexStart =
361 			wc->Type == GUNTYPE_GRENADE ? MAX_GUNS : 0;
362 		const int weaponIndexEnd =
363 			wc->Type == GUNTYPE_GRENADE ? MAX_WEAPONS : MAX_GUNS;
364 		GameEvent e = GameEventNew(GAME_EVENT_ACTOR_REPLACE_GUN);
365 		e.u.ActorReplaceGun.UID = a->uid;
366 		strcpy(e.u.ActorReplaceGun.Gun, wc->name);
367 		e.u.ActorReplaceGun.GunIdx = wc->Type == GUNTYPE_GRENADE
368 										 ? a->grenadeIndex + MAX_GUNS
369 										 : a->gunIndex;
370 		int replaceGunIndex = e.u.ActorReplaceGun.GunIdx;
371 		for (int i = weaponIndexStart; i < weaponIndexEnd; i++)
372 		{
373 			if (a->guns[i].Gun == NULL)
374 			{
375 				e.u.ActorReplaceGun.GunIdx = i;
376 				replaceGunIndex = -1;
377 				break;
378 			}
379 		}
380 		GameEventsEnqueue(&gGameEvents, e);
381 
382 		// If replacing a gun, "drop" the gun being replaced (i.e. create a gun
383 		// pickup)
384 		if (replaceGunIndex >= 0)
385 		{
386 			PickupAddGun(a->guns[replaceGunIndex].Gun, a->Pos);
387 		}
388 	}
389 
390 	// If the player has less ammo than the default amount,
391 	// replenish up to this amount
392 	// TODO: support multi gun
393 	const int ammoId = WC_BARREL_ATTR(*wc, AmmoId, 0);
394 	if (ammoId >= 0)
395 	{
396 		const Ammo *ammo = AmmoGetById(&gAmmo, ammoId);
397 		const int ammoDeficit = ammo->Amount * AMMO_STARTING_MULTIPLE -
398 								*(int *)CArrayGet(&a->ammo, ammoId);
399 		if (ammoDeficit > 0)
400 		{
401 			GameEvent e = GameEventNew(GAME_EVENT_ACTOR_ADD_AMMO);
402 			e.u.AddAmmo.UID = a->uid;
403 			e.u.AddAmmo.PlayerUID = a->PlayerUID;
404 			e.u.AddAmmo.Ammo.Id = ammoId;
405 			e.u.AddAmmo.Ammo.Amount = ammoDeficit;
406 			e.u.AddAmmo.IsRandomSpawned = false;
407 			GameEventsEnqueue(&gGameEvents, e);
408 
409 			// Also play an ammo pickup sound
410 			*sound = ammo->Sound;
411 		}
412 	}
413 
414 	return true;
415 }
416 
PickupIsManual(const Pickup * p)417 bool PickupIsManual(const Pickup *p)
418 {
419 	if (p->PickedUp)
420 		return false;
421 	switch (p->class->Type)
422 	{
423 	case PICKUP_GUN:
424 		return true;
425 	case PICKUP_AMMO: {
426 		const Ammo *ammo = AmmoGetById(&gAmmo, p->class->u.Ammo.Id);
427 		return ammo->DefaultGun != NULL;
428 	}
429 	default:
430 		return false;
431 	}
432 }
433 
PickupDraw(GraphicsDevice * g,const int id,const struct vec2i pos)434 static void PickupDraw(GraphicsDevice *g, const int id, const struct vec2i pos)
435 {
436 	const Pickup *p = CArrayGet(&gPickups, id);
437 	CASSERT(p->isInUse, "Cannot draw non-existent pickup");
438 	CPicDrawContext c = CPicDrawContextNew();
439 	const Pic *pic = CPicGetPic(&p->thing.CPic, c.Dir);
440 	if (pic != NULL)
441 	{
442 		c.Offset = svec2i_scale_divide(CPicGetSize(&p->class->Pic), -2);
443 	}
444 	CPicDraw(g, &p->thing.CPic, pos, &c);
445 }
446 
PickupGetByUID(const int uid)447 Pickup *PickupGetByUID(const int uid)
448 {
449 	CA_FOREACH(Pickup, p, gPickups)
450 	if (p->UID == uid)
451 	{
452 		return p;
453 	}
454 	CA_FOREACH_END()
455 	return NULL;
456 }
457