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