1 #include "Animation_Control.h"
2 #include "Animation_Data.h"
3 #include "FindLocations.h"
4 #include "Handle_Items.h"
5 #include "Isometric_Utils.h"
6 #include "AI.h"
7 #include "AIInternals.h"
8 #include "LOS.h"
9 #include "Soldier_Profile.h"
10 #include "Structure.h"
11 #include "Weapons.h"
12 #include "OppList.h"
13 #include "PathAI.h"
14 #include "Items.h"
15 #include "World_Items.h"
16 #include "Points.h"
17 #include "Map_Edgepoints.h"
18 #include "RenderWorld.h"
19 #include "Render_Fun.h"
20 #include "Boxing.h"
21 #include "Text.h"
22 #include "Structure_Wrap.h"
23 #ifdef _DEBUG
24 #include "Video.h"
25 #endif
26 #include "WorldMan.h"
27 #include "StrategicMap.h"
28 #include "Environment.h"
29 #include "Lighting.h"
30 #include "Debug.h"
31 #include "PathAIDebug.h"
32
33 #include "ContentManager.h"
34 #include "GameInstance.h"
35 #include "WeaponModels.h"
36
37 #include <algorithm>
38
39 #ifdef _DEBUG
40 INT16 gsCoverValue[WORLD_MAX];
41 INT16 gsBestCover;
42 #ifndef PATHAI_VISIBLE_DEBUG
43 // NB Change this to true to get visible cover debug -- CJC
44 BOOLEAN gfDisplayCoverValues = FALSE;
45 #endif
46 #endif
47
48 INT8 gubAIPathCosts[19][19];
49
50
CalcPercentBetter(INT32 iOldValue,INT32 iNewValue,INT32 iOldScale,INT32 iNewScale)51 static INT32 CalcPercentBetter(INT32 iOldValue, INT32 iNewValue, INT32 iOldScale, INT32 iNewScale)
52 {
53 INT32 iValueChange,iScaleSum,iPercentBetter;//,loopCnt,tempInt;
54
55 // calcalate how much better the new cover would be than the current cover
56 iValueChange = iNewValue - iOldValue;
57
58 // here, the change in cover HAS to be an improvement over current cover
59 if (iValueChange <= 0)
60 {
61 return(NOWHERE);
62 }
63
64 iScaleSum = iOldScale + iNewScale;
65
66 // here, the change in cover HAS to be an improvement over current cover
67 if (iScaleSum <= 0)
68 {
69 return(NOWHERE);
70 }
71 iPercentBetter = (iValueChange * 100) / iScaleSum;
72 SLOGD(ST::format("CalcPercentBetter: %Better {}, old {}, new {}, change {}\noldScale {}, newScale {}, scaleSum {}",
73 iPercentBetter, iOldValue, iNewValue, iValueChange, iOldScale,
74 iNewScale, iScaleSum));
75 return(iPercentBetter);
76 }
77
78
AICenterXY(INT16 sGridNo,FLOAT * pdX,FLOAT * pdY)79 static void AICenterXY(INT16 sGridNo, FLOAT* pdX, FLOAT* pdY)
80 {
81 INT16 sXPos, sYPos;
82
83 sXPos = sGridNo % WORLD_COLS;
84 sYPos = sGridNo / WORLD_COLS;
85
86 *pdX = (FLOAT) (sXPos * CELL_X_SIZE + CELL_X_SIZE / 2);
87 *pdY = (FLOAT) (sYPos * CELL_Y_SIZE + CELL_Y_SIZE / 2);
88 }
89
90
CalcWorstCTGTForPosition(SOLDIERTYPE * const pSoldier,const SOLDIERTYPE * const opponent,const INT16 sOppGridNo,const INT8 bLevel,const INT32 iMyAPsLeft)91 static INT8 CalcWorstCTGTForPosition(SOLDIERTYPE* const pSoldier, const SOLDIERTYPE* const opponent, const INT16 sOppGridNo, const INT8 bLevel, const INT32 iMyAPsLeft)
92 {
93 // When considering a gridno for cover, we want to take into account cover if we
94 // lie down, so we return the LOWEST chance to get through for that location.
95 INT8 bCubeLevel, bThisCTGT,bWorstCTGT = 100;
96
97 for (bCubeLevel = 1; bCubeLevel <= 3; bCubeLevel++)
98 {
99 switch (bCubeLevel)
100 {
101 case 1:
102 if (iMyAPsLeft < AP_CROUCH + AP_PRONE)
103 {
104 continue;
105 }
106 break;
107 case 2:
108 if (iMyAPsLeft < AP_CROUCH)
109 {
110 continue;
111 }
112 break;
113 default:
114 break;
115 }
116
117 bThisCTGT = SoldierToLocationChanceToGetThrough(pSoldier, sOppGridNo, bLevel, bCubeLevel, opponent);
118 if (bThisCTGT < bWorstCTGT)
119 {
120 bWorstCTGT = bThisCTGT;
121 // if there is perfect cover
122 if (bWorstCTGT == 0)
123 // then bail from the for loop, it can't possible get any better
124 break;
125 }
126 }
127 return( bWorstCTGT );
128 }
129
130
CalcAverageCTGTForPosition(SOLDIERTYPE * const pSoldier,const SOLDIERTYPE * const opponent,const INT16 sOppGridNo,const INT8 bLevel,const INT32 iMyAPsLeft)131 static INT8 CalcAverageCTGTForPosition(SOLDIERTYPE* const pSoldier, const SOLDIERTYPE* const opponent, const INT16 sOppGridNo, const INT8 bLevel, const INT32 iMyAPsLeft)
132 {
133 // When considering a gridno for cover, we want to take into account cover if we
134 // lie down, so we return the LOWEST chance to get through for that location.
135 INT8 bCubeLevel;
136 INT32 iTotalCTGT = 0;
137 INT32 bValidCubeLevels = 0;
138
139 for (bCubeLevel = 1; bCubeLevel <= 3; bCubeLevel++)
140 {
141 switch (bCubeLevel)
142 {
143 case 1:
144 if (iMyAPsLeft < AP_CROUCH + AP_PRONE)
145 {
146 continue;
147 }
148 break;
149 case 2:
150 if (iMyAPsLeft < AP_CROUCH)
151 {
152 continue;
153 }
154 break;
155 default:
156 break;
157 }
158 iTotalCTGT += SoldierToLocationChanceToGetThrough(pSoldier, sOppGridNo, bLevel, bCubeLevel, opponent);
159 bValidCubeLevels++;
160 }
161 iTotalCTGT /= bValidCubeLevels;
162 return( (INT8) iTotalCTGT );
163 }
164
165
CalcBestCTGT(SOLDIERTYPE * const pSoldier,const SOLDIERTYPE * const opponent,const INT16 sOppGridNo,const INT8 bLevel,const INT32 iMyAPsLeft)166 static INT8 CalcBestCTGT(SOLDIERTYPE* const pSoldier, const SOLDIERTYPE* const opponent, const INT16 sOppGridNo, const INT8 bLevel, const INT32 iMyAPsLeft)
167 {
168 // NOTE: CTGT stands for "ChanceToGetThrough..."
169
170 // using only ints for maximum execution speed here
171 // CJC: Well, so much for THAT idea!
172 INT16 sCentralGridNo, sAdjSpot, sNorthGridNo, sSouthGridNo, sDir, sCheckSpot;
173
174 INT8 bThisCTGT, bBestCTGT = 0;
175
176 sCheckSpot = -1;
177
178 sCentralGridNo = pSoldier->sGridNo;
179
180 // precalculate these for speed
181 // what was struct for?
182 sNorthGridNo = NewGridNo( sCentralGridNo, DirectionInc(NORTH) );
183 sSouthGridNo = NewGridNo( sCentralGridNo, DirectionInc(SOUTH) );
184
185 // look into all 8 adjacent tiles & determine where the cover is the worst
186 for (sDir = 1; sDir <= 8; sDir++)
187 {
188 // get the gridno of the adjacent spot lying in that direction
189 sAdjSpot = NewGridNo( sCentralGridNo, DirectionInc( sDir ) );
190
191 // if it wasn't out of bounds
192 if (sAdjSpot != sCentralGridNo)
193 {
194 // if the adjacent spot can we walked on and isn't in water or gas
195 if ((NewOKDestination( pSoldier, sAdjSpot, IGNOREPEOPLE, bLevel ) > 0) && !InWaterOrGas( pSoldier, sAdjSpot ))
196 {
197 switch (sDir)
198 {
199 case NORTH:
200 case EAST:
201 case SOUTH:
202 case WEST:
203 sCheckSpot = sAdjSpot;
204 break;
205 case NORTHEAST:
206 case NORTHWEST:
207 // spot to the NORTH is guaranteed to be in bounds since NE/NW was
208 sCheckSpot = sNorthGridNo;
209 break;
210 case SOUTHEAST:
211 case SOUTHWEST:
212 // spot to the SOUTH is guaranteed to be in bounds since SE/SW was
213 sCheckSpot = sSouthGridNo;
214 break;
215 }
216
217 // ATE: OLD STUFF
218 // if the adjacent gridno is reachable from the starting spot
219 if ( NewOKDestination( pSoldier, sCheckSpot, FALSE, bLevel ) )
220 {
221 // the dude could move to this adjacent gridno, so put him there
222 // "virtually" so we can calculate what our cover is from there
223
224 // NOTE: GOTTA SET THESE 3 FIELDS *BACK* AFTER USING THIS FUNCTION!!!
225 pSoldier->sGridNo = sAdjSpot; // pretend he's standing at 'sAdjSpot'
226 AICenterXY( sAdjSpot, &(pSoldier->dXPos), &(pSoldier->dYPos) );
227 bThisCTGT = CalcWorstCTGTForPosition(pSoldier, opponent, sOppGridNo, bLevel, iMyAPsLeft);
228 if (bThisCTGT > bBestCTGT)
229 {
230 bBestCTGT = bThisCTGT;
231 // if there is no cover
232 if (bBestCTGT == 100)
233 // then bail from the for loop, it can't possible get any better
234 break;
235 }
236 }
237 }
238 }
239 }
240
241 return( bBestCTGT );
242 }
243
244
CalcCoverValue(SOLDIERTYPE * pMe,INT16 sMyGridNo,INT32 iMyThreat,INT32 iMyAPsLeft,UINT32 uiThreatIndex,INT32 iRange,INT32 morale,INT32 * iTotalScale)245 static INT32 CalcCoverValue(SOLDIERTYPE* pMe, INT16 sMyGridNo, INT32 iMyThreat, INT32 iMyAPsLeft, UINT32 uiThreatIndex, INT32 iRange, INT32 morale, INT32* iTotalScale)
246 {
247 // all 32-bit integers for max. speed
248 INT32 iMyPosValue, iHisPosValue, iCoverValue;
249 INT32 iReductionFactor, iThisScale;
250 INT16 sHisGridNo, sMyRealGridNo = NOWHERE, sHisRealGridNo = NOWHERE;
251 INT16 sTempX, sTempY;
252 FLOAT dMyX, dMyY, dHisX, dHisY;
253 INT8 bHisBestCTGT, bHisActualCTGT, bHisCTGT, bMyCTGT;
254 INT32 iRangeChange, iRangeFactor, iRangeFactorMultiplier;
255 SOLDIERTYPE *pHim;
256
257 dMyX = dMyY = dHisX = dHisY = -1.0;
258
259 pHim = Threat[uiThreatIndex].pOpponent;
260 sHisGridNo = Threat[uiThreatIndex].sGridNo;
261
262 // THE FOLLOWING STUFF IS *VEERRRY SCAARRRY*, BUT SHOULD WORK. IF YOU REALLY
263 // HATE IT, THEN CHANGE ChanceToGetThrough() TO WORK FROM A GRIDNO TO GRIDNO
264
265 // if this is theoretical, and I'm not actually at sMyGridNo right now
266 if (pMe->sGridNo != sMyGridNo)
267 {
268 sMyRealGridNo = pMe->sGridNo; // remember where I REALLY am
269 dMyX = pMe->dXPos;
270 dMyY = pMe->dYPos;
271
272 pMe->sGridNo = sMyGridNo; // but pretend I'm standing at sMyGridNo
273 ConvertGridNoToCenterCellXY( sMyGridNo, &sTempX, &sTempY );
274 pMe->dXPos = (FLOAT) sTempX;
275 pMe->dYPos = (FLOAT) sTempY;
276 }
277
278 // if this is theoretical, and he's not actually at hisGrid right now
279 if (pHim->sGridNo != sHisGridNo)
280 {
281 sHisRealGridNo = pHim->sGridNo; // remember where he REALLY is
282 dHisX = pHim->dXPos;
283 dHisY = pHim->dYPos;
284
285 pHim->sGridNo = sHisGridNo; // but pretend he's standing at sHisGridNo
286 ConvertGridNoToCenterCellXY( sHisGridNo, &sTempX, &sTempY );
287 pHim->dXPos = (FLOAT) sTempX;
288 pHim->dYPos = (FLOAT) sTempY;
289 }
290
291
292 if (InWaterOrGas(pHim,sHisGridNo))
293 {
294 bHisActualCTGT = 0;
295 }
296 else
297 {
298 // optimistically assume we'll be behind the best cover available at this spot
299
300 //bHisActualCTGT = ChanceToGetThrough(pHim,sMyGridNo,FAKE,ACTUAL,TESTWALLS,9999,M9PISTOL,NOT_FOR_LOS); // assume a gunshot
301 bHisActualCTGT = CalcWorstCTGTForPosition(pHim, pMe, sMyGridNo, pMe->bLevel, iMyAPsLeft);
302 }
303
304 // normally, that will be the cover I'll use, unless worst case over-rides it
305 bHisCTGT = bHisActualCTGT;
306
307 // only calculate his best case CTGT if there is room for improvement!
308 if (bHisActualCTGT < 100)
309 {
310 // if we didn't remember his real gridno earlier up above, we got to now,
311 // because calculating worst case is about to play with it in a big way!
312 if (sHisRealGridNo == NOWHERE)
313 {
314 sHisRealGridNo = pHim->sGridNo; // remember where he REALLY is
315 dHisX = pHim->dXPos;
316 dHisY = pHim->dYPos;
317 }
318
319 // calculate where my cover is worst if opponent moves just 1 tile over
320 bHisBestCTGT = CalcBestCTGT(pHim, pMe, sMyGridNo, pMe->bLevel, iMyAPsLeft);
321
322 // if he can actually improve his CTGT by moving to a nearby gridno
323 if (bHisBestCTGT > bHisActualCTGT)
324 {
325 // he may not take advantage of his best case, so take only 2/3 of best
326 bHisCTGT = ((2 * bHisBestCTGT) + bHisActualCTGT) / 3;
327 }
328 }
329
330 // if my intended gridno is in water or gas, I can't attack at all from there
331 // here, for smoke, consider bad
332 if (InWaterGasOrSmoke(pMe,sMyGridNo))
333 {
334 bMyCTGT = 0;
335 }
336 else
337 {
338 // put him at sHisGridNo if necessary!
339 if (pHim->sGridNo != sHisGridNo )
340 {
341 pHim->sGridNo = sHisGridNo;
342 ConvertGridNoToCenterCellXY( sHisGridNo, &sTempX, &sTempY );
343 pHim->dXPos = (FLOAT) sTempX;
344 pHim->dYPos = (FLOAT) sTempY;
345 }
346 // bMyCTGT = ChanceToGetThrough(pMe,sHisGridNo,FAKE,ACTUAL,TESTWALLS,9999,M9PISTOL,NOT_FOR_LOS); // assume a gunshot
347 // bMyCTGT = SoldierToLocationChanceToGetThrough( pMe, sHisGridNo, pMe->bTargetLevel, pMe->bTargetCubeLevel );
348
349 // let's not assume anything about the stance the enemy might take, so take an average
350 // value... no cover give a higher value than partial cover
351 bMyCTGT = CalcAverageCTGTForPosition(pMe, pHim, sHisGridNo, pHim->bLevel, iMyAPsLeft);
352
353 // since NPCs are too dumb to shoot "blind", ie. at opponents that they
354 // themselves can't see (mercs can, using another as a spotter!), if the
355 // cover is below the "see_thru" threshold, it's equivalent to perfect cover!
356 if (bMyCTGT < SEE_THRU_COVER_THRESHOLD)
357 {
358 bMyCTGT = 0;
359 }
360 }
361
362 // UNDO ANY TEMPORARY "DAMAGE" DONE ABOVE
363 if (sMyRealGridNo != NOWHERE)
364 {
365 pMe->sGridNo = sMyRealGridNo; // put me back where I belong!
366 pMe->dXPos = dMyX; // also change the 'x'
367 pMe->dYPos = dMyY; // and the 'y'
368 }
369
370 if (sHisRealGridNo != NOWHERE)
371 {
372 pHim->sGridNo = sHisRealGridNo; // put HIM back where HE belongs!
373 pHim->dXPos = dHisX; // also change the 'x'
374 pHim->dYPos = dHisY; // and the 'y'
375 }
376
377
378 // these value should be < 1 million each
379 iHisPosValue = bHisCTGT * Threat[uiThreatIndex].iValue * Threat[uiThreatIndex].iAPs;
380 iMyPosValue = bMyCTGT * iMyThreat * iMyAPsLeft;
381
382
383 // try to account for who outnumbers who: the side with the advantage thus
384 // (hopefully) values offense more, while those in trouble will play defense
385 if (pHim->bOppCnt > 1)
386 {
387 iHisPosValue /= pHim->bOppCnt;
388 }
389
390 if (pMe->bOppCnt > 1)
391 {
392 iMyPosValue /= pMe->bOppCnt;
393 }
394
395
396 // if my positional value is worth something at all here
397 if (iMyPosValue > 0)
398 {
399 // if I CAN'T crouch when I get there, that makes it significantly less
400 // appealing a spot (how much depends on range), so that's a penalty to me
401 if (iMyAPsLeft < AP_CROUCH)
402 // subtract another 1 % penalty for NOT being able to crouch per tile
403 // the farther away we are, the bigger a difference crouching will make!
404 iMyPosValue -= ((iMyPosValue * (AIM_PENALTY_TARGET_CROUCHED + (iRange / CELL_X_SIZE))) / 100);
405 }
406
407
408 // high morale prefers decreasing the range (positive factor), while very
409 // low morale (HOPELESS) prefers increasing it
410
411 //if (bHisCTGT < 100 || (morale - 1 < 0))
412 {
413
414 iRangeFactorMultiplier = RangeChangeDesire( pMe );
415
416 if (iRangeFactorMultiplier)
417 {
418 iRangeChange = Threat[uiThreatIndex].iOrigRange - iRange;
419
420 if (iRangeChange)
421 {
422 //iRangeFactor = (iRangeChange * (morale - 1)) / 4;
423 iRangeFactor = (iRangeChange * iRangeFactorMultiplier) / 2;
424 SLOGD("CalcCoverValue: iRangeChange %d, iRangeFactor %d\n",
425 iRangeChange, iRangeFactor);
426
427 // aggression booster for stupider enemies
428 iMyPosValue += 100 * iRangeFactor * ( 5 - SoldierDifficultyLevel( pMe ) ) / 5 ;
429
430 // if factor is positive increase positional value, else decrease it
431 // change both values, since one or the other could be 0
432 if (iRangeFactor > 0)
433 {
434 iMyPosValue = (iMyPosValue * (100 + iRangeFactor)) / 100;
435 iHisPosValue = (100 * iHisPosValue) / (100 + iRangeFactor);
436 }
437 else if (iRangeFactor < 0)
438 {
439 iMyPosValue = (100 * iMyPosValue) / (100 - iRangeFactor);
440 iHisPosValue = (iHisPosValue * (100 - iRangeFactor)) / 100;
441 }
442 }
443 }
444 }
445
446 // the farther apart we are, the less important the cover differences are
447 // the less certain his position, the less important cover differences are
448 iReductionFactor = ((MAX_THREAT_RANGE - iRange) * Threat[uiThreatIndex].iCertainty) /
449 MAX_THREAT_RANGE;
450
451 // divide by a 100 to make the numbers more managable and avoid 32-bit limit
452 iThisScale = MAX( iMyPosValue, iHisPosValue) / 100;
453 iThisScale = (iThisScale * iReductionFactor) / 100;
454 *iTotalScale += iThisScale;
455 // this helps to decide the percent improvement later
456
457 // POSITIVE COVER VALUE INDICATES THE COVER BENEFITS ME, NEGATIVE RESULT
458 // MEANS IT BENEFITS THE OTHER GUY.
459 // divide by a 100 to make the numbers more managable and avoid 32-bit limit
460 iCoverValue = (iMyPosValue - iHisPosValue) / 100;
461 iCoverValue = (iCoverValue * iReductionFactor) / 100;
462
463 SLOGD("CalcCoverValue: iCoverValue %d, sMyGridNo %d, sHisGrid %d, iRange %d, morale %d",
464 iCoverValue, sMyGridNo, sHisGridNo, iRange, morale);
465 SLOGD("CalcCoverValue: iCertainty %d, his bOppCnt %d, my bOppCnt %d",
466 Threat[uiThreatIndex].iCertainty, pHim->bOppCnt, pMe->bOppCnt);
467 SLOGD("CalcCoverValue: bHisCTGT = %d, hisThreat = %d, hisFullAPs = %d",
468 bHisCTGT, Threat[uiThreatIndex].iValue, Threat[uiThreatIndex].iAPs);
469 SLOGD("CalcCoverValue: bMyCTGT = %d, iMyThreat = %d, iMyAPsLeft = %d",
470 bMyCTGT, iMyThreat, iMyAPsLeft);
471 SLOGD("CalcCoverValue: hisPosValue = %d, myPosValue = %d",
472 iHisPosValue, iMyPosValue);
473 SLOGD("CalcCoverValue: iThisScale = %d, iTotalScale = %d, iReductionFactor %d",
474 iThisScale, *iTotalScale, iReductionFactor);
475
476 return( iCoverValue );
477 }
478
479
NumberOfTeamMatesAdjacent(SOLDIERTYPE * pSoldier,INT16 sGridNo)480 static UINT8 NumberOfTeamMatesAdjacent(SOLDIERTYPE* pSoldier, INT16 sGridNo)
481 {
482 INT16 sTempGridNo;
483
484 UINT8 ubCount = 0;
485
486 for (UINT8 ubLoop = 0; ubLoop < NUM_WORLD_DIRECTIONS; ++ubLoop)
487 {
488 sTempGridNo = NewGridNo( sGridNo, DirectionInc( ubLoop ) );
489 if ( sTempGridNo != sGridNo )
490 {
491 const SOLDIERTYPE* const tgt = WhoIsThere2(sGridNo, pSoldier->bLevel);
492 if (tgt != NULL && tgt != pSoldier && tgt->bTeam == pSoldier->bTeam)
493 {
494 ubCount++;
495 }
496 }
497 }
498
499 return( ubCount );
500 }
501
FindBestNearbyCover(SOLDIERTYPE * pSoldier,INT32 morale,INT32 * piPercentBetter)502 INT16 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentBetter)
503 {
504 // all 32-bit integers for max. speed
505 INT32 iCurrentCoverValue, iCoverValue, iBestCoverValue;
506 INT32 iCurrentScale, iCoverScale;
507 INT32 iDistFromOrigin, iDistCoverFromOrigin, iThreatCertainty;
508 INT16 sGridNo, sBestCover = NOWHERE;
509 INT32 iPathCost;
510 INT32 iThreatRange, iClosestThreatRange = 1500;
511 // INT16 sClosestThreatGridno = NOWHERE;
512 INT32 iMyThreatValue;
513 INT16 sThreatLoc;
514 INT32 iMaxThreatRange;
515 UINT32 uiThreatCnt = 0;
516 INT32 iMaxMoveTilesLeft, iSearchRange, iRoamRange;
517 INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXOffset, sYOffset;
518 INT16 sOrigin; // has to be a short, need a pointer
519 INT16 *pusLastLoc;
520 INT8 *pbPersOL;
521 INT8 *pbPublOL;
522
523 UINT8 ubBackgroundLightLevel;
524 UINT8 ubBackgroundLightPercent = 0;
525 UINT8 ubLightPercentDifference;
526 BOOLEAN fNight;
527
528 INT32 iBestCoverScale = 0; // XXX HACK000E
529
530 bool const fHasGasMask = IsWearingHeadGear(*pSoldier, GASMASK);
531
532 if ( gbWorldSectorZ > 0 )
533 {
534 fNight = FALSE;
535 }
536 else
537 {
538 ubBackgroundLightLevel = GetTimeOfDayAmbientLightLevel();
539
540 if ( ubBackgroundLightLevel < NORMAL_LIGHTLEVEL_DAY + 2 )
541 {
542 fNight = FALSE;
543 }
544 else
545 {
546 fNight = TRUE;
547 ubBackgroundLightPercent = gbLightSighting[ 0 ][ ubBackgroundLightLevel ];
548 }
549 }
550
551
552 iBestCoverValue = -1;
553
554 #if defined( _DEBUG ) && !defined( PATHAI_VISIBLE_DEBUG )
555 if (gfDisplayCoverValues)
556 {
557 std::fill_n(gsCoverValue, WORLD_MAX, 0x7F7F);
558 }
559 #endif
560
561 // BUILD A LIST OF THREATENING GRID #s FROM PERSONAL & PUBLIC opplists
562
563 pusLastLoc = &(gsLastKnownOppLoc[pSoldier->ubID][0]);
564
565 // hang a pointer into personal opplist
566 pbPersOL = &(pSoldier->bOppList[0]);
567 // hang a pointer into public opplist
568 pbPublOL = &(gbPublicOpplist[pSoldier->bTeam][0]);
569
570 // decide how far we're gonna be looking
571 iSearchRange = gbDiff[DIFF_MAX_COVER_RANGE][ SoldierDifficultyLevel( pSoldier ) ];
572
573 /*
574 switch (pSoldier->bAttitude)
575 {
576 case DEFENSIVE: iSearchRange += 2; break;
577 case BRAVESOLO: iSearchRange -= 4; break;
578 case BRAVEAID: iSearchRange -= 4; break;
579 case CUNNINGSOLO: iSearchRange += 4; break;
580 case CUNNINGAID: iSearchRange += 4; break;
581 case AGGRESSIVE: iSearchRange -= 2; break;
582 }*/
583
584
585 // maximum search range is 1 tile / 8 pts of wisdom
586 if (iSearchRange > (pSoldier->bWisdom / 8))
587 {
588 iSearchRange = (pSoldier->bWisdom / 8);
589 }
590
591 if (!gfTurnBasedAI)
592 {
593 // don't search so far in realtime
594 iSearchRange /= 2;
595 }
596
597 if (pSoldier->bAlertStatus >= STATUS_RED) // if already in battle
598 {
599 UINT16 const usMovementMode = DetermineMovementMode(pSoldier, AI_ACTION_TAKE_COVER);
600
601 // must be able to reach the cover, so it can't possibly be more than
602 // action points left (rounded down) tiles away, since minimum
603 // cost to move per tile is 1 points.
604 iMaxMoveTilesLeft = __max( 0, pSoldier->bActionPoints - MinAPsToStartMovement( pSoldier, usMovementMode ) );
605
606 // if we can't go as far as the usual full search range
607 if (iMaxMoveTilesLeft < iSearchRange)
608 {
609 // then limit the search range to only as far as we CAN go
610 iSearchRange = iMaxMoveTilesLeft;
611 }
612 }
613
614 if (iSearchRange <= 0)
615 {
616 return(NOWHERE);
617 }
618
619 // those within 20 tiles of any tile we'll CONSIDER as cover are important
620 iMaxThreatRange = MAX_THREAT_RANGE + (CELL_X_SIZE * iSearchRange);
621
622 // calculate OUR OWN general threat value (not from any specific location)
623 iMyThreatValue = CalcManThreatValue(pSoldier,NOWHERE,FALSE,pSoldier);
624
625 // look through all opponents for those we know of
626 FOR_EACH_MERC(i)
627 {
628 SOLDIERTYPE* const pOpponent = *i;
629
630 // if this merc is inactive, at base, on assignment, dead, unconscious
631 if (pOpponent->bLife < OKLIFE) continue; // next merc
632
633 // if this man is neutral / on the same side, he's not an opponent
634 if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide))
635 {
636 continue; // next merc
637 }
638
639 pbPersOL = pSoldier->bOppList + pOpponent->ubID;
640 pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID;
641 pusLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpponent->ubID;
642
643 // if this opponent is unknown personally and publicly
644 if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN))
645 {
646 continue; // next merc
647 }
648
649 // Special stuff for Carmen the bounty hunter
650 if (pSoldier->bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != SLAY)
651 {
652 continue; // next opponent
653 }
654
655 // if personal knowledge is more up to date or at least equal
656 if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) ||
657 (*pbPersOL == *pbPublOL))
658 {
659 // using personal knowledge, obtain opponent's "best guess" gridno
660 sThreatLoc = *pusLastLoc;
661 iThreatCertainty = ThreatPercent[*pbPersOL - OLDEST_HEARD_VALUE];
662 }
663 else
664 {
665 // using public knowledge, obtain opponent's "best guess" gridno
666 sThreatLoc = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID];
667 iThreatCertainty = ThreatPercent[*pbPublOL - OLDEST_HEARD_VALUE];
668 }
669
670 // calculate how far away this threat is (in adjusted pixels)
671 //iThreatRange = AdjPixelsAway(CenterX(pSoldier->sGridNo),CenterY(pSoldier->sGridNo),CenterX(sThreatLoc),CenterY(sThreatLoc));
672 iThreatRange = GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, sThreatLoc );
673
674 // if this opponent is believed to be too far away to really be a threat
675 if (iThreatRange > iMaxThreatRange)
676 {
677 continue; // check next opponent
678 }
679
680 // remember this opponent as a current threat, but DON'T REDUCE FOR COVER!
681 Threat[uiThreatCnt].iValue = CalcManThreatValue(pOpponent,pSoldier->sGridNo,FALSE,pSoldier);
682
683 // if the opponent is no threat at all for some reason
684 if (Threat[uiThreatCnt].iValue == -999)
685 {
686 continue; // check next opponent
687 }
688
689 Threat[uiThreatCnt].pOpponent = pOpponent;
690 Threat[uiThreatCnt].sGridNo = sThreatLoc;
691 Threat[uiThreatCnt].iCertainty = iThreatCertainty;
692 Threat[uiThreatCnt].iOrigRange = iThreatRange;
693
694 // calculate how many APs he will have at the start of the next turn
695 Threat[uiThreatCnt].iAPs = CalcActionPoints(pOpponent);
696
697 if (iThreatRange < iClosestThreatRange)
698 {
699 iClosestThreatRange = iThreatRange;
700 // sClosestThreatGridNo = sThreatLoc;
701 }
702
703 uiThreatCnt++;
704 }
705
706 // if no known opponents were found to threaten us, can't worry about cover
707 if (!uiThreatCnt)
708 {
709 return(sBestCover);
710 }
711
712 // calculate our current cover value in the place we are now, since the
713 // cover we are searching for must be better than what we have now!
714 iCurrentCoverValue = 0;
715 iCurrentScale = 0;
716
717 // for every opponent that threatens, consider this spot's cover vs. him
718 for (UINT32 uiLoop = 0; uiLoop < uiThreatCnt; ++uiLoop)
719 {
720 // if this threat is CURRENTLY within 20 tiles
721 if (Threat[uiLoop].iOrigRange <= MAX_THREAT_RANGE)
722 {
723 // add this opponent's cover value to our current total cover value
724 iCurrentCoverValue += CalcCoverValue(pSoldier,pSoldier->sGridNo,iMyThreatValue,pSoldier->bActionPoints,uiLoop,Threat[uiLoop].iOrigRange,morale,&iCurrentScale);
725 }
726 }
727
728 iCurrentCoverValue -= (iCurrentCoverValue / 10) * NumberOfTeamMatesAdjacent( pSoldier, pSoldier->sGridNo );
729
730 // determine maximum horizontal limits
731 sMaxLeft = MIN(iSearchRange,(pSoldier->sGridNo % MAXCOL));
732 sMaxRight = MIN(iSearchRange,MAXCOL - ((pSoldier->sGridNo % MAXCOL) + 1));
733
734 // determine maximum vertical limits
735 sMaxUp = MIN(iSearchRange,(pSoldier->sGridNo / MAXROW));
736 sMaxDown = MIN(iSearchRange,MAXROW - ((pSoldier->sGridNo / MAXROW) + 1));
737
738 iRoamRange = RoamingRange(pSoldier,&sOrigin);
739
740 // if status isn't black (life & death combat), and roaming range is limited
741 if ((pSoldier->bAlertStatus != STATUS_BLACK) && (iRoamRange < MAX_ROAMING_RANGE) &&
742 (sOrigin != NOWHERE))
743 {
744 // must try to stay within or return to the point of origin
745 iDistFromOrigin = SpacesAway(sOrigin,pSoldier->sGridNo);
746 }
747 else
748 {
749 // don't care how far from origin we go
750 iDistFromOrigin = -1;
751 }
752 SLOGD("FBNC: iRoamRange %d, sMaxLeft %d, sMaxRight %d, sMaxUp %d, sMaxDown %d",
753 iRoamRange, sMaxLeft, sMaxRight, sMaxUp, sMaxDown);
754
755 // the initial cover value to beat is our current cover value
756 iBestCoverValue = iCurrentCoverValue;
757 SLOGD("FBNC: CURRENT iCoverValue = %d\n",iCurrentCoverValue);
758
759 if (pSoldier->bAlertStatus >= STATUS_RED) // if already in battle
760 {
761 // to speed this up, tell PathAI to cancel any paths beyond our AP reach!
762 gubNPCAPBudget = pSoldier->bActionPoints;
763 }
764 else
765 {
766 // even if not under pressure, limit to 1 turn's travelling distance
767 // hope this isn't too expensive...
768 gubNPCAPBudget = CalcActionPoints( pSoldier );
769 //gubNPCAPBudget = pSoldier->bInitialAPs;
770 }
771
772 // Call FindBestPath to set flags in all locations that we can
773 // walk into within range. We have to set some things up first...
774
775 // set the distance limit of the square region
776 gubNPCDistLimit = (UINT8) iSearchRange;
777
778 // reset the "reachable" flags in the region we're looking at
779 for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++)
780 {
781 for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++)
782 {
783 sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset);
784 if ( !(sGridNo >=0 && sGridNo < WORLD_MAX) )
785 {
786 continue;
787 }
788 gpWorldLevelData[sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE);
789 }
790 }
791
792 FindBestPath( pSoldier, NOWHERE, pSoldier->bLevel, DetermineMovementMode( pSoldier, AI_ACTION_TAKE_COVER ), COPYREACHABLE_AND_APS, 0 );
793
794 // Turn off the "reachable" flag for his current location
795 // so we don't consider it
796 gpWorldLevelData[pSoldier->sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE);
797
798 // SET UP DOUBLE-LOOP TO STEP THROUGH POTENTIAL GRID #s
799 for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++)
800 {
801 for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++)
802 {
803 //HandleMyMouseCursor(KEYBOARDALSO);
804
805 // calculate the next potential gridno
806 sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset);
807 if ( !(sGridNo >=0 && sGridNo < WORLD_MAX) )
808 {
809 continue;
810 }
811
812 // if we are limited to staying/returning near our place of origin
813 if (iDistFromOrigin != -1)
814 {
815 iDistCoverFromOrigin = SpacesAway(sOrigin,sGridNo);
816
817 // if this is outside roaming range, and doesn't get us closer to it
818 if ((iDistCoverFromOrigin > iRoamRange) &&
819 (iDistFromOrigin <= iDistCoverFromOrigin))
820 {
821 continue; // then we can't go there
822 }
823 }
824
825 if (!(gpWorldLevelData[sGridNo].uiFlags & MAPELEMENT_REACHABLE))
826 {
827 continue;
828 }
829
830 if ( !fHasGasMask )
831 {
832 if ( InGas( pSoldier, sGridNo ) )
833 {
834 continue;
835 }
836 }
837
838 // ignore blacklisted spot
839 if ( sGridNo == pSoldier->sBlackList )
840 {
841 continue;
842 }
843
844 iPathCost = gubAIPathCosts[AI_PATHCOST_RADIUS + sXOffset][AI_PATHCOST_RADIUS + sYOffset];
845 /*
846 // water is OK, if the only good hiding place requires us to get wet, OK
847 iPathCost = LegalNPCDestination(pSoldier,sGridNo,ENSURE_PATH_COST,WATEROK);
848
849 if (!iPathCost)
850 {
851 continue; // skip on to the next potential grid
852 }
853
854 // CJC: This should be a redundent check because the path code is given an
855 // AP limit to begin with!
856 if (pSoldier->bAlertStatus == STATUS_BLACK) // in battle
857 {
858 // must be able to afford the APs to get to this cover this turn
859 if (iPathCost > pSoldier->bActionPoints)
860 {
861 continue; // skip on to the next potential grid
862 }
863 }
864 */
865
866 // OK, this place shows potential. How useful is it as cover?
867 // EVALUATE EACH GRID #, remembering the BEST PROTECTED ONE
868 iCoverValue = 0;
869 iCoverScale = 0;
870
871 // for every opponent that threatens, consider this spot's cover vs. him
872 for (UINT32 uiLoop = 0; uiLoop < uiThreatCnt; ++uiLoop)
873 {
874 // calculate the range we would be at from this opponent
875 iThreatRange = GetRangeInCellCoordsFromGridNoDiff( sGridNo, Threat[uiLoop].sGridNo );
876 // if this threat would be within 20 tiles, count it
877 if (iThreatRange <= MAX_THREAT_RANGE)
878 {
879 iCoverValue += CalcCoverValue(pSoldier,sGridNo,iMyThreatValue,
880 (pSoldier->bActionPoints - iPathCost),
881 uiLoop,iThreatRange,morale,&iCoverScale);
882 }
883 }
884
885 // reduce cover for each person adjacent to this gridno who is on our team,
886 // by 10% (so locations next to several people will be very much frowned upon
887 if ( iCoverValue >= 0 )
888 {
889 iCoverValue -= (iCoverValue / 10) * NumberOfTeamMatesAdjacent( pSoldier, sGridNo );
890 }
891 else
892 {
893 // when negative, must add a negative to decrease the total
894 iCoverValue += (iCoverValue / 10) * NumberOfTeamMatesAdjacent( pSoldier, sGridNo );
895 }
896
897 if (fNight && GetRoom(sGridNo) == NO_ROOM) // ignore in buildings in case placed there
898 {
899 // reduce cover at nighttime based on how bright the light is at that location
900 // using the difference in sighting distance between the background and the
901 // light for this tile
902 ubLightPercentDifference = (gbLightSighting[ 0 ][ LightTrueLevel( sGridNo, pSoldier->bLevel ) ] - ubBackgroundLightPercent );
903 if ( iCoverValue >= 0 )
904 {
905 iCoverValue -= (iCoverValue / 100) * ubLightPercentDifference;
906 }
907 else
908 {
909 iCoverValue += (iCoverValue / 100) * ubLightPercentDifference;
910 }
911 }
912
913 // if there ARE multiple opponents
914 if (uiThreatCnt > 1)
915 {
916 SLOGD("FBNC: Total iCoverValue at gridno %d is %d",
917 sGridNo, iCoverValue);
918 }
919
920 #if defined( _DEBUG ) && !defined( PATHAI_VISIBLE_DEBUG )
921 if (gfDisplayCoverValues)
922 {
923 gsCoverValue[sGridNo] = (INT16) (iCoverValue / 100);
924 }
925 #endif
926
927 // if this is better than the best place found so far
928
929 if (iCoverValue > iBestCoverValue)
930 {
931 SLOGD("FBNC: NEW BEST iCoverValue at gridno %d is %d",
932 sGridNo, iCoverValue);
933 // remember it instead
934 sBestCover = sGridNo;
935 iBestCoverValue = iCoverValue;
936 iBestCoverScale = iCoverScale;
937 }
938 }
939 }
940
941 gubNPCAPBudget = 0;
942 gubNPCDistLimit = 0;
943
944 #if defined( _DEBUG ) && !defined( PATHAI_VISIBLE_DEBUG )
945 if (gfDisplayCoverValues)
946 {
947 // do a locate?
948 LocateSoldier(pSoldier, SETLOCATORFAST);
949 gsBestCover = sBestCover;
950 SetRenderFlags( RENDER_FLAG_FULL );
951 RenderWorld();
952 RenderCoverDebug( );
953 InvalidateScreen( );
954 RefreshScreen();
955 /*
956 iLoop = GetJA2Clock();
957 do
958 {
959
960 } while( ( GetJA2Clock( ) - iLoop ) < 2000 );
961 */
962 }
963 #endif
964
965 // if a better cover location was found
966 if (sBestCover != NOWHERE)
967 {
968 #if defined( _DEBUG ) && !defined( PATHAI_VISIBLE_DEBUG )
969 gsBestCover = sBestCover;
970 #endif
971 // cover values already take the AP cost of getting there into account in
972 // a BIG way, so no need to worry about that here, even small improvements
973 // are actually very significant once we get our APs back (if we live!)
974 *piPercentBetter = CalcPercentBetter(iCurrentCoverValue,iBestCoverValue,iCurrentScale,iBestCoverScale);
975
976 // if best cover value found was at least 5% better than our current cover
977 if (*piPercentBetter >= MIN_PERCENT_BETTER)
978 {
979 SLOGD(ST::format("Found Cover: current {}, best {}, %Better {}",
980 iCurrentCoverValue, iBestCoverValue, *piPercentBetter));
981 return((INT16)sBestCover); // return the gridno of that cover
982 }
983 }
984 return(NOWHERE); // return that no suitable cover was found
985 }
986
FindSpotMaxDistFromOpponents(SOLDIERTYPE * pSoldier)987 INT16 FindSpotMaxDistFromOpponents(SOLDIERTYPE *pSoldier)
988 {
989 INT16 sGridNo;
990 INT16 sBestSpot = NOWHERE;
991 INT32 iThreatRange,iClosestThreatRange = 1500, iSpotClosestThreatRange;
992 INT16 sThreatLoc, sThreatGridNo[MAXMERCS];
993 UINT32 uiThreatCnt = 0;
994 INT32 iSearchRange;
995 INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXOffset, sYOffset;
996 INT8 * pbPersOL,*pbPublOL, bEscapeDirection, bBestEscapeDirection = -1;
997 INT16 sOrigin;
998 INT32 iRoamRange;
999
1000 bool const fHasGasMask = IsWearingHeadGear(*pSoldier, GASMASK);
1001
1002 // BUILD A LIST OF THREATENING GRID #s FROM PERSONAL & PUBLIC opplistS
1003
1004 // look through all opponents for those we know of
1005 FOR_EACH_MERC(i)
1006 {
1007 SOLDIERTYPE* const pOpponent = *i;
1008
1009 // if this merc is inactive, at base, on assignment, dead, unconscious
1010 if (pOpponent->bLife < OKLIFE) continue; // next merc
1011
1012 // if this man is neutral / on the same side, he's not an opponent
1013 if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide))
1014 {
1015 continue; // next merc
1016 }
1017
1018 pbPersOL = &(pSoldier->bOppList[pOpponent->ubID]);
1019 pbPublOL = &(gbPublicOpplist[pSoldier->bTeam][pOpponent->ubID]);
1020
1021 // if this opponent is unknown personally and publicly
1022 if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN))
1023 {
1024 continue; // check next opponent
1025 }
1026
1027 // Special stuff for Carmen the bounty hunter
1028 if (pSoldier->bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != SLAY)
1029 {
1030 continue; // next opponent
1031 }
1032
1033 // if the opponent is no threat at all for some reason
1034 if (CalcManThreatValue(pOpponent,pSoldier->sGridNo,FALSE,pSoldier) == -999)
1035 {
1036 continue; // check next opponent
1037 }
1038
1039 // if personal knowledge is more up to date or at least equal
1040 if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) ||
1041 (*pbPersOL == *pbPublOL))
1042 {
1043 // using personal knowledge, obtain opponent's "best guess" gridno
1044 sThreatLoc = gsLastKnownOppLoc[pSoldier->ubID][pOpponent->ubID];
1045 }
1046 else
1047 {
1048 // using public knowledge, obtain opponent's "best guess" gridno
1049 sThreatLoc = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID];
1050 }
1051
1052 // calculate how far away this threat is (in adjusted pixels)
1053 iThreatRange = GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, sThreatLoc );
1054
1055 if (iThreatRange < iClosestThreatRange)
1056 {
1057 iClosestThreatRange = iThreatRange;
1058 }
1059
1060 // remember this threat's gridno
1061 sThreatGridNo[uiThreatCnt] = sThreatLoc;
1062 uiThreatCnt++;
1063 }
1064
1065 // if no known opponents were found to threaten us, can't worry about them
1066 if (!uiThreatCnt)
1067 {
1068 return( sBestSpot );
1069 }
1070
1071 // get roaming range here; for civilians, running away is limited by roam range
1072 if ( pSoldier->bTeam == CIV_TEAM )
1073 {
1074 iRoamRange = RoamingRange( pSoldier, &sOrigin );
1075 if ( iRoamRange == 0 )
1076 {
1077 return( sBestSpot );
1078 }
1079 }
1080 else
1081 {
1082 // dummy values
1083 iRoamRange = 100;
1084 sOrigin = pSoldier->sGridNo;
1085 }
1086
1087 // DETERMINE CO-ORDINATE LIMITS OF SQUARE AREA TO BE CHECKED
1088 // THIS IS A LOT QUICKER THAN COVER, SO DO A LARGER AREA, NOT AFFECTED BY
1089 // DIFFICULTY SETTINGS...
1090
1091 if (pSoldier->bAlertStatus == STATUS_BLACK) // if already in battle
1092 {
1093 iSearchRange = pSoldier->bActionPoints / 2;
1094
1095 // to speed this up, tell PathAI to cancel any paths beyond our AP reach!
1096 gubNPCAPBudget = pSoldier->bActionPoints;
1097 }
1098 else
1099 {
1100 // even if not under pressure, limit to 1 turn's travelling distance
1101 gubNPCAPBudget = __min( pSoldier->bActionPoints / 2, CalcActionPoints( pSoldier ) );
1102
1103 iSearchRange = gubNPCAPBudget / 2;
1104 }
1105
1106 if (!gfTurnBasedAI)
1107 {
1108 // search only half as far in realtime
1109 // but always allow a certain minimum!
1110
1111 if ( iSearchRange > 4 )
1112 {
1113 iSearchRange /= 2;
1114 gubNPCAPBudget /= 2;
1115 }
1116 }
1117
1118
1119 // assume we have to stand up!
1120 // use the min macro here to make sure we don't wrap the UINT8 to 255...
1121
1122 #if 0 /* doppelt? */
1123 gubNPCAPBudget = gubNPCAPBudget = __min( gubNPCAPBudget, gubNPCAPBudget - GetAPsToChangeStance( pSoldier, ANIM_STAND ) );
1124 #else
1125 gubNPCAPBudget = __min( gubNPCAPBudget, gubNPCAPBudget - GetAPsToChangeStance( pSoldier, ANIM_STAND ) );
1126 #endif
1127
1128 // determine maximum horizontal limits
1129 sMaxLeft = MIN( iSearchRange, (pSoldier->sGridNo % MAXCOL));
1130 sMaxRight = MIN( iSearchRange, MAXCOL - ((pSoldier->sGridNo % MAXCOL) + 1));
1131
1132 // determine maximum vertical limits
1133 sMaxUp = MIN( iSearchRange, (pSoldier->sGridNo / MAXROW));
1134 sMaxDown = MIN( iSearchRange, MAXROW - ((pSoldier->sGridNo / MAXROW) + 1));
1135
1136 // Call FindBestPath to set flags in all locations that we can
1137 // walk into within range. We have to set some things up first...
1138
1139 // set the distance limit of the square region
1140 gubNPCDistLimit = (UINT8) iSearchRange;
1141
1142 // reset the "reachable" flags in the region we're looking at
1143 for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++)
1144 {
1145 for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++)
1146 {
1147 sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset);
1148 if ( !(sGridNo >=0 && sGridNo < WORLD_MAX) )
1149 {
1150 continue;
1151 }
1152
1153 gpWorldLevelData[sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE);
1154 }
1155 }
1156
1157 FindBestPath( pSoldier, NOWHERE, pSoldier->bLevel, DetermineMovementMode( pSoldier, AI_ACTION_RUN_AWAY ), COPYREACHABLE, 0 );
1158
1159 // Turn off the "reachable" flag for his current location
1160 // so we don't consider it
1161 gpWorldLevelData[pSoldier->sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE);
1162
1163 for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++)
1164 {
1165 for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++)
1166 {
1167 // calculate the next potential gridno
1168 sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset);
1169 if ( !(sGridNo >=0 && sGridNo < WORLD_MAX) )
1170 {
1171 continue;
1172 }
1173
1174 if (!(gpWorldLevelData[sGridNo].uiFlags & MAPELEMENT_REACHABLE))
1175 {
1176 continue;
1177 }
1178
1179 if ( sGridNo == pSoldier->sBlackList )
1180 {
1181 continue;
1182 }
1183
1184 if ( !fHasGasMask )
1185 {
1186 if ( InGas( pSoldier, sGridNo ) )
1187 {
1188 continue;
1189 }
1190 }
1191
1192 if ( pSoldier->bTeam == CIV_TEAM )
1193 {
1194 iRoamRange = RoamingRange( pSoldier, &sOrigin );
1195 if ( PythSpacesAway( sOrigin, sGridNo ) > iRoamRange )
1196 {
1197 continue;
1198 }
1199 }
1200
1201 // exclude locations with tear/mustard gas (at this point, smoke is cool!)
1202 if ( InGas( pSoldier, sGridNo ) )
1203 {
1204 continue;
1205 }
1206
1207 // OK, this place shows potential. How useful is it as cover?
1208
1209 iSpotClosestThreatRange = 1500;
1210
1211 if ( pSoldier->bTeam == ENEMY_TEAM && GridNoOnEdgeOfMap( sGridNo, &bEscapeDirection ) )
1212 {
1213 // We can escape! This is better than anything else except a closer spot which we can
1214 // cross over from.
1215
1216 // Subtract the straight-line distance from our location to this one as an estimate of
1217 // path cost and for looks...
1218
1219 // The edge spot closest to us which is on the edge will have the highest value, so
1220 // it will be picked over locations further away.
1221 // Only reachable gridnos will be picked so this should hopefully look okay
1222 iSpotClosestThreatRange -= PythSpacesAway( pSoldier->sGridNo, sGridNo );
1223
1224 }
1225 else
1226 {
1227 bEscapeDirection = -1;
1228 // for every opponent that threatens, consider this spot's cover vs. him
1229 for (UINT32 uiLoop = 0; uiLoop < uiThreatCnt; ++uiLoop)
1230 {
1231 //iThreatRange = AdjPixelsAway(CenterX(sGridNo),CenterY(sGridNo), CenterX(sThreatGridNo[iLoop]),CenterY(sThreatGridNo[iLoop]));
1232 iThreatRange = GetRangeInCellCoordsFromGridNoDiff( sGridNo, sThreatGridNo[uiLoop] );
1233 if (iThreatRange < iSpotClosestThreatRange)
1234 {
1235 iSpotClosestThreatRange = iThreatRange;
1236 }
1237 }
1238 }
1239
1240 // if this is better than the best place found so far
1241 // (i.e. the closest guy would be farther away than previously)
1242 if (iSpotClosestThreatRange > iClosestThreatRange)
1243 {
1244 // remember it instead
1245 iClosestThreatRange = iSpotClosestThreatRange;
1246 sBestSpot = sGridNo;
1247 bBestEscapeDirection = bEscapeDirection;
1248 }
1249 }
1250
1251 }
1252
1253 gubNPCAPBudget = 0;
1254 gubNPCDistLimit = 0;
1255
1256 if (bBestEscapeDirection != -1)
1257 {
1258 // Woohoo! We can escape! Fake some stuff with the quote-related actions
1259 pSoldier->ubQuoteActionID = GetTraversalQuoteActionID( bBestEscapeDirection );
1260 }
1261
1262 return( sBestSpot );
1263 }
1264
FindNearestUngassedLand(SOLDIERTYPE * pSoldier)1265 INT16 FindNearestUngassedLand(SOLDIERTYPE *pSoldier)
1266 {
1267 INT16 sGridNo,sClosestLand = NOWHERE,sPathCost,sShortestPath = 1000;
1268 INT16 sMaxLeft,sMaxRight,sMaxUp,sMaxDown,sXOffset,sYOffset;
1269 INT32 iSearchRange;
1270
1271 // start with a small search area, and expand it if we're unsuccessful
1272 // this should almost never need to search farther than 5 or 10 squares...
1273 for (iSearchRange = 5; iSearchRange <= 25; iSearchRange += 5)
1274 {
1275 // determine maximum horizontal limits
1276 sMaxLeft = MIN(iSearchRange,(pSoldier->sGridNo % MAXCOL));
1277 sMaxRight = MIN(iSearchRange,MAXCOL - ((pSoldier->sGridNo % MAXCOL) + 1));
1278
1279 // determine maximum vertical limits
1280 sMaxUp = MIN(iSearchRange,(pSoldier->sGridNo / MAXROW));
1281 sMaxDown = MIN(iSearchRange,MAXROW - ((pSoldier->sGridNo / MAXROW) + 1));
1282
1283 // Call FindBestPath to set flags in all locations that we can
1284 // walk into within range. We have to set some things up first...
1285
1286 // set the distance limit of the square region
1287 gubNPCDistLimit = (UINT8) iSearchRange;
1288
1289 // reset the "reachable" flags in the region we're looking at
1290 for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++)
1291 {
1292 for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++)
1293 {
1294 sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset);
1295 if ( !(sGridNo >=0 && sGridNo < WORLD_MAX) )
1296 {
1297 continue;
1298 }
1299
1300 gpWorldLevelData[sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE);
1301 }
1302 }
1303
1304 FindBestPath( pSoldier, NOWHERE, pSoldier->bLevel, DetermineMovementMode( pSoldier, AI_ACTION_LEAVE_WATER_GAS ), COPYREACHABLE, 0 );
1305
1306 // Turn off the "reachable" flag for his current location
1307 // so we don't consider it
1308 gpWorldLevelData[pSoldier->sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE);
1309
1310 // SET UP DOUBLE-LOOP TO STEP THROUGH POTENTIAL GRID #s
1311 for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++)
1312 {
1313 for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++)
1314 {
1315 // calculate the next potential gridno
1316 sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset);
1317 if ( !(sGridNo >=0 && sGridNo < WORLD_MAX) )
1318 {
1319 continue;
1320 }
1321
1322 if (!(gpWorldLevelData[sGridNo].uiFlags & MAPELEMENT_REACHABLE))
1323 {
1324 continue;
1325 }
1326
1327 // ignore blacklisted spot
1328 if ( sGridNo == pSoldier->sBlackList )
1329 {
1330 continue;
1331 }
1332
1333 // CJC: here, unfortunately, we must calculate a path so we have an AP cost
1334
1335 // obviously, we're looking for LAND, so water is out!
1336 sPathCost = LegalNPCDestination(pSoldier,sGridNo,ENSURE_PATH_COST,NOWATER,0);
1337
1338 if (!sPathCost)
1339 {
1340 continue; // skip on to the next potential grid
1341 }
1342
1343 // if this path is shorter than the one to the closest land found so far
1344 if (sPathCost < sShortestPath)
1345 {
1346 // remember it instead
1347 sShortestPath = sPathCost;
1348 sClosestLand = sGridNo;
1349 }
1350 }
1351 }
1352
1353 // if we found a piece of land in this search area
1354 if (sClosestLand != NOWHERE) // quit now, no need to look any farther
1355 break;
1356 }
1357 return(sClosestLand);
1358 }
1359
FindNearbyDarkerSpot(SOLDIERTYPE * pSoldier)1360 INT16 FindNearbyDarkerSpot( SOLDIERTYPE *pSoldier )
1361 {
1362 INT16 sGridNo, sClosestSpot = NOWHERE, sPathCost;
1363 INT32 iSpotValue, iBestSpotValue = 1000;
1364 INT16 sMaxLeft,sMaxRight,sMaxUp,sMaxDown,sXOffset,sYOffset;
1365 INT32 iSearchRange;
1366 INT8 bLightLevel, bCurrLightLevel, bLightDiff;
1367 INT32 iRoamRange;
1368 INT16 sOrigin;
1369
1370 bCurrLightLevel = LightTrueLevel( pSoldier->sGridNo, pSoldier->bLevel );
1371
1372 iRoamRange = RoamingRange( pSoldier, &sOrigin );
1373
1374 // start with a small search area, and expand it if we're unsuccessful
1375 // this should almost never need to search farther than 5 or 10 squares...
1376 for (iSearchRange = 5; iSearchRange <= 15; iSearchRange += 5)
1377 {
1378 // determine maximum horizontal limits
1379 sMaxLeft = MIN(iSearchRange,(pSoldier->sGridNo % MAXCOL));
1380 sMaxRight = MIN(iSearchRange,MAXCOL - ((pSoldier->sGridNo % MAXCOL) + 1));
1381
1382 // determine maximum vertical limits
1383 sMaxUp = MIN(iSearchRange,(pSoldier->sGridNo / MAXROW));
1384 sMaxDown = MIN(iSearchRange,MAXROW - ((pSoldier->sGridNo / MAXROW) + 1));
1385
1386 // Call FindBestPath to set flags in all locations that we can
1387 // walk into within range. We have to set some things up first...
1388
1389 // set the distance limit of the square region
1390 gubNPCDistLimit = (UINT8) iSearchRange;
1391
1392 // reset the "reachable" flags in the region we're looking at
1393 for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++)
1394 {
1395 for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++)
1396 {
1397 sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset);
1398 if ( !(sGridNo >=0 && sGridNo < WORLD_MAX) )
1399 {
1400 continue;
1401 }
1402
1403 gpWorldLevelData[sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE);
1404 }
1405 }
1406
1407 FindBestPath( pSoldier, NOWHERE, pSoldier->bLevel, DetermineMovementMode( pSoldier, AI_ACTION_LEAVE_WATER_GAS ), COPYREACHABLE, 0 );
1408
1409 // Turn off the "reachable" flag for his current location
1410 // so we don't consider it
1411 gpWorldLevelData[pSoldier->sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE);
1412
1413 // SET UP DOUBLE-LOOP TO STEP THROUGH POTENTIAL GRID #s
1414 for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++)
1415 {
1416 for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++)
1417 {
1418 // calculate the next potential gridno
1419 sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset);
1420 if ( !(sGridNo >=0 && sGridNo < WORLD_MAX) )
1421 {
1422 continue;
1423 }
1424
1425 if (!(gpWorldLevelData[sGridNo].uiFlags & MAPELEMENT_REACHABLE))
1426 {
1427 continue;
1428 }
1429
1430 // ignore blacklisted spot
1431 if ( sGridNo == pSoldier->sBlackList )
1432 {
1433 continue;
1434 }
1435
1436 // require this character to stay within their roam range
1437 if ( PythSpacesAway( sOrigin, sGridNo ) > iRoamRange )
1438 {
1439 continue;
1440 }
1441
1442 // screen out anything brighter than our current best spot
1443 bLightLevel = LightTrueLevel( sGridNo, pSoldier->bLevel );
1444
1445 bLightDiff = gbLightSighting[0][ bCurrLightLevel ] - gbLightSighting[0][ bLightLevel ];
1446
1447 // if the spot is darker than our current location, then bLightDiff > 0
1448 // plus ignore differences of just 1 light level
1449 if ( bLightDiff <= 1 )
1450 {
1451 continue;
1452 }
1453
1454 // CJC: here, unfortunately, we must calculate a path so we have an AP cost
1455
1456 sPathCost = LegalNPCDestination(pSoldier,sGridNo,ENSURE_PATH_COST,NOWATER,0);
1457
1458 if (!sPathCost)
1459 {
1460 continue; // skip on to the next potential grid
1461 }
1462
1463 // decrease the "cost" of the spot by the amount of light/darkness
1464 iSpotValue = sPathCost * 2 - bLightDiff;
1465
1466 if ( iSpotValue < iBestSpotValue )
1467 {
1468 // remember it instead
1469 iBestSpotValue = iSpotValue;
1470 sClosestSpot = sGridNo;
1471 }
1472 }
1473 }
1474
1475 // if we found a piece of land in this search area
1476 if (sClosestSpot != NOWHERE) // quit now, no need to look any farther
1477 {
1478 break;
1479 }
1480 }
1481
1482 return(sClosestSpot);
1483 }
1484
1485
IsGunUsable(OBJECTTYPE const & o,SOLDIERTYPE const & s)1486 static bool IsGunUsable(OBJECTTYPE const& o, SOLDIERTYPE const& s)
1487 {
1488 Assert(GCM->getItem(o.usItem)->isGun());
1489 return
1490 o.bGunAmmoStatus > 0 &&
1491 o.ubGunShotsLeft != 0 &&
1492 (!HasObjectImprint(o) || o.ubImprintID == s.ubID); // XXX wrong: should compare profile
1493 }
1494
1495
1496 #define MINIMUM_REQUIRED_STATUS 70
1497
SearchForItems(SOLDIERTYPE & s,ItemSearchReason const reason,UINT16 const usItem)1498 INT8 SearchForItems(SOLDIERTYPE& s, ItemSearchReason const reason, UINT16 const usItem)
1499 {
1500 if (s.bActionPoints < AP_PICKUP_ITEM) return AI_ACTION_NONE;
1501
1502 if (!IS_MERC_BODY_TYPE(&s)) return AI_ACTION_NONE;
1503
1504 INT32 search_range = gbDiff[DIFF_MAX_COVER_RANGE][SoldierDifficultyLevel(&s)];
1505
1506 switch (s.bAttitude)
1507 {
1508 case DEFENSIVE: search_range -= 1; break;
1509 case BRAVESOLO: search_range += 2; break;
1510 case BRAVEAID: search_range += 2; break;
1511 case CUNNINGSOLO: search_range -= 2; break;
1512 case CUNNINGAID: search_range -= 2; break;
1513 case AGGRESSIVE: search_range += 1; break;
1514 }
1515
1516 // Maximum search range is 1 tile / 10 pts of wisdom
1517 if (search_range > s.bWisdom / 10)
1518 {
1519 search_range = s.bWisdom / 10;
1520 }
1521
1522 if (!gfTurnBasedAI)
1523 { // Don't search so far in realtime
1524 search_range /= 2;
1525 }
1526
1527 // Don't search so far for items
1528 search_range /= 2;
1529
1530 // determine maximum horizontal limits
1531 INT16 const max_left = MIN(search_range, s.sGridNo % MAXCOL);
1532 INT16 const max_right = MIN(search_range, MAXCOL - (s.sGridNo % MAXCOL + 1));
1533
1534 // determine maximum vertical limits
1535 INT16 const max_up = MIN(search_range, s.sGridNo / MAXROW);
1536 INT16 const max_down = MIN(search_range, MAXROW - (s.sGridNo / MAXROW + 1));
1537
1538 // Call FindBestPath to set flags in all locations that we can
1539 // walk into within range. We have to set some things up first...
1540
1541 // set the distance limit of the square region
1542 gubNPCDistLimit = search_range;
1543
1544 // set an AP limit too, to our APs less the cost of picking up an item
1545 // and less the cost of dropping an item since we might need to do that
1546 gubNPCAPBudget = s.bActionPoints - AP_PICKUP_ITEM;
1547
1548 // reset the "reachable" flags in the region we're looking at
1549 for (INT16 dy = -max_up; dy <= max_down; ++dy)
1550 {
1551 for (INT16 dx = -max_left; dx <= max_right; ++dx)
1552 {
1553 GridNo const grid_no = s.sGridNo + dx + MAXCOL * dy;
1554 if (grid_no < 0 || WORLD_MAX <= grid_no) continue;
1555 gpWorldLevelData[grid_no].uiFlags &= ~MAPELEMENT_REACHABLE;
1556 }
1557 }
1558
1559 FindBestPath(&s, NOWHERE, s.bLevel, DetermineMovementMode(&s, AI_ACTION_PICKUP_ITEM), COPYREACHABLE, 0);
1560
1561 GridNo best_spot = NOWHERE;
1562 INT32 best_value = 0;
1563 INT32 item_idx = -1;
1564 INT32 best_item_idx = -1;
1565 for (INT16 dy = -max_up; dy <= max_down; ++dy)
1566 {
1567 for (INT16 dx = -max_left; dx <= max_right; ++dx)
1568 {
1569 // calculate the next potential gridno
1570 INT16 const grid_no = s.sGridNo + dx + (MAXCOL * dy);
1571 if (grid_no < 0 || WORLD_MAX <= grid_no) continue;
1572
1573 // exclude locations with tear/mustard gas (at this point, smoke is cool!)
1574 if (InGasOrSmoke(&s, grid_no)) continue;
1575
1576 if (!(gpWorldLevelData[grid_no].uiFlags & MAPELEMENT_ITEMPOOL_PRESENT)) continue;
1577 if (!(gpWorldLevelData[grid_no].uiFlags & MAPELEMENT_REACHABLE)) continue;
1578
1579 // ignore blacklisted spot
1580 if (grid_no == s.sBlackList) continue;
1581
1582 INT32 value = 0;
1583 for (ITEM_POOL const* pItemPool = GetItemPool(grid_no, s.bLevel); pItemPool; pItemPool = pItemPool->pNext)
1584 {
1585 OBJECTTYPE const& o = GetWorldItem(pItemPool->iItemIndex).o;
1586 const ItemModel * item = GCM->getItem(o.usItem);
1587 INT32 temp_value;
1588 switch (reason)
1589 {
1590 case SEARCH_AMMO:
1591 // We are looking for ammo to match the gun in usItem
1592 if (item->getItemClass() == IC_GUN && o.bStatus[0] >= MINIMUM_REQUIRED_STATUS)
1593 { // Maybe this gun has ammo (adjust for whether it is better than ours!)
1594 if (!IsGunUsable(o, s)) continue;
1595 temp_value = o.ubGunShotsLeft * GCM->getWeapon(o.usItem)->ubDeadliness / GCM->getWeapon(usItem)->ubDeadliness;
1596 }
1597 else
1598 {
1599 if (!ValidAmmoType(usItem, o.usItem)) continue;
1600 temp_value = TotalPoints(&o);
1601 }
1602 break;
1603
1604 default:
1605 if (item->getItemClass() == IC_ARMOUR && o.bStatus[0] >= MINIMUM_REQUIRED_STATUS)
1606 {
1607 InvSlotPos pos;
1608 switch (Armour[item->getClassIndex()].ubArmourClass)
1609 {
1610 case ARMOURCLASS_HELMET: pos = HELMETPOS; break;
1611 case ARMOURCLASS_VEST: pos = VESTPOS; break;
1612 case ARMOURCLASS_LEGGINGS: pos = LEGPOS; break;
1613 default: continue;
1614 }
1615 OBJECTTYPE const& cur_armour = s.inv[pos];
1616 if (cur_armour.usItem == NOTHING)
1617 {
1618 temp_value = 200 + EffectiveArmour(&o);
1619 }
1620 else
1621 {
1622 INT8 const new_rating = EffectiveArmour(&o);
1623 if (EffectiveArmour(&s.inv[HELMETPOS]) <= new_rating) continue; // XXX makes no sense: always compare with helmet and only consider when worse
1624 temp_value = 100 * new_rating / EffectiveArmour(&cur_armour);
1625 }
1626 }
1627 /* FALLTHROUGH */
1628
1629 case SEARCH_WEAPONS:
1630 {
1631 if (!(item->isWeapon())) continue;
1632 if (o.bStatus[0] < MINIMUM_REQUIRED_STATUS) continue;
1633 if (item->isGun()&& !IsGunUsable(o, s)) continue;
1634 const WeaponModel * new_wpn = GCM->getWeapon(o.usItem);
1635 OBJECTTYPE const& in_hand = s.inv[HANDPOS];
1636 if (GCM->getItem(in_hand.usItem)->isWeapon())
1637 {
1638 const WeaponModel * cur_wpn = GCM->getWeapon(in_hand.usItem);
1639 if (new_wpn->ubDeadliness <= cur_wpn->ubDeadliness) continue;
1640 temp_value = 100 * new_wpn->ubDeadliness / cur_wpn->ubDeadliness;
1641 }
1642 else
1643 {
1644 temp_value = 200 + new_wpn->ubDeadliness;
1645 }
1646 break;
1647 }
1648 }
1649
1650 if (value < temp_value)
1651 {
1652 value = temp_value;
1653 item_idx = pItemPool->iItemIndex;
1654 }
1655 }
1656 value = 3 * value / (3 + PythSpacesAway(grid_no, s.sGridNo));
1657 if (best_value < value)
1658 {
1659 best_value = value;
1660 best_spot = grid_no;
1661 best_item_idx = item_idx;
1662 }
1663 }
1664 }
1665
1666 if (best_spot == NOWHERE) return AI_ACTION_NONE;
1667
1668 OBJECTTYPE const& o = GetWorldItem(best_item_idx).o;
1669 SLOGD("%d decides to pick up %s", s.ubID, ItemNames[o.usItem].c_str());
1670 if (GCM->getItem(o.usItem)->getItemClass() == IC_GUN &&
1671 !FindBetterSpotForItem(s, HANDPOS))
1672 {
1673 if (s.bActionPoints < AP_PICKUP_ITEM + AP_PICKUP_ITEM)
1674 {
1675 return AI_ACTION_NONE;
1676 }
1677 if (s.inv[HANDPOS].fFlags & OBJECT_UNDROPPABLE)
1678 { // Destroy this item
1679 SLOGD("%d decides he must drop %s first so destroys it", s.ubID, ItemNames[s.inv[HANDPOS].usItem].c_str());
1680 DeleteObj(&s.inv[HANDPOS]);
1681 DeductPoints(&s, AP_PICKUP_ITEM, 0);
1682 }
1683 else
1684 { // We want to drop this item
1685 SLOGD("%d decides he must drop %s first", s.ubID, ItemNames[s.inv[HANDPOS].usItem].c_str());
1686 s.bNextAction = AI_ACTION_PICKUP_ITEM;
1687 s.usNextActionData = best_spot;
1688 s.iNextActionSpecialData = best_item_idx;
1689 return AI_ACTION_DROP_ITEM;
1690 }
1691 }
1692 s.uiPendingActionData1 = best_item_idx;
1693 s.usActionData = best_spot;
1694 return AI_ACTION_PICKUP_ITEM;
1695 }
1696
1697
FindClosestDoor(SOLDIERTYPE * pSoldier)1698 INT16 FindClosestDoor( SOLDIERTYPE * pSoldier )
1699 {
1700 INT16 sClosestDoor = NOWHERE;
1701 INT32 iSearchRange;
1702 INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXOffset, sYOffset;
1703 INT16 sGridNo;
1704 INT32 iDist, iClosestDist = 10;
1705
1706 iSearchRange = 5;
1707
1708 // determine maximum horizontal limits
1709 sMaxLeft = MIN( iSearchRange, (pSoldier->sGridNo % MAXCOL));
1710 sMaxRight = MIN( iSearchRange, MAXCOL - ((pSoldier->sGridNo % MAXCOL) + 1));
1711
1712 // determine maximum vertical limits
1713 sMaxUp = MIN( iSearchRange, (pSoldier->sGridNo / MAXROW));
1714 sMaxDown = MIN( iSearchRange, MAXROW - ((pSoldier->sGridNo / MAXROW) + 1));
1715 // SET UP DOUBLE-LOOP TO STEP THROUGH POTENTIAL GRID #s
1716 for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++)
1717 {
1718 for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++)
1719 {
1720 // calculate the next potential gridno
1721 sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset);
1722 if (FindStructure( sGridNo, STRUCTURE_ANYDOOR ) != NULL)
1723 {
1724 iDist = PythSpacesAway( pSoldier->sGridNo, sGridNo );
1725 if (iDist < iClosestDist)
1726 {
1727 iClosestDist = iDist;
1728 sClosestDoor = sGridNo;
1729 }
1730 }
1731 }
1732 }
1733
1734 return( sClosestDoor );
1735 }
1736
FindNearestEdgepointOnSpecifiedEdge(INT16 sGridNo,INT8 bEdgeCode)1737 INT16 FindNearestEdgepointOnSpecifiedEdge( INT16 sGridNo, INT8 bEdgeCode )
1738 {
1739 std::vector<INT16>* pEdgepoints;
1740 INT16 sClosestSpot = NOWHERE, sClosestDist = 0x7FFF, sTempDist;
1741
1742 switch( bEdgeCode )
1743 {
1744 case NORTH_EDGEPOINT_SEARCH:
1745 pEdgepoints = &gps1stNorthEdgepointArray;
1746 break;
1747 case EAST_EDGEPOINT_SEARCH:
1748 pEdgepoints = &gps1stEastEdgepointArray;
1749 break;
1750 case SOUTH_EDGEPOINT_SEARCH:
1751 pEdgepoints = &gps1stSouthEdgepointArray;
1752 break;
1753 case WEST_EDGEPOINT_SEARCH:
1754 pEdgepoints = &gps1stWestEdgepointArray;
1755 break;
1756 default:
1757 // WTF???
1758 return( NOWHERE );
1759 }
1760
1761 // Do a 2D search to find the closest map edgepoint and
1762 // try to create a path there
1763
1764 for (INT16 edgepoint : *pEdgepoints)
1765 {
1766 sTempDist = PythSpacesAway(sGridNo, edgepoint);
1767 if ( sTempDist < sClosestDist )
1768 {
1769 sClosestDist = sTempDist;
1770 sClosestSpot = edgepoint;
1771 }
1772 }
1773
1774 return( sClosestSpot );
1775 }
1776
FindNearestEdgePoint(INT16 sGridNo)1777 INT16 FindNearestEdgePoint( INT16 sGridNo )
1778 {
1779 INT16 sScreenX, sScreenY, sMaxScreenX, sMaxScreenY;
1780 INT16 sDist[5], sMinDist;
1781 INT32 iLoop;
1782 INT8 bMinIndex;
1783 std::vector<INT16>* pEdgepoints;
1784 INT16 sClosestSpot = NOWHERE, sClosestDist = 0x7FFF, sTempDist;
1785
1786 GetAbsoluteScreenXYFromMapPos(sGridNo, &sScreenX, &sScreenY);
1787
1788 sMaxScreenX = gsRightX - gsLeftX;
1789 sMaxScreenY = gsBottomY - gsTopY;
1790
1791 sDist[0] = 0x7FFF;
1792 sDist[1] = sScreenX; // west
1793 sDist[2] = sMaxScreenX - sScreenX; // east
1794 sDist[3] = sScreenY; // north
1795 sDist[4] = sMaxScreenY - sScreenY; // south
1796
1797 sMinDist = sDist[0];
1798 bMinIndex = 0;
1799 for( iLoop = 1; iLoop < 5; iLoop++)
1800 {
1801 if ( sDist[ iLoop ] < sMinDist )
1802 {
1803 sMinDist = sDist[ iLoop ];
1804 bMinIndex = (INT8) iLoop;
1805 }
1806 }
1807
1808 switch( bMinIndex )
1809 {
1810 case 1:
1811 pEdgepoints = &gps1stWestEdgepointArray;
1812 break;
1813 case 2:
1814 pEdgepoints = &gps1stEastEdgepointArray;
1815 break;
1816 case 3:
1817 pEdgepoints = &gps1stNorthEdgepointArray;
1818 break;
1819 case 4:
1820 pEdgepoints = &gps1stSouthEdgepointArray;
1821 break;
1822 default:
1823 // WTF???
1824 return( NOWHERE );
1825 }
1826
1827 // Do a 2D search to find the closest map edgepoint and
1828 // try to create a path there
1829
1830 for (INT16 edgepoint : *pEdgepoints)
1831 {
1832 sTempDist = PythSpacesAway(sGridNo, edgepoint);
1833 if ( sTempDist < sClosestDist )
1834 {
1835 sClosestDist = sTempDist;
1836 sClosestSpot = edgepoint;
1837 }
1838 }
1839
1840 return( sClosestSpot );
1841 }
1842
1843 #define EDGE_OF_MAP_SEARCH 5
1844
FindNearbyPointOnEdgeOfMap(SOLDIERTYPE * pSoldier,INT8 * pbDirection)1845 INT16 FindNearbyPointOnEdgeOfMap( SOLDIERTYPE * pSoldier, INT8 * pbDirection )
1846 {
1847 INT32 iSearchRange;
1848 INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXOffset, sYOffset;
1849
1850 INT16 sGridNo, sClosestSpot = NOWHERE;
1851 INT8 bDirection, bClosestDirection;
1852 INT32 iPathCost, iClosestPathCost = 1000;
1853
1854 bClosestDirection = -1;
1855
1856 // Call FindBestPath to set flags in all locations that we can
1857 // walk into within range. We have to set some things up first...
1858
1859 // set the distance limit of the square region
1860 gubNPCDistLimit = EDGE_OF_MAP_SEARCH;
1861
1862 iSearchRange = EDGE_OF_MAP_SEARCH;
1863
1864 // determine maximum horizontal limits
1865 sMaxLeft = MIN( iSearchRange, (pSoldier->sGridNo % MAXCOL));
1866 sMaxRight = MIN( iSearchRange, MAXCOL - ((pSoldier->sGridNo % MAXCOL) + 1));
1867
1868 // determine maximum vertical limits
1869 sMaxUp = MIN( iSearchRange, (pSoldier->sGridNo / MAXROW));
1870 sMaxDown = MIN( iSearchRange, MAXROW - ((pSoldier->sGridNo / MAXROW) + 1));
1871
1872 // reset the "reachable" flags in the region we're looking at
1873 for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++)
1874 {
1875 for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++)
1876 {
1877 sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset);
1878 if ( !(sGridNo >=0 && sGridNo < WORLD_MAX) )
1879 {
1880 continue;
1881 }
1882
1883 gpWorldLevelData[sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE);
1884 }
1885 }
1886
1887 FindBestPath( pSoldier, NOWHERE, pSoldier->bLevel, WALKING, COPYREACHABLE, 0 );
1888
1889 // Turn off the "reachable" flag for his current location
1890 // so we don't consider it
1891 gpWorldLevelData[pSoldier->sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE);
1892
1893 // SET UP DOUBLE-LOOP TO STEP THROUGH POTENTIAL GRID #s
1894 for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++)
1895 {
1896 for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++)
1897 {
1898 // calculate the next potential gridno
1899 sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset);
1900 if ( !(sGridNo >=0 && sGridNo < WORLD_MAX) )
1901 {
1902 continue;
1903 }
1904
1905 if (!(gpWorldLevelData[sGridNo].uiFlags & MAPELEMENT_REACHABLE))
1906 {
1907 continue;
1908 }
1909
1910 if (GridNoOnEdgeOfMap( sGridNo, &bDirection ) )
1911 {
1912 iPathCost = PythSpacesAway( pSoldier->sGridNo, sGridNo );
1913
1914 if (iPathCost < iClosestPathCost)
1915 {
1916 // this place is closer
1917 sClosestSpot = sGridNo;
1918 iClosestPathCost = iPathCost;
1919 bClosestDirection = bDirection;
1920 }
1921 }
1922 }
1923 }
1924
1925 *pbDirection = bClosestDirection;
1926 return( sClosestSpot );
1927 }
1928
1929
FindClosestBoxingRingSpot(SOLDIERTYPE * pSoldier,BOOLEAN fInRing)1930 INT16 FindClosestBoxingRingSpot( SOLDIERTYPE * pSoldier, BOOLEAN fInRing )
1931 {
1932 INT32 iSearchRange;
1933 INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXOffset, sYOffset;
1934
1935 INT16 sGridNo, sClosestSpot = NOWHERE;
1936 INT32 iDistance, iClosestDistance = 9999;
1937
1938 // set the distance limit of the square region
1939 iSearchRange = 7;
1940
1941 // determine maximum horizontal limits
1942 sMaxLeft = MIN( iSearchRange, (pSoldier->sGridNo % MAXCOL));
1943 sMaxRight = MIN( iSearchRange, MAXCOL - ((pSoldier->sGridNo % MAXCOL) + 1));
1944
1945 if (pSoldier->bTeam == OUR_TEAM && !fInRing)
1946 {
1947 // have player not go to the left of the ring
1948 sMaxLeft = 0;
1949 }
1950
1951 // determine maximum vertical limits
1952 sMaxUp = MIN( iSearchRange, (pSoldier->sGridNo / MAXROW));
1953 sMaxDown = MIN( iSearchRange, MAXROW - ((pSoldier->sGridNo / MAXROW) + 1));
1954
1955 for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++)
1956 {
1957 for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++)
1958 {
1959 // calculate the next potential gridno
1960 sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset);
1961 UINT8 const room = GetRoom(sGridNo);
1962 if (room == NO_ROOM) continue;
1963
1964 if ((fInRing && room == BOXING_RING) ||
1965 ((!fInRing && room != BOXING_RING) && LegalNPCDestination(pSoldier, sGridNo, IGNORE_PATH, NOWATER, 0)))
1966 {
1967 iDistance = ABS( sXOffset ) + ABS( sYOffset );
1968 if (iDistance < iClosestDistance && WhoIsThere2(sGridNo, 0) == NULL)
1969 {
1970 sClosestSpot = sGridNo;
1971 iClosestDistance = iDistance;
1972 }
1973 }
1974 }
1975 }
1976
1977 return( sClosestSpot );
1978 }
1979
FindNearestOpenableNonDoor(INT16 sStartGridNo)1980 INT16 FindNearestOpenableNonDoor( INT16 sStartGridNo )
1981 {
1982 INT32 iSearchRange;
1983 INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXOffset, sYOffset;
1984
1985 INT16 sGridNo, sClosestSpot = NOWHERE;
1986 INT32 iDistance, iClosestDistance = 9999;
1987
1988 // set the distance limit of the square region
1989 iSearchRange = 7;
1990
1991 // determine maximum horizontal limits
1992 sMaxLeft = MIN( iSearchRange, (sStartGridNo % MAXCOL));
1993 sMaxRight = MIN( iSearchRange, MAXCOL - ((sStartGridNo % MAXCOL) + 1));
1994
1995 // determine maximum vertical limits
1996 sMaxUp = MIN( iSearchRange, (sStartGridNo / MAXROW));
1997 sMaxDown = MIN( iSearchRange, MAXROW - ((sStartGridNo / MAXROW) + 1));
1998
1999 for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++)
2000 {
2001 for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++)
2002 {
2003 // calculate the next potential gridno
2004 sGridNo = sStartGridNo + sXOffset + (MAXCOL * sYOffset);
2005 FOR_EACH_STRUCTURE(pStructure, sGridNo, STRUCTURE_OPENABLE)
2006 {
2007 // skip any doors
2008 if (pStructure->fFlags & STRUCTURE_ANYDOOR) continue;
2009
2010 // we have found a valid non-door openable structure
2011 iDistance = CardinalSpacesAway( sGridNo, sStartGridNo );
2012 if ( iDistance < iClosestDistance )
2013 {
2014 sClosestSpot = sGridNo;
2015 iClosestDistance = iDistance;
2016 }
2017 break;
2018 }
2019 }
2020 }
2021 return( sClosestSpot );
2022 }
2023