1 #include "AI.h"
2 #include "Animation_Control.h"
3 #include "Isometric_Utils.h"
4 #include "Weapons.h"
5 #include "OppList.h"
6 #include "Points.h"
7 #include "PathAI.h"
8 #include "WorldMan.h"
9 #include "AIInternals.h"
10 #include "Items.h"
11 #include "LOS.h"
12 #include "Assignments.h"
13 #include "Soldier_Functions.h"
14 #include "Buildings.h"
15 #include "Soldier_Macros.h"
16 #include "Render_Fun.h"
17 #include "StrategicMap.h"
18 #include "Environment.h"
19 #include "Lighting.h"
20 #include "Soldier_Create.h"
21 #include "GamePolicy.h"
22 #include "ContentManager.h"
23 #include "GameInstance.h"
24 #include "WeaponModels.h"
25
26 //
27 // CJC's DG->JA2 conversion notes
28 //
29 // Commented out:
30 //
31 // InWaterOrGas - gas stuff
32 // RoamingRange - point patrol stuff
33
34 UINT8 Urgency[NUM_STATUS_STATES][NUM_MORALE_STATES] =
35 {
36 {URGENCY_LOW, URGENCY_LOW, URGENCY_LOW, URGENCY_LOW, URGENCY_LOW}, // green
37 {URGENCY_HIGH, URGENCY_MED, URGENCY_MED, URGENCY_LOW, URGENCY_LOW}, // yellow
38 {URGENCY_HIGH, URGENCY_MED, URGENCY_MED, URGENCY_MED, URGENCY_MED}, // red
39 {URGENCY_HIGH, URGENCY_HIGH, URGENCY_HIGH, URGENCY_MED, URGENCY_MED} // black
40 };
41
42 UINT16 MovementMode[LAST_MOVEMENT_ACTION + 1][NUM_URGENCY_STATES] =
43 {
44 {WALKING, WALKING, WALKING }, // AI_ACTION_NONE
45
46 {WALKING, WALKING, WALKING }, // AI_ACTION_RANDOM_PATROL
47 {WALKING, RUNNING, RUNNING }, // AI_ACTION_SEEK_FRIEND
48 {WALKING, RUNNING, RUNNING }, // AI_ACTION_SEEK_OPPONENT
49 {RUNNING, RUNNING, RUNNING }, // AI_ACTION_TAKE_COVER
50 {WALKING, RUNNING, RUNNING }, // AI_ACTION_GET_CLOSER
51
52 {WALKING, WALKING, WALKING }, // AI_ACTION_POINT_PATROL,
53 {WALKING, RUNNING, RUNNING }, // AI_ACTION_LEAVE_WATER_GAS,
54 {WALKING, SWATTING, RUNNING }, // AI_ACTION_SEEK_NOISE,
55 {RUNNING, RUNNING, RUNNING }, // AI_ACTION_ESCORTED_MOVE,
56 {WALKING, RUNNING, RUNNING }, // AI_ACTION_RUN_AWAY,
57
58 {RUNNING, RUNNING, RUNNING }, // AI_ACTION_KNIFE_MOVE
59 {WALKING, WALKING, WALKING }, // AI_ACTION_APPROACH_MERC
60 {RUNNING, RUNNING, RUNNING }, // AI_ACTION_TRACK
61 {RUNNING, RUNNING, RUNNING }, // AI_ACTION_EAT
62 {WALKING, RUNNING, SWATTING}, // AI_ACTION_PICKUP_ITEM
63
64 {WALKING, WALKING, WALKING}, // AI_ACTION_SCHEDULE_MOVE
65 {WALKING, WALKING, WALKING}, // AI_ACTION_WALK
66 {RUNNING, RUNNING, RUNNING}, // AI_ACTION_RUN
67 {WALKING, RUNNING, RUNNING}, // AI_ACTION_MOVE_TO_CLIMB //extrapolated
68
69 {WALKING, RUNNING, RUNNING}, // AI_ACTION_CHANGE_FACING //extrapolated
70 {WALKING, RUNNING, RUNNING}, // AI_ACTION_CHANGE_STANCE //extrapolated
71
72 };
73
OKToAttack(const SOLDIERTYPE * pSoldier,int target)74 INT8 OKToAttack(const SOLDIERTYPE * pSoldier, int target)
75 {
76 // can't shoot yourself
77 if (target == pSoldier->sGridNo)
78 return(NOSHOOT_MYSELF);
79
80 if (WaterTooDeepForAttacks(pSoldier->sGridNo))
81 return(NOSHOOT_WATER);
82
83 // make sure a weapon is in hand (FEB.8 ADDITION: tossable items are also OK)
84 if (!WeaponInHand(pSoldier))
85 {
86 return(NOSHOOT_NOWEAPON);
87 }
88
89 // JUST PUT THIS IN ON JULY 13 TO TRY AND FIX OUT-OF-AMMO SITUATIONS
90
91 if ( GCM->getItem(pSoldier->inv[HANDPOS].usItem)->getItemClass() == IC_GUN)
92 {
93 if ( pSoldier->inv[HANDPOS].usItem == TANK_CANNON )
94 {
95 // look for another tank shell ELSEWHERE IN INVENTORY
96 if ( FindLaunchable( pSoldier, TANK_CANNON ) == NO_SLOT )
97 {
98 return(NOSHOOT_NOLOAD);
99 }
100 }
101 else if (pSoldier->inv[HANDPOS].ubGunShotsLeft == 0)
102 {
103 return(NOSHOOT_NOAMMO);
104 }
105 }
106 else if (GCM->getItem(pSoldier->inv[HANDPOS].usItem)->getItemClass() == IC_LAUNCHER)
107 {
108 if ( FindLaunchable( pSoldier, pSoldier->inv[HANDPOS].usItem ) == NO_SLOT )
109 {
110 return(NOSHOOT_NOLOAD);
111 }
112 }
113
114 return(TRUE);
115 }
116
117
ConsiderProne(SOLDIERTYPE * pSoldier)118 static BOOLEAN ConsiderProne(SOLDIERTYPE* pSoldier)
119 {
120 INT16 sOpponentGridNo;
121 INT8 bOpponentLevel;
122 INT32 iRange;
123
124 if (pSoldier->bAIMorale >= MORALE_NORMAL)
125 {
126 return( FALSE );
127 }
128 // We don't want to go prone if there is a nearby enemy
129 ClosestKnownOpponent( pSoldier, &sOpponentGridNo, &bOpponentLevel );
130 iRange = GetRangeFromGridNoDiff( pSoldier->sGridNo, sOpponentGridNo );
131 if (iRange > 10)
132 {
133 return( TRUE );
134 }
135 else
136 {
137 return( FALSE );
138 }
139 }
140
StanceChange(SOLDIERTYPE * pSoldier,UINT8 ubAttackAPCost)141 UINT8 StanceChange( SOLDIERTYPE * pSoldier, UINT8 ubAttackAPCost )
142 {
143 // consider crouching or going prone
144
145 if (PTR_STANDING)
146 {
147 if (pSoldier->bActionPoints - ubAttackAPCost >= AP_CROUCH)
148 {
149 if ( (pSoldier->bActionPoints - ubAttackAPCost >= AP_CROUCH + AP_PRONE) && IsValidStance( pSoldier, ANIM_PRONE ) && ConsiderProne( pSoldier ) )
150 {
151 return( ANIM_PRONE );
152 }
153 else if ( IsValidStance( pSoldier, ANIM_CROUCH ) )
154 {
155 return( ANIM_CROUCH );
156 }
157 }
158 }
159 else if (PTR_CROUCHED)
160 {
161 if ( (pSoldier->bActionPoints - ubAttackAPCost >= AP_PRONE) && IsValidStance( pSoldier, ANIM_PRONE ) && ConsiderProne( pSoldier ) )
162 {
163 return( ANIM_PRONE );
164 }
165 }
166 return( 0 );
167 }
168
ShootingStanceChange(SOLDIERTYPE * pSoldier,ATTACKTYPE * pAttack,INT8 bDesiredDirection)169 UINT8 ShootingStanceChange( SOLDIERTYPE * pSoldier, ATTACKTYPE * pAttack, INT8 bDesiredDirection )
170 {
171 // Figure out the best stance for this attack
172
173 // We don't want to go through a lot of complex calculations here,
174 // just compare the chance of the bullet hitting if we are
175 // standing, crouched, or prone
176
177 UINT16 usRealAnimState, usBestAnimState;
178 INT8 bBestStanceDiff=-1;
179 INT8 bLoop, bStanceNum, bStanceDiff, bAPsAfterAttack;
180 UINT32 uiChanceOfDamage, uiBestChanceOfDamage, uiCurrChanceOfDamage;
181 UINT32 uiStanceBonus, uiMinimumStanceBonusPerChange = 20 - 3 * pAttack->ubAimTime;
182 INT32 iRange;
183
184 bStanceNum = 0;
185 uiCurrChanceOfDamage = 0;
186
187 bAPsAfterAttack = pSoldier->bActionPoints - pAttack->ubAPCost - GetAPsToReadyWeapon( pSoldier, pSoldier->usAnimState );
188 if (bAPsAfterAttack < AP_CROUCH)
189 {
190 return( 0 );
191 }
192 // Unfortunately, to get this to work, we have to fake the AI guy's
193 // animation state so we get the right height values
194 usRealAnimState = pSoldier->usAnimState;
195 usBestAnimState = pSoldier->usAnimState;
196 uiBestChanceOfDamage = 0;
197 iRange = GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, pAttack->sTarget );
198
199 switch( gAnimControl[usRealAnimState].ubEndHeight )
200 {
201 // set a stance number comparable with our loop variable so we can easily compute
202 // stance differences and thus AP cost
203 case ANIM_STAND:
204 bStanceNum = 0;
205 break;
206 case ANIM_CROUCH:
207 bStanceNum = 1;
208 break;
209 case ANIM_PRONE:
210 bStanceNum = 2;
211 break;
212 }
213 for (bLoop = 0; bLoop < 3; bLoop++)
214 {
215 bStanceDiff = ABS( bLoop - bStanceNum );
216 if (bStanceDiff == 2 && bAPsAfterAttack < AP_CROUCH + AP_PRONE)
217 {
218 // can't consider this!
219 continue;
220 }
221
222 switch( bLoop )
223 {
224 case 0:
225 if ( !InternalIsValidStance( pSoldier, bDesiredDirection, ANIM_STAND ) )
226 {
227 continue;
228 }
229 pSoldier->usAnimState = STANDING;
230 break;
231 case 1:
232 if ( !InternalIsValidStance( pSoldier, bDesiredDirection, ANIM_CROUCH ) )
233 {
234 continue;
235 }
236 pSoldier->usAnimState = CROUCHING;
237 break;
238 default:
239 if ( !InternalIsValidStance( pSoldier, bDesiredDirection, ANIM_PRONE ) )
240 {
241 continue;
242 }
243 pSoldier->usAnimState = PRONE;
244 break;
245 }
246
247 uiChanceOfDamage = SoldierToLocationChanceToGetThrough(pSoldier, pAttack->sTarget, pSoldier->bTargetLevel, pSoldier->bTargetCubeLevel, pAttack->opponent) * CalcChanceToHitGun(pSoldier, pAttack->sTarget, pAttack->ubAimTime, AIM_SHOT_TORSO, false) / 100;
248 if (uiChanceOfDamage > 0)
249 {
250 uiStanceBonus = 0;
251 // artificially augment "chance of damage" to reflect penalty to be shot at various stances
252 switch( pSoldier->usAnimState )
253 {
254 case CROUCHING:
255 if (iRange > POINT_BLANK_RANGE + 10 * (AIM_PENALTY_TARGET_CROUCHED / 3))
256 {
257 uiStanceBonus = AIM_BONUS_CROUCHING;
258 }
259 else if (iRange > POINT_BLANK_RANGE)
260 {
261 // reduce chance to hit with distance to the prone/immersed target
262 uiStanceBonus = 3 * ((iRange - POINT_BLANK_RANGE) / CELL_X_SIZE); // penalty -3%/tile
263 }
264 break;
265 case PRONE:
266 if (iRange <= MIN_PRONE_RANGE)
267 {
268 // HATE being prone this close!
269 uiChanceOfDamage = 0;
270 }
271 else //if (iRange > POINT_BLANK_RANGE)
272 {
273 // reduce chance to hit with distance to the prone/immersed target
274 uiStanceBonus = 3 * ((iRange - POINT_BLANK_RANGE) / CELL_X_SIZE); // penalty -3%/tile
275 }
276 break;
277 default:
278 break;
279 }
280 // reduce stance bonus according to how much we have to change stance to get there
281 //uiStanceBonus = uiStanceBonus * (4 - bStanceDiff) / 4;
282 uiChanceOfDamage += uiStanceBonus;
283 }
284
285 if (bStanceDiff == 0)
286 {
287 uiCurrChanceOfDamage = uiChanceOfDamage;
288 }
289 if (uiChanceOfDamage > uiBestChanceOfDamage )
290 {
291 uiBestChanceOfDamage = uiChanceOfDamage;
292 usBestAnimState = pSoldier->usAnimState;
293 bBestStanceDiff = bStanceDiff;
294 }
295 }
296
297 pSoldier->usAnimState = usRealAnimState;
298
299 // return 0 or the best height value to be at
300 if (bBestStanceDiff == 0 || ((uiBestChanceOfDamage - uiCurrChanceOfDamage) / bBestStanceDiff) < uiMinimumStanceBonusPerChange)
301 {
302 // better off not changing our stance!
303 return( 0 );
304 }
305 else
306 {
307 return( gAnimControl[ usBestAnimState ].ubEndHeight );
308 }
309 }
310
311
DetermineMovementMode(SOLDIERTYPE * pSoldier,INT8 bAction)312 UINT16 DetermineMovementMode( SOLDIERTYPE * pSoldier, INT8 bAction )
313 {
314 if ( pSoldier->fUIMovementFast )
315 {
316 return( RUNNING );
317 }
318 else if ( CREATURE_OR_BLOODCAT( pSoldier ) )
319 {
320 if (pSoldier->bAlertStatus == STATUS_GREEN)
321 {
322 return( WALKING );
323 }
324 else
325 {
326 return( RUNNING );
327 }
328 }
329 else if (pSoldier->ubBodyType == COW || pSoldier->ubBodyType == CROW)
330 {
331 return( WALKING );
332 }
333 else
334 {
335 if ( (pSoldier->fAIFlags & AI_CAUTIOUS) && (MovementMode[bAction][Urgency[pSoldier->bAlertStatus][pSoldier->bAIMorale]] == RUNNING) )
336 {
337 return( WALKING );
338 }
339 else if ( bAction == AI_ACTION_SEEK_NOISE && pSoldier->bTeam == CIV_TEAM && !IS_MERC_BODY_TYPE( pSoldier ) )
340 {
341 return( WALKING );
342 }
343 else if ( (pSoldier->ubBodyType == HATKIDCIV || pSoldier->ubBodyType == KIDCIV) && (pSoldier->bAlertStatus == STATUS_GREEN) && Random( 10 ) == 0 )
344 {
345 return( KID_SKIPPING );
346 }
347 else
348 {
349 return( MovementMode[bAction][Urgency[pSoldier->bAlertStatus][pSoldier->bAIMorale]] );
350 }
351 }
352 }
353
NewDest(SOLDIERTYPE * pSoldier,UINT16 usGridNo)354 void NewDest(SOLDIERTYPE *pSoldier, UINT16 usGridNo)
355 {
356 // ATE: Setting sDestination? Tis does not make sense...
357 //pSoldier->sDestination = usGridNo;
358 BOOLEAN fSet = FALSE;
359
360 if ( IS_MERC_BODY_TYPE( pSoldier ) && pSoldier->bAction == AI_ACTION_TAKE_COVER && (pSoldier->bOrders == DEFENSIVE || pSoldier->bOrders == CUNNINGSOLO || pSoldier->bOrders == CUNNINGAID ) && (SoldierDifficultyLevel( pSoldier ) >= 2) )
361 {
362 UINT16 usMovementMode;
363
364 // getting real movement anim for someone who is going to take cover, not just considering
365 usMovementMode = MovementMode[AI_ACTION_TAKE_COVER][Urgency[pSoldier->bAlertStatus][pSoldier->bAIMorale]];
366 if ( usMovementMode != SWATTING )
367 {
368 // really want to look at path, see how far we could get on path while swatting
369 if ( EnoughPoints( pSoldier, RecalculatePathCost( pSoldier, SWATTING ), 0, FALSE ) || (pSoldier->bLastAction == AI_ACTION_TAKE_COVER && pSoldier->usUIMovementMode == SWATTING ) )
370 {
371 pSoldier->usUIMovementMode = SWATTING;
372 }
373 else
374 {
375 pSoldier->usUIMovementMode = usMovementMode;
376 }
377 }
378 else
379 {
380 pSoldier->usUIMovementMode = usMovementMode;
381 }
382 fSet = TRUE;
383 }
384 else
385 {
386 if ( pSoldier->bTeam == ENEMY_TEAM && pSoldier->bAlertStatus == STATUS_RED )
387 {
388 // switch( pSoldier->bAction )
389 {
390 /*
391 case AI_ACTION_MOVE_TO_CLIMB:
392 case AI_ACTION_RUN_AWAY:
393 pSoldier->usUIMovementMode = DetermineMovementMode( pSoldier, pSoldier->bAction );
394 fSet = TRUE;
395 break;*/
396 // default:
397 if ( PreRandom( 5 - SoldierDifficultyLevel( pSoldier ) ) == 0 )
398 {
399 INT16 sClosestNoise = (INT16) MostImportantNoiseHeard( pSoldier, NULL, NULL, NULL );
400 if ( sClosestNoise != NOWHERE && PythSpacesAway( pSoldier->sGridNo, sClosestNoise ) < MaxDistanceVisible() + 10 )
401 {
402 pSoldier->usUIMovementMode = SWATTING;
403 fSet = TRUE;
404 }
405 }
406 if ( !fSet )
407 {
408 pSoldier->usUIMovementMode = DetermineMovementMode( pSoldier, pSoldier->bAction );
409 fSet = TRUE;
410 }
411 // break;
412 }
413
414 }
415 else
416 {
417 pSoldier->usUIMovementMode = DetermineMovementMode( pSoldier, pSoldier->bAction );
418 fSet = TRUE;
419 }
420
421 if ( pSoldier->usUIMovementMode == SWATTING && !IS_MERC_BODY_TYPE( pSoldier ) )
422 {
423 pSoldier->usUIMovementMode = WALKING;
424 }
425 }
426
427 //EVENT_GetNewSoldierPath( pSoldier, pSoldier->sDestination, pSoldier->usUIMovementMode );
428 // ATE: Using this more versitile version
429 // Last paramater says whether to re-start the soldier's animation
430 // This should be done if buddy was paused for fNoApstofinishMove...
431 EVENT_InternalGetNewSoldierPath( pSoldier, usGridNo, pSoldier->usUIMovementMode , FALSE, pSoldier->fNoAPToFinishMove );
432
433 }
434
435
IsActionAffordable(SOLDIERTYPE * pSoldier)436 BOOLEAN IsActionAffordable(SOLDIERTYPE *pSoldier)
437 {
438 INT8 bMinPointsNeeded = 0;
439
440 switch (pSoldier->bAction)
441 {
442 case AI_ACTION_NONE: // maintain current position & facing
443 // no cost for doing nothing!
444 break;
445
446 case AI_ACTION_CHANGE_FACING: // turn to face another direction
447 bMinPointsNeeded = (INT8) GetAPsToLook( pSoldier );
448 break;
449
450 case AI_ACTION_RANDOM_PATROL: // move towards a particular location
451 case AI_ACTION_SEEK_FRIEND: // move towards friend in trouble
452 case AI_ACTION_SEEK_OPPONENT: // move towards a reported opponent
453 case AI_ACTION_TAKE_COVER: // run for nearest cover from threat
454 case AI_ACTION_GET_CLOSER: // move closer to a strategic location
455 case AI_ACTION_POINT_PATROL: // move towards next patrol point
456 case AI_ACTION_LEAVE_WATER_GAS: // seek nearest spot of ungassed land
457 case AI_ACTION_SEEK_NOISE: // seek most important noise heard
458 case AI_ACTION_ESCORTED_MOVE: // go where told to by escortPlayer
459 case AI_ACTION_RUN_AWAY: // run away from nearby opponent(s)
460 case AI_ACTION_APPROACH_MERC:
461 case AI_ACTION_TRACK:
462 case AI_ACTION_EAT:
463 case AI_ACTION_SCHEDULE_MOVE:
464 case AI_ACTION_WALK:
465 case AI_ACTION_MOVE_TO_CLIMB:
466 // for movement, must have enough APs to move at least 1 tile's worth
467 bMinPointsNeeded = MinPtsToMove(pSoldier);
468 break;
469
470 case AI_ACTION_PICKUP_ITEM: // grab things lying on the ground
471 bMinPointsNeeded = __max( MinPtsToMove( pSoldier ), AP_PICKUP_ITEM );
472 break;
473
474 case AI_ACTION_OPEN_OR_CLOSE_DOOR:
475 case AI_ACTION_UNLOCK_DOOR:
476 case AI_ACTION_LOCK_DOOR:
477 bMinPointsNeeded = MinPtsToMove(pSoldier);
478 break;
479
480 case AI_ACTION_DROP_ITEM:
481 bMinPointsNeeded = AP_PICKUP_ITEM;
482 break;
483
484 case AI_ACTION_FIRE_GUN: // shoot at nearby opponent
485 case AI_ACTION_TOSS_PROJECTILE: // throw grenade at/near opponent(s)
486 case AI_ACTION_KNIFE_MOVE: // preparing to stab adjacent opponent
487 case AI_ACTION_THROW_KNIFE:
488 // only FIRE_GUN currently actually pays extra turning costs!
489 bMinPointsNeeded = MinAPsToAttack(pSoldier,pSoldier->usActionData,ADDTURNCOST);
490 break;
491
492 case AI_ACTION_PULL_TRIGGER: // activate an adjacent panic trigger
493 bMinPointsNeeded = AP_PULL_TRIGGER;
494 break;
495
496 case AI_ACTION_USE_DETONATOR: // grab detonator and set off bomb(s)
497 bMinPointsNeeded = AP_USE_REMOTE;
498 break;
499
500 case AI_ACTION_YELLOW_ALERT: // tell friends opponent(s) heard
501 case AI_ACTION_RED_ALERT: // tell friends opponent(s) seen
502 case AI_ACTION_CREATURE_CALL: // for now
503 bMinPointsNeeded = AP_RADIO;
504 break;
505
506 case AI_ACTION_CHANGE_STANCE: // crouch
507 bMinPointsNeeded = AP_CROUCH;
508 break;
509
510 case AI_ACTION_GIVE_AID: // help injured/dying friend
511 bMinPointsNeeded = 0;
512 break;
513
514 case AI_ACTION_CLIMB_ROOF:
515 if (pSoldier->bLevel == 0)
516 {
517 bMinPointsNeeded = AP_CLIMBROOF;
518 }
519 else
520 {
521 bMinPointsNeeded = AP_CLIMBOFFROOF;
522 }
523 break;
524
525 case AI_ACTION_COWER:
526 case AI_ACTION_STOP_COWERING:
527 case AI_ACTION_LOWER_GUN:
528 case AI_ACTION_END_COWER_AND_MOVE:
529 case AI_ACTION_TRAVERSE_DOWN:
530 case AI_ACTION_OFFER_SURRENDER:
531 bMinPointsNeeded = 0;
532 break;
533
534 default:
535 break;
536 }
537
538 // check whether or not we can afford to do this action
539 if (bMinPointsNeeded > pSoldier->bActionPoints)
540 {
541 return(FALSE);
542 }
543 else
544 {
545 return(TRUE);
546 }
547 }
548
549
RandomFriendWithin(SOLDIERTYPE * const s)550 INT16 RandomFriendWithin(SOLDIERTYPE* const s)
551 {
552 // obtain maximum roaming distance from soldier's origin
553 INT16 usOrigin;
554 const UINT16 usMaxDist = RoamingRange(s, &usOrigin);
555
556 // make sure origin is a legal gridno!
557 if (usOrigin >= GRIDSIZE)
558 {
559 return FALSE;
560 }
561
562 // build a list of the guynums of all active, eligible friendly mercs
563
564 // go through each soldier, looking for "friends" (soldiers on same side)
565 UINT8 ubFriendCount = 0;
566 const SOLDIERTYPE* friends[MAXMERCS];
567 FOR_EACH_MERC(i)
568 {
569 const SOLDIERTYPE* const candidate = *i;
570 if (candidate == s) continue; // skip ourselves
571
572 /* if this man not neutral, but is on my side, OR if he is neutral, but so
573 * am I, then he's a "friend" for the purposes of random visitations */
574 if ((candidate->bNeutral ? s->bNeutral : s->bSide == candidate->bSide) &&
575 SpacesAway(s->sGridNo, candidate->sGridNo) > 1) // if we're not already neighbors
576 {
577 // remember his guynum, increment friend counter
578 friends[ubFriendCount++] = candidate;
579 }
580 }
581
582 while (ubFriendCount != 0)
583 {
584 // randomly select one of the remaining friends in the list
585 const UINT chosen_idx = PreRandom(ubFriendCount);
586 const SOLDIERTYPE* const chosen = friends[chosen_idx];
587
588 /* if our movement range is NOT restricted, or this friend's within range
589 * use distance - 1, because there must be at least 1 tile 1 space closer */
590 if (SpacesAway(usOrigin, chosen->sGridNo) - 1 <= usMaxDist)
591 {
592 // should be close enough, try to find a legal ->sDestination within 1 tile
593
594 BOOLEAN fDirChecked[8];
595 // clear dirChecked flag for all 8 directions
596 for (UINT16 usDirection = 0; usDirection < 8; ++usDirection)
597 {
598 fDirChecked[usDirection] = FALSE;
599 }
600
601 // examine all 8 spots around friend
602 // keep looking while directions remain and a satisfactory one not found
603 for (UINT8 ubDirsLeft = 8; ubDirsLeft--;)
604 {
605 // randomly select a direction which hasn't been 'checked' yet
606 UINT16 usDirection;
607 do
608 {
609 usDirection = Random(8);
610 }
611 while (fDirChecked[usDirection]);
612
613 fDirChecked[usDirection] = TRUE;
614
615 // determine the gridno 1 tile away from current friend in this direction
616 const UINT16 usDest = NewGridNo(chosen->sGridNo, DirectionInc( usDirection + 1 ));
617
618 // if that's out of bounds, ignore it & check next direction
619 if (usDest == chosen->sGridNo) continue;
620
621 // if our movement range is NOT restricted
622 if (SpacesAway(usOrigin,usDest) <= usMaxDist &&
623 LegalNPCDestination(s, usDest, ENSURE_PATH, NOWATER, 0))
624 {
625 s->usActionData = usDest; // store this ->sDestination
626 s->bPathStored = TRUE; // optimization - Ian
627 return TRUE;
628 }
629 }
630 }
631
632 friends[chosen_idx] = friends[--ubFriendCount];
633 }
634
635 return FALSE;
636 }
637
638
RandDestWithinRange(SOLDIERTYPE * pSoldier)639 INT16 RandDestWithinRange(SOLDIERTYPE *pSoldier)
640 {
641 INT16 sRandDest = NOWHERE;
642 UINT8 ubTriesLeft;
643 BOOLEAN fLimited = FALSE, fFound = FALSE;
644 INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXRange, sYRange, sXOffset, sYOffset;
645 INT16 sOrigX, sOrigY;
646 INT16 sX, sY;
647
648 sOrigX = sOrigY = -1;
649 sMaxLeft = sMaxRight = sMaxUp = sMaxDown = sXRange = sYRange = -1;
650
651 // Try to find a random ->sDestination that's no more than maxDist away from
652 // the given gridno of origin
653
654 if (gfTurnBasedAI)
655 {
656 ubTriesLeft = 10;
657 }
658 else
659 {
660 ubTriesLeft = 1;
661 }
662
663 INT16 usOrigin;
664 UINT16 usMaxDist = RoamingRange(pSoldier,&usOrigin);
665
666 UINT8 room = NO_ROOM;
667 if ( pSoldier->bOrders <= CLOSEPATROL && (pSoldier->bTeam == CIV_TEAM || pSoldier->ubProfile != NO_PROFILE ) )
668 {
669 // any other combo uses the default of room == NO_ROOM, set above
670 room = GetRoom(pSoldier->usPatrolGrid[0]);
671 }
672
673 // if the maxDist is truly a restriction
674 if (usMaxDist < (MAXCOL - 1))
675 {
676 fLimited = TRUE;
677
678 // determine maximum horizontal limits
679 sOrigX = usOrigin % MAXCOL;
680 sOrigY = usOrigin / MAXCOL;
681
682 sMaxLeft = MIN(usMaxDist, sOrigX);
683 sMaxRight = MIN(usMaxDist,MAXCOL - (sOrigX + 1));
684
685 // determine maximum vertical limits
686 sMaxUp = MIN(usMaxDist, sOrigY);
687 sMaxDown = MIN(usMaxDist,MAXROW - (sOrigY + 1));
688
689 sXRange = sMaxLeft + sMaxRight + 1;
690 sYRange = sMaxUp + sMaxDown + 1;
691 }
692
693 if (pSoldier->ubBodyType == LARVAE_MONSTER)
694 {
695 // only crawl 1 tile, within our roaming range
696 while ((ubTriesLeft--) && !fFound)
697 {
698 sXOffset = (INT16) Random( 3 ) - 1; // generates -1 to +1
699 sYOffset = (INT16) Random( 3 ) - 1;
700
701 if (fLimited)
702 {
703 sX = pSoldier->sGridNo % MAXCOL + sXOffset;
704 sY = pSoldier->sGridNo / MAXCOL + sYOffset;
705 if (sX < sOrigX - sMaxLeft || sX > sOrigX + sMaxRight)
706 {
707 continue;
708 }
709 if (sY < sOrigY - sMaxUp || sY > sOrigY + sMaxDown)
710 {
711 continue;
712 }
713 sRandDest = usOrigin + sXOffset + (MAXCOL * sYOffset);
714 }
715 else
716 {
717 sRandDest = usOrigin + sXOffset + (MAXCOL * sYOffset);
718 }
719
720 if (!LegalNPCDestination(pSoldier,sRandDest,ENSURE_PATH,NOWATER,0))
721 {
722 sRandDest = NOWHERE;
723 continue; // try again!
724 }
725
726 // passed all the tests, ->sDestination is acceptable
727 fFound = TRUE;
728 pSoldier->bPathStored = TRUE; // optimization - Ian
729 }
730 }
731 else
732 {
733 // keep rolling random ->sDestinations until one's satisfactory or retries used
734 while ((ubTriesLeft--) && !fFound)
735 {
736 if (fLimited)
737 {
738 sXOffset = ((INT16)Random(sXRange)) - sMaxLeft;
739 sYOffset = ((INT16)Random(sYRange)) - sMaxUp;
740
741 sRandDest = usOrigin + sXOffset + (MAXCOL * sYOffset);
742 }
743 else
744 {
745 sRandDest = (INT16) PreRandom(GRIDSIZE);
746 }
747
748 if (room != NO_ROOM)
749 {
750 UINT8 const temp_room = GetRoom(sRandDest);
751 if (temp_room != NO_ROOM && temp_room != room)
752 {
753 // outside of room available for patrol!
754 sRandDest = NOWHERE;
755 continue;
756 }
757 }
758
759 if (!LegalNPCDestination(pSoldier,sRandDest,ENSURE_PATH,NOWATER,0))
760 {
761 sRandDest = NOWHERE;
762 continue; // try again!
763 }
764
765 // passed all the tests, ->sDestination is acceptable
766 fFound = TRUE;
767 pSoldier->bPathStored = TRUE; // optimization - Ian
768 }
769 }
770
771 return(sRandDest); // defaults to NOWHERE
772 }
773
ClosestReachableDisturbance(SOLDIERTYPE * pSoldier,UINT8 ubUnconsciousOK,BOOLEAN * pfChangeLevel)774 INT16 ClosestReachableDisturbance(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK, BOOLEAN * pfChangeLevel )
775 {
776 INT16 *psLastLoc, *pusNoiseGridNo;
777 INT8 *pbLastLevel;
778 INT16 sGridNo=-1;
779 INT8 bLevel;
780 BOOLEAN fClimbingNecessary, fClosestClimbingNecessary = FALSE;
781 INT32 iPathCost;
782 INT16 sClosestDisturbance = NOWHERE;
783 INT8 *pbNoiseLevel;
784 INT8 *pbPersOL,*pbPublOL;
785 INT16 sClimbGridNo;
786
787 // CJC: can't trace a path to every known disturbance!
788 // for starters, try the closest one as the crow flies
789 INT16 sClosestEnemy = NOWHERE, sDistToClosestEnemy = 1000, sDistToEnemy;
790
791 *pfChangeLevel = FALSE;
792
793 pusNoiseGridNo = &gsPublicNoiseGridno[pSoldier->bTeam];
794 pbNoiseLevel = &gbPublicNoiseLevel[pSoldier->bTeam];
795
796 // hang pointers at start of this guy's personal and public opponent opplists
797 //pbPersOL = &pSoldier->bOppList[0];
798 //pbPublOL = &(gbPublicOpplist[pSoldier->bTeam][0]);
799 //psLastLoc = &(gsLastKnownOppLoc[pSoldier->ubID][0]);
800
801 // look through this man's personal & public opplists for opponents known
802 INT8 bClosestLevel = 0; // XXX HACK000E
803 FOR_EACH_MERC(i)
804 {
805 const SOLDIERTYPE* const pOpp = *i;
806
807 // if this merc is neutral/on same side, he's not an opponent
808 if ( CONSIDERED_NEUTRAL( pSoldier, pOpp ) || (pSoldier->bSide == pOpp->bSide) )
809 {
810 continue; // next merc
811 }
812
813 pbPersOL = pSoldier->bOppList + pOpp->ubID;
814 pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpp->ubID;
815 psLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpp->ubID;
816 pbLastLevel = gbLastKnownOppLevel[pSoldier->ubID] + pOpp->ubID;
817
818 // if this opponent is unknown personally and publicly
819 if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN))
820 {
821 continue; // next merc
822 }
823
824 // this is possible if get here from BLACK AI in one of those rare
825 // instances when we couldn't get a meaningful shot off at a guy in sight
826 if ((*pbPersOL == SEEN_CURRENTLY) && (pOpp->bLife >= OKLIFE))
827 {
828 // don't allow this to return any valid values, this guy remains a
829 // serious threat and the last thing we want to do is approach him!
830 return(NOWHERE);
831 }
832
833 // if personal knowledge is more up to date or at least equal
834 if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) ||
835 (*pbPersOL == *pbPublOL))
836 {
837 // using personal knowledge, obtain opponent's "best guess" gridno
838 sGridNo = *psLastLoc;
839 bLevel = *pbLastLevel;
840 }
841 else
842 {
843 // using public knowledge, obtain opponent's "best guess" gridno
844 sGridNo = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpp->ubID];
845 bLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpp->ubID];
846 }
847
848 // if we are standing at that gridno (!, obviously our info is old...)
849 if (sGridNo == pSoldier->sGridNo)
850 {
851 continue; // next merc
852 }
853
854 if (sGridNo == NOWHERE)
855 {
856 // huh?
857 continue;
858 }
859
860 sDistToEnemy = PythSpacesAway( pSoldier->sGridNo, sGridNo );
861 if (sDistToEnemy < sDistToClosestEnemy )
862 {
863 sClosestEnemy = sGridNo;
864 bClosestLevel = bLevel;
865 sDistToClosestEnemy = sDistToEnemy;
866 }
867
868 }
869
870 if (sClosestEnemy != NOWHERE)
871 {
872 iPathCost = EstimatePathCostToLocation( pSoldier, sClosestEnemy, bClosestLevel, FALSE, &fClimbingNecessary, &sClimbGridNo );
873 // if we can get there
874 if (iPathCost != 0)
875 {
876 if (fClimbingNecessary)
877 {
878 sClosestDisturbance = sClimbGridNo;
879 }
880 else
881 {
882 sClosestDisturbance = sClosestEnemy;
883 }
884 fClosestClimbingNecessary = fClimbingNecessary;
885 }
886 }
887
888 // if any "misc. noise" was also heard recently
889 if (pSoldier->sNoiseGridno != NOWHERE && pSoldier->sNoiseGridno != sClosestDisturbance)
890 {
891 // test this gridno, too
892 sGridNo = pSoldier->sNoiseGridno;
893 bLevel = pSoldier->bNoiseLevel;
894
895 // if we are there (at the noise gridno)
896 if (sGridNo == pSoldier->sGridNo)
897 {
898 pSoldier->sNoiseGridno = NOWHERE; // wipe it out, not useful anymore
899 pSoldier->ubNoiseVolume = 0;
900 }
901 else
902 {
903 // get the AP cost to get to the location of the noise
904 iPathCost = EstimatePathCostToLocation( pSoldier, sGridNo, bLevel, FALSE, &fClimbingNecessary, &sClimbGridNo );
905 // if we can get there
906 if (iPathCost != 0)
907 {
908 if (fClimbingNecessary)
909 {
910 sClosestDisturbance = sClimbGridNo;
911 }
912 else
913 {
914 sClosestDisturbance = sGridNo;
915 }
916 fClosestClimbingNecessary = fClimbingNecessary;
917 }
918 }
919 }
920
921
922 // if any PUBLIC "misc. noise" was also heard recently
923 if (*pusNoiseGridNo != NOWHERE && *pusNoiseGridNo != sClosestDisturbance )
924 {
925 // test this gridno, too
926 sGridNo = *pusNoiseGridNo;
927 bLevel = *pbNoiseLevel;
928
929 // if we are not NEAR the noise gridno...
930 if ( pSoldier->bLevel != bLevel || PythSpacesAway( pSoldier->sGridNo, sGridNo ) >= 6 || SoldierTo3DLocationLineOfSightTest( pSoldier, sGridNo, bLevel, 0, (UINT8) MaxDistanceVisible(), FALSE ) == 0 )
931 // if we are NOT there (at the noise gridno)
932 // if (sGridNo != pSoldier->sGridNo)
933 {
934 // get the AP cost to get to the location of the noise
935 iPathCost = EstimatePathCostToLocation( pSoldier, sGridNo, bLevel, FALSE, &fClimbingNecessary, &sClimbGridNo );
936 // if we can get there
937 if (iPathCost != 0)
938 {
939 if (fClimbingNecessary)
940 {
941 sClosestDisturbance = sClimbGridNo;
942 }
943 else
944 {
945 sClosestDisturbance = sGridNo;
946 }
947 fClosestClimbingNecessary = fClimbingNecessary;
948 }
949 }
950 else
951 {
952 // degrade our public noise a bit
953 *pusNoiseGridNo -= 2;
954 }
955 }
956
957 *pfChangeLevel = fClosestClimbingNecessary;
958 return(sClosestDisturbance);
959 }
960
961
ClosestKnownOpponent(SOLDIERTYPE * pSoldier,INT16 * psGridNo,INT8 * pbLevel)962 INT16 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT16 * psGridNo, INT8 * pbLevel)
963 {
964 INT16 sGridNo, sClosestOpponent = NOWHERE;
965 INT32 iRange, iClosestRange = 1500;
966 INT8 *pbPersOL, *pbPublOL;
967 INT8 bLevel, bClosestLevel;
968
969 bClosestLevel = -1;
970
971
972 // NOTE: THIS FUNCTION ALLOWS RETURN OF UNCONSCIOUS AND UNREACHABLE OPPONENTS
973
974 // hang pointers at start of this guy's personal and public opponent opplists
975 pbPersOL = &pSoldier->bOppList[0];
976 pbPublOL = &(gbPublicOpplist[pSoldier->bTeam][0]);
977
978 // look through this man's personal & public opplists for opponents known
979 FOR_EACH_MERC(i)
980 {
981 const SOLDIERTYPE* const pOpp = *i;
982
983 // if this merc is neutral/on same side, he's not an opponent
984 if ( CONSIDERED_NEUTRAL( pSoldier, pOpp ) || (pSoldier->bSide == pOpp->bSide))
985 {
986 continue; // next merc
987 }
988
989 // Special stuff for Carmen the bounty hunter
990 if (pSoldier->bAttitude == ATTACKSLAYONLY && pOpp->ubProfile != SLAY)
991 {
992 continue; // next opponent
993 }
994
995 pbPersOL = pSoldier->bOppList + pOpp->ubID;
996 pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpp->ubID;
997
998 // if this opponent is unknown personally and publicly
999 if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN))
1000 {
1001 continue; // next merc
1002 }
1003
1004 // if personal knowledge is more up to date or at least equal
1005 if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) ||
1006 (*pbPersOL == *pbPublOL))
1007 {
1008 // using personal knowledge, obtain opponent's "best guess" gridno
1009 sGridNo = gsLastKnownOppLoc[pSoldier->ubID][pOpp->ubID];
1010 bLevel = gbLastKnownOppLevel[pSoldier->ubID][pOpp->ubID];
1011 }
1012 else
1013 {
1014 // using public knowledge, obtain opponent's "best guess" gridno
1015 sGridNo = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpp->ubID];
1016 bLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpp->ubID];
1017 }
1018
1019 // if we are standing at that gridno(!, obviously our info is old...)
1020 if (sGridNo == pSoldier->sGridNo)
1021 {
1022 continue; // next merc
1023 }
1024
1025 // this function is used only for turning towards closest opponent or changing stance
1026 // as such, if they AI is in a building,
1027 // we should ignore people who are on the roof of the same building as the AI
1028 if ( (bLevel != pSoldier->bLevel) && SameBuilding( pSoldier->sGridNo, sGridNo ) )
1029 {
1030 continue;
1031 }
1032
1033 // I hope this will be good enough; otherwise we need a fractional/world-units-based 2D distance function
1034 //sRange = PythSpacesAway( pSoldier->sGridNo, sGridNo);
1035 iRange = GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, sGridNo );
1036
1037 if (iRange < iClosestRange)
1038 {
1039 iClosestRange = iRange;
1040 sClosestOpponent = sGridNo;
1041 bClosestLevel = bLevel;
1042 }
1043 }
1044 if (psGridNo)
1045 {
1046 *psGridNo = sClosestOpponent;
1047 }
1048 if (pbLevel)
1049 {
1050 *pbLevel = bClosestLevel;
1051 }
1052 return( sClosestOpponent );
1053 }
1054
ClosestSeenOpponent(SOLDIERTYPE * pSoldier,INT16 * psGridNo,INT8 * pbLevel)1055 INT16 ClosestSeenOpponent(SOLDIERTYPE *pSoldier, INT16 * psGridNo, INT8 * pbLevel)
1056 {
1057 INT16 sGridNo, sClosestOpponent = NOWHERE;
1058 INT32 iRange, iClosestRange = 1500;
1059 INT8 *pbPersOL;
1060 INT8 bLevel, bClosestLevel;
1061
1062 bClosestLevel = -1;
1063
1064 // look through this man's personal & public opplists for opponents known
1065 FOR_EACH_MERC(i)
1066 {
1067 const SOLDIERTYPE* const pOpp = *i;
1068
1069 // if this merc is neutral/on same side, he's not an opponent
1070 if ( CONSIDERED_NEUTRAL( pSoldier, pOpp ) || (pSoldier->bSide == pOpp->bSide))
1071 {
1072 continue; // next merc
1073 }
1074
1075 // Special stuff for Carmen the bounty hunter
1076 if (pSoldier->bAttitude == ATTACKSLAYONLY && pOpp->ubProfile != SLAY)
1077 {
1078 continue; // next opponent
1079 }
1080
1081 pbPersOL = pSoldier->bOppList + pOpp->ubID;
1082
1083 // if this opponent is not seen personally
1084 if (*pbPersOL != SEEN_CURRENTLY)
1085 {
1086 continue; // next merc
1087 }
1088
1089 // since we're dealing with seen people, use exact gridnos
1090 sGridNo = pOpp->sGridNo;
1091 bLevel = pOpp->bLevel;
1092
1093 // if we are standing at that gridno(!, obviously our info is old...)
1094 if (sGridNo == pSoldier->sGridNo)
1095 {
1096 continue; // next merc
1097 }
1098
1099 // this function is used only for turning towards closest opponent or changing stance
1100 // as such, if they AI is in a building,
1101 // we should ignore people who are on the roof of the same building as the AI
1102 if ( (bLevel != pSoldier->bLevel) && SameBuilding( pSoldier->sGridNo, sGridNo ) )
1103 {
1104 continue;
1105 }
1106
1107 // I hope this will be good enough; otherwise we need a fractional/world-units-based 2D distance function
1108 //sRange = PythSpacesAway( pSoldier->sGridNo, sGridNo);
1109 iRange = GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, sGridNo );
1110
1111 if (iRange < iClosestRange)
1112 {
1113 iClosestRange = iRange;
1114 sClosestOpponent = sGridNo;
1115 bClosestLevel = bLevel;
1116 }
1117 }
1118 if (psGridNo)
1119 {
1120 *psGridNo = sClosestOpponent;
1121 }
1122 if (pbLevel)
1123 {
1124 *pbLevel = bClosestLevel;
1125 }
1126 return( sClosestOpponent );
1127 }
1128
1129
ClosestPC(const SOLDIERTYPE * pSoldier,INT16 * psDistance)1130 INT16 ClosestPC(const SOLDIERTYPE* pSoldier, INT16* psDistance)
1131 {
1132 // used by NPCs... find the closest PC
1133
1134 // NOTE: skips EPCs!
1135
1136 INT16 sMinDist = (INT16)WORLD_MAX;
1137 INT16 sDist;
1138 INT16 sGridNo = NOWHERE;
1139
1140 CFOR_EACH_IN_TEAM(pTargetSoldier, OUR_TEAM)
1141 {
1142 if (!pTargetSoldier->bInSector)
1143 {
1144 continue;
1145 }
1146
1147 // if not conscious, skip him
1148 if (pTargetSoldier->bLife < OKLIFE)
1149 {
1150 continue;
1151 }
1152
1153 if ( AM_AN_EPC( pTargetSoldier ) )
1154 {
1155 continue;
1156 }
1157
1158 sDist = PythSpacesAway(pSoldier->sGridNo,pTargetSoldier->sGridNo);
1159
1160 // if this PC is not visible to the soldier, then add a penalty to the distance
1161 // so that we weight in favour of visible mercs
1162 if (pTargetSoldier->bTeam != pSoldier->bTeam && pSoldier->bOppList[pTargetSoldier->ubID] != SEEN_CURRENTLY)
1163 {
1164 sDist += 10;
1165 }
1166
1167 if (sDist < sMinDist)
1168 {
1169 sMinDist = sDist;
1170 sGridNo = pTargetSoldier->sGridNo;
1171 }
1172 }
1173
1174 if ( psDistance )
1175 {
1176 *psDistance = sMinDist;
1177 }
1178
1179 return( sGridNo );
1180 }
1181
1182
FindClosestClimbPointAvailableToAI(SOLDIERTYPE * pSoldier,INT16 sStartGridNo,INT16 sDesiredGridNo,BOOLEAN fClimbUp)1183 static INT16 FindClosestClimbPointAvailableToAI(SOLDIERTYPE* pSoldier, INT16 sStartGridNo, INT16 sDesiredGridNo, BOOLEAN fClimbUp)
1184 {
1185 INT16 sGridNo;
1186 INT16 sRoamingOrigin;
1187 INT16 sRoamingRange;
1188
1189 if ( pSoldier->uiStatusFlags & SOLDIER_PC )
1190 {
1191 sRoamingOrigin = pSoldier->sGridNo;
1192 sRoamingRange = 99;
1193 }
1194 else
1195 {
1196 sRoamingRange = RoamingRange( pSoldier, &sRoamingOrigin );
1197 }
1198
1199 //if the enemy is on the roof currently
1200 if (gamepolicy(stay_on_rooftop) && pSoldier->bLevel == 1)
1201 {
1202 //if the soldier is currently on close patrol or on guard
1203 if (pSoldier->bOrders == ONGUARD || pSoldier->bOrders == CLOSEPATROL)
1204 {
1205 //Make it so he cant climb down off the roof
1206 STLOGD("TacticalAI: soldier #{} is on guard ({}) and not allowed to climb down", pSoldier->ubID, pSoldier->bOrders);
1207 return NOWHERE;
1208 }
1209 }
1210
1211 // since climbing necessary involves going an extra tile, we compare against 1 less than the roam range...
1212 // or add 1 to the distance to the climb point
1213
1214 sGridNo = FindClosestClimbPoint( sStartGridNo, sDesiredGridNo, fClimbUp );
1215
1216
1217 if ( PythSpacesAway( sRoamingOrigin, sGridNo ) + 1 > sRoamingRange )
1218 {
1219 return( NOWHERE );
1220 }
1221 else
1222 {
1223 return( sGridNo );
1224 }
1225 }
1226
ClimbingNecessary(SOLDIERTYPE * pSoldier,INT16 sDestGridNo,INT8 bDestLevel)1227 BOOLEAN ClimbingNecessary( SOLDIERTYPE * pSoldier, INT16 sDestGridNo, INT8 bDestLevel )
1228 {
1229 if (pSoldier->bLevel == bDestLevel)
1230 {
1231 if ( (pSoldier->bLevel == 0) || ( gubBuildingInfo[ pSoldier->sGridNo ] == gubBuildingInfo[ sDestGridNo ] ) )
1232 {
1233 return( FALSE );
1234 }
1235 else // different buildings!
1236 {
1237 return( TRUE );
1238 }
1239 }
1240 else
1241 {
1242 return( TRUE );
1243 }
1244 }
1245
GetInterveningClimbingLocation(SOLDIERTYPE * pSoldier,INT16 sDestGridNo,INT8 bDestLevel,BOOLEAN * pfClimbingNecessary)1246 INT16 GetInterveningClimbingLocation( SOLDIERTYPE * pSoldier, INT16 sDestGridNo, INT8 bDestLevel, BOOLEAN * pfClimbingNecessary )
1247 {
1248 if (pSoldier->bLevel == bDestLevel)
1249 {
1250 if ( (pSoldier->bLevel == 0) || ( gubBuildingInfo[ pSoldier->sGridNo ] == gubBuildingInfo[ sDestGridNo ] ) )
1251 {
1252 // on ground or same building... normal!
1253 *pfClimbingNecessary = FALSE;
1254 return( NOWHERE );
1255 }
1256 else
1257 {
1258 // different buildings!
1259 // yes, pass in same gridno twice... want closest climb-down spot for building we are on!
1260 *pfClimbingNecessary = TRUE;
1261 return( FindClosestClimbPointAvailableToAI( pSoldier, pSoldier->sGridNo, pSoldier->sGridNo, FALSE ) );
1262 }
1263 }
1264 else
1265 {
1266 *pfClimbingNecessary = TRUE;
1267 // different levels
1268 if (pSoldier->bLevel == 0)
1269 {
1270 // got to go UP onto building
1271 return( FindClosestClimbPointAvailableToAI( pSoldier, pSoldier->sGridNo, sDestGridNo, TRUE ) );
1272 }
1273 else
1274 {
1275 // got to go DOWN off building
1276 return( FindClosestClimbPointAvailableToAI( pSoldier, pSoldier->sGridNo, pSoldier->sGridNo, FALSE ) );
1277 }
1278 }
1279 }
1280
EstimatePathCostToLocation(SOLDIERTYPE * pSoldier,INT16 sDestGridNo,INT8 bDestLevel,BOOLEAN fAddCostAfterClimbingUp,BOOLEAN * pfClimbingNecessary,INT16 * psClimbGridNo)1281 INT16 EstimatePathCostToLocation( SOLDIERTYPE * pSoldier, INT16 sDestGridNo, INT8 bDestLevel, BOOLEAN fAddCostAfterClimbingUp, BOOLEAN * pfClimbingNecessary, INT16 * psClimbGridNo )
1282 {
1283 INT16 sPathCost;
1284 INT16 sClimbGridNo;
1285
1286 if (pSoldier->bLevel == bDestLevel)
1287 {
1288 if ( (pSoldier->bLevel == 0) || ( gubBuildingInfo[ pSoldier->sGridNo ] == gubBuildingInfo[ sDestGridNo ] ) )
1289 {
1290 // on ground or same building... normal!
1291 sPathCost = EstimatePlotPath(pSoldier, sDestGridNo, FALSE, FALSE, WALKING, 0);
1292 *pfClimbingNecessary = FALSE;
1293 *psClimbGridNo = NOWHERE;
1294 }
1295 else
1296 {
1297 // different buildings!
1298 // yes, pass in same gridno twice... want closest climb-down spot for building we are on!
1299 sClimbGridNo = FindClosestClimbPointAvailableToAI( pSoldier, pSoldier->sGridNo, pSoldier->sGridNo, FALSE );
1300 if (sClimbGridNo == NOWHERE)
1301 {
1302 sPathCost = 0;
1303 }
1304 else
1305 {
1306 sPathCost = PlotPath(pSoldier, sClimbGridNo, FALSE, FALSE, WALKING, 0);
1307 if (sPathCost != 0)
1308 {
1309 // add in cost of climbing down
1310 if (fAddCostAfterClimbingUp)
1311 {
1312 // add in cost of later climbing up, too
1313 sPathCost += AP_CLIMBOFFROOF + AP_CLIMBROOF;
1314 // add in an estimate of getting there after climbing down
1315 sPathCost += (AP_MOVEMENT_FLAT + WALKCOST) * PythSpacesAway( sClimbGridNo, sDestGridNo );
1316 }
1317 else
1318 {
1319 sPathCost += AP_CLIMBOFFROOF;
1320 // add in an estimate of getting there after climbing down, *but not on top of roof*
1321 sPathCost += (AP_MOVEMENT_FLAT + WALKCOST) * PythSpacesAway( sClimbGridNo, sDestGridNo ) / 2;
1322 }
1323 *pfClimbingNecessary = TRUE;
1324 *psClimbGridNo = sClimbGridNo;
1325 }
1326 }
1327 }
1328 }
1329 else
1330 {
1331 // different levels
1332 if (pSoldier->bLevel == 0)
1333 {
1334 //got to go UP onto building
1335 sClimbGridNo = FindClosestClimbPointAvailableToAI( pSoldier, pSoldier->sGridNo, sDestGridNo, TRUE );
1336 }
1337 else
1338 {
1339 // got to go DOWN off building
1340 sClimbGridNo = FindClosestClimbPointAvailableToAI( pSoldier, pSoldier->sGridNo, pSoldier->sGridNo, FALSE );
1341 }
1342
1343 if (sClimbGridNo == NOWHERE)
1344 {
1345 sPathCost = 0;
1346 }
1347 else
1348 {
1349 sPathCost = PlotPath(pSoldier, sClimbGridNo, FALSE, FALSE, WALKING, 0);
1350 if (sPathCost != 0)
1351 {
1352 // add in the cost of climbing up or down
1353 if (pSoldier->bLevel == 0)
1354 {
1355 // must climb up
1356 sPathCost += AP_CLIMBROOF;
1357 if (fAddCostAfterClimbingUp)
1358 {
1359 // add to path a rough estimate of how far to go from the climb gridno to the friend
1360 // estimate walk cost
1361 sPathCost += (AP_MOVEMENT_FLAT + WALKCOST) * PythSpacesAway( sClimbGridNo, sDestGridNo );
1362 }
1363 }
1364 else
1365 {
1366 // must climb down
1367 sPathCost += AP_CLIMBOFFROOF;
1368 // add to path a rough estimate of how far to go from the climb gridno to the friend
1369 // estimate walk cost
1370 sPathCost += (AP_MOVEMENT_FLAT + WALKCOST) * PythSpacesAway( sClimbGridNo, sDestGridNo );
1371 }
1372 *pfClimbingNecessary = TRUE;
1373 *psClimbGridNo = sClimbGridNo;
1374 }
1375 }
1376 }
1377
1378 return( sPathCost );
1379 }
1380
1381
GuySawEnemyThisTurnOrBefore(SOLDIERTYPE * pSoldier)1382 static BOOLEAN GuySawEnemyThisTurnOrBefore(SOLDIERTYPE* pSoldier)
1383 {
1384 UINT8 ubTeamLoop;
1385 UINT8 ubIDLoop;
1386
1387 for ( ubTeamLoop = 0; ubTeamLoop < MAXTEAMS; ubTeamLoop++ )
1388 {
1389 if ( gTacticalStatus.Team[ ubTeamLoop ].bSide != pSoldier->bSide )
1390 {
1391 // consider guys in this team, which isn't on our side
1392 for ( ubIDLoop = gTacticalStatus.Team[ ubTeamLoop ].bFirstID; ubIDLoop <= gTacticalStatus.Team[ ubTeamLoop ].bLastID; ubIDLoop++ )
1393 {
1394 // if this guy SAW an enemy recently...
1395 if ( pSoldier->bOppList[ ubIDLoop ] >= SEEN_CURRENTLY )
1396 {
1397 return( TRUE );
1398 }
1399 }
1400 }
1401 }
1402
1403 return( FALSE );
1404 }
1405
ClosestReachableFriendInTrouble(SOLDIERTYPE * pSoldier,BOOLEAN * pfClimbingNecessary)1406 INT16 ClosestReachableFriendInTrouble(SOLDIERTYPE *pSoldier, BOOLEAN * pfClimbingNecessary)
1407 {
1408 INT16 sPathCost, sClosestFriend = NOWHERE, sShortestPath = 1000, sClimbGridNo;
1409 BOOLEAN fClimbingNecessary, fClosestClimbingNecessary = FALSE;
1410
1411 // civilians don't really have any "friends", so they don't bother with this
1412 if (IsOnCivTeam(pSoldier)) return sClosestFriend;
1413
1414 // consider every friend of this soldier (locations assumed to be known)
1415 FOR_EACH_MERC(i)
1416 {
1417 SOLDIERTYPE* const pFriend = *i;
1418
1419 // if this merc is neutral or NOT on the same side, he's not a friend
1420 if (pFriend->bNeutral || (pSoldier->bSide != pFriend->bSide))
1421 {
1422 continue; // next merc
1423 }
1424
1425 // if this "friend" is actually US
1426 if (pFriend == pSoldier) continue; // next merc
1427
1428 // CJC: restrict "last one to radio" to only if that guy saw us this turn or last turn
1429
1430 // if this friend is not under fire, and isn't the last one to radio
1431 if (!pFriend->bUnderFire &&
1432 (pFriend != gTacticalStatus.Team[pFriend->bTeam].last_merc_to_radio || !GuySawEnemyThisTurnOrBefore(pFriend)))
1433 {
1434 continue; // next merc
1435 }
1436
1437 // if we're already neighbors
1438 if (SpacesAway(pSoldier->sGridNo,pFriend->sGridNo) == 1)
1439 {
1440 continue; // next merc
1441 }
1442
1443 // get the AP cost to go to this friend's gridno
1444 sPathCost = EstimatePathCostToLocation( pSoldier, pFriend->sGridNo, pFriend->bLevel, TRUE, &fClimbingNecessary, &sClimbGridNo );
1445
1446 // if we can get there
1447 if (sPathCost != 0)
1448 {
1449 if (sPathCost < sShortestPath)
1450 {
1451 if (fClimbingNecessary)
1452 {
1453 sClosestFriend = sClimbGridNo;
1454 }
1455 else
1456 {
1457 sClosestFriend = pFriend->sGridNo;
1458 }
1459
1460 sShortestPath = sPathCost;
1461 fClosestClimbingNecessary = fClimbingNecessary;
1462 }
1463 }
1464 }
1465 *pfClimbingNecessary = fClosestClimbingNecessary;
1466 return(sClosestFriend);
1467 }
1468
DistanceToClosestFriend(SOLDIERTYPE * pSoldier)1469 INT16 DistanceToClosestFriend( SOLDIERTYPE * pSoldier )
1470 {
1471 // find the distance to the closest person on the same team
1472 INT16 sMinDist = 1000;
1473 INT16 sDist;
1474
1475 CFOR_EACH_IN_TEAM(pTargetSoldier, pSoldier->bTeam)
1476 {
1477 if (pTargetSoldier == pSoldier) continue;
1478
1479 if (pSoldier->bInSector)
1480 {
1481 if (!pTargetSoldier->bInSector)
1482 {
1483 continue;
1484 }
1485 // if not conscious, skip him
1486 else if (pTargetSoldier->bLife < OKLIFE)
1487 {
1488 continue;
1489 }
1490 }
1491 else
1492 {
1493 // compare sector #s
1494 if ( (pSoldier->sSectorX != pTargetSoldier->sSectorX) ||
1495 (pSoldier->sSectorY != pTargetSoldier->sSectorY) ||
1496 (pSoldier->bSectorZ != pTargetSoldier->bSectorZ) )
1497 {
1498 continue;
1499 }
1500 else if (pTargetSoldier->bLife < OKLIFE)
1501 {
1502 continue;
1503 }
1504 else
1505 {
1506 // well there's someone who could be near
1507 return( 1 );
1508 }
1509 }
1510
1511 sDist = SpacesAway(pSoldier->sGridNo,pTargetSoldier->sGridNo);
1512
1513 if (sDist < sMinDist)
1514 {
1515 sMinDist = sDist;
1516 }
1517 }
1518
1519 return( sMinDist );
1520 }
1521
1522
InSmoke(SOLDIERTYPE const * const s,GridNo const gridno)1523 static bool InSmoke(SOLDIERTYPE const* const s, GridNo const gridno)
1524 {
1525 return gpWorldLevelData[gridno].ubExtFlags[s->bLevel] & MAPELEMENT_EXT_SMOKE;
1526 }
1527
1528
InWaterGasOrSmoke(SOLDIERTYPE const * const s,GridNo const gridno)1529 bool InWaterGasOrSmoke(SOLDIERTYPE const* const s, GridNo const gridno)
1530 {
1531 return WaterTooDeepForAttacks(gridno) || InSmoke(s, gridno) || InGas(s, gridno);
1532 }
1533
1534
InGasOrSmoke(SOLDIERTYPE const * const s,GridNo const gridno)1535 bool InGasOrSmoke(SOLDIERTYPE const* const s, GridNo const gridno)
1536 {
1537 return InSmoke(s, gridno) || InGas(s, gridno);
1538 }
1539
1540
InWaterOrGas(SOLDIERTYPE const * const s,GridNo const grid_no)1541 bool InWaterOrGas(SOLDIERTYPE const* const s, GridNo const grid_no)
1542 {
1543 return WaterTooDeepForAttacks(grid_no) || InGas(s, grid_no);
1544 }
1545
1546
InGas(SOLDIERTYPE const * const s,GridNo const grid_no)1547 bool InGas(SOLDIERTYPE const* const s, GridNo const grid_no)
1548 {
1549 return
1550 gpWorldLevelData[grid_no].ubExtFlags[s->bLevel] & (MAPELEMENT_EXT_TEARGAS | MAPELEMENT_EXT_MUSTARDGAS) &&
1551 !IsWearingHeadGear(*s, GASMASK);
1552 }
1553
1554
WearGasMaskIfAvailable(SOLDIERTYPE * const s)1555 bool WearGasMaskIfAvailable(SOLDIERTYPE* const s)
1556 {
1557 INT8 const slot = FindObj(s, GASMASK);
1558 if (slot == NO_SLOT || slot == HEAD1POS || slot == HEAD2POS) return false;
1559
1560 INT8 const new_slot =
1561 s->inv[HEAD1POS].usItem == NOTHING ? HEAD1POS :
1562 s->inv[HEAD2POS].usItem == NOTHING ? HEAD2POS :
1563 HEAD1POS; // Screw it, going in position 1 anyhow
1564
1565 RearrangePocket(s, slot, new_slot, TRUE);
1566 return true;
1567 }
1568
1569
InLightAtNight(INT16 sGridNo,INT8 bLevel)1570 BOOLEAN InLightAtNight( INT16 sGridNo, INT8 bLevel )
1571 {
1572 UINT8 ubBackgroundLightLevel;
1573
1574 // do not consider us to be "in light" if we're in an underground sector
1575 if ( gbWorldSectorZ > 0 )
1576 {
1577 return( FALSE );
1578 }
1579
1580 ubBackgroundLightLevel = GetTimeOfDayAmbientLightLevel();
1581
1582 if ( ubBackgroundLightLevel < NORMAL_LIGHTLEVEL_DAY + 2 )
1583 {
1584 // don't consider it nighttime, too close to daylight levels
1585 return( FALSE );
1586 }
1587
1588 // could've been placed here, ignore the light
1589 if (GetRoom(sGridNo) != NO_ROOM) return FALSE;
1590
1591 // NB light levels are backwards, so a lower light level means the
1592 // spot in question is BRIGHTER
1593 return LightTrueLevel(sGridNo, bLevel) < ubBackgroundLightLevel;
1594 }
1595
CalcMorale(SOLDIERTYPE * pSoldier)1596 INT8 CalcMorale(SOLDIERTYPE *pSoldier)
1597 {
1598 INT32 iOurTotalThreat = 0, iTheirTotalThreat = 0;
1599 INT16 sOppThreatValue, sFrndThreatValue, sMorale;
1600 INT32 iPercent;
1601 INT8 bMostRecentOpplistValue;
1602 INT8 bMoraleCategory;
1603 INT8 *pbPersOL, *pbPublOL;
1604
1605 // if army guy has NO weapons left then panic!
1606 if ( pSoldier->bTeam == ENEMY_TEAM )
1607 {
1608 if ( FindAIUsableObjClass( pSoldier, IC_WEAPON ) == NO_SLOT )
1609 {
1610 return( MORALE_HOPELESS );
1611 }
1612 }
1613
1614 // hang pointers to my personal opplist, my team's public opplist, and my
1615 // list of previously seen opponents
1616 const INT8* pSeenOpp = &gbSeenOpponents[pSoldier->ubID][0];
1617
1618 // loop through every one of my possible opponents
1619 FOR_EACH_MERC(i)
1620 {
1621 SOLDIERTYPE* const pOpponent = *i;
1622
1623 // if this merc is inactive, at base, on assignment, dead, unconscious
1624 if (pOpponent->bLife < OKLIFE) continue; // next merc
1625
1626 // if this merc is neutral/on same side, he's not an opponent, skip him!
1627 if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide))
1628 continue; // next merc
1629
1630 // Special stuff for Carmen the bounty hunter
1631 if (pSoldier->bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != SLAY)
1632 {
1633 continue; // next opponent
1634 }
1635
1636 pbPersOL = pSoldier->bOppList + pOpponent->ubID;
1637 pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID;
1638 pSeenOpp = gbSeenOpponents[pSoldier->ubID] + pOpponent->ubID;
1639
1640 // if this opponent is unknown to me personally AND unknown to my team, too
1641 if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN))
1642 {
1643 // if I have never seen him before anywhere in this sector, either
1644 if (!(*pSeenOpp))
1645 continue; // next merc
1646
1647 // have seen him in the past, so he remains something of a threat
1648 bMostRecentOpplistValue = 0; // uses the free slot for 0 opplist
1649 }
1650 else // decide which opplist is more current
1651 {
1652 // if personal knowledge is more up to date or at least equal
1653 if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) || (*pbPersOL == *pbPublOL))
1654 bMostRecentOpplistValue = *pbPersOL; // use personal
1655 else
1656 bMostRecentOpplistValue = *pbPublOL; // use public
1657 }
1658
1659 iPercent = ThreatPercent[bMostRecentOpplistValue - OLDEST_HEARD_VALUE];
1660
1661 sOppThreatValue = (iPercent * CalcManThreatValue(pOpponent,pSoldier->sGridNo,FALSE,pSoldier)) / 100;
1662
1663 // ADD this to their running total threatValue (decreases my MORALE)
1664 iTheirTotalThreat += sOppThreatValue;
1665
1666 // NOW THE FUN PART: SINCE THIS OPPONENT IS KNOWN TO ME IN SOME WAY,
1667 // ANY FRIENDS OF MINE THAT KNOW ABOUT HIM BOOST MY MORALE. SO, LET'S GO
1668 // THROUGH THEIR PERSONAL OPPLISTS AND CHECK WHICH OF MY FRIENDS KNOW
1669 // SOMETHING ABOUT HIM AND WHAT THEIR THREAT VALUE TO HIM IS.
1670
1671 FOR_EACH_MERC(j)
1672 {
1673 SOLDIERTYPE* const pFriend = *j;
1674
1675 // if this merc is inactive, at base, on assignment, dead, unconscious
1676 if (pFriend->bLife < OKLIFE) continue; // next merc
1677
1678 // if this merc is not on my side, then he's NOT one of my friends
1679
1680 // WE CAN'T AFFORD TO CONSIDER THE ENEMY OF MY ENEMY MY FRIEND, HERE!
1681 // ONLY IF WE ARE ACTUALLY OFFICIALLY CO-OPERATING TOGETHER (SAME SIDE)
1682 if ( pFriend->bNeutral && !( pSoldier->ubCivilianGroup != NON_CIV_GROUP && pSoldier->ubCivilianGroup == pFriend->ubCivilianGroup ) )
1683 {
1684 continue; // next merc
1685 }
1686
1687 if ( pSoldier->bSide != pFriend->bSide )
1688 continue; // next merc
1689
1690 // THIS TEST IS INVALID IF A COMPUTER-TEAM IS PLAYING CO-OPERATIVELY
1691 // WITH A NON-COMPUTER TEAM SINCE THE OPPLISTS INVOLVED ARE NOT
1692 // UP-TO-DATE. THIS SITUATION IS CURRENTLY NOT POSSIBLE IN HTH/DG.
1693
1694 // ALSO NOTE THAT WE COUNT US AS OUR (BEST) FRIEND FOR THESE CALCULATIONS
1695
1696 // subtract HEARD_2_TURNS_AGO (which is negative) to make values start at 0 and
1697 // be positive otherwise
1698 iPercent = ThreatPercent[pFriend->bOppList[pOpponent->ubID] - OLDEST_HEARD_VALUE];
1699
1700 // reduce the percentage value based on how far away they are from the enemy, if they only hear him
1701 if ( pFriend->bOppList[ pOpponent->ubID ] <= HEARD_LAST_TURN )
1702 {
1703 iPercent -= PythSpacesAway( pSoldier->sGridNo, pFriend->sGridNo ) * 2;
1704 if ( iPercent <= 0 )
1705 {
1706 //ignore!
1707 continue;
1708 }
1709 }
1710
1711 sFrndThreatValue = (iPercent * CalcManThreatValue(pFriend,pOpponent->sGridNo,FALSE,pSoldier)) / 100;
1712
1713 // ADD this to our running total threatValue (increases my MORALE)
1714 // We multiply by sOppThreatValue to PRO-RATE this based on opponent's
1715 // threat value to ME personally. Divide later by sum of them all.
1716 iOurTotalThreat += sOppThreatValue * sFrndThreatValue;
1717 }
1718 }
1719
1720
1721 // if they are no threat whatsoever
1722 if (!iTheirTotalThreat)
1723 sMorale = 500; // our morale is just incredible
1724 else
1725 {
1726 // now divide sOutTotalThreat by sTheirTotalThreat to get the REAL value
1727 iOurTotalThreat /= iTheirTotalThreat;
1728
1729 // calculate the morale (100 is even, < 100 is us losing, > 100 is good)
1730 sMorale = (INT16) ((100 * iOurTotalThreat) / iTheirTotalThreat);
1731 }
1732
1733
1734 if (sMorale <= 25) // odds 1:4 or worse
1735 bMoraleCategory = MORALE_HOPELESS;
1736 else if (sMorale <= 50) // odds between 1:4 and 1:2
1737 bMoraleCategory = MORALE_WORRIED;
1738 else if (sMorale <= 150) // odds between 1:2 and 3:2
1739 bMoraleCategory = MORALE_NORMAL;
1740 else if (sMorale <= 300) // odds between 3:2 and 3:1
1741 bMoraleCategory = MORALE_CONFIDENT;
1742 else // odds better than 3:1
1743 bMoraleCategory = MORALE_FEARLESS;
1744
1745
1746 switch (pSoldier->bAttitude)
1747 {
1748 case DEFENSIVE: bMoraleCategory--; break;
1749 case BRAVESOLO: bMoraleCategory += 2; break;
1750 case BRAVEAID: bMoraleCategory += 2; break;
1751 case CUNNINGSOLO: break;
1752 case CUNNINGAID: break;
1753 case AGGRESSIVE: bMoraleCategory++; break;
1754 }
1755
1756 // make idiot administrators much more aggressive
1757 if ( pSoldier->ubSoldierClass == SOLDIER_CLASS_ADMINISTRATOR )
1758 {
1759 bMoraleCategory += 2;
1760 }
1761
1762
1763 // if still full of energy
1764 if (pSoldier->bBreath > 75)
1765 bMoraleCategory++;
1766 else
1767 {
1768 // if getting a bit low on breath
1769 if (pSoldier->bBreath < 40)
1770 bMoraleCategory--;
1771
1772 // if getting REALLY low on breath
1773 if (pSoldier->bBreath < 10)
1774 bMoraleCategory--;
1775 }
1776
1777
1778 // if still very healthy
1779 if (pSoldier->bLife > 75)
1780 bMoraleCategory++;
1781 else
1782 {
1783 // if getting a bit low on life
1784 if (pSoldier->bLife < 40)
1785 bMoraleCategory--;
1786
1787 // if getting REALLY low on life
1788 if (pSoldier->bLife < 20)
1789 bMoraleCategory--;
1790 }
1791
1792
1793 // if soldier is currently not under fire
1794 if (!pSoldier->bUnderFire)
1795 bMoraleCategory++;
1796
1797
1798 // if adjustments made it outside the allowed limits
1799 if (bMoraleCategory < MORALE_HOPELESS)
1800 bMoraleCategory = MORALE_HOPELESS;
1801 else
1802 {
1803 if (bMoraleCategory > MORALE_FEARLESS)
1804 bMoraleCategory = MORALE_FEARLESS;
1805 }
1806
1807 // brave guys never get hopeless, at worst they get worried
1808 if (bMoraleCategory == MORALE_HOPELESS &&
1809 (pSoldier->bAttitude == BRAVESOLO || pSoldier->bAttitude == BRAVEAID))
1810 bMoraleCategory = MORALE_WORRIED;
1811
1812 SLOGD("Morale = %d (category %d), iOurTotalThreat %d, iTheirTotalThreat %d",
1813 sMorale, bMoraleCategory, iOurTotalThreat, iTheirTotalThreat);
1814
1815 return(bMoraleCategory);
1816 }
1817
CalcManThreatValue(SOLDIERTYPE * pEnemy,INT16 sMyGrid,UINT8 ubReduceForCover,SOLDIERTYPE * pMe)1818 INT32 CalcManThreatValue( SOLDIERTYPE *pEnemy, INT16 sMyGrid, UINT8 ubReduceForCover, SOLDIERTYPE * pMe )
1819 {
1820 INT32 iThreatValue = 0;
1821 BOOLEAN fForCreature = CREATURE_OR_BLOODCAT( pMe );
1822
1823 // If man is inactive, at base, on assignment, dead, unconscious
1824 if (!pEnemy->bActive || !pEnemy->bInSector || !pEnemy->bLife)
1825 {
1826 // he's no threat at all, return a negative number
1827 iThreatValue = -999;
1828 return(iThreatValue);
1829 }
1830
1831 // in boxing mode, let only a boxer be considered a threat.
1832 if ( (gTacticalStatus.bBoxingState == BOXING) && !(pEnemy->uiStatusFlags & SOLDIER_BOXER) )
1833 {
1834 iThreatValue = -999;
1835 return( iThreatValue );
1836 }
1837
1838 if (fForCreature)
1839 {
1840 // health (1-100)
1841 iThreatValue += pEnemy->bLife;
1842 // bleeding (more attactive!) (1-100)
1843 iThreatValue += pEnemy->bBleeding;
1844 // decrease according to distance
1845 iThreatValue = (iThreatValue * 10) / (10 + PythSpacesAway( sMyGrid, pEnemy->sGridNo ) );
1846
1847 }
1848 else
1849 {
1850 // ADD twice the man's level (2-20)
1851 iThreatValue += pEnemy->bExpLevel;
1852
1853 // ADD man's total action points (10-35)
1854 iThreatValue += CalcActionPoints(pEnemy);
1855
1856 // ADD 1/2 of man's current action points (4-17)
1857 iThreatValue += (pEnemy->bActionPoints / 2);
1858
1859 // ADD 1/10 of man's current health (0-10)
1860 iThreatValue += (pEnemy->bLife / 10);
1861
1862 if (pEnemy->bAssignment < ON_DUTY )
1863 {
1864 // ADD 1/4 of man's protection percentage (0-25)
1865 iThreatValue += ArmourPercent( pEnemy ) / 4;
1866
1867 // ADD 1/5 of man's marksmanship skill (0-20)
1868 iThreatValue += (pEnemy->bMarksmanship / 5);
1869
1870 if ( GCM->getItem(pEnemy->inv[HANDPOS].usItem)->isWeapon() )
1871 {
1872 // ADD the deadliness of the item(weapon) he's holding (0-50)
1873 iThreatValue += GCM->getWeapon(pEnemy->inv[HANDPOS].usItem)->ubDeadliness;
1874 }
1875 }
1876
1877 // SUBTRACT 1/5 of man's bleeding (0-20)
1878 iThreatValue -= (pEnemy->bBleeding / 5);
1879
1880 // SUBTRACT 1/10 of man's breath deficiency (0-10)
1881 iThreatValue -= ((100 - pEnemy->bBreath) / 10);
1882
1883 // SUBTRACT man's current shock value
1884 iThreatValue -= pEnemy->bShock;
1885 }
1886
1887 // if I have a specifically defined spot where I'm at (sometime I don't!)
1888 if (sMyGrid != NOWHERE)
1889 {
1890 // ADD 10% if man's already been shooting at me
1891 if (pEnemy->sLastTarget == sMyGrid)
1892 {
1893 iThreatValue += (iThreatValue / 10);
1894 }
1895 else
1896 {
1897 // ADD 5% if man's already facing me
1898 if (pEnemy->bDirection == GetDirectionToGridNoFromGridNo(pEnemy->sGridNo, sMyGrid))
1899 {
1900 iThreatValue += (iThreatValue / 20);
1901 }
1902 }
1903 }
1904
1905 // if this man is conscious
1906 if (pEnemy->bLife >= OKLIFE)
1907 {
1908 // and we were told to reduce threat for my cover
1909 if (ubReduceForCover && (sMyGrid != NOWHERE))
1910 {
1911 // Reduce iThreatValue to same % as the chance HE has shoot through at ME
1912 //iThreatValue = (iThreatValue * ChanceToGetThrough( pEnemy, myGrid, FAKE, ACTUAL, TESTWALLS, 9999, M9PISTOL, NOT_FOR_LOS)) / 100;
1913 //iThreatValue = (iThreatValue * SoldierTo3DLocationChanceToGetThrough( pEnemy, myGrid, FAKE, ACTUAL, TESTWALLS, 9999, M9PISTOL, NOT_FOR_LOS)) / 100;
1914 iThreatValue = iThreatValue * SoldierToLocationChanceToGetThrough(pEnemy, sMyGrid, pMe->bLevel, 0, pMe) / 100;
1915 }
1916 }
1917 else
1918 {
1919 // if he's still something of a threat
1920 if (iThreatValue > 0)
1921 {
1922 // drastically reduce his threat value (divide by 5 to 18)
1923 iThreatValue /= (4 + (OKLIFE - pEnemy->bLife));
1924 }
1925 }
1926
1927 // threat value of any opponent can never drop below 1
1928 if (iThreatValue < 1)
1929 {
1930 iThreatValue = 1;
1931 }
1932 return(iThreatValue);
1933 }
1934
RoamingRange(SOLDIERTYPE * pSoldier,INT16 * pusFromGridNo)1935 INT16 RoamingRange(SOLDIERTYPE *pSoldier, INT16 * pusFromGridNo)
1936 {
1937 if ( CREATURE_OR_BLOODCAT( pSoldier ) )
1938 {
1939 if ( pSoldier->bAlertStatus == STATUS_BLACK )
1940 {
1941 *pusFromGridNo = pSoldier->sGridNo; // from current position!
1942 return(MAX_ROAMING_RANGE);
1943 }
1944 }
1945 if ( pSoldier->bOrders == POINTPATROL || pSoldier->bOrders == RNDPTPATROL )
1946 {
1947 // roam near NEXT PATROL POINT, not from where merc starts out
1948 *pusFromGridNo = pSoldier->usPatrolGrid[pSoldier->bNextPatrolPnt];
1949 }
1950 else
1951 {
1952 // roam around where mercs started
1953 //*pusFromGridNo = pSoldier->sInitialGridNo;
1954 *pusFromGridNo = pSoldier->usPatrolGrid[0];
1955 }
1956
1957 switch (pSoldier->bOrders)
1958 {
1959 // JA2 GOLD: give non-NPCs a 5 tile roam range for cover in combat when being shot at
1960 case STATIONARY:
1961 if (pSoldier->ubProfile != NO_PROFILE || (pSoldier->bAlertStatus < STATUS_BLACK && !(pSoldier->bUnderFire)))
1962 {
1963 return( 0 );
1964 }
1965 else
1966 {
1967 return( 5 );
1968 }
1969 case ONGUARD:
1970 return( 5 );
1971 case CLOSEPATROL:
1972 return( 15 );
1973 case RNDPTPATROL:
1974 case POINTPATROL:
1975 return(10 ); // from nextPatrolGrid, not whereIWas
1976 case FARPATROL:
1977 if (pSoldier->bAlertStatus < STATUS_RED)
1978 {
1979 return( 25 );
1980 }
1981 else
1982 {
1983 return( 50 );
1984 }
1985 case ONCALL:
1986 if (pSoldier->bAlertStatus < STATUS_RED)
1987 {
1988 return( 10 );
1989 }
1990 else
1991 {
1992 return( 30 );
1993 }
1994 case SEEKENEMY:
1995 *pusFromGridNo = pSoldier->sGridNo; // from current position!
1996 return(MAX_ROAMING_RANGE);
1997 default:
1998 return(0);
1999 }
2000 }
2001
2002
RearrangePocket(SOLDIERTYPE * pSoldier,INT8 bPocket1,INT8 bPocket2,UINT8 bPermanent)2003 void RearrangePocket(SOLDIERTYPE *pSoldier, INT8 bPocket1, INT8 bPocket2, UINT8 bPermanent)
2004 {
2005 // NB there's no such thing as a temporary swap for now...
2006 SwapObjs( &(pSoldier->inv[bPocket1]), &(pSoldier->inv[bPocket2]) );
2007 }
2008
2009
FindBetterSpotForItem(SOLDIERTYPE & s,INT8 const slot)2010 bool FindBetterSpotForItem(SOLDIERTYPE& s, INT8 const slot)
2011 {
2012 /* Look for a place in the slots to put an item in a hand or armour position,
2013 * and move it there. */
2014 if (slot >= BIGPOCK1POS) return false;
2015
2016 UINT16 const item = s.inv[slot].usItem;
2017 if (item == NOTHING) return true; // That's just fine
2018
2019 INT8 new_slot;
2020 if (GCM->getItem(item)->getPerPocket() != 0)
2021 { // Try a small pocket first
2022 new_slot = FindEmptySlotWithin(&s, SMALLPOCK1POS, SMALLPOCK8POS);
2023 if (new_slot == NO_SLOT) goto try_big_pocket;
2024 }
2025 else
2026 { // Look for a big pocket
2027 try_big_pocket:
2028 new_slot = FindEmptySlotWithin(&s, BIGPOCK1POS, BIGPOCK4POS);
2029 if (new_slot == NO_SLOT) return false;
2030 }
2031
2032 RearrangePocket(&s, HANDPOS, new_slot, FOREVER);
2033 return true;
2034 }
2035
2036
GetTraversalQuoteActionID(INT8 bDirection)2037 UINT8 GetTraversalQuoteActionID( INT8 bDirection )
2038 {
2039 switch( bDirection )
2040 {
2041 case NORTHEAST: // east
2042 return( QUOTE_ACTION_ID_TRAVERSE_EAST );
2043
2044 case SOUTHEAST: // south
2045 return( QUOTE_ACTION_ID_TRAVERSE_SOUTH );
2046
2047 case SOUTHWEST: // west
2048 return( QUOTE_ACTION_ID_TRAVERSE_WEST );
2049
2050 case NORTHWEST: // north
2051 return( QUOTE_ACTION_ID_TRAVERSE_NORTH );
2052
2053 default:
2054 return( 0 );
2055 }
2056 }
2057
SoldierDifficultyLevel(const SOLDIERTYPE * pSoldier)2058 UINT8 SoldierDifficultyLevel( const SOLDIERTYPE * pSoldier )
2059 {
2060 INT8 bDifficultyBase;
2061 INT8 bDifficulty;
2062
2063 // difficulty modifier ranges from 0 to 100
2064 // and we want to end up with a number between 0 and 4 (4=hardest)
2065 // to a base of 1, divide by 34 to get a range from 1 to 3
2066 bDifficultyBase = 1 + ( CalcDifficultyModifier( pSoldier->ubSoldierClass ) / 34 );
2067
2068 switch( pSoldier->ubSoldierClass )
2069 {
2070 case SOLDIER_CLASS_ADMINISTRATOR:
2071 bDifficulty = bDifficultyBase - 1;
2072 break;
2073
2074 case SOLDIER_CLASS_ARMY:
2075 bDifficulty = bDifficultyBase;
2076 break;
2077
2078 case SOLDIER_CLASS_ELITE:
2079 bDifficulty = bDifficultyBase + 1;
2080 break;
2081
2082 // hard code militia;
2083 case SOLDIER_CLASS_GREEN_MILITIA:
2084 bDifficulty = 2;
2085 break;
2086
2087 case SOLDIER_CLASS_REG_MILITIA:
2088 bDifficulty = 3;
2089 break;
2090
2091 case SOLDIER_CLASS_ELITE_MILITIA:
2092 bDifficulty = 4;
2093 break;
2094
2095 default:
2096 if (pSoldier->bTeam == CREATURE_TEAM)
2097 {
2098 bDifficulty = bDifficultyBase + pSoldier->bLevel / 4;
2099 }
2100 else // civ...
2101 {
2102 bDifficulty = (bDifficultyBase + pSoldier->bLevel / 4) - 1;
2103 }
2104 break;
2105
2106 }
2107
2108 bDifficulty = __max( bDifficulty, 0 );
2109 bDifficulty = __min( bDifficulty, 4 );
2110
2111 return( (UINT8) bDifficulty );
2112 }
2113
ValidCreatureTurn(SOLDIERTYPE * pCreature,INT8 bNewDirection)2114 BOOLEAN ValidCreatureTurn( SOLDIERTYPE * pCreature, INT8 bNewDirection )
2115 {
2116 INT8 bDirChange;
2117 INT8 bTempDir;
2118 INT8 bLoop;
2119 BOOLEAN fFound;
2120
2121 bDirChange = (INT8) QuickestDirection( pCreature->bDirection, bNewDirection );
2122
2123 for( bLoop = 0; bLoop < 2; bLoop++ )
2124 {
2125 fFound = TRUE;
2126
2127 bTempDir = pCreature->bDirection;
2128
2129 do
2130 {
2131
2132 bTempDir += bDirChange;
2133 if (bTempDir < NORTH)
2134 {
2135 bTempDir = NORTHWEST;
2136 }
2137 else if (bTempDir > NORTHWEST)
2138 {
2139 bTempDir = NORTH;
2140 }
2141 if (!InternalIsValidStance( pCreature, bTempDir, ANIM_STAND ))
2142 {
2143 fFound = FALSE;
2144 break;
2145 }
2146
2147 } while ( bTempDir != bNewDirection );
2148
2149 if ( fFound )
2150 {
2151 break;
2152 }
2153 else if ( bLoop > 0 )
2154 {
2155 // can't find a dir!
2156 return( FALSE );
2157 }
2158 else
2159 {
2160 // try the other direction
2161 bDirChange = bDirChange * -1;
2162 }
2163 }
2164
2165 return( TRUE );
2166 }
2167
RangeChangeDesire(const SOLDIERTYPE * pSoldier)2168 INT32 RangeChangeDesire( const SOLDIERTYPE * pSoldier )
2169 {
2170 INT32 iRangeFactorMultiplier;
2171
2172 iRangeFactorMultiplier = pSoldier->bAIMorale - 1;
2173 switch (pSoldier->bAttitude)
2174 {
2175 case DEFENSIVE: iRangeFactorMultiplier += -1; break;
2176 case BRAVESOLO: iRangeFactorMultiplier += 2; break;
2177 case BRAVEAID: iRangeFactorMultiplier += 2; break;
2178 case CUNNINGSOLO: iRangeFactorMultiplier += 0; break;
2179 case CUNNINGAID: iRangeFactorMultiplier += 0; break;
2180 case ATTACKSLAYONLY:
2181 case AGGRESSIVE: iRangeFactorMultiplier += 1; break;
2182 }
2183 if ( gTacticalStatus.bConsNumTurnsWeHaventSeenButEnemyDoes > 0 )
2184 {
2185 iRangeFactorMultiplier += gTacticalStatus.bConsNumTurnsWeHaventSeenButEnemyDoes;
2186 }
2187 return( iRangeFactorMultiplier );
2188 }
2189
ArmySeesOpponents(void)2190 BOOLEAN ArmySeesOpponents( void )
2191 {
2192 CFOR_EACH_IN_TEAM(s, ENEMY_TEAM)
2193 {
2194 if (s->bInSector && s->bLife >= OKLIFE && s->bOppCnt > 0)
2195 {
2196 return TRUE;
2197 }
2198 }
2199
2200 return( FALSE );
2201 }
2202