Skip to content

Writing Effects in Pyfa

blitzmann edited this page Aug 23, 2015 · 4 revisions

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.

Effect components

Runtime

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.

Type

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.

Handler

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 as gang if the effect is being called on gang members, or projected 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.

Direct Modifications

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

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.

Hull Bonuses per Skill Level

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.

Additional Resources

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.

Example: Webifier

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!

Example: Shield Boost Amplifier

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.

EOS and getmods.py

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