diff --git a/GameSettings.cpp b/GameSettings.cpp index 2daf95de..32aaee68 100644 --- a/GameSettings.cpp +++ b/GameSettings.cpp @@ -3161,6 +3161,11 @@ void LoadCTHConstants() gGameCTHConstants.GRAVITY_COEFFICIENT = iniReader.ReadFloat("General", "GRAVITY_COEFFICIENT", 1.0, 0.001f, 100.0); gGameCTHConstants.VERTICAL_BIAS = iniReader.ReadFloat("General", "VERTICAL_BIAS", 1.0f, 0.01f, 2.0f); gGameCTHConstants.SCOPE_RANGE_MULTIPLIER = iniReader.ReadFloat("General", "SCOPE_RANGE_MULTIPLIER", 0.7f, 0.5f, 1.5f); + gGameCTHConstants.SCOPE_EFFECTIVENESS_MULTIPLIER = iniReader.ReadFloat("General", "SCOPE_EFFECTIVENESS_MULTIPLIER", 1.1f, 0.5f, 1.5f); + gGameCTHConstants.SCOPE_EFFECTIVENESS_MINIMUM = iniReader.ReadInteger("General", "SCOPE_EFFECTIVENESS_MINIMUM", 50, 0, 100); + gGameCTHConstants.SCOPE_EFFECTIVENESS_MINIMUM_RANGER = iniReader.ReadInteger("General", "SCOPE_EFFECTIVENESS_MINIMUM_RANGER", 80, 0, 100); + gGameCTHConstants.SCOPE_EFFECTIVENESS_MINIMUM_MARKSMAN = iniReader.ReadInteger("General", "SCOPE_EFFECTIVENESS_MINIMUM_MARKSMAN", 90, 0, 100); + gGameCTHConstants.SCOPE_EFFECTIVENESS_MINIMUM_SNIPER = iniReader.ReadInteger("General", "SCOPE_EFFECTIVENESS_MINIMUM_SNIPER", 100, 0, 100); gGameCTHConstants.SIDE_FACING_DIVISOR = iniReader.ReadFloat("General", "SIDE_FACING_DIVISOR", 2.0, 1.0f, 10.0f); // HEADROCK HAM 5: Basic chance to lose condition point when firing gGameCTHConstants.BASIC_RELIABILITY_ODDS = iniReader.ReadInteger("General", "BASIC_RELIABILITY_ODDS", 15, 0, 100); diff --git a/GameSettings.h b/GameSettings.h index 27e24bdf..de2b5aff 100644 --- a/GameSettings.h +++ b/GameSettings.h @@ -1815,6 +1815,11 @@ typedef struct FLOAT GRAVITY_COEFFICIENT; // Changes the way gravity works in the game. Higher values mean bullets don't drop as quickly after reaching max range. FLOAT VERTICAL_BIAS; // This float can be used to reduce the chance of missing too far upwards or downwards (compared to left/right). FLOAT SCOPE_RANGE_MULTIPLIER; // Adjusts the minimum effective range of scopes + FLOAT SCOPE_EFFECTIVENESS_MULTIPLIER; // This modifies the maximum effective magnification that is applied to the shooting aperture. + UINT16 SCOPE_EFFECTIVENESS_MINIMUM; // Defines a minimum effectiveness level that every shooter gets. 0 = fully dynamic, no minimum, 100 = no skills required to use scopes + UINT16 SCOPE_EFFECTIVENESS_MINIMUM_RANGER; // the minimum effectiveness for the ranger. He can reach more using the normal formula. + UINT16 SCOPE_EFFECTIVENESS_MINIMUM_MARKSMAN; // the minimum effectiveness for the marksman. He can reach more using the normal formula. + UINT16 SCOPE_EFFECTIVENESS_MINIMUM_SNIPER; // the minimum effectiveness for the sniper. He can reach more using the normal formula. FLOAT SIDE_FACING_DIVISOR; // Deals with a visual error in NCTH relating to shooting at a target who is facing directly perpendicular to the shooters facing. UINT16 BASIC_RELIABILITY_ODDS; // Determines the base chance to lose one condition point when firing a gun. diff --git a/Tactical/Interface.cpp b/Tactical/Interface.cpp index eb410ab4..1b3f664c 100644 --- a/Tactical/Interface.cpp +++ b/Tactical/Interface.cpp @@ -297,6 +297,8 @@ UINT32 CalcUIMessageDuration( STR16 wString ); // sevenfm: this function is needed to show cover in health bar extern void CalculateCoverForSoldier( SOLDIERTYPE* pForSoldier, const INT32& sTargetGridNo, const BOOLEAN& fRoof, INT8& bCover ); +extern FLOAT Distance2D( FLOAT dDeltaX, FLOAT dDeltaY ); + BOOLEAN InitializeFaceGearGraphics() { VOBJECT_DESC VObjectDesc; @@ -2693,6 +2695,8 @@ BOOLEAN DrawCTHIndicator() SOLDIERTYPE *pSoldier; GetSoldier( &pSoldier, gusSelectedSoldier ); + OBJECTTYPE* pWeapon = pSoldier->GetUsedWeapon( &pSoldier->inv[ pSoldier->ubAttackingHand ] ); + // Create a Background Rect for us to draw our indicator on. With NCTH, the size and position of this rectangle // is equal exactly to the size of the tactical screen viewport. Unlike the OCTH indicator, the NCTH one can grow // so large as to fill the entire screen. @@ -2755,7 +2759,7 @@ BOOLEAN DrawCTHIndicator() FLOAT dDeltaY = dEndY - dStartY; // Calculate the distance of the shot, using Pythagorean Theorem - DOUBLE d2DDistance = sqrt( (DOUBLE) (dDeltaX * dDeltaX + dDeltaY * dDeltaY )); + DOUBLE d2DDistance = Distance2D( dDeltaX, dDeltaY ); // Round it upwards. INT32 iDistance = (INT32) d2DDistance; if ( d2DDistance != iDistance ) @@ -2764,11 +2768,6 @@ BOOLEAN DrawCTHIndicator() d2DDistance = (FLOAT) ( iDistance); } - // Calculate the Distance Ratio. This will increase or decrease the size of the Shooting Aperture based - // on both distance and Magnification Factor. - FLOAT iDistanceRatio = (FLOAT)(d2DDistance / gGameCTHConstants.NORMAL_SHOOTING_DISTANCE); - FLOAT iFinalRatio = iDistanceRatio / gCTHDisplay.FinalMagFactor; - /////////////////////////////////////////////////////////// // Now we calculate the Aperture size. This is done using the same method as the shooting formula uses. @@ -2826,22 +2825,35 @@ BOOLEAN DrawCTHIndicator() } } - // Calculate the Maximum Aperture. This is the margin of error for the "worst" shot we can have given the - // target's actual distance. This will later be used to draw the Outer Circle around the target. - FLOAT iMaxAperture = iBasicAperture * iDistanceRatio; + // Next, find out how large the aperture can be around the target, given range. The further the target is, the + // larger the aperture can be. + FLOAT iDistanceAperture = iBasicAperture * (d2DDistance / gGameCTHConstants.NORMAL_SHOOTING_DISTANCE); - // Calculate the aperture for our shot by applying both distance and scope magnification (which work against each - // other) to the size of a normal aperture. - FLOAT iAperture = iBasicAperture * iFinalRatio; + /////////////////////////////////////////////////////////////// + // To make things easier for us, we now calculate the Magnification Factor for this shot. A Mag Factor + // is a divisor to the sway of the muzzle. It's about the same as multiplying CTH by a certain amount. + // Note that both optical magnification devices (like scopes) and dot-projection devices (like lasers and + // reflex sights) provide this sort of bonus. + FLOAT iMagFactor = CalcMagFactor( pSoldier, pWeapon, d2DDistance, gCTHDisplay.iTargetGridNo, (UINT8)pSoldier->aiData.bAimTime ); - // Apply CTH to the aperture to find out how much smaller it's become thanks to skills and extra aiming. - UINT8 actualPct = __min(gCTHDisplay.MuzzleSwayPercentage,99); - iAperture = ((100 - actualPct) * iAperture) / 100; + // Get effective mag factor for this shooter. This represents his ability to use scopes. + FLOAT fEffectiveMagFactor = CalcEffectiveMagFactor( pSoldier, iMagFactor ); + + // Next step is to apply scope/projection factor to decrease the size of the aperture. This gives us the "Max + // Aperture" value - the size of the shooting circle if the gun is as unstable as possible. + FLOAT iMaxAperture = iDistanceAperture / fEffectiveMagFactor; + + // We now use the Muzzle Sway value, calculated by the CTH formula, to decrease the size of the shot aperture. + // It is used as a percentage: a 50% muzzle sway value gives a cone with half the maximum radius. A cone with + // 0% Muzzle Sway is a single line with no width (meaning all shots will fly right down the center, and all will + // hit the target), while a cone with 100% muzzle sway is as wide as possible. + FLOAT iAperture = ( iMaxAperture * (100.0f - (FLOAT)gCTHDisplay.MuzzleSwayPercentage) / 100.0f); ///////////////////////////////////////////// // Factor in Weapon "Effective Range". UINT16 sEffRange = Weapon[Item[pSoldier->inv[pSoldier->ubAttackingHand].usItem].ubClassIndex].usRange + GetRangeBonus(&(pSoldier->inv[ pSoldier->ubAttackingHand ])); FLOAT iRangeRatio = __max(1.0f, (FLOAT)(d2DDistance / sEffRange)); + FLOAT iDistanceRatio = (FLOAT)(d2DDistance / gGameCTHConstants.NORMAL_SHOOTING_DISTANCE); ///////////////////////////////////////////// // Factor in Gun Accuracy. @@ -2858,7 +2870,7 @@ BOOLEAN DrawCTHIndicator() // Since bullet dev is only affected by distance, we add it last as a flat modifier. iBasicAperture += iBulletDev; - iMaxAperture += iBulletDev; + iDistanceAperture += iBulletDev; iAperture += iBulletDev; // CHRISL: Moved here so we can base the cursor color on the iAperture value @@ -3076,7 +3088,7 @@ BOOLEAN DrawCTHIndicator() SetFont( TINYFONT1 ); // Find coordinates, using full string ("X.X x") - swprintf( pStr, L"%3.2f", iMaxAperture ); + swprintf( pStr, L"%3.2f", iDistanceAperture ); FindFontCenterCoordinates( (INT16)MagRect.left, (INT16)MagRect.top-5, (INT16)MagRect.right-(INT16)MagRect.left, 5, pStr, TINYFONT1, &curX, &curY); // Find width of this string. UINT16 usTotalWidth = StringPixLength ( pStr, TINYFONT1 ); @@ -3486,10 +3498,10 @@ BOOLEAN DrawCTHIndicator() sRight = gsVIEWPORT_END_X; sBottom = gsVIEWPORT_WINDOW_END_Y; - INT16 lastY = 0; - INT16 diffY = 0; - UINT32 uiAperture = __max(2,(UINT32)iAperture); - UINT32 uiMaxAperture = __max(2,(UINT32)iMaxAperture); +// INT16 lastY = 0; +// INT16 diffY = 0; +// UINT32 uiAperture = __max(2,(UINT32)iAperture); +// UINT32 uiMaxAperture = __max(2,(UINT32)iDistanceAperture); UINT32 uiApertureBarLength = 10; INT32 cnt = 0; @@ -3513,29 +3525,29 @@ BOOLEAN DrawCTHIndicator() FLOAT RADIANS_IN_CIRCLE = (FLOAT)(PI * 2); INT32 Circ = 0; - Circ = (INT32)((iMaxAperture * RADIANS_IN_CIRCLE) * dVerticalBias); + Circ = (INT32)((iDistanceAperture * RADIANS_IN_CIRCLE) * dVerticalBias); if(gGameSettings.fOptions[ TOPTION_CTH_CURSOR ]) { // Draw outer circle for (INT32 iCurPoint = 0; iCurPoint < Circ; iCurPoint++) { - curX = (INT16)(iMaxAperture * cos((iCurPoint * RADIANS_IN_CIRCLE)/Circ)); - curY = (INT16)((iMaxAperture * dVerticalBias) * sin((iCurPoint * RADIANS_IN_CIRCLE)/Circ)); + curX = (INT16)(iDistanceAperture * cos((iCurPoint * RADIANS_IN_CIRCLE)/Circ)); + curY = (INT16)((iDistanceAperture * dVerticalBias) * sin((iCurPoint * RADIANS_IN_CIRCLE)/Circ)); INT16 firstX = curX; INT16 firstY = curY; DrawCTHPixelToBuffer( ptrBuf, uiPitch, sLeft, sTop, sRight, sBottom, sStartScreenX+curX, sStartScreenY+curY+(INT16)zOffset, usCApertureBar ); // Draw a border circle which is 1 point wider -// curX = (INT16)((iMaxAperture+1) * cos((iCurPoint * RADIANS_IN_CIRCLE)/Circ)); -// curY = (INT16)(((iMaxAperture * dVerticalBias)+1) * sin((iCurPoint * RADIANS_IN_CIRCLE)/Circ)); +// curX = (INT16)((iDistanceAperture+1) * cos((iCurPoint * RADIANS_IN_CIRCLE)/Circ)); +// curY = (INT16)(((iDistanceAperture * dVerticalBias)+1) * sin((iCurPoint * RADIANS_IN_CIRCLE)/Circ)); // if (curX != firstX || curY != firstY) // DrawCTHPixelToBuffer( ptrBuf, uiPitch, sLeft, sTop, sRight, sBottom, sStartScreenX+curX, sStartScreenY+curY+(INT16)zOffset, usCApertureBorder ); // Draw a border circle which is 1 point narrower -// curX = (INT16)((iMaxAperture-1) * cos((iCurPoint * RADIANS_IN_CIRCLE)/Circ)); -// curY = (INT16)(((iMaxAperture * dVerticalBias)-1) * sin((iCurPoint * RADIANS_IN_CIRCLE)/Circ)); +// curX = (INT16)((iDistanceAperture-1) * cos((iCurPoint * RADIANS_IN_CIRCLE)/Circ)); +// curY = (INT16)(((iDistanceAperture * dVerticalBias)-1) * sin((iCurPoint * RADIANS_IN_CIRCLE)/Circ)); // if (curX != firstX || curY != firstY) // DrawCTHPixelToBuffer( ptrBuf, uiPitch, sLeft, sTop, sRight, sBottom, sStartScreenX+curX, sStartScreenY+curY+(INT16)zOffset, usCApertureBorder ); diff --git a/Tactical/LOS.cpp b/Tactical/LOS.cpp index 9d59a242..bb79349f 100644 --- a/Tactical/LOS.cpp +++ b/Tactical/LOS.cpp @@ -7601,18 +7601,20 @@ void AdjustTargetCenterPoint( SOLDIERTYPE *pShooter, INT32 iTargetGridNo, FLOAT // is a divisor to the sway of the muzzle. It's about the same as multiplying CTH by a certain amount. // Note that both optical magnification devices (like scopes) and dot-projection devices (like lasers and // reflex sights) provide this sort of bonus. - FLOAT iMagFactor = CalcMagFactor( pShooter, pWeapon, d2DDistance, iTargetGridNo, (UINT8)pShooter->aiData.bAimTime ); + // Get effective mag factor for this shooter. This represents his ability to use scopes. + FLOAT fEffectiveMagFactor = CalcEffectiveMagFactor( pShooter, iMagFactor ); + // Next step is to apply scope/projection factor to decrease the size of the aperture. This gives us the "Max // Aperture" value - the size of the shooting circle if the gun is as unstable as possible. - iMaxAperture = iDistanceAperture / iMagFactor; + iMaxAperture = iDistanceAperture / fEffectiveMagFactor; // We now use the Muzzle Sway value, calculated by the CTH formula, to decrease the size of the shot aperture. // It is used as a percentage: a 50% muzzle sway value gives a cone with half the maximum radius. A cone with // 0% Muzzle Sway is a single line with no width (meaning all shots will fly right down the center, and all will // hit the target), while a cone with 100% muzzle sway is as wide as possible. - iAperture = (FLOAT)((iMaxAperture * uiMuzzleSway) / 100); + iAperture = ((iMaxAperture * (FLOAT)uiMuzzleSway) / 100.0f); // Aperture ratio below is used for Experience Gain. The smaller the aperture, compared to the size of the // Distance Aperture, the more experience we get. @@ -7944,6 +7946,47 @@ FLOAT CalcMagFactor( SOLDIERTYPE *pShooter, OBJECTTYPE *pWeapon, FLOAT d2DDistan return iFinalMagFactor; } +// silversurfer: Effectivity in using scopes is determined by this function. Good shooters can use the full potential of scopes. +// Iron sights and reflex sights always provide full potential. +FLOAT CalcEffectiveMagFactor( SOLDIERTYPE *pShooter, FLOAT fRealMagFactor ) +{ + // can't be a scope... + if ( fRealMagFactor <= 1.0 ) + return fRealMagFactor; + + FLOAT fMaxEffectiveMagFactor = fRealMagFactor * gGameCTHConstants.SCOPE_EFFECTIVENESS_MULTIPLIER; + FLOAT fFinalEffectiveFactor = 0.1; + + // new trait system grants special thresholds + if ( gGameOptions.fNewTraitSystem ) + { + // snipers have most training + if ( NUM_SKILL_TRAITS( pShooter, SNIPER_NT ) == 2 ) + fFinalEffectiveFactor = fMaxEffectiveMagFactor * (FLOAT)gGameCTHConstants.SCOPE_EFFECTIVENESS_MINIMUM_SNIPER / 100.0f; + + // marksmen also have sniper training but not as much + else if ( NUM_SKILL_TRAITS( pShooter, SNIPER_NT ) == 1 ) + fFinalEffectiveFactor = fMaxEffectiveMagFactor * (FLOAT)gGameCTHConstants.SCOPE_EFFECTIVENESS_MINIMUM_MARKSMAN / 100.0f; + + // rangers have some scope training too + else if ( NUM_SKILL_TRAITS( pShooter, RANGER_NT ) == 2 ) + fFinalEffectiveFactor = fMaxEffectiveMagFactor * (FLOAT)gGameCTHConstants.SCOPE_EFFECTIVENESS_MINIMUM_RANGER / 100.0f; + } + + // this defines the minimum effective magnification any shooter gets + FLOAT fFixedPart = fMaxEffectiveMagFactor * (FLOAT)gGameCTHConstants.SCOPE_EFFECTIVENESS_MINIMUM / 100.0f; + + // this is the variable part that is based on the shooters skills (experience and marksmanship) + FLOAT fVariablePart = ( fMaxEffectiveMagFactor * (FLOAT)(100 - gGameCTHConstants.SCOPE_EFFECTIVENESS_MINIMUM) / 100.0f ) * + ( ( (FLOAT)EffectiveExpLevel( pShooter ) * 10.0f * gGameCTHConstants.AIM_EXP + (FLOAT)EffectiveMarksmanship( pShooter ) * gGameCTHConstants.AIM_MARKS ) / + ( gGameCTHConstants.AIM_EXP + gGameCTHConstants.AIM_MARKS ) / 100.0f ); + + // now add them together and enforce limits + fFinalEffectiveFactor = __min( fMaxEffectiveMagFactor, __max( fFixedPart + fVariablePart, fFinalEffectiveFactor ) ); + + return fFinalEffectiveFactor; +} + // HEADROCK HAM 4: This function calculates and returns the maximum shot aperture as a radius to which the muzzle of // the gun might sway off the target center when the trigger is pulled. FLOAT CalcBasicAperture() diff --git a/Tactical/LOS.h b/Tactical/LOS.h index c0eccefb..2c4c0d1e 100644 --- a/Tactical/LOS.h +++ b/Tactical/LOS.h @@ -226,6 +226,7 @@ extern INT32 GetSpreadPattern( OBJECTTYPE * pObj ); // which completely redesigns the way we calculate and handle CTH for the purposes of firing weapons. void AdjustTargetCenterPoint( SOLDIERTYPE *pShooter, INT32 iTargetGridNo, FLOAT *dEndX, FLOAT *dEndY, FLOAT *dEndZ, OBJECTTYPE *pWeapon, UINT32 uiMuzzleSway, INT16 *sApertureRatio ); FLOAT CalcMagFactor( SOLDIERTYPE *pShooter, OBJECTTYPE *pWeapon, FLOAT d2DDistance, INT32 iTargetGridNo, UINT8 ubAimTime ); +FLOAT CalcEffectiveMagFactor( SOLDIERTYPE *pShooter, FLOAT fRealMagFactor ); FLOAT CalcProjectionFactor( SOLDIERTYPE *pShooter, OBJECTTYPE *pWeapon, FLOAT d2DDistance, UINT8 ubAimTime ); FLOAT CalcBasicAperture(); void CalcTargetMovementOffset( SOLDIERTYPE *pShooter, SOLDIERTYPE *pTarget, OBJECTTYPE *pWeapon, FLOAT *dMuzzleOffsetX, DOUBLE ddShootingAngle, INT32 iAperture ); diff --git a/Tactical/Weapons.cpp b/Tactical/Weapons.cpp index 4006903d..5fb3822d 100644 --- a/Tactical/Weapons.cpp +++ b/Tactical/Weapons.cpp @@ -9032,14 +9032,18 @@ UINT32 AICalcChanceToHitGun(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT16 ubAimTim // distance to target FLOAT d2DDistance = (FLOAT) PythSpacesAway( pSoldier->sGridNo, sGridNo ) * (FLOAT) CELL_X_SIZE; - // magnification (1.0 or higher if scope is used) - FLOAT dMagFactor = CalcMagFactor( pSoldier, &(pSoldier->inv[pSoldier->ubAttackingHand]), d2DDistance, sGridNo, (UINT8)ubAimTime ); // basic aperture that is equal for everyone FLOAT dBasicAperture = CalcBasicAperture( ); // aperture at target distance without magnification FLOAT dAperture = dBasicAperture * (d2DDistance / gGameCTHConstants.NORMAL_SHOOTING_DISTANCE); + + // magnification (1.0 or higher if scope is used) + FLOAT dMagFactor = CalcMagFactor( pSoldier, &(pSoldier->inv[pSoldier->ubAttackingHand]), d2DDistance, sGridNo, (UINT8)ubAimTime ); + // Get effective mag factor for this shooter. This represents his ability to use scopes. + FLOAT fEffectiveMagFactor = CalcEffectiveMagFactor( pSoldier, dMagFactor ); // modify aperture with magnification - dAperture = dAperture / dMagFactor; + dAperture = dAperture / fEffectiveMagFactor; + // real aperture for shooter based on CTH calculation dAperture = dAperture * ( 100 - uiChance ) / 100.0f;