Replaced function body with one call to CanAddItemToInventory_CH

//---------------------------------------------------------------------------------------
//  FILE:    X2DownloadableContentInfo.uc
//  AUTHOR:  Ryan McFall
//
//	Mods and DLC derive from this class to define their behavior with respect to
//  certain in-game activities like loading a saved game. Should the DLC be installed
//  to a campaign that was already started?
//
//---------------------------------------------------------------------------------------
//  Copyright (c) 2016 Firaxis Games, Inc. All rights reserved.
//---------------------------------------------------------------------------------------

class X2DownloadableContentInfo extends Object
	Config(Game)
	native(Core);

//Backwards compatibility for mods
enum EUIAction
{
	eUIAction_Accept,   // User initiated
	eUIAction_Cancel,   // User initiated
	eUIAction_Closed    // Automatically closed by system
};

var config string DLCIdentifier; //The directory name that the DLC resides in
var config bool bHasOptionalNarrativeContent; // Does this DLC have optional narrative content, generates a checkbox in pre-campaign menu
var config array<string> AdditionalDLCResources;    // Resource paths for objects the game will load at startup and be synchronously accessible at runtime

var localized string PartContentLabel; // Label for use in the game play options menu allowing users to decide how this content pack is applied to new soldiers
var localized string PartContentSummary; // Tooltip for the part content slider

var localized string NarrativeContentLabel; // Label next to the checkbox in pre-campaign menu
var localized string NarrativeContentSummary; // Longer description of narrative content for pre-campaign menu

var localized string EnableContentLabel;
var localized string EnableContentSummary;
var localized string EnableContentAcceptLabel;
var localized string EnableContentCancelLabel;

/// <summary>
/// This method is run if the player loads a saved game that was created prior to this DLC / Mod being installed, and allows the
/// DLC / Mod to perform custom processing in response. This will only be called once the first time a player loads a save that was
/// create without the content installed. Subsequent saves will record that the content was installed.
/// </summary>
static event OnLoadedSavedGame()
{

}

/// <summary>
/// This method is run when the player loads a saved game directly into Strategy while this DLC is installed
/// </summary>
static event OnLoadedSavedGameToStrategy()
{

}

/// <summary>
/// Called when the player starts a new campaign while this DLC / Mod is installed. When a new campaign is started the initial state of the world
/// is contained in a strategy start state. Never add additional history frames inside of InstallNewCampaign, add new state objects to the start state
/// or directly modify start state objects
/// </summary>
static event InstallNewCampaign(XComGameState StartState)
{

}

/// <summary>
/// Called just before the player launches into a tactical a mission while this DLC / Mod is installed.
/// Allows dlcs/mods to modify the start state before launching into the mission
/// </summary>
static event OnPreMission(XComGameState StartGameState, XComGameState_MissionSite MissionState)
{

}

/// <summary>
/// Called when the player completes a mission while this DLC / Mod is installed.
/// </summary>
static event OnPostMission()
{

}

/// <summary>
/// Called when the player is doing a direct tactical->tactical mission transfer. Allows mods to modify the
/// start state of the new transfer mission if needed
/// </summary>
static event ModifyTacticalTransferStartState(XComGameState TransferStartState)
{

}

/// <summary>
/// Called after the player exits the post-mission sequence while this DLC / Mod is installed.
/// </summary>
static event OnExitPostMissionSequence()
{

}

/// <summary>
/// Called after the Templates have been created (but before they are validated) while this DLC / Mod is installed.
/// </summary>
static event OnPostTemplatesCreated()
{

}

/// <summary>
/// Called when the difficulty changes and this DLC is active
/// </summary>
static event OnDifficultyChanged()
{

}

/// <summary>
/// Called by the Geoscape tick
/// </summary>
static event UpdateDLC()
{

}

/// <summary>
/// Called after HeadquartersAlien builds a Facility
/// </summary>
static event OnPostAlienFacilityCreated(XComGameState NewGameState, StateObjectReference MissionRef)
{

}

/// <summary>
/// Called after a new Alien Facility's doom generation display is completed
/// </summary>
static event OnPostFacilityDoomVisualization()
{

}

/// <summary>
/// Called when viewing mission blades with the Shadow Chamber panel, used primarily to modify tactical tags for spawning
/// Returns true when the mission's spawning info needs to be updated
/// </summary>
static function bool UpdateShadowChamberMissionInfo(StateObjectReference MissionRef)
{
	return false;
}

/// <summary>
/// A dialogue popup used for players to confirm or deny whether new gameplay content should be installed for this DLC / Mod.
/// </summary>
static function EnableDLCContentPopup()
{
	local TDialogueBoxData kDialogData;

	kDialogData.eType = eDialog_Normal;
	kDialogData.strTitle = default.EnableContentLabel;
	kDialogData.strText = default.EnableContentSummary;
	kDialogData.strAccept = default.EnableContentAcceptLabel;
	kDialogData.strCancel = default.EnableContentCancelLabel;

	kDialogData.fnCallback = EnableDLCContentPopupCallback_Ex;
	`HQPRES.UIRaiseDialog(kDialogData);
}

simulated function EnableDLCContentPopupCallback(eUIAction eAction)
{
}

simulated function EnableDLCContentPopupCallback_Ex(Name eAction)
{
	switch (eAction)
	{
	case 'eUIAction_Accept':
		EnableDLCContentPopupCallback(eUIAction_Accept);
		break;
	case 'eUIAction_Cancel':
		EnableDLCContentPopupCallback(eUIAction_Cancel);
		break;
	case 'eUIAction_Closed':
		EnableDLCContentPopupCallback(eUIAction_Closed);
		break;
	}
}

/// <summary>
/// Called when viewing mission blades, used primarily to modify tactical tags for spawning
/// Returns true when the mission's spawning info needs to be updated
/// </summary>
static function bool ShouldUpdateMissionSpawningInfo(StateObjectReference MissionRef)
{
	return false;
}

/// <summary>
/// Called when viewing mission blades, used primarily to modify tactical tags for spawning
/// Returns true when the mission's spawning info needs to be updated
/// </summary>
static function bool UpdateMissionSpawningInfo(StateObjectReference MissionRef)
{
	return false;
}

/// <summary>
/// Called when viewing mission blades, used to add any additional text to the mission description
/// </summary>
static function string GetAdditionalMissionDesc(StateObjectReference MissionRef)
{
	return "";
}

/// <summary>
/// Called from X2AbilityTag:ExpandHandler after processing the base game tags. Return true (and fill OutString correctly)
/// to indicate the tag has been expanded properly and no further processing is needed.
/// </summary>
static function bool AbilityTagExpandHandler(string InString, out string OutString)
{
	return false;
}

/// <summary>
/// Called from XComGameState_Unit:GatherUnitAbilitiesForInit after the game has built what it believes is the full list of
/// abilities for the unit based on character, class, equipment, et cetera. You can add or remove abilities in SetupData.
/// </summary>
static function FinalizeUnitAbilitiesForInit(XComGameState_Unit UnitState, out array<AbilitySetupData> SetupData, optional XComGameState StartState, optional XComGameState_Player PlayerState, optional bool bMultiplayerDisplay)
{

}

/// <summary>
/// Calls DLC specific popup handlers to route messages to correct display functions
/// </summary>
static function bool DisplayQueuedDynamicPopup(DynamicPropertySet PropertySet)
{

}

// -------------------------------------------------------------
// ------------ X2WOTCCommunityHighlander Additions ------------
// -------------------------------------------------------------

/// Start Issue #21
/// <summary>
/// Called from XComUnitPawn.DLCAppendSockets
/// Allows DLC/Mods to append sockets to units
/// </summary>
static function string DLCAppendSockets(XComUnitPawn Pawn)
{
	return "";
}
/// End Issue #21

/// Start Issue #24
/// <summary>
/// Called from XComUnitPawn.UpdateAnimations
/// CustomAnimSets will be added to the pawns animsets
/// </summary>
static function UpdateAnimations(out array<AnimSet> CustomAnimSets, XComGameState_Unit UnitState, XComUnitPawn Pawn)
{

}
/// End Issue #24

/// Start Issue #18
/// <summary>
/// Calls DLC specific handlers to override spawn location
/// </summary>
static function bool GetValidFloorSpawnLocations(out array<Vector> FloorPoints, float SpawnSizeOverride, XComGroupSpawn SpawnPoint)
{
	return false;
}
/// End Issue #18

/// start Issue #114: added XComGameState_Item as something that can be passed down for disabled reason purposes
/// basically the inventory hook wtih an added paramter to pass through
/// we leave the old one alone for compatibility reasons, as we call it through here for those mods.
///
static function bool CanAddItemToInventory_CH_Improved(out int bCanAddItem, const EInventorySlot Slot, const X2ItemTemplate ItemTemplate, int Quantity, XComGameState_Unit UnitState, optional XComGameState CheckGameState, optional out string DisabledReason, optional XComGameState_Item ItemState)
{

	return CanAddItemToInventory_CH(bCanAddItem, Slot, ItemTemplate, Quantity, UnitState, CheckGameState, DisabledReason) //for mods not using item state, we can just by default, go straight to here. Newer mods can handle implementaion using the item state.

}
//end Issue #114

/// start Issue #50
/// <summary>
/// Called from XComGameState_Unit:CanAddItemToInventory & UIArmory_Loadout:GetDisabledReason
/// defaults to using the wrapper function below for calls from XCGS_U. Return false with a non-empty string in this function to show the disabled reason in UIArmory_Loadout
/// Note: due to how UIArmory_Loadout does its check, expect only Slot, ItemTemplate, and UnitState to be filled when trying to fill out a disabled reason. Hence the check for CheckGameState == none
/// </summary>
static function bool CanAddItemToInventory_CH(out int bCanAddItem, const EInventorySlot Slot, const X2ItemTemplate ItemTemplate, int Quantity, XComGameState_Unit UnitState, optional XComGameState CheckGameState, optional out string DisabledReason)
{
	if(CheckGameState == none)
		return false;

	return CanAddItemToInventory(bCanAddItem, Slot, ItemTemplate, Quantity, UnitState, CheckGameState);
}

/// <summary>
/// wrapper function: original function from base game LW/Community highlander
//  Return true to the actual DLC hook
/// </summary>
static private function bool CanAddItemToInventory(out int bCanAddItem, const EInventorySlot Slot, const X2ItemTemplate ItemTemplate, int Quantity, XComGameState_Unit UnitState, XComGameState CheckGameState)
{
	return false;
}

//end Issue #50

//---------------------------------------------------------------------------------------
//  FILE:    X2DownloadableContentInfo.uc
//  AUTHOR:  Ryan McFall
//
//	Mods and DLC derive from this class to define their behavior with respect to
//  certain in-game activities like loading a saved game. Should the DLC be installed
//  to a campaign that was already started?
//
//---------------------------------------------------------------------------------------
//  Copyright (c) 2016 Firaxis Games, Inc. All rights reserved.
//---------------------------------------------------------------------------------------

class X2DownloadableContentInfo extends Object
	Config(Game)
	native(Core);

//Backwards compatibility for mods
enum EUIAction
{
	eUIAction_Accept,   // User initiated
	eUIAction_Cancel,   // User initiated
	eUIAction_Closed    // Automatically closed by system
};

var config string DLCIdentifier; //The directory name that the DLC resides in
var config bool bHasOptionalNarrativeContent; // Does this DLC have optional narrative content, generates a checkbox in pre-campaign menu
var config array<string> AdditionalDLCResources;    // Resource paths for objects the game will load at startup and be synchronously accessible at runtime

var localized string PartContentLabel; // Label for use in the game play options menu allowing users to decide how this content pack is applied to new soldiers
var localized string PartContentSummary; // Tooltip for the part content slider

var localized string NarrativeContentLabel; // Label next to the checkbox in pre-campaign menu
var localized string NarrativeContentSummary; // Longer description of narrative content for pre-campaign menu

var localized string EnableContentLabel;
var localized string EnableContentSummary;
var localized string EnableContentAcceptLabel;
var localized string EnableContentCancelLabel;

/// <summary>
/// This method is run if the player loads a saved game that was created prior to this DLC / Mod being installed, and allows the
/// DLC / Mod to perform custom processing in response. This will only be called once the first time a player loads a save that was
/// create without the content installed. Subsequent saves will record that the content was installed.
/// </summary>
static event OnLoadedSavedGame()
{

}

/// <summary>
/// This method is run when the player loads a saved game directly into Strategy while this DLC is installed
/// </summary>
static event OnLoadedSavedGameToStrategy()
{

}

/// <summary>
/// Called when the player starts a new campaign while this DLC / Mod is installed. When a new campaign is started the initial state of the world
/// is contained in a strategy start state. Never add additional history frames inside of InstallNewCampaign, add new state objects to the start state
/// or directly modify start state objects
/// </summary>
static event InstallNewCampaign(XComGameState StartState)
{

}

/// <summary>
/// Called just before the player launches into a tactical a mission while this DLC / Mod is installed.
/// Allows dlcs/mods to modify the start state before launching into the mission
/// </summary>
static event OnPreMission(XComGameState StartGameState, XComGameState_MissionSite MissionState)
{

}

/// <summary>
/// Called when the player completes a mission while this DLC / Mod is installed.
/// </summary>
static event OnPostMission()
{

}

/// <summary>
/// Called when the player is doing a direct tactical->tactical mission transfer. Allows mods to modify the
/// start state of the new transfer mission if needed
/// </summary>
static event ModifyTacticalTransferStartState(XComGameState TransferStartState)
{

}

/// <summary>
/// Called after the player exits the post-mission sequence while this DLC / Mod is installed.
/// </summary>
static event OnExitPostMissionSequence()
{

}

/// <summary>
/// Called after the Templates have been created (but before they are validated) while this DLC / Mod is installed.
/// </summary>
static event OnPostTemplatesCreated()
{

}

/// <summary>
/// Called when the difficulty changes and this DLC is active
/// </summary>
static event OnDifficultyChanged()
{

}

/// <summary>
/// Called by the Geoscape tick
/// </summary>
static event UpdateDLC()
{

}

/// <summary>
/// Called after HeadquartersAlien builds a Facility
/// </summary>
static event OnPostAlienFacilityCreated(XComGameState NewGameState, StateObjectReference MissionRef)
{

}

/// <summary>
/// Called after a new Alien Facility's doom generation display is completed
/// </summary>
static event OnPostFacilityDoomVisualization()
{

}

/// <summary>
/// Called when viewing mission blades with the Shadow Chamber panel, used primarily to modify tactical tags for spawning
/// Returns true when the mission's spawning info needs to be updated
/// </summary>
static function bool UpdateShadowChamberMissionInfo(StateObjectReference MissionRef)
{
	return false;
}

/// <summary>
/// A dialogue popup used for players to confirm or deny whether new gameplay content should be installed for this DLC / Mod.
/// </summary>
static function EnableDLCContentPopup()
{
	local TDialogueBoxData kDialogData;

	kDialogData.eType = eDialog_Normal;
	kDialogData.strTitle = default.EnableContentLabel;
	kDialogData.strText = default.EnableContentSummary;
	kDialogData.strAccept = default.EnableContentAcceptLabel;
	kDialogData.strCancel = default.EnableContentCancelLabel;

	kDialogData.fnCallback = EnableDLCContentPopupCallback_Ex;
	`HQPRES.UIRaiseDialog(kDialogData);
}

