Lucubration Posted March 12, 2016 Share Posted March 12, 2016 (edited) Hey, folks. I've been working on a new pattern for abilities here that I thought would interest other modders in our community. For want of a better name, I'm privately calling it the "Transient Item" pattern. This pattern is designed to emulate the old EW perks that would grant more uses of an item during a mission, whether or not the soldier has the item equipped. In my investigations, I found no support for that sort of functionality in the core game scripts, so I put together this pattern to emulate it. This pattern should also enable you to create soldier abilities that use item mechanics (grenades, etc), such as with a number of the existing alien abilities. The upshot in this case is that you don't need to include those items as actual inventory items in the Player's HQ to be equipped during loadout. One caveat: this particular example that I've worked up is only set up for utility items with charges right now (Flashbangs, Battle Scanners, Plasma Grenades, etc). I think that it could be applied to other use cases such as vests or ammo, or even secondary weapons with some careful manipulation, but I haven't gotten around to dealing with those cases yet. I'm going to walk through and explain the pattern below in a set of example classes that create a Battle Scanner soldier ability. I believe all of the gamestate objects are handled properly in this example code; it certainly seems to work in my testing, but I invite anyone to bring it under scrutiny to make sure I'm not missing something that'll break the gamestate history, etc. The pattern consists of three classes:A custom soldier ability template.A custom effect.A custom gamestate object.The later two classes in the pattern should be re-usable by any number of custom soldier ability templates. The first component is a soldier ability template that does three things:It adds the ability that is normally granted by the item to the soldier using the AdditionalAbilities array.It applies a persistent effect to the soldier at the mission start that increases their eStat_UtilityItems during the mission by the item size of the utility item (found on the template).It applies a persistent effect to the soldier at the mission start that manages the necessary gamestate objects. //--------------------------------------------------------------------------------------------------- // Ability template class //--------------------------------------------------------------------------------------------------- class X2Ability_ExampleMod_ExampleAbilitySet extends X2Ability config(ExampleMod_Ability); // This method is natively called for subclasses of X2DataSet. It'll create and return ability templates for our new class static function array<X2DataTemplate> CreateTemplates() { local array<X2DataTemplate> Templates; Templates.Length = 0; Templates.AddItem(BattleScanner()); return Templates; } static function X2AbilityTemplate BattleScanner() { local X2AbilityTemplate Template; local X2AbilityTargetStyle TargetStyle; local X2AbilityTrigger Trigger; local X2Effect_PersistentStatChange StatChangeEffect; local X2Effect_ExampleMod_TransientUtilityItem TransientItemEffect; `CREATE_X2ABILITY_TEMPLATE(Template, 'ExampleMod_BattleScanner'); // Give the normal BattleScanner ability so that it will always show up in tactical, even without the item equipped Template.AdditionalAbilities.AddItem('BattleScanner'); Template.AbilitySourceName = 'eAbilitySource_Perk'; Template.eAbilityIconBehaviorHUD = eAbilityIconBehavior_NeverShow; Template.Hostility = eHostility_Neutral; Template.IconImage = "img:///UILibrary_PerkIcons.UIPerk_item_battlescanner"; Template.AbilityToHitCalc = default.DeadEye; TargetStyle = new class'X2AbilityTarget_Self'; Template.AbilityTargetStyle = TargetStyle; Trigger = new class'X2AbilityTrigger_UnitPostBeginPlay'; Template.AbilityTriggers.AddItem(Trigger); // Expand the unit's utility items to allow the transient item. This goes before the transient item effect StatChangeEffect = new class'X2Effect_PersistentStatChange'; StatChangeEffect.EffectName = 'ExampleMod_TransientBattleScannerUtilitySlot'; StatChangeEffect.BuildPersistentEffect(1, true, false); StatChangeEffect.SetDisplayInfo(ePerkBuff_Passive, Template.LocFriendlyName, Template.GetMyLongDescription(), Template.IconImage, false,,Template.AbilitySourceName); StatChangeEffect.DuplicateResponse = eDupe_Ignore; StatChangeEffect.AddPersistentStatChange(eStat_UtilityItems, 1); // Can't think of any clever way to make this value based on the item template, so I'll just hardcode the item size for now Template.AddTargetEffect(StatChangeEffect); TransientItemEffect = new class'X2Effect_ExampleMod_TransientUtilityItem'; TransientItemEffect.EffectName = 'ExampleMod_TransientBattleScanner'; TransientItemEffect.BuildPersistentEffect(1, true, false); TransientItemEffect.SetDisplayInfo(ePerkBuff_Passive, Template.LocFriendlyName, Template.GetMyLongDescription(), Template.IconImage, false,,Template.AbilitySourceName); TransientItemEffect.DuplicateResponse = eDupe_Ignore; TransientItemEffect.AbilityTemplateName = 'BattleScanner'; TransientItemEffect.ItemTemplateName = 'BattleScanner'; Template.AddTargetEffect(TransientItemEffect); Template.BuildNewGameStateFn = TypicalAbility_BuildGameState; return Template; } The second component is a persistent effect that manages a custom gamestate object using the process described by Amineri in http://forums.nexusmods.com/index.php?/topic/3820875-tutorial-using-gamestate-components/.It also manages the extra item or item charges for the soldier:If the soldier has utility item in their inventory, it's dead simple to add extra ammo when the effect is applied to the soldier.If the soldier does not have the utility item in their inventory, I spawn one (the "transient item") when the effect is applied to the soldier, add it to the soldier's inventory, and assign it as the ability's source weapon. //--------------------------------------------------------------------------------------------------- // Effect class //--------------------------------------------------------------------------------------------------- class X2Effect_ExampleMod_TransientUtilityItem extends X2Effect_Persistent; var name AbilityTemplateName; var name ItemTemplateName; simulated protected function OnEffectAdded(const out EffectAppliedData ApplyEffectParameters, XComGameState_BaseObject kNewTargetState, XComGameState NewGameState, XComGameState_Effect NewEffectState) { local bool CreatedNewItem; local XComGameState_Unit UnitState; local XComGameState_Item ItemState; local X2WeaponTemplate WeaponTemplate; local XComGameState_ExampleMod_Effect_TransientUtilityItem TransientItemEffectState; local StateObjectReference AbilityRef; local XComGameState_Ability AbilityState; local X2EventManager EventMgr; local Object ListenerObj; if (GetEffectComponent(NewEffectState) == none) { UnitState = XComGameState_Unit(kNewTargetState); if (UnitState != none) { WeaponTemplate = X2WeaponTemplate(class'X2ItemTemplateManager'.static.GetItemTemplateManager().FindItemTemplate(ItemTemplateName)); if (WeaponTemplate != none && WeaponTemplate.InventorySlot == eInvSlot_Utility) { // Check for whether the unit has this item already ItemState = GetItemOfTemplateName(UnitState, ItemTemplateName); CreatedNewItem = false; if (ItemState == none) { // The unit doesn't have this item already (in which case we would add ammo), so we need to create it // Create an instance of the item ItemState = WeaponTemplate.CreateInstanceFromTemplate(NewGameState); NewGameState.AddStateObject(ItemState); CreatedNewItem = true; `LOG("ExampleMod: Transient Item " @ ItemState.GetMyTemplateName() @ " state created."); // Add the transient item to the GameState_Unit inventory, adding the new state object to the NewGameState container UnitState.AddItemToInventory(ItemState, eInvSlot_Utility, NewGameState); // At this point we've created the item and added it to the unit's inventory, but the ability still doesn't know about it. // Update the ability to have the source weapon reference (for charges in the UI) AbilityRef = UnitState.FindAbility(AbilityTemplateName); AbilityState = XComGameState_Ability(NewGameState.GetGameStateForObjectID(AbilityRef.ObjectID)); if (AbilityState == none) AbilityState = XComGameState_Ability(`XCOMHISTORY.GetGameStateForObjectID(AbilityRef.ObjectID)); if (AbilityState != none) { AbilityState.SourceWeapon.ObjectID = ItemState.ObjectID; `LOG("ExampleMod: Transient Item " @ ItemState.GetMyTemplateName() @ " set as source weapon for unit " @ UnitState.GetFullName() @ " ability " @ AbilityState.GetMyTemplateName() @ "."); } `LOG("ExampleMod: Transient Item " @ ItemState.GetMyTemplateName() @ " added to unit " @ UnitState.GetFullName() @ " inventory."); } else { // The unit has this item already, so we'll just add ammo to it if (WeaponTemplate != none && WeaponTemplate.bMergeAmmo) { ItemState.Ammo += WeaponTemplate.iClipSize; `LOG("ExampleMod: Transient Item " @ ItemState.GetMyTemplateName() @ " merged " @ string(WeaponTemplate.iClipSize) @ " ammo into unit " @ UnitState.GetFullName() @ " inventory."); } } // Create effect component and attach it to GameState_Effect, adding the new state object to the NewGameState container TransientItemEffectState = XComGameState_ExampleMod_Effect_TransientUtilityItem(NewGameState.CreateStateObject(class'XComGameState_ExampleMod_Effect_TransientUtilityItem')); if (CreatedNewItem) { // Only set these values if we created an item. If we didn't create an item, we don't want to store these and delete the item // later because it's a real item and not a transient item TransientItemEffectState.UnitRef = UnitState.GetReference(); TransientItemEffectState.ItemRef = ItemState.GetReference(); TransientItemEffectState.AbilityTemplateName = AbilityTemplateName; } NewEffectState.AddComponentObject(TransientItemEffectState); NewGameState.AddStateObject(TransientItemEffectState); EventMgr = `XEVENTMGR; // The gamestate component should handle the callback ListenerObj = TransientItemEffectState; // Some missions the effect will be removed (e.g. extraction), some missions the tactical gameplay just stops. We'll GC our gamestate // by having the gamestate itself handle this callback EventMgr.RegisterForEvent(ListenerObj, 'TacticalGameEnd', class'XComGameState_ExampleMod_Effect_TransientUtilityItem'.static.OnTacticalGameEnd, ELD_OnStateSubmitted); `LOG("ExampleMod: Transient Item " @ ItemState.GetMyTemplateName() @ " passive effect registered for events."); } } } super.OnEffectAdded(ApplyEffectParameters, kNewTargetState, NewGameState, NewEffectState); } static function XComGameState_ExampleMod_Effect_TransientUtilityItem GetEffectComponent(XComGameState_Effect Effect) { if (Effect != none) return XComGameState_ExampleMod_Effect_TransientUtilityItem(Effect.FindComponentObject(class'XComGameState_ExampleMod_Effect_TransientUtilityItem')); return none; } static function XComGameState_Item GetItemOfTemplateName(XComGameState_Unit UnitState, name TemplateName) { local array<XComGameState_Item> ItemStates; local XComGameState_Item ItemState; local int i; ItemStates = UnitState.GetAllInventoryItems(); for (i = 0; i < ItemStates.Length; ++i) { ItemState = ItemStates[i]; if (ItemState != none && ItemState.GetMyTemplateName() == TemplateName) return ItemState; } return none; } The third component is the custom gamestate object. At the end of the tactical battle, a callback on this object (that was registered when the custom effect was applied the soldier) will:Remove the "transient item" gamestate object from the gamestate history (if and only if it was spawned when the effect was added).Remove itself from the gamestate history. //--------------------------------------------------------------------------------------------------- // GameState object class //--------------------------------------------------------------------------------------------------- // Gamestate object that hangs around and eventually removes an item from a soldier's inventory at the end of a tactical game. // Using this to create totally fake items for abilities like Battlescanner class XComGameState_ExampleMod_Effect_TransientUtilityItem extends XComGameState_BaseObject; var StateObjectReference UnitRef, ItemRef; var name AbilityTemplateName; function EventListenerReturn OnTacticalGameEnd(Object EventData, Object EventSource, XComGameState GameState, Name EventID) { local XComGameStateHistory History; local X2EventManager EventManager; local Object ListenerObj; local XComGameState NewGameState; local XComGameState_Item ItemState; local XComGameState_Unit UnitState; local StateObjectReference AbilityRef; local XComGameState_Ability AbilityState; History = `XCOMHISTORY; EventManager = `XEVENTMGR; // Unregister our callbacks ListenerObj = self; EventManager.UnRegisterFromEvent(ListenerObj, 'TacticalGameEnd'); NewGameState = class'XComGameStateContext_ChangeContainer'.static.CreateChangeState("Transient Item states cleanup"); if (ItemRef.ObjectID > 0) { ItemState = XComGameState_Item(GameState.GetGameStateForObjectID(ItemRef.ObjectID)); if (ItemState == none) ItemState = XComGameState_Item(History.GetGameStateForObjectID(ItemRef.ObjectID)); if (ItemState != none) { UnitState = XComGameState_Unit(GameState.GetGameStateForObjectID(UnitRef.ObjectID)); if (UnitState == none) UnitState = XComGameState_Unit(History.GetGameStateForObjectID(UnitRef.ObjectID)); if (UnitState != none) { // Remove the item from the unit's inventory UnitState.RemoveItemFromInventory(ItemState); // Update the ability to remove the source weapon reference AbilityRef = UnitState.FindAbility(AbilityTemplateName); AbilityState = XComGameState_Ability(NewGameState.GetGameStateForObjectID(AbilityRef.ObjectID)); if (AbilityState == none) AbilityState = XComGameState_Ability(History.GetGameStateForObjectID(AbilityRef.ObjectID)); if (AbilityState != none) { AbilityState.SourceWeapon.ObjectID = 0; `LOG("ExampleMod: Transient Item " @ ItemState.GetMyTemplateName() @ " removed as source weapon for unit " @ UnitState.GetFullName() @ " ability " @ AbilityState.GetMyTemplateName() @ "."); } `LOG("ExampleMod: Transient Item " @ ItemState.GetMyTemplateName() @ " removed from unit " @ UnitState.GetFullName() @ " inventory."); } // Remove the transient item's gamestate object from history NewGameState.RemoveStateObject(ItemRef.ObjectID); `LOG("ExampleMod: Transient " @ ItemState.GetMyTemplateName() @ " state removed from history."); } } // Remove this gamestate object from history NewGameState.RemoveStateObject(ObjectID); `GAMERULES.SubmitGameState(NewGameState); `LOG("ExampleMod: Transient Item passive effect unregistered from events."); return ELR_NoInterrupt; } Edited March 12, 2016 by Lucubration Link to comment Share on other sites More sharing options...
traenol Posted March 12, 2016 Share Posted March 12, 2016 Nice, good work around for those 'perks' that kinda felt like bugs(aka features) to me. Link to comment Share on other sites More sharing options...
davidlallen Posted March 12, 2016 Share Posted March 12, 2016 Interesting. I was able to make the viper spit work, by adding it into the inventory location eInvSlot_Unknown. This may be less complicated. Link to comment Share on other sites More sharing options...
Lucubration Posted March 12, 2016 Author Share Posted March 12, 2016 I definitely had the Viper's poison spit in mind while working on this solution. Link to comment Share on other sites More sharing options...
Recommended Posts