-
Notifications
You must be signed in to change notification settings - Fork 5
Custom Status Effects
We will be making our own StatusEffect for this tutorial. When making your own StatusEffect you need 4 things
-
A
StatusEffectDatawhich will be defined in JSON. -
A class inheriting from
StatusEffectState. -
A black and white icon (50x50px) for the tooltip icon.
-
A transparent bg and white foreground icon (also 50x50px) for the
CharacterUIdisplay.
The status effect we will be making is called Recoil. Recoil makes an unit take damage equal to the number of stacks of Recoil after the unit attacks.
Here are the icons used by this tutorial
- status_recoil_tooltip.png -
- status_recoil.png -
Save them under the textures/ folder as usual.
A side note to everything we've been doing so far. These JSON files we have been writing in these tutorials always form a Data object (CardData, CardEffectData, CharacterData, CharacterTriggerData). Some of these Data objects that require a name property specifies which State class models the behavior and effects of the data, in these scenarios we can write our own State subclass and define the effects and behaviour ourselves. The Data object we create specifies to the State class additional parameters.
So here's the JSON definition that creates our StatusEffectData
{
"$schema": "https://github.com/Monster-Train-2-Modding-Group/Trainworks-Reloaded/releases/latest/download/schema.json",
"status_effects": [
{
"id": "recoil",
"class_name": "@StatusEffectRecoilState",
"names": {
"english": "Recoil"
},
"stackable_names": {
"english": "Recoil {0}"
},
"card_tooltips": {
"english": "Unit takes 1 damage per stack after attacking.<br>Decreases every turn."
},
"character_tooltips": {
"english": "Takes [codeint0] damage after attacking.<br>Decreases every turn."
},
"replacement_texts": {
"english": "<b>Recoil</b>"
},
"trigger_stage": "on_post_attacking",
"remove_when_triggered": true,
"param_int": 1,
"is_propagatable": true,
"display_category": "negative",
"exclude_monster_propagation": true,
"is_stackable": true,
"icon": "@Recoil"
}
],
"sprites": [
{
"id": "Recoil",
"path": "textures/status_recoil.png"
}
],
"atlas_icons": [
{
"id": "Recoil",
"path": "textures/status_recoil_tooltip.png"
}
]
}-
idthis becomes the base of our statusID. You can now refer to it in effects using the same@idsyntax. -
class_name- This is thenameparameter for StatusEffects not to confuse withnameswhich is the player facing name of the Status Effect. This will be the name of the StatusEffectState subclass we will writeStatusEffectRecoilState. -
names- As mentioned this is the player facing name of the status effect as shown in tooltips. -
stackable_names- This is the name of the status effect used in certain places. You only need to set this if it is a stackable status effect. -
card_tooltips- This is the tooltip shown on any monster or spell card that grants the status effect. Generally want to use a generic description of what the status effect does. -
character_tooltips- This is the tooltip shown on a unit that has been afflicted with this status effect. Generally want to tell the player the current effect of what the status is doing to the character. Replacement text here is a bit different[codeint0]and[codeint1]are used to refer to specific values.-
[codeint0]Refers to the effect magnitude (total damage) -
[codeint1]Refers to the magnitude per stack -
[codeint2]Refers to the secondary magnitude -
[codeint3]Refers to the secondary magnitude per stack
-
-
replacement_texts- This parameter is optional, and if specified creates a special substitution text for your status effect for ease of localization. In this case when this status effect is defined the replacement text[ModGUID_statusID]becomes available. -
trigger_stage- This is an imporant parameter, much liketriggerin CharacterTriggers this determines when our StatusEffect code executes. More on this when we actually write the code for the status effect. If you need more than onetrigger_stagethere is theadditional_trigger_stageslist, however using this will make your StatusEffectState class more complex as you would then need to distinguish between what you want to do depending on the TriggerStage. -
remove_when_triggered- This parameter causes the status effect to remove stacks of the status effect when it is triggered. The StatusEffectState class can then control how many stacks are removed. -
param_int- This is a parameter we can use in our StatusEffectState class. There is alsoparam_str,param_int_2,param_floatfor passing a string, another integer, or float value respectively. Don't hardcode values it makes your code less reusuble. -
is_propagatable- Yes this does what you think it does. I will mention that Propagate is a lie. Each status effect can control how they respond to Propagate by this property andexclude_hero_propagationandexclude_monster_propagation. By convention, debuffs will exclude monster propagation and buffs will exclude hero propagtion. To completely disable Propagate from modifying stacks just set this to false like Dazed does. -
is_stackable- Allows the status effect to be stackable -
icon- We provide a Sprite for the icon.
Two icons must be provided
-
A black and white icon for the tooltip icon.
-
A transparent bg and white foreground icon for the
CharacterUIdisplay.
Here is where we use them:
"sprites": [
{
"id": "Recoil",
"path": "textures/status_recoil.png"
}
],
"atlas_icons": [
{
"id": "Recoil",
"path": "textures/status_recoil_tooltip.png"
}
]The transparent background version is used as a sprite. The black and white version is used as an AtlasIcon. Please note the ids of both items are the same. This is a requirement, as the game will reference the Sprite to lookup the associated AtlasIcon for it.
AtlasIcons are used in various Tooltip UIs and are separate from Sprites. Technically, they are just Unity Texture2D objects.
This class handles the implementation of our status effect. When the game requires one, it will instantiate an object based off the name given in the StatusEffectData object.
So here's our StatusEffectState subclass.
class StatusEffectRecoilState : StatusEffectState
{
public override bool TestTrigger(InputTriggerParams inputTriggerParams, OutputTriggerParams outputTriggerParams, ICoreGameManagers coreGameManagers)
{
var attacker = inputTriggerParams.attacker;
if (attacker != null && attacker.IsAlive)
{
return attacker.GetStatusEffectStacks(GetStatusId()) > 0;
}
return false;
}
protected override IEnumerator OnTriggered(InputTriggerParams inputTriggerParams, OutputTriggerParams outputTriggerParams, ICoreGameManagers coreGameManagers)
{
var attacker = inputTriggerParams.attacker;
var stacks = attacker.GetStatusEffectStacks(GetStatusId());
CoreSignals.DamageAppliedPlaySound.Dispatch(Damage.Type.Spikes);
yield return coreGameManagers.GetCombatManager().ApplyDamageToTarget(GetDamageAmount(stacks), attacker, new CombatManager.ApplyDamageToTargetParameters
{
damageType = Damage.Type.Default,
affectedVfx = GetSourceStatusEffectData()?.GetOnAffectedVFX(),
relicState = inputTriggerParams.suppressingRelic
});
}
public override int GetEffectMagnitude(int stacks = 1)
{
return GetDamageAmount(stacks);
}
private int GetDamageAmount(int stacks)
{
return (GetParamInt() + relicManager.GetModifiedStatusMagnitudePerStack(GetStatusId(), GetAssociatedCharacter().GetTeamType())) * stacks;
}
}As mentioned status effects have an associated trigger_stage These trigger states are the status effect equalivent to triggers. They have their own separate timings on when they fire than triggers do.
When the trigger_stage is fired, and the associated character has one or more stacks of the status. TestTrigger is first called. In this function you are responsible for determining whether or not to actually trigger your status. If this function returns false then processing ends and your status effect is skipped.
If it returns true. Then OnTriggered gets called. Here you will implement the effects of your status effect.
InputTriggerParams contains various information you can use to determine whether or not to actually trigger your status effect. However not all fields are valid depending on what trigger_stage it was fired form.
OutputTriggerParams contains various things we can set to propagate information to the next StatusEffectState that fires or back to the code base. You can do things like modify the damage, modify how many stacks of the status effect we lose, block CharacterTriggers from firing, and even block attacking or healing on the character afflicted by the status effect.
public override bool TestTrigger(InputTriggerParams inputTriggerParams, OutputTriggerParams outputTriggerParams, ICoreGameManagers coreGameManagers)
{
var attacker = inputTriggerParams.attacker;
if (attacker != null && attacker.IsAlive)
{
return attacker.GetStatusEffectStacks(GetStatusId()) > 0;
}
return false;Since we are using on_post_attacking we can get the following information from InputTriggerParams
-
damage
-
damageSustained
-
damageBlocked
-
unmodifiedDamage
-
damagePreventedByLifeLink
-
attacked (character)
-
attacker (character)
-
damageSourceCard
-
suppressingRelic
-
damageType
-
canAttackOrHeal
-
canFireTriggers
Most of these are self explanatory.
For our case the most important information is attacker. We want to get the unit that attacked, which should the same as the character with the status effect.
The very first thing you should do when you get the CharacterState object (inputTriggerParams.attacker gives you a CharacterState) is perform a NULL check. As mentioned not all fields on InputTriggerParams will be a valid object, or the function could be called unexpectedly. You should always perform NULL checks here just in case, if a NullReferenceException happens then the game will softlock and we do not want that to happen, so be paranoid!
Next we check if the character is Alive via IsAlive if so we get the number of stacks the character has with the status effect and check if it is nonzero if so then we return true we want to actually execute our effect. Otherwise we return false.
var attacker = inputTriggerParams.attacker;
var stacks = attacker.GetStatusEffectStacks(GetStatusId());
CoreSignals.DamageAppliedPlaySound.Dispatch(Damage.Type.Spikes);
yield return coreGameManagers.GetCombatManager().ApplyDamageToTarget(GetDamageAmount(stacks), attacker, new CombatManager.ApplyDamageToTargetParameters
{
damageType = Damage.Type.Default,
affectedVfx = GetSourceStatusEffectData()?.GetOnAffectedVFX(),
relicState = inputTriggerParams.suppressingRelic
});Here we want to perform the effects of our status effect. We get the attacker again (it should be non null, so no need to NULL check).
We get the number of stacks of recoil.
We play the Spikes sound effect
The next line of code is a Coroutine function call. Anytime you see a function that returns an IEnumerator that function is a Coroutine and you must call it with yield return CoroutineFunctionCall Otherwise nothing will happen.
So code-wise to Inflict damage to a unit you make a call to combatManagerInstance.ApplyDamageToTarget We get the current CombatManager instance through coreGameManagers. Then call ApplyDamageToTarget
This function requires the amount of damage we want to do, and the CharacterState to deal the damage to, along with some configuration.
-
damageType - We set this to Default damage for simplicity. Different Damage Types have different behaviours
-
affectedVfx - The VFX to play, we didn't actually set a VFX in the StatusEffectData for simplicity, but that must be forward to the function call.
-
relicState - Damage can be modified through certain Relics, we can associate the damage with a relic here.
Additionally by setting outputTriggerParams.count you can control how many status effects stacks are lost upon removal. Since we did not set this it will default to losing 1 stack of the status effect.
public override int GetEffectMagnitude(int stacks = 1)
{
return GetDamageAmount(stacks);
}
private int GetDamageAmount(int stacks)
{
return (GetParamInt() + relicManager.GetModifiedStatusMagnitudePerStack(GetStatusId(), GetAssociatedCharacter().GetTeamType())) * stacks;
}GetEffectMagnitude is an overridable function and just specifies the total amount of effect you get given stacks. Going back to the StatusEffect description the return value from this function determines what [codeint0] gets substituted with.
GetDamageAmount actually computes the damage.
-
We get the
param_intwe defined back in our StatusEffectData. -
We allow Relics to potentially modify the amount of damage per stack (Think Cuttlebeard w/ Frostbite damage modification)
-
We add them together and multiple by the stacks to get the damage amount.
Be sure to recompile your mod as code changes require you to recompile the mod.
-
You can make a card (or modify an existing one) to give stacks of your new status effect.
-
Alternatively with the Dev Console mod you can directly give it to an enemy unit or friendly unit
-
use the
unitinfocommand. This shows the names of the units for the next command-
setstatus UnitName ModGUID_statusID 10
Where
-
UnitName is the name from the UnitInfo display P# or E#
-
ModGUID is your ModGUID (in all lowercase)
-
statusID should be recoil
-
-
Example to refer to Conductor's Construct status effect and give it to P1
setstatus P1 conductor_construct 10
-
- When the unit attacks they should take stacks damage and lose 1 stack of recoil.