1 /*
2 	C-Dogs SDL
3 	A port of the legendary (and fun) action/arcade cdogs.
4 	Copyright (c) 2013-2021 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 "weapon_class.h"
29 
30 #include "ammo.h"
31 #include "game_events.h"
32 #include "json_utils.h"
33 #include "log.h"
34 #include "net_util.h"
35 #include "utils.h"
36 
37 WeaponClasses gWeaponClasses;
38 
GunTypeStr(const GunType t)39 static const char *GunTypeStr(const GunType t)
40 {
41 	switch (t)
42 	{
43 		T2S(GUNTYPE_NORMAL, "Normal");
44 		T2S(GUNTYPE_GRENADE, "Grenade");
45 		T2S(GUNTYPE_MULTI, "Multi");
46 	default:
47 		return "";
48 	}
49 }
50 
51 // Initialise all the static weapon data
52 #define VERSION 3
WeaponClassesInitialize(WeaponClasses * wcs)53 void WeaponClassesInitialize(WeaponClasses *wcs)
54 {
55 	memset(wcs, 0, sizeof *wcs);
56 	CArrayInit(&wcs->Guns, sizeof(WeaponClass));
57 	CArrayInit(&wcs->CustomGuns, sizeof(WeaponClass));
58 }
59 static void LoadWeaponClass(WeaponClass *wc, json_t *node, const int version);
60 static void WeaponClassTerminate(WeaponClass *wc);
WeaponClassesLoadJSON(WeaponClasses * wcs,CArray * classes,json_t * root)61 void WeaponClassesLoadJSON(WeaponClasses *wcs, CArray *classes, json_t *root)
62 {
63 	LOG(LM_MAP, LL_DEBUG, "loading weapons");
64 	int version;
65 	LoadInt(&version, root, "Version");
66 	if (version > VERSION || version <= 0)
67 	{
68 		CASSERT(false, "cannot read guns file version");
69 		return;
70 	}
71 
72 	if (classes == &wcs->Guns)
73 	{
74 		CASSERT(wcs->Guns.size == 0, "guns not empty");
75 		for (int i = 0; i < GUN_COUNT; i++)
76 		{
77 			WeaponClass gd;
78 			memset(&gd, 0, sizeof gd);
79 			CArrayPushBack(&wcs->Guns, &gd);
80 		}
81 	}
82 	json_t *gunsNode = json_find_first_label(root, "Guns")->child;
83 	for (json_t *child = gunsNode->child; child; child = child->next)
84 	{
85 		WeaponClass gd;
86 		LoadWeaponClass(&gd, child, version);
87 		int idx = -1;
88 		// Only allow index for non-custom guns
89 		if (classes == &wcs->Guns)
90 		{
91 			LoadInt(&idx, child, "Index");
92 		}
93 		if (idx >= 0 && idx < GUN_COUNT)
94 		{
95 			WeaponClass *gExisting = CArrayGet(&wcs->Guns, idx);
96 			WeaponClassTerminate(gExisting);
97 			memcpy(gExisting, &gd, sizeof gd);
98 		}
99 		else
100 		{
101 			CArrayPushBack(classes, &gd);
102 		}
103 	}
104 	json_t *pseudoGunsNode = json_find_first_label(root, "PseudoGuns");
105 	if (pseudoGunsNode != NULL)
106 	{
107 		for (json_t *child = pseudoGunsNode->child->child; child;
108 			 child = child->next)
109 		{
110 			WeaponClass gd;
111 			LoadWeaponClass(&gd, child, version);
112 			gd.IsRealGun = false;
113 			CArrayPushBack(classes, &gd);
114 		}
115 	}
116 }
LoadWeaponClass(WeaponClass * wc,json_t * node,const int version)117 static void LoadWeaponClass(WeaponClass *wc, json_t *node, const int version)
118 {
119 	memset(wc, 0, sizeof *wc);
120 
121 	const json_t *gunsNode = json_find_first_label(node, "Guns");
122 	if (gunsNode)
123 	{
124 		wc->Type = GUNTYPE_MULTI;
125 	}
126 	else
127 	{
128 		bool isGrenade = false;
129 		LoadBool(&isGrenade, node, "IsGrenade");
130 		if (isGrenade)
131 		{
132 			wc->Type = GUNTYPE_GRENADE;
133 		}
134 	}
135 
136 	char *tmp;
137 
138 	if (wc->Type != GUNTYPE_MULTI)
139 	{
140 		// Initialise default gun values
141 		CSTRDUP(wc->u.Normal.Sprites, "chars/guns/blaster");
142 		wc->u.Normal.Grips = 1;
143 		wc->Icon = PicManagerGetPic(&gPicManager, "peashooter");
144 		wc->u.Normal.Sound = StrSound("bang");
145 		wc->SwitchSound = StrSound("switch");
146 		wc->u.Normal.Spread.Count = 1;
147 		wc->u.Normal.MuzzleHeight = 10 * Z_FACTOR;
148 		wc->u.Normal.MuzzleFlash =
149 			StrParticleClass(&gParticleClasses, "muzzle_flash_default");
150 		wc->u.Normal.AmmoId = -1;
151 		wc->u.Normal.CanShoot = true;
152 		wc->CanDrop = true;
153 	}
154 
155 	const Pic *icon = NULL;
156 	LoadPic(&icon, node, "Icon");
157 	if (icon != NULL)
158 	{
159 		wc->Icon = icon;
160 	}
161 
162 	tmp = NULL;
163 	LoadStr(&tmp, node, "Name");
164 	if (tmp != NULL)
165 	{
166 		CFREE(wc->name);
167 		wc->name = tmp;
168 	}
169 
170 	tmp = NULL;
171 	LoadStr(&tmp, node, "Description");
172 	if (tmp != NULL)
173 	{
174 		CFREE(wc->Description);
175 		wc->Description = tmp;
176 	}
177 
178 	LoadInt(&wc->Lock, node, "Lock");
179 
180 	LoadSoundFromNode(&wc->SwitchSound, node, "SwitchSound");
181 
182 	LoadBool(&wc->CanDrop, node, "CanDrop");
183 
184 	LoadStr(&wc->DropGun, node, "DropGun");
185 
186 	switch (wc->Type)
187 	{
188 	case GUNTYPE_NORMAL:
189 	case GUNTYPE_GRENADE: // fallthrough
190 	{
191 		tmp = NULL;
192 		LoadStr(&tmp, node, "Pic");
193 		if (tmp != NULL)
194 		{
195 			CFREE(wc->u.Normal.Sprites);
196 			wc->u.Normal.Sprites = NULL;
197 			if (strlen(tmp) > 0)
198 			{
199 				char buf[CDOGS_PATH_MAX];
200 				sprintf(buf, "chars/guns/%s", tmp);
201 				CSTRDUP(wc->u.Normal.Sprites, buf);
202 			}
203 			CFREE(tmp);
204 		}
205 
206 		LoadInt(&wc->u.Normal.Grips, node, "Grips");
207 
208 		CArrayInit(&wc->u.Normal.Bullets, sizeof(const BulletClass *));
209 		tmp = NULL;
210 		LoadStr(&tmp, node, "Bullet");
211 		if (tmp != NULL)
212 		{
213 			const BulletClass *bullet = StrBulletClass(tmp);
214 			CArrayPushBack(&wc->u.Normal.Bullets, &bullet);
215 			CFREE(tmp);
216 		}
217 		const json_t *bulletsNode = json_find_first_label(node, "Bullets");
218 		if (bulletsNode)
219 		{
220 			for (const json_t *bulletNode = bulletsNode->child->child;
221 				 bulletNode; bulletNode = bulletNode->next)
222 			{
223 				const BulletClass *bullet = StrBulletClass(bulletNode->text);
224 				CArrayPushBack(&wc->u.Normal.Bullets, &bullet);
225 			}
226 		}
227 
228 		tmp = NULL;
229 		LoadStr(&tmp, node, "Ammo");
230 		if (tmp != NULL)
231 		{
232 			wc->u.Normal.AmmoId = StrAmmoId(tmp);
233 			if (wc->Type == GUNTYPE_GRENADE)
234 			{
235 				// Grenade weapons also allow the ammo pickups to act as gun
236 				// pickups
237 				Ammo *ammo = AmmoGetById(&gAmmo, wc->u.Normal.AmmoId);
238 				CFREE(ammo->DefaultGun);
239 				CSTRDUP(ammo->DefaultGun, wc->name);
240 				// Replace icon with that of the ammo
241 				wc->Icon = CPicGetPic(&ammo->Pic, 0);
242 			}
243 			CFREE(tmp);
244 		}
245 
246 		LoadInt(&wc->u.Normal.Cost, node, "Cost");
247 
248 		LoadInt(&wc->u.Normal.ReloadLead, node, "ReloadLead");
249 
250 		LoadSoundFromNode(&wc->u.Normal.Sound, node, "Sound");
251 		LoadSoundFromNode(&wc->u.Normal.ReloadSound, node, "ReloadSound");
252 
253 		wc->u.Normal.SoundLockLength = wc->Lock;
254 		LoadInt(&wc->u.Normal.SoundLockLength, node, "SoundLockLength");
255 
256 		LoadFloat(&wc->u.Normal.Recoil, node, "Recoil");
257 
258 		LoadInt(&wc->u.Normal.Spread.Count, node, "SpreadCount");
259 		LoadFloat(&wc->u.Normal.Spread.Width, node, "SpreadWidth");
260 		LoadFloat(&wc->u.Normal.AngleOffset, node, "AngleOffset");
261 
262 		int muzzleHeight = 0;
263 		LoadInt(&muzzleHeight, node, "MuzzleHeight");
264 		if (muzzleHeight)
265 		{
266 			wc->u.Normal.MuzzleHeight = muzzleHeight * Z_FACTOR;
267 		}
268 		if (json_find_first_label(node, "Elevation"))
269 		{
270 			LoadInt(&wc->u.Normal.ElevationLow, node, "Elevation");
271 			wc->u.Normal.ElevationHigh = wc->u.Normal.ElevationLow;
272 		}
273 		LoadInt(&wc->u.Normal.ElevationLow, node, "ElevationLow");
274 		LoadInt(&wc->u.Normal.ElevationHigh, node, "ElevationHigh");
275 		wc->u.Normal.ElevationLow =
276 			MIN(wc->u.Normal.ElevationLow, wc->u.Normal.ElevationHigh);
277 		wc->u.Normal.ElevationHigh =
278 			MAX(wc->u.Normal.ElevationLow, wc->u.Normal.ElevationHigh);
279 		tmp = NULL;
280 		LoadStr(&tmp, node, "MuzzleFlashParticle");
281 		if (tmp != NULL)
282 		{
283 			wc->u.Normal.MuzzleFlash =
284 				StrParticleClass(&gParticleClasses, tmp);
285 			CFREE(tmp);
286 		}
287 
288 		tmp = NULL;
289 		LoadStr(&tmp, node, "Brass");
290 		if (tmp != NULL)
291 		{
292 			wc->u.Normal.Brass = StrParticleClass(&gParticleClasses, tmp);
293 			CFREE(tmp);
294 		}
295 
296 		LoadBool(&wc->u.Normal.CanShoot, node, "CanShoot");
297 
298 		if (version < 3)
299 		{
300 			LoadInt(&wc->u.Normal.Shake.Amount, node, "ShakeAmount");
301 		}
302 		else if (json_find_first_label(node, "Shake"))
303 		{
304 			json_t *shake = json_find_first_label(node, "Shake")->child;
305 			LoadInt(&wc->u.Normal.Shake.Amount, shake, "Amount");
306 			LoadBool(
307 				&wc->u.Normal.Shake.CameraSubjectOnly, shake,
308 				"CameraSubjectOnly");
309 		}
310 	}
311 	break;
312 	case GUNTYPE_MULTI: {
313 		int i = 0;
314 		for (const json_t *gunNode = gunsNode->child->child; gunNode;
315 			 gunNode = gunNode->next, i++)
316 		{
317 			wc->u.Guns[i] = json_unescape(gunNode->text);
318 		}
319 	}
320 	break;
321 	default:
322 		CASSERT(false, "unknown gun type");
323 		break;
324 	}
325 
326 	wc->IsRealGun = true;
327 
328 	if (version < 2)
329 	{
330 		CASSERT(wc->Type == GUNTYPE_NORMAL, "unexpected gun type");
331 		if (!wc->u.Normal.CanShoot)
332 		{
333 			wc->Lock = 0;
334 		}
335 	}
336 
337 	LOG(LM_MAP, LL_DEBUG, "loaded %s name(%s) lock(%d)...",
338 		GunTypeStr(wc->Type), wc->name, wc->Lock);
339 	LOG(LM_MAP, LL_DEBUG, "...canDrop(%s)", wc->CanDrop ? "true" : "false");
340 	switch (wc->Type)
341 	{
342 	case GUNTYPE_NORMAL:
343 	case GUNTYPE_GRENADE: // fallthrough
344 		LOG(LM_MAP, LL_DEBUG, "bullets(");
345 		CA_FOREACH(const BulletClass *, bc, wc->u.Normal.Bullets)
346 		if (_ca_index > 0)
347 		{
348 			LOG(LM_MAP, LL_DEBUG, ", ");
349 		}
350 		LOG(LM_MAP, LL_DEBUG, "%s", (*bc)->Name);
351 		CA_FOREACH_END()
352 		LOG(LM_MAP, LL_DEBUG, ") ammo(%d) cost(%d)...", wc->u.Normal.AmmoId,
353 			wc->u.Normal.Cost);
354 		LOG(LM_MAP, LL_DEBUG,
355 			"...reloadLead(%d) soundLockLength(%d) recoil(%f)...",
356 			wc->u.Normal.ReloadLead, wc->u.Normal.SoundLockLength,
357 			wc->u.Normal.Recoil);
358 		LOG(LM_MAP, LL_DEBUG,
359 			"...spread(%frad x%d) angleOffset(%f) muzzleHeight(%d)...",
360 			wc->u.Normal.Spread.Width, wc->u.Normal.Spread.Count,
361 			wc->u.Normal.AngleOffset, wc->u.Normal.MuzzleHeight);
362 		LOG(LM_MAP, LL_DEBUG,
363 			"...elevation(%d-%d) muzzleFlash(%s) brass(%s) canShoot(%s)...",
364 			wc->u.Normal.ElevationLow, wc->u.Normal.ElevationHigh,
365 			wc->u.Normal.MuzzleFlash != NULL ? wc->u.Normal.MuzzleFlash->Name
366 											 : "",
367 			wc->u.Normal.Brass != NULL ? wc->u.Normal.Brass->Name : "",
368 			wc->u.Normal.CanShoot ? "true" : "false");
369 		LOG(LM_MAP, LL_DEBUG, "...shake{amount(%d), cameraSubjectOnly(%s)}",
370 			wc->u.Normal.Shake.Amount,
371 			wc->u.Normal.Shake.CameraSubjectOnly ? "true" : "false");
372 		break;
373 	case GUNTYPE_MULTI:
374 		LOG(LM_MAP, LL_DEBUG, "...guns{%s, %s}",
375 			wc->u.Guns[0] ? wc->u.Guns[0] : "",
376 			wc->u.Guns[1] ? wc->u.Guns[1] : "");
377 		break;
378 	default:
379 		CASSERT(false, "unknown gun type");
380 		break;
381 	}
382 }
WeaponClassesTerminate(WeaponClasses * wcs)383 void WeaponClassesTerminate(WeaponClasses *wcs)
384 {
385 	WeaponClassesClear(&wcs->Guns);
386 	CArrayTerminate(&wcs->Guns);
387 	WeaponClassesClear(&wcs->CustomGuns);
388 	CArrayTerminate(&wcs->CustomGuns);
389 }
WeaponClassesClear(CArray * classes)390 void WeaponClassesClear(CArray *classes)
391 {
392 	CA_FOREACH(WeaponClass, g, *classes)
393 	WeaponClassTerminate(g);
394 	CA_FOREACH_END()
395 	CArrayClear(classes);
396 }
WeaponClassTerminate(WeaponClass * wc)397 static void WeaponClassTerminate(WeaponClass *wc)
398 {
399 	CFREE(wc->name);
400 	CFREE(wc->Description);
401 	CFREE(wc->DropGun);
402 	switch (wc->Type)
403 	{
404 	case GUNTYPE_NORMAL:
405 	case GUNTYPE_GRENADE: // fallthrough
406 		CFREE(wc->u.Normal.Sprites);
407 		CArrayTerminate(&wc->u.Normal.Bullets);
408 		break;
409 	case GUNTYPE_MULTI:
410 		for (int i = 0; i < MAX_BARRELS; i++)
411 		{
412 			CFREE(wc->u.Guns[i]);
413 		}
414 		break;
415 	default:
416 		CASSERT(false, "unknown gun type");
417 		break;
418 	}
419 	memset(wc, 0, sizeof *wc);
420 }
421 
422 // TODO: use map structure?
StrWeaponClass(const char * s)423 const WeaponClass *StrWeaponClass(const char *s)
424 {
425 	CA_FOREACH(const WeaponClass, gd, gWeaponClasses.CustomGuns)
426 	if (strcmp(s, gd->name) == 0)
427 	{
428 		return gd;
429 	}
430 	CA_FOREACH_END()
431 	CA_FOREACH(const WeaponClass, gd, gWeaponClasses.Guns)
432 	if (strcmp(s, gd->name) == 0)
433 	{
434 		return gd;
435 	}
436 	CA_FOREACH_END()
437 	return NULL;
438 }
IdWeaponClass(const int i)439 WeaponClass *IdWeaponClass(const int i)
440 {
441 	CASSERT(
442 		i >= 0 && i < (int)gWeaponClasses.Guns.size +
443 						  (int)gWeaponClasses.CustomGuns.size,
444 		"Gun index out of bounds");
445 	if (i < (int)gWeaponClasses.Guns.size)
446 	{
447 		return CArrayGet(&gWeaponClasses.Guns, i);
448 	}
449 	return CArrayGet(&gWeaponClasses.CustomGuns, i - gWeaponClasses.Guns.size);
450 }
WeaponClassId(const WeaponClass * wc)451 int WeaponClassId(const WeaponClass *wc)
452 {
453 	int idx = 0;
454 	for (int i = 0; i < (int)gWeaponClasses.Guns.size; i++, idx++)
455 	{
456 		const WeaponClass *wc2 = CArrayGet(&gWeaponClasses.Guns, i);
457 		if (wc2 == wc)
458 		{
459 			return idx;
460 		}
461 	}
462 	for (int i = 0; i < (int)gWeaponClasses.CustomGuns.size; i++, idx++)
463 	{
464 		const WeaponClass *wc2 = CArrayGet(&gWeaponClasses.CustomGuns, i);
465 		if (wc2 == wc)
466 		{
467 			return idx;
468 		}
469 	}
470 	CASSERT(false, "cannot find gun");
471 	return -1;
472 }
473 
WeaponClassFire(const WeaponClass * wc,const struct vec2 pos,const float z,const double radians,const int flags,const int actorUID,const bool playSound,const bool isGun)474 void WeaponClassFire(
475 	const WeaponClass *wc, const struct vec2 pos, const float z,
476 	const double radians, const int flags, const int actorUID,
477 	const bool playSound, const bool isGun)
478 {
479 	CASSERT(wc->Type != GUNTYPE_MULTI, "unexpected gun type");
480 	GameEvent e = GameEventNew(GAME_EVENT_GUN_FIRE);
481 	e.u.GunFire.ActorUID = actorUID;
482 	strcpy(e.u.GunFire.Gun, wc->name);
483 	e.u.GunFire.MuzzlePos = Vec2ToNet(pos);
484 	// TODO: GunFire Z to float
485 	e.u.GunFire.Z = (int)z;
486 	e.u.GunFire.Angle = (float)radians;
487 	e.u.GunFire.Sound = playSound;
488 	e.u.GunFire.Flags = flags;
489 	e.u.GunFire.IsGun = isGun;
490 	GameEventsEnqueue(&gGameEvents, e);
491 }
492 
WeaponClassAddBrass(const WeaponClass * wc,const direction_e d,const struct vec2 pos)493 void WeaponClassAddBrass(
494 	const WeaponClass *wc, const direction_e d, const struct vec2 pos)
495 {
496 	// Check configuration
497 	if (!ConfigGetBool(&gConfig, "Graphics.Brass"))
498 	{
499 		return;
500 	}
501 	CASSERT(wc->Type == GUNTYPE_NORMAL, "gun type can't have brass");
502 	CASSERT(
503 		wc->u.Normal.Brass != NULL, "Cannot create brass for no-brass weapon");
504 	GameEvent e = GameEventNew(GAME_EVENT_ADD_PARTICLE);
505 	e.u.AddParticle.Class = wc->u.Normal.Brass;
506 	const float radians = dir2radians[d];
507 	const struct vec2 ejectionPortOffset =
508 		svec2_scale(Vec2FromRadiansScaled(radians), 7);
509 	e.u.AddParticle.Pos = svec2_subtract(pos, ejectionPortOffset);
510 	e.u.AddParticle.Z = (float)wc->u.Normal.MuzzleHeight;
511 	e.u.AddParticle.Vel =
512 		svec2_scale(Vec2FromRadians(radians + MPI_2), 0.333333f);
513 	e.u.AddParticle.Vel.x += RAND_FLOAT(-0.25f, 0.25f);
514 	e.u.AddParticle.Vel.y += RAND_FLOAT(-0.25f, 0.25f);
515 	e.u.AddParticle.Angle = RAND_DOUBLE(0, MPI * 2);
516 	e.u.AddParticle.DZ = (float)((rand() % 6) + 6);
517 	e.u.AddParticle.Spin = RAND_DOUBLE(-0.1, 0.1);
518 	GameEventsEnqueue(&gGameEvents, e);
519 }
520 
521 static struct vec2 GetMuzzleOffset(
522 	const direction_e d, const gunstate_e state);
WeaponClassGetBarrelMuzzleOffset(const WeaponClass * wc,const CharSprites * cs,const int barrel,direction_e dir,const gunstate_e state)523 struct vec2 WeaponClassGetBarrelMuzzleOffset(
524 	const WeaponClass *wc, const CharSprites *cs, const int barrel,
525 	direction_e dir, const gunstate_e state)
526 {
527 	if (wc->Type == GUNTYPE_MULTI)
528 	{
529 		wc = StrWeaponClass(wc->u.Guns[barrel]);
530 		CASSERT(wc != NULL, "cannot find gun");
531 	}
532 	else
533 	{
534 		CASSERT(barrel == 0, "unexpected barrel index");
535 	}
536 	if (!WeaponClassHasMuzzle(wc))
537 	{
538 		return svec2_zero();
539 	}
540 	CASSERT(wc->u.Normal.Sprites != NULL, "Gun has no pic");
541 	CASSERT(barrel < 2, "up to two barrels supported");
542 	// For the other barrel, mirror dir and offset along X axis
543 	if (barrel == 1)
544 	{
545 		dir = DirectionMirrorX(dir);
546 	}
547 	const struct vec2 gunOffset =
548 		cs->Offsets.Dir[barrel == 0 ? BODY_PART_GUN_R : BODY_PART_GUN_L][dir];
549 	const struct vec2 offset =
550 		svec2_add(gunOffset, GetMuzzleOffset(dir, state));
551 	if (barrel == 1)
552 	{
553 		return svec2(-offset.x, offset.y);
554 	}
555 	return offset;
556 }
GetMuzzleOffset(const direction_e d,const gunstate_e state)557 static struct vec2 GetMuzzleOffset(const direction_e d, const gunstate_e state)
558 {
559 // TODO: gun-specific muzzle offsets
560 #define BARREL_LENGTH 10
561 #define BARREL_LENGTH_READY 7
562 	// Barrel slightly shortened when not firing
563 	const double barrelLength =
564 		state == GUNSTATE_FIRING ? BARREL_LENGTH : BARREL_LENGTH_READY;
565 	return svec2_scale(Vec2FromRadians(dir2radians[d]), (float)barrelLength);
566 }
WeaponClassGetMuzzleHeight(const WeaponClass * wc,const gunstate_e state,const int barrel)567 float WeaponClassGetMuzzleHeight(
568 	const WeaponClass *wc, const gunstate_e state, const int barrel)
569 {
570 	// Muzzle slightly higher when not firing
571 	// TODO: convert MuzzleHeight to float
572 	const int muzzleHeight = WC_BARREL_ATTR(*wc, MuzzleHeight, barrel);
573 	return (
574 		float)(muzzleHeight + (state == GUNSTATE_FIRING ? 0 : 4 * Z_FACTOR));
575 }
576 
WeaponClassHasMuzzle(const WeaponClass * wc)577 bool WeaponClassHasMuzzle(const WeaponClass *wc)
578 {
579 	return wc->Type == GUNTYPE_NORMAL && wc->u.Normal.Sprites != NULL &&
580 		   wc->u.Normal.CanShoot;
581 }
WeaponClassIsHighDPS(const WeaponClass * wc)582 bool WeaponClassIsHighDPS(const WeaponClass *wc)
583 {
584 	if (wc->Type == GUNTYPE_MULTI)
585 	{
586 		return WeaponClassIsHighDPS(StrWeaponClass(wc->u.Guns[0])) ||
587 			   WeaponClassIsHighDPS(StrWeaponClass(wc->u.Guns[1]));
588 	}
589 	CA_FOREACH(const BulletClass *, bc, wc->u.Normal.Bullets)
590 	// TODO: generalised way of determining explosive bullets
591 	if ((*bc)->Falling.DropGuns.size > 0 || (*bc)->OutOfRangeGuns.size > 0 ||
592 		(*bc)->HitGuns.size > 0)
593 	{
594 		return true;
595 	}
596 	CA_FOREACH_END()
597 	return false;
598 }
WeaponClassGetRange(const WeaponClass * wc)599 float WeaponClassGetRange(const WeaponClass *wc)
600 {
601 	if (wc->Type == GUNTYPE_MULTI)
602 	{
603 		return (WeaponClassGetRange(StrWeaponClass(wc->u.Guns[0])) +
604 				WeaponClassGetRange(StrWeaponClass(wc->u.Guns[1]))) /
605 			   2.0f;
606 	}
607 	float maxRange = 0;
608 	CA_FOREACH(const BulletClass *, bc, wc->u.Normal.Bullets)
609 	const float speed = ((*bc)->SpeedLow + (*bc)->SpeedHigh) / 2;
610 	const int range = ((*bc)->RangeLow + (*bc)->RangeHigh) / 2;
611 	float effectiveRange = speed * range;
612 	if ((*bc)->Falling.GravityFactor != 0 && (*bc)->Falling.DestroyOnDrop)
613 	{
614 		// Halve effective range
615 		// TODO: this assumes a certain bouncing range
616 		effectiveRange *= 0.5f;
617 	}
618 	maxRange = MAX(maxRange, effectiveRange);
619 	CA_FOREACH_END()
620 	return maxRange;
621 }
WeaponClassIsLongRange(const WeaponClass * wc)622 bool WeaponClassIsLongRange(const WeaponClass *wc)
623 {
624 	return WeaponClassGetRange(wc) > 130;
625 }
WeaponClassIsShortRange(const WeaponClass * wc)626 bool WeaponClassIsShortRange(const WeaponClass *wc)
627 {
628 	return WeaponClassGetRange(wc) < 100;
629 }
WeaponClassCanShoot(const WeaponClass * wc)630 bool WeaponClassCanShoot(const WeaponClass *wc)
631 {
632 	if (wc->Type == GUNTYPE_MULTI)
633 	{
634 		return WeaponClassCanShoot(WeaponClassGetBarrel(wc, 0)) ||
635 			   WeaponClassCanShoot(WeaponClassGetBarrel(wc, 1));
636 	}
637 	return wc->u.Normal.CanShoot;
638 }
WeaponClassNumBarrels(const WeaponClass * wc)639 int WeaponClassNumBarrels(const WeaponClass *wc)
640 {
641 	return wc->Type == GUNTYPE_MULTI ? 2 : 1;
642 }
WeaponClassGetBarrel(const WeaponClass * wc,const int barrel)643 const WeaponClass *WeaponClassGetBarrel(
644 	const WeaponClass *wc, const int barrel)
645 {
646 	if (wc->Type == GUNTYPE_MULTI)
647 	{
648 		return StrWeaponClass(wc->u.Guns[barrel]);
649 	}
650 	return wc;
651 }
WeaponClassGetBullet(const WeaponClass * wc,const int barrel)652 const BulletClass *WeaponClassGetBullet(
653 	const WeaponClass *wc, const int barrel)
654 {
655 	if (barrel == -1)
656 	{
657 		return NULL;
658 	}
659 	wc = WeaponClassGetBarrel(wc, barrel);
660 	CA_FOREACH(const BulletClass *, bc, wc->u.Normal.Bullets)
661 	if (*bc)
662 		return *bc;
663 	CA_FOREACH_END()
664 	return NULL;
665 }
666 
BulletAndWeaponInitialize(BulletClasses * b,WeaponClasses * wcs,const char * bpath,const char * gpath)667 void BulletAndWeaponInitialize(
668 	BulletClasses *b, WeaponClasses *wcs, const char *bpath, const char *gpath)
669 {
670 	BulletInitialize(b);
671 
672 	FILE *bf = NULL;
673 	FILE *gf = NULL;
674 	json_t *broot = NULL;
675 	json_t *groot = NULL;
676 	enum json_error e;
677 
678 	// 2-pass bullet loading will free root for us
679 	bool freeBRoot = true;
680 	char buf[CDOGS_PATH_MAX];
681 	GetDataFilePath(buf, bpath);
682 	bf = fopen(buf, "r");
683 	if (bf == NULL)
684 	{
685 		LOG(LM_MAP, LL_ERROR, "Error: cannot load bullets file %s", buf);
686 		goto bail;
687 	}
688 	e = json_stream_parse(bf, &broot);
689 	if (e != JSON_OK)
690 	{
691 		LOG(LM_MAP, LL_ERROR, "Error parsing bullets file %s [error %d]", buf,
692 			(int)e);
693 		goto bail;
694 	}
695 	BulletLoadJSON(b, &b->Classes, broot);
696 
697 	WeaponClassesInitialize(wcs);
698 	GetDataFilePath(buf, gpath);
699 	gf = fopen(buf, "r");
700 	if (gf == NULL)
701 	{
702 		LOG(LM_MAP, LL_ERROR, "Error: cannot load guns file %s", buf);
703 		goto bail;
704 	}
705 	e = json_stream_parse(gf, &groot);
706 	if (e != JSON_OK)
707 	{
708 		LOG(LM_MAP, LL_ERROR, "Error parsing guns file %s [error %d]", buf,
709 			(int)e);
710 		goto bail;
711 	}
712 	WeaponClassesLoadJSON(wcs, &wcs->Guns, groot);
713 
714 	BulletLoadWeapons(b);
715 	freeBRoot = false;
716 
717 bail:
718 	if (bf)
719 	{
720 		fclose(bf);
721 	}
722 	if (gf)
723 	{
724 		fclose(gf);
725 	}
726 	if (freeBRoot)
727 	{
728 		json_free_value(&broot);
729 	}
730 	json_free_value(&groot);
731 }
732