diff --git a/Tactical/DisplayCover.cpp b/Tactical/DisplayCover.cpp index 653433fa..995f55b6 100644 --- a/Tactical/DisplayCover.cpp +++ b/Tactical/DisplayCover.cpp @@ -813,7 +813,7 @@ TileDefines GetTileMinesIndex( const INT8& bMines ) case MINES_NET_1: case MINES_LVL_4: return SPECIALTILE_COVER_1; // red - case MINE_BOMB_AND_WIRE: + case MINE_BOMB_AND_WIRE: case MINES_NET_2: case MINES_LVL_3: return SPECIALTILE_COVER_2; // orange @@ -1077,8 +1077,27 @@ void DetermineMineDisplayInTile( INT32 sGridNo, INT8 bLevel, INT8& bMines, BOOLE // if we're already marked as MINE_BOMB, switch to MINE_BOMB_AND_WIRE if ( bMines == MINE_BOMB ) bMines = MINE_BOMB_AND_WIRE; + else if ( bMines == MINE_BOMB_AND_WIRE ) + ; else - bMines = MINE_WIRE; + { + // check if the tripwire has a gun attached + BOOLEAN fgunfound = FALSE; + attachmentList::iterator iterend = (*pObj)[0]->attachments.end(); + for (attachmentList::iterator iter = (*pObj)[0]->attachments.begin(); iter != iterend; ++iter) + { + if ( iter->exists() && Item[iter->usItem].usItemClass == IC_GUN ) + { + fgunfound = TRUE; + break; + } + } + + if ( fgunfound ) + bMines = MINE_BOMB_AND_WIRE; + else + bMines = MINE_WIRE; + } } else { diff --git a/Tactical/LOS.cpp b/Tactical/LOS.cpp index fdf110f8..e72eca71 100644 --- a/Tactical/LOS.cpp +++ b/Tactical/LOS.cpp @@ -49,6 +49,7 @@ // HEADROCK HAM 3.6: This must be included, for testing whether Bloodcats and Enemies can see one another. #include "Campaign Types.h" #include "soldier tile.h" // added by Flugente +#include "Sound Control.h" // added by Flugente //forward declarations of common classes to eliminate includes class OBJECTTYPE; @@ -5249,6 +5250,579 @@ INT8 FireFragmentGivenTarget( SOLDIERTYPE * pThrower, FLOAT dStartX, FLOAT dStar return( TRUE ); } +// Flugente: fire a shot from a gun that has no user (used for traps with attached guns) +INT8 FireBulletGivenTargetTrapOnly( SOLDIERTYPE* pThrower, OBJECTTYPE* pObj, INT32 gridno, FLOAT dStartZ, FLOAT dEndX, FLOAT dEndY, FLOAT dEndZ, INT16 sHitBy ) +{ + if ( !pObj ) + return 0; + + if ( !pThrower ) + return 0; + + FLOAT dStartX = 0; + FLOAT dStartY = 0; + + FLOAT d2DDistance=0; + FLOAT dDeltaX=0; + FLOAT dDeltaY=0; + FLOAT dDeltaZ=0; + + DOUBLE ddOrigHorizAngle=0; + DOUBLE ddOrigVerticAngle=0; + DOUBLE ddHorizAngle=0; + DOUBLE ddVerticAngle=0; + DOUBLE ddAdjustedHorizAngle=0; + DOUBLE ddAdjustedVerticAngle=0; + DOUBLE ddDummyHorizAngle=0; + DOUBLE ddDummyVerticAngle=0; + + BULLET* pBullet=NULL; + INT32 iBullet=0; + + INT32 iDistance=0; + + UINT8 ubLoop=0; + UINT8 ubShots=0; + UINT8 ubImpact=0; + INT8 bCTGT; + UINT8 ubSpreadIndex = 0; + UINT16 usBulletFlags = 0; + int n=0; + + BOOLEAN fBuckshot = FALSE; + if ( AmmoTypes[(*pObj)[0]->data.gun.ubGunAmmoType].numberOfBullets > 1 ) + fBuckshot = TRUE; + + dStartX = (FLOAT) CenterX( gridno ); + dStartY = (FLOAT) CenterY( gridno ); + + dDeltaX = dEndX - dStartX; + dDeltaY = dEndY - dStartY; + dDeltaZ = dEndZ - dStartZ; + + //lal bugfix + if( dDeltaZ > 0 ) + d2DDistance = Distance3D( dDeltaX, dDeltaY, dDeltaZ ); + else + d2DDistance = Distance2D( dDeltaX, dDeltaY ); + + iDistance = (INT32) d2DDistance; + + if ( d2DDistance != iDistance ) + { + iDistance += 1; + d2DDistance = (FLOAT) ( iDistance); + } + + ddOrigHorizAngle = atan2( dDeltaY, dDeltaX ); + ddOrigVerticAngle = atan2( dDeltaZ, (d2DDistance * 2.56f) ); + ddAdjustedHorizAngle = ddOrigHorizAngle; + ddAdjustedVerticAngle = ddOrigVerticAngle; + + ubShots = 1; + fTracer = FALSE; + + // Check if we have spit as a weapon! + if ( Weapon[ pObj->usItem ].ubWeaponClass == MONSTERCLASS ) + { + usBulletFlags |= BULLET_FLAG_CREATURE_SPIT; + } + else if ( Item[ pObj->usItem ].usItemClass == IC_THROWING_KNIFE ) + { + usBulletFlags |= BULLET_FLAG_KNIFE; + } + else if ( Item[pObj->usItem].rocketlauncher ) + { + usBulletFlags |= BULLET_FLAG_MISSILE; + } + else if ( Item[pObj->usItem].cannon ) + { + usBulletFlags |= BULLET_FLAG_TANK_CANNON; + } + else if ( Item[pObj->usItem].rocketrifle ) + { + usBulletFlags |= BULLET_FLAG_SMALL_MISSILE; + } + else if ( pObj->usItem == FLAMETHROWER ) + { + usBulletFlags |= BULLET_FLAG_FLAME; + ubSpreadIndex = 2; + } + + // no option to use fire bursts or autofire yet + /*// HEADROCK HAM B2.5: Set tracer effect on/off for individual bullets in a Tracer Magazine, as part of the + // New Tracer System. + else if (gGameExternalOptions.ubRealisticTracers > 0 && gGameExternalOptions.ubNumBulletsPerTracer > 0 && (pFirer->bDoAutofire > 0 || pFirer->bDoBurst > 0) + && AmmoTypes[ (*pObjAttHand)[0]->data.gun.ubGunAmmoType ].tracerEffect ) + { + UINT16 iBulletsLeft, iBulletsPerTracer; + iBulletsPerTracer = gGameExternalOptions.ubNumBulletsPerTracer; + iBulletsLeft = (*pObjAttHand)[0]->data.gun.ubGunShotsLeft + pFirer->bDoBurst; + + if ((((iBulletsLeft - (pFirer->bDoBurst - 1)) / iBulletsPerTracer) - ((iBulletsLeft - pFirer->bDoBurst) / iBulletsPerTracer)) == 1) + { + fTracer = TRUE; + } + else + { + fTracer = FALSE; + } + }*/ + else if ( AmmoTypes[ (*pObj)[0]->data.gun.ubGunAmmoType ].tracerEffect && gGameSettings.fOptions[ TOPTION_TRACERS_FOR_SINGLE_FIRE ] ) + { + //usBulletFlags |= BULLET_FLAG_TRACER; + fTracer = TRUE; + } + + ubImpact =(UINT8) GetDamage( pObj ); + + // we have to adjust the damage... + //ubImpact = min(255, 4 * ubImpact ); + + //zilpin: Begin new code block for spread patterns, number of projectiles, impact adjustment, etc. + { + ObjectData *weapon = &((*pObj)[0]->data); + ubShots = AmmoTypes[ weapon->gun.ubGunAmmoType].numberOfBullets; + ubSpreadIndex = GetSpreadPattern( pObj ); + if( ubShots>1 ) + { + fBuckshot = true; + usBulletFlags |= BULLET_FLAG_BUCKSHOT; + ubImpact = (UINT8) (ubImpact * AmmoTypes[weapon->gun.ubGunAmmoType].multipleBulletDamageMultiplier / max(1,AmmoTypes[weapon->gun.ubGunAmmoType].multipleBulletDamageDivisor) ); + } + weapon=NULL; + } + //zilpin: End of new code block. + + // GET BULLET + for (ubLoop = 0; ubLoop < ubShots; ubLoop++) + { + iBullet = CreateBullet( NOBODY, FALSE, usBulletFlags, pObj->usItem ); + if (iBullet == -1) + { + //DebugMsg( TOPIC_JA2, DBG_LEVEL_3, String("Failed to create bullet") ); + + return( FALSE ); + } + pBullet = GetBulletPtr( iBullet ); + + // HEADROCK HAM 4: The HitBy value now holds the ratio between the Distance Aperture and the Final Aperture. + // Basically, this represents by how much our shooter has managed to make the shot more accurate than it would + // be without any extra aiming or optical equipment. HitBy can now only be positive, and equals 100 when the + // shot is perfectly on-target, or 0 when it is as inaccurate as the shooting system can possibly allow. + // In this sense, sApertureRatio is the closest "relative" that the NCTH system has to the old system. it tells + // us, on a scale of 0-100, how accurate we are compared to "Best" and "Worst" accuracy. + // Note also that references to this value have been changed accordingly. Most imporantly, functions that alter + // the path of the bullet, or determine whether collisions are more likely thanks to low HitBy, have been + // altered to _not use this value at all_. + // At the moment, HitBy is used only for determining extra damage (0-50% extra), and for determining + // how much extra experience is gained by the shooter if the target is actually hit by the bullet. + pBullet->sHitBy = sHitBy; + + // HEADROCK HAM 4: TODO: Determine whether this value is required anymore. Why ever let bullets + // pass through the roof at all?? + if (dStartZ < WALL_HEIGHT_UNITS) + { + if (dEndZ > WALL_HEIGHT_UNITS) + { + pBullet->fCheckForRoof = TRUE; + } + else + { + pBullet->fCheckForRoof = FALSE; + } + } + else // dStartZ >= WALL_HEIGHT_UNITS; presumably > + { + if (dEndZ < WALL_HEIGHT_UNITS) + { + pBullet->fCheckForRoof = TRUE; + } + else + { + pBullet->fCheckForRoof = FALSE; + } + } + + // Flugente: we have to set this, otherwise the gun that fired this shot wont be found in WeaponHit() + pBullet->fFragment = true; + + //zilpin: pellet spread patterns externalized in XML + //Now the first shot will use the pattern, as well. + //Single shot weapons will still be stuck with straight-ahead only, though. + //if ( ubLoop == 0 ) + if( ubShots == 1 ) + { + // CHRISL: If we don't set the ddHorizAngle, at the very least, shooting by corners is impossible. Unfortunately, I don't know what other + // impacts these two lines will have. Headrock didn't include them when he originally wrote NCTH but they are in a similar location in the + // OCTH code. Hopefully no issues will result from this change. + ddHorizAngle = ddOrigHorizAngle; + ddVerticAngle = ddOrigVerticAngle; + // HEADROCK HAM 4: Firing increments no longer required here (NCTH) + // calculate by hand (well, without angles) to match LOS + pBullet->qIncrX = FloatToFixed( dDeltaX / (FLOAT)iDistance ); + pBullet->qIncrY = FloatToFixed( dDeltaY / (FLOAT)iDistance ); + pBullet->qIncrZ = FloatToFixed( dDeltaZ / (FLOAT)iDistance ); + } + else if( ubShots > 1 ) + { + //Calculate the deviation due to to-hit, but only for the first shot. + //The bullet is only used temporarily. It gets reset later. + //Should this use d3DDistance? + //if( ubLoop<1 ){ + // CalculateFiringIncrements( ddOrigHorizAngle, ddOrigVerticAngle, d2DDistance, pBullet, &ddAdjustedHorizAngle, &ddAdjustedVerticAngle ); + //} + + // temporarily set bullet's sHitBy value to 0 to get unadjusted angles + //Note: this no longer needs to be done since the CalculateFiringIncrementsSimple is being used below. + //pBullet->sHitBy = 0; + + //Also prevent out-of-bounds overflows. + //Rotates through the array if more pellets than expected, and uses pattern index 0 if requested pattern does not exist. + if( gpSpreadPattern == NULL || giSpreadPatternCount<1 ){ + //XML file missing, or empty, or error while loading. Use hard-coded defaults. + //This is from the original code. + if( ubSpreadIndex > 2 || ubSpreadIndex<0 ) ubSpreadIndex=0; + n = ubLoop % BUCKSHOT_SHOTS; + ddHorizAngle = ddAdjustedHorizAngle + ddShotgunSpread[ubSpreadIndex][n][0]; + ddVerticAngle = ddAdjustedVerticAngle + ddShotgunSpread[ubSpreadIndex][n][1]; + }else{ + double d=0,r=0,xspread=0,yspread=0; + int n=0; + + //Use spread patterns loaded from XML. + if( ubSpreadIndex >= giSpreadPatternCount || ubSpreadIndex < 0) + ubSpreadIndex=0; + + xspread=gpSpreadPattern[ubSpreadIndex].xspread; + yspread=gpSpreadPattern[ubSpreadIndex].yspread; + + //Only use randomized spread pattern if the random spread is defined AND each static angle already fired once. + if( ubLoop >= gpSpreadPattern[ubSpreadIndex].iCount && (xspread + yspread) ){ + //Create random angle within range, positive and negative. + switch( gpSpreadPattern[ubSpreadIndex].method ) + { + case SPREADPATTERNMETHOD_RECT: //Rectangle Method. (Simple) + //Applying a new random number to each angle results in a rectangular spread pattern, rather than an oval one. + ddHorizAngle = (double)rand() * 2 * xspread / RAND_MAX - xspread; + ddVerticAngle = (double)rand() * 2 * yspread / RAND_MAX - yspread; + break; + case SPREADPATTERNMETHOD_DIAMOND: //Diamond Method. (Kinda Simple) + //Angles are generated within a diamond shaped region. + //This is more natural than the rectangular pattern, but still not optimal. + //The first random number is in a range of 0 to (x+y). + d = (xspread+yspread) * (double)rand() / (double)RAND_MAX; + //The second random number determines the percentage of that value to use on x. The rest is spent on y. + r = (double)rand() / (double)RAND_MAX; + ddHorizAngle = r*d; + ddVerticAngle = (1-r)*d; + //Positive and negative are then randomly determined. Otherwise, everthing would always shoot to the high right (+,+). + n = rand()%4; + if(n&1) ddHorizAngle *= -1; + if(n&2) ddVerticAngle *= -1; + break; + case SPREADPATTERNMETHOD_ELLIPSE: //Ellipse Method. + //Angles are generated within an ellipse. + //This is getting close to true spread behaviour, and may be the preferred general purpose method. + //However, due to the distribution of random numbers for r, the pattern generated tends to have + //more pellets land along the axis, making the pattern look a bit like the Swiss cross sometimes. + //(It only becomes noticable in simulations I do in an external program. In-game it looks fine.) + // + //First, get our random range of -pi to pi. This could be recalculated for every use, but doesn't need to be. + r=(double)rand()*2*PI/RAND_MAX - PI; + //Which axis is our major axis? + if( xspread > yspread ) + { + //The Ellipse. + // Any point on an Ellipse border line = (x,y) + // where x = m * cos(r) and y = n * sin(r) + // where m = major axis radius and n = minor axis radius and r = all values -pi <= r <= pi + //We use any random value between -pi and pi, but vary the radius to get points inside the ellipse as well. + ddHorizAngle = ( (double)rand()*xspread / RAND_MAX ) * cos(r); + ddVerticAngle = ( (double)rand()*yspread / RAND_MAX ) * sin(r); + }else{ + //Reverse sine and cosine if our major axis is y. + ddHorizAngle = ( (double)rand()*xspread / RAND_MAX ) * sin(r); + ddVerticAngle = ( (double)rand()*yspread / RAND_MAX ) * cos(r); + } + break; + case -1: //Optimal Method. (Most realistic) + //Not yet implemented. + //Using the random distribution above, normal distrubution causes more pellets to end up toward the middle. + //This is generally not noticable, and in fact feels more natural to some people (not zilpin), + //but is not _even_ distribution. + //In addition, since it is random, it is possible for all of the pellets to vear off center, + //resulting in rare unnatural freak shots, which would never occur in a real shotgun. + //This may add to gameplay, but it may bother some people (like zilpin). + //Should anyone devise a way to adjust the ellipse distribution to account for this, here is where to put it. + //This would probably be done by adjusting the random value of 'r' above. + //Contact zilpin for real life shotgun spread pattern data, if you happen to be one of those mathematicians + //who can extrapolate a succinct algorithm from raw data. But keep in mind that there's not much data to go on. + // + //Also note that in the real world, shot spread tends to take the shape of a sagging funnel, rather than a cone. + //That is to say, the path of each pellet follows a mild curve, not a straight line. + //I'm really not going to worry about that here. Ever. + break; + default: + //If an invalid method is set in the structure, no randomized pellets are shot. + //Note that there still may have been static angle pellets fired. + //This should never happen, since the XML loading function will set them to giSpreadPatternMethod_Default. + break; + } + }else if( gpSpreadPattern[ubSpreadIndex].iCount>0 ){ + //Use static angle, if they exist. + n = ubLoop % gpSpreadPattern[ubSpreadIndex].iCount; + ddHorizAngle = gpSpreadPattern[ubSpreadIndex].x[n]; + ddVerticAngle = gpSpreadPattern[ubSpreadIndex].y[n]; + }else{ + //No angles defined, so just fire straight. + ddHorizAngle = ddVerticAngle = 0.0; + } + + + //#ifdef JA2TESTVERSION + DOUBLE ddRawHorizAngle = ddHorizAngle; + DOUBLE ddRawVerticAngle = ddVerticAngle; + //#endif + + //Adjust based on the to-hit deviation calculated when the first pellet was fired. + ddHorizAngle += ddAdjustedHorizAngle; + ddVerticAngle += ddAdjustedVerticAngle; + + //Logging for debugging + //#ifdef JA2TESTVERSION + if( TRUE ) + { + FILE *OutFile; + if ((OutFile = fopen("SpreadPatternLog.txt", "a+t")) != NULL) + { + //To easily cut-and-paste these values from the log into a C/C++ source file for later analysis + //Lots of reference debug info in a comment. + fprintf(OutFile, "{ % 9.8f , % 9.8f , % 9.8f , % 9.8f }, //DEBUG: merc %4d fired pellet %4d of %4d using method %4d %12s with SpreadPattern %4d %s\n", + ddRawHorizAngle, ddRawVerticAngle, + ddHorizAngle, ddVerticAngle, + NOBODY, ubLoop, ubShots, + gpSpreadPattern[ubSpreadIndex].method, gSpreadPatternMethodNames[gpSpreadPattern[ubSpreadIndex].method], + ubSpreadIndex, gpSpreadPattern[ubSpreadIndex].Name, + NULL + ); + fclose(OutFile); + } + } + //#endif + } + + //Just calculate the increments the bullet will use, not any of the to-hit adjustments, because we already did. + CalculateFiringIncrementsSimple( ddHorizAngle, ddVerticAngle, pBullet ); + } + + + pBullet->ddHorizAngle = ddHorizAngle; + + /*if (ubLoop == 0 && pFirer->bDoBurst < 2) + { + pBullet->fAimed = TRUE; + } + else + { + // buckshot pellets after the first can hit friendlies even at close range + pBullet->fAimed = FALSE; + }*/ + + // buckshot pellets after the first can hit friendlies even at close range + pBullet->fAimed = FALSE; + + if ( pBullet->usFlags & BULLET_FLAG_KNIFE ) + { + pBullet->ubItemStatus = (*pObj)[0]->data.objectStatus; + } + + // apply increments for first move + + //zilpin: pellet spread patterns externalized in XML + //This is a bugfix for strange behavior when firing, such as the projectile hitting walls behind a merc. + //The bullet should start it's journey at the beginning, not down range. + //Commented out the qIncr adjustments. + pBullet->qCurrX = FloatToFixed( dStartX ) ; //+ pBullet->qIncrX; + pBullet->qCurrY = FloatToFixed( dStartY ) ; //+ pBullet->qIncrY; + pBullet->qCurrZ = FloatToFixed( dStartZ ) ; //+ pBullet->qIncrZ; + + //zilpin: pellet spread patterns externalized in XML + //Also bugfix. I understand why this was added, but it causes problems. + /* + // NB we can only apply correction for leftovers if the bullet is going to hit + // because otherwise the increments are not right for the calculations! + if ( pBullet->sHitBy >= 0 ) + { + pBullet->qCurrX += ( FloatToFixed( dDeltaX ) - pBullet->qIncrX * iDistance ) / 2; + pBullet->qCurrY += ( FloatToFixed( dDeltaY ) - pBullet->qIncrY * iDistance ) / 2; + pBullet->qCurrZ += ( FloatToFixed( dDeltaZ ) - pBullet->qIncrZ * iDistance ) / 2; + } + */ + + pBullet->iImpact = ubImpact; + + pBullet->iRange = GunRange( pObj, NULL ); // SANDRO - added argument + // HEADROCK HAM 5.1: Define original point. + pBullet->sOrigGridNo = ((INT32)dStartX) / CELL_X_SIZE + ((INT32)dStartY) / CELL_Y_SIZE * WORLD_COLS; + pBullet->sTargetGridNo = ((INT32)dEndX) / CELL_X_SIZE + ((INT32)dEndY) / CELL_Y_SIZE * WORLD_COLS; + + //pBullet->bStartCubesAboveLevelZ = (INT8) CONVERT_HEIGHTUNITS_TO_INDEX( (INT32)dStartZ - CONVERT_PIXELS_TO_HEIGHTUNITS( gpWorldLevelData[ gridno ].sHeight ) ); + //pBullet->bEndCubesAboveLevelZ = (INT8) CONVERT_HEIGHTUNITS_TO_INDEX( (INT32)dEndZ - CONVERT_PIXELS_TO_HEIGHTUNITS( gpWorldLevelData[ pBullet->sTargetGridNo ].sHeight ) ); + + pBullet->bStartCubesAboveLevelZ = (INT8) CONVERT_HEIGHTUNITS_TO_INDEX( (INT32)dStartZ ); + pBullet->bEndCubesAboveLevelZ = (INT8) CONVERT_HEIGHTUNITS_TO_INDEX( (INT32)dEndZ ); + + // HEADROCK HAM BETA2.5: New method for signifying whether a bullet is a tracer or not, using an individual + // bullet structure flag. Hehehehe, I think this is kind of reverting to old code, isn't it? + /*if (gGameExternalOptions.ubRealisticTracers > 0 && gGameExternalOptions.ubNumBulletsPerTracer > 0 && (pFirer->bDoAutofire > 0 || pFirer->bDoBurst > 0) + && AmmoTypes[ (*pObj)[0]->data.gun.ubGunAmmoType ].tracerEffect ) + { + UINT16 iBulletsLeft, iBulletsPerTracer; + iBulletsPerTracer = gGameExternalOptions.ubNumBulletsPerTracer; + iBulletsLeft = (*pObj)[0]->data.gun.ubGunShotsLeft + pFirer->bDoBurst; + + // Is this specific bullet a tracer? - based on how many tracers there are per regular bullets in + // a tracer magazine (INI-settable). + if ((((iBulletsLeft - (pFirer->bDoBurst - 1)) / iBulletsPerTracer) - ((iBulletsLeft - pFirer->bDoBurst) / iBulletsPerTracer)) == 1) + { + pBullet->fTracer = TRUE; + } + else + { + pBullet->fTracer = FALSE; + } + }*/ + + ///////////////////////// SOUND //////////////////////////// + //PLAY SOUND + // ( For throwing knife.. it's earlier in the animation + if ( Weapon[ pObj->usItem ].sSound != 0 && Item[ pObj->usItem ].usItemClass != IC_THROWING_KNIFE ) + { + // Switch on silencer... + UINT16 noisefactor = GetPercentNoiseVolume( pObj ); + if( noisefactor < MAX_PERCENT_NOISE_VOLUME_FOR_SILENCED_SOUND || Weapon[ pObj->usItem ].ubAttackVolume <= 10 ) + { + INT32 uiSound; + + uiSound = Weapon [ pObj->usItem ].silencedSound; + + PlayJA2Sample( uiSound, RATE_11025, SoundVolume( HIGHVOLUME, gridno ), 1, SoundDir( gridno ) ); + } + else + { + INT8 volume = HIGHVOLUME; + if ( noisefactor < 100 ) volume = (volume * noisefactor) / 100; + PlayJA2Sample( Weapon[ pObj->usItem ].sSound, RATE_11025, SoundVolume( volume, gridno ), 1, SoundDir( gridno ) ); + } + } + ///////////////////////// SOUND //////////////////////////// + + // deduct ammo + (*pObj)[0]->data.gun.ubGunShotsLeft = max(0, (*pObj)[0]->data.gun.ubGunShotsLeft - 1); + + //gTacticalStatus.ubAttackBusyCount++; + + if(is_client)send_bullet( pBullet, pObj->usItem ); + FireBullet( pThrower, pBullet, FALSE ); + + ///////////////////////// SOUND //////////////////////////// + UINT8 ubVolume = Weapon[ pObj->usItem ].ubAttackVolume; + + // Snap: get cumulative noise reduction from the weapon and its attachments + UINT16 noisefactor = GetPercentNoiseVolume( pObj ); + if ( ubVolume * noisefactor > 25000 ) + { // Snap: hack this to prevent overflow (damn miserly programmers!) + ubVolume = 250; + } + else + { + ubVolume = __max( 1, ( ubVolume * GetPercentNoiseVolume( pObj ) ) / 100 ); + } + + MakeNoise( NOBODY, gridno, 0, pThrower->bOverTerrainType, ubVolume, NOISE_GUNFIRE ); + ///////////////////////// SOUND //////////////////////////// + + ///////////////////////// OVERHEATING AND STATUS REDUCTION //////////////////////////// + INT16 iOverheatReliabilityMalus = 0; + // Flugente: Increase Weapon Temperature + if ( gGameOptions.fWeaponOverheating ) + { + FLOAT overheatjampercentage = GetGunOverheatDamagePercentage( pObj ); // ... how much above the gun's usOverheatingDamageThreshold are we? ... + + if ( overheatjampercentage > 1.0 ) + iOverheatReliabilityMalus = (INT16)floor(overheatjampercentage*overheatjampercentage); + + GunIncreaseHeat( pObj ); + } + + // Flugente : Added a malus to reliability for overheated guns + // HEADROCK HAM 5: Variable NCTH base change + UINT32 uiDepreciateTest = 0; + if ( UsingNewCTHSystem() == true) + { + UINT16 usBaseChance = gGameCTHConstants.BASIC_RELIABILITY_ODDS; + FLOAT dReliabilityRatio = 3.0f * ((FLOAT)usBaseChance / (FLOAT)BASIC_DEPRECIATE_CHANCE); // Compare original odds to new odds. + uiDepreciateTest = usBaseChance + (INT16)( dReliabilityRatio * GetReliability( pObj ) - iOverheatReliabilityMalus); + uiDepreciateTest = max(0, uiDepreciateTest); + } + else + { + uiDepreciateTest = max( BASIC_DEPRECIATE_CHANCE + 3 * GetReliability( pObj ) - iOverheatReliabilityMalus, 0); + } + if ( !PreRandom( uiDepreciateTest ) && ( (*pObj)[0]->data.objectStatus > 1) ) + { + (*pObj)[0]->data.objectStatus--; + } + ///////////////////////// OVERHEATING AND STATUS REDUCTION //////////////////////////// + + ///////////////////////// JAMMING //////////////////////////// + // Algorithm for jamming + int maxJamChance = 50; // Externalize this? + int reliability = GetReliability( pObj ); + int condition = (*pObj)[0]->data.gun.bGunStatus; + int invertedBaseJamChance = condition + (reliability * 2) - gGameExternalOptions.ubWeaponReliabilityReductionPerRainIntensity * 1; + + // Flugente FTW 1: If overheating is allowed, a gun will be prone to more overheating if its temperature is high + if ( gGameOptions.fWeaponOverheating ) + { + FLOAT overheatjampercentage = GetGunOverheatJamPercentage( pObj ); // how much above the gun's usOverheatingJamThreshold are we? ... + + int overheatjamfactor = (int)(100* overheatjampercentage); // We need an integer value and rough percentages + + overheatjamfactor = max(0, overheatjamfactor - 100); // If we haven't reached the OverheatJamThreshold, no increased chance of jamming because of overheating + + invertedBaseJamChance -= overheatjamfactor; // lower invertedBaseJamChance (thereby increasing jamChance later on) + } + + if (invertedBaseJamChance < 0) + invertedBaseJamChance = 0; + else if (invertedBaseJamChance > 100) + invertedBaseJamChance = 100; + int jamChance = 100 - (int)sqrt((double)invertedBaseJamChance * (75.0 + (double)invertedBaseJamChance / 2.0)); + if (jamChance < 0) + jamChance = 0; + else if (jamChance > maxJamChance - reliability) + jamChance = maxJamChance - reliability; + + + if ((INT32) PreRandom( 100 ) < jamChance ) + { + // jam! negate the gun ammo status. + (*pObj)[0]->data.gun.bGunAmmoStatus *= -1; + } + ///////////////////////// JAMMING //////////////////////////// + + // manual recharge + if (Weapon[Item[pObj->usItem].ubClassIndex].APsToReloadManually > 0) + (*pObj)[0]->data.gun.ubGunState &= ~GS_CARTRIDGE_IN_CHAMBER; + // + } + + + return( TRUE ); +} + INT8 ChanceToGetThrough( SOLDIERTYPE * pFirer, FLOAT dEndX, FLOAT dEndY, FLOAT dEndZ ) { if ( Item[pFirer->usAttackingWeapon].usItemClass == IC_GUN || Item[ pFirer->usAttackingWeapon ].usItemClass == IC_THROWING_KNIFE || Item[pFirer->usAttackingWeapon].rocketlauncher ) diff --git a/Tactical/LOS.h b/Tactical/LOS.h index f6588706..834a0610 100644 --- a/Tactical/LOS.h +++ b/Tactical/LOS.h @@ -70,6 +70,9 @@ INT8 FireBulletGivenTargetNCTH( SOLDIERTYPE * pFirer, FLOAT dEndX, FLOAT dEndY, // HEADROCK HAM 5: Function for fragments ejected from an explosion. INT8 FireFragmentGivenTarget( SOLDIERTYPE * pThrower, FLOAT dStartX, FLOAT dStartY, FLOAT dStartZ, FLOAT dEndX, FLOAT dEndY, FLOAT dEndZ, UINT16 usExplosiveItem ); +// Flugente: fire a shot from a gun that has no user (used for traps with attached guns) +INT8 FireBulletGivenTargetTrapOnly( SOLDIERTYPE* pThrower, OBJECTTYPE* pObj, INT32 gridno, FLOAT dStartZ, FLOAT dEndX, FLOAT dEndY, FLOAT dEndZ, INT16 sHitBy); + #define CALC_FROM_ALL_DIRS -1 #define CALC_FROM_WANTED_DIR -2 #define NO_DISTANCE_LIMIT -3 diff --git a/Tactical/Weapons.cpp b/Tactical/Weapons.cpp index 9322aa69..7dc85034 100644 --- a/Tactical/Weapons.cpp +++ b/Tactical/Weapons.cpp @@ -66,9 +66,6 @@ extern INT8 gbCurrentRainIntensity; //#define MINCHANCETOHIT 1 //#define MAXCHANCETOHIT 99 -// NB this is arbitrary, chances in DG ranged from 1 in 6 to 1 in 20 -#define BASIC_DEPRECIATE_CHANCE 15 - #define NORMAL_RANGE 90 // # world units considered an 'avg' shot #define MIN_SCOPE_RANGE 60 // # world units after which scope's useful diff --git a/Tactical/Weapons.h b/Tactical/Weapons.h index ba0ea583..834b3d01 100644 --- a/Tactical/Weapons.h +++ b/Tactical/Weapons.h @@ -77,6 +77,9 @@ void HandleTacticalEffectsOfEquipmentChange( SOLDIERTYPE *pSoldier, UINT32 uiInv #define MIN_MORTAR_RANGE 150 // minimum range of a mortar +// NB this is arbitrary, chances in DG ranged from 1 in 6 to 1 in 20 +#define BASIC_DEPRECIATE_CHANCE 15 + // WEAPON CLASSES enum { diff --git a/TileEngine/Explosion Control.cpp b/TileEngine/Explosion Control.cpp index 20d45eca..c80a631d 100644 --- a/TileEngine/Explosion Control.cpp +++ b/TileEngine/Explosion Control.cpp @@ -106,6 +106,9 @@ UINT8 DetermineFlashbangEffect( SOLDIERTYPE *pSoldier, INT8 ubExplosionDir, BOOL // HEADROCK HAM 5.1: Explosion Fragments launcher void FireFragments( SOLDIERTYPE * pThrower, INT16 sX, INT16 sY, INT16 sZ, UINT16 usItem, UINT8 ubDirection = DIRECTION_IRRELEVANT ); +// Flugente: shoot a gun without anyone operating it (used for makeshift traps wih guns) +void FireFragmentsTrapGun( SOLDIERTYPE* pThrower, INT32 gridno, INT16 sZ, OBJECTTYPE* pObj, UINT8 ubDirection = NORTH ); + extern INT8 gbSAMGraphicList[ MAX_NUMBER_OF_SAMS ]; extern void AddToShouldBecomeHostileOrSayQuoteList( UINT8 ubID ); extern void RecompileLocalMovementCostsForWall( INT32 sGridNo, UINT8 ubOrientation ); @@ -3604,7 +3607,9 @@ BOOLEAN ActivateSurroundingTripwire( UINT8 ubID, INT32 sGridNo, INT8 bLevel, UIN if ( Item[pObj->usItem].tripwireactivation == 1 ) { // tripwire just gets activated - if ( Item[pObj->usItem].tripwire == 1 ) + // this is important - we have to check for the tripwire's temperature. + // The temperature serves as a temporary flag replacement, marking that a tripwire has already been activated, adn shouldn't be activated again + if ( Item[pObj->usItem].tripwire == 1 && (*pObj)[0]->data.bTemperature < 1 ) { // determine this tripwire's flag UINT32 ubWireNetworkFlag = (*pObj)[0]->data.ubWireNetworkFlag; @@ -3635,32 +3640,28 @@ BOOLEAN ActivateSurroundingTripwire( UINT8 ubID, INT32 sGridNo, INT8 bLevel, UIN if ( samenetwork && sameorlowerhierarchy ) { - // if we have passed the check, our tripwire belongs to the same tripwirenetwork as the one that caused this call - // it is also of the sme or a lower hierarchy - // so, this wire calls other wires in the surrounding area. But the new flag is ubWireNetworkFlag instead of ubFlag, so the hierarchy level might be lower + gubPersonToSetOffExplosions = ubID; - OBJECTTYPE newtripwireObject; - CreateItem( pObj->usItem, (*pObj)[0]->data.objectStatus, &newtripwireObject ); - - // this is important: delete the tripwire, otherwise we get into an infinite loop if there are two piecs of tripwire.... - RemoveItemFromPool( adjgrid, gWorldBombs[ uiWorldBombIndex ].iItemIndex, bLevel ); - - // if no other bomb exists here - if ( FindWorldItemForBombInGridNo(adjgrid, bLevel) == -1 ) + // put this bomb on the queue + AddBombToQueue( uiWorldBombIndex, uiTimeStamp ); + if (pObj->usItem != ACTION_ITEM || (*pObj)[0]->data.misc.bActionValue == ACTION_ITEM_BLOW_UP) { - // make sure no one thinks there is a bomb here any more! - if ( gpWorldLevelData[adjgrid].uiFlags & MAPELEMENT_PLAYER_MINE_PRESENT ) - { - RemoveBlueFlag( adjgrid, bLevel ); - } - gpWorldLevelData[adjgrid].uiFlags &= ~(MAPELEMENT_ENEMY_MINE_PRESENT); + uiTimeStamp += BOMB_QUEUE_DELAY; } - // no add a tripwire item to the floor, simulating that activating tripwire deactivates it - AddItemToPool( adjgrid, &newtripwireObject, 1, bLevel, 0, -1 ); + if ( (*pObj)[0]->data.misc.usBombItem != NOTHING && Item[ (*pObj)[0]->data.misc.usBombItem ].usItemClass & IC_EXPLOSV ) + { + fFoundMine = TRUE; + } + + // Flugente hack: the tripwire will get removed in HandleExplosionQueue (it is still needed in there until the bomb gets called). + // Because of this, ActivateSurroundingTripwire wouldm normally find this wire again, a loop would occur. + // As i do not want to create an itm flag for this purpose right now (but its on my TODO-list), we set the temperature of the wire to a higher value + // (temperature for tripwire isn't needed anywhere else, so its ok) + (*pObj)[0]->data.bTemperature = 1000.0; // activate surrounding tripwires, unless tripwire is too much damaged and we are unlucky.. - if ( newtripwireObject[0]->data.objectStatus > (INT16)Random(50) ) + if ( (*pObj)[0]->data.objectStatus > (INT16)Random(50) ) fFoundMine = ActivateSurroundingTripwire(ubID, adjgrid, bLevel, ubWireNetworkFlag); } } @@ -3699,6 +3700,53 @@ BOOLEAN ActivateSurroundingTripwire( UINT8 ubID, INT32 sGridNo, INT8 bLevel, UIN return( fFoundMine ); } +// Flugente: A special function for tripwire gun traps. Search if pObj has a gun attached. If so, fire a shot from that gun in a specific direction. Afterwards place the gun on the ground +void CheckAndFireTripwireGun( OBJECTTYPE* pObj, INT32 sGridNo, INT8 bLevel, UINT8 ubId, UINT8 ubDirection ) +{ + if ( !pObj ) + return; + + // search for attached guns + BOOLEAN fgunfound = FALSE; + OBJECTTYPE* pAttGun = NULL; + // check all attachments + attachmentList::iterator iterend = (*pObj)[0]->attachments.end(); + for (attachmentList::iterator iter = (*pObj)[0]->attachments.begin(); iter != iterend; ++iter) + { + if ( iter->exists() && Item[iter->usItem].usItemClass == IC_GUN ) + { + fgunfound = TRUE; + pAttGun = &(*iter); + break; + } + } + + if ( fgunfound && pAttGun ) + { + // we actually found a gun. + // if the gun has ammo and is not jammed, fire it + if ( (*pAttGun)[0]->data.gun.ubGunShotsLeft > 0 && (*pAttGun)[0]->data.gun.bGunAmmoStatus > 0 ) + { + // Increment attack counter... + if (gubElementsOnExplosionQueue == 0) + { + // single explosion, disable sight until the end, and set flag + // to check sight at end of attack + + gTacticalStatus.uiFlags |= (DISALLOW_SIGHT | CHECK_SIGHT_AT_END_OF_ATTACK); + } + + FireFragmentsTrapGun( MercPtrs[ubId], sGridNo, 0, pAttGun, ubDirection ); + + // this is important... if not set, the game will remain in a loop + gTacticalStatus.ubAttackBusyCount = 0; + } + + // add this gun to the floor + AddItemToPool( sGridNo, pAttGun, 1, bLevel, 0, -1 ); + } +} + void HandleExplosionQueue( void ) { UINT32 uiIndex; @@ -3743,6 +3791,39 @@ void HandleExplosionQueue( void ) NewLightEffect( sGridNo, (UINT8)Explosive[pObj->usItem].ubDuration, (UINT8)Explosive[pObj->usItem].ubStartRadius ); RemoveItemFromPool( sGridNo, gWorldBombs[ uiWorldBombIndex ].iItemIndex, ubLevel ); } + // Flugente: handle tripwire gun traps here... + // tripwire gets called and activated in ActivateSurroundingTripwire + else if ( Item[pObj->usItem].tripwire == 1 ) + { + // check if there is a gun attached to this piece of wire, and eventually fire it + CheckAndFireTripwireGun( pObj, gWorldItems[ gWorldBombs[uiWorldBombIndex].iItemIndex ].sGridNo, ubLevel, (*pObj)[0]->data.misc.ubBombOwner, (*pObj)[0]->data.ubDirection ); + + OBJECTTYPE newtripwireObject; + CreateItem( pObj->usItem, (*pObj)[0]->data.objectStatus, &newtripwireObject ); + + // determine this tripwire's flag + UINT32 ubWireNetworkFlag = (*pObj)[0]->data.ubWireNetworkFlag; + + // this is important: delete the tripwire, otherwise we get into an infinite loop if there are two piecs of tripwire.... + RemoveItemFromPool( sGridNo, gWorldBombs[ uiWorldBombIndex ].iItemIndex, ubLevel ); + + // if no other bomb exists here + if ( FindWorldItemForBombInGridNo(sGridNo, ubLevel) == -1 ) + { + // make sure no one thinks there is a bomb here any more! + if ( gpWorldLevelData[sGridNo].uiFlags & MAPELEMENT_PLAYER_MINE_PRESENT ) + { + RemoveBlueFlag( sGridNo, ubLevel ); + } + gpWorldLevelData[sGridNo].uiFlags &= ~(MAPELEMENT_ENEMY_MINE_PRESENT); + } + + // we have to set the wire's temperature back to 0, otherwise tripwire will only work once + (newtripwireObject)[0]->data.bTemperature = 0; + + // now add a tripwire item to the floor, simulating that activating tripwire deactivates it + AddItemToPool( sGridNo, &newtripwireObject, 1, ubLevel, 0, -1 ); + } else { gfExplosionQueueMayHaveChangedSight = TRUE; @@ -4204,32 +4285,30 @@ BOOLEAN SetOffBombsInGridNo( UINT8 ubID, INT32 sGridNo, BOOLEAN fAllBombs, INT8 // Flugente: a tripwire activates all other tripwires in connection, and detonates all bombs in connection that are tripwire-activated else if ( Item[pObj->usItem].tripwire == 1 ) { - OBJECTTYPE newtripwireObject; - CreateItem( pObj->usItem, (*pObj)[0]->data.objectStatus, &newtripwireObject ); + gubPersonToSetOffExplosions = ubID; - // determine this tripwire's flag - UINT32 ubWireNetworkFlag = (*pObj)[0]->data.ubWireNetworkFlag; - - // this is important: delete the tripwire, otherwise we get into an infinite loop if there are two piecs of tripwire.... - RemoveItemFromPool( sGridNo, gWorldBombs[ uiWorldBombIndex ].iItemIndex, bLevel ); - - // if no other bomb exists here - if ( FindWorldItemForBombInGridNo(sGridNo, bLevel) == -1 ) + // put this bomb on the queue + AddBombToQueue( uiWorldBombIndex, uiTimeStamp ); + if (pObj->usItem != ACTION_ITEM || (*pObj)[0]->data.misc.bActionValue == ACTION_ITEM_BLOW_UP) { - // make sure no one thinks there is a bomb here any more! - if ( gpWorldLevelData[sGridNo].uiFlags & MAPELEMENT_PLAYER_MINE_PRESENT ) - { - RemoveBlueFlag( sGridNo, bLevel ); - } - gpWorldLevelData[sGridNo].uiFlags &= ~(MAPELEMENT_ENEMY_MINE_PRESENT); + uiTimeStamp += BOMB_QUEUE_DELAY; + //uiTimeStamp += BOMB_QUEUE_DELAY; } - // no add a tripwire item to the floor, simulating that activating tripwire deactivates it - AddItemToPool( sGridNo, &newtripwireObject, 1, bLevel, 0, -1 ); + if ( (*pObj)[0]->data.misc.usBombItem != NOTHING && Item[ (*pObj)[0]->data.misc.usBombItem ].usItemClass & IC_EXPLOSV ) + { + fFoundMine = TRUE; + } + + // Flugente hack: the tripwire will get removed in HandleExplosionQueue (it is still needed in there until the bomb gets called). + // Because of this, ActivateSurroundingTripwire wouldm normally find this wire again, a loop would occur. + // As i do not want to create an itm flag for this purpose right now (but its on my TODO-list), we set the temperature of the wire to a higher value + // (temperature for tripwire isn't needed anywhere else, so its ok) + (*pObj)[0]->data.bTemperature = 1000.0; // activate surrounding tripwires and tripwire-activated mines, unless tripwire is too much damaged and we are unlucky.. - if ( newtripwireObject[0]->data.objectStatus > (INT16)Random(50) ) - fFoundMine = ActivateSurroundingTripwire(ubID, sGridNo, bLevel, ubWireNetworkFlag); + if ( (*pObj)[0]->data.objectStatus > (INT16)Random(50) ) + fFoundMine = ActivateSurroundingTripwire(ubID, sGridNo, bLevel, (*pObj)[0]->data.ubWireNetworkFlag); } else { @@ -4759,6 +4838,91 @@ void FireFragments( SOLDIERTYPE * pThrower, INT16 sX, INT16 sY, INT16 sZ, UINT16 } } +// Flugente: shoot a gun without anyone operating it (used for makeshift traps wih guns) +void FireFragmentsTrapGun( SOLDIERTYPE* pThrower, INT32 gridno, INT16 sZ, OBJECTTYPE* pObj, UINT8 ubDirection ) +{ + if ( !pObj ) + return; + + if ( !pThrower ) + return; + + // at the moment always only 1 bullet + UINT16 usNumFragments = 1; + UINT16 ubFragRange = GunRange( pObj, NULL ); + + // deviation arcs. A gun fired by tripping a wire isn't exactly precise + INT16 horizontalarc = 2; + INT16 verticalarc = 2; + + INT16 sX = CenterX(gridno); + INT16 sY = CenterY(gridno); + + AssertMsg( ubFragRange > 0 , "Fragmentation data lacks range property!" ); + + for (UINT16 x = 0; x < usNumFragments; ++x) + { + FLOAT dRandomX = 0; + FLOAT dRandomY = 0; + FLOAT dRandomZ = 0; + + // Flugente: if item is a directional explosive, determine in what direction the frags should fly + INT16 degree = (45 + ubDirection * 45) % 360; // modulo 360 to prevent nonsense from nonsensical input + //INT16 horizontalarc = horizontalarc % 360; // modulo 360 to prevent nonsense from nonsensical input + INT16 halfhorizontalarc = (INT16)(horizontalarc / 2); + INT16 sLowHorizontalD = (360 + degree - halfhorizontalarc) % 360; + INT16 dRandomDegreeH = (sLowHorizontalD + Random(horizontalarc) ) % 360; + + // transform the degree into our coordinates + if ( dRandomDegreeH < 90 ) + { + dRandomX = (FLOAT)dRandomDegreeH / 45.0f; + dRandomY = 0.0; + } + else if ( dRandomDegreeH < 180 ) + { + dRandomX = (FLOAT)2.0; + dRandomY = (FLOAT)(dRandomDegreeH - 90 ) / 45.0f; + } + else if ( dRandomDegreeH < 270 ) + { + dRandomX = (FLOAT)(270 - dRandomDegreeH) / 45.0f; + dRandomY = (FLOAT)2.0; + } + else + { + dRandomX = 0.0; + dRandomY = (FLOAT)(360 - dRandomDegreeH) / 45.0f; + } + + // X and Y now need to be distributed, at the moment they are on a circle + // project into [-1.0, 1.0] + dRandomX -= 1.0f; + dRandomY -= 1.0f; + + // vertical stuff + //INT16 verticalarc = verticalarc % 180; // modulo 180 to prevent nonsense from nonsensical input + INT16 halfverticalarc = (INT16)(verticalarc / 2); + INT16 sLowVerticalD = (90 - halfverticalarc) % 180; + + INT16 dRandomDegreeV = sLowVerticalD + Random(verticalarc); + + dRandomZ = ((FLOAT)(dRandomDegreeV) / 90.0f) - 1.0f; + + FLOAT dDeltaX = (dRandomX * ubFragRange); + FLOAT dDeltaY = (dRandomY * ubFragRange); + FLOAT dDeltaZ = ((dRandomZ * 25.6f) * 50 ); + + FLOAT dRangeMultiplier = 10; // Arbitrary, but gives good results. + + FLOAT dEndX = (FLOAT)(sX + (dDeltaX * dRangeMultiplier)); + FLOAT dEndY = (FLOAT)(sY + (dDeltaY * dRangeMultiplier)); + FLOAT dEndZ = (FLOAT)(sZ + (dDeltaZ * dRangeMultiplier)); + + FireBulletGivenTargetTrapOnly( pThrower, pObj, gridno, 150, dEndX, dEndY, dEndZ, 100 ); + } +} + #ifdef JA2UB //-- UB diff --git a/TileEngine/Explosion Control.h b/TileEngine/Explosion Control.h index ee5a56f1..7e1eda0b 100644 --- a/TileEngine/Explosion Control.h +++ b/TileEngine/Explosion Control.h @@ -136,6 +136,9 @@ void AddBombToQueue( UINT32 uiWorldBombIndex, UINT32 uiTimeStamp, BOOL fFromRemo // Flugente: activate everything connected to a tripwire in the surrounding if sGridNo on level bLevel with regard to the tripwire netwrok and hierarchy determined by ubFlag BOOLEAN ActivateSurroundingTripwire( UINT8 ubID, INT32 sGridNo, INT8 bLevel, UINT32 ubFlag ); +// Flugente: A special function for tripwire gun traps. Search if pObj has a gun attached. If so, fire a shot from that gun in a specific direction. Afterwards place the gun on the ground +void CheckAndFireTripwireGun( OBJECTTYPE* pObj, INT32 sGridNo, INT8 bLevel, UINT8 ubId, UINT8 ubDirection ); + extern void ToggleActionItemsByFrequency( INT8 bFrequency ); extern void PerformItemAction( INT32 sGridNo, OBJECTTYPE * pObj ); extern BOOLEAN gfExplosionQueueMayHaveChangedSight;