simulated function EnableDLCContentPopupCallback(eUIAction eAction)
{
}

simulated function EnableDLCContentPopupCallback_Ex(Name eAction)
{
	switch (eAction)
	{
	case 'eUIAction_Accept':
		EnableDLCContentPopupCallback(eUIAction_Accept);
		break;
	case 'eUIAction_Cancel':
		EnableDLCContentPopupCallback(eUIAction_Cancel);
		break;
	case 'eUIAction_Closed':
		EnableDLCContentPopupCallback(eUIAction_Closed);
		break;
	}
}

/// <summary>
/// Called when viewing mission blades, used primarily to modify tactical tags for spawning
/// Returns true when the mission's spawning info needs to be updated
/// </summary>
static function bool ShouldUpdateMissionSpawningInfo(StateObjectReference MissionRef)
{
	return false;
}

/// <summary>
/// Called when viewing mission blades, used primarily to modify tactical tags for spawning
/// Returns true when the mission's spawning info needs to be updated
/// </summary>
static function bool UpdateMissionSpawningInfo(StateObjectReference MissionRef)
{
	return false;
}

/// <summary>
/// Called when viewing mission blades, used to add any additional text to the mission description
/// </summary>
static function string GetAdditionalMissionDesc(StateObjectReference MissionRef)
{
	return "";
}

/// <summary>
/// Called from X2AbilityTag:ExpandHandler after processing the base game tags. Return true (and fill OutString correctly)
/// to indicate the tag has been expanded properly and no further processing is needed.
/// </summary>
static function bool AbilityTagExpandHandler(string InString, out string OutString)
{
	return false;
}

/// <summary>
/// Called from XComGameState_Unit:GatherUnitAbilitiesForInit after the game has built what it believes is the full list of
/// abilities for the unit based on character, class, equipment, et cetera. You can add or remove abilities in SetupData.
/// </summary>
static function FinalizeUnitAbilitiesForInit(XComGameState_Unit UnitState, out array<AbilitySetupData> SetupData, optional XComGameState StartState, optional XComGameState_Player PlayerState, optional bool bMultiplayerDisplay)
{

}

/// <summary>
/// Calls DLC specific popup handlers to route messages to correct display functions
/// </summary>
static function bool DisplayQueuedDynamicPopup(DynamicPropertySet PropertySet)
{

}

// -------------------------------------------------------------
// ------------ X2WOTCCommunityHighlander Additions ------------
// -------------------------------------------------------------

/// Start Issue #21
/// <summary>
/// Called from XComUnitPawn.DLCAppendSockets
/// Allows DLC/Mods to append sockets to units
/// </summary>
static function string DLCAppendSockets(XComUnitPawn Pawn)
{
	return "";
}
/// End Issue #21

/// Start Issue #24
/// <summary>
/// Called from XComUnitPawn.UpdateAnimations
/// CustomAnimSets will be added to the pawns animsets
/// </summary>
static function UpdateAnimations(out array<AnimSet> CustomAnimSets, XComGameState_Unit UnitState, XComUnitPawn Pawn)
{

}
/// End Issue #24

/// Start Issue #18
/// <summary>
/// Calls DLC specific handlers to override spawn location
/// </summary>
static function bool GetValidFloorSpawnLocations(out array<Vector> FloorPoints, float SpawnSizeOverride, XComGroupSpawn SpawnPoint)
{
	return false;
}
/// End Issue #18

/// start Issue #114: added XComGameState_Item as something that can be passed down for disabled reason purposes
/// basically the inventory hook wtih an added paramter to pass through
/// we leave the old one alone for compatibility reasons, as we call it through here for those mods.
///
static function bool CanAddItemToInventory_CH_Improved(out int bCanAddItem, const EInventorySlot Slot, const X2ItemTemplate ItemTemplate, int Quantity, XComGameState_Unit UnitState, optional XComGameState CheckGameState, optional out string DisabledReason, optional XComGameState_Item ItemState)
{
	if(CheckGameState == none)
		return CanAddItemToInventory_CH(bCanAddItem, Slot, ItemTemplate, Quantity, UnitState, CheckGameState, DisabledReason) //this can be handled by this function by newer mods

	return CanAddItemToInventory(bCanAddItem, Slot, ItemTemplate, Quantity, UnitState, CheckGameState);
}
//end Issue #114

/// start Issue #50
/// <summary>
/// Called from XComGameState_Unit:CanAddItemToInventory & UIArmory_Loadout:GetDisabledReason
/// defaults to using the wrapper function below for calls from XCGS_U. Return false with a non-empty string in this function to show the disabled reason in UIArmory_Loadout
/// Note: due to how UIArmory_Loadout does its check, expect only Slot, ItemTemplate, and UnitState to be filled when trying to fill out a disabled reason. Hence the check for CheckGameState == none
/// </summary>
static function bool CanAddItemToInventory_CH(out int bCanAddItem, const EInventorySlot Slot, const X2ItemTemplate ItemTemplate, int Quantity, XComGameState_Unit UnitState, optional XComGameState CheckGameState, optional out string DisabledReason, optional XComGameState_Item ItemState)
{
	if(CheckGameState == none)
		return false;

	return CanAddItemToInventory(bCanAddItem, Slot, ItemTemplate, Quantity, UnitState, CheckGameState);
}

/// <summary>
/// wrapper function: original function from base game LW/Community highlander
//  Return true to the actual DLC hook
/// </summary>
static private function bool CanAddItemToInventory(out int bCanAddItem, const EInventorySlot Slot, const X2ItemTemplate ItemTemplate, int Quantity, XComGameState_Unit UnitState, XComGameState CheckGameState, optional XComGameState_Item ItemState)
{
	return false;
}

//end Issue #50


Functionality to auto-destroy template mutators (if desired)

class TestCWRTemplate extends UTMutator
	dependson(TestCentralWeaponReplacement)
	hidedropdown
	abstract;

var array<TemplateInfo> WeaponsToReplace;
var array<TemplateInfo> AmmoToReplace;

var const bool bAutoDestroy;

event PreBeginPlay()
{
	local array<TemplateDynamicInfo> Replacements;
	local int i;
	local Object Registrar;

	super.PreBeginPlay();

	if (!IsPendingKill())
	{
		Registrar = GetRegistrar();
		RegisterByArray(Registrar, WeaponsToReplace, false);
		RegisterByArray(Registrar, AmmoToReplace, true);

		StaticGetDynamicReplacements(Replacements);
		for (i=0; i<Replacements.Length; i++)
		{
			RegisterByInfo(Registrar, Replacements[i].Template, Replacements[i].bAmmo);
		}

		if (bAutoDestroy)
		{
			Destroy();
		}
	}
}

event Destroyed()
{
	if (!bAutoDestroy)
	{
		Unregister(self);
	}
	Super.Destroyed();
}

protected final function Object GetRegistrar()
{
	return bAutoDestroy ? class : self;
}

static private final function bool RegisterByArray(Object Registrar, array<TemplateInfo> ItemsToReplace, bool bAmmo, optional bool bPre, optional bool bOnlyCheck, optional out string ErrorMessage)
{
	local int i;

	for (i=0; i<ItemsToReplace.Length; i++)
	{
		if (!RegisterByInfo(Registrar, ItemsToReplace[i], bAmmo, bPre, bOnlyCheck, ErrorMessage))
		{
			return false;
		}
	}

	return true;
}

static private final function bool RegisterByInfo(Object Registrar, TemplateInfo item, bool bAmmo, optional bool bPre, optional bool bOnlyCheck, optional out string ErrorMessage)
{
	local string path;

	path = item.NewClassPath;
	if (item.AddPackage && Registrar != none) path = (class(Registrar) != none ? Registrar.GetPackageName() : Registrar.Class.GetPackageName())$"."$path;

	if (!RegisterWeaponReplacement(Registrar, item.OldClassName, path, bAmmo, item.Options, bPre, bOnlyCheck, ErrorMessage))
	{
		if (bOnlyCheck)
		{
			return false;
		}
	}

	return true;
}

static protected final function bool RegisterWeaponReplacement(Object Registrar, coerce name OldClassName, string NewClassPath, bool bAmmo, optional ReplacementOptionsInfo ReplacementOptions, optional bool bPre, optional bool bOnlyCheck, optional out string ErrorMessage)
{
	return class'TestCentralWeaponReplacement'.static.StaticRegisterWeaponReplacement(Registrar, OldClassName, NewClassPath, bAmmo, ReplacementOptions, bPre, bOnlyCheck, ErrorMessage);
}

