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; }