If you have experience with script modding, then creating your own karma powers shouldn't be too difficult. You don't even need an instantiator!
This guide shows how to create a custom power from scratch using an example "Lightning Strike" power I've made. The final mod package is available on the main download page, and the full source for this project (as well as the main mod) is available
here.
Start by opening up the main mod in
S3PE and finding the S3SA resource named "Gamefreak130.WonderPowers". Right-click and select "Export DLL", then choose a location to save the file.
Next, follow the steps outlined
here or
here to set up a new script modding project in your IDE of choice. Once the project is set up, go to the references list and add a reference to the Gamefreak130.WonderPowers assembly you just exported. Your reference list should look similar to this:
Note:
Because this project directly references my karma powers mod, packages created using this tutorial will crash and/or otherwise break the game if the main mod is not installed. Make sure your users are aware of this!
Set the C# project aside for a moment and create a new package in S3PE. In it, add the following items:
- An _XML (0x0333406C) resource. The name/instance doesn't matter, but make a note of it, as it will be used later.
- An S3SA (0x073FAA07) resource, whose name/instance matches that of your C# project.
- One or more STBL (0x220557DA) resources for each language you plan to support. Again, the name/instance of these resources does not matter, except that the first two numbers of the instance hash must match the code of the language associated with that STBL. You can check out this tutorial for more info on STBLs and language codes.
The group for these and all other items in the package can simply be set to 0, unless you have a need for some other group value (e.g. when adding audio files).
In order to load powers into the game, the main mod relies on "Power Booters", which point to XML resources containing all the information the mod needs to add the power to the system. We'll start by filling out this power info XML so that later we can write the Booter that will point to it.
Start by right-clicking on the XML resource you created in the package, selecting "Text Editor", and copy-pasting the following into Notepad:
Code:
<?xml version="1.0" encoding="utf-8" ?>
<KarmaPowers>
<Power>
<ProductVersion>BaseGame</ProductVersion>
<PowerName></PowerName>
<IsBad></IsBad>
<Cost></Cost>
<EffectMethod></EffectMethod>
</Power>
</KarmaPowers>
Then, for each power you want to add, copy the "<Power>" tags and everything in between them, and paste below the first set of "<Power>" tags but within the "<KarmaPowers>" tags. Then, in between each set of tags inside the "<Power>" tags you pasted, enter the following information:
- ProductVersion: The expansion pack, if any, that must be installed for the power to become available. Internally, expansions start with "EP" and are numbered in the order they were originally released; valid entries include "BaseGame" if no expansions are required, "EP2" for Ambitions, and "EP11" for Into the Future.
- PowerName: The name used to refer to the power internally. It doesn't really matter what it is as long as it's unique. Make a note of it, as it will be used later.
- IsBad: True/False: Whether or not the power will appear as a "bad" power in-game, as opposed to a "good" one.
- Cost: The karma points required to purchase the power. Cannot exceed 200.
- EffectMethod: The static activation method the game will trigger when the karma power is activated. Leave this blank for now, we'll come back to it later.
Your XML should now look something like this:
Code:
<?xml version="1.0" encoding="utf-8" ?>
<KarmaPowers>
<Power>
<ProductVersion>BaseGame</ProductVersion>
<PowerName></PowerName>
<IsBad></IsBad>
<Cost></Cost>
<EffectMethod></EffectMethod>
</Power>
<Power>
<ProductVersion>EP8</ProductVersion>
<PowerName>LightningStrike</PowerName>
<IsBad>True</IsBad>
<Cost>20</Cost>
<EffectMethod></EffectMethod>
</Power>
</KarmaPowers>
Save the file and commit the changes.
Note:
While you cannot require multiple expansion packs for a power to become available, you can allow a power to support one of any number of expansions by creating duplicate entries in the XML, with each <ProductVersion> value set to a different expansion you want to support. An example of this is the "Lucky Find" power in the main mod, which will appear if either Pets or Supernatural is installed.
Next, let's add the images and strings that will appear in the karma power selection menu. For each power in your XML, add two IMAG (0x2F7D0004) resources to your package named "[PowerName]" and "[PowerName]_Preview" (minus the quotes), respectively, where "[PowerName]" is the name you gave to the power in the XML. Then, right-click each IMAG and select "Replace" to save a PNG file into the resource. The resource named "[PowerName]" is the icon that will appear in the power grid; a resolution of around 52 x 52 seems to work best. The other image is the preview that appear next to the power's title, cost, and description, and should be around 200 x 200, though feel free to experiment later if you feel it looks off.
Once the images are added, right-click on your STBL and select "Edit STBL". Click on the text box at the bottom of the screen and enter "UI/WonderMode/KarmaMenu:[PowerName]" (minus the quotes), where "[PowerName]" is the name you gave to the power in the XML, then click "Add". Then, in the text box on the right, enter the name of the power as you want it to appear in the karma power selection menu (e.g. "Lightning Strike").
Then, in the text box at the bottom, enter "UI/WonderMode/KarmaMenu:[PowerName]Description" (again minus the quotes and replacing "[PowerName]") and click "Add" again. On the right, enter the description for the power that will appear when it is selected in the karma menu.
Finally, click "Save" and commmit the changes. The preview of the STBL in
S3PE should now look like this:
Repeat this process for each STBL (and language) you want to support. Note that any other text you display to the user should similarly be localized and added to these STBLs; check out the tutorial
here for more info on how to do that. For this example, all additional strings will simply be hardcoded in English.
With all the resources in place, it's time to get coding! In this example, we'll take advantage of the existing "GetStruckByLightning" interaction to give Sims a healthy jolt of electricity when the power is activated.
Start by adding
Code:
using Gamefreak130.WonderPowersSpace.Booters;
At the top of then new file. Then, replace the default project class with the following Booter template:
Code:
namespace Gamefreak130.SampleLightningPowerSpace.Booters
{
public class LightningPowerBooter : PowerBooter
{
}
}
Change the namespace and class name however you like, but leave the ": PowerBooter" untouched. This is what tells the mod that our class has information about karma powers that needs to be loaded.
You'll notice your IDE complaining about the class missing some required elements. This can be fixed by adding the following constructor inside the class:
Code:
public LightningPowerBooter() : base("Gamefreak130_SampleLightningPower")
{
}
Change "LightningPowerBooter" to match the containing class's name, and replace Gamefreak130_SampleLightningPower with the name you gave your XML resource. This constructor is what will allow the main mod to read the XML file and load the provided power info into the game!
It is worth noting that because this is executed when the game first loads into the main menu, if you have any extra state to set up or code to run OnPreLoad or OnWorldLoadFinished, you can create EventHandlers for these events elsewhere and add them as subscribers to the events inside the constructor. In this example, the tuning from the original GetStruckByLightning interaction will have to be copied to another custom interaction, so I'll add a call do a method that will do so:
Code:
public LightningPowerBooter() : base("Gamefreak130_SampleLightningPower")
{
Common.Tunings.Inject(Sim.GetStruckByLightning.Singleton.GetType(), typeof(Sim), typeof(Interactions.LightningStrike.Definition), typeof(Sim), true);
}
The source code for this Inject method can be found in the GitHub repo linked above.
Now we'll write the method to run when the power is activated. Start by adding a static class with a static method in it, like so:
Code:
namespace Gamefreak130
{
public static class SampleLightningPower
{
public static bool Activate(bool isBacklash)
{
}
}
}
You will need a separate activation method for each power you plan to add, though all of these methods can exist under the same class and namespace. All power activation methods must take as a parameter a boolean indicating whether the power was activated as part of a karmic backlash instead of being purchased from the power selection menu; they must also return a boolean indicating whether the initial power activation completed successfully. Note that only bad powers can be selected for backlash, so if you set the "IsBad" value of the power to False in your XML, you can assume that "isBacklash" will always be false.
Since this example is a bad power that is valid for backlash and we need to find a Sim to be struck by lightning, I'll start the method with the following code:
Code:
Sim selectedSim = null;
if (isBacklash)
{
List<Sim> validSims = Household.ActiveHousehold.Sims.FindAll(sim => sim.SimDescription.TeenOrAbove && sim.CanBeKilled() && !sim.IsInRidingPosture);
if (validSims.Count > 0)
{
selectedSim = RandomUtil.GetRandomObjectFromList(validSims);
}
}
else
{
List<SimDescription> targets = PlumbBob.SelectedActor.LotCurrent.GetSims(sim => sim.SimDescription.TeenOrAbove && sim.CanBeKilled() && !sim.IsInRidingPosture)
.ConvertAll(sim => sim.SimDescription);
SimDescription selectedDescription = HelperMethods.SelectTarget(targets, "Lightning Strike");
if (selectedDescription != null)
{
selectedSim = selectedDescription.CreatedSim;
}
}
if (selectedSim == null)
{
return false;
}
This might look intimidating, but it's really not too difficult to understand once you break it down. If the power was activated as part of the backlash, we don't give the user any choice in which Sim is selected to be struck; we find all Sims in the active household that can be struck (i.e. Teen or older, able to die from electrocution, and not riding a horse), and pick one at random if there are any. Otherwise, we get the list of Sims on the same lot as the currently selected Sim and convert it into a list of their associated SimDescriptions. HelperMethods.SelectTarget() is a method in Gamefreak130.WonderPowersSpace.Helpers that takes our list of SimDescriptions and opens a dialog for the user to select one; it then returns the selected SimDescription, or null if there were no Sims to pick from.
Note:
There is another method with the same name that allows for the selection of Lots as well; take a look at the "Meteor Strike" activation method for a usage example.
In fact, there are several helper methods in the Gamefreak130.WonderPowersSpace.Helpers namespace, such as WonderPowerManager.PlayPowerSting(), that are available for you to use. Again, feel free to look at the source for implementations and usage examples.
Finally, we get the original Sim back from the selected SimDescription if it exists, and if at the end we were not able to select a Sim, then we return false to signal that the power activation failed.
If we are able to select a valid Sim, then we'll need to focus on them and strike them with lightning. To do so, I'll add the following code:
Code:
Camera.FocusOnSim(selectedSim);
if (selectedSim.IsSelectable)
{
PlumbBob.SelectActor(selectedSim);
}
InteractionInstance instance = new LightningStrike.Definition().CreateInstance(selectedSim, selectedSim, new InteractionPriority(InteractionPriorityLevel.CriticalNPCBehavior), false, false);
if (!instance.Test())
{
return false;
}
selectedSim.InteractionQueue.AddNext(instance);
return true;
Fortunately, this part of the method is more straightforward. The interaction used here is a custom one that I will define shortly. Note that the interaction priority is set to CriticalNPCBehavior so that it will automatically stop all interactions except those relating to pregnancy, fire, or death, and that the last argument is set to "false", meaning that the interaction cannot be canceled by the user once added. If for whatever reason the test for adding the interaction instance fails (e.g. if there's an active fire on the lot), then we return false to notify the system of failure. Otherwise, we can add the interaction to the Sim and return true.
The final activation method, then, should look something like this:
Code:
using Gamefreak130.SampleLightningPowerSpace.Interactions;
using Gamefreak130.WonderPowersSpace.Helpers;
using Sims3.Gameplay.Actors;
using Sims3.Gameplay.CAS;
using Sims3.Gameplay.Core;
using Sims3.Gameplay.Interactions;
using System.Collections.Generic;
namespace Gamefreak130
{
public static class SampleLightningPower
{
public static bool Activate(bool isBacklash)
{
Sim selectedSim = null;
if (isBacklash)
{
List<Sim> validSims = Household.ActiveHousehold.Sims.FindAll(sim => sim.SimDescription.TeenOrAbove && sim.CanBeKilled() && !sim.IsInRidingPosture);
if (validSims.Count > 0)
{
selectedSim = RandomUtil.GetRandomObjectFromList(validSims);
}
}
else
{
List<SimDescription> targets = PlumbBob.SelectedActor.LotCurrent.GetSims(sim => sim.SimDescription.TeenOrAbove && sim.CanBeKilled() && !sim.IsInRidingPosture)
.ConvertAll(sim => sim.SimDescription);
SimDescription selectedDescription = HelperMethods.SelectTarget(targets, "Lightning Strike");
if (selectedDescription != null)
{
selectedSim = selectedDescription.CreatedSim;
}
}
if (selectedSim == null)
{
return false;
}
Camera.FocusOnSim(selectedSim);
if (selectedSim.IsSelectable)
{
PlumbBob.SelectActor(selectedSim);
}
InteractionInstance instance = new LightningStrike.Definition().CreateInstance(selectedSim, selectedSim, new InteractionPriority(InteractionPriorityLevel.CriticalNPCBehavior), false, false);
if (!instance.Test())
{
return false;
}
selectedSim.InteractionQueue.AddNext(instance);
return true;
}
}
}
Now, the only code left to write is the LightningStrike interaction, which I've written as follows:
Code:
using Gamefreak130.WonderPowersSpace.Helpers;
using Sims3.Gameplay.Actors;
using Sims3.Gameplay.Interactions;
using Sims3.SimIFace;
namespace Gamefreak130.SampleLightningPowerSpace.Interactions
{
public class LightningStrike : Sim.GetStruckByLightning
{
new public class Definition : InteractionDefinition<Sim, Sim, LightningStrike>
{
public override bool Test(Sim actor, Sim target, bool isAutonomous, ref GreyedOutTooltipCallback greyedOutTooltipCallback)
{
return true;
}
}
public override bool Run()
{
try
{
return base.Run();
}
finally
{
WonderPowerManager.TogglePowerRunning();
}
}
}
}
If you've worked with custom interactions before, this code is fairly self-explanatory, since it just directly reuses the Run() method from GetStruckByLightning to generate the lightning strike. The only notable addition is WonderPowerManager.TogglePowerRunning(), another method from Gamefreak130.WonderPowersSpace.Helpers that signals to the system that all power effects have completely finished and that it is safe to re-enable the karma power selection menu so that another power can be chosen. (Karmic backlashes will also start at this time, if necessary.)
All powers must call this method at the end of activation, or else the selection menu will be stuck in the disabled state!
Also note that the TogglePowerRunning() method is part of a try-finally block. Put simply, wrapping code in a finally block ensures that it will always be executed after the try block, even if an error occurs in the try block that would ordinarily halt code execution. This is an extremely important, but subtle point: there is no need for such try-finally blocks in the main activation method, since the system already has checks in place to undo power execution and re-enable the selection menu should an error occur,
but if your power uses interactions, situations, or anything else that would exit the scope of the initial activation method, you MUST include error handling like this in them and test everything thoroughly to ensure that power activation can never be inadvertently left running in the event of script errors! You can take a look at the source code of the main mod for some more complex examples of error handling to ensure that power effects are applied and TogglePowerRunning() is called whenever possible.
If you've made it this far, congratulations! We're basically done, but there are a couple more finishing touches to make. In your IDE, compile your project into a DLL, then open up your saved package in S3PE. Right-click on the S3SA resource you created earlier, select "Import DLL", and find your compiled project DLL to add it to the package.
Finally, we'll need to link the powers to their activation methods when booting, so reopen the power booter XML in a text editor and enter the following between each power's "<EffectMethod>" tags:
Code:
[ClassName], [ProjectName], [MethodName]
Where "[ClassName]" is the fully qualified type name of the class containing the activation method, "[ProjectName]" is the name of the DLL (minus the .dll extension) containing the activation method, and "[MethodName]" is the name of the activation method itself. In the case of our Lightning Strike example, this is what the final XML would look like:
Code:
<?xml version="1.0" encoding="utf-8" ?>
<KarmaPowers>
<Power>
<ProductVersion>BaseGame</ProductVersion>
<PowerName></PowerName>
<IsBad></IsBad>
<Cost></Cost>
<EffectMethod></EffectMethod>
</Power>
<Power>
<ProductVersion>EP8</ProductVersion>
<PowerName>LightningStrike</PowerName>
<IsBad>True</IsBad>
<Cost>20</Cost>
<EffectMethod>Gamefreak130.SampleLightningPower, Gamefreak130.SampleLightningPower, Activate</EffectMethod>
</Power>
</KarmaPowers>
Once done, save your package to your mods folder and open the game. Your custom karma power should appear with all the others in the selection menu!
As always, feel free to ask any questions if any part of this guide was confusing or if there is something I can clarify
"The Internet is the first thing that humanity has built that humanity doesn't understand, the largest experiment in anarchy that we have ever had." - Eric Schmidt
If you enjoy the mods I put out, consider supporting me on patreon:
www.patreon.com/Gamefreak130