static private final function bool Unregister(Object Registrar, optional bool bPre)
{
	return class'TestCentralWeaponReplacement'.static.StaticUnRegisterWeaponReplacement(Registrar, bPre);
}

static private final function Update(Object Registrar, bool bBatchOp, optional bool bPre)
{
	class'TestCentralWeaponReplacement'.static.StaticUpdateWeaponReplacement(Registrar, bBatchOp, bPre);
}

//**********************************************************************************
// UI related static interface functions
//**********************************************************************************

// For UI support to check for conflict (called by UI menu)
static function string Localize( string SectionName, string KeyName, string PackageName )
{
	local string ErrorMessage;
	if (KeyName == "" && PackageName == "")
	{
		if (SectionName ~= "IsConflicting")
		{
			return StaticIsConflicting(ErrorMessage) ? "1"$Chr(10)$ErrorMessage : "0";
		}
		else if (SectionName ~= "PreAdd")
		{
			StaticInitialize();
			return "1";
		}
		else if (SectionName ~= "PreRemove")
		{
			StaticDestroy();
			return "1";
		}
		else if (SectionName ~= "PreUpdate")
		{
			StaticUpdate();
			return "1";
		}
	}

	return super.Localize(SectionName, KeyName, PackageName);
}

static protected function bool StaticIsConflicting(optional out string ErrorMessage)
{
	local bool bConflicting;
	local array<TemplateDynamicInfo> Replacements;
	local int i;

	bConflicting = bConflicting || !RegisterByArray(default.Class, default.WeaponsToReplace, false, true, true, ErrorMessage);
	bConflicting = bConflicting || !RegisterByArray(default.Class, default.AmmoToReplace, true, true, true, ErrorMessage);

	if (!bConflicting)
	{
		StaticGetDynamicReplacements(Replacements);
		for (i=0; i<Replacements.Length; i++)
		{
			if (!RegisterByInfo(default.Class, Replacements[i].Template, Replacements[i].bAmmo, true, true, ErrorMessage))
			{
				bConflicting = true;
				break;
			}
		}
	}

	return bConflicting;
}

static protected function StaticInitialize()
{
	local array<TemplateDynamicInfo> Replacements;
	local int i;

	RegisterByArray(default.Class, default.WeaponsToReplace, false, true);
	RegisterByArray(default.Class, default.AmmoToReplace, true, true);

	StaticGetDynamicReplacements(Replacements);
	for (i=0; i<Replacements.Length; i++)
	{
		RegisterByInfo(default.Class, Replacements[i].Template, Replacements[i].bAmmo, true);
	}
}

static protected function StaticDestroy()
{
	Unregister(default.Class, true);
}

static private final function StaticUpdate()
{
	Update(default.Class, true, true);
	StaticDestroy();
	StaticInitialize();
	Update(default.Class, false, true);
}

// override for dynamic replacements
static function StaticGetDynamicReplacements(out array<TemplateDynamicInfo> Replacements);

//**********************************************************************************
// Dynamic static interface functions
//**********************************************************************************

static protected final function TemplateDynamicInfo CreateTemplate(bool bAmmo, coerce name OldClassName, string NewClassPath, optional ReplacementOptionsInfo Options)
{
	local TemplateDynamicInfo item;
	item.bAmmo = bAmmo;
	item.Template.OldClassName = OldClassName;
	item.Template.NewClassPath = NewClassPath;
	item.Template.Options = Options;
	return item;
}

DefaultProperties
{
}

class TestCWRTemplate extends UTMutator
	dependson(TestCentralWeaponReplacement)
	hidedropdown
	abstract;

var array<TemplateInfo> WeaponsToReplace;
var array<TemplateInfo> AmmoToReplace;

event PreBeginPlay()
{
	local array<TemplateDynamicInfo> Replacements;
	local int i;

	super.PreBeginPlay();

	if (!IsPendingKill())
	{
		RegisterByArray(self, WeaponsToReplace, false);
		RegisterByArray(self, AmmoToReplace, true);

		StaticGetDynamicReplacements(Replacements);
		for (i=0; i<Replacements.Length; i++)
		{
			RegisterByInfo(self, Replacements[i].Template, Replacements[i].bAmmo);
		}
	}
}

event Destroyed()
{
	Unregister(self);
	Super.Destroyed();
}

static private final function bool RegisterByArray(Object Registrar, array<TemplateInfo> ItemsToReplace, bool bAmmo, optional bool bPre, optional bool bOnlyCheck, optional out string ErrorMessage)
{
	local int i;

	for (i=0; i<ItemsToReplace.Length; i++)
	{
		if (!RegisterByInfo(Registrar, ItemsToReplace[i], bAmmo, bPre, bOnlyCheck, ErrorMessage))
		{
			return false;
		}
	}

	return true;
}

static private final function bool RegisterByInfo(Object Registrar, TemplateInfo item, bool bAmmo, optional bool bPre, optional bool bOnlyCheck, optional out string ErrorMessage)
{
	local string path;

	path = item.NewClassPath;
	if (item.AddPackage && Registrar != none) path = (class(Registrar) != none ? Registrar.GetPackageName() : Registrar.Class.GetPackageName())$"."$path;

	if (!RegisterWeaponReplacement(Registrar, item.OldClassName, path, bAmmo, item.Options, bPre, bOnlyCheck, ErrorMessage))
	{
		if (bOnlyCheck)
		{
			return false;
		}
	}

	return true;
}

static protected final function bool RegisterWeaponReplacement(Object Registrar, coerce name OldClassName, string NewClassPath, bool bAmmo, optional ReplacementOptionsInfo ReplacementOptions, optional bool bPre, optional bool bOnlyCheck, optional out string ErrorMessage)
{
	return class'TestCentralWeaponReplacement'.static.StaticRegisterWeaponReplacement(Registrar, OldClassName, NewClassPath, bAmmo, ReplacementOptions, bPre, bOnlyCheck, ErrorMessage);
}

static private final function bool Unregister(Object Registrar, optional bool bPre)
{
	return class'TestCentralWeaponReplacement'.static.StaticUnRegisterWeaponReplacement(Registrar, bPre);
}

static private final function Update(Object Registrar, bool bBatchOp, optional bool bPre)
{
	class'TestCentralWeaponReplacement'.static.StaticUpdateWeaponReplacement(Registrar, bBatchOp, bPre);
}

//**********************************************************************************
// UI related static interface functions
//**********************************************************************************

// For UI support to check for conflict (called by UI menu)
static function string Localize( string SectionName, string KeyName, string PackageName )
{
	local string ErrorMessage;
	if (KeyName == "" && PackageName == "")
	{
		if (SectionName ~= "IsConflicting")
		{
			return StaticIsConflicting(ErrorMessage) ? "1"$Chr(10)$ErrorMessage : "0";
		}
		else if (SectionName ~= "PreAdd")
		{
			StaticInitialize();
			return "1";
		}
		else if (SectionName ~= "PreRemove")
		{
			StaticDestroy();
			return "1";
		}
		else if (SectionName ~= "PreUpdate")
		{
			StaticUpdate();
			return "1";
		}
	}

	return super.Localize(SectionName, KeyName, PackageName);
}

static protected function bool StaticIsConflicting(optional out string ErrorMessage)
{
	local bool bConflicting;
	local array<TemplateDynamicInfo> Replacements;
	local int i;

	bConflicting = bConflicting || !RegisterByArray(default.Class, default.WeaponsToReplace, false, true, true, ErrorMessage);
	bConflicting = bConflicting || !RegisterByArray(default.Class, default.AmmoToReplace, true, true, true, ErrorMessage);

	if (!bConflicting)
	{
		StaticGetDynamicReplacements(Replacements);
		for (i=0; i<Replacements.Length; i++)
		{
			if (!RegisterByInfo(default.Class, Replacements[i].Template, Replacements[i].bAmmo, true, true, ErrorMessage))
			{
				bConflicting = true;
				break;
			}
		}
	}

	return bConflicting;
}

static protected function StaticInitialize()
{
	local array<TemplateDynamicInfo> Replacements;
	local int i;

	RegisterByArray(default.Class, default.WeaponsToReplace, false, true);
	RegisterByArray(default.Class, default.AmmoToReplace, true, true);

	StaticGetDynamicReplacements(Replacements);
	for (i=0; i<Replacements.Length; i++)
	{
		RegisterByInfo(default.Class, Replacements[i].Template, Replacements[i].bAmmo, true);
	}
}

static protected function StaticDestroy()
{
	Unregister(default.Class, true);
}

static private final function StaticUpdate()
{
	Update(default.Class, true, true);
	StaticDestroy();
	StaticInitialize();
	Update(default.Class, false, true);
}

// override for dynamic replacements
static function StaticGetDynamicReplacements(out array<TemplateDynamicInfo> Replacements);

//**********************************************************************************
// Dynamic static interface functions
//**********************************************************************************

static protected final function TemplateDynamicInfo CreateTemplate(bool bAmmo, coerce name OldClassName, string NewClassPath, optional ReplacementOptionsInfo Options)
{
	local TemplateDynamicInfo item;
	item.bAmmo = bAmmo;
	item.Template.OldClassName = OldClassName;
	item.Template.NewClassPath = NewClassPath;
	item.Template.Options = Options;
	return item;
}

DefaultProperties
{
}


Updated documentation based on robojumper's suggestions in discord

//---------------------------------------------------------------------------------------
//  FILE:    X2EventListener_DefaultWillEvents.uc
//  AUTHOR:  David Burchanowski
//
//---------------------------------------------------------------------------------------
//  Copyright (c) 2016 Firaxis Games, Inc. All rights reserved.
//---------------------------------------------------------------------------------------

class XComGameStateContext_WillRoll extends XComGameStateContext
	config(GameCore)
	native(Core);

enum WillEventRoll_StatType
{
	WillEventRollStat_None,
	WillEventRollStat_PercentageHealthLost,
	WillEventRollStat_SquadmateRank,
	WillEventRollStat_MaxWill,
	WillEventRollStat_BondLevel,
	WillEventRollStat_Flat
};

struct native WillEventRollData_PanicWeight
{
	var name PanicAbilityName;
	var float Weight; // 0.0-1.0

	structdefaultproperties
	{
		Weight = 1
	}
};

// This structure provides a simple uniform mechanism for doing rolls to lose will
struct native WillEventRollData
{
	var EMentalState MinimumTiredState;  // this will data will not be applied to units that are less fatigued than this state
	var float WillEventActivationChance;  // if all other conditions are met, this is the chance that this will event should actually modify will/roll on panic
	var name WillEventActivationType;	// if non-none, will events of this type are restricted to only happening once per unit per mission

	// values for determining how much will to lose
	var float WillLossChance;    // chance to lose will.
	var bool FlatWillLossChance; // if true, will do a straight roll against WillLossChance, if false rolls against WillLossChance - eStat_Will

	// calculated will loss is the value of the chosen WillLossStat *  WillLossStatMultiplier.
	// the final will loss is max(<calculated will loss>, MinimumWillLoss)
	var WillEventRoll_StatType WillLossStat;
	var float WillLossStatMultiplier;
	var int MinimumWillLoss;

	var float MaxWillPercentageLostPerMission; // if > 0, will will only be lost up to this percentage of the unit's max will
	var bool CanZeroOutWill; // if false, will always leave at least 1 will remaining

	// When a unit loses will, a roll will be made against PanicChance. If the roll passes,
	// one of the abilities in PanicWeights will be selected.
	var float PanicChance; // 0.0-1.0f, will be further reduced by a unit's will
	var array<WillEventRollData_PanicWeight> PanicWeights;

