-
Notifications
You must be signed in to change notification settings - Fork 411
Writing Effects in Pyfa
Note: this document is meant for developers. If you have no interest in pyfa development, then this doesn't offer you much
The update procedure for pyfa is mostly automatic: we gather the data we need from the cache, compile it into a SQLite database, and then compare the information to the current release to get a diff. However, due to legacy reasons, pyfa does not currently compile the effect information needed to understand how one item affects another.
To work around this limitation of the engine, we have python files that define the various effects used in pyfa. These files can be found in the eos/effects
directory. Each file represents one effect, and can have multiple modifications contained within. Any time a new effect is introduced into the game - usually with a new item or balance pass - or when an effect is changed (rare), we must reflect this information in the effects directory. Writing effects is a straightforward process once you get the hang of it. Even though they are written in python, it should be easy for anyone to create or modify one with a little experience.
Some effects may need to be run before other effects in order for them to apply correctly. Because of this, pyfa has the concept of runtimes, which tells certain effects to run sooner or later. Valid values are:
- early
- normal
- late
Normal runtime is used by default when no other runtime is specified, and is used by the majority of effects. An example of an effect that would require runtime tweaking would be that of a Triage fit when projected. We must ensure this is run first before anything, because if not then the remote reps might be calculated and applied to a projected fit before the triage bonus effect modifies their amount.
Getting a feel for if runtime is needed or not somewhat depends on knowing how the calculation system iterates through the modules of the fit. Since that is not the point of this article, it's probably best to leave runtime alone unless an effect is not being properly expressed through the fit calculation, and then tweak it if needed.
The effect type simply determines when the effect is run and is based on the state of the item that calls it. We can have multiple types if need be by making a comma-separated list.
Valid values are:
- offline - effect will be applied all the time (even if module is offline)
- passive - effect will be applied if module has online state or higher
- active - effect will be applied if module has active state or higher
- overheat - effect will be applied if module is in overheat state
- projected - effect will be applied if we're running this effect as a projected module
- gang - effect will be applied if we're running this effect as a gang boost module
For example, if we have an effect that provides a bonus only when activated, we would set the type to "active", as setting it to passive would cause the effect to run when the item is simply online.
Also note that, as a limitation of the engine, valid states of modules are actually determined by their effects. If we want to activate a module, we basically loop through all effect and see if we have at least one that has the active
type. If so, we allow the program to activate the module. Take the Web for example: you may think that we only need to supply the projected type, as activating a web has no local effect (besides capacitor use, which is not an effect and is handled elsewhere). However, we still need to activate the module, and thus the effect should also have active
along with projected
. But won't doing so mean that the effect will be run locally as well? We will talk about how we deal with that later.
The handler is the function that is called when we determine from the runtime and effect type that the effect needs to be run. We supply it with three arguments:
- fit - For local modules, this is the fit object of the current fit we are working on. If we are looking at a projected or gang effect, this is the object of the target fit.
- src - This is the source item that is calling the effect, and contains the source attribute used in the modification.
- context - For some effects, we need context on how the effect is being run. If we are using the effect as a local effect on our fit, the context will be the kind of item the source is, such as
module
,implant
,drone
, etc. However, additional context may also be injected, such asgang
if the effect is being called on gang members, orprojected
if the effect is being called on projected fits. This usually helps to determine the path we take to apply it. There are some instances when multiple items use the same effect (an implant giving the same kind of bonus as a module, for instance), in which case we may need to alter the logic a bit to properly implement the effect.
Before discussing the handler in more detail, we must discuss the two kinds of modifiers that we will use: direct item attribute modification, and filtered item attribute modification.
Lets look at the direct modifications first. These functions are applied directly to an item (usually a ship, such as fit.ship.boostItemAttr()
) and define the kind of modification we want to achieve.
-
preAssignItemAttr
: Overwrites original value of the item with given one, allowing further modification. This is used in very specific circumstances (at the moment, only Subsystems use this to overwrite basic ship attributes with ones that the Subsystem provides) -
increaseItemAttr
: Increase value of given attribute by given number. A good example would be Subsystem effects that increase the amount of slots of a fit. -
multiplyItemAttr
: Multiply value of given attribute by given factor. -
boostItemAttr
: Boost value by some percentage. -
forceItemAttr
: Force value of an attribute and prohibit any changes to it.
These functions are passed two arguments at minimum: the target attribute (the one that we are trying to change), and the source attribute (the one that contains the modifier). We also also supply stackingPenalties = True
for modifiers that must be penalized.
Filtered modifications are the exact same as direct modification, only they are applied on a collection of items rather than a specific item, and you should also pass a filter that limits which items the effect is being applied to (so that you don't boost your armor reps with a Shield Boost Amplifier). Collections can be fit.modules
, fit.drones
, fit.implants
, and fit.boosters
.
filteredItemPreAssign
filteredItemIncrease
filteredItemMultiply
filteredItemBoost
filteredItemForce
The first argument should be a lambda that returns true for items in the collection that match a filter. For example, to make a modifier that only affects laser turrets we would define our filter as lambda mod: mod.item.group.name == "Energy Weapon"
, where mod
is an item in the collection, and mod.item.group.name == "Energy Weapon"
equals True
if the module is in the Energy Weapon item group. You have to know a little about how module and item objects are represented in pyfa, so I would suggest looking at other effects and getting a feel for how they work. The next two mandatory arguments are the same as the direct modifiers: the target attribute and the source attribute. Again, you can define a stacking penalty as well.
A note on charges: Charge modifiers () use the same functions, but instead of something like filteredItemBoost
, it would be filteredChargeBoost
, and these are always applied to the fit.modules
collection.
A note on ship hull bonuses: Effects that relates to ship skill bonuses (eg: 20% for every level of x skill), you must include the argument "skill". For example, a Retribution has 5% Small Energy Turret damage per Assault Frigate skill level, you would include the argument skill="Assault Frigates"
in your modifier.
eos/modifiedAttributeDict.py
contains, among other things, the actual functions used in the modifiers. Some of them may have additional arguments that are rarely used, but may be necessary.
Lets take a look at a simple effect, the decreaseTargetSpeed
effect used on Webifiers.
type = "active", "projected"
def handler(fit, src, context):
if "projected" not in context:
return
fit.ship.boostItemAttr(
"maxVelocity",
src.getModifiedItemAttr("speedFactor"),
stackingPenalties = True
)
We first define that this effect can be active, which allows us to activate the item on our local fit, and also projected, which allows us to use the effect on projected fits. As stated in the explanation of types above, we define the effect as active to simply be able to activate it, however, we do not want the effect to run on our own fit (which would decrease our speed). Therefore, we use the context argument to filter out use cases:
if "projected" not in context:
return
If projected is not in the context (in other words, if we are looking at this effect in any situation other than projected onto a fit), the we simply return. This prevents the effect from being applied to our own fit, but maintaining the projection effect.
The next block is the actual modifier. Recall that fit
in a projected context is the fit that we are projecting onto. fit.ship
point to the ship object and all of it's attributes, and it it this that we want to modify. Since we are applying a negative percentage to the target, we use boostItemAttr
as our modifier. The first argument is the target attribute that we want, and the second argument is the source attribute (we get it using src.getModifiedItemAttr()
). This effect does stack, so we add stackingPenalties = True
, and we're done!
type = "passive"
def handler(fit, module, context):
fit.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill("Shield Operation") \
or mod.item.requiresSkill("Capital Shield Operation"),
"shieldBonus",
module.getModifiedItemAttr("shieldBoostMultiplier"),
stackingPenalties=True
)
Here we have a passive module that uses a filter, and the module does percentage boost to Shield Boosters, which are modules, hence we used the filtered item boost on our fits' module collection:(fit.modules.filteredItemBoost). We only apply this effect to modules that require the Shield Operation or Capital Shield Operation skills, which narrows it down to only shield boosters; this is the first argument. The other arguments are the same as the last example.
To help with determining what each effect does, we can use the new EOS engine, along with a helper script found here (make sure you edit the json_path
to point to Phobos data dump), to find the different modifiers for a supplied effect. Do note that you need to run this script with Python 3, and also note that it may take a while for EOS to build it's modifier cache the first time you run it.
python3 getmods.py -e shipMissileRoFMF2
Here we supplied it with the shipMissileRoFMF2
, which was a new effect introduced in Rubicon 1.3 to replace the Breachers damage bonus with a rate of fire bonus. The output tells us more about the effect:
effect shipmissilerofmf2.py (id: 5778) - build status is ok_full
Modifier 1:
state: offline
scope: local
srcattr: shipBonusMF2
operator: post_percent
tgtattr: speed (penalized)
location: ship
filter type: skill
filter value: Missile Launcher Operation
It shows that this effect is completely passive (applied regardless of item state), it takes value of shipBonusMF2
attribute and boosts (post_percent
) attribute speed
of all items which have skill Missile Launcher Operation as a skill requirement. The scope tells us if it's a local modification, "projected", or "gang". Target attribute is stacking penalized, but here it doesn't make any difference, because attribute modifications coming from ship are immune to stacking penalty. Now, let's implement the effect:
$ cat eos/effects/shipmissilerofmf2.py
type = "passive"
def handler(fit, ship, context):
fit.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill("Missile Launcher Operation"),
"speed",
ship.getModifiedItemAttr("shipBonusMF2"),
skill="Minmatar Frigate"
)
Note that since this is a ship hull bonus that is tied to a skill, we also include the skill
argument.
Here is the output using the previous example of decreaseTargetSpeed
of the Webifiers. Compare the output produced here to the effect that we discussed earlier.
effect decreasetargetspeed.py (id: 586) - build status is ok_full
Modifier 1:
state: active
scope: projected
srcattr: speedFactor
operator: post_percen
tgtattr: maxVelocity (penalized)
location: target
filter type: None