From 54fe4e810358440bc2c899c851ff82983c4bc817 Mon Sep 17 00:00:00 2001 From: DaloLorn Date: Thu, 18 Dec 2025 09:58:47 +0100 Subject: [PATCH 1/3] VSCode tasks should no longer misbehave when paths to the workspace, SDK, or game include spaces. --- .vscode/tasks.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 From 037759f3115e382c112ada60b7f3942eda88bc0f Mon Sep 17 00:00:00 2001 From: DaloLorn Date: Sun, 12 Apr 2026 09:46:16 +0200 Subject: [PATCH 2/3] Added VSCode workspaces to gitignore, on the grounds that a good workspace contains user-specific information. --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 7a97014fc9dab4de080c6fe8018778c9db91e453 Mon Sep 17 00:00:00 2001 From: DaloLorn Date: Sun, 12 Apr 2026 13:52:35 +0200 Subject: [PATCH 3/3] [#1540] Added AdjustArmorMitigation and PREVIEW_ARMOR_MITIGATION: - AdjustArmorMitigation allows mods to intercept armor mitigation, armor piercing, and minimum armor mitigation for an attack. - The PREVIEW_ARMOR_MITIGATION config flag makes the UI account for armor mitigation when previewing attack damage. - This fully supports AdjustArmorMitigation, and uses the unused WeaponDamageValue fields Spread and PlusOne (plus the previously-used Pierce) to communicate the extra values therefrom. - Also performed some cleanup on #579, and added missing support for #321 and #743 to unit flags. --- X2WOTCCommunityHighlander/Config/XComGame.ini | 26 + .../Src/XComGame/Classes/CHHelpers.uc | 258 +++++++- .../Classes/UITacticalHUD_InfoPanel.uc | 369 +++++++++++ .../XComGame/Classes/UITacticalHUD_ShotHUD.uc | 552 ++++++++++++++++ .../Classes/UITacticalHUD_ShotWings.uc | 595 ++++++++++++++++++ .../Src/XComGame/Classes/UIUnitFlagManager.uc | 87 ++- .../Classes/X2Effect_ApplyWeaponDamage.uc | 97 ++- .../XComGame/Classes/XComGameState_Ability.uc | 16 + 8 files changed, 1967 insertions(+), 33 deletions(-) create mode 100644 X2WOTCCommunityHighlander/Src/XComGame/Classes/UITacticalHUD_InfoPanel.uc create mode 100644 X2WOTCCommunityHighlander/Src/XComGame/Classes/UITacticalHUD_ShotHUD.uc create mode 100644 X2WOTCCommunityHighlander/Src/XComGame/Classes/UITacticalHUD_ShotWings.uc diff --git a/X2WOTCCommunityHighlander/Config/XComGame.ini b/X2WOTCCommunityHighlander/Config/XComGame.ini index e28d2a7d0..841fe1550 100644 --- a/X2WOTCCommunityHighlander/Config/XComGame.ini +++ b/X2WOTCCommunityHighlander/Config/XComGame.ini @@ -321,3 +321,29 @@ InterruptionsUpdateTurnStartLocation = false ; Should interruptions update tur InterruptionsResetPanicTestsPerformedThisTurn = false ; Should interruptions reset the tracker for the amount of panic tests performed this turn, just like a normal turn? Default is false, because the unit isn't taking a typical turn. InterruptionsTriggerGroupTurnBegunEvent = true ; Should interruptions trigger the GroupTurnBegunEvent, just like what happens in a normal turn. Default is true, because interruptions work closely with "groups" (XComGameState_AIGroup) in the code side of things. ; End Issue #1325 + +; Begin Issue #1540 +;;; HL-Docs: ref:AdjustArmorMitigation +;;; # Include Armor in Damage Preview +;;; A new config flag has been provided in XComGame.ini which +;;; adjusts all damage previews to include armor mitigation. +;;; For instance, an attack dealing 3-5 damage with no piercing +;;; will preview 2-4 damage against a target with 1 armor pip, +;;; and report an "Armor" damage modifier of -1. +;;; +;;; This damage modifier also includes any mitigation added by AdjustArmorMitigation. +;;; If the adjusted mitigation is not constant for the attack (e.g. it scales +;;; off the damage dealt by the attack), it will show a range, e.g. "-1-2". +;;; +;;; Note that this flag is only guaranteed to apply to vanilla UI. +;;; Mods overriding UIUnitFlagManager, UITacticalHUD_ShotHUD, or +;;; UITacticalHUD_ShotWings may ignore this setting for one reason or another. +;;; +;;;```ini +;;;[XComGame.CHHelpers] +;;;; Set to false/commented out if you do not want armor mitigation to be factored into damage previews (vanilla behavior) +;;;; Set to true/uncomment it if you want armor mitigation to be factored into damage previews +;;;;PREVIEW_ARMOR_MITIGATION=true +;;;``` +;PREVIEW_ARMOR_MITIGATION=true +; End Issue #1540 \ No newline at end of file diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/CHHelpers.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/CHHelpers.uc index a61ad7328..8c5192ab5 100644 --- a/X2WOTCCommunityHighlander/Src/XComGame/Classes/CHHelpers.uc +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/CHHelpers.uc @@ -287,6 +287,10 @@ var config bool bDisableAutomaticBondPhoto; var config bool bManualPhotoTakenOnLastMission; // End Issue #1453 +// Begin Issue #1540 +var config bool PREVIEW_ARMOR_MITIGATION; +// End Issue #1540 + // Start Issue #885 enum EHLDelegateReturn { @@ -332,6 +336,20 @@ struct OverrideHasHeightAdvantageStruct var protectedwrite array OverrideHasHeightAdvantageCallbacks; // End Issue #851 +// Start Issue #1540 +struct AdjustArmorMitigationStruct +{ + var delegate AdjustArmorMitigationFn; + var int Priority; + + structdefaultproperties + { + Priority = 50 + } +}; +var protectedwrite array AdjustArmorMitigationCallbacks; +// End Issue #1540 + // Start Issue #1138 struct PrioritizeRightClickMeleeStruct { @@ -351,6 +369,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 AdjustArmorMitigationDelegate(int WeaponDamage, out int ArmorMitigation, out int ArmorPiercing, out int MinMitigation, EffectAppliedData ApplyEffectParams, X2Effect_ApplyWeaponDamage Source, optional bool ForMinDamagePreview, optional XComGameState NewGameState); // Issue #1540 + // Start Issue #123 simulated static function RebuildPerkContentCache() { local XComContentManager Content; @@ -1168,4 +1188,240 @@ static function bool GeoscapeReadyForUpdate() StrategyMap != none && StrategyMap.m_eUIState != eSMS_Flight && StrategyMap.Movie.Pres.ScreenStack.GetCurrentScreen() == StrategyMap; -} \ No newline at end of file +} + +// Begin Issue #1540 +/// HL-Docs: feature:AdjustArmorMitigation; issue:1540; tags:tactical +/// This feature allows mods to override the amount of armor mitigation, +/// armor piercing, or minimum/mandatory armor mitigation for an attack +/// on a case-by-case basis. +/// +/// Normally this override would have been implemented as an event, but the implementing dev heard that events in To Hit Chance Calculation logic can cause issues, +/// (see [GetHitChanceEvents](../tactical/GetHitChanceEvents.md)), and resorted to delegates in an attempt to resolve a crash he was unable to explain at the time. +/// +/// This feature does not apply if the attack ignores armor (e.g. bIgnoreArmor is set to +/// true). Additionally, it only applies to attacks using `X2Effect_ApplyWeaponDamage`, +/// or a subclass which has not overridden its `GetDamagePreview()` and `CalculateDamageAmount` +/// functions (except if they have included support for this feature). +/// +/// ## Delegate structure +/// +/// - int WeaponDamage: Full damage dealt by the attack before mitigation, to help determine the results. +/// - out int ArmorMitigation: Amount of armor mitigation available to reduce the attack. When first invoked, this is equal to the target's armor, but may be modified by other delegates running before yours. +/// - out int ArmorPiercing: Amount of armor piercing available to reduce mitigation. When first invoked, this is equal to the attack's armor piercing, but may be modified by other delegates running before yours. +/// - out int MinMitigation: Minimum threshold below which mitigation cannot be reduced. When first invoked, this is 0 (per vanilla behavior), but may be modified by other delegates running before yours. Negative values are currently permitted, but will result in undefined behavior. +/// - EffectAppliedData ApplyEffectParams: Effect context containing information such as the parent ability, source and target units, etc., to help determine the results. +/// - X2Effect_ApplyWeaponDamage Source: The effect itself, to help determine the results. *Do not* attempt to modify it or invoke functions which would have side effects. +/// - optional bool ForMinDamagePreview: If invoked by a damage preview, specifies whether `WeaponDamage` contains the minimum or maximum damage for the attack in the previewed hit context. +/// - optional XComGameState NewGameState: The new game state after the attack resolves. If no game state was provided, then the delegate has been invoked by a damage preview (check ForMinDamagePreview to determine which one). +/// +/// ## How to use (for end users) +/// +/// 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.AddAdjustArmorMitigationCallback(AdjustArmorMitigation); +/// } +/// } +/// +/// // 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 AdjustArmorMitigation(int WeaponDamage, out int ArmorMitigation, out int ArmorPiercing, out int MinMitigation, EffectAppliedData ApplyEffectParams, X2Effect_ApplyWeaponDamage Source, optional bool ForMinDamagePreview, optional XComGameState NewGameState) +/// { +/// // Detect whether it's a damage preview (and if so, which kind). +/// // Then optionally modify any of ArmorMitigation, ArmorPiercing, and MinMitigation. +/// +/// // Return EHLDR_NoInterrupt or EHLDR_InterruptDelegates depending on +/// // if you want to allow other delegates to run after yours +/// // and potentially modify mitigation values further. +/// return EHLDR_NoInterrupt; +///} +/// +/// ## How to use (for effect/UI devs) +/// +/// ### Damage Previews +/// +/// After all damage has been calculated for the hit context being previewed, +/// invoke `class'CHHelpers'.static.GetCDO().TriggerAdjustArmorMitigation(WeaponDamage, ArmorMitigation, ArmorPiercing, MinMitigation, ApplyEffectParams, Source, ForMinDamagePreview)`. +/// +/// You will need to make separate calls for minimum and maximum damage, using +/// **separate** variables for `ArmorMitigation`, `ArmorPiercing`, and `MinMitigation`. +/// Set `ForMinDamagePreview` according to whether it is the minimum or maximum damage. +/// +/// If previewing multiple hit contexts (e.g. normal and critical hits), +/// make sure to pass *total* damage for that hit context into `WeaponDamage` for correct results. +/// For instance, previewing minimum damage for a 3-5 (+2) weapon should be done with +/// a `WeaponDamage` of 5 (i.e. 3+2), **not** 2. +/// +/// If you are not also providing overrides for all UIs that might use your damage preview, +/// then please store the delegate results in your WeaponDamageValue structs, +/// so that downstream UIs will be aware of them and be able to correctly preview them: +/// +/// - ArmorMitigation --> WeaponDamageValue.Spread +/// - ArmorPiercing --> WeaponDamageValue.Pierce +/// - MinMitigation --> WeaponDamageValue.PlusOne +/// +/// ### Damage Calculations +/// +/// After all damage has been calculated for the attack, invoke `class'CHHelpers'.static.GetCDO().TriggerAdjustArmorMitigation(WeaponDamage, ArmorMitigation, ArmorPiercing, MinMitigation, ApplyEffectParams, Source, ForMinDamagePreview, NewGameState)`. +/// +/// The value of `ForMinDamagePreview` does not matter, as the delegates should ignore it +/// whenever a `NewGameState` is provided. However, the Highlander implementation passes +/// `false`, so it is recommended that you do the same for consistency. +/// +/// # Delegate Priority +/// You can optionally specify callback Priority. +///```unrealscript +///CHHelpersObj.AddAdjustArmorMitigationCallback(AdjustArmorMitigation, 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 AddAdjustArmorMitigationCallback(delegate AdjustArmorMitigationFn, optional int Priority = 50) +{ + local AdjustArmorMitigationStruct NewAdjustArmorMitigationCallback; + local int i, PriorityIndex; + local bool bPriorityIndexFound; + + if (AdjustArmorMitigationFn == none) + { + return false; + } + + //Cycle through the array of callbacks backwards + for (i = AdjustArmorMitigationCallbacks.Length - 1; i >= 0; i--) + { + // Do not allow registering the same delegate more than once. + if (AdjustArmorMitigationCallbacks[i].AdjustArmorMitigationFn == AdjustArmorMitigationFn) + { + 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 (AdjustArmorMitigationCallbacks[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; + } + } + + NewAdjustArmorMitigationCallback.Priority = Priority; + NewAdjustArmorMitigationCallback.AdjustArmorMitigationFn = AdjustArmorMitigationFn; + AdjustArmorMitigationCallbacks.InsertItem(PriorityIndex, NewAdjustArmorMitigationCallback); + + return true; +} + +simulated function bool RemoveAdjustArmorMitigationCallback(delegate AdjustArmorMitigationFn) +{ + local int i; + + for (i = AdjustArmorMitigationCallbacks.Length - 1; i >= 0; i--) + { + if (AdjustArmorMitigationCallbacks[i].AdjustArmorMitigationFn == AdjustArmorMitigationFn) + { + AdjustArmorMitigationCallbacks.Remove(i, 1); + return true; + } + } + return false; +} + +// Internal helper function to trigger an AdjustArmorMitigation event. +simulated function TriggerAdjustArmorMitigation(int WeaponDamage, out int ArmorMitigation, out int ArmorPiercing, out int MinMitigation, EffectAppliedData ApplyEffectParams, X2Effect_ApplyWeaponDamage Source, optional bool ForMinDamagePreview, optional XComGameState NewGameState) +{ + local delegate AdjustArmorMitigationFn; + local int i; + + for (i = 0; i < AdjustArmorMitigationCallbacks.Length; i++) + { + AdjustArmorMitigationFn = AdjustArmorMitigationCallbacks[i].AdjustArmorMitigationFn; + + if (AdjustArmorMitigationFn(WeaponDamage, ArmorMitigation, ArmorPiercing, MinMitigation, ApplyEffectParams, Source, ForMinDamagePreview, NewGameState) == EHLDR_InterruptDelegates) + { + break; + } + } +} + +// Helper function to calculate mitigated damage for ShotHUD. +// Only exists because I'm not sure what happens if I pass a literal to an out parameter. +static simulated function CalculateMitigatedDamagePreviewHUD(StateObjectReference Target, WeaponDamageValue MinDamageValue, WeaponDamageValue MaxDamageValue, int AllowsShield, out int MinDamage, out int MaxDamage) +{ + local int Dummy1, Dummy2; + Dummy1 = 0; + Dummy2 = 0; + CalculateMitigatedDamagePreview(Target, MinDamageValue, MaxDamageValue, AllowsShield, MinDamage, MaxDamage, Dummy1, Dummy2); +} + +// Helper function to calculate mitigated damage for ShotWings/ShotHUD. +// Quick reminder: Spread holds mitigation, PlusOne holds minimum mitigation. +static simulated function CalculateMitigatedDamagePreview(StateObjectReference Target, WeaponDamageValue MinDamageValue, WeaponDamageValue MaxDamageValue, int AllowsShield, out int MinDamage, out int MaxDamage, out int NetMitigationMin, out int NetMitigationMax) +{ + local XComGameStateHistory History; + local XComGameState_Unit TargetUnit; + local int TargetShields; + local int MandatoryDamage; + local int GuaranteedDamageMin, GuaranteedDamageMax; + + History = `XCOMHISTORY; + + if (MinDamageValue.Spread == 0 && MaxDamageValue.Spread == 0 || Target.ObjectId == 0) + { + return; + } + + TargetUnit = XComGameState_Unit(History.GetGameStateForObjectId(Target.ObjectID)); + if (TargetUnit == none) + { + return; + } + + if (!class'X2Effect_ApplyWeaponDamage'.default.NO_MINIMUM_DAMAGE) // Account for issue #321 + { + MandatoryDamage = 1; + } + else + { + MandatoryDamage = 0; + } + GuaranteedDamageMin = MandatoryDamage; + GuaranteedDamageMax = MandatoryDamage; + + // If shields are applicable and not subject to mitigation, + // then we are never guaranteed HP damage, but always + // guaranteed shield damage so let's preview that. + if (AllowsShield > 0 && !class'X2Effect_ApplyWeaponDamage'.default.ARMOR_BEFORE_SHIELD) // From issue #743 + { + TargetShields = TargetUnit.GetCurrentStat(eStat_ShieldHP); + } + if (TargetShields > MandatoryDamage) + { + GuaranteedDamageMin = min(MinDamage, TargetShields); + GuaranteedDamageMax = min(MaxDamage, TargetShields); + } + + // This is a bit condensed from the real damage calculator, so to summarize: + // 1. Subtract piercing from mitigation. + // 2. Don't let mitigation drop below the minimum mitigation. + // 3. Now subtract mitigation from damage. + // 4. Make sure damage is *at least* the guaranteed damage for this attack. + MinDamage = max(MinDamage - max(MinDamageValue.Spread - MinDamageValue.Pierce, MinDamageValue.PlusOne), min(GuaranteedDamageMin, MinDamage)); + MaxDamage = max(MaxDamage - max(MaxDamageValue.Spread - MaxDamageValue.Pierce, MaxDamageValue.PlusOne), min(GuaranteedDamageMax, MaxDamage)); + + // Net mitigation for ShotWings: How much damage did the armor actually prevent? + NetMitigationMin = MinDamage - MinDamageValue.Damage; + NetMitigationMax = MaxDamage - MaxDamageValue.Damage; +} +// End Issue #1540 \ No newline at end of file diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/UITacticalHUD_InfoPanel.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/UITacticalHUD_InfoPanel.uc new file mode 100644 index 000000000..4b4cb9ba0 --- /dev/null +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/UITacticalHUD_InfoPanel.uc @@ -0,0 +1,369 @@ +//---------------------------------------------------------------------------- +// ********* FIRAXIS SOURCE CODE ****************** +// FILE: UITacticalHUD_InfoPanel.uc +// AUTHOR: Tronster +// PURPOSE: The information panel that is raised for a particular ability/shot +// type. +//---------------------------------------------------------------------------- +// Copyright (c) 2010 Firaxis Games, Inc. All rights reserved. +//---------------------------------------------------------------------------- + +class UITacticalHUD_InfoPanel extends UIPanel; + //dependson(XGUnit); + +//---------------------------------------------------------------------------- +// CONSTANTS +// +enum eUI_BonusIcon +{ + eUIBonusIcon_Flanked, + eUIBonusIcon_Height +}; + +var private XGUnit m_kUnit; // saved for when the UI needs to update constantly + +//Used for mouse hovering. +var string HitMCPath; +var string CritMCPath; +var string InfoButtonMC; + +//---------------------------------------------------------------------------- +// LOCALIZATION +// +var private UIPanel DamageContainer; + +var localized string m_sMessageShotUnavailable; //Generic message +var localized string m_sNoTargetsHelp; +var localized string m_sNoAmmoHelp; +var localized string m_sOverheatedHelp; +var localized string m_sMoveLimitedHelp; + +var localized string m_sUnavailable; +var localized string m_sFreeAimingTitle; +var localized string m_sShotChanceLabel; +var localized string m_sCritChanceLabel; +var localized string m_sDamageRangeLabel; + +//---------------------------------------------------------------------------- +// METHODS +// +simulated function UITacticalHUD_InfoPanel InitInfoPanel() +{ + InitPanel(); + return self; +} + +simulated function OnMouseEvent(int cmd, array args) +{ + local string target; + + if( cmd == class'UIUtilities_Input'.const.FXS_L_MOUSE_UP) + { + target = args[args.Length-1]; + + switch(target) + { + case "shotButton": + UITacticalHUD(Screen).m_kAbilityHUD.OnAccept(); + break; + case "theInfoButton": + // TODO: Show Enemy Info Immediately + break; + case "theBackButton": + UITacticalHUD(Screen).CancelTargetingAction(); + break; + } + } +} + +simulated function Update() +{ + local bool isValidShot; + local string ShotName, ShotDescription, ShotDamage; + local int HitChance, CritChance, TargetIndex, MinDamage, MaxDamage, AllowsShield; + local ShotBreakdown kBreakdown; + local StateObjectReference Shooter, Target; + local XComGameState_Ability SelectedAbilityState; + local X2AbilityTemplate SelectedAbilityTemplate; + local AvailableAction SelectedUIAction; + local AvailableTarget kTarget; + local XGUnit ActionUnit; + local UITacticalHUD TacticalHUD; + local WeaponDamageValue MinDamageValue, MaxDamageValue; + + TacticalHUD = UITacticalHUD(Screen); + + SelectedUIAction = TacticalHUD.GetSelectedAction(); + + SelectedAbilityState = XComGameState_Ability(`XCOMHISTORY.GetGameStateForObjectID(SelectedUIAction.AbilityObjectRef.ObjectID)); + SelectedAbilityTemplate = SelectedAbilityState.GetMyTemplate(); + ActionUnit = XGUnit(`XCOMHISTORY.GetGameStateForObjectID(SelectedAbilityState.OwnerStateObject.ObjectID).GetVisualizer()); + + TargetIndex = TacticalHUD.GetTargetingMethod().GetTargetIndex(); + if( SelectedUIAction.AvailableTargets.Length > 0 && TargetIndex < SelectedUIAction.AvailableTargets.Length ) + kTarget = SelectedUIAction.AvailableTargets[TargetIndex]; + + //Update L3 help and OK button based on ability. + //********************************************************************************* + if( SelectedUIAction.bFreeAim ) + { + AS_SetButtonVisibility(Movie.IsMouseActive(), false); + isValidShot = true; + } + else if( SelectedUIAction.AvailableTargets.Length == 0 || SelectedUIAction.AvailableTargets[0].PrimaryTarget.ObjectID < 1 ) + { + AS_SetButtonVisibility(Movie.IsMouseActive(), false); + isValidShot = false; + } + else + { + AS_SetButtonVisibility(Movie.IsMouseActive(), Movie.IsMouseActive()); + isValidShot = true; + } + + // Disable Shot Button if we don't have a valid target. + AS_SetShotButtonDisabled(!isValidShot); + + //Set shot name / help text + //********************************************************************************* + if( SelectedUIAction.bFreeAim ) + ShotName = m_sFreeAimingTitle $":" @ SelectedAbilityState.GetMyFriendlyName(); + else + ShotName = SelectedAbilityState.GetMyFriendlyName(); + + if( SelectedUIAction.AvailableCode == 'AA_Success' ) + { + ShotDescription = SelectedAbilityState.GetMyHelpText(); + if(ShotDescription == "") ShotDescription = "Missing 'LocHelpText' from ability template."; + } + else + { + ShotDescription = class'X2AbilityTemplateManager'.static.GetDisplayStringForAvailabilityCode(SelectedUIAction.AvailableCode); + } + + AS_SetShotInfo( ShotName, ShotDescription ); + + ResetDamageBreakdown(); + + SelectedAbilityState.GetDamagePreview(kTarget.PrimaryTarget, MinDamageValue, MaxDamageValue, AllowsShield); + MinDamage = MinDamageValue.Damage; + MaxDamage = MaxDamageValue.Damage; + + if (MinDamage > 0 && MaxDamage > 0) + { + // Begin Issue #1540 + // Adjust damage preview for net armor mitigation + // The order of operations here does mean the UI might preview 0 damage if NO_MINIMUM_DAMAGE + // is enabled, but I actually feel that's a good thing here, to call out + // that the attack's original damage has been completely nullified by armor. + if (class'CHHelpers'.default.PREVIEW_ARMOR_MITIGATION) + { + // We don't show net mitigation anywhere, so might as well pass nothing. + class'CHHelpers'.static.CalculateMitigatedDamagePreviewHUD(kTarget.PrimaryTarget, MinDamageValue, MaxDamageValue, AllowsShield, MinDamage, MaxDamage); + } + // End Issue #1540 + + if(MinDamage == MaxDamage) + ShotDamage = String(MinDamage); + else + ShotDamage = MinDamage $ "-" $ MaxDamage; + + AddDamage(class'UIUtilities_Text'.static.GetColoredText(ShotDamage, eUIState_Good, 36), true); + } + + //Set up percent to hit / crit values + //********************************************************************************* + + if( SelectedAbilityTemplate.AbilityToHitCalc != none && SelectedAbilityState.iCooldown == 0 ) + { + Shooter = SelectedAbilityState.OwnerStateObject; + Target = kTarget.PrimaryTarget; + + SelectedAbilityState.LookupShotBreakdown(Shooter, Target, SelectedAbilityState.GetReference(), kBreakdown); + HitChance = min(((kBreakdown.bIsMultishot ) ? kBreakdown.MultiShotHitChance : kBreakdown.FinalHitChance ), 100); + CritChance = kBreakdown.ResultTable[eHit_Crit]; + + if( HitChance > -1 && !kBreakdown.HideShotBreakdown) + { + AS_SetShotChance(class'UIUtilities_Text'.static.GetColoredText(m_sShotChanceLabel, eUIState_Header), HitChance ); + AS_SetCriticalChance(class'UIUtilities_Text'.static.GetColoredText(kBreakdown.SpecialCritLabel == "" ? m_sCritChanceLabel : kBreakdown.SpecialCritLabel, eUIState_Header), CritChance ); + TacticalHUD.SetReticleAimPercentages(float(HitChance) / 100.0f, float(CritChance) / 100.0f); + } + else + { + AS_SetShotChance("", -1 ); + AS_SetCriticalChance("", -1 ); + TacticalHUD.SetReticleAimPercentages(-1, -1); + } + } + else + { + AS_SetShotChance( "", -1 ); + } + TacticalHUD.m_kShotInfoWings.Show(); + + //@TODO - jbouscher - ranges need to be implemented in a template friendly way. + //Hide any current range meshes before we evaluate their visibility state + if( !ActionUnit.GetPawn().RangeIndicator.HiddenGame ) + { + ActionUnit.RemoveRanges(); + } +} + +simulated function ResetDamageBreakdown(optional bool RemoveOnly) +{ + local UIText DamageLabel; + + if(DamageContainer != none) + { + DamageContainer.Remove(); + if(RemoveOnly) return; + } + + // Spawn container on the screen so anchoring works + DamageContainer = Spawn(class'UIPanel', self).InitPanel(); + DamageContainer.AnchorBottomCenter().SetY(-118); + DamageContainer.bAnimateOnInit = false; + DamageContainer.Hide(); // start off hidden until text size is realized on all children + + DamageLabel = Spawn(class'UIText', DamageContainer).InitText(, m_sDamageRangeLabel,, RepositionDamageContainer); + DamageLabel.bAnimateOnInit = false; + DamageLabel.SetY(12); +} + +simulated private function AddDamage(string label, optional bool isLastOne) +{ + local UIPanel Divider; + local UIText Text; + + Text = Spawn(class'UIText', DamageContainer); + Text.InitText(, label, true, RepositionDamageContainer).SetHeight(50); + Text.bAnimateOnInit = false; + + if(!isLastOne) + { + Divider = Spawn(class'UIPanel', DamageContainer); + Divider.InitPanel(, class'UIUtilities_Controls'.const.MC_GenericPixel).SetSize(2, 40); + Divider.bAnimateOnInit = false; + } +} + +// Required because we need to take text size into account +simulated function RepositionDamageContainer() +{ + local int i, NextX; + local UIPanel Control; + local UIText Text; + local bool bAllTextRealized; + + // Do nothing if we just added the label and nothing else + if(DamageContainer.NumChildren() == 1) + return; + + NextX = 0; + bAllTextRealized = true; + for(i = 0; i < DamageContainer.Children.Length; ++i) + { + Control = DamageContainer.GetChildAt(i); + Control.SetX(NextX); + NextX += 10; + + Text = UIText(Control); + if( Text != none ) + { + if( Text.TextSizeRealized ) + NextX += Text.Width; + else + bAllTextRealized = false; + } + } + + if( bAllTextRealized ) + { + DamageContainer.SetX(NextX * -0.5); + DamageContainer.Show(); + DamageContainer.AnimateIn(0); + } +} + +simulated private function string FormatModifiers( array arrLabels, array arrValues, optional string sValuePrefix = "", optional string sValueSuffix = "" ) +{ + local int i; + local string displayString, strLabel, strValue; + + displayString = ""; + + for( i=0; i < arrLabels.length; i++) + { + strLabel = arrLabels[i]; + strValue= string(arrValues[i]); + + if( displayString == "" ) + displayString = strLabel @ sValuePrefix $ strValue $ sValueSuffix; + else + displayString = displayString $"
" $ strLabel @ sValuePrefix $ strValue $ sValueSuffix; + } + + return displayString; +} + +//============================================================================== +// FLASH INTERFACE: +//============================================================================== + +simulated private function AS_SetShotInfo( string shotName, string helpText ) +{ + Movie.ActionScriptVoid(MCPath$".SetShotInfo"); +} +simulated private function AS_SetWeaponName( string weaponName ) +{ + Movie.ActionScriptVoid(MCPath$".SetWeaponName"); +} +simulated private function AS_SetCriticalChance( string label, float val ) +{ + Movie.ActionScriptVoid(MCPath$".SetCriticalChance"); +} +simulated private function AS_SetShotChance( string label, float val ) +{ + Movie.ActionScriptVoid(MCPath$".SetShotChance"); +} +simulated function AS_SetButtonVisibility(bool BackButtonVisible, bool GermanModeButtonVisible ) +{ + Movie.ActionScriptVoid(MCPath$".SetButtonVisibility"); +} +simulated private function AS_SetShotButtonDisabled( bool isDisabled ) +{ + Movie.ActionScriptVoid(MCPath$".SetShotButtonDisabled"); +} + +//============================================================================== +// TOOLTIP PATHS: +//============================================================================== + +public function string GetHitMCPath() +{ + return MCPath $ "." $ HitMCPath; +} +public function string GetCritMCPath() +{ + return MCPath $ "." $ CritMCPath; +} +public function string GetInfoButtonMCPath() +{ + return MCPath $ "." $ InfoButtonMC; +} + +//============================================================================== +// DEFAULTS: +//============================================================================== + +defaultproperties +{ + MCName = "theInfoBox"; + HitMCPath = "infoMC.statsHit"; + CritMCPath = "infoMC.statsCrit"; + InfoButtonMC = "infoMC.theInfoButton"; + bAnimateOnInit = false; + + Width = 680; +} \ No newline at end of file diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/UITacticalHUD_ShotHUD.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/UITacticalHUD_ShotHUD.uc new file mode 100644 index 000000000..25375aa8c --- /dev/null +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/UITacticalHUD_ShotHUD.uc @@ -0,0 +1,552 @@ +//---------------------------------------------------------------------------- +// ********* FIRAXIS SOURCE CODE ****************** +// FILE: UITacticalHUD_ShotHUD.uc +// AUTHOR: Tronster +// PURPOSE: The information panel that is raised for a particular ability/shot +// type. +//---------------------------------------------------------------------------- +// Copyright (c) 2016 Firaxis Games, Inc. All rights reserved. +//---------------------------------------------------------------------------- + +class UITacticalHUD_ShotHUD extends UIPanel; + +//---------------------------------------------------------------------------- +// CONSTANTS +// +enum eUI_BonusIcon +{ + eUIBonusIcon_Flanked, + eUIBonusIcon_Height +}; + +//Used for mouse hovering. +var string HitMCPath; +var string CritMCPath; +var string ShotButtonPath; + +//---------------------------------------------------------------------------- +// LOCALIZATION +// +var UIPanel DamageContainer; +var bool bIsShiney; + +var localized string m_sMessageShotUnavailable; //Generic message +var localized string m_sNoTargetsHelp; +var localized string m_sNoAmmoHelp; +var localized string m_sOverheatedHelp; +var localized string m_sMoveLimitedHelp; + +var localized string m_sUnavailable; +var localized string m_sShotChanceLabel; +var localized string m_sCritChanceLabel; +var localized string m_sDamageRangeLabel; + +var localized string m_sEndTurnTooltip; +var localized string m_sRevealedTooltip; + + +var localized string m_sRevealText; +var localized string m_sEndTurnText; +var int TooltipEndTurnID; +var int TooltipRevealID; + +//---------------------------------------------------------------------------- +// METHODS +// +simulated function UITacticalHUD_ShotHUD InitShotHUD() +{ + InitPanel(); + + RefreshTooltips(); + MC.BeginFunctionOp("SetIndicatorText"); + MC.QueueString(m_sRevealText); + MC.QueueString(class'UIUtilities_Text'.static.AlignRight(m_sEndTurnText)); + MC.EndOp(); + return self; +} + +function AnimateShotIn() +{ + MC.FunctionVoid("AnimateShotIn"); +} + +function AnimateShotOut() +{ + MC.FunctionVoid("AnimateShotOut"); +} + +function RefreshTooltips() +{ + //ID check is to prevent unnecessary repeated calls to the tooltip manageer + + if( TooltipEndTurnID == -1 && UITacticalHUD(Owner).IsMenuRaised() ) + { + TooltipEndTurnID = AddTextTooltip("endTurnMC", m_sEndTurnTooltip, Class'UIUtilities'.const.ANCHOR_MIDDLE_LEFT); + TooltipRevealID = AddTextTooltip("revealedMC", m_sRevealedTooltip, Class'UIUtilities'.const.ANCHOR_MIDDLE_RIGHT); + } + + if( TooltipEndTurnID != -1 && !UITacticalHUD(Owner).IsMenuRaised() ) + { + Screen.Movie.Pres.m_kTooltipMgr.RemoveTooltipByID(TooltipEndTurnID); + Screen.Movie.Pres.m_kTooltipMgr.RemoveTooltipByID(TooltipRevealID); + + TooltipEndTurnID = -1; + TooltipRevealID = -1; + } +} + +simulated function AddTooltip(string childPath, string tooltipText, int newAnchor); //DEPRECATED +simulated function int AddTextTooltip(string childPath, string tooltipText, int newAnchor) +{ + local UITextTooltip Tooltip; + + Tooltip = Screen.Spawn(class'UITextTooltip', Screen.Movie.Pres.m_kTooltipMgr); + Tooltip.InitTextTooltip(); + Tooltip.bFollowMouse = true; + Tooltip.bRelativeLocation = false; + Tooltip.tDelay = 0.0; //Instant + Tooltip.SetAnchor( newAnchor ); + Tooltip.sBody = tooltipText; + Tooltip.targetPath = string(MCPath) $ "." $ childPath; + Tooltip.ID = Screen.Movie.Pres.m_kTooltipMgr.AddPreformedTooltip( Tooltip ); + return Tooltip.ID; +} + +simulated function OnMouseEvent(int cmd, array args) +{ + local string target; + + target = args[args.Length-1]; + + switch(cmd) + { + case class'UIUtilities_Input'.const.FXS_L_MOUSE_IN: + case class'UIUtilities_Input'.const.FXS_L_MOUSE_OVER: + case class'UIUtilities_Input'.const.FXS_L_MOUSE_DRAG_OVER: + switch(target) + { + case HitMCPath: UITacticalHUD(Screen).m_kShotInfoWings.LeftWingArea.OnReceiveFocus(); break; + case CritMCPath: UITacticalHUD(Screen).m_kShotInfoWings.RightWingArea.OnReceiveFocus(); break; + } + break; + case class'UIUtilities_Input'.const.FXS_L_MOUSE_OUT: + case class'UIUtilities_Input'.const.FXS_L_MOUSE_DRAG_OVER: + case class'UIUtilities_Input'.const.FXS_L_MOUSE_RELEASE_OUTSIDE: + switch(target) + { + case HitMCPath: UITacticalHUD(Screen).m_kShotInfoWings.LeftWingArea.OnLoseFocus(); break; + case CritMCPath: UITacticalHUD(Screen).m_kShotInfoWings.RightWingArea.OnLoseFocus(); break; + } + break; + case class'UIUtilities_Input'.const.FXS_L_MOUSE_UP: + switch(target) + { + case ShotButtonPath: UITacticalHUD(Screen).m_kAbilityHUD.OnAccept(); break; + case HitMCPath: UITacticalHUD(Screen).m_kShotInfoWings.LeftWingMouseEvent(none, cmd); break; + case CritMCPath: UITacticalHUD(Screen).m_kShotInfoWings.RightWingMouseEvent(none, cmd); break; + } + break; + } +} + +simulated function Update() +{ + local bool isValidShot; + local string ShotName, ShotDescription, ShotDamage; + local int HitChance, CritChance, TargetIndex, MinDamage, MaxDamage, AllowsShield; + local ShotBreakdown kBreakdown; + local StateObjectReference Shooter, Target, EmptyRef; + local XComGameState_Ability SelectedAbilityState; + local X2AbilityTemplate SelectedAbilityTemplate; + local AvailableAction SelectedUIAction; + local AvailableTarget kTarget; + local XGUnit ActionUnit; + local UITacticalHUD TacticalHUD; + local UIUnitFlag UnitFlag; + local WeaponDamageValue MinDamageValue, MaxDamageValue; + local X2TargetingMethod TargetingMethod; + local bool WillBreakConcealment, WillEndTurn; + local XComGameStateHistory History; + + local bool NotSelfTargeting; // Issue #1540 + + TacticalHUD = UITacticalHUD(Screen); + + SelectedUIAction = TacticalHUD.GetSelectedAction(); + if (SelectedUIAction.AbilityObjectRef.ObjectID > 0) //If we do not have a valid action selected, ignore this update request + { + History = `XCOMHISTORY; + + SelectedAbilityState = XComGameState_Ability(History.GetGameStateForObjectID(SelectedUIAction.AbilityObjectRef.ObjectID)); + SelectedAbilityTemplate = SelectedAbilityState.GetMyTemplate(); + ActionUnit = XGUnit(History.GetGameStateForObjectID(SelectedAbilityState.OwnerStateObject.ObjectID).GetVisualizer()); + TargetingMethod = TacticalHUD.GetTargetingMethod(); + if( TargetingMethod != None ) + { + TargetIndex = TargetingMethod.GetTargetIndex(); + if( SelectedUIAction.AvailableTargets.Length > 0 && TargetIndex < SelectedUIAction.AvailableTargets.Length ) + kTarget = SelectedUIAction.AvailableTargets[TargetIndex]; + } + + //Update L3 help and OK button based on ability. + //********************************************************************************* + if (SelectedUIAction.bFreeAim) + { + AS_SetButtonVisibility(Movie.IsMouseActive(), false); + isValidShot = true; + } + else if (SelectedUIAction.AvailableTargets.Length == 0 || SelectedUIAction.AvailableTargets[0].PrimaryTarget.ObjectID < 1) + { + AS_SetButtonVisibility(Movie.IsMouseActive(), false); + isValidShot = false; + } + else + { + AS_SetButtonVisibility(Movie.IsMouseActive(), Movie.IsMouseActive()); + isValidShot = true; + } + + //Set shot name / help text + //********************************************************************************* + ShotName = SelectedAbilityState.GetMyFriendlyName(kTarget.PrimaryTarget); + + if (SelectedUIAction.AvailableCode == 'AA_Success') + { + ShotDescription = SelectedAbilityState.GetMyHelpText(); + if (ShotDescription == "") ShotDescription = "Missing 'LocHelpText' from ability template."; + } + else + { + if (SelectedUIAction.AvailableCode == 'AA_AbilityUnavailable') + { + ShotDescription = SelectedAbilityState.GetMyHelpText(); + } + else + { + ShotDescription = class'X2AbilityTemplateManager'.static.GetDisplayStringForAvailabilityCode(SelectedUIAction.AvailableCode); + } + + if (ShotDescription == "") + { + ShotDescription = "Missing 'LocHelpText' from ability template."; + } + } + + + WillBreakConcealment = SelectedAbilityState.MayBreakConcealmentOnActivation(kTarget.PrimaryTarget.ObjectID); + WillEndTurn = SelectedAbilityState.WillEndTurn(); + + AS_SetShotInfo(ShotName, ShotDescription, WillBreakConcealment, WillEndTurn); + + // Disable Shot Button if we don't have a valid target. + AS_SetShotButtonDisabled(!isValidShot); + + ResetDamageBreakdown(); + + // In the rare case that this ability is self-targeting, but has a multi-target effect on units around it, + // look at the damage preview, just not against the target (self). + if( SelectedAbilityTemplate.AbilityTargetStyle.IsA('X2AbilityTarget_Self') + && SelectedAbilityTemplate.AbilityMultiTargetStyle != none + && SelectedAbilityTemplate.AbilityMultiTargetEffects.Length > 0 ) + { + SelectedAbilityState.GetDamagePreview(EmptyRef, MinDamageValue, MaxDamageValue, AllowsShield); + } + else + { + SelectedAbilityState.GetDamagePreview(kTarget.PrimaryTarget, MinDamageValue, MaxDamageValue, AllowsShield); + // Begin Issue #1540 + NotSelfTargeting = true; + // End Issue #1540 + } + MinDamage = MinDamageValue.Damage; + MaxDamage = MaxDamageValue.Damage; + + if (MinDamage > 0 && MaxDamage > 0) + { + // Begin Issue #1540 + // Adjust damage preview for net armor mitigation + // The order of operations here does mean the UI might preview 0 damage if NO_MINIMUM_DAMAGE + // is enabled, but I actually feel that's a good thing here, to call out + // that the attack's original damage has been completely nullified by armor. + if (NotSelfTargeting && class'CHHelpers'.default.PREVIEW_ARMOR_MITIGATION) + { + // We don't show net mitigation anywhere, so might as well pass nothing. + class'CHHelpers'.static.CalculateMitigatedDamagePreviewHUD(kTarget.PrimaryTarget, MinDamageValue, MaxDamageValue, AllowsShield, MinDamage, MaxDamage); + } + // End Issue #1540 + + if (MinDamage == MaxDamage) + ShotDamage = String(MinDamage); + else + ShotDamage = MinDamage $ "-" $ MaxDamage; + + if( MinDamageValue.BonusDamageInfo.Length > 0 || MaxDamageValue.BonusDamageInfo.Length > 0 ) + { + AddDamage(class'UIUtilities_Text'.static.GetColoredText(ShotDamage, eUIState_Warning2, 38), true); + } + else + { + AddDamage(class'UIUtilities_Text'.static.GetColoredText(ShotDamage, eUIState_Good, 36), true); + } + } + + //Set up percent to hit / crit values + //********************************************************************************* + + if (SelectedAbilityTemplate.AbilityToHitCalc != none && SelectedAbilityState.iCooldown == 0) + { + Shooter = SelectedAbilityState.OwnerStateObject; + Target = kTarget.PrimaryTarget; + + SelectedAbilityState.LookupShotBreakdown(Shooter, Target, SelectedAbilityState.GetReference(), kBreakdown); + HitChance = Clamp(((kBreakdown.bIsMultishot) ? kBreakdown.MultiShotHitChance : kBreakdown.FinalHitChance), 0, 100); + CritChance = kBreakdown.ResultTable[eHit_Crit]; + + if (HitChance > -1 && !kBreakdown.HideShotBreakdown) + { + AS_SetShotChance(class'UIUtilities_Text'.static.GetColoredText(m_sShotChanceLabel, eUIState_Header), HitChance); + AS_SetCriticalChance(class'UIUtilities_Text'.static.GetColoredText(kBreakdown.SpecialCritLabel == "" ? m_sCritChanceLabel : kBreakdown.SpecialCritLabel, eUIState_Header), CritChance); + TacticalHUD.SetReticleAimPercentages(float(HitChance) / 100.0f, float(CritChance) / 100.0f); + } + else + { + AS_SetShotChance("", -1); + AS_SetCriticalChance("", -1); + TacticalHUD.SetReticleAimPercentages(-1, -1); + } + } + else + { + AS_SetShotChance("", -1); + AS_SetCriticalChance("", -1); + } + TacticalHUD.m_kShotInfoWings.Show(); + + //Show preview points, must be negative + UnitFlag = XComPresentationLayer(Owner.Owner).m_kUnitFlagManager.GetFlagForObjectID(Target.ObjectID); + if( UnitFlag != none ) + { + XComPresentationLayer(Owner.Owner).m_kUnitFlagManager.SetAbilityDamagePreview(UnitFlag, SelectedAbilityState, kTarget.PrimaryTarget); + } + + //@TODO - jbouscher - ranges need to be implemented in a template friendly way. + //Hide any current range meshes before we evaluate their visibility state + if (!ActionUnit.GetPawn().RangeIndicator.HiddenGame) + { + ActionUnit.RemoveRanges(); + } + } + + if (`REPLAY.bInTutorial) + { + if (SelectedAbilityTemplate != none && `TUTORIAL.IsNextAbility(SelectedAbilityTemplate.DataName) && `TUTORIAL.IsTarget(Target.ObjectID)) + { + ShowShine(); + } + else + { + HideShine(); + } + } + RefreshTooltips(); +} + +simulated function LowerShotHUD() +{ + ResetDamageBreakdown(true); + RefreshTooltips(); +} + +simulated function ResetDamageBreakdown(optional bool RemoveOnly) +{ + local UIText DamageLabel; + + if(DamageContainer != none) + { + DamageContainer.Remove(); + if(RemoveOnly) return; + } + + // Spawn container on the screen so anchoring works + DamageContainer = Spawn(class'UIPanel', self).InitPanel(); + if (UITacticalHUD(Owner).m_kAbilityHUD.ActiveAbilities < 16) + { + DamageContainer.AnchorBottomCenter().SetY(-113); + } + else + { + DamageContainer.AnchorBottomCenter().SetY(-145); + } + DamageContainer.bAnimateOnInit = false; + DamageContainer.Hide(); // start off hidden until text size is realized on all children + + DamageLabel = Spawn(class'UIText', DamageContainer).InitText(, m_sDamageRangeLabel,, RepositionDamageContainer); + DamageLabel.bAnimateOnInit = false; + DamageLabel.SetY(12); +} + +simulated function AddDamage(string label, optional bool isLastOne) +{ + local UIPanel Divider; + local UIText Text; + + Text = Spawn(class'UIText', DamageContainer); + Text.InitText(, label, true, RepositionDamageContainer).SetHeight(50); + Text.bAnimateOnInit = false; + + if(!isLastOne) + { + Divider = Spawn(class'UIPanel', DamageContainer); + Divider.InitPanel(, class'UIUtilities_Controls'.const.MC_GenericPixel).SetSize(2, 40); + Divider.bAnimateOnInit = false; + } +} + +// Required because we need to take text size into account +simulated function RepositionDamageContainer() +{ + local int i, NextX; + local UIPanel Control; + local UIText Text; + local bool bAllTextRealized; + + // Do nothing if we just added the label and nothing else + if(DamageContainer.NumChildren() == 1) + return; + + NextX = 0; + bAllTextRealized = true; + for(i = 0; i < DamageContainer.Children.Length; ++i) + { + Control = DamageContainer.GetChildAt(i); + Control.SetX(NextX); + NextX += 10; + + Text = UIText(Control); + if( Text != none ) + { + if( Text.TextSizeRealized ) + NextX += Text.Width; + else + bAllTextRealized = false; + } + } + + if( bAllTextRealized ) + { + DamageContainer.SetX(NextX * -0.5); + DamageContainer.Show(); + DamageContainer.AnimateIn(0); + } +} + +simulated function string FormatModifiers( array arrLabels, array arrValues, optional string sValuePrefix = "", optional string sValueSuffix = "" ) +{ + local int i; + local string displayString, strLabel, strValue; + + displayString = ""; + + for( i=0; i < arrLabels.length; i++) + { + strLabel = arrLabels[i]; + strValue= string(arrValues[i]); + + if( displayString == "" ) + displayString = strLabel @ sValuePrefix $ strValue $ sValueSuffix; + else + displayString = displayString $"
" $ strLabel @ sValuePrefix $ strValue $ sValueSuffix; + } + + return displayString; +} + +//============================================================================== +// FLASH INTERFACE: +//============================================================================== + +simulated function AS_SetShotInfo(string shotName, string helpText, bool WillBreakConcealment, bool WillEndTurn) +{ + MC.BeginFunctionOp("SetShotInfo"); + MC.QueueString(shotName); + MC.QueueString(helpText); + MC.QueueBoolean(WillBreakConcealment); + MC.QueueBoolean(WillEndTurn); + MC.EndOp(); +} +simulated function AS_SetCriticalChance( string label, float val ) +{ + MC.BeginFunctionOp("SetCriticalChance"); + MC.QueueString(label); + MC.QueueNumber(val); + MC.EndOp(); +} +simulated function AS_SetShotChance( string label, float val ) +{ + MC.BeginFunctionOp("SetShotChance"); + MC.QueueString(label); + MC.QueueNumber(val); + MC.EndOp(); +} +simulated function AS_SetButtonVisibility(bool BackButtonVisible, bool GermanModeButtonVisible ) +{ + MC.BeginFunctionOp("SetButtonVisibility"); + MC.QueueBoolean(BackButtonVisible); + MC.QueueBoolean(GermanModeButtonVisible); + MC.EndOp(); +} +simulated function AS_SetShotButtonDisabled( bool isDisabled ) +{ + MC.FunctionBool("SetShotButtonDisabled", isDisabled); +} + +simulated function ShowShine() +{ + if( !bIsShiney ) + { + bIsShiney = true; + MC.FunctionVoid("ShowShine"); + + AS_SetShotButtonDisabled(false); + } +} + +simulated function HideShine() +{ + if( bIsShiney ) + { + bIsShiney = false; + MC.FunctionVoid("HideShine"); + } + + AS_SetShotButtonDisabled(true); +} + + +//============================================================================== +// TOOLTIP PATHS: +//============================================================================== + +public function string GetHitMCPath() +{ + return MCPath $ "." $ HitMCPath; +} +public function string GetCritMCPath() +{ + return MCPath $ "." $ CritMCPath; +} + +//============================================================================== +// DEFAULTS: +//============================================================================== + +defaultproperties +{ + MCName = "shotHUD"; + HitMCPath = "statsHit"; + CritMCPath = "statsCrit"; + ShotButtonPath = "shotButton"; + bAnimateOnInit = false; + + Width = 680; +} \ No newline at end of file diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/UITacticalHUD_ShotWings.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/UITacticalHUD_ShotWings.uc new file mode 100644 index 000000000..396e1418a --- /dev/null +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/UITacticalHUD_ShotWings.uc @@ -0,0 +1,595 @@ +//--------------------------------------------------------------------------------------- + // FILE: UITacticalHUD_ShotWings.uc + // AUTHOR: Brit Steiner -- 11/2014 + // PURPOSE: Pop-out wings for the player's current shot stats used in the TacticalHUD. + //--------------------------------------------------------------------------------------- + // Copyright (c) 2016 Firaxis Games, Inc. All rights reserved. + //--------------------------------------------------------------------------------------- + +class UITacticalHUD_ShotWings extends UIPanel; + +var public UIPanel LeftWingArea; +var public UIButton LeftWingButton; +var public UIText HitLabel; +var public UIText HitPercent; +var public UIMask HitMask; +var public UIPanel HitBodyArea; +var public UIStatList HitStatList; + +var public UIPanel RightWingArea; +var public UIButton RightWingButton; + +var public UIText DamageLabel; +var public UIText DamagePercent; +var public UIMask DamageMask; +var public UIPanel DamageBodyArea; +var public UIStatList DamageStatList; + +var public UIText CritLabel; +var public UIText CritPercent; +var public UIMask CritMask; +var public UIPanel CritBodyArea; +var public UIStatList CritStatList; + +var public UIScrollingText TargetName; + +var bool bLeftWingOpen; +var bool bRightWingOpen; +var bool bLeftWingWasOpen; +var bool bRightWingWasOpen; + +simulated function UITacticalHUD_ShotWings InitShotWings(optional name InitName, optional name InitLibID) +{ + local int TmpInt, ArrowButtonWidth, ArrowButtonOffset, StatsWidth, StatsOffset; + local UIPanel HitLine, CritLine, DamageLine, HitBG, CritBG, LeftContainer, RightContainer; + + InitPanel(InitName, InitLibID); + + StatsWidth = 188; + StatsOffset = 24; + ArrowButtonWidth = 16; + ArrowButtonOffset = 4; + + // ---------- + // The wings are actually contained within the ShotHUD so they animate in and anchor properly -sbatista + LeftWingArea = Spawn(class'UIPanel', UITacticalHUD(Screen).m_kShotHUD); + LeftWingArea.bAnimateOnInit = false; + LeftWingArea.InitPanel('leftWing'); + + HitBG = Spawn(class'UIPanel', LeftWingArea).InitPanel('wingBG'); + HitBG.bAnimateOnInit = false; + HitBG.ProcessMouseEvents(LeftWingMouseEvent); + + LeftContainer = Spawn(class'UIPanel', LeftWingArea); + LeftContainer.bAnimateOnInit = false; + LeftContainer.InitPanel('wingContainer'); + + LeftWingButton = Spawn(class'UIButton', LeftContainer); + LeftWingButton.LibID = 'X2DrawerButton'; + LeftWingButton.bAnimateOnInit = false; + LeftWingButton.InitButton(,,OnWingButtonClicked).SetPosition(ArrowButtonOffset, (height - 26) * 0.5); + LeftWingButton.MC.ChildFunctionString("bg.arrow", "gotoAndStop", bLeftWingOpen ? "right" : "left"); + + HitPercent = Spawn(class'UIText', LeftContainer); + HitPercent.bAnimateOnInit = false; + HitPercent.InitText('HitPercent'); + HitPercent.SetWidth(StatsWidth); + HitPercent.SetPosition(StatsOffset, 0); + HitLabel = Spawn(class'UIText', LeftContainer); + HitLabel.bAnimateOnInit = false; + HitLabel.InitText('HitLabel'); + HitLabel.SetPosition(StatsOffset, 0); + + HitLine = Spawn(class'UIPanel', LeftContainer); + HitLine.bAnimateOnInit = false; + HitLine.InitPanel('HitHeaderLine', class'UIUtilities_Controls'.const.MC_GenericPixel); + HitLine.SetPosition(StatsOffset, 24); + HitLine.SetSize(StatsWidth, 2); + HitLine.SetAlpha(50); + + TmpInt = HitLine.Y + 4; + + HitBodyArea = Spawn(class'UIPanel', LeftContainer); + HitBodyArea.bAnimateOnInit = false; + HitBodyArea.InitPanel('HitBodyArea').SetPosition(HitLine.X, TmpInt); + HitBodyArea.width = StatsWidth; + HitBodyArea.height = height - TmpInt; + + HitMask = Spawn(class'UIMask', LeftContainer).InitMask(, HitBodyArea); + HitMask.SetPosition(HitBodyArea.X, HitBodyArea.Y); + HitMask.SetSize(StatsWidth, Height - 10); + + HitStatList = Spawn(class'UIStatList', HitBodyArea); + HitStatList.bAnimateOnInit = false; + HitStatList.InitStatList('StatListLeft',,,, HitBodyArea.Width, HitBodyArea.Height, 0, 0); + + // ----------- + // The wings are actually contained within the ShotHUD so they animate in and anchor properly -sbatista + RightWingArea = Spawn(class'UIPanel', UITacticalHUD(Screen).m_kShotHUD); + RightWingArea.bAnimateOnInit = false; + RightWingArea.InitPanel('rightWing'); + + CritBG = Spawn(class'UIPanel', RightWingArea); + CritBG.bAnimateOnInit = false; + CritBG.InitPanel('wingBG'); + CritBG.ProcessMouseEvents(RightWingMouseEvent); + + RightContainer = Spawn(class'UIPanel', RightWingArea); + RightContainer.bAnimateOnInit = false; + RightContainer.InitPanel('wingContainer'); + + RightWingButton = Spawn(class'UIButton', RightContainer); + RightWingButton.LibID = 'X2DrawerButton'; + RightWingButton.bAnimateOnInit = false; + RightWingButton.InitButton(,,OnWingButtonClicked).SetPosition(-ArrowButtonWidth - ArrowButtonOffset, (height - 26) * 0.5); + RightWingButton.MC.FunctionString("gotoAndStop", "right"); + RightWingButton.MC.ChildFunctionString("bg.arrow", "gotoAndStop", bRightWingOpen ? "right" : "left"); + + + DamagePercent = Spawn(class'UIText', RightContainer); + DamagePercent.bAnimateOnInit = false; + DamagePercent.InitText('DamagePercent'); + DamagePercent.SetWidth(StatsWidth); + DamagePercent.SetPosition(-StatsWidth - StatsOffset, 0); + DamageLabel = Spawn(class'UIText', RightContainer); + DamageLabel.bAnimateOnInit = false; + DamageLabel.InitText('DamageLabel'); + DamageLabel.SetWidth(StatsWidth); + DamageLabel.SetPosition(-StatsWidth - StatsOffset, 0); + + DamageLine = Spawn(class'UIPanel', RightContainer); + DamageLine.bAnimateOnInit = false; + DamageLine.InitPanel('DamageHeaderLine', class'UIUtilities_Controls'.const.MC_GenericPixel); + DamageLine.SetSize(StatsWidth, 2); + DamageLine.SetPosition(-StatsWidth - StatsOffset, 24); + DamageLine.SetAlpha(50); + + TmpInt = DamageLine.Y + 4; + + DamageBodyArea = Spawn(class'UIPanel', RightContainer); + DamageBodyArea.bAnimateOnInit = false; + DamageBodyArea.InitPanel('DamageBodyArea').SetPosition(DamageLine.X, TmpInt); + DamageBodyArea.width = StatsWidth; + DamageBodyArea.height = height / 2 - TmpInt; + + DamageMask = Spawn(class'UIMask', RightContainer).InitMask(, DamageBodyArea); + DamageMask.SetPosition(DamageBodyArea.X, DamageBodyArea.Y); + DamageMask.SetSize(StatsWidth, DamageBodyArea.height); + + DamageStatList = Spawn(class'UIStatList', DamageBodyArea); + DamageStatList.bAnimateOnInit = false; + DamageStatList.InitStatList('DamageStatList', , , , DamageBodyArea.Width, DamageBodyArea.Height, 0, 0); + + + TmpInt = height / 2 + 4; + + CritPercent = Spawn(class'UIText', RightContainer); + CritPercent.bAnimateOnInit = false; + CritPercent.InitText('CritPercent'); + CritPercent.SetWidth(StatsWidth); + CritPercent.SetPosition(-StatsWidth - StatsOffset, TmpInt); + CritLabel = Spawn(class'UIText', RightContainer); + CritLabel.bAnimateOnInit = false; + CritLabel.InitText('CritLabel'); + CritLabel.SetWidth(StatsWidth); + CritLabel.SetPosition(-StatsWidth - StatsOffset, TmpInt); + + CritLine = Spawn(class'UIPanel', RightContainer); + CritLine.bAnimateOnInit = false; + CritLine.InitPanel('CritHeaderLine', class'UIUtilities_Controls'.const.MC_GenericPixel); + CritLine.SetSize(StatsWidth, 2); + CritLine.SetPosition(-StatsWidth - StatsOffset, TmpInt + 24); + CritLine.SetAlpha(50); + + TmpInt = CritLine.Y + 4; + + CritBodyArea = Spawn(class'UIPanel', RightContainer); + CritBodyArea.bAnimateOnInit = false; + CritBodyArea.InitPanel('CritBodyArea').SetPosition(CritLine.X, TmpInt); + CritBodyArea.width = StatsWidth; + CritBodyArea.height = height - TmpInt; + + CritMask = Spawn(class'UIMask', RightContainer).InitMask(, CritBodyArea); + CritMask.SetPosition(CritBodyArea.X, CritBodyArea.Y); + CritMask.SetSize(StatsWidth, CritBodyArea.height); + + CritStatList = Spawn(class'UIStatList', CritBodyArea); + CritStatList.bAnimateOnInit = false; + CritStatList.InitStatList('StatListLeft',,,, CritBodyArea.Width, CritBodyArea.Height, 0, 0); + + Hide(); + + return self; +} + +simulated function Show() +{ + local int ScrollHeight; + + super.Show(); + + if(UITacticalHUD(Screen) != none) + RefreshData(); + else + PopulateDebugData(); + + if(bIsVisible) + { + LeftWingArea.Show(); + RightWingArea.Show(); + + HitBodyArea.ClearScroll(); + DamageBodyArea.ClearScroll(); + CritBodyArea.ClearScroll(); + HitBodyArea.MC.SetNum("_alpha", 100); + DamageBodyArea.MC.SetNum("_alpha", 100); + CritBodyArea.MC.SetNum("_alpha", 100); + + //This will reset the scrolling upon showing this tooltip. + ScrollHeight = (HitStatList.height > HitBodyArea.height ) ? HitStatList.height : HitBodyArea.height; + HitBodyArea.AnimateScroll(ScrollHeight, HitBodyArea.height); + + ScrollHeight = (DamageStatList.height > DamageBodyArea.height ) ? DamageStatList.height : DamageBodyArea.height; + DamageBodyArea.AnimateScroll(ScrollHeight, DamageBodyArea.height); + + ScrollHeight = (CritStatList.height > CritBodyArea.height) ? CritStatList.height : CritBodyArea.height; + CritBodyArea.AnimateScroll(ScrollHeight, CritBodyArea.height); + } +} + +simulated function RefreshData() +{ + local StateObjectReference kEnemyRef; + local StateObjectReference Shooter, Target; + local AvailableAction kAction; + local AvailableTarget kTarget; + local XComGameState_Ability AbilityState; + local ShotBreakdown Breakdown; + local UIHackingBreakdown kHackingBreakdown; + local int TargetIndex, iShotBreakdown; + local ShotModifierInfo ShotInfo; + local bool bMultiShots; + local string TmpStr; + local X2TargetingMethod TargetingMethod; + local WeaponDamageValue MinDamageValue, MaxDamageValue; + local int AllowsShield; + + // Begin Issue #1540 + local Array DamageBreakdown; + local int MinDamage, MaxDamage, NetMitigationMin, NetMitigationMax; + // End Issue #1540 + + + kAction = UITacticalHUD(Screen).m_kAbilityHUD.GetSelectedAction(); + kEnemyRef = XComPresentationLayer(Movie.Pres).GetTacticalHUD().m_kEnemyTargets.GetSelectedEnemyStateObjectRef(); + AbilityState = XComGameState_Ability(`XCOMHISTORY.GetGameStateForObjectID(kAction.AbilityObjectRef.ObjectID)); + + // Bail if we have nothing to show ------------------------------------- + if( AbilityState == none ) + { + Hide(); + return; + } + + //Don't show this normal shot breakdown for the hacking action ------------ + AbilityState.GetUISummary_HackingBreakdown( kHackingBreakdown, kEnemyRef.ObjectID ); + if( kHackingBreakdown.bShow ) + { + Hide(); + return; + } + + // Refresh game data ------------------------------------------------------ + + // If no targeted icon, we're actually hovering the shot "to hit" info field, + // so use the selected enemy for calculation. + TargetingMethod = UITacticalHUD(screen).GetTargetingMethod(); + if (TargetingMethod != none) + TargetIndex = TargetingMethod.GetTargetIndex(); + if( kAction.AvailableTargets.Length > 0 && TargetIndex < kAction.AvailableTargets.Length ) + { + kTarget = kAction.AvailableTargets[TargetIndex]; + } + + Shooter = AbilityState.OwnerStateObject; + Target = kTarget.PrimaryTarget; + + iShotBreakdown = AbilityState.LookupShotBreakdown(Shooter, Target, AbilityState.GetReference(), Breakdown); + + // Hide if requested ------------------------------------------------------- + if (Breakdown.HideShotBreakdown) + { + Hide(); + + if(bLeftWingOpen) + { + bLeftWingWasOpen = true; + OnWingButtonClicked(LeftWingButton); + } + + if(bRightWingOpen) + { + bRightWingWasOpen = true; + OnWingButtonClicked(RightWingButton); + } + + LeftWingButton.Hide(); + RightWingButton.Hide(); + return; + } + else + { + UITacticalHUD(screen).m_kShotHUD.MC.FunctionVoid( "ShowHit" ); + UITacticalHUD(screen).m_kShotHUD.MC.FunctionVoid( "ShowCrit" ); + + LeftWingButton.Show(); + RightWingButton.Show(); + + if( !bLeftWingOpen && bLeftWingWasOpen ) + { + bLeftWingWasOpen = false; + OnWingButtonClicked(LeftWingButton); + } + + if( !bRightWingOpen && bRightWingWasOpen ) + { + bRightWingWasOpen = false; + OnWingButtonClicked(RightWingButton); + } + } + + if (Target.ObjectID == 0) + { + Hide(); + return; + } + + // Gameplay special hackery for multi-shot display. ----------------------- + if(iShotBreakdown != Breakdown.FinalHitChance) + { + bMultiShots = true; + ShotInfo.ModType = eHit_Success; + ShotInfo.Value = iShotBreakdown - Breakdown.FinalHitChance; + ShotInfo.Reason = class'XLocalizedData'.default.MultiShotChance; + Breakdown.Modifiers.AddItem(ShotInfo); + Breakdown.FinalHitChance = iShotBreakdown; + } + + // Now update the UI ------------------------------------------------------ + Breakdown.Modifiers.Sort(SortModifiers); + + if (bMultiShots) + HitLabel.SetHtmlText(class'UIUtilities_Text'.static.StyleText(class'XLocalizedData'.default.MultiHitLabel, eUITextStyle_Tooltip_StatLabel)); + else + HitLabel.SetHtmlText(class'UIUtilities_Text'.static.StyleText(class'XLocalizedData'.default.HitLabel, eUITextStyle_Tooltip_StatLabel)); + + TmpStr = ((Breakdown.bIsMultishot) ? Breakdown.MultiShotHitChance : Breakdown.FinalHitChance) $ "%"; + HitPercent.SetHtmlText(class'UIUtilities_Text'.static.StyleText(TmpStr, eUITextStyle_Tooltip_StatValue)); + HitStatList.RefreshData(ProcessHitCritBreakdown(Breakdown, eHit_Success)); + + if(Breakdown.ResultTable[eHit_Crit] >= 0) + { + AbilityState.GetDamagePreview(Target, MinDamageValue, MaxDamageValue, AllowsShield); + DamageBreakdown = ProcessDamageBreakdown(MinDamageValue); // Issue #1540 - extract into a variable and populate it early + MinDamage = MinDamageValue.Damage; + MaxDamage = MaxDamageValue.Damage; + + // Begin Issue #1540 + if (class'CHHelpers'.default.PREVIEW_ARMOR_MITIGATION) + { + // Calculate mitigated damage + class'CHHelpers'.static.CalculateMitigatedDamagePreview(Target, MinDamageValue, MaxDamageValue, AllowsShield, MinDamage, MaxDamage, NetMitigationMin, NetMitigationMax); + + // Add net mitigation to damage breakdown + if (NetMitigationMin != 0 || NetMitigationMax != 0) + { + DamageBreakdown.InsertItem(0, BuildDamageBreakdownItem(NetMitigationMin, class'XGLocalizedData'.default.ArmorMitigation, true, NetMitigationMax)); + } + } + // End Issue #1540 + + DamageLabel.SetHtmlText(class'UIUtilities_Text'.static.StyleText(class'XLocalizedData'.default.DamageLabel, eUITextStyle_Tooltip_StatLabel)); + TmpStr = string(MinDamage) $ "-" $ string(MaxDamage); // Issue #1540 - Use Min/MaxDamage instead of Min/MaxDamageValue.Damage + DamagePercent.SetHtmlText(class'UIUtilities_Text'.static.StyleText(TmpStr, eUITextStyle_Tooltip_StatValue)); + DamageStatList.RefreshData(DamageBreakdown); // Issue #1540 - use the previously extracted variable + + CritLabel.SetHtmlText(class'UIUtilities_Text'.static.StyleText(Breakdown.SpecialCritLabel == "" ? class'XLocalizedData'.default.CritLabel : Breakdown.SpecialCritLabel, eUITextStyle_Tooltip_StatLabel)); + TmpStr = string(Breakdown.ResultTable[eHit_Crit]) $ "%"; + CritPercent.SetHtmlText(class'UIUtilities_Text'.static.StyleText(TmpStr, eUITextStyle_Tooltip_StatValue)); + CritStatList.RefreshData(ProcessHitCritBreakdown(Breakdown, eHit_Crit)); + RightWingArea.Show(); + } + else + RightWingArea.Hide(); +} + +simulated function PopulateDebugData() +{ + local int i; + local ShotBreakdown Breakdown; + local ShotModifierInfo ShotInfo; + + Breakdown.Modifiers.Length = 0; + for(i = 0; i < 10; i++) + { + ShotInfo.ModType = eHit_Success; + ShotInfo.Value = 10; + ShotInfo.Reason = "Test "$i; + Breakdown.Modifiers.AddItem(ShotInfo); + } + + HitLabel.SetHtmlText(class'UIUtilities_Text'.static.StyleText(class'XLocalizedData'.default.HitLabel, eUITextStyle_Tooltip_StatLabel)); + HitPercent.SetHtmlText(class'UIUtilities_Text'.static.StyleText("##%", eUITextStyle_Tooltip_StatValue)); + HitStatList.RefreshData(ProcessHitCritBreakdown(Breakdown, eHit_Success)); + LeftWingArea.Show(); + + Breakdown.Modifiers.Length = 0; + for(i = 0; i < 10; i++) + { + ShotInfo.ModType = eHit_Crit; + ShotInfo.Value = 10; + ShotInfo.Reason = "Test "$i; + Breakdown.Modifiers.AddItem(ShotInfo); + } + + CritLabel.SetHtmlText(class'UIUtilities_Text'.static.StyleText(Breakdown.SpecialCritLabel == "" ? class'XLocalizedData'.default.CritLabel : Breakdown.SpecialCritLabel, eUITextStyle_Tooltip_StatLabel)); + CritPercent.SetHtmlText(class'UIUtilities_Text'.static.StyleText("##%", eUITextStyle_Tooltip_StatValue)); + CritStatList.RefreshData(ProcessHitCritBreakdown(Breakdown, eHit_Crit)); + RightWingArea.Show(); +} + +simulated function int SortModifiers( ShotModifierInfo a, ShotModifierInfo b) +{ + if( B.Value > A.Value ) + return -1; + else + return 0; +} + +// Begin Issue #1540 +// Extracted code from ProcessDamageBreakdown, with added support for custom messages +// and a damage range. +// Does not currently support a negative minimum with positive maximum, +// but I can't think of a reason that would be necessary right now. +simulated function UISummary_ItemStat BuildDamageBreakdownItem(int MinDamage, string Message, optional bool ShowDamageRange = false, optional int MaxDamage) +{ + local UISummary_ItemStat Item; + local string strLabel, strValue, strColoredValue, strPrefix; + local EUIState eState; + + if (ShowDamageRange && MaxDamage != MinDamage) + { + if (abs(MaxDamage) < abs(MinDamage)) // Someone got the damage numbers backwards! + { + return BuildDamageBreakdownItem(MaxDamage, Message, ShowDamageRange, MinDamage); + } + + strValue = string(MinDamage) $ "-" $ string(int(abs(MaxDamage))); + } + else + { + strValue = string(MinDamage); + } + + if( MinDamage < 0 ) + { + eState = eUIState_Bad; + strPrefix = ""; + } + else + { + eState = eUIState_Normal; + strPrefix = "+"; + } + + strLabel = class'UIUtilities_Text'.static.GetColoredText(Message, eState); + strColoredValue = class'UIUtilities_Text'.static.GetColoredText(strPrefix $ strValue, eState); + + Item.Label = strLabel; + Item.Value = strColoredValue; + return Item; +} +// End Issue #1540 + +simulated function array ProcessDamageBreakdown(const out WeaponDamageValue DamageValue) +{ + local array Stats; + local int i; + + for( i = 0; i < DamageValue.BonusDamageInfo.Length; i++ ) + { + // Issue #1540 - extracted into expanded helper function, see above + Stats.AddItem(BuildDamageBreakdownItem(DamageValue.BonusDamageInfo[i].Value, class'Helpers'.static.GetMessageFromDamageModifierInfo(DamageValue.BonusDamageInfo[i]))); + } + + return Stats; +} + +simulated function array ProcessHitCritBreakdown( ShotBreakdown Breakdown, int eHitType ) +{ + local array Stats; + local UISummary_ItemStat Item; + local int i; + local string strLabel, strValue, strPrefix; + local EUIState eState; + + for( i=0; i < Breakdown.Modifiers.Length; i++) + { + if( Breakdown.Modifiers[i].ModType == eHitType ) + { + if( Breakdown.Modifiers[i].Value < 0 ) + { + eState = eUIState_Bad; + strPrefix = ""; + } + else + { + eState = eUIState_Normal; + strPrefix = "+"; + } + + strLabel = class'UIUtilities_Text'.static.GetColoredText( Breakdown.Modifiers[i].Reason, eState ); + strValue = class'UIUtilities_Text'.static.GetColoredText( strPrefix $ string(Breakdown.Modifiers[i].Value) $ "%", eState ); + + Item.Label = strLabel; + Item.Value = strValue; + Stats.AddItem(Item); + } + } + + if( eHitType == eHit_Crit && Stats.length == 1 && Breakdown.ResultTable[eHit_Crit] == 0 ) + Stats.length = 0; + + return Stats; +} + +simulated function LeftWingMouseEvent(UIPanel Control, int Cmd) +{ + if( Cmd != class'UIUtilities_Input'.const.FXS_L_MOUSE_UP || !LeftWingButton.bIsVisible) return; + + if( bLeftWingOpen ) + { + LeftWingArea.MC.FunctionString("gotoAndPlay", "in"); + bLeftWingOpen = false; + } + else + { + LeftWingArea.MC.FunctionString("gotoAndPlay", "out"); + bLeftWingOpen = true; + } + + LeftWingButton.MC.ChildFunctionString("bg.arrow", "gotoAndStop", bLeftWingOpen ? "right" : "left"); +} +simulated function RightWingMouseEvent(UIPanel Control, int Cmd) +{ + if( Cmd != class'UIUtilities_Input'.const.FXS_L_MOUSE_UP || !RightWingButton.bIsVisible) return; + + if( bRightWingOpen ) + { + RightWingArea.MC.FunctionString("gotoAndPlay", "in"); + bRightWingOpen = false; + } + else + { + RightWingArea.MC.FunctionString("gotoAndPlay", "out"); + bRightWingOpen = true; + } + + RightWingButton.MC.ChildFunctionString("bg.arrow", "gotoAndStop", bRightWingOpen ? "right" : "left"); +} +simulated function OnWingButtonClicked(UIButton Button) +{ + if(Button == LeftWingButton) + LeftWingMouseEvent(Button, class'UIUtilities_Input'.const.FXS_L_MOUSE_UP); + else if(Button == RightWingButton) + RightWingMouseEvent(Button, class'UIUtilities_Input'.const.FXS_L_MOUSE_UP); +} + +//Defaults: ------------------------------------------------------------------------------ +defaultproperties +{ + Height = 160; + bLeftWingOpen = true; + bRightWingOpen = true; + MCName = "shotWingsMC"; +} \ No newline at end of file diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/UIUnitFlagManager.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/UIUnitFlagManager.uc index 25c2a02fb..d20463543 100644 --- a/X2WOTCCommunityHighlander/Src/XComGame/Classes/UIUnitFlagManager.uc +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/UIUnitFlagManager.uc @@ -638,6 +638,17 @@ simulated function SetAbilityDamagePreview(UIUnitFlag kFlag, XComGameState_Abili local WeaponDamageValue MinDamageValue; local WeaponDamageValue MaxDamageValue; + // Start Issue #1540 + // Streamline #579 to simply choose a WDV to preview, + // instead of duplicating the whole preview implementation. Grrr. >:( + local WeaponDamageValue PreviewDamageValue; + + local int NetMitigation; + local bool AppliedMitigation; // Used for #743 + local int MinimumDamage; // Used for #321 + // End Issue #1540 + + if( kFlag == none || AbilityState == none ) { return; @@ -651,46 +662,66 @@ simulated function SetAbilityDamagePreview(UIUnitFlag kFlag, XComGameState_Abili // Start Issue #579 /// HL-Docs: feature:UseMinDamageForUnitFlagPreview; issue:579; tags:tactical,ui /// Allows using ability's minimum damage rather than max damage for the unit flag damage preview. - if (class'CHHelpers'.default.bUseMinDamageForUnitFlagPreview) + if( class'CHHelpers'.default.bUseMinDamageForUnitFlagPreview ) + { + PreviewDamageValue = MinDamageValue; + } + else { - possibleHPDamage = MinDamageValue.Damage; - possibleShieldDamage = 0; + PreviewDamageValue = MaxDamageValue; + } + // End Issue #579 - // MaxHP contains extra HP points given by shield - if( shieldPoints > 0 && AllowedShield > 0 ) - { - possibleShieldDamage = min(shieldPoints, MinDamageValue.Damage); - possibleShieldDamage = min(possibleShieldDamage, AllowedShield); - possibleHPDamage = MinDamageValue.Damage - possibleShieldDamage; - } + // Start Issue #321 + if( class'X2Effect_ApplyWeaponDamage'.default.NO_MINIMUM_DAMAGE ) + { + MinimumDamage = 0; + } + else + { + MinimumDamage = 1; + } + // End Issue #321 - if( possibleHPDamage > 0 && !AbilityState.DamageIgnoresArmor() && FlagUnit != none ) - possibleHPDamage -= max(0, FlagUnit.GetArmorMitigationForUnitFlag() - MinDamageValue.Pierce); - kFlag.SetShieldPointsPreview(possibleShieldDamage); - kFlag.SetHitPointsPreview(possibleHPDamage); - kFlag.SetArmorPointsPreview(MinDamageValue.Shred, MinDamageValue.Pierce); + possibleHPDamage = PreviewDamageValue.Damage; + possibleShieldDamage = 0; + // Begin Issue #743 and Issue #1540 + if( FlagUnit != none && PreviewDamageValue.Spread != 0 ) + { + NetMitigation = max(PreviewDamageValue.Spread - PreviewDamageValue.Pierce, PreviewDamageValue.PlusOne); } - else // End Issue #579 + // End Issue #743 and Issue #1540 + + // MaxHP contains extra HP points given by shield + if( shieldPoints > 0 && AllowedShield > 0 ) { - possibleHPDamage = MaxDamageValue.Damage; - possibleShieldDamage = 0; + possibleShieldDamage = min(shieldPoints, PreviewDamageValue.Damage); + possibleShieldDamage = min(possibleShieldDamage, AllowedShield); + possibleHPDamage = PreviewDamageValue.Damage - possibleShieldDamage; - // MaxHP contains extra HP points given by shield - if( shieldPoints > 0 && AllowedShield > 0 ) + // Begin Issue #743 and Issue #1540 - apply net mitigation to shields if appropriate + if( class'X2Effect_ApplyWeaponDamage'.default.ARMOR_BEFORE_SHIELD ) { - possibleShieldDamage = min(shieldPoints, MaxDamageValue.Damage); - possibleShieldDamage = min(possibleShieldDamage, AllowedShield); - possibleHPDamage = MaxDamageValue.Damage - possibleShieldDamage; + possibleShieldDamage = max(possibleShieldDamage - NetMitigation, MinimumDamage); + AppliedMitigation = true; } + // End Issue #743 and Issue #1540 - if( possibleHPDamage > 0 && !AbilityState.DamageIgnoresArmor() && FlagUnit != none ) - possibleHPDamage -= max(0, FlagUnit.GetArmorMitigationForUnitFlag() - MaxDamageValue.Pierce); + MinimumDamage = 0; // Issue #321, sort of? + } - kFlag.SetShieldPointsPreview(possibleShieldDamage); - kFlag.SetHitPointsPreview(possibleHPDamage); - kFlag.SetArmorPointsPreview(MaxDamageValue.Shred, MaxDamageValue.Pierce); + // Issue #743 and Issue #1540 - replace the condition! + //if( possibleHPDamage > 0 && !AbilityState.DamageIgnoresArmor() && FlagUnit != none ) + if( possibleHPDamage > 0 && !AppliedMitigation ) + { + // Begin Issue #1540 - account for extra armor, and fix #321 to apply to previews + possibleHPDamage = max(possibleHPDamage - NetMitigation, MinimumDamage); } + + kFlag.SetShieldPointsPreview(possibleShieldDamage); + kFlag.SetHitPointsPreview(possibleHPDamage); + kFlag.SetArmorPointsPreview(PreviewDamageValue.Shred, PreviewDamageValue.Pierce); } simulated function LockFlagToReticle(bool bShouldLock, UITargetingReticle kReticle, StateObjectReference ObjectRef) diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_ApplyWeaponDamage.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_ApplyWeaponDamage.uc index 46dd8ede1..2bf469f92 100644 --- a/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_ApplyWeaponDamage.uc +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2Effect_ApplyWeaponDamage.uc @@ -390,6 +390,12 @@ simulated function GetDamagePreview(StateObjectReference TargetRef, XComGameStat local DamageModifierInfo DamageModInfo; local array DamageMods; // Issue #923 + // Begin Issue #1540 - variables for cover DR + local int OriginalMitigation, MinMandatoryMitigation, MaxMandatoryMitigation; + local int MinPierce, MaxPierce; + local int MinMitigation, MaxMitigation; + // End Issue #1540 + MinDamagePreview = UpgradeTemplateBonusDamage; MaxDamagePreview = UpgradeTemplateBonusDamage; bDoesDamageIgnoreShields = bBypassShields; @@ -665,7 +671,62 @@ simulated function GetDamagePreview(StateObjectReference TargetRef, XComGameStat // End Issue #923 if (!bDoesDamageIgnoreShields) + { AllowsShield += MaxDamagePreview.Damage; + } + + // Begin Issue #1540 - preview armor DR + // Unlike the UI, this doesn't require extra configs, because the original values of + // Spread and PlusOne were always defaulted to 0 so there's no way + // anyone was getting useful info from them. + // + // However, UI mods overriding ShotWings, ShotHUD, or UnitFlagManager + // will not use this information - with or without configs - until they are updated. + if (TargetUnit != none && bIgnoreArmor && MinDamagePreview.Damage > 0) + { + // The original mitigation (and original minimum mitigation, i.e. 0) + // are shared across both damage values, so can be initialized here. + OriginalMitigation = TargetUnit.GetArmorMitigationForUnitFlag(); + + // Get adjusted mitigation, piercing, and minimum mitigation + // for the attack. + // It seems that passing struct values as out parameters + // prevents them from being edited by the function, + // so we need a few extra variables to store the data... + MinMitigation = OriginalMitigation; + MinPierce = MinDamagePreview.Pierce; + MinMandatoryMitigation = 0; + class'CHHelpers'.static.GetCDO().TriggerAdjustArmorMitigation( + MinDamagePreview.Damage, + MinMitigation, + MinPierce, + MinMandatoryMitigation, // Starts at 0 + TestEffectParams, + self, + // Tells the event handlers that this is a minimum damage preview. + // (The absence of a game state tells them it's *a* damage preview.) + true + ); + MinDamagePreview.Spread = MinMitigation; + MinDamagePreview.Pierce = MinPierce; + MinDamagePreview.PlusOne = MinMandatoryMitigation; + + MaxMitigation = OriginalMitigation; + MaxPierce = MaxDamagePreview.Pierce; + MaxMandatoryMitigation = 0; + class'CHHelpers'.static.GetCDO().TriggerAdjustArmorMitigation( + MaxDamagePreview.Damage, + MaxMitigation, + MaxPierce, + MaxMandatoryMitigation, // Starts at 0 + TestEffectParams, + self + ); + MaxDamagePreview.Spread = MaxMitigation; + MaxDamagePreview.Pierce = MaxPierce; + MaxDamagePreview.PlusOne = MaxMandatoryMitigation; + } + // End Issue #1540 // Start Issue #1281 /// HL-Docs: ref:Bugfixes; issue:1281 @@ -760,6 +821,10 @@ simulated function int CalculateDamageAmount(const out EffectAppliedData ApplyEf local DamageModifierInfo ModifierInfo; local bool bWasImmune, bHadAnyDamage; + // Begin Issue #1540 + local int MinMitigation; + // End Issue #1540 + // Issue #1299 - comment out unused Rupture Cap. //local int RuptureCap; @@ -1119,14 +1184,38 @@ simulated function int CalculateDamageAmount(const out EffectAppliedData ApplyEf if (kTarget != none && !bIgnoreArmor) { ArmorMitigation = kTarget.GetArmorMitigation(ApplyEffectParameters.AbilityResultContext.ArmorMitigation); - if (ArmorMitigation != 0) - { + + // Begin Issue #1540 + // Moved these two lines out of the if block for eventing OriginalMitigation = ArmorMitigation; ArmorPiercing += kSourceUnit.GetCurrentStat(eStat_ArmorPiercing); + + // This is a huge event (as implied by this equally huge param list), + // and we're reusing it for damage previews, so it gets a helper. + // For easier reference: Only ArmorMitigation, ArmorPiercing, and MinMitigation + // are affected by this event here. + class'CHHelpers'.static.GetCDO().TriggerAdjustArmorMitigation( + WeaponDamage, + ArmorMitigation, // We read this one. + ArmorPiercing, + MinMitigation, // This starts at 0. + ApplyEffectParameters, + self, + false, // Not a minimum damage preview! + NewGameState + ); + // End Issue #1540 + + if (ArmorMitigation != 0) + { `log("Armor mitigation! Target armor mitigation value:" @ ArmorMitigation @ "Attacker armor piercing value:" @ ArmorPiercing, true, 'XCom_HitRolls'); ArmorMitigation -= ArmorPiercing; - if (ArmorMitigation < 0) - ArmorMitigation = 0; + + // Issue #1540 - minimum mitigation might not be 0 like in vanilla + // (i.e. impenetrable armor), so check against MinMitigation instead. + if (ArmorMitigation < MinMitigation) + ArmorMitigation = MinMitigation; + // Issue #321 if (ArmorMitigation >= WeaponDamage) { diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameState_Ability.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameState_Ability.uc index 0732f54f5..715725918 100644 --- a/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameState_Ability.uc +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/XComGameState_Ability.uc @@ -1202,6 +1202,10 @@ function NormalDamagePreview(StateObjectReference TargetRef, out WeaponDamageVal MaxDamagePreview.Damage += TempMaxDamage.Damage; MaxDamagePreview.Pierce += TempMaxDamage.Pierce; MaxDamagePreview.Shred += TempMaxDamage.Shred; + // Begin Issue #1540 - serialize extra preview data! + MaxDamagePreview.Spread += TempMaxDamage.Spread; + MaxDamagePreview.PlusOne += TempMaxDamage.PlusOne; + // End Issue #1540 for( DamageModIndex = 0; DamageModIndex < TempMaxDamage.BonusDamageInfo.Length; ++DamageModIndex ) { MaxDamagePreview.BonusDamageInfo.AddItem(TempMaxDamage.BonusDamageInfo[DamageModIndex]); @@ -1210,6 +1214,10 @@ function NormalDamagePreview(StateObjectReference TargetRef, out WeaponDamageVal MinDamagePreview.Damage += TempMinDamage.Damage; MinDamagePreview.Pierce += TempMinDamage.Pierce; MinDamagePreview.Shred += TempMinDamage.Shred; + // Begin Issue #1540 - serialize extra preview data! + MinDamagePreview.Spread += TempMinDamage.Spread; + MinDamagePreview.PlusOne += TempMinDamage.PlusOne; + // End Issue #1540 for( DamageModIndex = 0; DamageModIndex < TempMinDamage.BonusDamageInfo.Length; ++DamageModIndex ) { MinDamagePreview.BonusDamageInfo.AddItem(TempMinDamage.BonusDamageInfo[DamageModIndex]); @@ -1224,10 +1232,18 @@ function NormalDamagePreview(StateObjectReference TargetRef, out WeaponDamageVal MinDamagePreview.Damage += MinDamagePreview.Damage * BurstFire.NumExtraShots; MinDamagePreview.Pierce += MinDamagePreview.Pierce * BurstFire.NumExtraShots; MinDamagePreview.Shred += MinDamagePreview.Shred * BurstFire.NumExtraShots; + // Begin Issue #1540 - serialize extra preview data! + MinDamagePreview.Spread *= BurstFire.NumExtraShots; + MinDamagePreview.PlusOne *= BurstFire.NumExtraShots; + // End Issue #1540 MaxDamagePreview.Damage += MaxDamagePreview.Damage * BurstFire.NumExtraShots; MaxDamagePreview.Pierce += MaxDamagePreview.Pierce * BurstFire.NumExtraShots; MaxDamagePreview.Shred += MaxDamagePreview.Shred * BurstFire.NumExtraShots; + // Begin Issue #1540 - serialize extra preview data! + MaxDamagePreview.Spread *= BurstFire.NumExtraShots; + MaxDamagePreview.PlusOne *= BurstFire.NumExtraShots; + // End Issue #1540 } }