class Override_X2AbilityToHitCalc_StandardAim extends X2AbilityToHitCalc_StandardAim config (RWRealisticAimingAnglesHL);
//WATCHOUT!!! CHECK FOR FUNCTION PASSESWEAPONCHECK IN X2EVENTLISTENER_ABILITYPOINTS CLASS WHICH "SPECIFICALLY"
//CHECKS FOR SPECIAL HITS FROM THE "UNOVERRIDDEN" X2ABILITYTOHITCALC_STANDARDAIM!!!
//WHICH MEANS THAT THIS OVERRIDE COULD PREVENT WINNING SPECIAL ABILITYPOINTS...!!!
//NO... IT SEEMS THAT ABILITYPOINTS ARE WON EVEN WITH THIS MOD... ;)
//ADDED THIS PARAGRAPH!!!
var config float MinimumGoodAngleBonusCoverReductionPercentage;
var config float MaximumGoodAngleBonusCoverReductionPercentage;
var config float MinimumGoodAngleBonusStartsAtThisHorizontalAngleBetweenLineOfFireAndCoverSurface;
var config float MaximumGoodAngleBonusEndsAtThisHorizontalAngleBetweenLineOfFireAndCoverSurface;
var config float MinimumHeightAdvantageBonusCoverReductionPercentage;
var config float MaximumHeightAdvantageBonusCoverReductionPercentage;
var config float MinimumHeightAdvantageBonusStartsAtThisVerticalAngleBetweenLineOfFireAndCoverSurface;
var config float MaximumHeightAdvantageBonusEndsAtThisVerticalAngleBetweenLineOfFireAndCoverSurface;
var config bool ApplyGoodAngleBonusNotOnlyForXComSoldiersButAlsoForAnyoneShooting;
var config bool ApplyGoodAngleBonusEvenFartherThan11TilesAway;
var config float TargetingObjectsRealisticallyAllowsToMissThemAndUsesConventionalHitChanceStatsPlusThisAimingBonus;
var config bool ApplyVerticalGoodAngleAgainstLowerTargetAsProportionalHeightAdvantage;
var config bool ApplyVerticalBadAngleAgainstHigherTargetAsProportionalHeightDisadvantage;
var config bool AllowFarCoverToGiveSomeCoverProtection;
var config bool GiveHitChanceZeroWhenTargetableEnemiesAreInsteadNotTrulyInLineOfSight;
var config bool OnlyAlertedTargetsCanHaveTheExtraProtectionOfFarCoverAndHeightDisadvantage;
var config float NO_LINE_OF_SIGHT_PENALTY;
var float FarCoverBonus;
var bool ShowChances;
var bool ShowCriticalAndDodgeChances;
var bool ShowDiceRolls;
var bool ShowOnlyForAllies;
var bool UseIndividualConsecutiveRollsForHitChanceCriticalChanceDodgeChance;
var bool PrioritizeCriticalChanceOverDodgeChance;
var bool AlwaysRollForBothDodgeAndCriticalEvenWhenOneUnsuccesful;
var bool GuaranteedHitsCannotDodge;
var bool IndirectFireGuaranteesHit;
var bool EnableGamblerFallacySystemRoll;
//END OF ADDED PARAGRAPH!!!
var config bool OVERWATCH_BYPASS_COVER;
var config array<name> OVERWATCH_ABILITIES;
var config array<name> MOVEMENT_ABILITY;
function InternalRollForAbilityHit(XComGameState_Ability kAbility, AvailableTarget kTarget, bool bIsPrimaryTarget, const out AbilityResultContext ResultContext, out EAbilityHitResult Result, out ArmorMitigationResults ArmorMitigated, out int HitChance)
{
local int i, RandRoll, Current, ModifiedHitChance;
local EAbilityHitResult DebugResult, ChangeResult;
local ArmorMitigationResults Armor;
local XComGameState_Unit TargetState, UnitState;
local XComGameState_Player PlayerState;
local XComGameStateHistory History;
local StateObjectReference EffectRef;
local XComGameState_Effect EffectState;
local bool bRolledResultIsAMiss, bModHitRoll;
local bool HitsAreCrits;
local string LogMsg;
local ETeam CurrentPlayerTeam;
local ShotBreakdown m_ShotBreakdown;
//ADDED THIS PARAGRAPH!!!
local int OriginalHitRoll;
local float DodgeChance;
local String AbilityName;
local int RealHitChance;
local int TotalHitChance;
local string DebugRollOutput;
//END OF ADDED PARAGRAPH!!!
History = `XCOMHISTORY;
`log("===" $ GetFuncName() $ "===", true, 'XCom_HitRolls');
`log("Attacker ID:" @ kAbility.OwnerStateObject.ObjectID, true, 'XCom_HitRolls');
`log("Target ID:" @ kTarget.PrimaryTarget.ObjectID, true, 'XCom_HitRolls');
`log("Ability:" @ kAbility.GetMyTemplate().LocFriendlyName @ "(" $ kAbility.GetMyTemplateName() $ ")", true, 'XCom_HitRolls');
ArmorMitigated = Armor; // clear out fields just in case
HitsAreCrits = bHitsAreCrits;
if (`CHEATMGR != none)
{
if (`CHEATMGR.bForceCritHits)
HitsAreCrits = true;
if (`CHEATMGR.bNoLuck)
{
`log("NoLuck cheat forcing a miss.", true, 'XCom_HitRolls');
Result = eHit_Miss;
return;
}
if (`CHEATMGR.bDeadEye)
{
UnitState = XComGameState_Unit(History.GetGameStateForObjectID(kAbility.OwnerStateObject.ObjectID));
if( !`CHEATMGR.bXComOnlyDeadEye || !UnitState.ControllingPlayerIsAI() || default.UseIndividualConsecutiveRollsForHitChanceCriticalChanceDodgeChance == true)
{
`log("DeadEye cheat forcing a hit.", true, 'XCom_HitRolls');
Result = eHit_Success;
if( HitsAreCrits )
Result = eHit_Crit;
return;
}
}
}
HitChance = GetHitChance(kAbility, kTarget, m_ShotBreakdown, true);
if (default.UseIndividualConsecutiveRollsForHitChanceCriticalChanceDodgeChance == false) RandRoll = `SYNC_RAND_TYPED(100, ESyncRandType_Generic);
Result = eHit_Miss;
`log("=" $ GetFuncName() $ "=", true, 'XCom_HitRolls');
`log("Final hit chance:" @ HitChance, true, 'XCom_HitRolls');
//ADDED THIS PARAGRAPH!!!
//HEART OF EUROLLS INJECTION FROM XCOM1
if (default.UseIndividualConsecutiveRollsForHitChanceCriticalChanceDodgeChance)
{
UnitState = XComGameState_Unit(History.GetGameStateForObjectID(kAbility.OwnerStateObject.ObjectID));
TargetState = XComGameState_Unit(History.GetGameStateForObjectID(kTarget.PrimaryTarget.ObjectID));
// Revert real hit chance manipulated by FinalizeHitChance()
RealHitChance = Clamp(HitChance, 0, 100);
// Roll for hit
if (default.EnableGamblerFallacySystemRoll)
{
// RandRoll = class'XComGameState_GamblerRolls'.static.StaticRoll(RealHitChance, false, false, IsAlien);
}
else
{
RandRoll = class'Engine'.static.GetEngine().SyncRand(100, string(Name)@string(GetStateName())@string(GetFuncName()));
}
OriginalHitRoll = RandRoll;
DebugResult = EAbilityHitResult(eHit_Success);
`log("Checking table" @ DebugResult @ "(" $ RealHitChance $ ")...", true, 'XCom_HitRolls');
`log("Random roll:" @ RandRoll, true, 'XCom_HitRolls');
DebugRollOutput = "Hit roll:"@RandRoll;
}
if (default.UseIndividualConsecutiveRollsForHitChanceCriticalChanceDodgeChance)
{
TotalHitChance = RealHitChance;
// EU aim rolls
if (RandRoll < RealHitChance)
{
Result = eHit_Success;
`log("MATCH!", true, 'XCom_HitRolls');
// Hits, now roll for crits (Super priority)
if (default.PrioritizeCriticalChanceOverDodgeChance)
{
if (default.EnableGamblerFallacySystemRoll)
{
// RandRoll = class'XComGameState_GamblerRolls'.static.StaticRoll(m_ShotBreakdown.ResultTable[eHit_Crit], true, false, IsAlien);
}
else
{
RandRoll = class'Engine'.static.GetEngine().SyncRand(100, string(Name)@string(GetStateName())@string(GetFuncName()));
}
DebugResult = EAbilityHitResult(eHit_Crit);
if (default.ShowCriticalAndDodgeChances)
{
DebugRollOutput = DebugRollOutput@", Crit roll:"@RandRoll;
}
`log("Checking table" @ DebugResult @ "(" $ m_ShotBreakdown.ResultTable[eHit_Crit] $ ")...", true, 'XCom_HitRolls');
`log("Random roll:" @ RandRoll, true, 'XCom_HitRolls');
if (RandRoll < m_ShotBreakdown.ResultTable[eHit_Crit])
{
Result = eHit_Crit;
`log("MATCH!", true, 'XCom_HitRolls');
if (default.AlwaysRollForBothDodgeAndCriticalEvenWhenOneUnsuccesful && m_ShotBreakdown.ResultTable[eHit_Graze] > 0)
{
if (default.EnableGamblerFallacySystemRoll)
{
// RandRoll = class'XComGameState_GamblerRolls'.static.StaticRoll(m_ShotBreakdown.ResultTable[eHit_Graze] * 100 / RealHitChance, false, true, IsAlien) * 100 / RealHitChance;
}
else
{
RandRoll = class'Engine'.static.GetEngine().SyncRand(RealHitChance, string(Name)@string(GetStateName())@string(GetFuncName()));
}
DebugResult = EAbilityHitResult(eHit_Graze);
if (default.ShowCriticalAndDodgeChances && m_ShotBreakdown.ResultTable[eHit_Graze] > 0)
{
DebugRollOutput = DebugRollOutput@", Dodge roll:"@RandRoll;
}
`log("Checking table" @ DebugResult @ "(" $ m_ShotBreakdown.ResultTable[eHit_Graze] $ ")...", true, 'XCom_HitRolls');
`log("Random roll over hit chance:" @ RandRoll, true, 'XCom_HitRolls');
// Roll dodge over hit chance (Because of a function earlier multiplied dodge chance by actual hit chance)
if (RandRoll < m_ShotBreakdown.ResultTable[eHit_Graze])
{
Result = eHit_Success; // Dodge cancels crit and becomes a standard hit.
`log("MATCH!", true, 'XCom_HitRolls');
}
}
}
else if (m_ShotBreakdown.ResultTable[eHit_Graze] > 0)
{
if (default.EnableGamblerFallacySystemRoll)
{
// RandRoll = class'XComGameState_GamblerRolls'.static.StaticRoll(m_ShotBreakdown.ResultTable[eHit_Graze] * 100 / RealHitChance, false, true, IsAlien) * 100 / RealHitChance;
}
else
{
RandRoll = class'Engine'.static.GetEngine().SyncRand(RealHitChance, string(Name)@string(GetStateName())@string(GetFuncName()));
}
DebugResult = EAbilityHitResult(eHit_Graze);
if (default.ShowCriticalAndDodgeChances && m_ShotBreakdown.ResultTable[eHit_Graze] > 0)
{
DebugRollOutput = DebugRollOutput@", Dodge roll:"@RandRoll;
}
`log("Checking table" @ DebugResult @ "(" $ m_ShotBreakdown.ResultTable[eHit_Graze] $ ")...", true, 'XCom_HitRolls');
`log("Random roll over hit chance:" @ RandRoll, true, 'XCom_HitRolls');
// Roll dodge over hit chance (Because of a function earlier multiplied dodge chance by actual hit chance)
if (RandRoll < m_ShotBreakdown.ResultTable[eHit_Graze])
{
Result = eHit_Graze;
`log("MATCH!", true, 'XCom_HitRolls');
}
}
}
else
{
if (default.EnableGamblerFallacySystemRoll && m_ShotBreakdown.ResultTable[eHit_Graze] > 0)
{
// RandRoll = class'XComGameState_GamblerRolls'.static.StaticRoll(m_ShotBreakdown.ResultTable[eHit_Graze] * 100 / RealHitChance, false, true, IsAlien) * 100 / RealHitChance;
}
else
{
RandRoll = class'Engine'.static.GetEngine().SyncRand(RealHitChance, string(Name)@string(GetStateName())@string(GetFuncName()));
}
DebugResult = EAbilityHitResult(eHit_Graze);
if (default.ShowCriticalAndDodgeChances && m_ShotBreakdown.ResultTable[eHit_Graze] > 0)
{
DebugRollOutput = DebugRollOutput@", Dodge roll:"@RandRoll;
}
`log("Checking table" @ DebugResult @ "(" $ m_ShotBreakdown.ResultTable[eHit_Graze] $ ")...", true, 'XCom_HitRolls');
`log("Random roll over hit chance:" @ RandRoll, true, 'XCom_HitRolls');
// Roll dodge over hit chance (Because of a function earlier multiplied dodge chance by actual hit chance)
if (RandRoll < m_ShotBreakdown.ResultTable[eHit_Graze])
{
Result = eHit_Graze;
`log("MATCH!", true, 'XCom_HitRolls');
if (default.AlwaysRollForBothDodgeAndCriticalEvenWhenOneUnsuccesful)
{
if (default.EnableGamblerFallacySystemRoll)
{
// RandRoll = class'XComGameState_GamblerRolls'.static.StaticRoll(m_ShotBreakdown.ResultTable[eHit_Crit], true, false, IsAlien);
}
else
{
RandRoll = class'Engine'.static.GetEngine().SyncRand(100, string(Name)@string(GetStateName())@string(GetFuncName()));
}
DebugResult = EAbilityHitResult(eHit_Crit);
if (default.ShowCriticalAndDodgeChances)
{
DebugRollOutput = DebugRollOutput@", Crit roll:"@RandRoll;
}
`log("Checking table" @ DebugResult @ "(" $ m_ShotBreakdown.ResultTable[eHit_Crit] $ ")...", true, 'XCom_HitRolls');
`log("Random roll:" @ RandRoll, true, 'XCom_HitRolls');
if (RandRoll < m_ShotBreakdown.ResultTable[eHit_Crit])
{
Result = eHit_Success; // Crit cancels dodge and becomes a standard hit.
`log("MATCH!", true, 'XCom_HitRolls');
}
}
}
else
{
if (default.EnableGamblerFallacySystemRoll)
{
// RandRoll = class'XComGameState_GamblerRolls'.static.StaticRoll(m_ShotBreakdown.ResultTable[eHit_Crit], true, false, IsAlien);
}
else
{
RandRoll = class'Engine'.static.GetEngine().SyncRand(100, string(Name)@string(GetStateName())@string(GetFuncName()));
}
DebugResult = EAbilityHitResult(eHit_Crit);
if (default.ShowCriticalAndDodgeChances)
{
DebugRollOutput = DebugRollOutput@", Crit roll:"@RandRoll;
}
`log("Checking table" @ DebugResult @ "(" $ m_ShotBreakdown.ResultTable[eHit_Crit] $ ")...", true, 'XCom_HitRolls');
`log("Random roll:" @ RandRoll, true, 'XCom_HitRolls');
if (RandRoll < m_ShotBreakdown.ResultTable[eHit_Crit])
{
Result = eHit_Crit;
`log("MATCH!", true, 'XCom_HitRolls');
}
}
}
}
}
if (default.UseIndividualConsecutiveRollsForHitChanceCriticalChanceDodgeChance == false)
{
TotalHitChance = m_ShotBreakdown.ResultTable[eHit_Success] + m_ShotBreakdown.ResultTable[eHit_Crit] + m_ShotBreakdown.ResultTable[eHit_Graze];
TotalHitChance = Clamp(TotalHitChance, 0, 100);
// Vanilla aim rolls
Result = eHit_Miss;
//THIS ABOVE WAS THE HEART OF EUROLLS INJECTION...
//END OF ADDED PARAGRAPH!!
`log("Random roll:" @ RandRoll, true, 'XCom_HitRolls');
// GetHitChance fills out m_ShotBreakdown and its ResultTable
for (i = 0; i < eHit_Miss; ++i) // If we don't match a result before miss, then it's a miss.
{
Current += m_ShotBreakdown.ResultTable[i];
DebugResult = EAbilityHitResult(i);
`log("Checking table" @ DebugResult @ "(" $ Current $ ")...", true, 'XCom_HitRolls');
if (RandRoll < Current)
{
Result = EAbilityHitResult(i);
`log("MATCH!", true, 'XCom_HitRolls');
break;
}
}
}
// Start Issue #1300
/// HL-Docs: ref:Bugfixes; issue:1300
/// Code block moved to be right after aim assist logic, so that if a miss is converted to a hit by aim assist, the ability will still crit if it is set up to always crit on hit.
//if (HitsAreCrits && Result == eHit_Success)
// Result = eHit_Crit;
// End Issue #1300
if (default.UseIndividualConsecutiveRollsForHitChanceCriticalChanceDodgeChance == false) UnitState = XComGameState_Unit(History.GetGameStateForObjectID(kAbility.OwnerStateObject.ObjectID));
if (default.UseIndividualConsecutiveRollsForHitChanceCriticalChanceDodgeChance == false) TargetState = XComGameState_Unit(History.GetGameStateForObjectID(kTarget.PrimaryTarget.ObjectID));
//ADDED THIS PARAGRAPH!!!
if (default.GuaranteedHitsCannotDodge && (bIndirectFire || bGuaranteedHit || TargetState == none)) // No target states are supposed to be guarantee hits
{
if (Result == eHit_Graze)
Result = eHit_Success;
}
if (default.IndirectFireGuaranteesHit && bIndirectFire)
{
if (Result == eHit_Miss) // Countering the unintended effect of graze band for LW2
Result = eHit_Success;
}
if (default.ShowChances && !bIndirectFire && !bGuaranteedHit && TargetState != none)
{
AbilityName = kAbility.GetMyFriendlyName();
if (AbilityName == "")
{
AbilityName = string(kAbility.GetMyTemplateName());
}
if (default.UseIndividualConsecutiveRollsForHitChanceCriticalChanceDodgeChance)
{
DodgeChance = (m_ShotBreakdown.ResultTable[eHit_Graze] * 100.00) / RealHitChance;
DodgeChance = Clamp(DodgeChance, 0, 100);
}
}
//END OF ADDED PARAGRAPH!!!
// Issue #426: ChangeHitResultForX() code block moved to later in method.
/// HL-Docs: ref:Bugfixes; issue:426
/// Fix `X2AbilityToHitCalc_StandardAim` discarding unfavorable (for XCOM) changes to hit results from effects
// Due to how GetModifiedHitChanceForCurrentDifficulty() is implemented, it reverts attempts to change
// XCom Hits to Misses, or enemy misses to hits.
// The LW2 graze band issues are related to this phenomenon, since the graze band has the effect
// of changing some what "should" be enemy misses to hits (specifically graze result)
// Aim Assist (miss streak prevention)
bRolledResultIsAMiss = class'XComGameStateContext_Ability'.static.IsHitResultMiss(Result);
// reaction fire shots and guaranteed hits do not get adjusted for difficulty
if( UnitState != None &&
!bReactionFire &&
!bGuaranteedHit &&
m_ShotBreakdown.SpecialGuaranteedHit == '')
{
PlayerState = XComGameState_Player(History.GetGameStateForObjectID(UnitState.GetAssociatedPlayerID()));
CurrentPlayerTeam = PlayerState.GetTeam();
`log("CurrentPlayerTeam:"@CurrentPlayerTeam, true, 'XCom_HitRolls');
if( bRolledResultIsAMiss && CurrentPlayerTeam == eTeam_XCom )
{
ModifiedHitChance = GetModifiedHitChanceForCurrentDifficulty(PlayerState, TargetState, HitChance);
//ADDED THIS PARAGRAPH!!!
if (default.UseIndividualConsecutiveRollsForHitChanceCriticalChanceDodgeChance == true)
{
if( ModifiedHitChance != HitChance && OriginalHitRoll < ModifiedHitChance )
{
Result = eHit_Success;
bModHitRoll = true;
`log("*** AIM ASSIST forcing an XCom MISS to become a HIT!", true, 'XCom_HitRolls');
}
}
if (default.UseIndividualConsecutiveRollsForHitChanceCriticalChanceDodgeChance == false)
{
//END OF ADDED PARAGRAPH!!!
if( RandRoll < ModifiedHitChance )
{
Result = eHit_Success;
bModHitRoll = true;
`log("*** AIM ASSIST forcing an XCom MISS to become a HIT!", true, 'XCom_HitRolls');
}
} //ADDED THIS LINE!!!
}
else if( !bRolledResultIsAMiss && (CurrentPlayerTeam == eTeam_Alien || CurrentPlayerTeam == eTeam_TheLost) )
{
ModifiedHitChance = GetModifiedHitChanceForCurrentDifficulty(PlayerState, TargetState, HitChance);
//ADDED THIS PARAGRAPH!!!
if (default.UseIndividualConsecutiveRollsForHitChanceCriticalChanceDodgeChance == true)
{
if( ModifiedHitChance != HitChance && OriginalHitRoll >= ModifiedHitChance )
{
Result = eHit_Miss;
bModHitRoll = true;
`log("*** AIM ASSIST forcing an Alien HIT to become a MISS!", true, 'XCom_HitRolls');
}
}
if (default.UseIndividualConsecutiveRollsForHitChanceCriticalChanceDodgeChance == false)
{
//END OF ADDED PARAGRAPH!!!
if( RandRoll >= ModifiedHitChance )
{
Result = eHit_Miss;
bModHitRoll = true;
`log("*** AIM ASSIST forcing an Alien HIT to become a MISS!", true, 'XCom_HitRolls');
}
} //ADDED THIS LINE!!!
}
}
// Start Issue #1300
// Code block moved from earlier.
if (HitsAreCrits && Result == eHit_Success)
Result = eHit_Crit;
// End Issue #1300
`log("***HIT" @ Result, !bRolledResultIsAMiss, 'XCom_HitRolls');
`log("***MISS" @ Result, bRolledResultIsAMiss, 'XCom_HitRolls');
// Start Issue #426: Block moved from earlier. Only code change is for lightning reflexes,
// because bRolledResultIsAMiss was used for both aim assist and reflexes
if (UnitState != none && TargetState != none)
{
foreach UnitState.AffectedByEffects(EffectRef)
{
EffectState = XComGameState_Effect(History.GetGameStateForObjectID(EffectRef.ObjectID));
if (EffectState != none)
{
if (EffectState.GetX2Effect().ChangeHitResultForAttacker(UnitState, TargetState, kAbility, Result, ChangeResult))
{
`log("Effect" @ EffectState.GetX2Effect().FriendlyName @ "changing hit result for attacker:" @ ChangeResult,true,'XCom_HitRolls');
Result = ChangeResult;
}
}
}
foreach TargetState.AffectedByEffects(EffectRef)
{
EffectState = XComGameState_Effect(History.GetGameStateForObjectID(EffectRef.ObjectID));
if (EffectState != none)
{
if (EffectState.GetX2Effect().ChangeHitResultForTarget(EffectState, UnitState, TargetState, kAbility, bIsPrimaryTarget, Result, ChangeResult))
{
`log("Effect" @ EffectState.GetX2Effect().FriendlyName @ "changing hit result for target:" @ ChangeResult, true, 'XCom_HitRolls');
Result = ChangeResult;
}
}
}
}
if (TargetState != none)
{
// Check for Lightning Reflexes
if (bReactionFire && TargetState.bLightningReflexes && !class'XComGameStateContext_Ability'.static.IsHitResultMiss(Result))
{
Result = eHit_LightningReflexes;
`log("Lightning Reflexes triggered! Shot will miss.", true, 'XCom_HitRolls');
}
//ADDED THIS LINE!!!
if (default.UseIndividualConsecutiveRollsForHitChanceCriticalChanceDodgeChance) class'X2AbilityArmorHitRolls'.static.RollArmorMitigation(m_ShotBreakdown.ArmorMitigation, ArmorMitigated, TargetState); // add armor mitigation (regardless of hit/miss as some shots deal damage on a miss)
}
// End Issue #426
if (UnitState != none && TargetState != none)
{
LogMsg = class'XLocalizedData'.default.StandardAimLogMsg;
LogMsg = repl(LogMsg, "#Shooter", UnitState.GetName(eNameType_RankFull));
LogMsg = repl(LogMsg, "#Target", TargetState.GetName(eNameType_RankFull));
LogMsg = repl(LogMsg, "#Ability", kAbility.GetMyTemplate().LocFriendlyName);
LogMsg = repl(LogMsg, "#Chance", bModHitRoll ? ModifiedHitChance : HitChance);
LogMsg = repl(LogMsg, "#Roll", RandRoll);
LogMsg = repl(LogMsg, "#Result", class'X2TacticalGameRulesetDataStructures'.default.m_aAbilityHitResultStrings[Result]);
`COMBATLOG(LogMsg);
}
}
protected function int GetHitChance(XComGameState_Ability kAbility, AvailableTarget kTarget, optional out ShotBreakdown m_ShotBreakdown, optional bool bDebugLog = false)
{
local XComGameState_Unit UnitState, TargetState;
local XComGameState_Item SourceWeapon;
local GameRulesCache_VisibilityInfo VisInfo;
local array<X2WeaponUpgradeTemplate> WeaponUpgrades;
local int i, iWeaponMod, iRangeModifier, Tiles;
local ShotBreakdown EmptyShotBreakdown;
local array<ShotModifierInfo> EffectModifiers;
local StateObjectReference EffectRef;
local XComGameState_Effect EffectState;
local XComGameStateHistory History;
local bool bFlanking, bIgnoreGraze, bSquadsight;
local string IgnoreGrazeReason;
local X2AbilityTemplate AbilityTemplate;
local array<XComGameState_Effect> StatMods;
local array<float> StatModValues;
local X2Effect_Persistent PersistentEffect;
local array<X2Effect_Persistent> UniqueToHitEffects;
local float FinalAdjust, CoverValue, AngleToCoverModifier, Alpha;
local int TileDistance;
//ADDED THIS PARAGRAPH!!!
local TTile ShooterTile;
local TTile TargetTile;
local TTile ShooterHead;
local TTile TargetHead;
local float ShooterHeadRelativeElevation;
local float VerticalAngle;
local float GoodAngleBonus;
local float HeightAdvantageBonus; //ALWAYS FLOATS INSTEAD OF INTS!!!!
local ECoverType CoverTypeOfTileAboveTarget;
local TTile ShooterPeekTile, LosTile, TargetPeekTile;
local VoxelRaytraceCheckResult RayTrace;
local int ExistingCoverHeight, TargetHeight, FarCover;
//END OF ADDED PARAGRAPH!!!
local bool NoLineOfSight;
local vector ShooterPeekVec, LosVec;
local int floor;
// ========================================
// From -bg-'s EU Aim Rolls mod
// ========================================
local XComGameState LastGameState;
local bool isRunningOverwatch;
local XComGameStateContext_Ability LastAbilityContext;
/// HL-Docs: feature:GetHitChanceEvents; issue:1031; tags:tactical
/// WARNING! Triggering events in `X2AbilityToHitCalc::GetHitChance()` and other functions called by this function
/// may freeze (hard hang) the game under certain circumstances.
///
/// In our experiments, the game would hang when the player used a moving melee ability when an event was triggered
/// in `UITacticalHUD_AbilityContainer::ConfirmAbility()` right above the
/// `XComPresentationLayer(Owner.Owner).PopTargetingStates();` line or anywhere further down the script trace,
/// while another event was also triggered in `GetHitChance()` or anywhere further down the script trace.
///
/// The game hangs while executing UI code, but it is the event in the To Hit Calculation logic that induces it.
/// The speculation is that triggering events in `GetHitChance()` somehow corrupts the event manager, or it
/// could be a threading issue.
`log("=" $ GetFuncName() $ "=", bDebugLog, 'XCom_HitRolls');
// @TODO gameplay handle non-unit targets
History = `XCOMHISTORY;
UnitState = XComGameState_Unit(History.GetGameStateForObjectID( kAbility.OwnerStateObject.ObjectID ));
TargetState = XComGameState_Unit(History.GetGameStateForObjectID( kTarget.PrimaryTarget.ObjectID ));
//ADDED THIS PARAGRAPH!!!
UnitState .GetKeystoneVisibilityLocation(ShooterTile);
TargetState.GetKeystoneVisibilityLocation(TargetTile );
`TACTICALRULES.VisibilityMgr.GetVisibilityInfo(UnitState.ObjectID, TargetState.ObjectID, VisInfo);
CoverTypeOfTileAboveTarget = CoverTypeOfTileAboveCover(ShooterTile, TargetTile, VisInfo.TargetCoverAngle);
GoodAngleBonus = 0; //JUST IN CASE... WIERD BEHAVIOR
//END OF ADDED PARAGRAPH!!!
if (kAbility != none)
{
AbilityTemplate = kAbility.GetMyTemplate();
SourceWeapon = kAbility.GetSourceWeapon();
// ========================================
// From -bg-'s EU Aim Rolls mod
// ========================================
// Check if it's overwatch
if (default.OVERWATCH_ABILITIES.Find(kAbility.GetMyTemplateName()) != INDEX_NONE)
{
// Check if target is moving
// Is interrupt state, so using same game state
LastGameState = History.GetGameStateFromHistory(History.GetCurrentHistoryIndex());
LastAbilityContext = XComGameStateContext_Ability(LastGameState.GetContext());
if (LastAbilityContext != none)
if (default.MOVEMENT_ABILITY.Find(LastAbilityContext.InputContext.AbilityTemplateName) != INDEX_NONE)
isRunningOverwatch = true;
}
}
// reset shot breakdown
m_ShotBreakdown = EmptyShotBreakdown;
// check for a special guaranteed hit
m_ShotBreakdown.SpecialGuaranteedHit = UnitState.CheckSpecialGuaranteedHit(kAbility, SourceWeapon, TargetState);
m_ShotBreakdown.SpecialCritLabel = UnitState.CheckSpecialCritLabel(kAbility, SourceWeapon, TargetState);
// add all of the built-in modifiers
if (bGuaranteedHit || m_ShotBreakdown.SpecialGuaranteedHit != '')
{
// call the super version to bypass our check to ignore success mods for guaranteed hits
super.AddModifier(100, AbilityTemplate.LocFriendlyName, m_ShotBreakdown, eHit_Success, bDebugLog); //WATCHOUT!!! SUPER. IS NOT ANYMORE THE PARENT
super(X2AbilitytoHitCalc).AddModifier(100, AbilityTemplate.LocFriendlyName, m_ShotBreakdown, eHit_Success, bDebugLog); //OF ABILITYTOHIT_STANDARDAIM BUT BUT THE PARENT
}
else if (bIndirectFire)
{
m_ShotBreakdown.HideShotBreakdown = true;
AddModifier(100, AbilityTemplate.LocFriendlyName, m_ShotBreakdown, eHit_Success, bDebugLog);
}
// Start Issue #1298
/// HL-Docs: ref:Bugfixes; issue:1298
/// Add 100 crit chance to guaranteed crit abilities for the purposes of UI.
if (bHitsAreCrits)
{
// call the super version to bypass our check to ignore crit chance mods for guaranteed crits
super.AddModifier(100, AbilityTemplate.LocFriendlyName, m_ShotBreakdown, eHit_Crit, bDebugLog);
}
// End Issue #1298
// Issue #346: AddModifier(BuiltIn...Mod) block moved later in method.
/// HL-Docs: ref:Bugfixes; issue:346
/// Prevent `X2AbilityToHitCalc_StandardAim` from applying BuiltInHitMod and BuiltInCritMod against non-units.
if (UnitState != none && TargetState == none)
{
// when targeting non-units, we have a 100% chance to hit. They can't dodge or otherwise
// mess up our shots
m_ShotBreakdown.HideShotBreakdown = true;
AddModifier(100, class'XLocalizedData'.default.OffenseStat, m_ShotBreakdown, eHit_Success, bDebugLog);
}
else if (UnitState != none && TargetState != none)
{
// Start Issue #346: Block moved from earlier.
AddModifier(BuiltInHitMod, AbilityTemplate.LocFriendlyName, m_ShotBreakdown, eHit_Success, bDebugLog);
AddModifier(BuiltInCritMod, AbilityTemplate.LocFriendlyName, m_ShotBreakdown, eHit_Crit, bDebugLog);
// End Issue #346
if (!bIndirectFire)
{
// StandardAim (with direct fire) will require visibility info between source and target (to check cover).
if (`TACTICALRULES.VisibilityMgr.GetVisibilityInfo(UnitState.ObjectID, TargetState.ObjectID, VisInfo))
{
if (UnitState.CanFlank() && TargetState.GetMyTemplate().bCanTakeCover && VisInfo.TargetCover == CT_None)
bFlanking = true;
if (VisInfo.bClearLOS && !VisInfo.bVisibleGameplay)
bSquadsight = true;
// Add basic offense and defense values
AddModifier(UnitState.GetBaseStat(eStat_Offense), class'XLocalizedData'.default.OffenseStat, m_ShotBreakdown, eHit_Success, bDebugLog);
// Single Line Change for Issue #313
/// HL-Docs: ref:GetStatModifiersFixed
UnitState.GetStatModifiersFixed(eStat_Offense, StatMods, StatModValues);
for (i = 0; i < StatMods.Length; ++i)
{
AddModifier(int(StatModValues[i]), StatMods[i].GetX2Effect().FriendlyName, m_ShotBreakdown, eHit_Success, bDebugLog);
}
// Flanking bonus (do not apply to overwatch shots)
if (bFlanking && !bReactionFire && !bMeleeAttack)
{
AddModifier(UnitState.GetCurrentStat(eStat_FlankingAimBonus), class'XLocalizedData'.default.FlankingAimBonus, m_ShotBreakdown, eHit_Success, bDebugLog);
}
// Squadsight penalty
if (bSquadsight)
{
Tiles = UnitState.TileDistanceBetween(TargetState);
// remove number of tiles within visible range (which is in meters, so convert to units, and divide that by tile size)
Tiles -= UnitState.GetVisibilityRadius() * class'XComWorldData'.const.WORLD_METERS_TO_UNITS_MULTIPLIER / class'XComWorldData'.const.WORLD_StepSize;
if (Tiles > 0) // pretty much should be since a squadsight target is by definition beyond sight range. but...
AddModifier(default.SQUADSIGHT_DISTANCE_MOD * Tiles, class'XLocalizedData'.default.SquadsightMod, m_ShotBreakdown, eHit_Success, bDebugLog);
else if (Tiles == 0) // right at the boundary, but squadsight IS being used so treat it like one tile
AddModifier(default.SQUADSIGHT_DISTANCE_MOD, class'XLocalizedData'.default.SquadsightMod, m_ShotBreakdown, eHit_Success, bDebugLog);
}
// Check for modifier from weapon
if (SourceWeapon != none)
{
iWeaponMod = SourceWeapon.GetItemAimModifier();
AddModifier(iWeaponMod, class'XLocalizedData'.default.WeaponAimBonus, m_ShotBreakdown, eHit_Success, bDebugLog);
WeaponUpgrades = SourceWeapon.GetMyWeaponUpgradeTemplates();
for (i = 0; i < WeaponUpgrades.Length; ++i)
{
if (WeaponUpgrades[i].AddHitChanceModifierFn != None)
{
if (WeaponUpgrades[i].AddHitChanceModifierFn(WeaponUpgrades[i], VisInfo, iWeaponMod))
{
AddModifier(iWeaponMod, WeaponUpgrades[i].GetItemFriendlyName(), m_ShotBreakdown, eHit_Success, bDebugLog);
}
}
}
}
// Target defense
// Start Issue #1295
// Add separate entries for different sources of Defense on the target unit rather than one entry with all sources of Defense rolled into it.
//AddModifier(-TargetState.GetCurrentStat(eStat_Defense), class'XLocalizedData'.default.DefenseStat, m_ShotBreakdown, eHit_Success, bDebugLog);
AddModifier(-TargetState.GetBaseStat(eStat_Defense), class'XLocalizedData'.default.DefenseStat, m_ShotBreakdown, eHit_Success, bDebugLog);
/// HL-Docs: ref:GetStatModifiersFixed
TargetState.GetStatModifiersFixed(eStat_Defense, StatMods, StatModValues);
for (i = 0; i < StatMods.Length; ++i)
{
AddModifier(-int(StatModValues[i]), StatMods[i].GetX2Effect().FriendlyName, m_ShotBreakdown, eHit_Success, bDebugLog);
}
// End Issue #1295
// Add weapon range
if (SourceWeapon != none)
{
iRangeModifier = GetWeaponRangeModifier(UnitState, TargetState, SourceWeapon);
AddModifier(iRangeModifier, class'XLocalizedData'.default.WeaponRange, m_ShotBreakdown, eHit_Success, bDebugLog);
}
// Cover modifiers
if (bMeleeAttack)
{
AddModifier(MELEE_HIT_BONUS, class'XLocalizedData'.default.MeleeBonus, m_ShotBreakdown, eHit_Success, bDebugLog);
}
else
{
`log("TargetState.CanTakeCover():"@TargetState.CanTakeCover()@", isRunningOverwatch:"@isRunningOverwatch, true, 'XCom_HitRolls');
// Add cover penalties
if (TargetState.CanTakeCover()
// ========================================
// From -bg-'s EU Aim Rolls mod
// ========================================
&& (!OVERWATCH_BYPASS_COVER || !isRunningOverwatch))
{
// if any cover is being taken, factor in the angle to attack
if( VisInfo.TargetCover != CT_None && !bIgnoreCoverBonus )
{
switch( VisInfo.TargetCover )
{
case CT_MidLevel: // half cover
AddModifier(-LOW_COVER_BONUS, class'XLocalizedData'.default.TargetLowCover, m_ShotBreakdown, eHit_Success, bDebugLog);
CoverValue = LOW_COVER_BONUS;
break;
case CT_Standing: // High Cover
AddModifier(-HIGH_COVER_BONUS, class'XLocalizedData'.default.TargetHighCover, m_ShotBreakdown, eHit_Success, bDebugLog);
CoverValue = HIGH_COVER_BONUS;
break;
}
TileDistance = UnitState.TileDistanceBetween(TargetState);
// from Angle 0 -> MIN_ANGLE_TO_COVER, receive full MAX_ANGLE_BONUS_MOD
// As Angle increases from MIN_ANGLE_TO_COVER -> MAX_ANGLE_TO_COVER, reduce bonus received by lerping MAX_ANGLE_BONUS_MOD -> MIN_ANGLE_BONUS_MOD
// Above MAX_ANGLE_TO_COVER, receive no bonus
if( VisInfo.TargetCoverAngle < 90-default.MinimumGoodAngleBonusStartsAtThisHorizontalAngleBetweenLineOfFireAndCoverSurface && (TileDistance <= MAX_TILE_DISTANCE_TO_COVER || default.ApplyGoodAngleBonusEvenFartherThan11TilesAway) && (UnitState.GetTeam() == eTeam_XCom || default.ApplyGoodAngleBonusNotOnlyForXComSoldiersButAlsoForAnyoneShooting))
{
Alpha = FClamp((VisInfo.TargetCoverAngle - default.MaximumGoodAngleBonusEndsAtThisHorizontalAngleBetweenLineOfFireAndCoverSurface) / (default.MinimumGoodAngleBonusStartsAtThisHorizontalAngleBetweenLineOfFireAndCoverSurface - default.MaximumGoodAngleBonusEndsAtThisHorizontalAngleBetweenLineOfFireAndCoverSurface), 0.0, 1.0);
AngleToCoverModifier = Lerp(default.MaximumGoodAngleBonusCoverReductionPercentage /100, default.MinimumGoodAngleBonusCoverReductionPercentage /100, Alpha);
GoodAngleBonus = Round(CoverValue * AngleToCoverModifier);
`log("GoodAngleBonus:"@GoodAngleBonus, true, 'XCom_HitRolls');
AddModifier(GoodAngleBonus, class'XLocalizedData'.default.AngleToTargetCover, m_ShotBreakdown, eHit_Success, bDebugLog);
}
}
}
// Add height advantage
if (UnitState.HasHeightAdvantageOver(TargetState, true) && default.ApplyVerticalGoodAngleAgainstLowerTargetAsProportionalHeightAdvantage != true)
{
AddModifier(class'X2TacticalGameRuleset'.default.UnitHeightAdvantageBonus, class'XLocalizedData'.default.HeightAdvantage, m_ShotBreakdown, eHit_Success, bDebugLog);
}
// Check for height disadvantage
if (TargetState.HasHeightAdvantageOver(UnitState, false) && default.ApplyVerticalGoodAngleAgainstLowerTargetAsProportionalHeightAdvantage != true)
{
AddModifier(class'X2TacticalGameRuleset'.default.UnitHeightDisadvantagePenalty, class'XLocalizedData'.default.HeightDisadvantage, m_ShotBreakdown, eHit_Success, bDebugLog);
}
//REPLACED 'STUPID' PARAGRAPH ABOVE
//WITH PARAGRAPH BELOW!!!
ShooterHead = ShooterTile; ShooterHead.Z = `XWORLD.GetFloorTileZ(ShooterHead) + UnitState.UnitHeight;
TargetHead = TargetTile; TargetHead.Z = `XWORLD.GetFloorTileZ(TargetHead ) + TargetState.UnitHeight;
ShooterHeadRelativeElevation = ShooterHead.Z - TargetHead.Z; //TARGETEXTRAHEIGHT MUST NOT BE CONSIDERED BUT TEST IT FOR A WHILE TO SEE IF IT IS CONSIDERED WHILE ALIENS ARE USING THIS FUNCTION
TileDistance = UnitState.TileDistanceBetween(TargetState);
VerticalAngle = atan(ShooterHeadRelativeElevation / TileDistance) / (2*PI) * 360; //GOD BLESS TRIGONOMETRY
if (default.ApplyVerticalGoodAngleAgainstLowerTargetAsProportionalHeightAdvantage && ShooterHeadRelativeElevation > 0 && ((VisInfo.TargetCover == CT_MidLevel) //HEIGHT ADVANTAGE REDUCES ONLY LOW COVER .... OR SHOULD IT???
|| (VisInfo.TargetCover == CT_Standing && CoverTypeOfTileAboveTarget == CT_None)))
{ //...WELL DEPENDS ON WETHER IT'S A VERTICAL TREE OR AN ORIZONTAL HIGHCOVER FENCE... ;)
if (GoodAngleBonus != 0) CoverValue = CoverValue - GoodAngleBonus; //HEIGHT ADVANTAGE APPLIED "AFTER" GOOD ANGLE IS APPLIED (NOT SEPARATELY,
//OTHERWISE IT COULD SUBTRACT 2 VALUES CUMULATEVELY HIGHER THAN COVERVALUE ITSELF)
Alpha = FClamp(((90-VerticalAngle) - default.MaximumHeightAdvantageBonusEndsAtThisVerticalAngleBetweenLineOfFireAndCoverSurface) / (default.MinimumHeightAdvantageBonusStartsAtThisVerticalAngleBetweenLineOfFireAndCoverSurface - default.MaximumHeightAdvantageBonusEndsAtThisVerticalAngleBetweenLineOfFireAndCoverSurface), 0.0, 1.0);
AngleToCoverModifier = Lerp(default.MaximumHeightAdvantageBonusCoverReductionPercentage/100, default.MinimumHeightAdvantageBonusCoverReductionPercentage/100 , Alpha);
HeightAdvantageBonus = round(AngleToCoverModifier * CoverValue);
`log("HeightAdvantageBonus:"@HeightAdvantageBonus, true, 'XCom_HitRolls');
AddModifier(HeightAdvantageBonus, class'XLocalizedData'.default.HeightAdvantage, m_ShotBreakdown, eHit_Success, bDebugLog);
}
if ((default.AllowFarCoverToGiveSomeCoverProtection || default.GiveHitChanceZeroWhenTargetableEnemiesAreInsteadNotTrulyInLineOfSight)
// ========================================
// From -bg-'s EU Aim Rolls mod
// ========================================
&& (!OVERWATCH_BYPASS_COVER || !isRunningOverwatch))
{
// Both these modifiers are based off far cover
FarCover = 0;
ShooterPeekTile = VisInfo.SourceTile;
ShooterPeekTile.Z += 1;
`log("ShooterPeekTile:"@ShooterPeekTile.X@","@ShooterPeekTile.Y@","@ShooterPeekTile.Z, true, 'XCom_HitRolls');
TargetPeekTile = VisInfo.DestTile;
`log("TargetTile:"@TargetTile.X@","@TargetTile.Y@","@TargetTile.Z, true, 'XCom_HitRolls');
`log("TargetPeekTile:"@TargetPeekTile.X@","@TargetPeekTile.Y@","@TargetPeekTile.Z, true, 'XCom_HitRolls');
`log("Target Height:"@TargetState.UnitHeight, true, 'XCom_HitRolls');
if (VisInfo.TargetCover == CT_None)
{
ExistingCoverHeight = 0;
TargetHeight = TargetState.UnitHeight;
}
else if (TargetState.CanTakeCover())
{
if (VisInfo.TargetCover == CT_MidLevel)
{
ExistingCoverHeight = 1;
TargetHeight = TargetState.UnitHeight - 1; // Crouched behind low cover
}
else
{
ExistingCoverHeight = 2;
TargetHeight = TargetState.UnitHeight;
}
}
// FarCover to a tile which may already be granting cover
FarCover = 0;
NoLineOfSight = true;
ShooterPeekVec = `XWORLD.GetPositionFromTileCoordinates(ShooterPeekTile);
LosTile = TargetTile;
LosVec = `XWORLD.GetPositionFromTileCoordinates(LosTile);
Floor = LosVec.Z;
for (i = 16; i < TargetState.UnitHeight * 64; i += 16)
{
LosVec.Z = Floor + i;
if (`XWORLD.VoxelRaytrace_Locations(ShooterPeekVec, LosVec, RayTrace))
{
// Don't apply far cover to existing cover or above the target's head
if (i > ExistingCoverHeight*64 && i <= TargetHeight*64)
{
`log("LOS blocked by far cover vector:"@i@","@VisInfo.TargetCover@","@RayTrace.BlockedTile.X@","@RayTrace.BlockedTile.Y@","@RayTrace.BlockedTile.Z, true, 'XCom_HitRolls');
FarCover += 1;
}
else if (i <= ExistingCoverHeight*64)
{
`log("Hit existing cover:"@i@RayTrace.BlockedTile.X@","@RayTrace.BlockedTile.Y@","@RayTrace.BlockedTile.Z, true, 'XCom_HitRolls');
}
}
else if (i <= TargetHeight*64)
{
if (NoLineOfSight) `log("Seen target:"@i, true, 'XCom_HitRolls');
NoLineOfSight = false;
}
}
// FarCover for which cover hasn't been determined already
LosTile = TargetPeekTile;
LosTile.Z = TargetTile.Z; // not based on peek tile as that can be on a different level
LosVec = `XWORLD.GetPositionFromTileCoordinates(LosTile);
for (i = 16; i < TargetState.UnitHeight * 64; i += 16)
{
LosVec.Z = Floor + i;
if (`XWORLD.VoxelRaytrace_Locations(ShooterPeekVec, LosVec, RayTrace))
{
`log("LOS blocked by far cover vector:"@i@","@RayTrace.BlockedTile.X@","@RayTrace.BlockedTile.Y@","@RayTrace.BlockedTile.Z, true, 'XCom_HitRolls');
FarCover += 1;
}
else // Target is always standing when peeking
{
if (NoLineOfSight) `log("Seen target:"@i, true, 'XCom_HitRolls');
NoLineOfSight = false;
}
}
if (default.GiveHitChanceZeroWhenTargetableEnemiesAreInsteadNotTrulyInLineOfSight && NoLineOfSight)
{
`log("NoLineOfSightMalus:"@default.NO_LINE_OF_SIGHT_PENALTY, true, 'XCom_HitRolls');
AddModifier(NO_LINE_OF_SIGHT_PENALTY, "No Line of Sight", m_ShotBreakdown, eHit_Success, bDebugLog);
}
else if (default.AllowFarCoverToGiveSomeCoverProtection && (TargetState.GetTeam() == eTeam_XCom || TargetState.GetCurrentStat(eStat_AlertLevel)==`ALERT_LEVEL_RED || (TargetState.GetCurrentStat(eStat_AlertLevel)!=`ALERT_LEVEL_RED && !default.OnlyAlertedTargetsCanHaveTheExtraProtectionOfFarCoverAndHeightDisadvantage)))
{
// Big things are easy to hit even if partially covered
if (TargetState.UnitHeight > 2)
{
`log("FarCover base:"@FarCover, true, 'XCom_HitRolls');
FarCover = Max(0, FarCover - (TargetState.UnitHeight - 2) * 8);
`log("FarCover after big target factor:"@FarCover, true, 'XCom_HitRolls');
}
// Scale far cover to high cover against possible tile blocks for a human sized target
FarCoverBonus = FarCover * HIGH_COVER_BONUS / 14;
`log("FarCoverBonus:"@round(-FarCoverBonus), true, 'XCom_HitRolls');
AddModifier(round(-FarCoverBonus), "Far Cover", m_ShotBreakdown, eHit_Success, bDebugLog);
}
}
//END OF REPLACED
//PARAGRAPH... ;)
}
}
if (UnitState.IsConcealed())
{
`log("Shooter is concealed, target cannot dodge.", bDebugLog, 'XCom_HitRolls');
}
else
{
if (SourceWeapon == none || SourceWeapon.CanWeaponBeDodged())
{
if (TargetState.CanDodge(UnitState, kAbility))
{
AddModifier(TargetState.GetCurrentStat(eStat_Dodge), class'XLocalizedData'.default.DodgeStat, m_ShotBreakdown, eHit_Graze, bDebugLog);
}
else
{
`log("Target cannot dodge due to some gameplay effect.", bDebugLog, 'XCom_HitRolls');
}
}
}
}
// Now check for critical chances.
if (bAllowCrit)
{
AddModifier(UnitState.GetBaseStat(eStat_CritChance), class'XLocalizedData'.default.CharCritChance, m_ShotBreakdown, eHit_Crit, bDebugLog);
// Single Line Change for Issue #313
/// HL-Docs: ref:GetStatModifiersFixed
UnitState.GetStatModifiersFixed(eStat_CritChance, StatMods, StatModValues);
for (i = 0; i < StatMods.Length; ++i)
{
AddModifier(int(StatModValues[i]), StatMods[i].GetX2Effect().FriendlyName, m_ShotBreakdown, eHit_Crit, bDebugLog);
}
if (bSquadsight)
{
AddModifier(default.SQUADSIGHT_CRIT_MOD, class'XLocalizedData'.default.SquadsightMod, m_ShotBreakdown, eHit_Crit, bDebugLog);
}
if (SourceWeapon != none)
{
AddModifier(SourceWeapon.GetItemCritChance(), class'XLocalizedData'.default.WeaponCritBonus, m_ShotBreakdown, eHit_Crit, bDebugLog);
// Issue #237 start, let upgrades modify the crit chance of the breakdown
WeaponUpgrades = SourceWeapon.GetMyWeaponUpgradeTemplates();
for (i = 0; i < WeaponUpgrades.Length; ++i)
{
// Make sure we check to only use anything from the ini that we've specified doesn't use an Effect to modify crit chance
// Everything that does use an Effect, e.g. base game Laser Sights, get added in about 23 lines down from here
if (WeaponUpgrades[i].AddCritChanceModifierFn != None && default.CritUpgradesThatDontUseEffects.Find(WeaponUpgrades[i].DataName) != INDEX_NONE)
{
if (WeaponUpgrades[i].AddCritChanceModifierFn(WeaponUpgrades[i], iWeaponMod))
{
AddModifier(iWeaponMod, WeaponUpgrades[i].GetItemFriendlyName(), m_ShotBreakdown, eHit_Crit, bDebugLog);
}
}
}
// Issue #237 end
}
if (bFlanking && !bMeleeAttack)
{
if (`XENGINE.IsMultiplayerGame())
{
AddModifier(default.MP_FLANKING_CRIT_BONUS, class'XLocalizedData'.default.FlankingCritBonus, m_ShotBreakdown, eHit_Crit, bDebugLog);
}
else
{
AddModifier(UnitState.GetCurrentStat(eStat_FlankingCritChance), class'XLocalizedData'.default.FlankingCritBonus, m_ShotBreakdown, eHit_Crit, bDebugLog);
}
}
}
foreach UnitState.AffectedByEffects(EffectRef)
{
EffectModifiers.Length = 0;
EffectState = XComGameState_Effect(History.GetGameStateForObjectID(EffectRef.ObjectID));
if (EffectState == none)
continue;
PersistentEffect = EffectState.GetX2Effect();
if (PersistentEffect == none)
continue;
if (UniqueToHitEffects.Find(PersistentEffect) != INDEX_NONE)
continue;
PersistentEffect.GetToHitModifiers(EffectState, UnitState, TargetState, kAbility, class'X2AbilitytoHitCalc_StandardAim', bMeleeAttack, bFlanking, bIndirectFire, EffectModifiers);
if (EffectModifiers.Length > 0)
{
if (PersistentEffect.UniqueToHitModifiers())
UniqueToHitEffects.AddItem(PersistentEffect);
for (i = 0; i < EffectModifiers.Length; ++i)
{
if (!bAllowCrit && EffectModifiers[i].ModType == eHit_Crit)
{
if (!PersistentEffect.AllowCritOverride())
continue;
}
AddModifier(EffectModifiers[i].Value, EffectModifiers[i].Reason, m_ShotBreakdown, EffectModifiers[i].ModType, bDebugLog);
}
}
if (PersistentEffect.ShotsCannotGraze())
{
bIgnoreGraze = true;
IgnoreGrazeReason = PersistentEffect.FriendlyName;
}
}
UniqueToHitEffects.Length = 0;
if (TargetState.AffectedByEffects.Length > 0)
{
foreach TargetState.AffectedByEffects(EffectRef)
{
EffectModifiers.Length = 0;
EffectState = XComGameState_Effect(History.GetGameStateForObjectID(EffectRef.ObjectID));
if (EffectState == none)
continue;
PersistentEffect = EffectState.GetX2Effect();
if (PersistentEffect == none)
continue;
if (UniqueToHitEffects.Find(PersistentEffect) != INDEX_NONE)
continue;
PersistentEffect.GetToHitAsTargetModifiers(EffectState, UnitState, TargetState, kAbility, class'X2AbilitytoHitCalc_StandardAim', bMeleeAttack, bFlanking, bIndirectFire, EffectModifiers);
if (EffectModifiers.Length > 0)
{
if (PersistentEffect.UniqueToHitAsTargetModifiers())
UniqueToHitEffects.AddItem(PersistentEffect);
for (i = 0; i < EffectModifiers.Length; ++i)
{
if (!bAllowCrit && EffectModifiers[i].ModType == eHit_Crit)
continue;
if (bIgnoreGraze && EffectModifiers[i].ModType == eHit_Graze)
continue;
AddModifier(EffectModifiers[i].Value, EffectModifiers[i].Reason, m_ShotBreakdown, EffectModifiers[i].ModType, bDebugLog);
}
}
}
}
// Remove graze if shooter ignores graze chance.
if (bIgnoreGraze)
{
AddModifier(-m_ShotBreakdown.ResultTable[eHit_Graze], IgnoreGrazeReason, m_ShotBreakdown, eHit_Graze, bDebugLog);
}
// Remove crit from reaction fire. Must be done last to remove all crit.
if (bReactionFire)
{
AddReactionCritModifier(UnitState, TargetState, m_ShotBreakdown, bDebugLog);
}
}
// Start Issue #1271
/// HL-Docs: feature:GetAdditionalHitModifiers; issue:1271; tags:tactical
/// This feature adds a method that can be used by subclasses to apply
/// additional hit modifiers without having to override and copy paste
/// the entire `GetHitChance()` function.
GetAdditionalHitModifiers_CH(kAbility, kTarget, m_ShotBreakdown, bDebugLog);
// End Issue #1271
// Final multiplier based on end Success chance
if (bReactionFire && !bGuaranteedHit)
{
FinalAdjust = m_ShotBreakdown.ResultTable[eHit_Success] * GetReactionAdjust(UnitState, TargetState);
AddModifier(-int(FinalAdjust), AbilityTemplate.LocFriendlyName, m_ShotBreakdown, eHit_Success, bDebugLog);
AddReactionFlatModifier(UnitState, TargetState, m_ShotBreakdown, bDebugLog);
}
else if (FinalMultiplier != 1.0f)
{
FinalAdjust = m_ShotBreakdown.ResultTable[eHit_Success] * FinalMultiplier;
AddModifier(-int(FinalAdjust), AbilityTemplate.LocFriendlyName, m_ShotBreakdown, eHit_Success, bDebugLog);
}
FinalizeHitChance(m_ShotBreakdown, bDebugLog);
return m_ShotBreakdown.FinalHitChance;
}
//ADDED THIS SECTION
function ECoverType CoverTypeOfTileAboveCover(const out TTile SourceTile, const out TTile DestTile, int TargetCoverAngle) {
local TTile TileDifference;
local TTile TileAboveCover;
TileDifference.X = SourceTile.X - DestTile.X;
TileDifference.Y = SourceTile.Y - DestTile.Y;
TileAboveCover = DestTile; TileAboveCover.Z += 2;
if( TileDifference.X > 0) TileAboveCover.X += 1; else TileAboveCover.X -= 1;
if(`XWORLD.IsTileFullyOccupied(TileAboveCover) || `XWORLD.IsTileOccupied(TileAboveCover)) return CT_Standing;
TileAboveCover = DestTile; TileAboveCover.Z += 2;
if( TileDifference.Y > 0) TileAboveCover.Y += 1; else TileAboveCover.Y -= 1;
if(`XWORLD.IsTileFullyOccupied(TileAboveCover) || `XWORLD.IsTileOccupied(TileAboveCover)) return CT_Standing;
}