1 #include <math.h> // floorf
2 
3 #include "Directories.h"
4 #include "Font_Control.h"
5 #include "Handle_Items.h"
6 #include "Overhead_Types.h"
7 #include "Soldier_Control.h"
8 #include "Overhead.h"
9 #include "Event_Pump.h"
10 #include "Structure.h"
11 #include "TileDef.h"
12 #include "Timer_Control.h"
13 #include "Weapons.h"
14 #include "Animation_Control.h"
15 #include "Handle_UI.h"
16 #include "Isometric_Utils.h"
17 #include "WorldMan.h"
18 #include "Points.h"
19 #include "AI.h"
20 #include "LOS.h"
21 #include "RenderWorld.h"
22 #include "OppList.h"
23 #include "Interface.h"
24 #include "Message.h"
25 #include "Campaign.h"
26 #include "Items.h"
27 #include "Text.h"
28 #include "Soldier_Profile.h"
29 #include "Tile_Animation.h"
30 #include "Dialogue_Control.h"
31 #include "SkillCheck.h"
32 #include "Explosion_Control.h"
33 #include "Quests.h"
34 #include "Physics.h"
35 #include "Random.h"
36 #include "Vehicles.h"
37 #include "Bullets.h"
38 #include "Morale.h"
39 #include "Meanwhile.h"
40 #include "GameSettings.h"
41 #include "SaveLoadMap.h"
42 #include "Soldier_Macros.h"
43 #include "SmokeEffects.h"
44 #include "Auto_Resolve.h"
45 #include "SoundMan.h"
46 #include "MemMan.h"
47 #include "Debug.h"
48 
49 #include "CalibreModel.h"
50 #include "ContentManager.h"
51 #include "GameInstance.h"
52 #include "WeaponModels.h"
53 #include "Logger.h"
54 #include "policy/GamePolicy.h"
55 
56 // NB this is arbitrary, chances in DG ranged from 1 in 6 to 1 in 20
57 #define BASIC_DEPRECIATE_CHANCE	15
58 
59 #define NORMAL_RANGE		90 // # world units considered an 'avg' shot
60 #define MIN_SCOPE_RANGE	60 // # world units after which scope's useful
61 
62 #define MIN_TANK_RANGE		120 // range at which tank starts really having trouble aiming
63 
64 // percent reduction in sight range per point of aiming
65 #define SNIPERSCOPE_AIM_BONUS	20
66 // bonus to hit with working laser scope
67 #define LASERSCOPE_BONUS	20
68 
69 #define CRITICAL_HIT_THRESHOLD	30
70 
71 #define HTH_MODE_PUNCH		1
72 #define HTH_MODE_STAB		2
73 #define HTH_MODE_STEAL		3
74 
75 // JA2 GOLD: for weapons and attachments, give penalties only for status values below 85
76 #define WEAPON_STATUS_MOD( x )	( (x) >= 85 ? 100 : (((x) * 100) / 85) )
77 
78 BOOLEAN gfNextFireJam      = FALSE;
79 BOOLEAN gfNextShotKills    = FALSE;
80 BOOLEAN gfReportHitChances = FALSE;
81 
82 //GLOBALS
83 
84 // TODO: Move strings to extern file
85 
86 ARMOURTYPE const Armour[] =
87 {
88 	//Class,		Protection,	Degradation%	Description
89 	//---------------	----------	------------	----------------
90 	{ARMOURCLASS_VEST,	10,		25}, /* Flak jacket     */
91 	{ARMOURCLASS_VEST,	13,		20}, /* Flak jacket w X */
92 	{ARMOURCLASS_VEST,	16,		15}, /* Flak jacket w Y */
93 	{ARMOURCLASS_VEST,	15,		20}, /* Kevlar jacket   */
94 	{ARMOURCLASS_VEST,	19,		15}, /* Kevlar jack w X */
95 	{ARMOURCLASS_VEST,	24,		10}, /* Kevlar jack w Y */
96 	{ARMOURCLASS_VEST,	30,		15}, /* Spectra jacket  */
97 	{ARMOURCLASS_VEST,	36,		10}, /* Spectra jack w X*/
98 	{ARMOURCLASS_VEST,	42,		5}, /* Spectra jack w Y*/
99 	{ARMOURCLASS_LEGGINGS,	15,		20}, /* Kevlar leggings */
100 	{ARMOURCLASS_LEGGINGS,	19,		15}, /* Kevlar legs w X */
101 
102 	{ARMOURCLASS_LEGGINGS,	24,		10}, /* Kevlar legs w Y */
103 	{ARMOURCLASS_LEGGINGS,	30,		15}, /* Spectra leggings*/
104 	{ARMOURCLASS_LEGGINGS,	36,		10}, /* Spectra legs w X*/
105 	{ARMOURCLASS_LEGGINGS,	42,		5}, /* Spectra legs w Y*/
106 	{ARMOURCLASS_HELMET,	10,		5}, /* Steel helmet    */
107 	{ARMOURCLASS_HELMET,	15,		20}, /* Kevlar helmet   */
108 	{ARMOURCLASS_HELMET,	19,		15}, /* Kevlar helm w X */
109 	{ARMOURCLASS_HELMET,	24,		10}, /* Kevlar helm w Y */
110 	{ARMOURCLASS_HELMET,	30,		15}, /* Spectra helmet  */
111 	{ARMOURCLASS_HELMET,	36,		10}, /* Spectra helm w X*/
112 
113 	{ARMOURCLASS_HELMET,	42,		5}, /* Spectra helm w Y*/
114 	{ARMOURCLASS_PLATE,	15,		200}, /* Ceramic plates  */
115 	{ARMOURCLASS_MONST,	3,		0}, /* Infant creature hide */
116 	{ARMOURCLASS_MONST,	5,		0}, /* Young male creature hide  */
117 	{ARMOURCLASS_MONST,	6,		0}, /* Male creature hide  */
118 	{ARMOURCLASS_MONST,	20,		0}, /* Queen creature hide  */
119 	{ARMOURCLASS_VEST,	2,		25}, /* Leather jacket    */
120 	{ARMOURCLASS_VEST,	12,		30}, /* Leather jacket w kevlar */
121 	{ARMOURCLASS_VEST,	16,		25}, /* Leather jacket w kevlar & compound 18 */
122 	{ARMOURCLASS_VEST,	19,		20}, /* Leather jacket w kevlar & queen blood */
123 
124 	{ARMOURCLASS_MONST,	7,		0}, /* Young female creature hide */
125 	{ARMOURCLASS_MONST,	8,		0}, /* Old female creature hide  */
126 	{ARMOURCLASS_VEST,	1,		25}, /* T-shirt */
127 	{ARMOURCLASS_VEST,	22,		20}, /* Kevlar 2 jacket   */
128 	{ARMOURCLASS_VEST,	27,		15}, /* Kevlar 2 jack w X */
129 	{ARMOURCLASS_VEST,	32,		10}, /* Kevlar 2 jack w Y */
130 };
131 
132 EXPLOSIVETYPE const Explosive[] =
133 {
134 	//Type,		Yield,	Yield2,	Radius,	Volume,	Volatility,	Animation,	Description
135 	//-----		-----	------	------	------	--------- 	------------------
136 	{EXPLOSV_STUN,		1,	70,	4,	0,	0,	STUN_BLAST}, /* stun grenade */
137 	{EXPLOSV_TEARGAS,	0,	20,	4,	0,	0,	TARGAS_EXP}, /* tear gas grenade */
138 	{EXPLOSV_MUSTGAS,	15,	40,	4,	0,	0,	MUSTARD_EXP}, /* mustard gas grenade*/
139 	{EXPLOSV_NORMAL,	15,	7,	3,	15,	1,	BLAST_1}, /* mini hand grenade */
140 	{EXPLOSV_NORMAL,	25,	10,	4,	25,	1,	BLAST_1}, /* reg hand grenade */
141 	{EXPLOSV_NORMAL,	40,	12,	5,	20,	10,	BLAST_2}, /* RDX */
142 	{EXPLOSV_NORMAL,	50,	15,	5,	50,	2,	BLAST_2}, /* TNT (="explosives")*/
143 	{EXPLOSV_NORMAL,	60,	15,	6,	60,	2,	BLAST_2}, /* HMX (=RDX+TNT) */
144 	{EXPLOSV_NORMAL,	55,	15,	6,	55,	0,	BLAST_2}, /* C1 (=RDX+min oil) */
145 	{EXPLOSV_NORMAL,	50,	22,	6,	50,	2,	BLAST_2}, /* mortar shell */
146 
147 	{EXPLOSV_NORMAL,	30,	30,	2,	30,	2,	BLAST_1}, /* mine */
148 	{EXPLOSV_NORMAL,	65,	30,	7,	65,	0,	BLAST_1}, /* C4 ("plastique") */
149 	{EXPLOSV_FLARE,		0,	0,	10,	0,	0,	BLAST_1}, /* trip flare */
150 	{EXPLOSV_NOISE,		0,	0,	50,	50,	0,	BLAST_1}, /* trip klaxon */
151 	{EXPLOSV_NORMAL,	20,	0,	1,	20,	0,	BLAST_1}, /* shaped charge */
152 	{EXPLOSV_FLARE,		0,	0,	10,	0,	0,	BLAST_1,}, /* break light */
153 	{EXPLOSV_NORMAL,	25,	5,	4,	25,	1,	BLAST_1,}, /* GL grenade */
154 	{EXPLOSV_TEARGAS,	0,	20,	3,	0,	0,	TARGAS_EXP,}, /* GL tear gas grenade*/
155 	{EXPLOSV_STUN,		1,	50,	4,	0,	0,	STUN_BLAST,}, /* GL stun grenade */
156 	{EXPLOSV_SMOKE,		0,	0,	3,	0,	0,	SMOKE_EXP,}, /* GL smoke grenade */
157 
158 	{EXPLOSV_SMOKE,		0,	0,	4,	0,	0,	SMOKE_EXP,}, /* smoke grenade */
159 	{EXPLOSV_NORMAL,	60,	20,	6,	60,	2,	BLAST_2,}, /* Tank Shell */
160 	{EXPLOSV_NORMAL,	100,	0,	0,	0,	0,	BLAST_1,}, /* Fake structure igniter */
161 	{EXPLOSV_NORMAL,	100,	0,	1,	0,	0,	BLAST_1,}, /* creature cocktail */
162 	{EXPLOSV_NORMAL,	50,	10,	5,	50,	2,	BLAST_2,}, /* fake struct explosion*/
163 	{EXPLOSV_NORMAL,	50,	10,	5,	50,	2,	BLAST_3,}, /* fake vehicle explosion*/
164 	{EXPLOSV_TEARGAS,	0,	40,	4,	0,	0,	TARGAS_EXP}, /* big tear gas */
165 	{EXPLOSV_CREATUREGAS,	5,	0,	1,	0,	0,	NO_BLAST}, /* small creature gas*/
166 	{EXPLOSV_CREATUREGAS,	8,	0,	3,	0,	0,	NO_BLAST}, /* big creature gas*/
167 	{EXPLOSV_CREATUREGAS,	0,	0,	0,	0,	0,	NO_BLAST}, /* vry sm creature gas*/
168 };
169 
170 
171 // the amount of momentum reduction for the head, torso, and legs
172 // used to determine whether the bullet will go through someone
173 static const UINT8 BodyImpactReduction[4] = { 0, 15, 30, 23 };
174 
175 
GunRange(OBJECTTYPE const & o)176 UINT16 GunRange(OBJECTTYPE const& o)
177 {
178 	// return a minimal value of 1 tile
179 	if (!(GCM->getItem(o.usItem)->isWeapon())) return CELL_X_SIZE;
180 
181 	INT8   const attach_pos = FindAttachment(&o, GUN_BARREL_EXTENDER);
182 	UINT16       range      = GCM->getWeapon(o.usItem)->usRange;
183 	if (attach_pos != ITEM_NOT_FOUND)
184 	{
185 		range += GUN_BARREL_RANGE_BONUS * WEAPON_STATUS_MOD(o.bAttachStatus[attach_pos]) / 100;
186 	}
187 	return range;
188 }
189 
190 
EffectiveArmour(OBJECTTYPE const * const o)191 INT8 EffectiveArmour(OBJECTTYPE const* const o)
192 {
193 	if (!o) return 0;
194 
195 	const ItemModel * item = GCM->getItem(o->usItem);
196 	if (item->getItemClass() != IC_ARMOUR) return 0;
197 
198 	INT32       armour_val = Armour[item->getClassIndex()].ubProtection * o->bStatus[0] / 100;
199 	INT8  const plate_pos  = FindAttachment(o, CERAMIC_PLATES);
200 	if (plate_pos != ITEM_NOT_FOUND)
201 	{
202 		armour_val += Armour[GCM->getItem(CERAMIC_PLATES)->getClassIndex()].ubProtection * o->bAttachStatus[plate_pos] / 100;
203 	}
204 	return armour_val;
205 }
206 
207 
ArmourPercent(const SOLDIERTYPE * pSoldier)208 INT8 ArmourPercent(const SOLDIERTYPE* pSoldier)
209 {
210 	INT32 iVest, iHelmet, iLeg;
211 
212 	if (pSoldier->inv[VESTPOS].usItem)
213 	{
214 		iVest = EffectiveArmour( &(pSoldier->inv[VESTPOS]) );
215 		// convert to % of best; ignoring bug-treated stuff
216 		iVest = 65 * iVest / ( Armour[ GCM->getItem(SPECTRA_VEST_18)->getClassIndex() ].ubProtection + Armour[ GCM->getItem(CERAMIC_PLATES)->getClassIndex() ].ubProtection );
217 	}
218 	else
219 	{
220 		iVest = 0;
221 	}
222 
223 	if (pSoldier->inv[HELMETPOS].usItem)
224 	{
225 		iHelmet = EffectiveArmour( &(pSoldier->inv[HELMETPOS]) );
226 		// convert to % of best; ignoring bug-treated stuff
227 		iHelmet = 15 * iHelmet / Armour[ GCM->getItem(SPECTRA_HELMET_18)->getClassIndex() ].ubProtection;
228 	}
229 	else
230 	{
231 		iHelmet = 0;
232 	}
233 
234 	if (pSoldier->inv[LEGPOS].usItem)
235 	{
236 		iLeg = EffectiveArmour( &(pSoldier->inv[LEGPOS]) );
237 		// convert to % of best; ignoring bug-treated stuff
238 		iLeg = 25 * iLeg / Armour[ GCM->getItem(SPECTRA_LEGGINGS_18)->getClassIndex() ].ubProtection;
239 	}
240 	else
241 	{
242 		iLeg = 0;
243 	}
244 	return( (INT8) (iHelmet + iVest + iLeg) );
245 }
246 
247 
ExplosiveEffectiveArmour(OBJECTTYPE * pObj)248 static INT8 ExplosiveEffectiveArmour(OBJECTTYPE* pObj)
249 {
250 	INT32 iValue;
251 	INT8  bPlate;
252 
253 	if (pObj == NULL || GCM->getItem(pObj->usItem)->getItemClass() != IC_ARMOUR)
254 	{
255 		return( 0 );
256 	}
257 	iValue = Armour[ GCM->getItem(pObj->usItem)->getClassIndex() ].ubProtection;
258 	iValue = iValue * pObj->bStatus[0] / 100;
259 	if ( pObj->usItem == FLAK_JACKET || pObj->usItem == FLAK_JACKET_18 || pObj->usItem == FLAK_JACKET_Y )
260 	{
261 		// increase value for flak jackets!
262 		iValue *= 3;
263 	}
264 
265 	bPlate = FindAttachment( pObj, CERAMIC_PLATES );
266 	if ( bPlate != ITEM_NOT_FOUND )
267 	{
268 		INT32 iValue2;
269 
270 		iValue2 = Armour[ GCM->getItem(CERAMIC_PLATES)->getClassIndex() ].ubProtection;
271 		iValue2 = iValue2 * pObj->bAttachStatus[ bPlate ] / 100;
272 
273 		iValue += iValue2;
274 	}
275 	return( (INT8) iValue );
276 }
277 
ArmourVersusExplosivesPercent(SOLDIERTYPE * pSoldier)278 INT8 ArmourVersusExplosivesPercent( SOLDIERTYPE * pSoldier )
279 {
280 	// returns the % damage reduction from grenades
281 	INT32 iVest, iHelmet, iLeg;
282 
283 	if (pSoldier->inv[VESTPOS].usItem)
284 	{
285 		iVest = ExplosiveEffectiveArmour( &(pSoldier->inv[VESTPOS]) );
286 		// convert to % of best; ignoring bug-treated stuff
287 		iVest = __min( 65, 65 * iVest / ( Armour[ GCM->getItem(SPECTRA_VEST_18)->getClassIndex() ].ubProtection + Armour[ GCM->getItem(CERAMIC_PLATES)->getClassIndex() ].ubProtection) );
288 	}
289 	else
290 	{
291 		iVest = 0;
292 	}
293 
294 	if (pSoldier->inv[HELMETPOS].usItem)
295 	{
296 		iHelmet = ExplosiveEffectiveArmour( &(pSoldier->inv[HELMETPOS]) );
297 		// convert to % of best; ignoring bug-treated stuff
298 		iHelmet = __min( 15, 15 * iHelmet / Armour[ GCM->getItem(SPECTRA_HELMET_18)->getClassIndex() ].ubProtection );
299 	}
300 	else
301 	{
302 		iHelmet = 0;
303 	}
304 
305 	if (pSoldier->inv[LEGPOS].usItem)
306 	{
307 		iLeg = ExplosiveEffectiveArmour( &(pSoldier->inv[LEGPOS]) );
308 		// convert to % of best; ignoring bug-treated stuff
309 		iLeg = __min( 25, 25 * iLeg / Armour[ GCM->getItem(SPECTRA_LEGGINGS_18)->getClassIndex() ].ubProtection );
310 	}
311 	else
312 	{
313 		iLeg = 0;
314 	}
315 	return( (INT8) (iHelmet + iVest + iLeg) );
316 }
317 
318 
AdjustImpactByHitLocation(INT32 iImpact,UINT8 ubHitLocation,INT32 * piNewImpact,INT32 * piImpactForCrits)319 static void AdjustImpactByHitLocation(INT32 iImpact, UINT8 ubHitLocation, INT32* piNewImpact, INT32* piImpactForCrits)
320 {
321 	switch( ubHitLocation )
322 	{
323 		case AIM_SHOT_HEAD:
324 		{
325 			// vanilla was: 1.5x damage from successful hits to the head!
326 			float critical_damage_to_head = gamepolicy(critical_damage_head_multiplier);
327 			float impactForCrits = floorf(critical_damage_to_head * (float)iImpact);
328 			*piImpactForCrits = (INT32)impactForCrits;
329 			*piNewImpact = *piImpactForCrits;
330 			break;
331 		}
332 		case AIM_SHOT_LEGS:
333 		{
334 			// for vanilla:
335 			// half damage for determining critical hits
336 			// quarter actual damage
337 			float critical_damage_to_legs = gamepolicy(critical_damage_legs_multiplier);
338 			float impactForCrits = floorf(critical_damage_to_legs * (float)iImpact);
339 			float newImpact = floorf(critical_damage_to_legs * impactForCrits);
340 			// NOTE: to calculate new impact, multiplying by crit damage multiplier twice might be overkill
341 			// might be better to halve the multiplier:
342 			// float newImpact = floor(0.5f * critical_damage_to_legs * (float)iImpact);
343 			*piImpactForCrits = (INT32)impactForCrits;
344 			*piNewImpact = (INT32)newImpact;
345 			break;
346 		}
347 		default:
348 			*piImpactForCrits = iImpact;
349 			*piNewImpact = iImpact;
350 			break;
351 	}
352 }
353 
354 
355 // #define TESTGUNJAM
356 
CheckForGunJam(SOLDIERTYPE * pSoldier)357 BOOLEAN CheckForGunJam( SOLDIERTYPE * pSoldier )
358 {
359 	OBJECTTYPE *pObj;
360 	INT32      iChance, iResult;
361 
362 	// should jams apply to enemies?
363 	if (pSoldier->uiStatusFlags & SOLDIER_PC)
364 	{
365 		if ( GCM->getItem(pSoldier->usAttackingWeapon)->getItemClass() == IC_GUN && !EXPLOSIVE_GUN( pSoldier->usAttackingWeapon ) )
366 		{
367 			pObj = &(pSoldier->inv[pSoldier->ubAttackingHand]);
368 				if (pObj->bGunAmmoStatus > 0)
369 			{
370 				// gun might jam, figure out the chance
371 				iChance = (80 - pObj->bGunStatus);
372 
373 				// CJC: removed reliability from formula...
374 
375 				// jams can happen to unreliable guns "earlier" than normal or reliable ones.
376 				//iChance = iChance - GCM->getItem(pObj->usItem)->getReliability() * 2;
377 
378 				// decrease the chance of a jam by 20% per point of reliability;
379 				// increased by 20% per negative point...
380 				//iChance = iChance * (10 - GCM->getItem(pObj->usItem)->getReliability() * 2) / 10;
381 
382 				if (pSoldier->bDoBurst > 1)
383 				{
384 					// if at bullet in a burst after the first, higher chance
385 					iChance -= PreRandom( 80 );
386 				}
387 				else
388 				{
389 					iChance -= PreRandom( 100 );
390 				}
391 
392 #ifdef TESTGUNJAM
393 				if ( 1 )
394 #else
395 				if ((INT32) PreRandom( 100 ) < iChance || gfNextFireJam )
396 #endif
397 				{
398 					gfNextFireJam = FALSE;
399 
400 					// jam! negate the gun ammo status.
401 					pObj->bGunAmmoStatus *= -1;
402 
403 					// Deduct AMMO!
404 					DeductAmmo( pSoldier, pSoldier->ubAttackingHand );
405 
406 					TacticalCharacterDialogue( pSoldier, QUOTE_JAMMED_GUN );
407 					return( TRUE );
408 				}
409 			}
410 			else if (pObj->bGunAmmoStatus < 0)
411 			{
412 				// try to unjam gun
413 				iResult = SkillCheck( pSoldier, UNJAM_GUN_CHECK, (INT8) (GCM->getItem(pObj->usItem)->getReliability() * 4) );
414 				if (iResult > 0)
415 				{
416 					// yay! unjammed the gun
417 					pObj->bGunAmmoStatus *= -1;
418 
419 					// MECHANICAL/DEXTERITY GAIN: Unjammed a gun
420 					StatChange(*pSoldier, MECHANAMT, 5, FROM_SUCCESS);
421 					StatChange(*pSoldier, DEXTAMT,   5, FROM_SUCCESS);
422 
423 					DirtyMercPanelInterface( pSoldier, DIRTYLEVEL2 );
424 
425 					// We unjammed gun, return appropriate value!
426 					return( 255 );
427 				}
428 				else
429 				{
430 					return( TRUE );
431 				}
432 			}
433 		}
434 	}
435 	return( FALSE );
436 }
437 
438 
OKFireWeapon(SOLDIERTYPE * pSoldier)439 BOOLEAN	OKFireWeapon( SOLDIERTYPE *pSoldier )
440 {
441 	BOOLEAN bGunJamVal;
442 
443 	// 1) Are we attacking with our second hand?
444 	if ( pSoldier->ubAttackingHand == SECONDHANDPOS )
445 	{
446 		if ( !EnoughAmmo( pSoldier, FALSE, pSoldier->ubAttackingHand ) )
447 		{
448 			if (pSoldier->bTeam == OUR_TEAM)
449 			{
450 				ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, g_langRes->Message[ STR_2ND_CLIP_DEPLETED ] );
451 				return( FALSE );
452 			}
453 
454 		}
455 	}
456 
457 	bGunJamVal = CheckForGunJam( pSoldier );
458 
459 	if ( bGunJamVal == 255 )
460 	{
461 		return( 255 );
462 	}
463 
464 	if ( bGunJamVal )
465 	{
466 		return( FALSE );
467 	}
468 
469 	return( TRUE );
470 }
471 
472 
473 static BOOLEAN UseGun(      SOLDIERTYPE* pSoldier, INT16 sTargetGridNo);
474 static void    UseBlade(    SOLDIERTYPE* pSoldier, INT16 sTargetGridNo);
475 static void    UseThrown(   SOLDIERTYPE* pSoldier, INT16 sTargetGridNo);
476 static BOOLEAN UseLauncher( SOLDIERTYPE* pSoldier, INT16 sTargetGridNo);
477 
478 
FireWeapon(SOLDIERTYPE * pSoldier,INT16 sTargetGridNo)479 BOOLEAN FireWeapon( SOLDIERTYPE *pSoldier , INT16 sTargetGridNo )
480 {
481 	// ignore passed in target gridno for now
482 
483 	// if target gridno is the same as ours, do not fire!
484 	if (sTargetGridNo == pSoldier->sGridNo)
485 	{
486 		// FREE UP NPC!
487 		SLOGD("Freeing up attacker - attack on own gridno!");
488 		FreeUpAttacker(pSoldier);
489 		return( FALSE );
490 	}
491 
492 	// SET ATTACKER TO NOBODY, WILL GET SET EVENTUALLY
493 	pSoldier->opponent = NULL;
494 
495 	switch( GCM->getItem(pSoldier->usAttackingWeapon)->getItemClass() )
496 	{
497 		case IC_THROWING_KNIFE:
498 		case IC_GUN:
499 
500 			// ATE: PAtch up - bookkeeping for spreading done out of whak
501 			if ( pSoldier->fDoSpread && !pSoldier->bDoBurst )
502 			{
503 				pSoldier->fDoSpread = FALSE;
504 			}
505 
506 			if ( pSoldier->fDoSpread >= 6 )
507 			{
508 				pSoldier->fDoSpread = FALSE;
509 			}
510 
511 
512 			if ( pSoldier->fDoSpread )
513 			{
514 				if ( pSoldier->sSpreadLocations[ pSoldier->fDoSpread - 1 ] != 0 )
515 				{
516 					UseGun( pSoldier, pSoldier->sSpreadLocations[ pSoldier->fDoSpread - 1 ] );
517 				}
518 				else
519 				{
520 					UseGun( pSoldier, sTargetGridNo );
521 				}
522 				pSoldier->fDoSpread++;
523 			}
524 			else
525 			{
526 				UseGun( pSoldier, sTargetGridNo );
527 			}
528 			break;
529 		case IC_BLADE:
530 
531 			UseBlade( pSoldier, sTargetGridNo );
532 			break;
533 		case IC_PUNCH:
534 			UseHandToHand( pSoldier, sTargetGridNo, FALSE );
535 			break;
536 
537 		case IC_LAUNCHER:
538 			UseLauncher( pSoldier, sTargetGridNo );
539 			break;
540 
541 		default:
542 			// attempt to throw
543 			UseThrown( pSoldier, sTargetGridNo );
544 			break;
545 	}
546 	return( TRUE );
547 }
548 
549 
GetTargetWorldPositions(SOLDIERTYPE * pSoldier,INT16 sTargetGridNo,FLOAT * pdXPos,FLOAT * pdYPos,FLOAT * pdZPos)550 void GetTargetWorldPositions( SOLDIERTYPE *pSoldier, INT16 sTargetGridNo, FLOAT *pdXPos, FLOAT *pdYPos, FLOAT *pdZPos )
551 {
552 	FLOAT  dTargetX;
553 	FLOAT  dTargetY;
554 	FLOAT  dTargetZ;
555 	INT16  sXMapPos, sYMapPos;
556 	UINT32 uiRoll;
557 
558 	SOLDIERTYPE* const pTargetSoldier = WhoIsThere2(sTargetGridNo, pSoldier->bTargetLevel);
559 	if ( pTargetSoldier )
560 	{
561 		pSoldier->opponent = pTargetSoldier;
562 		dTargetX = (FLOAT) CenterX( pTargetSoldier->sGridNo );
563 		dTargetY = (FLOAT) CenterY( pTargetSoldier->sGridNo );
564 
565 		INT8 const bAimShotLocation = pSoldier->bAimShotLocation;
566 
567 		if (pSoldier->bAimShotLocation == AIM_SHOT_RANDOM)
568 		{
569 			uiRoll = PreRandom( 100 );
570 			if (uiRoll < 15)
571 			{
572 				pSoldier->bAimShotLocation = AIM_SHOT_LEGS;
573 			}
574 			else if (uiRoll > 94)
575 			{
576 				pSoldier->bAimShotLocation = AIM_SHOT_HEAD;
577 			}
578 			else
579 			{
580 				pSoldier->bAimShotLocation = AIM_SHOT_TORSO;
581 			}
582 			if ( pSoldier->bAimShotLocation != AIM_SHOT_HEAD )
583 			{
584 				UINT32 uiChanceToGetThrough = SoldierToSoldierBodyPartChanceToGetThrough( pSoldier, pTargetSoldier, pSoldier->bAimShotLocation );
585 
586 				if ( uiChanceToGetThrough < 25 )
587 				{
588 					if ( SoldierToSoldierBodyPartChanceToGetThrough( pSoldier, pTargetSoldier, AIM_SHOT_HEAD ) > uiChanceToGetThrough * 2 )
589 					{
590 						// try for a head shot then
591 						pSoldier->bAimShotLocation = AIM_SHOT_HEAD;
592 					}
593 				}
594 			}
595 
596 		}
597 
598 		if (gamepolicy(ai_better_aiming_choice) && bAimShotLocation == AIM_SHOT_RANDOM)
599 		{
600 			UINT32 const threshold_cth_head = gamepolicy(threshold_cth_head);
601 			UINT32 const threshold_cth_legs = gamepolicy(threshold_cth_legs);
602 			UINT32 const cth_aim_shot_head = SoldierToSoldierBodyPartChanceToGetThrough( pSoldier, pTargetSoldier, AIM_SHOT_HEAD );
603 			UINT32 const cth_aim_shot_torso = SoldierToSoldierBodyPartChanceToGetThrough( pSoldier, pTargetSoldier, AIM_SHOT_TORSO );
604 			UINT32 const cth_aim_shot_legs = SoldierToSoldierBodyPartChanceToGetThrough( pSoldier, pTargetSoldier, AIM_SHOT_LEGS );
605 
606 			UINT32 cth_choice = cth_aim_shot_torso;
607 			pSoldier->bAimShotLocation = AIM_SHOT_TORSO; // default
608 
609 			if (cth_aim_shot_legs >= threshold_cth_legs || (cth_aim_shot_legs + 5) > cth_choice)
610 			{
611 				pSoldier->bAimShotLocation = AIM_SHOT_LEGS;
612 				cth_choice = cth_aim_shot_legs;
613 			}
614 
615 			if (cth_aim_shot_head >= threshold_cth_head ||   // good enough, override
616 				cth_aim_shot_head >= cth_choice) // close enough, better if extra damage
617 			{
618 				pSoldier->bAimShotLocation = AIM_SHOT_HEAD;
619 			}
620 		}
621 
622 		switch( pSoldier->bAimShotLocation )
623 		{
624 			case AIM_SHOT_HEAD:
625 				CalculateSoldierZPos( pTargetSoldier, HEAD_TARGET_POS, &dTargetZ );
626 				break;
627 			case AIM_SHOT_TORSO:
628 				CalculateSoldierZPos( pTargetSoldier, TORSO_TARGET_POS, &dTargetZ );
629 				break;
630 			case AIM_SHOT_LEGS:
631 				CalculateSoldierZPos( pTargetSoldier, LEGS_TARGET_POS, &dTargetZ );
632 				break;
633 			default:
634 				// %)@#&(%?
635 				CalculateSoldierZPos( pTargetSoldier, TARGET_POS, &dTargetZ );
636 				break;
637 		}
638 	}
639 	else
640 	{
641 
642 		// GET TARGET XY VALUES
643 		ConvertGridNoToCenterCellXY( sTargetGridNo, &sXMapPos, &sYMapPos );
644 
645 		// fire at centre of tile
646 		dTargetX = (FLOAT) sXMapPos;
647 		dTargetY = (FLOAT) sYMapPos;
648 		if (pSoldier->bTargetCubeLevel)
649 		{
650 			// fire at the centre of the cube specified
651 			dTargetZ = ( (FLOAT) (pSoldier->bTargetCubeLevel + pSoldier->bTargetLevel * PROFILE_Z_SIZE) - 0.5f) * HEIGHT_UNITS_PER_INDEX;
652 		}
653 		else
654 		{
655 			INT8 bStructHeight = GetStructureTargetHeight(sTargetGridNo, pSoldier->bTargetLevel == 1);
656 			if (bStructHeight > 0)
657 			{
658 				// fire at the centre of the cube *one below* the tallest of the tallest structure
659 				if (bStructHeight > 1)
660 				{
661 					// reduce target level by 1
662 					bStructHeight--;
663 				}
664 				dTargetZ = ((FLOAT) (bStructHeight + pSoldier->bTargetLevel * PROFILE_Z_SIZE) - 0.5f) * HEIGHT_UNITS_PER_INDEX;
665 			}
666 			else
667 			{
668 				// fire at 1 unit above the level of the ground
669 				dTargetZ = (FLOAT) (pSoldier->bTargetLevel * PROFILE_Z_SIZE) * HEIGHT_UNITS_PER_INDEX + 1;
670 			}
671 		}
672 		// adjust for terrain height
673 		dTargetZ += CONVERT_PIXELS_TO_HEIGHTUNITS( gpWorldLevelData[sTargetGridNo].sHeight );
674 	}
675 
676 	*pdXPos = dTargetX;
677 	*pdYPos = dTargetY;
678 	*pdZPos = dTargetZ;
679 }
680 
681 
ModifyExpGainByTarget(const UINT16 exp_gain,const SOLDIERTYPE * const tgt)682 static UINT16 ModifyExpGainByTarget(const UINT16 exp_gain, const SOLDIERTYPE* const tgt)
683 {
684 	if (tgt->ubBodyType == COW || tgt->ubBodyType == CROW)
685 	{
686 		return exp_gain / 2;
687 	}
688 	else if (IsMechanical(*tgt) || TANK(tgt))
689 	{
690 		// no exp from shooting a vehicle that you can't damage and can't move!
691 		return 0;
692 	}
693 	return exp_gain;
694 }
695 
696 
697 static BOOLEAN WillExplosiveWeaponFail(const SOLDIERTYPE* pSoldier, const OBJECTTYPE* pObj);
698 
699 
UseGun(SOLDIERTYPE * pSoldier,INT16 sTargetGridNo)700 static BOOLEAN UseGun(SOLDIERTYPE* pSoldier, INT16 sTargetGridNo)
701 {
702 	UINT32  uiHitChance, uiDiceRoll;
703 	INT16   sAPCost;
704 	FLOAT   dTargetX;
705 	FLOAT   dTargetY;
706 	FLOAT   dTargetZ;
707 	UINT16  usItemNum;
708 	BOOLEAN fBuckshot;
709 	UINT8   ubVolume;
710 	INT8    bSilencerPos;
711 	char    zBurstString[50];
712 	UINT8   ubDirection;
713 	INT16   sNewGridNo;
714 	BOOLEAN fGonnaHit = FALSE;
715 	UINT16  usExpGain = 0;
716 	UINT32  uiDepreciateTest;
717 
718 	// Deduct points!
719 	sAPCost = CalcTotalAPsToAttack( pSoldier, sTargetGridNo, FALSE, pSoldier->bAimTime );
720 
721 	usItemNum = pSoldier->usAttackingWeapon;
722 
723 	if ( pSoldier->bDoBurst )
724 	{
725 		// ONly deduct points once
726 		if ( pSoldier->bDoBurst == 1 )
727 		{
728 			if ( GCM->getWeapon( usItemNum )->hasBurstSound() )
729 			{
730 				// IF we are silenced?
731 				if( FindAttachment( &( pSoldier->inv[ pSoldier->ubAttackingHand ] ), SILENCER ) != NO_SLOT )
732 				{
733 					// Pick sound file baed on how many bullets we are going to fire...
734 					sprintf( zBurstString, SOUNDSDIR "/weapons/silencer burst %d.wav", pSoldier->bBulletsLeft );
735 
736 					// Try playing sound...
737 					pSoldier->uiBurstSoundID = PlayLocationJA2SampleFromFile(pSoldier->sGridNo, zBurstString, HIGHVOLUME, 1);
738 				}
739 				else
740 				{
741 					// Pick sound file baed on how many bullets we are going to fire...
742 					sprintf(zBurstString, SOUNDSDIR "/weapons/%s%d.wav",
743 							GCM->getWeapon(usItemNum)->calibre->burstSoundString.c_str(),
744 							pSoldier->bBulletsLeft);
745 
746 					// Try playing sound...
747 					pSoldier->uiBurstSoundID = PlayLocationJA2SampleFromFile(pSoldier->sGridNo, zBurstString, HIGHVOLUME, 1);
748 				}
749 
750 				if ( pSoldier->uiBurstSoundID == NO_SAMPLE )
751 				{
752 					// If failed, play normal default....
753 					pSoldier->uiBurstSoundID = PlayLocationJA2Sample(pSoldier->sGridNo, GCM->getWeapon(usItemNum)->burstSound, HIGHVOLUME, 1);
754 				}
755 			}
756 
757 			DeductPoints( pSoldier, sAPCost, 0 );
758 		}
759 
760 	}
761 	else
762 	{
763 		// ONLY DEDUCT FOR THE FIRST HAND when doing two-pistol attacks
764 		if ( IsValidSecondHandShot( pSoldier ) && pSoldier->inv[ HANDPOS ].bGunStatus >= USABLE && pSoldier->inv[HANDPOS].bGunAmmoStatus > 0 )
765 		{
766 			// only deduct APs when the main gun fires
767 			if ( pSoldier->ubAttackingHand == HANDPOS )
768 			{
769 				DeductPoints( pSoldier, sAPCost, 0 );
770 			}
771 		}
772 		else
773 		{
774 			DeductPoints( pSoldier, sAPCost, 0 );
775 		}
776 
777 		//PLAY SOUND
778 		// ( For throwing knife.. it's earlier in the animation
779 		if ( GCM->getWeapon( usItemNum )->hasSound() && GCM->getItem(usItemNum)->getItemClass() != IC_THROWING_KNIFE )
780 		{
781 			// Switch on silencer...
782 			if( FindAttachment( &( pSoldier->inv[ pSoldier->ubAttackingHand ] ), SILENCER ) != NO_SLOT )
783 			{
784 				SoundID uiSound = (SoundID) GCM->getWeapon( usItemNum )->calibre->silencerSound;
785 				PlayLocationJA2Sample(pSoldier->sGridNo, uiSound, HIGHVOLUME, 1);
786 			}
787 			else
788 			{
789 				PlayLocationJA2Sample(pSoldier->sGridNo, GCM->getWeapon(usItemNum)->sound, HIGHVOLUME, 1);
790 			}
791 		}
792 	}
793 
794 
795 	// CALC CHANCE TO HIT
796 	if ( GCM->getItem(usItemNum)->getItemClass() == IC_THROWING_KNIFE )
797 	{
798 		uiHitChance = CalcThrownChanceToHit( pSoldier, sTargetGridNo, pSoldier->bAimTime, pSoldier->bAimShotLocation );
799 	}
800 	else
801 	{
802 		uiHitChance = CalcChanceToHitGun( pSoldier, sTargetGridNo, pSoldier->bAimTime, pSoldier->bAimShotLocation, true );
803 	}
804 
805 	//ATE: Added if we are in meanwhile, we always hit...
806 	if ( AreInMeanwhile( ) )
807 	{
808 		uiHitChance = 100;
809 	}
810 
811 	// ROLL DICE
812 	uiDiceRoll = PreRandom( 100 );
813 
814 	fGonnaHit = uiDiceRoll <= uiHitChance;
815 
816 	// ATE; Moved a whole blotch if logic code for finding target positions to a function
817 	// so other places can use it
818 	GetTargetWorldPositions( pSoldier, sTargetGridNo, &dTargetX, &dTargetY, &dTargetZ );
819 
820 	// Some things we don't do for knives...
821 	if ( GCM->getItem(usItemNum)->getItemClass() != IC_THROWING_KNIFE )
822 	{
823 		// Deduct AMMO!
824 		DeductAmmo( pSoldier, pSoldier->ubAttackingHand );
825 
826 		// ATE: Check if we should say quote...
827 		if ( pSoldier->inv[ pSoldier->ubAttackingHand ].ubGunShotsLeft == 0 && pSoldier->usAttackingWeapon != ROCKET_LAUNCHER )
828 		{
829 			if ( pSoldier->bTeam == OUR_TEAM )
830 			{
831 				pSoldier->fSayAmmoQuotePending = TRUE;
832 			}
833 		}
834 
835 		// NB bDoBurst will be 2 at this point for the first shot since it was incremented
836 		// above
837 		if (IsOnOurTeam(*pSoldier) &&
838 			pSoldier->target != NULL  &&
839 			(!pSoldier->bDoBurst || pSoldier->bDoBurst == 2) &&
840 			gTacticalStatus.uiFlags & INCOMBAT)
841 		{
842 			const SOLDIERTYPE* const tgt = pSoldier->target;
843 			if (SoldierToSoldierBodyPartChanceToGetThrough(pSoldier, tgt, pSoldier->bAimShotLocation) > 0)
844 			{
845 				if ( fGonnaHit )
846 				{
847 					// grant extra exp for hitting a difficult target
848 					usExpGain += (UINT8) (100 - uiHitChance) / 25;
849 
850 					if ( pSoldier->bAimTime && !pSoldier->bDoBurst )
851 					{
852 						// gain extra exp for aiming, up to the amount from
853 						// the difficulty of the shot
854 						usExpGain += __min( pSoldier->bAimTime, usExpGain );
855 					}
856 
857 					// base pts extra for hitting
858 					usExpGain	+= 3;
859 				}
860 
861 				// add base pts for taking a shot, whether it hits or misses
862 				usExpGain += 3;
863 
864 				if ( IsValidSecondHandShot( pSoldier ) && pSoldier->inv[ HANDPOS ].bGunStatus >= USABLE && pSoldier->inv[HANDPOS].bGunAmmoStatus > 0 )
865 				{
866 					// reduce exp gain for two pistol shooting since both shots give xp
867 					usExpGain = (usExpGain * 2) / 3;
868 				}
869 
870 				usExpGain = ModifyExpGainByTarget(usExpGain, tgt);
871 
872 				// MARKSMANSHIP GAIN: gun attack
873 				StatChange(*pSoldier, MARKAMT, usExpGain, fGonnaHit ? FROM_SUCCESS : FROM_FAILURE);
874 			}
875 		}
876 
877 		// set buckshot and muzzle flash
878 		fBuckshot = FALSE;
879 		if (!CREATURE_OR_BLOODCAT( pSoldier ) )
880 		{
881 			pSoldier->fMuzzleFlash = TRUE;
882 			switch ( pSoldier->inv[ pSoldier->ubAttackingHand ].ubGunAmmoType )
883 			{
884 				case AMMO_BUCKSHOT:
885 					fBuckshot = TRUE;
886 					break;
887 				case AMMO_SLEEP_DART:
888 					pSoldier->fMuzzleFlash = FALSE;
889 					break;
890 				default:
891 					break;
892 			}
893 		}
894 	}
895 	else //  throwing knife
896 	{
897 		fBuckshot = FALSE;
898 		pSoldier->fMuzzleFlash = FALSE;
899 
900 		// Deduct knife from inv! (not here, later?)
901 
902 		// Improve for using a throwing knife....
903 		if (IsOnOurTeam(*pSoldier) && pSoldier->target != NULL)
904 		{
905 			if ( fGonnaHit )
906 			{
907 				// grant extra exp for hitting a difficult target
908 				usExpGain += (UINT8) (100 - uiHitChance) / 10;
909 
910 				if (pSoldier->bAimTime)
911 				{
912 					// gain extra exp for aiming, up to the amount from
913 					// the difficulty of the throw
914 					usExpGain += ( 2 * __min( pSoldier->bAimTime, usExpGain ) );
915 				}
916 
917 				// base pts extra for hitting
918 				usExpGain	+= 10;
919 			}
920 
921 			// add base pts for taking a shot, whether it hits or misses
922 			usExpGain += 10;
923 
924 			usExpGain = ModifyExpGainByTarget(usExpGain, pSoldier->target);
925 
926 			// MARKSMANSHIP/DEXTERITY GAIN: throwing knife attack
927 			StatChangeCause const cause = fGonnaHit ? FROM_SUCCESS : FROM_FAILURE;
928 			StatChange(*pSoldier, MARKAMT, usExpGain / 2, cause);
929 			StatChange(*pSoldier, DEXTAMT, usExpGain / 2, cause);
930 		}
931 	}
932 
933 	if ( usItemNum == ROCKET_LAUNCHER)
934 	{
935 		if ( WillExplosiveWeaponFail( pSoldier, &( pSoldier->inv[ HANDPOS ] ) ) )
936 		{
937 			CreateItem( DISCARDED_LAW, pSoldier->inv[ HANDPOS ].bStatus[ 0 ], &(pSoldier->inv[ HANDPOS ] ) );
938 			DirtyMercPanelInterface( pSoldier, DIRTYLEVEL2 );
939 
940 			IgniteExplosion(pSoldier, 0, pSoldier->sGridNo, C1, pSoldier->bLevel);
941 
942 			// Reduce again for attack end 'cause it has been incremented for a normal attack
943 			//
944 			SLOGD(
945 				"Freeing up attacker - ATTACK ANIMATION %s ENDED BY BAD EXPLOSIVE CHECK, Now %d",
946 				gAnimControl[pSoldier->usAnimState].zAnimStr, gTacticalStatus.ubAttackBusyCount);
947 			ReduceAttackBusyCount(pSoldier, FALSE);
948 
949 			return( FALSE );
950 		}
951 	}
952 
953 	FireBulletGivenTarget(pSoldier, dTargetX, dTargetY, dTargetZ, pSoldier->usAttackingWeapon,
954 				(UINT16) (uiHitChance - uiDiceRoll), fBuckshot, FALSE);
955 
956 	ubVolume = GCM->getWeapon( pSoldier->usAttackingWeapon )->ubAttackVolume;
957 
958 	if ( GCM->getItem(usItemNum)->getItemClass() == IC_THROWING_KNIFE )
959 	{
960 		// Here, remove the knife...	or (for now) rocket launcher
961 		RemoveObjs( &(pSoldier->inv[ HANDPOS ] ), 1 );
962 		if(pSoldier->inv[HANDPOS].usItem == NOTHING)
963 		{
964 			INT8 slot=FindObj(pSoldier, BLOODY_THROWING_KNIFE);
965 			if(slot==NO_SLOT) slot=FindObj(pSoldier, THROWING_KNIFE);
966 			if(slot!=NO_SLOT)
967 			{
968 				OBJECTTYPE *item=&pSoldier->inv[slot];
969 				CreateItem(item->usItem, item->bStatus[0], &pSoldier->inv[HANDPOS]);
970 				RemoveObjs(item, 1);
971 			}
972 		}
973 		DirtyMercPanelInterface( pSoldier, DIRTYLEVEL2 );
974 	}
975 	else if ( usItemNum == ROCKET_LAUNCHER)
976 	{
977 		CreateItem( DISCARDED_LAW, pSoldier->inv[ HANDPOS ].bStatus[ 0 ], &(pSoldier->inv[ HANDPOS ] ) );
978 		DirtyMercPanelInterface( pSoldier, DIRTYLEVEL2 );
979 
980 		// Direction to center of explosion
981 		ubDirection = OppositeDirection(pSoldier->bDirection);
982 		sNewGridNo  = NewGridNo( (UINT16)pSoldier->sGridNo, DirectionInc( ubDirection ) );
983 
984 		// Check if a person exists here and is not prone....
985 		SOLDIERTYPE* const tgt = WhoIsThere2(sNewGridNo, pSoldier->bLevel);
986 		if (tgt != NULL)
987 		{
988 			if (gAnimControl[tgt->usAnimState].ubHeight != ANIM_PRONE)
989 			{
990 				// Increment attack counter...
991 				gTacticalStatus.ubAttackBusyCount++;
992 				SLOGD(ST::format("Incrementing Attack: Exhaust from LAW ({})",
993 					gTacticalStatus.ubAttackBusyCount));
994 				EVENT_SoldierGotHit(tgt, MINI_GRENADE, 10, 200, pSoldier->bDirection, 0, pSoldier, 0, ANIM_CROUCH, sNewGridNo);
995 			}
996 		}
997 	}
998 	else
999 	{
1000 		// if the weapon has a silencer attached
1001 		bSilencerPos = FindAttachment( &(pSoldier->inv[HANDPOS]), SILENCER );
1002 		if (bSilencerPos != -1)
1003 		{
1004 			// reduce volume by a percentage equal to silencer's work %age (min 1)
1005 			ubVolume = 1 + ((100 - WEAPON_STATUS_MOD(pSoldier->inv[HANDPOS].bAttachStatus[bSilencerPos])) / (100 / (ubVolume - 1)));
1006 		}
1007 	}
1008 
1009 	MakeNoise(pSoldier, pSoldier->sGridNo, pSoldier->bLevel, ubVolume, NOISE_GUNFIRE);
1010 
1011 	if ( pSoldier->bDoBurst )
1012 	{
1013 		// done, if bursting, increment
1014 		pSoldier->bDoBurst++;
1015 	}
1016 
1017 	// CJC: since jamming is no longer affected by reliability, increase chance of status going down for really unreliabile guns
1018 	uiDepreciateTest = BASIC_DEPRECIATE_CHANCE + 3 * GCM->getItem(usItemNum)->getReliability();
1019 
1020 	if ( !PreRandom( uiDepreciateTest ) && ( pSoldier->inv[ pSoldier->ubAttackingHand ].bStatus[0] > 1) )
1021 	{
1022 		pSoldier->inv[ pSoldier->ubAttackingHand ].bStatus[ 0 ]--;
1023 	}
1024 
1025 	// reduce monster smell (gunpowder smell)
1026 	if ( pSoldier->bMonsterSmell > 0 && Random( 2 ) == 0 )
1027 	{
1028 		pSoldier->bMonsterSmell--;
1029 	}
1030 
1031 	return( TRUE );
1032 }
1033 
1034 
AgilityForEnemyMissingPlayer(const SOLDIERTYPE * const attacker,SOLDIERTYPE * const target,const UINT agil_amt)1035 static void AgilityForEnemyMissingPlayer(const SOLDIERTYPE* const attacker, SOLDIERTYPE* const target, const UINT agil_amt)
1036 {
1037 	// if it was another team attacking someone under our control
1038 	if (target->bTeam != attacker->bTeam &&
1039 			target->bTeam == OUR_TEAM && target->bLife >= OKLIFE)
1040 	{
1041 		StatChange(*target, AGILAMT, agil_amt, FROM_SUCCESS);
1042 	}
1043 }
1044 
1045 
UseBlade(SOLDIERTYPE * const pSoldier,INT16 const sTargetGridNo)1046 static void UseBlade(SOLDIERTYPE* const pSoldier, INT16 const sTargetGridNo)
1047 {
1048 	INT32          iHitChance, iDiceRoll;
1049 	INT16          sAPCost;
1050 	EV_S_WEAPONHIT SWeaponHit;
1051 	INT32          iImpact, iImpactForCrits;
1052 	BOOLEAN        fGonnaHit = FALSE;
1053 	UINT16         usExpGain = 0;
1054 	INT8           bMaxDrop;
1055 	BOOLEAN        fSurpriseAttack;
1056 
1057 	// Deduct points!
1058 	sAPCost = CalcTotalAPsToAttack( pSoldier, sTargetGridNo, FALSE, pSoldier->bAimTime );
1059 
1060 	DeductPoints( pSoldier, sAPCost, 0 );
1061 
1062 	// See if a guy is here!
1063 	SOLDIERTYPE* const pTargetSoldier = WhoIsThere2(sTargetGridNo, pSoldier->bTargetLevel);
1064 	if ( pTargetSoldier )
1065 	{
1066 		// set target as noticed attack
1067 		pSoldier->uiStatusFlags |= SOLDIER_ATTACK_NOTICED;
1068 		pTargetSoldier->fIntendedTarget = TRUE;
1069 
1070 		pSoldier->opponent = pTargetSoldier;
1071 
1072 		// CHECK IF BUDDY KNOWS ABOUT US
1073 		if ( pTargetSoldier->bOppList[ pSoldier->ubID ] == NOT_HEARD_OR_SEEN ||
1074 			pTargetSoldier->bLife < OKLIFE || pTargetSoldier->bCollapsed )
1075 		{
1076 			iHitChance = 100;
1077 			fSurpriseAttack = TRUE;
1078 		}
1079 		else
1080 		{
1081 			iHitChance = CalcChanceToStab( pSoldier, pTargetSoldier, pSoldier->bAimTime );
1082 			fSurpriseAttack = FALSE;
1083 		}
1084 
1085 		// ROLL DICE
1086 		iDiceRoll = (INT32) PreRandom( 100 );
1087 
1088 		if ( iDiceRoll <= iHitChance )
1089 		{
1090 			fGonnaHit = TRUE;
1091 
1092 			// CALCULATE DAMAGE!
1093 			// attack HITS, calculate damage (base damage is 1-maximum knife sImpact)
1094 			iImpact = HTHImpact( pSoldier, pTargetSoldier, (iHitChance - iDiceRoll), TRUE );
1095 
1096 			// modify this by the knife's condition (if it's dull, not much good)
1097 			iImpact = ( iImpact * WEAPON_STATUS_MOD(pSoldier->inv[pSoldier->ubAttackingHand].bStatus[0]) ) / 100;
1098 
1099 			// modify by hit location
1100 			AdjustImpactByHitLocation( iImpact, pSoldier->bAimShotLocation, &iImpact, &iImpactForCrits );
1101 
1102 			// bonus for surprise
1103 			if ( fSurpriseAttack )
1104 			{
1105 				iImpact = (iImpact * 3) / 2;
1106 			}
1107 
1108 			// any successful hit does at LEAST 1 pt minimum damage
1109 			if (iImpact < 1)
1110 			{
1111 				iImpact = 1;
1112 			}
1113 
1114 			if ( pSoldier->inv[ pSoldier->ubAttackingHand ].bStatus[ 0 ] > USABLE )
1115 			{
1116 				bMaxDrop = (iImpact / 20);
1117 
1118 				// the duller they get, the slower they get any worse...
1119 				bMaxDrop = __min( bMaxDrop, pSoldier->inv[ pSoldier->ubAttackingHand ].bStatus[ 0 ] / 10 );
1120 
1121 				// as long as its still > USABLE, it drops another point 1/2 the time
1122 				bMaxDrop = __max( bMaxDrop, 2 );
1123 
1124 				pSoldier->inv[ pSoldier->ubAttackingHand ].bStatus[ 0 ] -= (INT8) Random( bMaxDrop );     // 0 to (maxDrop - 1)
1125 			}
1126 
1127 			// Send event for getting hit
1128 			SWeaponHit = EV_S_WEAPONHIT{};
1129 			SWeaponHit.usSoldierID = pTargetSoldier->ubID;
1130 			SWeaponHit.usWeaponIndex = pSoldier->usAttackingWeapon;
1131 			SWeaponHit.sDamage = (INT16) iImpact;
1132 			SWeaponHit.usDirection = GetDirectionFromGridNo( pSoldier->sGridNo, pTargetSoldier );
1133 			SWeaponHit.sXPos = (INT16)pTargetSoldier->dXPos;
1134 			SWeaponHit.sYPos = (INT16)pTargetSoldier->dYPos;
1135 			SWeaponHit.sZPos = 20;
1136 			SWeaponHit.sRange = 1;
1137 			SWeaponHit.ubAttackerID = pSoldier->ubID;
1138 			SWeaponHit.ubSpecial = FIRE_WEAPON_NO_SPECIAL;
1139 			AddGameEvent(SWeaponHit, 20);
1140 		}
1141 		else
1142 		{
1143 			// AGILITY GAIN (10):  Target avoids a knife attack
1144 			AgilityForEnemyMissingPlayer(pSoldier, pTargetSoldier, 10);
1145 			SLOGD("Freeing up attacker - missed in knife attack");
1146 			FreeUpAttacker(pSoldier);
1147 		}
1148 
1149 		if (IsOnOurTeam(*pSoldier) && pSoldier->target != NULL)
1150 		{
1151 			if ( fGonnaHit )
1152 			{
1153 				// grant extra exp for hitting a difficult target
1154 				usExpGain += (UINT8) (100 - iHitChance) / 10;
1155 
1156 				if (pSoldier->bAimTime)
1157 				{
1158 					// gain extra exp for aiming, up to the amount from
1159 					// the difficulty of the attack
1160 					usExpGain += ( 2 * __min( pSoldier->bAimTime, usExpGain ) );
1161 				}
1162 
1163 				// base pts extra for hitting
1164 				usExpGain += 10;
1165 			}
1166 
1167 			// add base pts for taking a shot, whether it hits or misses
1168 			usExpGain += 10;
1169 
1170 			usExpGain = ModifyExpGainByTarget(usExpGain, pSoldier->target);
1171 
1172 			// DEXTERITY GAIN:  Made a knife attack, successful or not
1173 			StatChange(*pSoldier, DEXTAMT, usExpGain, fGonnaHit ? FROM_SUCCESS : FROM_FAILURE);
1174 		}
1175 	}
1176 	else
1177 	{
1178 		SLOGD("Freeing up attacker - missed in knife attack");
1179 		FreeUpAttacker(pSoldier);
1180 	}
1181 
1182 	// possibly reduce monster smell
1183 	if ( pSoldier->bMonsterSmell > 0 && Random( 5 ) == 0 )
1184 	{
1185 		pSoldier->bMonsterSmell--;
1186 	}
1187 }
1188 
1189 
1190 static UINT32 CalcChanceToSteal(SOLDIERTYPE* pAttacker, SOLDIERTYPE* pDefender, UINT8 ubAimTime);
1191 
1192 
UseHandToHand(SOLDIERTYPE * const pSoldier,INT16 const sTargetGridNo,BOOLEAN const fStealing)1193 void UseHandToHand(SOLDIERTYPE* const pSoldier, INT16 const sTargetGridNo, BOOLEAN const fStealing)
1194 {
1195 	INT32          iHitChance, iDiceRoll;
1196 	INT16          sAPCost;
1197 	EV_S_WEAPONHIT SWeaponHit;
1198 	INT32          iImpact;
1199 	UINT16         usOldItem;
1200 
1201 	// Deduct points!
1202 	// August 13 2002: unless stealing - APs already deducted elsewhere
1203 	if (!fStealing)
1204 	{
1205 		sAPCost = CalcTotalAPsToAttack( pSoldier, sTargetGridNo, FALSE, pSoldier->bAimTime );
1206 
1207 		DeductPoints( pSoldier, sAPCost, 0 );
1208 	}
1209 
1210 	// See if a guy is here!
1211 	SOLDIERTYPE* const pTargetSoldier = WhoIsThere2(sTargetGridNo, pSoldier->bTargetLevel);
1212 	if ( pTargetSoldier )
1213 	{
1214 		// set target as noticed attack
1215 		pSoldier->uiStatusFlags |= SOLDIER_ATTACK_NOTICED;
1216 		pTargetSoldier->fIntendedTarget = TRUE;
1217 
1218 		pSoldier->opponent = pTargetSoldier;
1219 
1220 		if (fStealing)
1221 		{
1222 			if (AM_A_ROBOT(pTargetSoldier) || TANK(pTargetSoldier) || CREATURE_OR_BLOODCAT(pTargetSoldier))
1223 			{
1224 				iHitChance = 0;
1225 			}
1226 			else if ( pTargetSoldier->bOppList[ pSoldier->ubID ] == NOT_HEARD_OR_SEEN )
1227 			{
1228 				// give bonus for surprise, but not so much as struggle would still occur
1229 				iHitChance = CalcChanceToSteal( pSoldier, pTargetSoldier, pSoldier->bAimTime ) + 20;
1230 			}
1231 			else if ( pTargetSoldier->bLife < OKLIFE || pTargetSoldier->bCollapsed )
1232 			{
1233 				iHitChance = 100;
1234 			}
1235 			else
1236 			{
1237 				iHitChance = CalcChanceToSteal( pSoldier, pTargetSoldier, pSoldier->bAimTime );
1238 			}
1239 		}
1240 		else
1241 		{
1242 			if ( pTargetSoldier->bOppList[ pSoldier->ubID ] == NOT_HEARD_OR_SEEN || pTargetSoldier->bLife < OKLIFE || pTargetSoldier->bCollapsed )
1243 			{
1244 				iHitChance = 100;
1245 			}
1246 			else
1247 			{
1248 				iHitChance = CalcChanceToPunch( pSoldier, pTargetSoldier, pSoldier->bAimTime );
1249 			}
1250 		}
1251 
1252 		// ROLL DICE
1253 		iDiceRoll = (INT32) PreRandom( 100 );
1254 
1255 		if (fStealing )
1256 		{
1257 			if ( pTargetSoldier->inv[HANDPOS].usItem != NOTHING )
1258 			{
1259 
1260 				if ( iDiceRoll <= iHitChance )
1261 				{
1262 					// Was a good steal!
1263 					ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, st_format_printf(g_langRes->Message[ STR_STOLE_SOMETHING ], pSoldier->name, ShortItemNames[ pTargetSoldier->inv[HANDPOS].usItem ]) );
1264 
1265 					usOldItem = pTargetSoldier->inv[HANDPOS].usItem;
1266 
1267 					if (pSoldier->bTeam == OUR_TEAM && pTargetSoldier->bTeam != OUR_TEAM && !IsMechanical(*pTargetSoldier) && !TANK(pTargetSoldier))
1268 					{
1269 						// made a steal; give experience
1270 						StatChange(*pSoldier, STRAMT, 8, FROM_SUCCESS);
1271 					}
1272 
1273 					if ( iDiceRoll <= iHitChance * 2 / 3)
1274 					{
1275 						// Grabbed item
1276 						if (AutoPlaceObject( pSoldier, &(pTargetSoldier->inv[HANDPOS]), TRUE ))
1277 						{
1278 							// Item transferred; remove it from the target's inventory
1279 							DeleteObj( &(pTargetSoldier->inv[HANDPOS]) );
1280 						}
1281 						else
1282 						{
1283 							// No room to hold it so the item should drop in our tile again
1284 							AddItemToPool(pSoldier->sGridNo, &pTargetSoldier->inv[HANDPOS], VISIBLE, pSoldier->bLevel, 0, -1);
1285 							DeleteObj( &(pTargetSoldier->inv[HANDPOS]) );
1286 						}
1287 					}
1288 					else
1289 					{
1290 
1291 						if ( pSoldier->bTeam == OUR_TEAM )
1292 						{
1293 							DoMercBattleSound( pSoldier, BATTLE_SOUND_CURSE1 );
1294 						}
1295 
1296 						// Item dropped somewhere... roll based on the same chance to determine where!
1297 						iDiceRoll = (INT32) PreRandom( 100 );
1298 						if (iDiceRoll < iHitChance)
1299 						{
1300 							// Drop item in the our tile
1301 							AddItemToPool(pSoldier->sGridNo, &(pTargetSoldier->inv[HANDPOS]), VISIBLE, pSoldier->bLevel, 0, -1);
1302 						}
1303 						else
1304 						{
1305 							// Drop item in the target's tile
1306 							AddItemToPool(pTargetSoldier->sGridNo, &pTargetSoldier->inv[HANDPOS], VISIBLE, pSoldier->bLevel, 0, -1);
1307 						}
1308 						DeleteObj( &(pTargetSoldier->inv[HANDPOS]) );
1309 					}
1310 
1311 					// Reload buddy's animation...
1312 					ReLoadSoldierAnimationDueToHandItemChange( pTargetSoldier, usOldItem, NOTHING );
1313 
1314 				}
1315 				else
1316 				{
1317 					ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE,
1318 						st_format_printf(g_langRes->Message[ STR_FAILED_TO_STEAL_SOMETHING ],
1319 						pSoldier->name, ShortItemNames[ pTargetSoldier->inv[HANDPOS].usItem ]) );
1320 					if ( pSoldier->bTeam == OUR_TEAM )
1321 					{
1322 						DoMercBattleSound( pSoldier, BATTLE_SOUND_CURSE1 );
1323 					}
1324 
1325 					if (iHitChance > 0 && pSoldier->bTeam == OUR_TEAM && pTargetSoldier->bTeam != OUR_TEAM && !IsMechanical(*pTargetSoldier) && !TANK(pTargetSoldier))
1326 					{
1327 						// failed a steal; give some experience
1328 						StatChange(*pSoldier, STRAMT, 4, FROM_FAILURE);
1329 					}
1330 
1331 				}
1332 			}
1333 			SLOGD("Freeing up attacker - steal");
1334 			FreeUpAttacker(pSoldier);
1335 		}
1336 		else
1337 		{
1338 
1339 			// ATE/CC: if doing ninja spin kick (only), automatically make it a hit
1340 			if ( pSoldier->usAnimState == NINJA_SPINKICK)
1341 			{
1342 				// Let him to succeed by a random amount
1343 				iDiceRoll = PreRandom( iHitChance );
1344 			}
1345 
1346 			if ( pSoldier->bTeam == OUR_TEAM && pTargetSoldier->bTeam != OUR_TEAM )
1347 			{
1348 				// made an HTH attack; give experience
1349 				UINT8           ubExpGain;
1350 				StatChangeCause reason;
1351 				if (iDiceRoll <= iHitChance)
1352 				{
1353 					ubExpGain = 8;
1354 					reason    = FROM_SUCCESS;
1355 				}
1356 				else
1357 				{
1358 					ubExpGain = 4;
1359 					reason    = FROM_FAILURE;
1360 				}
1361 
1362 				ubExpGain = ModifyExpGainByTarget(ubExpGain, pTargetSoldier);
1363 
1364 				StatChange(*pSoldier, STRAMT,  ubExpGain, reason);
1365 				StatChange(*pSoldier, DEXTAMT, ubExpGain, reason);
1366 			}
1367 			else if (iDiceRoll > iHitChance)
1368 			{
1369 				// being attacked... successfully dodged, give experience
1370 				AgilityForEnemyMissingPlayer(pSoldier, pTargetSoldier, 8);
1371 			}
1372 
1373 			if ( iDiceRoll <= iHitChance || AreInMeanwhile( ) )
1374 			{
1375 				// CALCULATE DAMAGE!
1376 				iImpact = HTHImpact( pSoldier, pTargetSoldier, (iHitChance - iDiceRoll), FALSE );
1377 
1378 				// Send event for getting hit
1379 				SWeaponHit = EV_S_WEAPONHIT{};
1380 				SWeaponHit.usSoldierID = pTargetSoldier->ubID;
1381 				SWeaponHit.usWeaponIndex = pSoldier->usAttackingWeapon;
1382 				SWeaponHit.sDamage = (INT16) iImpact;
1383 				SWeaponHit.usDirection = GetDirectionFromGridNo( pSoldier->sGridNo, pTargetSoldier );
1384 				SWeaponHit.sXPos = (INT16)pTargetSoldier->dXPos;
1385 				SWeaponHit.sYPos = (INT16)pTargetSoldier->dYPos;
1386 				SWeaponHit.sZPos = 20;
1387 				SWeaponHit.sRange = 1;
1388 				SWeaponHit.ubAttackerID = pSoldier->ubID;
1389 				SWeaponHit.ubSpecial = FIRE_WEAPON_NO_SPECIAL;
1390 				AddGameEvent(SWeaponHit, 20);
1391 			}
1392 			else
1393 			{
1394 				SLOGD("Freeing up attacker - missed in HTH attack");
1395 				FreeUpAttacker(pSoldier);
1396 			}
1397 		}
1398 	}
1399 
1400 	// possibly reduce monster smell (gunpowder smell)
1401 	if ( pSoldier->bMonsterSmell > 0 && Random( 5 ) == 0 )
1402 	{
1403 		pSoldier->bMonsterSmell--;
1404 	}
1405 }
1406 
1407 
UseThrown(SOLDIERTYPE * const pSoldier,INT16 const sTargetGridNo)1408 static void UseThrown(SOLDIERTYPE* const pSoldier, INT16 const sTargetGridNo)
1409 {
1410 	UINT32 uiHitChance, uiDiceRoll;
1411 	INT8   bLoop;
1412 
1413 	uiHitChance = CalcThrownChanceToHit( pSoldier, sTargetGridNo, pSoldier->bAimTime, AIM_SHOT_TORSO );
1414 
1415 	uiDiceRoll = PreRandom( 100 );
1416 
1417 	if ( pSoldier->bTeam == OUR_TEAM && gTacticalStatus.uiFlags & INCOMBAT )
1418 	{
1419 		// check target gridno
1420 		const SOLDIERTYPE* pTargetSoldier = WhoIsThere2(pSoldier->sTargetGridNo, pSoldier->bTargetLevel);
1421 		if ( pTargetSoldier && pTargetSoldier->bTeam == pSoldier->bTeam )
1422 		{
1423 			// ignore!
1424 			pTargetSoldier = NULL;
1425 		}
1426 
1427 		if ( pTargetSoldier == NULL )
1428 		{
1429 			// search for an opponent near the target gridno
1430 			for ( bLoop = 0; bLoop < NUM_WORLD_DIRECTIONS; bLoop++ )
1431 			{
1432 				pTargetSoldier = WhoIsThere2(NewGridNo(pSoldier->sTargetGridNo, DirectionInc(bLoop)), pSoldier->bTargetLevel);
1433 				if (pTargetSoldier != NULL && pTargetSoldier->bTeam != pSoldier->bTeam) break;
1434 			}
1435 		}
1436 
1437 		if ( pTargetSoldier )
1438 		{
1439 			// ok this is a real attack on someone, grant experience
1440 			StatChange(*pSoldier, STRAMT, 5, FROM_SUCCESS);
1441 			if ( uiDiceRoll < uiHitChance )
1442 			{
1443 				StatChange(*pSoldier, DEXTAMT, 5, FROM_SUCCESS);
1444 				StatChange(*pSoldier, MARKAMT, 5, FROM_SUCCESS);
1445 			}
1446 			else
1447 			{
1448 				StatChange(*pSoldier, DEXTAMT, 2, FROM_FAILURE);
1449 				StatChange(*pSoldier, MARKAMT, 2, FROM_FAILURE);
1450 			}
1451 		}
1452 	}
1453 
1454 
1455 	CalculateLaunchItemParamsForThrow( pSoldier, sTargetGridNo, pSoldier->bTargetLevel, (INT16)(pSoldier->bTargetLevel * 256 ), &(pSoldier->inv[ HANDPOS ] ), (INT8)(uiDiceRoll - uiHitChance), THROW_ARM_ITEM, 0 );
1456 
1457 	// OK, goto throw animation
1458 	HandleSoldierThrowItem( pSoldier, pSoldier->sTargetGridNo );
1459 
1460 	UINT16 const thrown_item=pSoldier->inv[HANDPOS].usItem;
1461 	RemoveObjs( &(pSoldier->inv[ HANDPOS ] ), 1 );
1462 	if(pSoldier->inv[HANDPOS].usItem == NOTHING)
1463 	{
1464 		INT8 const slot=FindObj(pSoldier, thrown_item);
1465 		if(slot!=NO_SLOT)
1466 		{
1467 			OBJECTTYPE *item=&pSoldier->inv[slot];
1468 			CreateItem(item->usItem, item->bStatus[0], &pSoldier->inv[HANDPOS]);
1469 			RemoveObjs(item, 1);
1470 		}
1471 		DirtyMercPanelInterface( pSoldier, DIRTYLEVEL2 );
1472 	}
1473 }
1474 
1475 
UseLauncher(SOLDIERTYPE * pSoldier,INT16 sTargetGridNo)1476 static BOOLEAN UseLauncher(SOLDIERTYPE* pSoldier, INT16 sTargetGridNo)
1477 {
1478 	UINT32     uiHitChance, uiDiceRoll;
1479 	INT16      sAPCost = 0;
1480 	INT8       bAttachPos;
1481 	OBJECTTYPE Launchable;
1482 	OBJECTTYPE *pObj;
1483 	UINT16     usItemNum;
1484 
1485 	usItemNum = pSoldier->usAttackingWeapon;
1486 
1487 	if ( !EnoughAmmo( pSoldier, TRUE, pSoldier->ubAttackingHand ) )
1488 	{
1489 		return( FALSE );
1490 	}
1491 
1492 	pObj = &(pSoldier->inv[HANDPOS]);
1493 	for (bAttachPos = 0; bAttachPos < MAX_ATTACHMENTS; bAttachPos++)
1494 	{
1495 		if (pObj->usAttachItem[ bAttachPos ] != NOTHING)
1496 		{
1497 			if ( GCM->getItem(pObj->usAttachItem[ bAttachPos ])->isExplosive() )
1498 			{
1499 				break;
1500 			}
1501 		}
1502 	}
1503 	if (bAttachPos == MAX_ATTACHMENTS)
1504 	{
1505 		// this should not happen!!
1506 		return( FALSE );
1507 	}
1508 
1509 	CreateItem( pObj->usAttachItem[ bAttachPos ],	pObj->bAttachStatus[ bAttachPos ], &Launchable );
1510 
1511 	if ( pSoldier->usAttackingWeapon == pObj->usItem)
1512 	{
1513 		DeductAmmo( pSoldier, HANDPOS );
1514 	}
1515 	else
1516 	{
1517 		// Firing an attached grenade launcher... the attachment we found above
1518 		// is the one to remove!
1519 		RemoveAttachment( pObj, bAttachPos, NULL );
1520 	}
1521 
1522 	// ATE: Check here if the launcher should fail 'cause of bad status.....
1523 	if ( WillExplosiveWeaponFail( pSoldier, pObj ) )
1524 	{
1525 		// Explode dude!
1526 
1527 		// So we still should have ABC > 0
1528 		// Begin explosion due to failure...
1529 		IgniteExplosion(pSoldier, 0, pSoldier->sGridNo, Launchable.usItem, pSoldier->bLevel);
1530 
1531 		// Reduce again for attack end 'cause it has been incremented for a normal attack
1532 		SLOGD(
1533 			"Freeing up attacker - ATTACK ANIMATION %s ENDED BY BAD EXPLOSIVE CHECK, Now %d",
1534 			gAnimControl[pSoldier->usAnimState].zAnimStr, gTacticalStatus.ubAttackBusyCount);
1535 		ReduceAttackBusyCount(pSoldier, FALSE);
1536 
1537 		// So all's well, should be good from here....
1538 		return( FALSE );
1539 	}
1540 
1541 
1542 	if ( GCM->getWeapon( usItemNum )->hasSound()  )
1543 	{
1544 		PlayLocationJA2Sample(pSoldier->sGridNo, GCM->getWeapon(usItemNum)->sound, HIGHVOLUME, 1);
1545 	}
1546 
1547 	uiHitChance = CalcThrownChanceToHit( pSoldier, sTargetGridNo, pSoldier->bAimTime, AIM_SHOT_TORSO );
1548 
1549 	uiDiceRoll = PreRandom( 100 );
1550 
1551 	if ( GCM->getItem(usItemNum)->getItemClass() == IC_LAUNCHER )
1552 	{
1553 		// Preserve gridno!
1554 		//pSoldier->sLastTarget = sTargetGridNo;
1555 
1556 		sAPCost = MinAPsToAttack( pSoldier, sTargetGridNo, TRUE );
1557 	}
1558 	else
1559 	{
1560 		// Throw....
1561 		sAPCost = MinAPsToThrow(*pSoldier, sTargetGridNo, FALSE);
1562 	}
1563 
1564 	DeductPoints( pSoldier, sAPCost, 0 );
1565 
1566 	CalculateLaunchItemParamsForThrow( pSoldier, pSoldier->sTargetGridNo, pSoldier->bTargetLevel, 0, &Launchable, (INT8)(uiDiceRoll - uiHitChance), THROW_ARM_ITEM, 0 );
1567 
1568 	const THROW_PARAMS* const t = pSoldier->pThrowParams;
1569 	CreatePhysicalObject(pSoldier->pTempObject, t->dLifeSpan, t->dX, t->dY, t->dZ, t->dForceX, t->dForceY, t->dForceZ, pSoldier, t->ubActionCode, t->target);
1570 
1571 	delete pSoldier->pTempObject;
1572 	pSoldier->pTempObject = NULL;
1573 
1574 	delete pSoldier->pThrowParams;
1575 	pSoldier->pThrowParams = NULL;
1576 
1577 	return( TRUE );
1578 }
1579 
1580 
DoSpecialEffectAmmoMiss(SOLDIERTYPE * const attacker,const INT16 sGridNo,const INT16 sXPos,const INT16 sYPos,const INT16 sZPos,const BOOLEAN fSoundOnly,const BOOLEAN fFreeupAttacker,BULLET * const bullet)1581 static BOOLEAN DoSpecialEffectAmmoMiss(SOLDIERTYPE* const attacker, const INT16 sGridNo, const INT16 sXPos, const INT16 sYPos, const INT16 sZPos, const BOOLEAN fSoundOnly, const BOOLEAN fFreeupAttacker, BULLET* const bullet)
1582 {
1583 	ANITILE_PARAMS AniParams;
1584 	UINT8          ubAmmoType;
1585 	UINT16         usItem;
1586 
1587 	ubAmmoType = attacker->inv[attacker->ubAttackingHand].ubGunAmmoType;
1588 	usItem     = attacker->inv[attacker->ubAttackingHand].usItem;
1589 
1590 	AniParams = ANITILE_PARAMS{};
1591 
1592 	if ( ubAmmoType == AMMO_HE || ubAmmoType == AMMO_HEAT )
1593 	{
1594 		if ( !fSoundOnly )
1595 		{
1596 			AniParams.sGridNo = sGridNo;
1597 			AniParams.ubLevelID = ANI_TOPMOST_LEVEL;
1598 			AniParams.sDelay = (INT16)( 100 );
1599 			AniParams.sStartFrame = 0;
1600 			AniParams.uiFlags = ANITILE_FORWARD | ANITILE_ALWAYS_TRANSLUCENT;
1601 			AniParams.sX = sXPos;
1602 			AniParams.sY = sYPos;
1603 			AniParams.sZ = sZPos;
1604 			AniParams.zCachedFile = TILECACHEDIR "/miniboom.sti";
1605 			CreateAnimationTile( &AniParams );
1606 
1607 			if ( fFreeupAttacker )
1608 			{
1609 				if (bullet) RemoveBullet(bullet);
1610 				SLOGD("Freeing up attacker - bullet hit structure - explosive ammo");
1611 				FreeUpAttacker(attacker);
1612 			}
1613 		}
1614 
1615 		if ( sGridNo != NOWHERE )
1616 		{
1617 			PlayLocationJA2Sample(sGridNo, SMALL_EXPLODE_1, HIGHVOLUME, 1);
1618 		}
1619 		else
1620 		{
1621 			PlayJA2Sample(SMALL_EXPLODE_1, MIDVOLUME, 1, MIDDLE);
1622 		}
1623 
1624 		return( TRUE );
1625 	}
1626 	else if (ubAmmoType == AMMO_MONSTER)
1627 	{
1628 		UINT16 gas = GCM->getWeapon(usItem)->usSmokeEffect;
1629 		if (gas == NONE)
1630 		{
1631 			return FALSE;
1632 		}
1633 
1634 		// Increment attack busy...
1635 		// gTacticalStatus.ubAttackBusyCount++;
1636 		// SLOGD("Incrementing Attack: Explosion gone off, COunt now %d", gTacticalStatus.ubAttackBusyCount);
1637 
1638 		PlayLocationJA2Sample(sGridNo, CREATURE_GAS_NOISE, HIGHVOLUME, 1);
1639 
1640 		NewSmokeEffect(sGridNo, gas, 0, attacker);
1641 	}
1642 
1643 	return( FALSE );
1644 }
1645 
1646 
WeaponHit(SOLDIERTYPE * const pTargetSoldier,const UINT16 usWeaponIndex,const INT16 sDamage,const INT16 sBreathLoss,const UINT16 usDirection,const INT16 sXPos,const INT16 sYPos,const INT16 sZPos,const INT16 sRange,SOLDIERTYPE * const attacker,const UINT8 ubSpecial,const UINT8 ubHitLocation)1647 void WeaponHit(SOLDIERTYPE* const pTargetSoldier, const UINT16 usWeaponIndex, const INT16 sDamage,
1648 		const INT16 sBreathLoss, const UINT16 usDirection, const INT16 sXPos, const INT16 sYPos,
1649 		const INT16 sZPos, const INT16 sRange, SOLDIERTYPE* const attacker, const UINT8 ubSpecial,
1650 		const UINT8 ubHitLocation)
1651 {
1652 	MakeNoise(attacker, pTargetSoldier->sGridNo, pTargetSoldier->bLevel,
1653 			GCM->getWeapon(usWeaponIndex)->ubHitVolume, NOISE_BULLET_IMPACT);
1654 
1655 	if ( EXPLOSIVE_GUN( usWeaponIndex ) )
1656 	{
1657 		// Reduce attacker count!
1658 		const UINT16 item = (usWeaponIndex == ROCKET_LAUNCHER ? C1 : TANK_SHELL);
1659 		IgniteExplosionXY(attacker, sXPos, sYPos, 0, GETWORLDINDEXFROMWORLDCOORDS(sYPos, sXPos), item, pTargetSoldier->bLevel);
1660 
1661 		SLOGD("Freeing up attacker - end of LAW fire");
1662 		FreeUpAttacker(attacker);
1663 		return;
1664 	}
1665 
1666 	DoSpecialEffectAmmoMiss(attacker, pTargetSoldier->sGridNo, sXPos, sYPos, sZPos, FALSE, FALSE, NULL);
1667 
1668 	// OK, SHOT HAS HIT, DO THINGS APPROPRIATELY
1669 	// ATE: This is 'cause of that darn smoke effect that could potnetially kill
1670 	// the poor bastard .. so check
1671 	if ( !pTargetSoldier->fDoingExternalDeath )
1672 	{
1673 		EVENT_SoldierGotHit(pTargetSoldier, usWeaponIndex, sDamage, sBreathLoss,
1674 					usDirection, sRange, attacker, ubSpecial, ubHitLocation, NOWHERE);
1675 	}
1676 	else
1677 	{
1678 		// Buddy had died from additional dammage - free up attacker here...
1679 		ReduceAttackBusyCount(pTargetSoldier->attacker, FALSE);
1680 		SLOGD("Special effect killed before bullet impact, attack count now %d",
1681 			gTacticalStatus.ubAttackBusyCount);
1682 	}
1683 }
1684 
1685 
StructureHit(BULLET * const pBullet,const INT16 sXPos,const INT16 sYPos,const INT16 sZPos,const UINT16 usStructureID,const INT32 iImpact,const BOOLEAN fStopped)1686 void StructureHit(BULLET* const pBullet, const INT16 sXPos, const INT16 sYPos, const INT16 sZPos, const UINT16 usStructureID, const INT32 iImpact, const BOOLEAN fStopped)
1687 {
1688 	BOOLEAN        fDoMissForGun = FALSE;
1689 	ANITILE        *pNode;
1690 	INT16          sGridNo;
1691 	ANITILE_PARAMS AniParams;
1692 	UINT32         uiMissVolume = MIDVOLUME;
1693 
1694 	SOLDIERTYPE* const attacker      = pBullet->pFirer;
1695 	const UINT16       usWeaponIndex = attacker->usAttackingWeapon;
1696 	const INT8         bWeaponStatus = pBullet->ubItemStatus;
1697 
1698 	if (fStopped)
1699 	{
1700 		// AGILITY GAIN: Opponent "dodged" a bullet shot at him (it missed)
1701 		SOLDIERTYPE* const opp = attacker->opponent;
1702 		if (opp != NULL) AgilityForEnemyMissingPlayer(attacker, opp, 5);
1703 	}
1704 
1705 	const BOOLEAN fHitSameStructureAsBefore = (usStructureID == pBullet->usLastStructureHit);
1706 
1707 	sGridNo = MAPROWCOLTOPOS( (sYPos/CELL_Y_SIZE), (sXPos/CELL_X_SIZE) );
1708 	if ( !fHitSameStructureAsBefore )
1709 	{
1710 		const INT8 level = (sZPos > WALL_HEIGHT ? 1 : 0);
1711 		MakeNoise(attacker, sGridNo, level, GCM->getWeapon(usWeaponIndex)->ubHitVolume, NOISE_BULLET_IMPACT);
1712 	}
1713 
1714 	if (fStopped)
1715 	{
1716 		if ( usWeaponIndex == ROCKET_LAUNCHER )
1717 		{
1718 			RemoveBullet(pBullet);
1719 
1720 			// Reduce attacker count!
1721 			SLOGD("Freeing up attacker - end of LAW fire");
1722 			FreeUpAttacker(attacker);
1723 
1724 			IgniteExplosion(attacker, 0, sGridNo, C1, sZPos >= WALL_HEIGHT);
1725 			//FreeUpAttacker(attacker);
1726 
1727 			return;
1728 		}
1729 
1730 		if ( usWeaponIndex == TANK_CANNON )
1731 		{
1732 			RemoveBullet(pBullet);
1733 
1734 			// Reduce attacker count!
1735 			SLOGD("Freeing up attacker - end of TANK fire");
1736 			FreeUpAttacker(attacker);
1737 
1738 			IgniteExplosion(attacker, 0, sGridNo, TANK_SHELL, sZPos >= WALL_HEIGHT);
1739 			//FreeUpAttacker(attacker);
1740 
1741 			return;
1742 		}
1743 	}
1744 
1745 	// Get Structure pointer and damage it!
1746 	if ( usStructureID != INVALID_STRUCTURE_ID )
1747 	{
1748 		STRUCTURE* const pStructure = FindStructureByID(sGridNo, usStructureID);
1749 		DamageStructure(pStructure, iImpact, STRUCTURE_DAMAGE_GUNFIRE, sGridNo, sXPos, sYPos, attacker);
1750 	}
1751 
1752 	switch(  GCM->getWeapon( usWeaponIndex )->ubWeaponClass )
1753 	{
1754 		case HANDGUNCLASS:
1755 		case RIFLECLASS:
1756 		case SHOTGUNCLASS:
1757 		case SMGCLASS:
1758 		case MGCLASS:
1759 			// Guy has missed, play random sound
1760 			if (attacker->bTeam == OUR_TEAM &&
1761 				!attacker->bDoBurst &&
1762 				Random(40) == 0)
1763 			{
1764 				DoMercBattleSound(attacker, BATTLE_SOUND_CURSE1);
1765 			}
1766 			//fDoMissForGun = TRUE;
1767 			//break;
1768 			fDoMissForGun = TRUE;
1769 			break;
1770 
1771 		case MONSTERCLASS:
1772 			DoSpecialEffectAmmoMiss(attacker, sGridNo, sXPos, sYPos, sZPos, FALSE, TRUE, pBullet);
1773 
1774 			RemoveBullet(pBullet);
1775 			SLOGD("Freeing up attacker - monster attack hit structure");
1776 			FreeUpAttacker(attacker);
1777 
1778 			//PlayJA2Sample(SPIT_RICOCHET, uiMissVolume, 1, SoundDir(sGridNo));
1779 			break;
1780 
1781 		case KNIFECLASS:
1782 
1783 			// When it hits the ground, leave on map...
1784 			if ( GCM->getItem(usWeaponIndex)->getItemClass() == IC_THROWING_KNIFE )
1785 			{
1786 				OBJECTTYPE Object;
1787 
1788 				// OK, have we hit ground?
1789 				if ( usStructureID == INVALID_STRUCTURE_ID )
1790 				{
1791 					// Add item
1792 					CreateItem( THROWING_KNIFE, bWeaponStatus, &Object );
1793 
1794 					AddItemToPool(sGridNo, &Object, INVISIBLE, 0, 0, -1);
1795 
1796 					// Make team look for items
1797 					NotifySoldiersToLookforItems( );
1798 				}
1799 
1800 				if ( !fHitSameStructureAsBefore )
1801 				{
1802 					PlayJA2Sample(MISS_KNIFE, uiMissVolume, 1, SoundDir(sGridNo));
1803 				}
1804 
1805 				RemoveBullet(pBullet);
1806 				SLOGD("Freeing up attacker - knife attack hit structure");
1807 				FreeUpAttacker(attacker);
1808 			}
1809 	}
1810 
1811 	if ( fDoMissForGun )
1812 	{
1813 		// OK, are we a shotgun, if so , make sounds lower...
1814 		if ( GCM->getWeapon( usWeaponIndex )->ubWeaponClass == SHOTGUNCLASS )
1815 		{
1816 			uiMissVolume = LOWVOLUME;
1817 		}
1818 
1819 		// Free guy!
1820 		//SLOGD("Freeing up attacker - bullet hit structure");
1821 		//FreeUpAttacker(attacker);
1822 
1823 
1824 		// PLAY SOUND AND FLING DEBRIS
1825 		// RANDOMIZE SOUND SYSTEM
1826 
1827 		// IF WE HIT THE GROUND
1828 
1829 		if ( fHitSameStructureAsBefore )
1830 		{
1831 			if ( fStopped )
1832 			{
1833 				RemoveBullet(pBullet);
1834 				SLOGD("Freeing up attacker - bullet hit same structure twice");
1835 				FreeUpAttacker(attacker);
1836 			}
1837 		}
1838 		else
1839 		{
1840 			if (!fStopped || !DoSpecialEffectAmmoMiss(attacker, sGridNo, sXPos, sYPos, sZPos, FALSE, TRUE, pBullet))
1841 			{
1842 				if ( sZPos == 0 )
1843 				{
1844 					PlayJA2Sample(MISS_G2, uiMissVolume, 1, SoundDir(sGridNo));
1845 				}
1846 				else
1847 				{
1848 					PlayJA2Sample(SoundRange<MISS_1, MISS_8>(), uiMissVolume, 1, SoundDir(sGridNo));
1849 				}
1850 
1851 				// Default hit is the ground
1852 				UINT16 usMissTileIndex = FIRSTMISS1;
1853 
1854 				// Check if we are in water...
1855 				if ( gpWorldLevelData[ sGridNo ].ubTerrainID == LOW_WATER ||  gpWorldLevelData[ sGridNo ].ubTerrainID == DEEP_WATER )
1856 				{
1857 					usMissTileIndex = SECONDMISS1;
1858 
1859 					// Add ripple
1860 					AniParams = ANITILE_PARAMS{};
1861 					AniParams.sGridNo = sGridNo;
1862 					AniParams.ubLevelID = ANI_STRUCT_LEVEL;
1863 					AniParams.usTileIndex = THIRDMISS1;
1864 					AniParams.sDelay = 50;
1865 					AniParams.sStartFrame = 0;
1866 					AniParams.uiFlags = ANITILE_FORWARD;
1867 
1868 					pNode = CreateAnimationTile( &AniParams );
1869 
1870 					// Adjust for absolute positioning
1871 					pNode->pLevelNode->uiFlags |= LEVELNODE_USEABSOLUTEPOS;
1872 					pNode->pLevelNode->sRelativeX	= sXPos;
1873 					pNode->pLevelNode->sRelativeY	= sYPos;
1874 					pNode->pLevelNode->sRelativeZ = sZPos;
1875 
1876 				}
1877 
1878 				AniParams = ANITILE_PARAMS{};
1879 				AniParams.sGridNo = sGridNo;
1880 				AniParams.ubLevelID = ANI_STRUCT_LEVEL;
1881 				AniParams.usTileIndex = usMissTileIndex;
1882 				AniParams.sDelay = 80;
1883 				AniParams.sStartFrame = 0;
1884 				if (fStopped)
1885 				{
1886 					AniParams.uiFlags = ANITILE_FORWARD | ANITILE_RELEASE_ATTACKER_WHEN_DONE;
1887 					AniParams.v.bullet = pBullet;
1888 				}
1889 				else
1890 				{
1891 					AniParams.uiFlags = ANITILE_FORWARD;
1892 				}
1893 
1894 				pNode = CreateAnimationTile( &AniParams );
1895 
1896 				// Adjust for absolute positioning
1897 				pNode->pLevelNode->uiFlags |= LEVELNODE_USEABSOLUTEPOS;
1898 				pNode->pLevelNode->sRelativeX = sXPos;
1899 				pNode->pLevelNode->sRelativeY = sYPos;
1900 				pNode->pLevelNode->sRelativeZ = sZPos;
1901 
1902 				// ATE: Show misses...( if our team )
1903 				if (gGameSettings.fOptions[TOPTION_SHOW_MISSES] &&
1904 					attacker->bTeam == OUR_TEAM)
1905 				{
1906 					LocateGridNo(sGridNo);
1907 				}
1908 			}
1909 
1910 			pBullet->usLastStructureHit = usStructureID;
1911 
1912 		}
1913 	}
1914 }
1915 
WindowHit(INT16 sGridNo,UINT16 usStructureID,BOOLEAN fBlowWindowSouth,BOOLEAN fLargeForce)1916 void WindowHit( INT16 sGridNo, UINT16 usStructureID, BOOLEAN fBlowWindowSouth, BOOLEAN fLargeForce )
1917 {
1918 	STRUCTURE      *pWallAndWindow;
1919 	DB_STRUCTURE   *pWallAndWindowInDB;
1920 	INT16          sShatterGridNo;
1921 	UINT16         usTileIndex;
1922 	ANITILE_PARAMS AniParams;
1923 
1924 
1925 	// ATE: Make large force always for now ( feel thing )
1926 	fLargeForce = TRUE;
1927 
1928 	// we have to do two things here: swap the window structure
1929 	// (right now just using the partner stuff in a chain from
1930 	// intact to cracked to shattered) and display the
1931 	// animation if we've reached shattered
1932 
1933 	// find the wall structure, and go one length along the chain
1934 	pWallAndWindow = FindStructureByID( sGridNo, usStructureID );
1935 	if (pWallAndWindow == NULL)
1936 	{
1937 		return;
1938 	}
1939 
1940 	pWallAndWindow = SwapStructureForPartner(pWallAndWindow);
1941 	if (pWallAndWindow == NULL)
1942 	{
1943 		return;
1944 	}
1945 
1946 	// record window smash
1947 	AddWindowHitToMapTempFile( sGridNo );
1948 
1949 	pWallAndWindowInDB = pWallAndWindow->pDBStructureRef->pDBStructure;
1950 
1951 	if ( fLargeForce )
1952 	{
1953 		// Force to destruction animation!
1954 		if (pWallAndWindowInDB->bPartnerDelta != NO_PARTNER_STRUCTURE  )
1955 		{
1956 			pWallAndWindow = SwapStructureForPartner(pWallAndWindow);
1957 			if ( pWallAndWindow )
1958 			{
1959 				// record 2nd window smash
1960 				AddWindowHitToMapTempFile( sGridNo );
1961 
1962 				pWallAndWindowInDB = pWallAndWindow->pDBStructureRef->pDBStructure;
1963 			}
1964 		}
1965 	}
1966 
1967 	SetRenderFlags( RENDER_FLAG_FULL );
1968 
1969 	if (pWallAndWindowInDB->ubArmour == MATERIAL_THICKER_METAL_WITH_SCREEN_WINDOWS)
1970 	{
1971 		// don't play any sort of animation or sound
1972 		return;
1973 	}
1974 
1975 	if (pWallAndWindowInDB->bPartnerDelta != NO_PARTNER_STRUCTURE  )
1976 	{ // just cracked; don't display the animation
1977 		MakeNoise(NULL, sGridNo, 0, WINDOW_CRACK_VOLUME, NOISE_BULLET_IMPACT);
1978 		return;
1979 	}
1980 	MakeNoise(NULL, sGridNo, 0, WINDOW_SMASH_VOLUME, NOISE_BULLET_IMPACT);
1981 	if (pWallAndWindowInDB->ubWallOrientation == INSIDE_TOP_RIGHT || pWallAndWindowInDB->ubWallOrientation == OUTSIDE_TOP_RIGHT)
1982 	{
1983 		/*
1984 		sShatterGridNo = sGridNo + 1;
1985 		// check for wrapping around edge of map
1986 		if (sShatterGridNo % WORLD_COLS == 0)
1987 		{
1988 			// in which case we don't play the animation!
1989 			return;
1990 		}*/
1991 		if (fBlowWindowSouth)
1992 		{
1993 			usTileIndex = WINDOWSHATTER1;
1994 			sShatterGridNo = sGridNo + 1;
1995 		}
1996 		else
1997 		{
1998 			usTileIndex = WINDOWSHATTER11;
1999 			sShatterGridNo = sGridNo;
2000 		}
2001 
2002 	}
2003 	else
2004 	{
2005 		/*
2006 		sShatterGridNo = sGridNo + WORLD_COLS;
2007 		// check for wrapping around edge of map
2008 		if (sShatterGridNo % WORLD_ROWS == 0)
2009 		{
2010 			// in which case we don't play the animation!
2011 			return;
2012 		}*/
2013 		if (fBlowWindowSouth)
2014 		{
2015 			usTileIndex = WINDOWSHATTER6;
2016 			sShatterGridNo = sGridNo + WORLD_COLS;
2017 		}
2018 		else
2019 		{
2020 			usTileIndex = WINDOWSHATTER16;
2021 			sShatterGridNo = sGridNo;
2022 		}
2023 	}
2024 
2025 	AniParams = ANITILE_PARAMS{};
2026 	AniParams.sGridNo = sShatterGridNo;
2027 	AniParams.ubLevelID = ANI_STRUCT_LEVEL;
2028 	AniParams.usTileIndex = usTileIndex;
2029 	AniParams.sDelay = 50;
2030 	AniParams.sStartFrame = 0;
2031 	AniParams.uiFlags = ANITILE_FORWARD;
2032 
2033 	CreateAnimationTile( &AniParams );
2034 
2035 	PlayJA2Sample(SoundRange<GLASS_SHATTER1, GLASS_SHATTER2>(), MIDVOLUME, 1, SoundDir(sGridNo));
2036 }
2037 
2038 
InRange(const SOLDIERTYPE * pSoldier,INT16 sGridNo)2039 BOOLEAN InRange(const SOLDIERTYPE* pSoldier, INT16 sGridNo)
2040 {
2041 	INT16  sRange;
2042 	UINT16 usInHand;
2043 
2044 	usInHand = pSoldier->inv[HANDPOS].usItem;
2045 
2046 	if ( GCM->getItem(usInHand)->getItemClass() == IC_GUN ||
2047 		GCM->getItem(usInHand)->getItemClass() == IC_THROWING_KNIFE  )
2048 	{
2049 		// Determine range
2050 		sRange = (INT16)GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, sGridNo );
2051 
2052 		if ( GCM->getItem(usInHand)->getItemClass() == IC_THROWING_KNIFE )
2053 		{
2054 			// NB CalcMaxTossRange returns range in tiles, not in world units
2055 			if ( sRange <= CalcMaxTossRange( pSoldier, THROWING_KNIFE, TRUE ) * CELL_X_SIZE )
2056 			{
2057 				return( TRUE );
2058 			}
2059 		}
2060 		else
2061 		{
2062 			// For given weapon, check range
2063 			if (sRange <= GunRange(pSoldier->inv[HANDPOS]))
2064 			{
2065 				return( TRUE );
2066 			}
2067 		}
2068 	}
2069 	return( FALSE );
2070 }
2071 
CalcChanceToHitGun(SOLDIERTYPE * pSoldier,UINT16 sGridNo,UINT8 ubAimTime,UINT8 ubAimPos,BOOLEAN fModify)2072 UINT32 CalcChanceToHitGun(SOLDIERTYPE *pSoldier, UINT16 sGridNo, UINT8 ubAimTime, UINT8 ubAimPos, BOOLEAN fModify )
2073 {
2074 	INT32 iChance, iRange, iSightRange, iMaxRange, iScopeBonus, iBonus; //, minRange;
2075 	INT32 iGunCondition, iMarksmanship;
2076 	INT32 iPenalty;
2077 	UINT16 usInHand;
2078 	OBJECTTYPE *pInHand;
2079 	INT8 bAttachPos;
2080 	INT8 bBandaged;
2081 	INT16 sDistVis;
2082 	UINT8 ubAdjAimPos;
2083 
2084 	if ( pSoldier->bMarksmanship == 0 )
2085 	{
2086 		// always min chance
2087 		return( MINCHANCETOHIT );
2088 	}
2089 
2090 	// make sure the guy's actually got a weapon in his hand!
2091 	pInHand = &(pSoldier->inv[pSoldier->ubAttackingHand]);
2092 	usInHand = pSoldier->usAttackingWeapon;
2093 
2094 	// DETERMINE BASE CHANCE OF HITTING
2095 	iGunCondition = WEAPON_STATUS_MOD( pInHand->bGunStatus );
2096 
2097 	if (usInHand == ROCKET_LAUNCHER)
2098 	{
2099 		// use the same calculation as for mechanical thrown weapons
2100 		iMarksmanship = ( EffectiveDexterity( pSoldier ) + EffectiveMarksmanship( pSoldier ) + EffectiveWisdom( pSoldier ) + (10 * EffectiveExpLevel( pSoldier ) )) / 4;
2101 		// heavy weapons trait helps out
2102 		iMarksmanship += gbSkillTraitBonus[HEAVY_WEAPS] * NUM_SKILL_TRAITS(pSoldier, HEAVY_WEAPS);
2103 	}
2104 	else
2105 	{
2106 		iMarksmanship = EffectiveMarksmanship( pSoldier );
2107 
2108 		if ( AM_A_ROBOT( pSoldier ) )
2109 		{
2110 			const SOLDIERTYPE * pSoldier2;
2111 
2112 			pSoldier2 = GetRobotController( pSoldier );
2113 			if ( pSoldier2 )
2114 			{
2115 				iMarksmanship = __max( iMarksmanship, EffectiveMarksmanship( pSoldier2 ) );
2116 			}
2117 		}
2118 	}
2119 
2120 	// modify chance to hit by morale
2121 	iMarksmanship += GetMoraleModifier( pSoldier );
2122 
2123 	// penalize marksmanship for fatigue
2124 	iMarksmanship -= GetSkillCheckPenaltyForFatigue( pSoldier, iMarksmanship );
2125 
2126 	if (iGunCondition >= iMarksmanship)
2127 		// base chance is equal to the shooter's marksmanship skill
2128 		iChance = iMarksmanship;
2129 	else
2130 		// base chance is equal to the average of marksmanship & gun's condition!
2131 		iChance = (iMarksmanship + iGunCondition) / 2;
2132 
2133 	// if shooting same target as the last shot
2134 	if (sGridNo == pSoldier->sLastTarget )
2135 		iChance += AIM_BONUS_SAME_TARGET; // give a bonus to hit
2136 
2137 	if ( pSoldier->ubProfile != NO_PROFILE && gMercProfiles[ pSoldier->ubProfile ].bPersonalityTrait == PSYCHO )
2138 	{
2139 		iChance += AIM_BONUS_PSYCHO;
2140 	}
2141 
2142 	// calculate actual range (in units, 10 units = 1 tile)
2143 	iRange = GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, sGridNo );
2144 
2145 	// if shooter is crouched, he aims slightly better (to max of AIM_BONUS_CROUCHING)
2146 	if ( gAnimControl[ pSoldier->usAnimState ].ubEndHeight == ANIM_CROUCH )
2147 	{
2148 		iBonus = iRange / 10;
2149 		if (iBonus > AIM_BONUS_CROUCHING)
2150 		{
2151 			iBonus = AIM_BONUS_CROUCHING;
2152 		}
2153 		iChance += iBonus;
2154 	}
2155 	// if shooter is prone, he aims even better, except at really close range
2156 	else if ( gAnimControl[ pSoldier->usAnimState ].ubEndHeight == ANIM_PRONE )
2157 	{
2158 		if (iRange > MIN_PRONE_RANGE)
2159 		{
2160 			iBonus = iRange / 10;
2161 			if (iBonus > AIM_BONUS_PRONE)
2162 			{
2163 				iBonus = AIM_BONUS_PRONE;
2164 			}
2165 			bAttachPos = FindAttachment( pInHand, BIPOD );
2166 			if (bAttachPos != ITEM_NOT_FOUND)
2167 			{
2168 				// extra bonus to hit for a bipod, up to half the prone bonus itself
2169 				iBonus += (iBonus * WEAPON_STATUS_MOD(pInHand->bAttachStatus[bAttachPos]) / 100) / 2;
2170 			}
2171 			iChance += iBonus;
2172 		}
2173 	}
2174 
2175 	if (GCM->getItem(usInHand)->isWeapon() && !(GCM->getItem(usInHand)->isTwoHanded()))
2176 	{
2177 		// SMGs are treated as pistols for these purpose except there is a -5 penalty;
2178 		if (GCM->getWeapon(usInHand)->ubWeaponClass == SMGCLASS)
2179 		{
2180 			iChance -= AIM_PENALTY_SMG; // TODO0007
2181 		}
2182 
2183 		/*
2184 		if (pSoldier->inv[SECONDHANDPOS].usItem == NOTHING)
2185 		{
2186 			// firing with pistol in right hand, and second hand empty.
2187 			iChance += AIM_BONUS_TWO_HANDED_PISTOL;
2188 		}
2189 		else */
2190 		if ( !HAS_SKILL_TRAIT( pSoldier, AMBIDEXT ) )
2191 		{
2192 			if ( IsValidSecondHandShot( pSoldier ) )
2193 			{
2194 				// penalty to aim when firing two pistols
2195 				iChance -= AIM_PENALTY_DUAL_PISTOLS;
2196 			}
2197 			/*
2198 			else
2199 			{
2200 				// penalty to aim with pistol being fired one-handed
2201 				iChance -= AIM_PENALTY_ONE_HANDED_PISTOL;
2202 			}
2203 			*/
2204 		}
2205 	}
2206 
2207 	// If in burst mode, deduct points for change to hit for each shot after the first
2208 	if ( pSoldier->bDoBurst )
2209 	{
2210 		iPenalty = GCM->getWeapon(usInHand)->ubBurstPenalty * (pSoldier->bDoBurst - 1);
2211 
2212 		// halve the penalty for people with the autofire trait
2213 		UINT AutoWeaponsSkill = NUM_SKILL_TRAITS(pSoldier, AUTO_WEAPS);
2214 		if (AutoWeaponsSkill != 0)
2215 		{
2216 			iPenalty /= 2 * AutoWeaponsSkill;
2217 		}
2218 		iChance -= iPenalty;
2219 	}
2220 
2221 	sDistVis = DistanceVisible( pSoldier, DIRECTION_IRRELEVANT, DIRECTION_IRRELEVANT, sGridNo, 0 );
2222 
2223 	// give some leeway to allow people to spot for each other...
2224 	// use distance limitation for LOS routine of 2 x maximum distance EVER visible, so that we get accurate
2225 	// calculations out to around 50 tiles.  Because we multiply max distance by 2, we must divide by 2 later
2226 
2227 	// CJC August 13 2002:  Wow, this has been wrong the whole time.  bTargetCubeLevel seems to be generally set to 2 -
2228 	// but if a character is shooting at an enemy in a particular spot, then we should be using the target position on the body.
2229 
2230 	// CJC August 13, 2002
2231 	// If the start soldier has a body part they are aiming at, and know about the person in the tile, then use that height instead
2232 	iSightRange = -1;
2233 
2234 	// best to use team knowledge as well, in case of spotting for someone else
2235 	const SOLDIERTYPE* const tgt = WhoIsThere2(sGridNo, pSoldier->bTargetLevel);
2236 	if (tgt != NULL && (pSoldier->bOppList[tgt->ubID] == SEEN_CURRENTLY || gbPublicOpplist[pSoldier->bTeam][tgt->ubID] == SEEN_CURRENTLY))
2237 	{
2238 		iSightRange = SoldierToBodyPartLineOfSightTest(pSoldier, sGridNo, pSoldier->bTargetLevel,
2239 								pSoldier->bAimShotLocation,
2240 								(UINT8) (MaxDistanceVisible() * 2), TRUE);
2241 	}
2242 
2243 	if (iSightRange == -1) // didn't do a bodypart-based test
2244 	{
2245 		iSightRange = SoldierTo3DLocationLineOfSightTest(pSoldier, sGridNo, pSoldier->bTargetLevel,
2246 									pSoldier->bTargetCubeLevel,
2247 									(UINT8) (MaxDistanceVisible() * 2), TRUE);
2248 	}
2249 
2250 	iSightRange *= 2;
2251 
2252 	if ( iSightRange > (sDistVis * CELL_X_SIZE) )
2253 	{
2254 		// shooting beyond max normal vision... penalize such distance at double (also later we halve the remaining chance)
2255 		iSightRange += (iSightRange - sDistVis * CELL_X_SIZE);
2256 	}
2257 
2258 	// if shooter spent some extra time aiming and can see the target
2259 	if (iSightRange > 0 && ubAimTime && !pSoldier->bDoBurst)
2260 		iChance += (AIM_BONUS_PER_AP * ubAimTime); // bonus for every pt of aiming
2261 
2262 	if ( !(pSoldier->uiStatusFlags & SOLDIER_PC ) ) // if this is a computer AI controlled enemy
2263 	{
2264 		if ( gGameOptions.ubDifficultyLevel == DIF_LEVEL_EASY )
2265 		{
2266 			// On easy, penalize all enemies by 5%
2267 			iChance -= 5;
2268 		}
2269 		else
2270 		{
2271 			// max with 0 to prevent this being a bonus, for JA2 it's just a penalty to make early enemies easy
2272 			// CJC note: IDIOT!  This should have been a min.  It's kind of too late now...
2273 			// CJC 2002-05-17: changed the max to a min to make this work.
2274 			iChance += __min( 0, gbDiff[ DIFF_ENEMY_TO_HIT_MOD ][ SoldierDifficultyLevel( pSoldier ) ] );
2275 		}
2276 	}
2277 
2278 	// if shooter is being affected by gas
2279 	if ( pSoldier->uiStatusFlags & SOLDIER_GASSED )
2280 	{
2281 		iChance -= AIM_PENALTY_GASSED;
2282 	}
2283 
2284 	// if shooter is being bandaged at the same time, his concentration is off
2285 	if (pSoldier->ubServiceCount > 0)
2286 		iChance -= AIM_PENALTY_GETTINGAID;
2287 
2288 	// if shooter is still in shock
2289 	if (pSoldier->bShock)
2290 		iChance -= (pSoldier->bShock * AIM_PENALTY_PER_SHOCK);
2291 
2292 	if ( GCM->getItem(usInHand)->getItemClass() == IC_GUN )
2293 	{
2294 		bAttachPos = FindAttachment( pInHand, GUN_BARREL_EXTENDER );
2295 		if ( bAttachPos != ITEM_NOT_FOUND && fModify)
2296 		{
2297 			// reduce status and see if it falls off
2298 			pInHand->bAttachStatus[ bAttachPos ] -= (INT8) Random( 2 );
2299 
2300 			if ( pInHand->bAttachStatus[ bAttachPos ] - Random( 35 ) - Random( 35 ) < USABLE )
2301 			{
2302 				// barrel extender falls off!
2303 				OBJECTTYPE Temp;
2304 
2305 				// since barrel extenders are not removable we cannot call RemoveAttachment here
2306 				// and must create the item by hand
2307 				CreateItem( GUN_BARREL_EXTENDER, pInHand->bAttachStatus[ bAttachPos ], &Temp );
2308 				pInHand->usAttachItem[ bAttachPos ] = NOTHING;
2309 				pInHand->bAttachStatus[ bAttachPos ] = 0;
2310 
2311 				// drop it to ground
2312 				AddItemToPool(pSoldier->sGridNo, &Temp, VISIBLE, pSoldier->bLevel, 0, -1);
2313 
2314 				// big penalty to hit
2315 				iChance -= 30;
2316 
2317 				// curse!
2318 				if ( pSoldier->bTeam == OUR_TEAM )
2319 				{
2320 					DoMercBattleSound( pSoldier, BATTLE_SOUND_CURSE1 );
2321 
2322 					ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, st_format_printf(gzLateLocalizedString[STR_LATE_46], pSoldier->name));
2323 				}
2324 			}
2325 		}
2326 
2327 		iMaxRange = GunRange(*pInHand);
2328 	}
2329 	else
2330 	{
2331 		iMaxRange = CELL_X_SIZE; // one tile
2332 	}
2333 
2334 	if ( iSightRange > 0 )
2335 	{
2336 
2337 		if (IsWearingHeadGear(*pSoldier, SUNGOGGLES))
2338 		{
2339 			// decrease effective range by 10% when using sungoggles (w or w/o scope)
2340 			iSightRange -= iRange / 10; //basically, +1% to hit per every 2 squares
2341 		}
2342 
2343 		bAttachPos = FindAttachment( pInHand, SNIPERSCOPE );
2344 
2345 		// does gun have scope, long range recommends its use, and shooter's aiming?
2346 		if (bAttachPos != NO_SLOT && (iRange > MIN_SCOPE_RANGE) && (ubAimTime > 0))
2347 		{
2348 			// reduce effective sight range by 20% per extra aiming time AP of the distance
2349 			// beyond MIN_SCOPE_RANGE.  Max reduction is 80% of the range beyond.
2350 			iScopeBonus = ((SNIPERSCOPE_AIM_BONUS * ubAimTime) * (iRange - MIN_SCOPE_RANGE)) / 100;
2351 
2352 			// adjust for scope condition, only has full affect at 100%
2353 			iScopeBonus = (iScopeBonus * WEAPON_STATUS_MOD(pInHand->bAttachStatus[bAttachPos])) / 100;
2354 
2355 			// reduce effective range by the bonus obtained from the scope
2356 			iSightRange -= iScopeBonus;
2357 			if (iSightRange < 1)
2358 			{
2359 				iSightRange = 1;
2360 			}
2361 		}
2362 
2363 		bAttachPos = FindAttachment( pInHand, LASERSCOPE );
2364 		if (usInHand == ROCKET_RIFLE || usInHand == AUTO_ROCKET_RIFLE ||
2365 			bAttachPos != NO_SLOT) // rocket rifle has one built in
2366 		{
2367 			INT8 bLaserStatus;
2368 
2369 			if ( usInHand == ROCKET_RIFLE || usInHand == AUTO_ROCKET_RIFLE )
2370 			{
2371 				bLaserStatus = WEAPON_STATUS_MOD(pInHand->bGunStatus);
2372 			}
2373 			else
2374 			{
2375 				bLaserStatus = WEAPON_STATUS_MOD(pInHand->bAttachStatus[ bAttachPos ]);
2376 			}
2377 
2378 			// laser scope isn't of much use in high light levels; add something for that
2379 			if (bLaserStatus > 50)
2380 			{
2381 				iScopeBonus = LASERSCOPE_BONUS * (bLaserStatus - 50) / 50;
2382 			}
2383 			else
2384 			{
2385 				// laser scope in bad condition creates aim penalty!
2386 				iScopeBonus = - LASERSCOPE_BONUS * (50 - bLaserStatus) / 50;
2387 			}
2388 
2389 			iChance += iScopeBonus;
2390 
2391 		}
2392 
2393 	}
2394 
2395 	// if aiming at the head, reduce chance to hit
2396 	if (ubAimPos == AIM_SHOT_HEAD)
2397 	{
2398 		// penalty of 3% per tile
2399 		iPenalty = 3 * iSightRange / 10;
2400 		iChance -= iPenalty;
2401 	}
2402 	else if (ubAimPos == AIM_SHOT_LEGS)
2403 	{
2404 		// penalty of 1% per tile
2405 		iPenalty = iSightRange / 10;
2406 		iChance -= iPenalty;
2407 	}
2408 
2409 	// ADJUST FOR RANGE
2410 	// bonus if range is less than normal range, penalty if it's more
2411 	//iChance += (NORMAL_RANGE - iRange) / (CELL_X_SIZE / 5);	// 5% per tile
2412 
2413 	// Effects of actual gun max range... the numbers are based on wanting -40%
2414 	// at range 26for a pistol with range 13, and -0 for a sniper rifle with range 80
2415 	iPenalty = ((iMaxRange - iRange * 3) * 10) / (17 * CELL_X_SIZE);
2416 	if ( iPenalty < 0 )
2417 	{
2418 		iChance += iPenalty;
2419 	}
2420 	//iChance -= 20 * iRange / iMaxRange;
2421 
2422 	if ( TANK( pSoldier ) && ( iRange / CELL_X_SIZE < MaxDistanceVisible() ) )
2423 	{
2424 		// tank; penalize at close range!
2425 		// 2 percent per tile closer than max visible distance
2426 		iChance -= 2 * ( MaxDistanceVisible() - (iRange / CELL_X_SIZE) );
2427 	}
2428 
2429 	if (iSightRange == 0)
2430 	{
2431 		// firing blind!
2432 		iChance -= AIM_PENALTY_BLIND;
2433 	}
2434 	else
2435 	{
2436 		// Effects based on aiming & sight
2437 		// From for JA2.5:  3% bonus/penalty for each tile different from range NORMAL_RANGE.
2438 		// This doesn't provide a bigger bonus at close range, but stretches it out, making medium
2439 		// range less penalized, and longer range more penalized
2440 		iChance += 3 * ( NORMAL_RANGE - iSightRange ) / CELL_X_SIZE;
2441 		/*
2442 		if (iSightRange < NORMAL_RANGE)
2443 		{
2444 			// bonus to hit of 20% at point blank (would be 25% at range 0);
2445 			//at NORMAL_RANGE, bonus is 0
2446 			iChance += 25 * (NORMAL_RANGE - iSightRange) / NORMAL_RANGE;
2447 		}
2448 		else
2449 		{
2450 			// penalty of 2% / tile
2451 			iChance -= (iSightRange - NORMAL_RANGE) / 5;
2452 		}
2453 		*/
2454 	}
2455 
2456 	// adjust for roof/not on roof
2457 	if ( pSoldier->bLevel == 0 )
2458 	{
2459 		if ( pSoldier->bTargetLevel > 0 )
2460 		{
2461 			// penalty for firing up
2462 			iChance -= AIM_PENALTY_FIRING_UP;
2463 		}
2464 	}
2465 	else // pSoldier->bLevel > 0 )
2466 	{
2467 		if ( pSoldier->bTargetLevel == 0 )
2468 		{
2469 			iChance += AIM_BONUS_FIRING_DOWN;
2470 		}
2471 		// if have roof trait, give bonus
2472 		iChance += gbSkillTraitBonus[ONROOF] * NUM_SKILL_TRAITS(pSoldier, ONROOF);
2473 	}
2474 
2475 
2476 	const SOLDIERTYPE* const pTarget = WhoIsThere2(sGridNo, pSoldier->bTargetLevel);
2477 	if (pTarget != NULL)
2478 	{
2479 		// targeting a merc
2480 		// adjust for crouched/prone target
2481 		switch( gAnimControl[ pTarget->usAnimState ].ubHeight )
2482 		{
2483 			case ANIM_CROUCH:
2484 				if ( TANK( pSoldier ) && iRange < MIN_TANK_RANGE )
2485 				{
2486 					// 13% penalty per tile closer than min range
2487 					iChance -= 13 * ( ( MIN_TANK_RANGE - iRange ) / CELL_X_SIZE );
2488 				}
2489 				else
2490 				{
2491 					// at anything other than point-blank range
2492 					if (iRange > POINT_BLANK_RANGE + 10 * (AIM_PENALTY_TARGET_CROUCHED / 3) )
2493 					{
2494 						iChance -= AIM_PENALTY_TARGET_CROUCHED;
2495 					}
2496 					else if (iRange > POINT_BLANK_RANGE)
2497 					{
2498 						// at close range give same bonus as prone, up to maximum of AIM_PENALTY_TARGET_CROUCHED
2499 						iChance -= 3 * ((iRange - POINT_BLANK_RANGE) / CELL_X_SIZE); // penalty -3%/tile
2500 					}
2501 				}
2502 				break;
2503 			case ANIM_PRONE:
2504 				if ( TANK( pSoldier ) && iRange < MIN_TANK_RANGE )
2505 				{
2506 					// 25% penalty per tile closer than min range
2507 					iChance -= 25 * ( ( MIN_TANK_RANGE - iRange ) / CELL_X_SIZE );
2508 				}
2509 				else
2510 				{
2511 					// at anything other than point-blank range
2512 					if (iRange > POINT_BLANK_RANGE)
2513 					{
2514 						// reduce chance to hit with distance to the prone/immersed target
2515 						iPenalty = 3 * ((iRange - POINT_BLANK_RANGE) / CELL_X_SIZE); // penalty -3%/tile
2516 						iPenalty = __min( iPenalty, AIM_PENALTY_TARGET_PRONE );
2517 
2518 						iChance -= iPenalty;
2519 					}
2520 				}
2521 				break;
2522 			case ANIM_STAND:
2523 				// if we are prone and at close range, then penalize shots to the torso or head!
2524 				if ( iRange <= MIN_PRONE_RANGE && gAnimControl[ pSoldier->usAnimState ].ubEndHeight == ANIM_PRONE )
2525 				{
2526 					if ( ubAimPos == AIM_SHOT_RANDOM || ubAimPos == AIM_SHOT_GLAND )
2527 					{
2528 						ubAdjAimPos = AIM_SHOT_TORSO;
2529 					}
2530 					else
2531 					{
2532 						ubAdjAimPos = ubAimPos;
2533 					}
2534 					// lose 10% per height difference, lessened by distance
2535 					// e.g. 30% to aim at head at range 1, only 10% at range 3
2536 					// or 20% to aim at torso at range 1, no penalty at range 3
2537 					// NB torso aim position is 2, so (5-aimpos) is 3, for legs it's 2, for head 4
2538 					iChance -= (5 - ubAdjAimPos - iRange / CELL_X_SIZE) * 10;
2539 				}
2540 				break;
2541 			default:
2542 				break;
2543 		}
2544 
2545 		// penalty for amount that enemy has moved
2546 		iPenalty = __min( ((pTarget->bTilesMoved * 3) / 2), 30 );
2547 		iChance -= iPenalty;
2548 
2549 		// if target sees us, he may have a chance to dodge before the gun goes off
2550 		// but ability to dodge is reduced if crouched or prone!
2551 		if (pTarget->bOppList[pSoldier->ubID] == SEEN_CURRENTLY && !TANK( pTarget ) && !(pSoldier->ubBodyType != QUEENMONSTER) )
2552 		{
2553 			iPenalty = ( EffectiveAgility( pTarget ) / 5 + EffectiveExpLevel( pTarget ) * 2);
2554 			switch( gAnimControl[ pTarget->usAnimState ].ubHeight )
2555 			{
2556 				case ANIM_CROUCH:
2557 					iPenalty = iPenalty * 2 / 3;
2558 					break;
2559 				case ANIM_PRONE:
2560 					iPenalty /= 3;
2561 					break;
2562 			}
2563 
2564 			// reduce dodge ability by the attacker's stats
2565 			iBonus = ( EffectiveDexterity( pSoldier ) / 5 + EffectiveExpLevel( pSoldier ) * 2);
2566 			if ( TANK( pTarget ) || (pSoldier->ubBodyType != QUEENMONSTER) )
2567 			{
2568 				// reduce ability to track shots
2569 				iBonus = iBonus / 2;
2570 			}
2571 
2572 			if ( iPenalty > iBonus )
2573 			{
2574 				iChance -= (iPenalty - iBonus);
2575 			}
2576 		}
2577 	}
2578 	else if ( TANK( pSoldier ) && iRange < MIN_TANK_RANGE )
2579 	{
2580 		// 25% penalty per tile closer than min range
2581 		iChance -= 25 * ( ( MIN_TANK_RANGE - iRange ) / CELL_X_SIZE );
2582 	}
2583 
2584 	// IF CHANCE EXISTS, BUT SHOOTER IS INJURED
2585 	if ((iChance > 0) && (pSoldier->bLife < pSoldier->bLifeMax))
2586 	{
2587 		// if bandaged, give 1/2 of the bandaged life points back into equation
2588 		bBandaged = pSoldier->bLifeMax - pSoldier->bLife - pSoldier->bBleeding;
2589 
2590 		// injury penalty is based on % damage taken (max 2/3rds chance)
2591 		iPenalty = (iChance * 2 * (pSoldier->bLifeMax - pSoldier->bLife + (bBandaged / 2))) /
2592 						(3 * pSoldier->bLifeMax);
2593 
2594 		// reduce injury penalty due to merc's experience level (he can take it!)
2595 		iChance -= (iPenalty * (100 - (10 * ( EffectiveExpLevel( pSoldier ) - 1)))) / 100;
2596 	}
2597 
2598 	// IF CHANCE EXISTS, BUT SHOOTER IS LOW ON BREATH
2599 	if ((iChance > 0) && (pSoldier->bBreath < 100))
2600 	{
2601 		// breath penalty is based on % breath missing (max 1/2 chance)
2602 		iPenalty = (iChance * (100 - pSoldier->bBreath)) / 200;
2603 		// reduce breath penalty due to merc's dexterity (he can compensate!)
2604 		iChance -= (iPenalty * (100 - ( EffectiveDexterity( pSoldier ) - 10))) / 100;
2605 	}
2606 
2607 
2608 	// CHECK IF TARGET IS WITHIN GUN'S EFFECTIVE MAXIMUM RANGE
2609 	if ( iRange > iMaxRange )
2610 	{
2611 		// a bullet WILL travel that far if not blocked, but it's NOT accurate,
2612 		// because beyond maximum range, the bullet drops rapidly
2613 
2614 		// This won't cause the bullet to be off to the left or right, only make it
2615 		// drop in flight.
2616 		iChance /= 2;
2617 	}
2618 	if ( iSightRange > (sDistVis * CELL_X_SIZE) )
2619 	{
2620 		// penalize out of sight shots, cumulative to effective range penalty
2621 		iChance /= 2;
2622 	}
2623 
2624 	// MAKE SURE CHANCE TO HIT IS WITHIN DEFINED LIMITS
2625 	if (iChance < MINCHANCETOHIT)
2626 	{
2627 		if ( TANK( pSoldier ) )
2628 		{
2629 			// allow absolute minimums
2630 			iChance = 0;
2631 		}
2632 		else
2633 		{
2634 			iChance = MINCHANCETOHIT;
2635 		}
2636 	}
2637 	else
2638 	{
2639 		if (iChance > MAXCHANCETOHIT)
2640 			iChance = MAXCHANCETOHIT;
2641 	}
2642 
2643 	return (iChance);
2644 }
2645 
2646 
AICalcChanceToHitGun(SOLDIERTYPE * pSoldier,UINT16 sGridNo,UINT8 ubAimTime,UINT8 ubAimPos)2647 UINT32 AICalcChanceToHitGun(SOLDIERTYPE *pSoldier, UINT16 sGridNo, UINT8 ubAimTime, UINT8 ubAimPos )
2648 {
2649 	UINT16 usTrueState;
2650 	UINT32 uiChance;
2651 
2652 	// same as CCTHG but fakes the attacker always standing
2653 	usTrueState = pSoldier->usAnimState;
2654 	pSoldier->usAnimState = STANDING;
2655 	uiChance = CalcChanceToHitGun( pSoldier, sGridNo, ubAimTime, ubAimPos, false );
2656 	pSoldier->usAnimState = usTrueState;
2657 	return( uiChance );
2658 }
2659 
CalcBodyImpactReduction(UINT8 ubAmmoType,UINT8 ubHitLocation)2660 INT32 CalcBodyImpactReduction( UINT8 ubAmmoType, UINT8 ubHitLocation )
2661 {
2662 	// calculate how much bullets are slowed by passing through someone
2663 	INT32 iReduction = BodyImpactReduction[ubHitLocation];
2664 
2665 	switch (ubAmmoType)
2666 	{
2667 		case AMMO_HP:
2668 			iReduction = AMMO_ARMOUR_ADJUSTMENT_HP( iReduction );
2669 			break;
2670 		case AMMO_AP:
2671 		case AMMO_HEAT:
2672 			iReduction = AMMO_ARMOUR_ADJUSTMENT_AP( iReduction );
2673 			break;
2674 		case AMMO_SUPER_AP:
2675 			iReduction = AMMO_ARMOUR_ADJUSTMENT_SAP( iReduction );
2676 			break;
2677 		default:
2678 			break;
2679 	}
2680 	return( iReduction );
2681 }
2682 
2683 
ArmourProtection(SOLDIERTYPE const & pTarget,UINT8 const ubArmourType,INT8 * const pbStatus,INT32 const iImpact,UINT8 const ubAmmoType)2684 static INT32 ArmourProtection(SOLDIERTYPE const& pTarget, UINT8 const ubArmourType, INT8* const pbStatus, INT32 const iImpact, UINT8 const ubAmmoType)
2685 {
2686 	INT32 iProtection, iAppliedProtection, iFailure;
2687 
2688 	iProtection = Armour[ ubArmourType ].ubProtection;
2689 
2690 	if (!AM_A_ROBOT(&pTarget))
2691 	{
2692 		// check for the bullet hitting a weak spot in the armour
2693 		iFailure = PreRandom( 100 ) + 1 - *pbStatus;
2694 		if (iFailure > 0)
2695 		{
2696 			iProtection -= iFailure;
2697 			if (iProtection < 0)
2698 			{
2699 				return( 0 );
2700 			}
2701 		}
2702 	}
2703 
2704 	// adjust protection of armour due to different ammo types
2705 	switch (ubAmmoType)
2706 	{
2707 		case AMMO_HP:
2708 			iProtection = AMMO_ARMOUR_ADJUSTMENT_HP( iProtection );
2709 			break;
2710 		case AMMO_AP:
2711 		case AMMO_HEAT:
2712 			iProtection = AMMO_ARMOUR_ADJUSTMENT_AP( iProtection );
2713 			break;
2714 		case AMMO_SUPER_AP:
2715 			iProtection = AMMO_ARMOUR_ADJUSTMENT_SAP( iProtection );
2716 			break;
2717 		default:
2718 			break;
2719 	}
2720 
2721 	// figure out how much of the armour's protection value is necessary
2722 	// in defending against this bullet
2723 	if (iProtection > iImpact )
2724 	{
2725 		iAppliedProtection = iImpact;
2726 	}
2727 	else
2728 	{
2729 		// applied protection is the full strength of the armour, before AP/HP changes
2730 		iAppliedProtection = Armour[ ubArmourType ].ubProtection;
2731 	}
2732 
2733 	// reduce armour condition
2734 
2735 	if (ubAmmoType == AMMO_KNIFE || ubAmmoType == AMMO_SLEEP_DART)
2736 	{
2737 		// knives and darts damage armour but are not stopped by kevlar
2738 		if (Armour[ ubArmourType ].ubArmourClass == ARMOURCLASS_VEST ||
2739 			Armour[ ubArmourType ].ubArmourClass == ARMOURCLASS_LEGGINGS)
2740 		{
2741 			iProtection = 0;
2742 		}
2743 	}
2744 	else if (ubAmmoType == AMMO_MONSTER)
2745 	{
2746 		// creature spit damages armour a lot! an extra 3x for a total of 4x normal
2747 		*pbStatus -= 3 * (iAppliedProtection * Armour[ubArmourType].ubDegradePercent) / 100;
2748 
2749 		// reduce amount of protection from armour
2750 		iProtection /= 2;
2751 	}
2752 
2753 	if (!AM_A_ROBOT(&pTarget))
2754 	{
2755 		*pbStatus -= (iAppliedProtection * Armour[ubArmourType].ubDegradePercent) / 100;
2756 	}
2757 
2758 	// return armour protection
2759 	return( iProtection );
2760 }
2761 
2762 
TotalArmourProtection(SOLDIERTYPE & pTarget,const UINT8 ubHitLocation,const INT32 iImpact,const UINT8 ubAmmoType)2763 INT32 TotalArmourProtection(SOLDIERTYPE& pTarget, const UINT8 ubHitLocation, const INT32 iImpact, const UINT8 ubAmmoType)
2764 {
2765 	INT32      iTotalProtection = 0, iSlot;
2766 	OBJECTTYPE *pArmour;
2767 	INT8       bPlatePos = -1;
2768 
2769 	if (pTarget.uiStatusFlags & SOLDIER_VEHICLE)
2770 	{
2771 		INT8 bDummyStatus = 100;
2772 		iTotalProtection += ArmourProtection(pTarget, GetVehicleArmourType(pTarget.bVehicleID), &bDummyStatus, iImpact, ubAmmoType);
2773 	}
2774 	else
2775 	{
2776 		switch( ubHitLocation )
2777 		{
2778 			case AIM_SHOT_GLAND:
2779 				// creature hit in the glands!!! no armour there!
2780 				return( 0 );
2781 			case AIM_SHOT_HEAD:
2782 				iSlot = HELMETPOS;
2783 				break;
2784 			case AIM_SHOT_LEGS:
2785 				iSlot = LEGPOS;
2786 				break;
2787 			case AIM_SHOT_TORSO:
2788 			default:
2789 				iSlot = VESTPOS;
2790 				break;
2791 
2792 		}
2793 
2794 		pArmour = &pTarget.inv[iSlot];
2795 		if (pArmour->usItem != NOTHING)
2796 		{
2797 			// check plates first
2798 			if ( iSlot == VESTPOS )
2799 			{
2800 				bPlatePos = FindAttachment( pArmour, CERAMIC_PLATES );
2801 				if (bPlatePos != -1)
2802 				{
2803 					// bullet got through jacket; apply ceramic plate armour
2804 					iTotalProtection += ArmourProtection(pTarget, GCM->getItem(pArmour->usAttachItem[bPlatePos])->getClassIndex(), &(pArmour->bAttachStatus[bPlatePos]), iImpact, ubAmmoType);
2805 					if ( pArmour->bAttachStatus[bPlatePos] < USABLE )
2806 					{
2807 						// destroy plates!
2808 						pArmour->usAttachItem[ bPlatePos ] = NOTHING;
2809 						pArmour->bAttachStatus[ bPlatePos ] = 0;
2810 						DirtyMercPanelInterface(&pTarget, DIRTYLEVEL2);
2811 						if (pTarget.bTeam == OUR_TEAM)
2812 						{
2813 							// report plates destroyed!
2814 							ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, st_format_printf(str_ceramic_plates_smashed, pTarget.name));
2815 						}
2816 					}
2817 				}
2818 			}
2819 
2820 			// if the plate didn't stop the bullet...
2821 			if ( iImpact > iTotalProtection )
2822 			{
2823 				iTotalProtection += ArmourProtection( pTarget, GCM->getItem(pArmour->usItem)->getClassIndex(), &(pArmour->bStatus[0]), iImpact, ubAmmoType );
2824 				if ( pArmour->bStatus[ 0 ] < USABLE )
2825 				{
2826 					DeleteObj( pArmour );
2827 					DirtyMercPanelInterface(&pTarget, DIRTYLEVEL2);
2828 				}
2829 			}
2830 		}
2831 	}
2832 	return( iTotalProtection );
2833 }
2834 
BulletImpact(SOLDIERTYPE * pFirer,SOLDIERTYPE * pTarget,UINT8 ubHitLocation,INT32 iOrigImpact,INT16 sHitBy,UINT8 * pubSpecial)2835 INT32 BulletImpact( SOLDIERTYPE *pFirer, SOLDIERTYPE * pTarget, UINT8 ubHitLocation, INT32 iOrigImpact, INT16 sHitBy, UINT8 * pubSpecial )
2836 {
2837 	INT32 iImpact, iFluke, iBonus, iImpactForCrits = 0;
2838 	INT8  bStatLoss;
2839 	UINT8 ubAmmoType;
2840 
2841 	// NOTE: reduction of bullet impact due to range and obstacles is handled
2842 	// in MoveBullet.
2843 
2844 	// Set a few things up:
2845 	if ( GCM->getItem(pFirer->usAttackingWeapon)->getItemClass() == IC_THROWING_KNIFE )
2846 	{
2847 		ubAmmoType = AMMO_KNIFE;
2848 	}
2849 	else
2850 	{
2851 		ubAmmoType = pFirer->inv[pFirer->ubAttackingHand].ubGunAmmoType;
2852 	}
2853 
2854 	if ( TANK( pTarget ) )
2855 	{
2856 		if ( ubAmmoType != AMMO_HEAT )
2857 		{
2858 			// ping!
2859 			return( 0 );
2860 		}
2861 	}
2862 
2863 	// plus/minus up to 25% due to "random" factors (major organs hit or missed,
2864 	// lucky lighter in breast pocket, divine intervention on behalf of "Rev"...)
2865 	iFluke = PreRandom(51) - 25; // gives (0 to 50 -25) -> -25% to +25%
2866 
2867 	// up to 50% extra impact for making particularly accurate successful shots
2868 	iBonus = sHitBy / 2;
2869 
2870 	iOrigImpact = iOrigImpact * (100 + iFluke + iBonus) / 100;
2871 
2872 	// at very long ranges (1.5x maxRange and beyond) impact could go negative
2873 	if (iOrigImpact < 1)
2874 	{
2875 		iOrigImpact = 1; // raise impact to a minimum of 1 for any hit
2876 	}
2877 
2878 	// adjust for HE rounds
2879 	if (ubAmmoType == AMMO_HE || ubAmmoType == AMMO_HEAT)
2880 	{
2881 		iOrigImpact = AMMO_DAMAGE_ADJUSTMENT_HE( iOrigImpact );
2882 
2883 		if ( TANK( pTarget ) )
2884 		{
2885 			// HEAT round on tank, divide by 3 for damage
2886 			iOrigImpact /= 2;
2887 		}
2888 	}
2889 
2890 	if (pubSpecial && *pubSpecial == FIRE_WEAPON_BLINDED_BY_SPIT_SPECIAL)
2891 	{
2892 		iImpact = iOrigImpact;
2893 	}
2894 	else
2895 	{
2896 		iImpact = iOrigImpact - TotalArmourProtection(*pTarget, ubHitLocation, iOrigImpact, ubAmmoType);
2897 	}
2898 
2899 	// calc minimum damage
2900 	if (ubAmmoType == AMMO_HP || ubAmmoType == AMMO_SLEEP_DART)
2901 	{
2902 		if (iImpact < 0)
2903 		{
2904 			iImpact = 0;
2905 		}
2906 	}
2907 	else
2908 	{
2909 		if (iImpact < ((iOrigImpact + 5) / 10) )
2910 		{
2911 			iImpact = (iOrigImpact + 5) / 10;
2912 		}
2913 
2914 		if ( (ubAmmoType == AMMO_BUCKSHOT) && (pTarget->bNumPelletsHitBy > 0) )
2915 		{
2916 			iImpact += (pTarget->bNumPelletsHitBy - 1)  / 2;
2917 		}
2918 
2919 	}
2920 
2921 	if (gfNextShotKills)
2922 	{
2923 		// big time cheat key effect!
2924 		iImpact = 100;
2925 		gfNextShotKills = FALSE;
2926 	}
2927 
2928 	if ( iImpact > 0 && !TANK( pTarget ) )
2929 	{
2930 
2931 		if ( ubAmmoType == AMMO_SLEEP_DART && sHitBy > 20 )
2932 		{
2933 			if (pubSpecial)
2934 			{
2935 				*pubSpecial = FIRE_WEAPON_SLEEP_DART_SPECIAL;
2936 			}
2937 			return( iImpact );
2938 		}
2939 
2940 		if (ubAmmoType == AMMO_HP)
2941 		{ // good solid hit with a hollow-point bullet, which got through armour!
2942 			iImpact = AMMO_DAMAGE_ADJUSTMENT_HP( iImpact );
2943 		}
2944 
2945 		AdjustImpactByHitLocation( iImpact, ubHitLocation, &iImpact, &iImpactForCrits );
2946 
2947 		switch( ubHitLocation )
2948 		{
2949 			case AIM_SHOT_HEAD:
2950 				// is the blow deadly enough for an instant kill?
2951 				if ( PythSpacesAway( pFirer->sGridNo, pTarget->sGridNo ) <= MAX_DISTANCE_FOR_MESSY_DEATH )
2952 				{
2953 					if (iImpactForCrits > MIN_DAMAGE_FOR_INSTANT_KILL && iImpactForCrits < pTarget->bLife)
2954 					{
2955 						// blow to the head is so deadly that it causes instant death;
2956 						// the target has more life than iImpact so we increase it
2957 						iImpact = pTarget->bLife + Random( 10 );
2958 						iImpactForCrits = iImpact;
2959 					}
2960 
2961 					if (pubSpecial)
2962 					{
2963 						// is the blow deadly enough to cause a head explosion?
2964 						if ( iImpactForCrits >= pTarget->bLife )
2965 						{
2966 							if (iImpactForCrits > MIN_DAMAGE_FOR_HEAD_EXPLOSION )
2967 							{
2968 								*pubSpecial = FIRE_WEAPON_HEAD_EXPLODE_SPECIAL;
2969 							}
2970 							else if ( iImpactForCrits > (MIN_DAMAGE_FOR_HEAD_EXPLOSION / 2) && ( PreRandom( MIN_DAMAGE_FOR_HEAD_EXPLOSION / 2 ) < (UINT32)(iImpactForCrits - MIN_DAMAGE_FOR_HEAD_EXPLOSION / 2) ) )
2971 							{
2972 								*pubSpecial = FIRE_WEAPON_HEAD_EXPLODE_SPECIAL;
2973 							}
2974 						}
2975 
2976 					}
2977 				}
2978 				break;
2979 			case AIM_SHOT_LEGS:
2980 				// is the damage enough to make us fall over?
2981 				if (pubSpecial && IS_MERC_BODY_TYPE(pTarget) && gAnimControl[pTarget->usAnimState].ubEndHeight == ANIM_STAND && !MercInWater(pTarget))
2982 				{
2983 					if (iImpactForCrits > MIN_DAMAGE_FOR_AUTO_FALL_OVER )
2984 					{
2985 						*pubSpecial = FIRE_WEAPON_LEG_FALLDOWN_SPECIAL;
2986 					}
2987 					// else ramping up chance from 1/2 the automatic value onwards
2988 					else if ( iImpactForCrits > (MIN_DAMAGE_FOR_AUTO_FALL_OVER / 2) && ( PreRandom( MIN_DAMAGE_FOR_AUTO_FALL_OVER / 2 ) < (UINT32)(iImpactForCrits - MIN_DAMAGE_FOR_AUTO_FALL_OVER / 2) ) )
2989 					{
2990 						*pubSpecial = FIRE_WEAPON_LEG_FALLDOWN_SPECIAL;
2991 					}
2992 				}
2993 				break;
2994 			case AIM_SHOT_TORSO:
2995 				// normal damage to torso
2996 				// is the blow deadly enough for an instant kill?
2997 				// since this value is much lower than the others, it only applies at short range...
2998 				if ( PythSpacesAway( pFirer->sGridNo, pTarget->sGridNo ) <= MAX_DISTANCE_FOR_MESSY_DEATH )
2999 				{
3000 					if (iImpact > MIN_DAMAGE_FOR_INSTANT_KILL && iImpact < pTarget->bLife)
3001 					{
3002 						// blow to the chest is so deadly that it causes instant death;
3003 						// the target has more life than iImpact so we increase it
3004 						iImpact = pTarget->bLife + Random( 10 );
3005 						iImpactForCrits = iImpact;
3006 					}
3007 					// special thing for hitting chest - allow cumulative damage to count
3008 					else if ( (iImpact + pTarget->sDamage) > (MIN_DAMAGE_FOR_BLOWN_AWAY + MIN_DAMAGE_FOR_INSTANT_KILL) )
3009 					{
3010 						iImpact = pTarget->bLife + Random( 10 );
3011 						iImpactForCrits = iImpact;
3012 					}
3013 
3014 					// is the blow deadly enough to cause a chest explosion?
3015 					if (pubSpecial)
3016 					{
3017 						if (iImpact > MIN_DAMAGE_FOR_BLOWN_AWAY && iImpact >= pTarget->bLife)
3018 						{
3019 							*pubSpecial = FIRE_WEAPON_CHEST_EXPLODE_SPECIAL;
3020 						}
3021 					}
3022 				}
3023 				break;
3024 		}
3025 	}
3026 
3027 	if ( AM_A_ROBOT( pTarget ) )
3028 	{
3029 		iImpactForCrits = 0;
3030 	}
3031 
3032 	// don't do critical hits against people who are gonna die!
3033 	if( !IsAutoResolveActive() )
3034 	{
3035 
3036 		if ( ubAmmoType == AMMO_KNIFE && pFirer->bOppList[ pTarget->ubID ] == SEEN_CURRENTLY )
3037 		{
3038 			// is this a stealth attack?
3039 			if ( pTarget->bOppList[ pFirer->ubID ] == NOT_HEARD_OR_SEEN && !CREATURE_OR_BLOODCAT( pTarget ) && (ubHitLocation == AIM_SHOT_HEAD || ubHitLocation == AIM_SHOT_TORSO ) )
3040 			{
3041 				if ( PreRandom( 100 ) < (UINT32)(sHitBy + 10 * NUM_SKILL_TRAITS( pFirer, THROWING )) )
3042 				{
3043 					// instant death!
3044 					iImpact = pTarget->bLife + Random( 10 );
3045 					iImpactForCrits = iImpact;
3046 				}
3047 			}
3048 		}
3049 
3050 		if (iImpactForCrits > 0 && iImpactForCrits < pTarget->bLife )
3051 		{
3052 			if (PreRandom( iImpactForCrits / 2 + pFirer->bAimTime * 5) + 1 > CRITICAL_HIT_THRESHOLD)
3053 			{
3054 				bStatLoss = (INT8) PreRandom( iImpactForCrits / 2 ) + 1;
3055 				switch( ubHitLocation )
3056 				{
3057 					case AIM_SHOT_HEAD:
3058 						if (bStatLoss >= pTarget->bWisdom)
3059 						{
3060 							bStatLoss = pTarget->bWisdom - 1;
3061 						}
3062 						if ( bStatLoss > 0 )
3063 						{
3064 							pTarget->bWisdom -= bStatLoss;
3065 
3066 							if (pTarget->ubProfile != NO_PROFILE)
3067 							{
3068 								gMercProfiles[ pTarget->ubProfile ].bWisdom = pTarget->bWisdom;
3069 							}
3070 
3071 
3072 							if (pTarget->name[0] && pTarget->bVisible == TRUE)
3073 							{
3074 								// make stat RED for a while...
3075 								pTarget->uiChangeWisdomTime = GetJA2Clock();
3076 								pTarget->usValueGoneUp &= ~( WIS_INCREASE );
3077 
3078 								if (bStatLoss == 1)
3079 								{
3080 									ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, st_format_printf(g_langRes->Message[STR_LOSES_1_WISDOM], pTarget->name) );
3081 								}
3082 								else
3083 								{
3084 									ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, st_format_printf(g_langRes->Message[STR_LOSES_WISDOM], pTarget->name, bStatLoss) );
3085 								}
3086 							}
3087 						}
3088 						else if ( pTarget->bNumPelletsHitBy == 0 )
3089 						{
3090 							ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, st_format_printf(g_langRes->Message[STR_HEAD_HIT], pTarget->name) );
3091 						}
3092 						break;
3093 					case AIM_SHOT_TORSO:
3094 						if (PreRandom( 1 ) == 0 && !(pTarget->uiStatusFlags & SOLDIER_MONSTER) )
3095 						{
3096 							if (bStatLoss >= pTarget->bDexterity)
3097 							{
3098 								bStatLoss = pTarget->bDexterity - 1;
3099 							}
3100 							if ( bStatLoss > 0 )
3101 							{
3102 								pTarget->bDexterity -= bStatLoss;
3103 
3104 								if (pTarget->ubProfile != NO_PROFILE)
3105 								{
3106 									gMercProfiles[ pTarget->ubProfile ].bDexterity = pTarget->bDexterity;
3107 								}
3108 
3109 								if (pTarget->name[0] && pTarget->bVisible == TRUE)
3110 								{
3111 									// make stat RED for a while...
3112 									pTarget->uiChangeDexterityTime = GetJA2Clock();
3113 									pTarget->usValueGoneUp &= ~( DEX_INCREASE );
3114 
3115 									if (bStatLoss == 1)
3116 									{
3117 										ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, st_format_printf(g_langRes->Message[STR_LOSES_1_DEX], pTarget->name) );
3118 									}
3119 									else
3120 									{
3121 										ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, st_format_printf(g_langRes->Message[STR_LOSES_DEX], pTarget->name, bStatLoss) );
3122 									}
3123 								}
3124 							}
3125 						}
3126 						else
3127 						{
3128 							if (bStatLoss >= pTarget->bStrength)
3129 							{
3130 								bStatLoss = pTarget->bStrength - 1;
3131 							}
3132 							if ( bStatLoss > 0 )
3133 							{
3134 								pTarget->bStrength -= bStatLoss;
3135 
3136 								if (pTarget->ubProfile != NO_PROFILE)
3137 								{
3138 									gMercProfiles[ pTarget->ubProfile ].bStrength = pTarget->bStrength;
3139 								}
3140 
3141 								if (pTarget->name[0] && pTarget->bVisible == TRUE)
3142 								{
3143 									// make stat RED for a while...
3144 									pTarget->uiChangeStrengthTime = GetJA2Clock();
3145 									pTarget->usValueGoneUp &= ~( STRENGTH_INCREASE );
3146 
3147 									if (bStatLoss == 1)
3148 									{
3149 										ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, st_format_printf(g_langRes->Message[STR_LOSES_1_STRENGTH], pTarget->name) );
3150 									}
3151 									else
3152 									{
3153 										ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, st_format_printf(g_langRes->Message[STR_LOSES_STRENGTH], pTarget->name, bStatLoss) );
3154 									}
3155 								}
3156 							}
3157 						}
3158 						break;
3159 					case AIM_SHOT_LEGS:
3160 						if (bStatLoss >= pTarget->bAgility)
3161 						{
3162 							bStatLoss = pTarget->bAgility - 1;
3163 						}
3164 						if ( bStatLoss > 0 )
3165 						{
3166 							pTarget->bAgility -= bStatLoss;
3167 
3168 							if (pTarget->ubProfile != NO_PROFILE)
3169 							{
3170 								gMercProfiles[ pTarget->ubProfile ].bAgility = pTarget->bAgility;
3171 							}
3172 
3173 							if (pTarget->name[0] && pTarget->bVisible == TRUE)
3174 							{
3175 								// make stat RED for a while...
3176 								pTarget->uiChangeAgilityTime = GetJA2Clock();
3177 								pTarget->usValueGoneUp &= ~( AGIL_INCREASE );
3178 
3179 								if (bStatLoss == 1)
3180 								{
3181 									ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, st_format_printf(g_langRes->Message[STR_LOSES_1_AGIL], pTarget->name) );
3182 								}
3183 								else
3184 								{
3185 									ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, st_format_printf(g_langRes->Message[STR_LOSES_AGIL], pTarget->name, bStatLoss) );
3186 								}
3187 							}
3188 						}
3189 						break;
3190 				}
3191 			}
3192 			else if ( ubHitLocation == AIM_SHOT_HEAD && pTarget->bNumPelletsHitBy == 0 )
3193 			{
3194 				ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, st_format_printf(g_langRes->Message[STR_HEAD_HIT], pTarget->name) );
3195 			}
3196 		}
3197 	}
3198 
3199 	return( iImpact );
3200 }
3201 
3202 
HTHImpact(const SOLDIERTYPE * const att,const SOLDIERTYPE * const tgt,const INT32 iHitBy,const BOOLEAN fBladeAttack)3203 INT32 HTHImpact(const SOLDIERTYPE* const att, const SOLDIERTYPE* const tgt, const INT32 iHitBy, const BOOLEAN fBladeAttack)
3204 {
3205 	INT32        impact   = EffectiveExpLevel(att) / 2; // 0 to 4 for level
3206 	const INT8   strength = EffectiveStrength(att);
3207 	const UINT16 weapon   = att->usAttackingWeapon;
3208 	if (fBladeAttack)
3209 	{
3210 		impact += strength / 20; // 0 to 5 for strength, adjusted by damage taken
3211 		impact += GCM->getWeapon(weapon)->ubImpact;
3212 
3213 		if (AM_A_ROBOT(tgt)) impact /= 4;
3214 	}
3215 	else
3216 	{
3217 		impact += strength / 5; // 0 to 20 for strength, adjusted by damage taken
3218 
3219 		// NB martial artists don't get a bonus for using brass knuckles!
3220 		if (weapon && !HAS_SKILL_TRAIT(att, MARTIALARTS))
3221 		{
3222 			impact += GCM->getWeapon(weapon)->ubImpact;
3223 			if (AM_A_ROBOT(tgt)) impact /= 2;
3224 		}
3225 		else
3226 		{
3227 			// base HTH damage
3228 			impact += 5;
3229 			if (AM_A_ROBOT(tgt)) impact = 0;
3230 		}
3231 	}
3232 
3233 	const INT32 fluke = PreRandom(51) - 25; // +/-25% bonus due to random factors
3234 	const INT32 bonus = iHitBy / 2;         // up to 50% extra impact for accurate attacks
3235 	impact = impact * (100 + fluke + bonus) / 100;
3236 
3237 	if (!fBladeAttack)
3238 	{
3239 		// add bonuses for hand-to-hand and martial arts
3240 		if (HAS_SKILL_TRAIT(att, MARTIALARTS))
3241 		{
3242 			impact = impact * (100 + gbSkillTraitBonus[MARTIALARTS] * NUM_SKILL_TRAITS(att, MARTIALARTS)) / 100;
3243 			if (att->usAnimState == NINJA_SPINKICK) impact *= 2;
3244 		}
3245 		// SPECIAL  - give TRIPLE bonus for damage for hand-to-hand trait
3246 		// because the HTH bonus is half that of martial arts, and gets only 1x for to-hit bonus
3247 		impact = impact * (100 + 3 * gbSkillTraitBonus[HANDTOHAND] * NUM_SKILL_TRAITS(att, HANDTOHAND)) / 100;
3248 	}
3249 
3250 	return impact;
3251 }
3252 
3253 
ShotMiss(const BULLET * const b)3254 void ShotMiss(const BULLET* const b)
3255 {
3256 	SOLDIERTYPE* const pAttacker = b->pFirer;
3257 	SOLDIERTYPE* const opponent = pAttacker->opponent;
3258 	// AGILITY GAIN: Opponent "dodged" a bullet shot at him (it missed)
3259 	if (opponent != NULL) AgilityForEnemyMissingPlayer(pAttacker, opponent, 5);
3260 
3261 	switch (GCM->getWeapon(pAttacker->usAttackingWeapon)->ubWeaponClass)
3262 	{
3263 		case HANDGUNCLASS:
3264 		case RIFLECLASS:
3265 		case SHOTGUNCLASS:
3266 		case SMGCLASS:
3267 		case MGCLASS:
3268 			// Guy has missed, play random sound
3269 			if (pAttacker->bTeam == OUR_TEAM && Random(40) == 0)
3270 			{
3271 				DoMercBattleSound(pAttacker, BATTLE_SOUND_CURSE1);
3272 			}
3273 
3274 			// PLAY SOUND AND FLING DEBRIS
3275 			// RANDOMIZE SOUND SYSTEM
3276 			if (!DoSpecialEffectAmmoMiss(pAttacker, NOWHERE, 0, 0, 0, TRUE, TRUE, NULL))
3277 			{
3278 				PlayJA2Sample(SoundRange<MISS_1, MISS_8>(), HIGHVOLUME, 1, MIDDLEPAN);
3279 			}
3280 
3281 			// ATE: Show misses...( if our team )
3282 			if (gGameSettings.fOptions[TOPTION_SHOW_MISSES] &&
3283 				pAttacker->bTeam == OUR_TEAM)
3284 			{
3285 				LocateGridNo(b->sGridNo);
3286 			}
3287 			break;
3288 
3289 		case MONSTERCLASS:
3290 			PlayJA2Sample(SPIT_RICOCHET, HIGHVOLUME, 1, MIDDLEPAN);
3291 			break;
3292 	}
3293 
3294 	SLOGD("Freeing up attacker - bullet missed");
3295 	FreeUpAttacker(pAttacker);
3296 }
3297 
3298 
CalcChanceHTH(SOLDIERTYPE * pAttacker,SOLDIERTYPE * pDefender,UINT8 ubAimTime,UINT8 ubMode)3299 static UINT32 CalcChanceHTH(SOLDIERTYPE* pAttacker, SOLDIERTYPE* pDefender, UINT8 ubAimTime, UINT8 ubMode)
3300 {
3301 	UINT16 usInHand;
3302 	UINT8  ubBandaged;
3303 	INT32  iAttRating, iDefRating;
3304 	INT32  iChance;
3305 
3306 	usInHand = pAttacker->usAttackingWeapon;
3307 
3308 	if ( (usInHand != CREATURE_QUEEN_TENTACLES ) && (pDefender->bLife < OKLIFE || pDefender->bBreath < OKBREATH) )
3309 	{
3310 		// there is NO way to miss
3311 		return( 100 );
3312 	}
3313 
3314 	if (ubMode == HTH_MODE_STAB)
3315 	{
3316 		// safety check
3317 		if (GCM->getWeapon(usInHand)->ubWeaponClass != KNIFECLASS)
3318 		{
3319 			return(0);
3320 		}
3321 	}
3322 	else
3323 	{
3324 		if ( GCM->getItem(usInHand)->getItemClass() != IC_PUNCH )
3325 		{
3326 			return(0);
3327 		}
3328 	}
3329 
3330 	// CALCULATE ATTACKER'S CLOSE COMBAT RATING (1-100)
3331 	if (ubMode == HTH_MODE_STEAL)
3332 	{
3333 		// this is more of a brute force strength-vs-strength check
3334 		iAttRating = (EffectiveDexterity(pAttacker) + // coordination, accuracy
3335 				EffectiveAgility(pAttacker) +    // speed & reflexes
3336 				3 * pAttacker->bStrength +    // physical strength (TRIPLED!)
3337 				(10 * EffectiveExpLevel(pAttacker)));  // experience, knowledge
3338 	}
3339 	else
3340 	{
3341 		iAttRating = (3 * EffectiveDexterity(pAttacker) + // coordination, accuracy (TRIPLED!)
3342 				EffectiveAgility(pAttacker) +    // speed & reflexes
3343 				pAttacker->bStrength +    // physical strength
3344 				(10 * EffectiveExpLevel(pAttacker)));  // experience, knowledge
3345 	}
3346 
3347 	iAttRating /= 6;  // convert from 6-600 to 1-100
3348 
3349 	// psycho bonus
3350 	if ( pAttacker->ubProfile != NO_PROFILE && gMercProfiles[ pAttacker->ubProfile ].bPersonalityTrait == PSYCHO )
3351 	{
3352 		iAttRating += AIM_BONUS_PSYCHO;
3353 	}
3354 
3355 	// modify chance to hit by morale
3356 	iAttRating += GetMoraleModifier( pAttacker );
3357 
3358 	// modify for fatigue
3359 	iAttRating -= GetSkillCheckPenaltyForFatigue( pAttacker, iAttRating );
3360 
3361 	// if attacker spent some extra time aiming
3362 	if (ubAimTime)
3363 	{
3364 		// use only HALF of the normal aiming bonus for knife aiming.
3365 		// since there's no range penalty, the bonus is otherwise too generous
3366 		iAttRating += ((AIM_BONUS_PER_AP * ubAimTime) / 2);    //bonus for aiming
3367 	}
3368 
3369 	if (! (pAttacker->uiStatusFlags & SOLDIER_PC) )   // if attacker is a computer AI controlled enemy
3370 	{
3371 		iAttRating += gbDiff[ DIFF_ENEMY_TO_HIT_MOD ][ SoldierDifficultyLevel( pAttacker ) ];
3372 	}
3373 
3374 	// if attacker is being affected by gas
3375 	if ( pAttacker->uiStatusFlags & SOLDIER_GASSED )
3376 		iAttRating -= AIM_PENALTY_GASSED;
3377 
3378 	// if attacker is being bandaged at the same time, his concentration is off
3379 	if (pAttacker->ubServiceCount > 0)
3380 		iAttRating -= AIM_PENALTY_GETTINGAID;
3381 
3382 	// if attacker is still in shock
3383 	if (pAttacker->bShock)
3384 		iAttRating -= (pAttacker->bShock * AIM_PENALTY_PER_SHOCK);
3385 
3386 	// If attacker injured, reduce chance accordingly (by up to 2/3rds)
3387 	if ((iAttRating > 0) && (pAttacker->bLife < pAttacker->bLifeMax))
3388 	{
3389 		// if bandaged, give 1/2 of the bandaged life points back into equation
3390 		ubBandaged = pAttacker->bLifeMax - pAttacker->bLife - pAttacker->bBleeding;
3391 
3392 		iAttRating -= (2 * iAttRating * (pAttacker->bLifeMax - pAttacker->bLife + (ubBandaged / 2))) /
3393 				(3 * pAttacker->bLifeMax);
3394 	}
3395 
3396 	// If attacker tired, reduce chance accordingly (by up to 1/2)
3397 	if ((iAttRating > 0) && (pAttacker->bBreath < 100))
3398 		iAttRating -= (iAttRating * (100 - pAttacker->bBreath)) / 200;
3399 
3400 	if (pAttacker->ubProfile != NO_PROFILE)
3401 	{
3402 		if (ubMode == HTH_MODE_STAB)
3403 		{
3404 			iAttRating += gbSkillTraitBonus[KNIFING] * NUM_SKILL_TRAITS(pAttacker, KNIFING);
3405 		}
3406 		else
3407 		{
3408 			// add bonuses for hand-to-hand and martial arts
3409 			iAttRating += gbSkillTraitBonus[MARTIALARTS] * NUM_SKILL_TRAITS(pAttacker, MARTIALARTS);
3410 			iAttRating += gbSkillTraitBonus[HANDTOHAND]  * NUM_SKILL_TRAITS(pAttacker, HANDTOHAND);
3411 		}
3412 	}
3413 
3414 
3415 	if (iAttRating < 1)
3416 		iAttRating = 1;
3417 
3418 
3419 	// CALCULATE DEFENDER'S CLOSE COMBAT RATING (0-100)
3420 	if (ubMode == HTH_MODE_STEAL)
3421 	{
3422 		iDefRating = (EffectiveAgility( pDefender )) +   // speed & reflexes
3423 			EffectiveDexterity( pDefender ) +  // coordination, accuracy
3424 			3 * pDefender->bStrength +    // physical strength (TRIPLED!)
3425 			(10 * EffectiveExpLevel( pDefender ) );  // experience, knowledge
3426 	}
3427 	else
3428 	{
3429 		iDefRating = (3 * EffectiveAgility( pDefender ) ) +   // speed & reflexes (TRIPLED!)
3430 			EffectiveDexterity( pDefender ) +  // coordination, accuracy
3431 			pDefender->bStrength +    // physical strength
3432 			(10 * EffectiveExpLevel( pDefender ) );  // experience, knowledge
3433 	}
3434 
3435 	iDefRating /= 6;  // convert from 6-600 to 1-100
3436 
3437 	// modify chance to dodge by morale
3438 	iDefRating += GetMoraleModifier( pDefender );
3439 
3440 	// modify for fatigue
3441 	iDefRating -= GetSkillCheckPenaltyForFatigue( pDefender, iDefRating );
3442 
3443 	// if attacker is being affected by gas
3444 	if ( pDefender->uiStatusFlags & SOLDIER_GASSED )
3445 		iDefRating -= AIM_PENALTY_GASSED;
3446 
3447 	// if defender is being bandaged at the same time, his concentration is off
3448 	if (pDefender->ubServiceCount > 0)
3449 		iDefRating -= AIM_PENALTY_GETTINGAID;
3450 
3451 	// if defender is still in shock
3452 	if (pDefender->bShock)
3453 		iDefRating -= (pDefender->bShock * AIM_PENALTY_PER_SHOCK);
3454 
3455 	// If defender injured, reduce chance accordingly (by up to 2/3rds)
3456 	if ((iDefRating > 0) && (pDefender->bLife < pDefender->bLifeMax))
3457 	{
3458 		// if bandaged, give 1/2 of the bandaged life points back into equation
3459 		ubBandaged = pDefender->bLifeMax - pDefender->bLife - pDefender->bBleeding;
3460 
3461 		iDefRating -= (2 * iDefRating * (pDefender->bLifeMax - pDefender->bLife + (ubBandaged / 2))) /
3462 		(3 * pDefender->bLifeMax);
3463 
3464 	}
3465 
3466 	// If defender tired, reduce chance accordingly (by up to 1/2)
3467 	if ((iDefRating > 0) && (pDefender->bBreath < 100))
3468 		iDefRating -= (iDefRating * (100 - pDefender->bBreath)) / 200;
3469 
3470 	if ((usInHand == CREATURE_QUEEN_TENTACLES && pDefender->ubBodyType == LARVAE_MONSTER) ||
3471 		pDefender->ubBodyType == INFANT_MONSTER)
3472 	{
3473 		// try to prevent queen from killing the kids, ever!
3474 		iDefRating += 10000;
3475 	}
3476 
3477 	if (gAnimControl[ pDefender->usAnimState ].ubEndHeight < ANIM_STAND)
3478 	{
3479 		if (usInHand == CREATURE_QUEEN_TENTACLES)
3480 		{
3481 			if ( gAnimControl[ pDefender->usAnimState ].ubEndHeight == ANIM_PRONE )
3482 			{
3483 				// make it well-nigh impossible to hit someone who is prone!
3484 				iDefRating += 1000;
3485 			}
3486 			else
3487 			{
3488 				iDefRating += BAD_DODGE_POSITION_PENALTY * 2;
3489 			}
3490 		}
3491 		else
3492 		{
3493 			// if defender crouched, reduce chance accordingly (harder to dodge)
3494 			iDefRating -= BAD_DODGE_POSITION_PENALTY;
3495 			// If our target is prone, double the penalty!
3496 			if ( gAnimControl[ pDefender->usAnimState ].ubEndHeight == ANIM_PRONE )
3497 			{
3498 				iDefRating -= BAD_DODGE_POSITION_PENALTY;
3499 			}
3500 		}
3501 	}
3502 
3503 
3504 	if (pDefender->ubProfile != NO_PROFILE)
3505 	{
3506 		if (ubMode == HTH_MODE_STAB)
3507 		{
3508 			if (GCM->getItem(pDefender->inv[HANDPOS].usItem)->getItemClass() == IC_BLADE)
3509 			{
3510 				// good with knives, got one, so we're good at parrying
3511 				iDefRating += gbSkillTraitBonus[KNIFING] * NUM_SKILL_TRAITS(pDefender, KNIFING);
3512 				// the knife gets in the way but we're still better than nobody
3513 				iDefRating += gbSkillTraitBonus[MARTIALARTS] * NUM_SKILL_TRAITS(pDefender, MARTIALARTS) / 3;
3514 			}
3515 			else
3516 			{
3517 				// good with knives, don't have one, but we know a bit about dodging
3518 				iDefRating += gbSkillTraitBonus[KNIFING]     * NUM_SKILL_TRAITS(pDefender, KNIFING)     / 3;
3519 				// bonus for dodging knives
3520 				iDefRating += gbSkillTraitBonus[MARTIALARTS] * NUM_SKILL_TRAITS(pDefender, MARTIALARTS) / 2;
3521 			}
3522 		}
3523 		else
3524 		{	// punch/hand-to-hand/martial arts attack/steal
3525 			if (GCM->getItem(pDefender->inv[HANDPOS].usItem)->getItemClass() == IC_BLADE && ubMode != HTH_MODE_STEAL)
3526 			{
3527 				// with our knife, we get some bonus at defending from HTH attacks
3528 				iDefRating += gbSkillTraitBonus[KNIFING] * NUM_SKILL_TRAITS(pDefender, KNIFING) / 2;
3529 			}
3530 			else
3531 			{
3532 				iDefRating += gbSkillTraitBonus[MARTIALARTS] * NUM_SKILL_TRAITS(pDefender, MARTIALARTS);
3533 				iDefRating += gbSkillTraitBonus[HANDTOHAND]  * NUM_SKILL_TRAITS(pDefender, HANDTOHAND);
3534 			}
3535 		}
3536 	}
3537 
3538 	if (iDefRating < 1)
3539 		iDefRating = 1;
3540 	// calculate chance to hit by comparing the 2 opponent's ratings
3541 	//  iChance = (100 * iAttRating) / (iAttRating + iDefRating);
3542 
3543 
3544 	if (ubMode == HTH_MODE_STEAL)
3545 	{
3546 		// make this more extreme so that weak people have a harder time stealing from
3547 		// the stronger
3548 		iChance = 50 * iAttRating / iDefRating;
3549 	}
3550 	else
3551 	{
3552 		// Changed from DG by CJC to give higher chances of hitting with a stab or punch
3553 		iChance = 67 + (iAttRating - iDefRating) / 3;
3554 
3555 		if ( pAttacker->bAimShotLocation == AIM_SHOT_HEAD )
3556 		{
3557 			// make this harder!
3558 			iChance -= 20;
3559 		}
3560 
3561 	}
3562 
3563 
3564 	// MAKE SURE CHANCE TO HIT IS WITHIN DEFINED LIMITS
3565 	if (iChance < MINCHANCETOHIT)
3566 	{
3567 		iChance = MINCHANCETOHIT;
3568 	}
3569 	else
3570 	{
3571 		if (iChance > MAXCHANCETOHIT)
3572 			iChance = MAXCHANCETOHIT;
3573 	}
3574 	return (iChance);
3575 }
3576 
CalcChanceToStab(SOLDIERTYPE * pAttacker,SOLDIERTYPE * pDefender,UINT8 ubAimTime)3577 UINT32 CalcChanceToStab(SOLDIERTYPE * pAttacker,SOLDIERTYPE *pDefender, UINT8 ubAimTime)
3578 {
3579 	return( CalcChanceHTH( pAttacker, pDefender, ubAimTime, HTH_MODE_STAB ) );
3580 }
3581 
CalcChanceToPunch(SOLDIERTYPE * pAttacker,SOLDIERTYPE * pDefender,UINT8 ubAimTime)3582 UINT32 CalcChanceToPunch(SOLDIERTYPE *pAttacker, SOLDIERTYPE * pDefender, UINT8 ubAimTime)
3583 {
3584 	return( CalcChanceHTH( pAttacker, pDefender, ubAimTime, HTH_MODE_PUNCH ) );
3585 }
3586 
3587 
CalcChanceToSteal(SOLDIERTYPE * pAttacker,SOLDIERTYPE * pDefender,UINT8 ubAimTime)3588 static UINT32 CalcChanceToSteal(SOLDIERTYPE* pAttacker, SOLDIERTYPE* pDefender, UINT8 ubAimTime)
3589 {
3590 	return( CalcChanceHTH( pAttacker, pDefender, ubAimTime, HTH_MODE_STEAL ) );
3591 }
3592 
3593 
ReloadWeapon(SOLDIERTYPE * const s,UINT8 const inv_pos)3594 void ReloadWeapon(SOLDIERTYPE* const s, UINT8 const inv_pos)
3595 {
3596 	// NB this is a cheat function, don't award experience
3597 	OBJECTTYPE& o = s->inv[inv_pos];
3598 	if (o.usItem == NOTHING) return;
3599 
3600 	o.ubGunShotsLeft = GCM->getWeapon(o.usItem)->ubMagSize;
3601 	DirtyMercPanelInterface(s, DIRTYLEVEL1);
3602 }
3603 
3604 
IsGunBurstCapable(SOLDIERTYPE const * const s,UINT8 const inv_pos)3605 bool IsGunBurstCapable(SOLDIERTYPE const* const s, UINT8 const inv_pos)
3606 {
3607 	UINT16 const item = s->inv[inv_pos].usItem;
3608 	return GCM->getItem(item)->isWeapon() &&
3609 		GCM->getWeapon(item)->ubShotsPerBurst > 0;
3610 }
3611 
3612 
CalcMaxTossRange(const SOLDIERTYPE * pSoldier,UINT16 usItem,BOOLEAN fArmed)3613 INT32 CalcMaxTossRange(const SOLDIERTYPE* pSoldier, UINT16 usItem, BOOLEAN fArmed)
3614 {
3615 	INT32  iRange;
3616 	UINT16 usSubItem;
3617 
3618 	if ( EXPLOSIVE_GUN( usItem ) )
3619 	{
3620 		// oops! return value in weapons table
3621 		return( GCM->getWeapon( usItem )->usRange / CELL_X_SIZE );
3622 	}
3623 
3624 	// if item's fired mechanically
3625 	// ATE: If we are sent in a LAUNCHABLE, get the LAUCNHER, and sub ONLY if we are armed...
3626 	usSubItem = GetLauncherFromLaunchable( usItem );
3627 
3628 	if ( fArmed && usSubItem != NOTHING )
3629 	{
3630 		usItem = usSubItem;
3631 	}
3632 
3633 	if ( GCM->getItem(usItem)->getItemClass() == IC_LAUNCHER && fArmed )
3634 	{
3635 		// this function returns range in tiles so, stupidly, we have to divide by 10 here
3636 		iRange = GCM->getWeapon(usItem)->usRange / CELL_X_SIZE;
3637 	}
3638 	else
3639 	{
3640 		if ( GCM->getItem(usItem)->getFlags() & ITEM_UNAERODYNAMIC )
3641 		{
3642 			iRange = 1;
3643 		}
3644 		else if ( GCM->getItem(usItem)->getItemClass() == IC_GRENADE )
3645 		{
3646 			// start with the range based on the soldier's strength and the item's weight
3647 			INT32 iThrowingStrength = ( EffectiveStrength( pSoldier ) * 2 + 100 ) / 3;
3648 			iRange = 2 + ( iThrowingStrength / __min( ( 3 + (GCM->getItem(usItem)->getWeight()) / 3 ), 4 ) );
3649 		}
3650 		else
3651 		{	// not as aerodynamic!
3652 
3653 			// start with the range based on the soldier's strength and the item's weight
3654 			iRange = 2 + ( ( EffectiveStrength( pSoldier ) / ( 5 + GCM->getItem(usItem)->getWeight()) ) );
3655 		}
3656 
3657 		// adjust for thrower's remaining breath (lose up to 1/2 of range)
3658 		iRange -= (iRange * (100 - pSoldier->bBreath)) / 200;
3659 
3660 		// better max range due to expertise
3661 		iRange = iRange * (100 + gbSkillTraitBonus[THROWING] * NUM_SKILL_TRAITS(pSoldier, THROWING)) / 100;
3662 	}
3663 
3664 	if (iRange < 1)
3665 	{
3666 		iRange = 1;
3667 	}
3668 
3669 	return( iRange );
3670 }
3671 
3672 
CalcThrownChanceToHit(SOLDIERTYPE * pSoldier,INT16 sGridNo,UINT8 ubAimTime,UINT8 ubAimPos)3673 UINT32 CalcThrownChanceToHit(SOLDIERTYPE *pSoldier, INT16 sGridNo, UINT8 ubAimTime, UINT8 ubAimPos )
3674 {
3675 	INT32  iChance, iMaxRange, iRange;
3676 	UINT16 usHandItem;
3677 	INT8   bPenalty, bBandaged;
3678 
3679 	if ( pSoldier->bWeaponMode == WM_ATTACHED)
3680 	{
3681 		usHandItem = UNDER_GLAUNCHER;
3682 	}
3683 	else
3684 	{
3685 		usHandItem = pSoldier->inv[HANDPOS].usItem;
3686 	}
3687 
3688 	if ( GCM->getItem(usHandItem)->getItemClass() != IC_LAUNCHER && pSoldier->bWeaponMode != WM_ATTACHED )
3689 	{
3690 		// PHYSICALLY THROWN arced projectile (ie. grenade)
3691 		// for lack of anything better, base throwing accuracy on dex & marskmanship
3692 		iChance = ( EffectiveDexterity( pSoldier ) + EffectiveMarksmanship( pSoldier ) ) / 2;
3693 		// throwing trait helps out
3694 		iChance += gbSkillTraitBonus[THROWING] * NUM_SKILL_TRAITS(pSoldier, THROWING);
3695 	}
3696 	else
3697 	{
3698 		// MECHANICALLY FIRED arced projectile (ie. mortar), need brains & know-how
3699 		iChance = ( EffectiveDexterity( pSoldier ) + EffectiveMarksmanship( pSoldier ) + EffectiveWisdom( pSoldier ) + pSoldier->bExpLevel ) / 4;
3700 
3701 		// heavy weapons trait helps out
3702 		iChance += gbSkillTraitBonus[HEAVY_WEAPS] * NUM_SKILL_TRAITS(pSoldier, HEAVY_WEAPS);
3703 	}
3704 
3705 	// modify based on morale
3706 	iChance += GetMoraleModifier( pSoldier );
3707 
3708 	// modify by fatigue
3709 	iChance -= GetSkillCheckPenaltyForFatigue( pSoldier, iChance );
3710 
3711 	// if shooting same target from same position as the last shot
3712 	if (sGridNo == pSoldier->sLastTarget)
3713 	{
3714 		iChance += AIM_BONUS_SAME_TARGET; // give a bonus to hit
3715 	}
3716 
3717 	// ADJUST FOR EXTRA AIMING TIME
3718 	if (ubAimTime)
3719 	{
3720 		iChance += (AIM_BONUS_PER_AP * ubAimTime); // bonus for every pt of aiming
3721 	}
3722 
3723 	// if shooter is being affected by gas
3724 	if ( pSoldier->uiStatusFlags & SOLDIER_GASSED )
3725 	{
3726 		iChance -= AIM_PENALTY_GASSED;
3727 	}
3728 
3729 	// if shooter is being bandaged at the same time, his concentration is off
3730 	if (pSoldier->ubServiceCount > 0)
3731 	{
3732 		iChance -= AIM_PENALTY_GETTINGAID;
3733 	}
3734 
3735 	// if shooter is still in shock
3736 	if (pSoldier->bShock)
3737 	{
3738 		iChance -= (pSoldier->bShock * AIM_PENALTY_PER_SHOCK);
3739 	}
3740 
3741 	// calculate actual range (in world units)
3742 	iRange = (INT16)GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, sGridNo );
3743 
3744 	if (IsWearingHeadGear(*pSoldier, SUNGOGGLES))
3745 	{
3746 		// decrease effective range by 10% when using sungoggles (w or w/o scope)
3747 		iRange -= iRange / 10;	//basically, +1% to hit per every 2 squares
3748 	}
3749 
3750 	// ADJUST FOR RANGE
3751 
3752 	if ( usHandItem == MORTAR && iRange < MIN_MORTAR_RANGE)
3753 	{
3754 		return(0);
3755 	}
3756 	else
3757 	{
3758 		iMaxRange = CalcMaxTossRange( pSoldier, usHandItem , TRUE ) * CELL_X_SIZE;
3759 
3760 		// bonus if range is less than 1/2 maximum range, penalty if it's more
3761 
3762 		// bonus is 50% at range 0, -50% at maximum range
3763 
3764 		iChance += 50 * 2 * ( (iMaxRange / 2) - iRange ) / iMaxRange;
3765 		//iChance += ((iMaxRange / 2) - iRange); // increments of 1% per pixel
3766 
3767 		// IF TARGET IS BEYOND MAXIMUM THROWING RANGE
3768 		if (iRange > iMaxRange)
3769 		{
3770 			// the object CAN travel that far if not blocked, but it's NOT accurate!
3771 			iChance /= 2;
3772 		}
3773 	}
3774 
3775 	// IF CHANCE EXISTS, BUT ATTACKER IS INJURED
3776 	if ((iChance > 0) && (pSoldier->bLife < pSoldier->bLifeMax))
3777 	{
3778 		// if bandaged, give 1/2 of the bandaged life points back into equation
3779 		bBandaged = pSoldier->bLifeMax - pSoldier->bLife - pSoldier->bBleeding;
3780 
3781 		// injury penalty is based on % damage taken (max 2/3rds iChance)
3782 		bPenalty = (2 * iChance * (pSoldier->bLifeMax - pSoldier->bLife + (bBandaged / 2))) /
3783 				(3 * pSoldier->bLifeMax);
3784 
3785 		// for mechanically-fired projectiles, reduce penalty in half
3786 		if ( GCM->getItem(usHandItem)->getItemClass() == IC_LAUNCHER )
3787 		{
3788 			bPenalty /= 2;
3789 		}
3790 
3791 		// reduce injury penalty due to merc's experience level (he can take it!)
3792 		iChance -= (bPenalty * (100 - (10 * ( EffectiveExpLevel( pSoldier ) - 1)))) / 100;
3793 	}
3794 
3795 	// IF CHANCE EXISTS, BUT ATTACKER IS LOW ON BREATH
3796 	if ((iChance > 0) && (pSoldier->bBreath < 100))
3797 	{
3798 		// breath penalty is based on % breath missing (max 1/2 iChance)
3799 		bPenalty = (iChance * (100 - pSoldier->bBreath)) / 200;
3800 
3801 		// for mechanically-fired projectiles, reduce penalty in half
3802 		if ( GCM->getItem(usHandItem)->getItemClass() == IC_LAUNCHER )
3803 			bPenalty /= 2;
3804 
3805 		// reduce breath penalty due to merc's dexterity (he can compensate!)
3806 		iChance -= (bPenalty * (100 - ( EffectiveDexterity( pSoldier ) - 10))) / 100;
3807 	}
3808 
3809 	// if iChance exists, but it's a mechanical item being used
3810 	if ((iChance > 0) && (GCM->getItem(usHandItem)->getItemClass() == IC_LAUNCHER ))
3811 		// reduce iChance to hit DIRECTLY by the item's working condition
3812 		iChance = (iChance * WEAPON_STATUS_MOD(pSoldier->inv[HANDPOS].bStatus[0])) / 100;
3813 
3814 	// MAKE SURE CHANCE TO HIT IS WITHIN DEFINED LIMITS
3815 	if (iChance < MINCHANCETOHIT)
3816 		iChance = MINCHANCETOHIT;
3817 	else
3818 	{
3819 		if (iChance > MAXCHANCETOHIT)
3820 			iChance = MAXCHANCETOHIT;
3821 	}
3822 	return (iChance);
3823 }
3824 
3825 
HasLauncher(const SOLDIERTYPE * const s)3826 static BOOLEAN HasLauncher(const SOLDIERTYPE* const s)
3827 {
3828 	OBJECTTYPE const& o = s->inv[HANDPOS];
3829 	return FindAttachment(&o, UNDER_GLAUNCHER) != ITEM_NOT_FOUND &&
3830 		FindLaunchableAttachment(&o, UNDER_GLAUNCHER) != ITEM_NOT_FOUND;
3831 }
3832 
3833 
ChangeWeaponMode(SOLDIERTYPE * const s)3834 void ChangeWeaponMode(SOLDIERTYPE* const s)
3835 {
3836 	// ATE: Don't do this if in a fire amimation.....
3837 	if (gAnimControl[s->usAnimState].uiFlags & ANIM_FIRE) return;
3838 
3839 	INT8 mode = s->bWeaponMode;
3840 	switch (mode)
3841 	{
3842 		case WM_NORMAL:
3843 			if (IsGunBurstCapable(s, HANDPOS))
3844 			{
3845 				mode = WM_BURST;
3846 			}
3847 			else if (HasLauncher(s))
3848 			{
3849 				mode = WM_ATTACHED;
3850 			}
3851 			else
3852 			{
3853 				ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_UI_FEEDBACK, st_format_printf(g_langRes->Message[STR_NOT_BURST_CAPABLE], s->name));
3854 			}
3855 			break;
3856 
3857 		case WM_BURST: mode = (HasLauncher(s) ? WM_ATTACHED : WM_NORMAL); break;
3858 
3859 		default:
3860 		case WM_ATTACHED: mode = WM_NORMAL; break;
3861 	}
3862 
3863 	s->bWeaponMode = mode;
3864 	s->bDoBurst    = (mode == WM_BURST);
3865 	DirtyMercPanelInterface(s, DIRTYLEVEL2);
3866 	gfUIForceReExamineCursorData = TRUE;
3867 }
3868 
3869 
DishoutQueenSwipeDamage(SOLDIERTYPE * pQueenSoldier)3870 void DishoutQueenSwipeDamage( SOLDIERTYPE *pQueenSoldier )
3871 {
3872 	static const INT8 bValidDishoutDirs[3][3] =
3873 	{
3874 		{ NORTH, NORTHEAST, -1 },
3875 		{ EAST,  SOUTHEAST, -1 },
3876 		{ SOUTH, -1,        -1 }
3877 	};
3878 
3879 	INT8  bDir;
3880 	INT32 iChance;
3881 	INT32 iImpact;
3882 	INT32 iHitBy;
3883 
3884 	// Loop through all mercs and make go
3885 	FOR_EACH_MERC(i)
3886 	{
3887 		SOLDIERTYPE* const pSoldier = *i;
3888 		if (pSoldier == pQueenSoldier) continue;
3889 
3890 		// ATE: Ok, lets check for some basic things here!
3891 		if (pSoldier->bLife >= OKLIFE && pSoldier->sGridNo != NOWHERE && pSoldier->bInSector)
3892 		{
3893 			// Get Pyth spaces away....
3894 			if ( GetRangeInCellCoordsFromGridNoDiff( pQueenSoldier->sGridNo, pSoldier->sGridNo ) <= GCM->getWeapon( CREATURE_QUEEN_TENTACLES)->usRange )
3895 			{
3896 				// get direction
3897 				bDir = (INT8)GetDirectionFromGridNo( pSoldier->sGridNo, pQueenSoldier );
3898 
3899 				//
3900 				for (UINT32 cnt2 = 0; cnt2 < 2; ++cnt2)
3901 				{
3902 					if ( bValidDishoutDirs[ pQueenSoldier->uiPendingActionData1 ][ cnt2 ] == bDir )
3903 					{
3904 						iChance = CalcChanceToStab( pQueenSoldier, pSoldier, 0 );
3905 
3906 						// CC: Look here for chance to hit, damage, etc...
3907 						// May want to not hit if target is prone, etc....
3908 						iHitBy = iChance - (INT32) PreRandom( 100 );
3909 						if ( iHitBy > 0 )
3910 						{
3911 							// Hit!
3912 							iImpact = HTHImpact( pQueenSoldier, pSoldier, iHitBy, TRUE );
3913 							EVENT_SoldierGotHit(pSoldier, CREATURE_QUEEN_TENTACLES, iImpact, iImpact, OppositeDirection(bDir), 50, pQueenSoldier, 0, ANIM_CROUCH, 0);
3914 						}
3915 					}
3916 				}
3917 			}
3918 		}
3919 	}
3920 
3921 	pQueenSoldier->uiPendingActionData1++;
3922 }
3923 
3924 
WillExplosiveWeaponFail(const SOLDIERTYPE * pSoldier,const OBJECTTYPE * pObj)3925 static BOOLEAN WillExplosiveWeaponFail(const SOLDIERTYPE* pSoldier, const OBJECTTYPE* pObj)
3926 {
3927 	if ( pSoldier->bTeam == OUR_TEAM || pSoldier->bVisible == 1 )
3928 	{
3929 		if ( (INT8)(PreRandom( 40 ) + PreRandom( 40 ) ) > pObj->bStatus[0] )
3930 		{
3931 			// Do second dice roll
3932 			if ( PreRandom( 2 ) == 1 )
3933 			{
3934 				// Fail
3935 				return( TRUE );
3936 			}
3937 		}
3938 	}
3939 
3940 	return( FALSE );
3941 }
3942 
3943 
3944 // #ifdef WITH_UNITTESTS
3945 // #include "gtest/gtest.h"
3946 
3947 // TEST(Weapons, asserts)
3948 // {
3949 //   EXPECT_EQ(lengthof(OLD_Weapon), MAX_WEAPONS);
3950 // }
3951 
3952 // #endif
3953