diff --git a/.gitignore b/.gitignore index 7e29421a1..ba852a4a0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # Project specific user settings .vscode/settings.json +*.code-workspace # Python *.pyc diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e829a6064..edc662e93 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,54 +6,54 @@ { "label": "Build (final release)", "type": "shell", - "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file '${workspaceRoot}\\.scripts\\build.ps1' -srcDirectory '${workspaceRoot}' -sdkPath '${config:xcom.highlander.sdkroot}' -gamePath '${config:xcom.highlander.gameroot}' -config final_release", + "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file \"${workspaceRoot}\\.scripts\\build.ps1\" -srcDirectory \"${workspaceRoot}\" -sdkPath \"${config:xcom.highlander.sdkroot}\" -gamePath \"${config:xcom.highlander.gameroot}\" -config final_release", "group": "build", "problemMatcher": [] }, { "label": "Build (cooked)", "type": "shell", - "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file '${workspaceRoot}\\.scripts\\build.ps1' -srcDirectory '${workspaceRoot}' -sdkPath '${config:xcom.highlander.sdkroot}' -gamePath '${config:xcom.highlander.gameroot}' -config default", + "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file \"${workspaceRoot}\\.scripts\\build.ps1\" -srcDirectory \"${workspaceRoot}\" -sdkPath \"${config:xcom.highlander.sdkroot}\" -gamePath \"${config:xcom.highlander.gameroot}\" -config default", "group": "build", "problemMatcher": [] }, { "label": "Build (debug)", "type": "shell", - "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file '${workspaceRoot}\\.scripts\\build.ps1' -srcDirectory '${workspaceRoot}' -sdkPath '${config:xcom.highlander.sdkroot}' -gamePath '${config:xcom.highlander.gameroot}' -config debug", + "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file \"${workspaceRoot}\\.scripts\\build.ps1\" -srcDirectory \"${workspaceRoot}\" -sdkPath \"${config:xcom.highlander.sdkroot}\" -gamePath \"${config:xcom.highlander.gameroot}\" -config debug", "group": "build", "problemMatcher": [] }, { "label": "Build (compiletest)", "type": "shell", - "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file '${workspaceRoot}\\.scripts\\build.ps1' -srcDirectory '${workspaceRoot}' -sdkPath '${config:xcom.highlander.sdkroot}' -gamePath '${config:xcom.highlander.gameroot}' -config compiletest", + "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file \"${workspaceRoot}\\.scripts\\build.ps1\" -srcDirectory \"${workspaceRoot}\" -sdkPath \"${config:xcom.highlander.sdkroot}\" -gamePath \"${config:xcom.highlander.gameroot}\" -config compiletest", "group": "build", "problemMatcher": [] }, { "label": "Build for workshop stable version", "type": "shell", - "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file '${workspaceRoot}\\.scripts\\build.ps1' -srcDirectory '${workspaceRoot}' -sdkPath '${config:xcom.highlander.sdkroot}' -gamePath '${config:xcom.highlander.gameroot}' -config stable", + "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file \"${workspaceRoot}\\.scripts\\build.ps1\" -srcDirectory \"${workspaceRoot}\" -sdkPath \"${config:xcom.highlander.sdkroot}\" -gamePath \"${config:xcom.highlander.gameroot}\" -config stable", "group": "build", "problemMatcher": [] }, { "label": "runGame", "type": "shell", - "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file '${workspaceRoot}\\.scripts\\run.ps1' -gamePath '${config:xcom.highlander.gameroot}'", + "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file \"${workspaceRoot}\\.scripts\\run.ps1\" -gamePath \"${config:xcom.highlander.gameroot}\"", "problemMatcher": [] }, { "label": "runUnrealEditor", "type": "shell", - "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file '${workspaceRoot}\\.scripts\\runUnrealEditor.ps1' -sdkPath '${config:xcom.highlander.sdkroot}'", + "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file \"${workspaceRoot}\\.scripts\\runUnrealEditor.ps1\" -sdkPath \"${config:xcom.highlander.sdkroot}\"", "problemMatcher": [] }, { "label": "updateVersions", "type": "shell", - "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file '${workspaceRoot}\\.scripts\\update_version.ps1' -ps '${workspaceRoot}\\VERSION.ps1' -srcDirectory '${workspaceRoot}' -no_cache", + "command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file \"${workspaceRoot}\\.scripts\\update_version.ps1\" -ps \"${workspaceRoot}\\VERSION.ps1\" -srcDirectory \"${workspaceRoot}\" -no_cache", "problemMatcher": [] }, { @@ -70,6 +70,6 @@ }, "command": "python ..\\..\\.scripts\\make_docs.py .\\test_src --outdir .\\test_output --docsdir .\\test_tags --dumpelt .\\test_output\\CHL_Event_Compiletest.uc | % {$_.replace('\\', '/')} | Out-File .\\test_output\\stdout.log -Encoding ASCII", "problemMatcher": [] - }, + } ] } \ No newline at end of file diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/CHHelpers.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/CHHelpers.uc index a61ad7328..74fe2b8a9 100644 --- a/X2WOTCCommunityHighlander/Src/XComGame/Classes/CHHelpers.uc +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/CHHelpers.uc @@ -332,6 +332,20 @@ struct OverrideHasHeightAdvantageStruct var protectedwrite array OverrideHasHeightAdvantageCallbacks; // End Issue #851 +// Start Issue #1565 +struct OverrideCoverLevelStruct +{ + var delegate OverrideCoverLevelFn; + var int Priority; + + structdefaultproperties + { + Priority = 50 + } +}; +var protectedwrite array OverrideCoverLevelCallbacks; +// End Issue #1565 + // Start Issue #1138 struct PrioritizeRightClickMeleeStruct { @@ -351,6 +365,8 @@ delegate EHLDelegateReturn ShouldDisplayMultiSlotItemInTacticalDelegate(XComGame delegate EHLDelegateReturn OverrideHasHeightAdvantageDelegate(XComGameState_Unit Attacker, XComGameState_Unit TargetUnit, out int bHasHeightAdvantage); // Issue #851 delegate EHLDelegateReturn PrioritizeRightClickMeleeDelegate(XComGameState_Unit UnitState, out XComGameState_Ability PrioritizedMeleeAbility, optional XComGameState_BaseObject TargetObject); // Issue #1138 +delegate EHLDelegateReturn OverrideCoverLevelDelegate(XComGameState_Unit Attacker, XComGameState_Unit Target, out GameRulesCache_VisibilityInfo VisInfo, Object EventSource); // Issue #1565 + // Start Issue #123 simulated static function RebuildPerkContentCache() { local XComContentManager Content; @@ -1010,6 +1026,143 @@ simulated function TriggerOverrideHasHeightAdvantage(XComGameState_Unit Attacker } // End Issue #851 +// Start Issue #1565 +/// HL-Docs: feature:CoverLevelOverride; issue:1565; tags:tactical +/// This feature allows mods to override which cover level a unit has against an attack by +/// another unit on a case-by-case basis, gaining various tactical benefits. +/// +/// Normally this override would have been implemented as an event, but events in To Hit Chance Calculation logic can cause issues, +/// see [GetHitChanceEvents](../tactical/GetHitChanceEvents.md), so the delegates system is used instead. +/// +/// This feature applies to X2Effect_HuntersInstinctDamage, X2AbilityToHitCalc_StandardAim, and all descendants thereof. +/// It does not apply to hit calcs if: +/// - It is a descendant class which overrides GetHitChance without supporting this feature +/// - The attack is not subject to cover (bIndirectFire, bMeleeAttack) +/// - It *does* apply if bIgnoreCoverBonus is true, *but* does not override its behavior +/// +/// It does not apply to Hunter's Instinct if: +/// - It is a descendant class which overrides GetAttackingDamageModifier without supporting this feature +/// - The effect would not be affected by cover level (e.g. it's a melee attack and thus ineligible for Hunter's Instinct) +/// +/// **NOTE:** While this feature allows the delegate to affect other visibility data, the +/// results of changing it are not fully understood by the developer. Use with caution! +/// +/// ## How to use +/// +/// Implement the following code in your mod's `X2DownloadableContentInfo` class: +/// ```unrealscript +/// static event OnPostTemplatesCreated() +/// { +/// local CHHelpers CHHelpersObj; +/// +/// CHHelpersObj = class'CHHelpers'.static.GetCDO(); +/// if (CHHelpersObj != none) +/// { +/// CHHelpersObj.AddOverrideHasHeightAdvantageCallback(OverrideHasHeightAdvantage); +/// } +/// } +/// +/// // To avoid crashes associated with garbage collection failure when transitioning between Tactical and Strategy, +/// // this function must be bound to the ClassDefaultObject of your class. Having this function in a class that +/// // `extends X2DownloadableContentInfo` is the easiest way to ensure that. +/// static private function EHLDelegateReturn OverrideHasHeightAdvantage(XComGameState_Unit Attacker, XComGameState_Unit TargetUnit, out GamesRuleCache_VisibilityInfo VisInfo, Object EventSource) +/// { +/// // Optionally modify `VisInfo.TargetCover` here. +/// // This is an enum containing the target's cover level against the attacker. +/// +/// // Return EHLDR_NoInterrupt or EHLDR_InterruptDelegates depending on +/// // if you want to allow other delegates to run after yours +/// // and potentially modify VisInfo further. +/// return EHLDR_NoInterrupt; +///} +/// ``` +/// +/// # Delegate Priority +/// You can optionally specify callback Priority. +///```unrealscript +///CHHelpersObj.AddOverrideCoverLevelCallback(OverrideCoverLevel, 45); +///``` +/// Delegates with higher Priority value are executed first. +/// Delegates with the same Priority are executed in the order they were added to CHHelpers, +/// which would normally be the same as [DLCRunOrder](../misc/DLCRunOrder.md). +/// This function will return `true` if the delegate was successfully registered. +simulated function bool AddOverrideCoverLevelCallback(delegate OverrideCoverLevelFn, optional int Priority = 50) +{ + local OverrideCoverLevelStruct NewOverrideCoverLevelCallback; + local int i, PriorityIndex; + local bool bPriorityIndexFound; + + if (OverrideCoverLevelFn == none) + { + return false; + } + // Cycle through the array of callbacks backwards + for (i = OverrideCoverLevelCallbacks.Length - 1; i >= 0; i--) + { + // Do not allow registering the same delegate more than once. + if (OverrideCoverLevelCallbacks[i].OverrideCoverLevelFn == OverrideCoverLevelFn) + { + return false; + } + + // Record the array index of the callback whose priority is higher or equal to the priority of the new callback, + // so that the new callback can be inserted right after it. + if (OverrideCoverLevelCallbacks[i].Priority >= Priority && !bPriorityIndexFound) + { + PriorityIndex = i + 1; // +1 so that InsertItem puts the new callback *after* this one. + + // Keep cycling through the array so that the previous check for duplicate delegates can run for every currently registered delegate. + bPriorityIndexFound = true; + } + } + + NewOverrideCoverLevelCallback.Priority = Priority; + NewOverrideCoverLevelCallback.OverrideCoverLevelFn = OverrideCoverLevelFn; + OverrideCoverLevelCallbacks.InsertItem(PriorityIndex, NewOverrideCoverLevelCallback); + + return true; +} + +/// HL-Docs: ref:CoverLevelOverride +/// # Removing Delegates +/// If necessary, it's possible to remove a delegate. +///```unrealscript +///CHHelpersObj.RemoveOverrideCoverLevelCallback(OverrideCoverLevel); +///``` +/// The function will return `true` if the Callback was successfully deleted, return false otherwise. +simulated function bool RemoveOverrideCoverLevelCallback(delegate OverrideCoverLevelFn) +{ + local int i; + + for (i = OverrideCoverLevelCallbacks.Length - 1; i >= 0; i--) + { + if (OverrideCoverLevelCallbacks[i].OverrideCoverLevelFn == OverrideCoverLevelFn) + { + OverrideCoverLevelCallbacks.Remove(i, 1); + return true; + } + } + return false; +} + +// Should be called by anything that wants to know a unit's cover level. +simulated function TriggerOverrideCoverLevel(XComGameState_Unit Attacker, XComGameState_Unit TargetUnit, out GameRulesCache_VisibilityInfo VisInfo, Object EventSource) +{ + local delegate OverrideCoverLevelFn; + local int i; + + for (i = 0; i < OverrideCoverLevelCallbacks.Length; i++) + { + OverrideCoverLevelFn = OverrideCoverLevelCallbacks[i].OverrideCoverLevelFn; + + if (OverrideCoverLevelFn(Attacker, TargetUnit, VisInfo, EventSource) == EHLDR_InterruptDelegates) + { + break; + } + } +} +// End Issue #1565 + // Start Issue #1138 /// HL-Docs: ref:PrioritizeRightClickMelee /// # Delegate Priority diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2AbilityToHitCalc_StandardAim.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2AbilityToHitCalc_StandardAim.uc index d3cea7483..37fffaa19 100644 --- a/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2AbilityToHitCalc_StandardAim.uc +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2AbilityToHitCalc_StandardAim.uc @@ -406,6 +406,12 @@ protected function int GetHitChance(XComGameState_Ability kAbility, AvailableTar // StandardAim (with direct fire) will require visibility info between source and target (to check cover). if (`TACTICALRULES.VisibilityMgr.GetVisibilityInfo(UnitState.ObjectID, TargetState.ObjectID, VisInfo)) { + // Issue #1565 - check if we're really considered "in cover" + if(!bMeleeAttack) + { + class'CHHelpers'.static.GetCDO().TriggerOverrideCoverLevel(UnitState, TargetState, VisInfo, self); + } + if (UnitState.CanFlank() && TargetState.GetMyTemplate().bCanTakeCover && VisInfo.TargetCover == CT_None) bFlanking = true; if (VisInfo.bClearLOS && !VisInfo.bVisibleGameplay) diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_HuntersInstinctDamage.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_HuntersInstinctDamage.uc new file mode 100644 index 000000000..c115fedde --- /dev/null +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_HuntersInstinctDamage.uc @@ -0,0 +1,48 @@ +class X2Effect_HuntersInstinctDamage extends X2Effect_Persistent; + +var int BonusDamage; +var int BonusCritChance; + +function int GetAttackingDamageModifier(XComGameState_Effect EffectState, XComGameState_Unit Attacker, Damageable TargetDamageable, XComGameState_Ability AbilityState, const out EffectAppliedData AppliedData, const int CurrentDamage, optional XComGameState NewGameState) +{ + local XComGameState_Unit TargetUnit; + local GameRulesCache_VisibilityInfo VisInfo; + + TargetUnit = XComGameState_Unit(TargetDamageable); + if (!AbilityState.IsMeleeAbility() && TargetUnit != None && class'XComGameStateContext_Ability'.static.IsHitResultHit(AppliedData.AbilityResultContext.HitResult)) + { + if (AbilityState.GetSourceWeapon() != none && AbilityState.GetSourceWeapon().InventorySlot == eInvSlot_PrimaryWeapon) + { + if (`TACTICALRULES.VisibilityMgr.GetVisibilityInfo(Attacker.ObjectID, TargetUnit.ObjectID, VisInfo)) + { + // Issue #1565 - check if we're really considered "in cover" + class'CHHelpers'.static.GetCDO().TriggerOverrideCoverLevel(Attacker, TargetUnit, VisInfo, self); + + if (Attacker.CanFlank() && TargetUnit.CanTakeCover() && VisInfo.TargetCover == CT_None) + { + return BonusDamage; + } + } + } + } + return 0; +} + +function GetToHitModifiers(XComGameState_Effect EffectState, XComGameState_Unit Attacker, XComGameState_Unit Target, XComGameState_Ability AbilityState, class ToHitType, bool bMelee, bool bFlanking, bool bIndirectFire, out array ShotModifiers) +{ + local ShotModifierInfo ShotMod; + + if (AbilityState.IsMeleeAbility() && Target != none) + { + ShotMod.ModType = eHit_Crit; + ShotMod.Reason = FriendlyName; + ShotMod.Value = BonusCritChance; + ShotModifiers.AddItem(ShotMod); + } +} + +DefaultProperties +{ + DuplicateResponse = eDupe_Ignore + bDisplayInSpecialDamageMessageUI = true +}