	structdefaultproperties
	{
		MinimumTiredState = eMentalState_Ready
		WillEventActivationChance = 1.0
	}
};

// text that appears above soldier's head when he loses will from a will event
var localized const string LostWillFlyover;
var localized const string LostWillWorldMessage;
var localized const string DefaultLossSource;
var localized const string PanicTestCaption;
var localized const string PanicResultFlyover;
var localized const string PanicResultWorldMessage;
var localized const string ResistedText;

// Percentage of will to lose per bond level when checking WillEventRollStat_BondLevel
var protected const config array<float>      BondLevelWillLoss;

// the reduction in will loss when this soldier is on a mission with his bondmate
var protected const config float SoldierBondWillReductionMultiplier;

// If a soldier does a "normal" panic ability more than once, reroll using the values in this table
var protected const config array<WillEventRollData_PanicWeight> MultiplePanicAltWeights;

// It's possible to do multiple rolls per context, so keep track of the running total here.
var privatewrite int RunningWillLossTotal;

// the type of will event being conducted
var privatewrite name WillEventType;

// It's possible to get multiple panic results from different rolls, so just use the most recent one
var privatewrite name PanicAbilityName;

// The unit we are performing the roll on.
var privatewrite XComGameState_Unit SourceUnit;
// The target id our source unit is rolling against.
var privatewrite int TargetUnitID;

// Localized friendly name of the roll source. Ex. "Fear of Missing Shots"
var protectedwrite string RollSourceFriendly;
var protectedwrite name RollSource;

// If true, will show flyovers and world messages
var protectedwrite bool ShowMessages;

// If the target unit is affected by any of these Effects, skip the panic roll
var protected const config array<name>      SkipPanicTestWhenAfflictedByEffects;

//////////////////////////////////////////////////////////////////////////////////

