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