static event bool ShouldPerformWillRoll(const out WillEventRollData RollInfo, XComGameState_Unit AffectedUnit)
{
	if( AffectedUnit == none )
	{
		return false;
	}

	// skip this test on any dead, or evacuated units
	if( AffectedUnit.IsDead() ||
	   AffectedUnit.IsInStasis() ||
	   AffectedUnit.bRemovedFromPlay )
	{
		return false;
	}

	// raw random chance to skip this activation
	if( `SYNC_FRAND_STATIC() >= RollInfo.WillEventActivationChance )
	{
		return false;
	}

	return true;
}

static function bool ShouldPerformPanicRoll(const out WillEventRollData RollInfo, XComGameState_Unit AffectedUnit)
{
	local name TestEffect;

	if( RollInfo.PanicChance <= 0.0 )
	{
		return false;
	}

	// restrict rolling on will while panicked
	if( !AffectedUnit.IsAbleToAct() )
	{
		return false;
	}

	// restrict by the units current fatigue state
	if( AffectedUnit.GetMentalState() > RollInfo.MinimumTiredState )
	{
		return false;
	}

	// restrict panic checks to once per turn
	if( AffectedUnit.PanicTestsPerformedThisTurn > 0 )
	{
		return false;
	}

	// restrict if the unit has already rolled on this event type this mission
	if( RollInfo.WillEventActivationType != '' && AffectedUnit.WillEventsActivatedThisMission.Find(RollInfo.WillEventActivationType) != INDEX_NONE )
	{
		return false;
	}

	// Avoid panic tests on units affected by certain status effects
	foreach default.SkipPanicTestWhenAfflictedByEffects(TestEffect)
	{
		if( AffectedUnit.AffectedByEffectNames.Find(TestEffect) != INDEX_NONE )
		{
			return false;
		}
	}

	// skip the roll for panic for any condition that would fail a panic ability
	if( class'X2Condition_Panic'.static.StaticCallMeetsCondition(AffectedUnit) != 'AA_Success' )
	{
		return false;
	}

	return true;
}

// "SquadmateUnit" needs to be supplied when doing rolls against squadmate rank
event DoWillRoll(WillEventRollData RollInfo, optional XComGameState_Unit SquadmateUnit)
{
	if( SquadmateUnit == None )
	{
		SquadmateUnit = SourceUnit;
	}

	TargetUnitID = SquadmateUnit.ObjectID;

	WillEventType = RollInfo.WillEventActivationType;

	CalculateWillRoll(RollInfo, SquadmateUnit, SourceUnit, RollSource, RunningWillLossTotal, PanicAbilityName);

	if( ShouldPerformPanicRoll(RollInfo, SquadmateUnit) )
	{
		// also make a panic roll
		DoPanicRoll(SquadmateUnit, RunningWillLossTotal, RollInfo.PanicChance, RollInfo.PanicWeights, false, PanicAbilityName);
	}
	else
	{
		// never show messages if there was no panic roll
		ShowMessages = false;
	}
}

static function PerformWillRollOnUnitForNewGameState(WillEventRollData RollInfo, XComGameState_Unit InSourceUnit, name InRollSource, XComGameState NewGameState)
{
	local int RunningWillLoss;
	local name PanicAbility;
	local XComGameState_Unit NewSourceUnitState;

	CalculateWillRoll(RollInfo, None, InSourceUnit, InRollSource, RunningWillLoss, PanicAbility);

	NewSourceUnitState = XComGameState_Unit(NewGameState.ModifyStateObject(class'XComGameState_Unit', InSourceUnit.ObjectId));
	NewSourceUnitState.ModifyCurrentStat(eStat_Will, -RunningWillLoss);
	++NewSourceUnitState.PanicTestsPerformedThisTurn;

	if( RollInfo.WillEventActivationType != '' && NewSourceUnitState.WillEventsActivatedThisMission.Find(RollInfo.WillEventActivationType) == INDEX_NONE )
	{
		NewSourceUnitState.WillEventsActivatedThisMission.AddItem(RollInfo.WillEventActivationType);
	}
}

static function CalculateWillRoll(WillEventRollData RollInfo, XComGameState_Unit SquadmateUnit, XComGameState_Unit InSourceUnit, name InRollSource, out int InRunningWillLossTotal, out name InPanicAbilityName)
{
	local XComGameStateHistory History;
	local XComGameState_Unit StartOfMissionUnit;
	local bool ShouldLoseWill;
	local float CalculatedWillLoss;
	local int WillLossOverage;
	local float WillLossRemainder;
	local int CurrentWill; // current will, including the running loss total
	local int MaximumWillLossPerMission;
	local int MinimumAllowedWill;
	local SoldierBond BondData;
	local bool bBondmateOnMission;
	local StateObjectReference BondmateRef;
	local XComGameState_BattleData BattleDataState;
	local X2SitRepEffect_ModifyWillPenalties SitRepEffect;
	local float SitRepWillLossScalar;
	// Start Issue #44
	local float WillStartOfMission;
	local UnitValue WillStartOfMissionUnitVal;
	// End Issue #44

	if( InSourceUnit.IsDead()
		|| InSourceUnit.IsIncapacitated()
		|| InSourceUnit.IsMindControlled())
	{
		return;
	}

	if( InSourceUnit.UsesWillSystem())
	{
		if(RollInfo.FlatWillLossChance)
		{
			ShouldLoseWill = class'Engine'.static.SyncFRand("DoWillRoll") < RollInfo.WillLossChance;
		}
		else
		{
			ShouldLoseWill = class'Engine'.static.SyncFRand("DoWillRoll") < RollInfo.WillLossChance - (InSourceUnit.GetCurrentStat(eStat_Will) * 0.01f);
		}
	}

	if(ShouldLoseWill)
	{
		bBondmateOnMission = InSourceUnit.HasSoldierBond(BondmateRef, BondData) && `XCOMHQ.IsUnitInSquad(BondmateRef);

		// determine how much will we want to lose
		switch(RollInfo.WillLossStat)
		{
		case WillEventRollStat_None:
			CalculatedWillLoss = 0;
			break;
		case WillEventRollStat_PercentageHealthLost:
			CalculatedWillLoss = InSourceUnit.GetCurrentStat(eStat_HP) / InSourceUnit.GetMaxStat(eStat_HP);
			break;
		case WillEventRollStat_SquadmateRank:
			assert(SquadmateUnit != none);
			CalculatedWillLoss = SquadmateUnit.GetSoldierRank();
			break;
		case WillEventRollStat_MaxWill:
			CalculatedWillLoss = InSourceUnit.GetMaxStat(eStat_Will);
			break;
		case WillEventRollStat_BondLevel:
			if( bBondmateOnMission )
			{
				if(BondData.BondLevel < 0 || BondData.BondLevel > default.BondLevelWillLoss.Length)
				{
					`Redscreen("DoWillRoll(): " $ InSourceUnit.GetFullName() $ "has an unexpected bond level " $ BondData.BondLevel);
					BondData.BondLevel = 0;
				}
				CalculatedWillLoss = InSourceUnit.GetMaxStat(eStat_Will) * default.BondLevelWillLoss[BondData.BondLevel];
			}
			break;
		// Start Issue #936
		/// HL-Docs: feature:AdditionalWillRollStats; issue:936; tags:tactical
        /// This feature consists of new or previously unused elements, like
        /// `WillEventRollStat_Flat`, of `enum WillEventRoll_StatType` to the
        /// switch block in `CalculateWillRoll`.
        ///
        /// The `WillEventRollStat_Flat` case sets `CalculatedWillLoss` to
        /// `1.0f`. This allows it to be modified by `SitRepWillLossScalar`
        /// and `RollInfo.WillLossStatMultiplier`, which is impossible to do
        /// using `WillEventRollStat_None`.
        ///
        /// ```ini
        /// ; XComGameCore.ini
        /// [XComGame.X2EventListener_DefaultWillEvents]
        /// ExampleWillRollData=(WillLossChance=1, \\
        ///                      FlatWillLossChance=true, \\
        ///                      WillLossStat=WillEventRollStat_Flat, \\
        ///                      WillLossStatMultiplier=4, \\
        ///                      MaxWillPercentageLostPerMission=0.33)
        /// ```
		case WillEventRollStat_Flat:
			CalculatedWillLoss = 1.0f;
			break;
		// End Issue #936
		default:
			`assert(false);
		}

		// Gather sitrep granted abilities
		BattleDataState = XComGameState_BattleData(`XCOMHISTORY.GetSingleGameStateObjectForClass(class'XComGameState_BattleData'));
		SitRepWillLossScalar = 1.0f;
		if (BattleDataState != none)
		{
			foreach class'X2SitreptemplateManager'.static.IterateEffects(class'X2SitRepEffect_ModifyWillPenalties', SitRepEffect, BattleDataState.ActiveSitReps)
			{
				if ((SitRepEffect.WillEventNames.Length == 0) || (SitRepEffect.WillEventNames.Find(InRollSource) != INDEX_NONE))
				{
					SitRepWillLossScalar *= SitRepEffect.ModifyScalar;
				}
			}
		}

		CalculatedWillLoss = (CalculatedWillLoss * RollInfo.WillLossStatMultiplier * SitRepWillLossScalar);

		if( bBondmateOnMission )
		{
			CalculatedWillLoss *= default.SoldierBondWillReductionMultiplier;
		}

		// if we have a remainder, roll on it for an extra integer point
		WillLossRemainder = CalculatedWillLoss % 1.0f;
		if(class'Engine'.static.SyncFRand("DoWillRoll") < WillLossRemainder)
		{
			CalculatedWillLoss += 1;
		}

		// make sure we are taking at least the minimum amount
		CalculatedWillLoss = max(int(CalculatedWillLoss), RollInfo.MinimumWillLoss);

		// since the history hasn't been updated yet, add in the will loss from previous rolls
		CurrentWill = InSourceUnit.GetCurrentStat(eStat_Will) - InRunningWillLossTotal;

		// and determine if we have any lower floor on how much will we are allowed to lose in the mission.
		// by default, we can go all the way to zero
		MinimumAllowedWill = 0;
		if(!RollInfo.CanZeroOutWill)
		{
			// don't allow dropping below 1
			MinimumAllowedWill = 1;
		}

		if(RollInfo.MaxWillPercentageLostPerMission > 0)
		{
			// Start Issue #44
			// If we want to use the changed behavior (no reset of Start-of-mission Will on Tactical-To-Tactical Transfer),
			// check if we have the UnitValue recorded -- otherwise it might break when we add the Highlander in the middle of a mission
			if (class'CHHelpers'.default.MULTIPART_MISSION_WILL_LOSS_CAP && InSourceUnit.GetUnitValue('CH_StartMissionWill', WillStartOfMissionUnitVal))
			{
				WillStartOfMission = WillStartOfMissionUnitVal.fValue;
			}
			else
			{
				// get the unit's will at the start of the mission
				History = `XCOMHISTORY;
				StartOfMissionUnit = XComGameState_Unit(History.GetGameStateForObjectID(InSourceUnit.ObjectID,, History.FindStartStateIndex()));
				WillStartOfMission = StartOfMissionUnit.GetCurrentStat(eStat_Will);
			}
			MaximumWillLossPerMission = RollInfo.MaxWillPercentageLostPerMission * InSourceUnit.GetMaxStat(eStat_Will);
			MinimumAllowedWill = max(MinimumAllowedWill, WillStartOfMission - MaximumWillLossPerMission);
			// End Issue #44
		}

		// now that we know our lower floor, make sure we won't drop below the minimum
		WillLossOverage = CurrentWill - (CalculatedWillLoss + MinimumAllowedWill);
		CalculatedWillLoss -= Max(0, -WillLossOverage);

		// add in the loss for this roll to the running total
		InRunningWillLossTotal += CalculatedWillLoss;
	}
}

static function DoPanicRoll(XComGameState_Unit InSourceUnit, int InRunningWillLossTotal, float PanicChance, array<WillEventRollData_PanicWeight> PanicWeights, bool Reroll, out name InPanicAbilityName)
{
	local int CurrentWill;
	local float PanicRoll;
	local WillEventRollData_PanicWeight PanicWeight;
	local float TotalPanicWeight;

	CurrentWill = InSourceUnit.GetCurrentStat(eStat_Will) - InRunningWillLossTotal;

	PanicRoll = `SYNC_FRAND_STATIC("DoWillRoll");
	if((PanicRoll < (PanicChance - (CurrentWill * 0.01)))
		|| CurrentWill <= 0
		|| (`CHEATMGR != none && `CHEATMGR.bAlwaysPanic))
	{
		// do a weighted pick from the available choices. Add up the total weight of all options,
		// do a random roll from [0..TotalWeight], and then start adding up the weights again.
		// when the total goes over the random number, we have our choice.
		foreach PanicWeights(PanicWeight)
		{
			TotalPanicWeight += PanicWeight.Weight;
		}

		PanicRoll = `SYNC_FRAND_STATIC("DoWillRoll") * TotalPanicWeight;

		TotalPanicWeight = 0.0f;
		foreach PanicWeights(PanicWeight)
		{
			TotalPanicWeight += PanicWeight.Weight;
			if(TotalPanicWeight >= PanicRoll)
			{
				InPanicAbilityName = PanicWeight.PanicAbilityName;

				// To favor the new panic behaviors, if we roll a normal panic and this unit has already
				// done a normal panic on this mission, reroll (but only once)
				if(!Reroll
					&& InPanicAbilityName == 'Panicked'
					&& UnitHasAlreadyUsedNormalPanicThisMission(InSourceUnit))
				{
					DoPanicRoll(InSourceUnit, InRunningWillLossTotal, PanicChance, default.MultiplePanicAltWeights, true, InPanicAbilityName);
				}

				break;
			}
		}
	}
}

// Does what it says, nativized since it can require a lot of iteration late into missions.
static native function bool UnitHasAlreadyUsedNormalPanicThisMission(XComGameState_Unit InSourceUnit);

function bool Validate(optional EInterruptionStatus InInterruptionStatus)
{
	return true;
}

function XComGameState ContextBuildGameState()
{
	local XComGameState NewGameState;
	local XComGameState_Unit UnitState;
	local X2EventManager Manager;
	local Object SelfObj;

	// Add the units to the new game state
	NewGameState = `XCOMHISTORY.CreateNewGameState(true, self);

	UnitState = XComGameState_Unit(NewGameState.ModifyStateObject(SourceUnit.Class, SourceUnit.ObjectId));
	UnitState.ModifyCurrentStat(eStat_Will, -RunningWillLossTotal);

	if( WillEventType != '' && UnitState.WillEventsActivatedThisMission.Find(WillEventType) == INDEX_NONE )
	{
		UnitState.WillEventsActivatedThisMission.AddItem(WillEventType);
	}

	SelfObj = self;
	Manager = `XEVENTMGR;

	// trigger an event to do our panic behavior if one was selected. We need to wait for this context
	// to complete before we can do the panic, and an event will serve that purpose just fine
	if(PanicAbilityName != '')
	{
		Manager.RegisterForEvent(SelfObj, 'DoPanicAbility', OnDoPanicAbility, ELD_OnStateSubmitted);
		Manager.TriggerEvent('DoPanicAbility', UnitState, self, NewGameState);
	}

	NewGameState.GetContext().SetAssociatedPlayTiming(SPT_AfterSequential);

	// Start Issue #13
	Manager.TriggerEvent('WillRollContext', UnitState, self, NewGameState);
	// End Issue #13

	return NewGameState;
}

function EventListenerReturn OnDoPanicAbility(Object EventData, Object EventSource, XComGameState GameState, Name Event, Object CallbackData)
{
	local XComGameStateHistory History;
	local GameRulesCache_Unit UnitCache;
	local XComGameState_Unit UnitState;
	local XComGameState_Ability AbilityState;
	local int i;

	History = `XCOMHISTORY;

	UnitState = XComGameState_Unit(EventData);
	if(EventSource != self)
	{
		return ELR_NoInterrupt;
	}

	// find the specified panic ability and activate it
	if (`TACTICALRULES.GetGameRulesCache_Unit(UnitState.GetReference(), UnitCache))
	{
		for (i = 0; i < UnitCache.AvailableActions.Length; ++i)
		{
			AbilityState = XComGameState_Ability(History.GetGameStateForObjectID(UnitCache.AvailableActions[i].AbilityObjectRef.ObjectID));
			if (AbilityState.GetMyTemplateName() == PanicAbilityName)
			{
				if (UnitCache.AvailableActions[i].AvailableCode == 'AA_Success' && UnitCache.AvailableActions[i].AvailableTargets.Length > 0)
				{
					class'XComGameStateContext_Ability'.static.ActivateAbility(UnitCache.AvailableActions[i], 0, , , , , , , SPT_AfterSequential);
				}
				break;
			}
		}
	}

	`XEVENTMGR.UnRegisterFromEvent(EventSource, 'DoPanicAbility');
	return ELR_NoInterrupt;
}

protected function ContextBuildVisualization()
{
	local VisualizationActionMetadata ActionMetadata, EmptyMetadata;
	local X2Action_PlaySoundAndFlyover FlyoverAction;
	//local X2Action_PlayMessageBanner WorldMessageAction;
	local XComGameState_Unit UnitState;
	local XGParamTag Tag;
	local X2Action_RevealArea RevealAreaAction;
	local XComWorldData WorldData;
	local TTile TileLocation;
	local Vector WorldLocation;
	local X2Action_CameraLookAt LookAtAction;
	local X2Action_StartStopSound SoundAction;
	local X2Action_UpdateUI UIUpdateAction;
	local X2AbilityTemplate AbilityTemplate;
	local X2Action_Delay DelayAction;
	local X2Action_CentralBanner BannerAction;

	// add a flyover track for every unit in the game state. This timing of this is too late to look good
	// but since Ryan is tearing it all out there is no reason to augment the visualization system to allow
	// it to look pretty
	if(ShowMessages)
	{
		WorldData = `XWORLD;

		Tag = XGParamTag(`XEXPANDCONTEXT.FindTag("XGParam"));
		Tag.StrValue0 = RollSourceFriendly;
		Tag.IntValue0 = RunningWillLossTotal;

		if( PanicAbilityName == '' )
		{
			Tag.StrValue2 = ResistedText;
		}
		else
		{
			AbilityTemplate = class'XComGameState_Ability'.static.GetMyTemplateManager().FindAbilityTemplate(PanicAbilityName);
			Tag.StrValue2 = AbilityTemplate.LocFriendlyName;
		}

		// clear unnecessary HUD components
		UIUpdateAction = X2Action_UpdateUI(class'X2Action_UpdateUI'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
		UIUpdateAction.UpdateType = EUIUT_SetHUDVisibility;
		UIUpdateAction.DesiredHUDVisibility.bMessageBanner = true;

		foreach AssociatedState.IterateByClassType(class'XComGameState_Unit', UnitState)
		{
			UnitState.GetKeystoneVisibilityLocation(TileLocation);
			WorldLocation = WorldData.GetPositionFromTileCoordinates(TileLocation);

			Tag.StrValue1 = UnitState.GetFullName();

			ActionMetadata = EmptyMetadata;
			ActionMetadata.StateObject_OldState = UnitState.GetPreviousVersion();
			ActionMetadata.StateObject_NewState = UnitState;

			Tag.StrValue0 = RollSourceFriendly;

			// new visualization flow:
			// for each affected unit:
			// 1a) Clear FOW around unit
			RevealAreaAction = X2Action_RevealArea(class'X2Action_RevealArea'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			RevealAreaAction.ScanningRadius = class'XComWorldData'.const.WORLD_StepSize * 5.0f;
			RevealAreaAction.TargetLocation = WorldLocation;
			RevealAreaAction.bDestroyViewer = false;
			RevealAreaAction.AssociatedObjectID = UnitState.ObjectID;

			// 1b) Focus camera on unit (wait until centered)
			LookAtAction = X2Action_CameraLookAt(class'X2Action_CameraLookAt'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			LookAtAction.LookAtLocation = WorldLocation;
			LookAtAction.BlockUntilActorOnScreen = true;
			LookAtAction.LookAtDuration = 10.0f;		// longer than we need - camera will be removed by tag below
			LookAtAction.TargetZoomAfterArrival = -0.7f;
			LookAtAction.CameraTag = 'WillTestCamera';
			LookAtAction.bRemoveTaggedCamera = false;

			// 2a) Raise Special Event overlay "Breaking Point: <RollSource>"
			BannerAction = X2Action_CentralBanner(class'X2Action_CentralBanner'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			BannerAction.BannerText = `XEXPAND.ExpandString(PanicTestCaption);
			BannerAction.BannerState = eUIState_Normal;

			// 2b) Play will loss update on UnitFlag will bar
			UIUpdateAction = X2Action_UpdateUI(class'X2Action_UpdateUI'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			UIUpdateAction.SpecificID = UnitState.ObjectID;
			UIUpdateAction.UpdateType = EUIUT_UnitFlag_Health;

			// 2c) Trigger Panic Event buildup audio FX
			SoundAction = X2Action_StartStopSound(class'X2Action_StartStopSound'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			SoundAction.Sound = new class'SoundCue';
			SoundAction.Sound.AkEventOverride = AkEvent'XPACK_SoundTacticalUI.Panic_Check_Start';
			SoundAction.bIsPositional = false;
			SoundAction.vWorldPosition = WorldLocation;
			SoundAction.WaitForCompletion = true;

			// 3a) Wait 0.5s - or until SoundAction is complete
			DelayAction = X2Action_Delay(class'X2Action_Delay'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			DelayAction.Duration = 2.5;
			DelayAction.bIgnoreZipMode = true;

			// 4a) Update Special Event overlay to show panic results
			BannerAction = X2Action_CentralBanner(class'X2Action_CentralBanner'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			BannerAction.BannerText = `XEXPAND.ExpandString(PanicResultFlyover);
			if( PanicAbilityName == '' )
			{
				BannerAction.BannerState = eUIState_Good;
			}
			else
			{
				BannerAction.BannerState = eUIState_Bad;
			}

			// 4b) Trigger Result flyover (ex. "Resisted", "Panicked")
			// Update 4/20 - only trigger if the result was "resisted"
			if( PanicAbilityName == '' )
			{
				FlyoverAction = X2Action_PlaySoundAndFlyover(class'X2Action_PlaySoundAndFlyover'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
				FlyoverAction.SetSoundAndFlyOverParameters(none, `XEXPAND.ExpandString(PanicResultFlyover), '', eColor_Good);
			}

			// 4c) Trigger result audio FX
			SoundAction = X2Action_StartStopSound(class'X2Action_StartStopSound'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			SoundAction.Sound = new class'SoundCue';
			if( PanicAbilityName == '' )
			{
				// good result sound
				SoundAction.Sound.AkEventOverride = AkEvent'SoundTacticalUI.TacticalUI_UnitFlagPositive';
			}
			else
			{
				// bad result sound
				SoundAction.Sound.AkEventOverride = AkEvent'SoundTacticalUI.TacticalUI_UnitFlagWarning';
			}
			SoundAction.bIsPositional = false;
			SoundAction.vWorldPosition = WorldLocation;
			SoundAction.WaitForCompletion = false;

			// 5a) complete panic behavior visualization (if applicable)
			// - for now, will just rely on the panic processing to complete the behavior visualization

			// 5a) Wait 1.5s
			DelayAction = X2Action_Delay(class'X2Action_Delay'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			DelayAction.bIgnoreZipMode = true;
			if( PanicAbilityName == '' )
			{
				// if the panic was resisted, there will not be a panic visualization following this action,
				// so we need to give the user more time to enjoy the "resisted" flyover
				DelayAction.Duration = 3.0;
			}
			else
			{
				DelayAction.Duration = 1.5;
			}

			// 6a) Lower Special Event overlay
			BannerAction = X2Action_CentralBanner(class'X2Action_CentralBanner'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			BannerAction.BannerText = "";

			// 6b) restore FOW
			RevealAreaAction = X2Action_RevealArea(class'X2Action_RevealArea'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			RevealAreaAction.bDestroyViewer = true;
			RevealAreaAction.AssociatedObjectID = UnitState.ObjectID;

			// 6c) release camera
			LookAtAction = X2Action_CameraLookAt(class'X2Action_CameraLookAt'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			LookAtAction.CameraTag = 'WillTestCamera';
			LookAtAction.bRemoveTaggedCamera = true;

			// 6d) Play world message explaining what just happened
			// Update 4/20 - no banner needed when resisting; the panic state will handle the banner when the will check is failed
			//WorldMessageAction = X2Action_PlayMessageBanner(class'X2Action_PlayMessageBanner'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			//WorldMessageAction.AddMessageBanner(class'UIEventNoticesTactical'.default.PanicCheckTitle,
			//							   /*class'UIUtilities_Image'.const.UnitStatus_Panicked*/,
			//								UnitState.GetName(eNameType_RankFull),
			//							   `XEXPAND.ExpandString(PanicResultWorldMessage),
			//							   (PanicAbilityName == '') ? eUIState_Good : eUIState_Bad);
		}

		// restore HUD components to previous vis state
		UIUpdateAction = X2Action_UpdateUI(class'X2Action_UpdateUI'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
		UIUpdateAction.UpdateType = EUIUT_RestoreHUDVisibility;
	}
}

static event XComGameStateContext_WillRoll CreateWillRollContext(XComGameState_Unit InSourceUnit, name InRollSource, optional string InRollSourceFriendly, optional bool InShowMessages = true)
{
	local XComGameStateContext_WillRoll RollContext;

	`assert(InSourceUnit != none);
	RollContext = XComGameStateContext_WillRoll(CreateXComGameStateContext());
	RollContext.SourceUnit = InSourceUnit;
	RollContext.RollSource = InRollSource;
	RollContext.RollSourceFriendly = InRollSourceFriendly == "" ? default.DefaultLossSource : InRollSourceFriendly;
	RollContext.ShowMessages = InShowMessages;
	return RollContext;
}

event Submit()
{
	local X2TacticalGameRuleset Rules;

	if(SourceUnit.IsDead() || SourceUnit.IsIncapacitated() || SourceUnit.IsMindControlled())
	{
		return;
	}

	if(RunningWillLossTotal > 0 || PanicAbilityName != '')
	{
		Rules = `TACTICALRULES;
		if(Rules == none)
		{
			`Redscreen("Attempting to submit a will roll outside of tactical.");
			return;
		}

		if(!Rules.SubmitGameStateContext(self))
		{
			`Redscreen("Unable to submit will roll context.");
		}
	}
}

function string SummaryString()
{
	return "Will Roll: " $ SourceUnit.GetFullName() $ " lost " $ RunningWillLossTotal $ " Will";
}

//---------------------------------------------------------------------------------------
//  FILE:    X2EventListener_DefaultWillEvents.uc
//  AUTHOR:  David Burchanowski
//
//---------------------------------------------------------------------------------------
//  Copyright (c) 2016 Firaxis Games, Inc. All rights reserved.
//---------------------------------------------------------------------------------------

class XComGameStateContext_WillRoll extends XComGameStateContext
	config(GameCore)
	native(Core);

enum WillEventRoll_StatType
{
	WillEventRollStat_None,
	WillEventRollStat_PercentageHealthLost,
	WillEventRollStat_SquadmateRank,
	WillEventRollStat_MaxWill,
	WillEventRollStat_BondLevel,
	WillEventRollStat_Flat
};

struct native WillEventRollData_PanicWeight
{
	var name PanicAbilityName;
	var float Weight; // 0.0-1.0

	structdefaultproperties
	{
		Weight = 1
	}
};

// This structure provides a simple uniform mechanism for doing rolls to lose will
struct native WillEventRollData
{
	var EMentalState MinimumTiredState;  // this will data will not be applied to units that are less fatigued than this state
	var float WillEventActivationChance;  // if all other conditions are met, this is the chance that this will event should actually modify will/roll on panic
	var name WillEventActivationType;	// if non-none, will events of this type are restricted to only happening once per unit per mission

	// values for determining how much will to lose
	var float WillLossChance;    // chance to lose will.
	var bool FlatWillLossChance; // if true, will do a straight roll against WillLossChance, if false rolls against WillLossChance - eStat_Will

	// calculated will loss is the value of the chosen WillLossStat *  WillLossStatMultiplier.
	// the final will loss is max(<calculated will loss>, MinimumWillLoss)
	var WillEventRoll_StatType WillLossStat;
	var float WillLossStatMultiplier;
	var int MinimumWillLoss;

	var float MaxWillPercentageLostPerMission; // if > 0, will will only be lost up to this percentage of the unit's max will
	var bool CanZeroOutWill; // if false, will always leave at least 1 will remaining

	// When a unit loses will, a roll will be made against PanicChance. If the roll passes,
	// one of the abilities in PanicWeights will be selected.
	var float PanicChance; // 0.0-1.0f, will be further reduced by a unit's will
	var array<WillEventRollData_PanicWeight> PanicWeights;

	structdefaultproperties
	{
		MinimumTiredState = eMentalState_Ready
		WillEventActivationChance = 1.0
	}
};

// text that appears above soldier's head when he loses will from a will event
var localized const string LostWillFlyover;
var localized const string LostWillWorldMessage;
var localized const string DefaultLossSource;
var localized const string PanicTestCaption;
var localized const string PanicResultFlyover;
var localized const string PanicResultWorldMessage;
var localized const string ResistedText;

// Percentage of will to lose per bond level when checking WillEventRollStat_BondLevel
var protected const config array<float>      BondLevelWillLoss;

// the reduction in will loss when this soldier is on a mission with his bondmate
var protected const config float SoldierBondWillReductionMultiplier;

// If a soldier does a "normal" panic ability more than once, reroll using the values in this table
var protected const config array<WillEventRollData_PanicWeight> MultiplePanicAltWeights;

// It's possible to do multiple rolls per context, so keep track of the running total here.
var privatewrite int RunningWillLossTotal;

// the type of will event being conducted
var privatewrite name WillEventType;

// It's possible to get multiple panic results from different rolls, so just use the most recent one
var privatewrite name PanicAbilityName;

// The unit we are performing the roll on.
var privatewrite XComGameState_Unit SourceUnit;
// The target id our source unit is rolling against.
var privatewrite int TargetUnitID;

// Localized friendly name of the roll source. Ex. "Fear of Missing Shots"
var protectedwrite string RollSourceFriendly;
var protectedwrite name RollSource;

// If true, will show flyovers and world messages
var protectedwrite bool ShowMessages;

// If the target unit is affected by any of these Effects, skip the panic roll
var protected const config array<name>      SkipPanicTestWhenAfflictedByEffects;

//////////////////////////////////////////////////////////////////////////////////

static event bool ShouldPerformWillRoll(const out WillEventRollData RollInfo, XComGameState_Unit AffectedUnit)
{
	if( AffectedUnit == none )
	{
		return false;
	}

	// skip this test on any dead, or evacuated units
	if( AffectedUnit.IsDead() ||
	   AffectedUnit.IsInStasis() ||
	   AffectedUnit.bRemovedFromPlay )
	{
		return false;
	}

	// raw random chance to skip this activation
	if( `SYNC_FRAND_STATIC() >= RollInfo.WillEventActivationChance )
	{
		return false;
	}

	return true;
}

static function bool ShouldPerformPanicRoll(const out WillEventRollData RollInfo, XComGameState_Unit AffectedUnit)
{
	local name TestEffect;

	if( RollInfo.PanicChance <= 0.0 )
	{
		return false;
	}

	// restrict rolling on will while panicked
	if( !AffectedUnit.IsAbleToAct() )
	{
		return false;
	}

	// restrict by the units current fatigue state
	if( AffectedUnit.GetMentalState() > RollInfo.MinimumTiredState )
	{
		return false;
	}

	// restrict panic checks to once per turn
	if( AffectedUnit.PanicTestsPerformedThisTurn > 0 )
	{
		return false;
	}

	// restrict if the unit has already rolled on this event type this mission
	if( RollInfo.WillEventActivationType != '' && AffectedUnit.WillEventsActivatedThisMission.Find(RollInfo.WillEventActivationType) != INDEX_NONE )
	{
		return false;
	}

	// Avoid panic tests on units affected by certain status effects
	foreach default.SkipPanicTestWhenAfflictedByEffects(TestEffect)
	{
		if( AffectedUnit.AffectedByEffectNames.Find(TestEffect) != INDEX_NONE )
		{
			return false;
		}
	}

	// skip the roll for panic for any condition that would fail a panic ability
	if( class'X2Condition_Panic'.static.StaticCallMeetsCondition(AffectedUnit) != 'AA_Success' )
	{
		return false;
	}

	return true;
}

// "SquadmateUnit" needs to be supplied when doing rolls against squadmate rank
event DoWillRoll(WillEventRollData RollInfo, optional XComGameState_Unit SquadmateUnit)
{
	if( SquadmateUnit == None )
	{
		SquadmateUnit = SourceUnit;
	}

	TargetUnitID = SquadmateUnit.ObjectID;

	WillEventType = RollInfo.WillEventActivationType;

	CalculateWillRoll(RollInfo, SquadmateUnit, SourceUnit, RollSource, RunningWillLossTotal, PanicAbilityName);

	if( ShouldPerformPanicRoll(RollInfo, SquadmateUnit) )
	{
		// also make a panic roll
		DoPanicRoll(SquadmateUnit, RunningWillLossTotal, RollInfo.PanicChance, RollInfo.PanicWeights, false, PanicAbilityName);
	}
	else
	{
		// never show messages if there was no panic roll
		ShowMessages = false;
	}
}

static function PerformWillRollOnUnitForNewGameState(WillEventRollData RollInfo, XComGameState_Unit InSourceUnit, name InRollSource, XComGameState NewGameState)
{
	local int RunningWillLoss;
	local name PanicAbility;
	local XComGameState_Unit NewSourceUnitState;

	CalculateWillRoll(RollInfo, None, InSourceUnit, InRollSource, RunningWillLoss, PanicAbility);

	NewSourceUnitState = XComGameState_Unit(NewGameState.ModifyStateObject(class'XComGameState_Unit', InSourceUnit.ObjectId));
	NewSourceUnitState.ModifyCurrentStat(eStat_Will, -RunningWillLoss);
	++NewSourceUnitState.PanicTestsPerformedThisTurn;

	if( RollInfo.WillEventActivationType != '' && NewSourceUnitState.WillEventsActivatedThisMission.Find(RollInfo.WillEventActivationType) == INDEX_NONE )
	{
		NewSourceUnitState.WillEventsActivatedThisMission.AddItem(RollInfo.WillEventActivationType);
	}
}

static function CalculateWillRoll(WillEventRollData RollInfo, XComGameState_Unit SquadmateUnit, XComGameState_Unit InSourceUnit, name InRollSource, out int InRunningWillLossTotal, out name InPanicAbilityName)
{
	local XComGameStateHistory History;
	local XComGameState_Unit StartOfMissionUnit;
	local bool ShouldLoseWill;
	local float CalculatedWillLoss;
	local int WillLossOverage;
	local float WillLossRemainder;
	local int CurrentWill; // current will, including the running loss total
	local int MaximumWillLossPerMission;
	local int MinimumAllowedWill;
	local SoldierBond BondData;
	local bool bBondmateOnMission;
	local StateObjectReference BondmateRef;
	local XComGameState_BattleData BattleDataState;
	local X2SitRepEffect_ModifyWillPenalties SitRepEffect;
	local float SitRepWillLossScalar;
	// Start Issue #44
	local float WillStartOfMission;
	local UnitValue WillStartOfMissionUnitVal;
	// End Issue #44

	if( InSourceUnit.IsDead()
		|| InSourceUnit.IsIncapacitated()
		|| InSourceUnit.IsMindControlled())
	{
		return;
	}

	if( InSourceUnit.UsesWillSystem())
	{
		if(RollInfo.FlatWillLossChance)
		{
			ShouldLoseWill = class'Engine'.static.SyncFRand("DoWillRoll") < RollInfo.WillLossChance;
		}
		else
		{
			ShouldLoseWill = class'Engine'.static.SyncFRand("DoWillRoll") < RollInfo.WillLossChance - (InSourceUnit.GetCurrentStat(eStat_Will) * 0.01f);
		}
	}

	if(ShouldLoseWill)
	{
		bBondmateOnMission = InSourceUnit.HasSoldierBond(BondmateRef, BondData) && `XCOMHQ.IsUnitInSquad(BondmateRef);

		// determine how much will we want to lose
		switch(RollInfo.WillLossStat)
		{
		case WillEventRollStat_None:
			CalculatedWillLoss = 0;
			break;
		case WillEventRollStat_PercentageHealthLost:
			CalculatedWillLoss = InSourceUnit.GetCurrentStat(eStat_HP) / InSourceUnit.GetMaxStat(eStat_HP);
			break;
		case WillEventRollStat_SquadmateRank:
			assert(SquadmateUnit != none);
			CalculatedWillLoss = SquadmateUnit.GetSoldierRank();
			break;
		case WillEventRollStat_MaxWill:
			CalculatedWillLoss = InSourceUnit.GetMaxStat(eStat_Will);
			break;
		case WillEventRollStat_BondLevel:
			if( bBondmateOnMission )
			{
				if(BondData.BondLevel < 0 || BondData.BondLevel > default.BondLevelWillLoss.Length)
				{
					`Redscreen("DoWillRoll(): " $ InSourceUnit.GetFullName() $ "has an unexpected bond level " $ BondData.BondLevel);
					BondData.BondLevel = 0;
				}
				CalculatedWillLoss = InSourceUnit.GetMaxStat(eStat_Will) * default.BondLevelWillLoss[BondData.BondLevel];
			}
			break;
		// Start Issue #936
		/// HL-Docs: feature:AdditionalWillRollStats; issue:936; tags:tactical
		/// Unused element `WillEventRollStat_Flat` of `enum WillEventRoll_StatType` added to the switch block in `CalculateWillRoll`.
		/// The `WillEventRollStat_Flat` case sets `CalculatedWillLoss` to `1.0f` so that it can be adjusted by `RollInfo.WillLossStatMultiplier` and `SitRepWillLossScalar`.
		case WillEventRollStat_Flat:
			CalculatedWillLoss = 1.0f;
			break;
		// End Issue #936
		default:
			`assert(false);
		}

		// Gather sitrep granted abilities
		BattleDataState = XComGameState_BattleData(`XCOMHISTORY.GetSingleGameStateObjectForClass(class'XComGameState_BattleData'));
		SitRepWillLossScalar = 1.0f;
		if (BattleDataState != none)
		{
			foreach class'X2SitreptemplateManager'.static.IterateEffects(class'X2SitRepEffect_ModifyWillPenalties', SitRepEffect, BattleDataState.ActiveSitReps)
			{
				if ((SitRepEffect.WillEventNames.Length == 0) || (SitRepEffect.WillEventNames.Find(InRollSource) != INDEX_NONE))
				{
					SitRepWillLossScalar *= SitRepEffect.ModifyScalar;
				}
			}
		}

		CalculatedWillLoss = (CalculatedWillLoss * RollInfo.WillLossStatMultiplier * SitRepWillLossScalar);

		if( bBondmateOnMission )
		{
			CalculatedWillLoss *= default.SoldierBondWillReductionMultiplier;
		}

		// if we have a remainder, roll on it for an extra integer point
		WillLossRemainder = CalculatedWillLoss % 1.0f;
		if(class'Engine'.static.SyncFRand("DoWillRoll") < WillLossRemainder)
		{
			CalculatedWillLoss += 1;
		}

		// make sure we are taking at least the minimum amount
		CalculatedWillLoss = max(int(CalculatedWillLoss), RollInfo.MinimumWillLoss);

		// since the history hasn't been updated yet, add in the will loss from previous rolls
		CurrentWill = InSourceUnit.GetCurrentStat(eStat_Will) - InRunningWillLossTotal;

		// and determine if we have any lower floor on how much will we are allowed to lose in the mission.
		// by default, we can go all the way to zero
		MinimumAllowedWill = 0;
		if(!RollInfo.CanZeroOutWill)
		{
			// don't allow dropping below 1
			MinimumAllowedWill = 1;
		}

		if(RollInfo.MaxWillPercentageLostPerMission > 0)
		{
			// Start Issue #44
			// If we want to use the changed behavior (no reset of Start-of-mission Will on Tactical-To-Tactical Transfer),
			// check if we have the UnitValue recorded -- otherwise it might break when we add the Highlander in the middle of a mission
			if (class'CHHelpers'.default.MULTIPART_MISSION_WILL_LOSS_CAP && InSourceUnit.GetUnitValue('CH_StartMissionWill', WillStartOfMissionUnitVal))
			{
				WillStartOfMission = WillStartOfMissionUnitVal.fValue;
			}
			else
			{
				// get the unit's will at the start of the mission
				History = `XCOMHISTORY;
				StartOfMissionUnit = XComGameState_Unit(History.GetGameStateForObjectID(InSourceUnit.ObjectID,, History.FindStartStateIndex()));
				WillStartOfMission = StartOfMissionUnit.GetCurrentStat(eStat_Will);
			}
			MaximumWillLossPerMission = RollInfo.MaxWillPercentageLostPerMission * InSourceUnit.GetMaxStat(eStat_Will);
			MinimumAllowedWill = max(MinimumAllowedWill, WillStartOfMission - MaximumWillLossPerMission);
			// End Issue #44
		}

		// now that we know our lower floor, make sure we won't drop below the minimum
		WillLossOverage = CurrentWill - (CalculatedWillLoss + MinimumAllowedWill);
		CalculatedWillLoss -= Max(0, -WillLossOverage);

		// add in the loss for this roll to the running total
		InRunningWillLossTotal += CalculatedWillLoss;
	}
}

static function DoPanicRoll(XComGameState_Unit InSourceUnit, int InRunningWillLossTotal, float PanicChance, array<WillEventRollData_PanicWeight> PanicWeights, bool Reroll, out name InPanicAbilityName)
{
	local int CurrentWill;
	local float PanicRoll;
	local WillEventRollData_PanicWeight PanicWeight;
	local float TotalPanicWeight;

	CurrentWill = InSourceUnit.GetCurrentStat(eStat_Will) - InRunningWillLossTotal;

	PanicRoll = `SYNC_FRAND_STATIC("DoWillRoll");
	if((PanicRoll < (PanicChance - (CurrentWill * 0.01)))
		|| CurrentWill <= 0
		|| (`CHEATMGR != none && `CHEATMGR.bAlwaysPanic))
	{
		// do a weighted pick from the available choices. Add up the total weight of all options,
		// do a random roll from [0..TotalWeight], and then start adding up the weights again.
		// when the total goes over the random number, we have our choice.
		foreach PanicWeights(PanicWeight)
		{
			TotalPanicWeight += PanicWeight.Weight;
		}

		PanicRoll = `SYNC_FRAND_STATIC("DoWillRoll") * TotalPanicWeight;

		TotalPanicWeight = 0.0f;
		foreach PanicWeights(PanicWeight)
		{
			TotalPanicWeight += PanicWeight.Weight;
			if(TotalPanicWeight >= PanicRoll)
			{
				InPanicAbilityName = PanicWeight.PanicAbilityName;

				// To favor the new panic behaviors, if we roll a normal panic and this unit has already
				// done a normal panic on this mission, reroll (but only once)
				if(!Reroll
					&& InPanicAbilityName == 'Panicked'
					&& UnitHasAlreadyUsedNormalPanicThisMission(InSourceUnit))
				{
					DoPanicRoll(InSourceUnit, InRunningWillLossTotal, PanicChance, default.MultiplePanicAltWeights, true, InPanicAbilityName);
				}

				break;
			}
		}
	}
}

// Does what it says, nativized since it can require a lot of iteration late into missions.
static native function bool UnitHasAlreadyUsedNormalPanicThisMission(XComGameState_Unit InSourceUnit);

function bool Validate(optional EInterruptionStatus InInterruptionStatus)
{
	return true;
}

function XComGameState ContextBuildGameState()
{
	local XComGameState NewGameState;
	local XComGameState_Unit UnitState;
	local X2EventManager Manager;
	local Object SelfObj;

	// Add the units to the new game state
	NewGameState = `XCOMHISTORY.CreateNewGameState(true, self);

	UnitState = XComGameState_Unit(NewGameState.ModifyStateObject(SourceUnit.Class, SourceUnit.ObjectId));
	UnitState.ModifyCurrentStat(eStat_Will, -RunningWillLossTotal);

	if( WillEventType != '' && UnitState.WillEventsActivatedThisMission.Find(WillEventType) == INDEX_NONE )
	{
		UnitState.WillEventsActivatedThisMission.AddItem(WillEventType);
	}

	SelfObj = self;
	Manager = `XEVENTMGR;

	// trigger an event to do our panic behavior if one was selected. We need to wait for this context
	// to complete before we can do the panic, and an event will serve that purpose just fine
	if(PanicAbilityName != '')
	{
		Manager.RegisterForEvent(SelfObj, 'DoPanicAbility', OnDoPanicAbility, ELD_OnStateSubmitted);
		Manager.TriggerEvent('DoPanicAbility', UnitState, self, NewGameState);
	}

	NewGameState.GetContext().SetAssociatedPlayTiming(SPT_AfterSequential);

	// Start Issue #13
	Manager.TriggerEvent('WillRollContext', UnitState, self, NewGameState);
	// End Issue #13

	return NewGameState;
}

function EventListenerReturn OnDoPanicAbility(Object EventData, Object EventSource, XComGameState GameState, Name Event, Object CallbackData)
{
	local XComGameStateHistory History;
	local GameRulesCache_Unit UnitCache;
	local XComGameState_Unit UnitState;
	local XComGameState_Ability AbilityState;
	local int i;

	History = `XCOMHISTORY;

	UnitState = XComGameState_Unit(EventData);
	if(EventSource != self)
	{
		return ELR_NoInterrupt;
	}

	// find the specified panic ability and activate it
	if (`TACTICALRULES.GetGameRulesCache_Unit(UnitState.GetReference(), UnitCache))
	{
		for (i = 0; i < UnitCache.AvailableActions.Length; ++i)
		{
			AbilityState = XComGameState_Ability(History.GetGameStateForObjectID(UnitCache.AvailableActions[i].AbilityObjectRef.ObjectID));
			if (AbilityState.GetMyTemplateName() == PanicAbilityName)
			{
				if (UnitCache.AvailableActions[i].AvailableCode == 'AA_Success' && UnitCache.AvailableActions[i].AvailableTargets.Length > 0)
				{
					class'XComGameStateContext_Ability'.static.ActivateAbility(UnitCache.AvailableActions[i], 0, , , , , , , SPT_AfterSequential);
				}
				break;
			}
		}
	}

	`XEVENTMGR.UnRegisterFromEvent(EventSource, 'DoPanicAbility');
	return ELR_NoInterrupt;
}

protected function ContextBuildVisualization()
{
	local VisualizationActionMetadata ActionMetadata, EmptyMetadata;
	local X2Action_PlaySoundAndFlyover FlyoverAction;
	//local X2Action_PlayMessageBanner WorldMessageAction;
	local XComGameState_Unit UnitState;
	local XGParamTag Tag;
	local X2Action_RevealArea RevealAreaAction;
	local XComWorldData WorldData;
	local TTile TileLocation;
	local Vector WorldLocation;
	local X2Action_CameraLookAt LookAtAction;
	local X2Action_StartStopSound SoundAction;
	local X2Action_UpdateUI UIUpdateAction;
	local X2AbilityTemplate AbilityTemplate;
	local X2Action_Delay DelayAction;
	local X2Action_CentralBanner BannerAction;

	// add a flyover track for every unit in the game state. This timing of this is too late to look good
	// but since Ryan is tearing it all out there is no reason to augment the visualization system to allow
	// it to look pretty
	if(ShowMessages)
	{
		WorldData = `XWORLD;

		Tag = XGParamTag(`XEXPANDCONTEXT.FindTag("XGParam"));
		Tag.StrValue0 = RollSourceFriendly;
		Tag.IntValue0 = RunningWillLossTotal;

		if( PanicAbilityName == '' )
		{
			Tag.StrValue2 = ResistedText;
		}
		else
		{
			AbilityTemplate = class'XComGameState_Ability'.static.GetMyTemplateManager().FindAbilityTemplate(PanicAbilityName);
			Tag.StrValue2 = AbilityTemplate.LocFriendlyName;
		}

		// clear unnecessary HUD components
		UIUpdateAction = X2Action_UpdateUI(class'X2Action_UpdateUI'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
		UIUpdateAction.UpdateType = EUIUT_SetHUDVisibility;
		UIUpdateAction.DesiredHUDVisibility.bMessageBanner = true;

		foreach AssociatedState.IterateByClassType(class'XComGameState_Unit', UnitState)
		{
			UnitState.GetKeystoneVisibilityLocation(TileLocation);
			WorldLocation = WorldData.GetPositionFromTileCoordinates(TileLocation);

			Tag.StrValue1 = UnitState.GetFullName();

			ActionMetadata = EmptyMetadata;
			ActionMetadata.StateObject_OldState = UnitState.GetPreviousVersion();
			ActionMetadata.StateObject_NewState = UnitState;

			Tag.StrValue0 = RollSourceFriendly;

			// new visualization flow:
			// for each affected unit:
			// 1a) Clear FOW around unit
			RevealAreaAction = X2Action_RevealArea(class'X2Action_RevealArea'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			RevealAreaAction.ScanningRadius = class'XComWorldData'.const.WORLD_StepSize * 5.0f;
			RevealAreaAction.TargetLocation = WorldLocation;
			RevealAreaAction.bDestroyViewer = false;
			RevealAreaAction.AssociatedObjectID = UnitState.ObjectID;

			// 1b) Focus camera on unit (wait until centered)
			LookAtAction = X2Action_CameraLookAt(class'X2Action_CameraLookAt'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			LookAtAction.LookAtLocation = WorldLocation;
			LookAtAction.BlockUntilActorOnScreen = true;
			LookAtAction.LookAtDuration = 10.0f;		// longer than we need - camera will be removed by tag below
			LookAtAction.TargetZoomAfterArrival = -0.7f;
			LookAtAction.CameraTag = 'WillTestCamera';
			LookAtAction.bRemoveTaggedCamera = false;

			// 2a) Raise Special Event overlay "Breaking Point: <RollSource>"
			BannerAction = X2Action_CentralBanner(class'X2Action_CentralBanner'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			BannerAction.BannerText = `XEXPAND.ExpandString(PanicTestCaption);
			BannerAction.BannerState = eUIState_Normal;

			// 2b) Play will loss update on UnitFlag will bar
			UIUpdateAction = X2Action_UpdateUI(class'X2Action_UpdateUI'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			UIUpdateAction.SpecificID = UnitState.ObjectID;
			UIUpdateAction.UpdateType = EUIUT_UnitFlag_Health;

			// 2c) Trigger Panic Event buildup audio FX
			SoundAction = X2Action_StartStopSound(class'X2Action_StartStopSound'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			SoundAction.Sound = new class'SoundCue';
			SoundAction.Sound.AkEventOverride = AkEvent'XPACK_SoundTacticalUI.Panic_Check_Start';
			SoundAction.bIsPositional = false;
			SoundAction.vWorldPosition = WorldLocation;
			SoundAction.WaitForCompletion = true;

			// 3a) Wait 0.5s - or until SoundAction is complete
			DelayAction = X2Action_Delay(class'X2Action_Delay'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			DelayAction.Duration = 2.5;
			DelayAction.bIgnoreZipMode = true;

			// 4a) Update Special Event overlay to show panic results
			BannerAction = X2Action_CentralBanner(class'X2Action_CentralBanner'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			BannerAction.BannerText = `XEXPAND.ExpandString(PanicResultFlyover);
			if( PanicAbilityName == '' )
			{
				BannerAction.BannerState = eUIState_Good;
			}
			else
			{
				BannerAction.BannerState = eUIState_Bad;
			}

			// 4b) Trigger Result flyover (ex. "Resisted", "Panicked")
			// Update 4/20 - only trigger if the result was "resisted"
			if( PanicAbilityName == '' )
			{
				FlyoverAction = X2Action_PlaySoundAndFlyover(class'X2Action_PlaySoundAndFlyover'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
				FlyoverAction.SetSoundAndFlyOverParameters(none, `XEXPAND.ExpandString(PanicResultFlyover), '', eColor_Good);
			}

			// 4c) Trigger result audio FX
			SoundAction = X2Action_StartStopSound(class'X2Action_StartStopSound'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			SoundAction.Sound = new class'SoundCue';
			if( PanicAbilityName == '' )
			{
				// good result sound
				SoundAction.Sound.AkEventOverride = AkEvent'SoundTacticalUI.TacticalUI_UnitFlagPositive';
			}
			else
			{
				// bad result sound
				SoundAction.Sound.AkEventOverride = AkEvent'SoundTacticalUI.TacticalUI_UnitFlagWarning';
			}
			SoundAction.bIsPositional = false;
			SoundAction.vWorldPosition = WorldLocation;
			SoundAction.WaitForCompletion = false;

			// 5a) complete panic behavior visualization (if applicable)
			// - for now, will just rely on the panic processing to complete the behavior visualization

			// 5a) Wait 1.5s
			DelayAction = X2Action_Delay(class'X2Action_Delay'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			DelayAction.bIgnoreZipMode = true;
			if( PanicAbilityName == '' )
			{
				// if the panic was resisted, there will not be a panic visualization following this action,
				// so we need to give the user more time to enjoy the "resisted" flyover
				DelayAction.Duration = 3.0;
			}
			else
			{
				DelayAction.Duration = 1.5;
			}

			// 6a) Lower Special Event overlay
			BannerAction = X2Action_CentralBanner(class'X2Action_CentralBanner'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			BannerAction.BannerText = "";

			// 6b) restore FOW
			RevealAreaAction = X2Action_RevealArea(class'X2Action_RevealArea'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			RevealAreaAction.bDestroyViewer = true;
			RevealAreaAction.AssociatedObjectID = UnitState.ObjectID;

			// 6c) release camera
			LookAtAction = X2Action_CameraLookAt(class'X2Action_CameraLookAt'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			LookAtAction.CameraTag = 'WillTestCamera';
			LookAtAction.bRemoveTaggedCamera = true;

			// 6d) Play world message explaining what just happened
			// Update 4/20 - no banner needed when resisting; the panic state will handle the banner when the will check is failed
			//WorldMessageAction = X2Action_PlayMessageBanner(class'X2Action_PlayMessageBanner'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
			//WorldMessageAction.AddMessageBanner(class'UIEventNoticesTactical'.default.PanicCheckTitle,
			//							   /*class'UIUtilities_Image'.const.UnitStatus_Panicked*/,
			//								UnitState.GetName(eNameType_RankFull),
			//							   `XEXPAND.ExpandString(PanicResultWorldMessage),
			//							   (PanicAbilityName == '') ? eUIState_Good : eUIState_Bad);
		}

		// restore HUD components to previous vis state
		UIUpdateAction = X2Action_UpdateUI(class'X2Action_UpdateUI'.static.AddToVisualizationTree(ActionMetadata, self, false, ActionMetadata.LastActionAdded));
		UIUpdateAction.UpdateType = EUIUT_RestoreHUDVisibility;
	}
}

static event XComGameStateContext_WillRoll CreateWillRollContext(XComGameState_Unit InSourceUnit, name InRollSource, optional string InRollSourceFriendly, optional bool InShowMessages = true)
{
	local XComGameStateContext_WillRoll RollContext;

	`assert(InSourceUnit != none);
	RollContext = XComGameStateContext_WillRoll(CreateXComGameStateContext());
	RollContext.SourceUnit = InSourceUnit;
	RollContext.RollSource = InRollSource;
	RollContext.RollSourceFriendly = InRollSourceFriendly == "" ? default.DefaultLossSource : InRollSourceFriendly;
	RollContext.ShowMessages = InShowMessages;
	return RollContext;
}

event Submit()
{
	local X2TacticalGameRuleset Rules;

	if(SourceUnit.IsDead() || SourceUnit.IsIncapacitated() || SourceUnit.IsMindControlled())
	{
		return;
	}

	if(RunningWillLossTotal > 0 || PanicAbilityName != '')
	{
		Rules = `TACTICALRULES;
		if(Rules == none)
		{
			`Redscreen("Attempting to submit a will roll outside of tactical.");
			return;
		}

		if(!Rules.SubmitGameStateContext(self))
		{
			`Redscreen("Unable to submit will roll context.");
		}
	}
}

function string SummaryString()
{
	return "Will Roll: " $ SourceUnit.GetFullName() $ " lost " $ RunningWillLossTotal $ " Will";
}