Attribute Functions 1.17.1 - CleverNucleus/data-attributes GitHub Wiki

Data Attributes introduces two entirely new ideas to entity attributes: functions and properties. Attribute functions are best explained using examples:

Example 1

Say the player has the attribute constitution; and for every point added in constitution, a point is also added to max health. This would be done using an attribute function. Functions allow for an attribute to influence another attribute (or a collection of attributes), which can then also have functions.

This is common in many games: Dota 2 has the primary attributes Strength, Dexterity and Intelligence - where strength increases the health pool, dexterity increases armor and intelligence increases mana; PoE is much the same, where strength increases maximum life, dexterity grants evasion and intelligence also increases mana.

Attribute Functions provide this functionality to Minecraft.

Example 2

Using Example 1 as a reference, this example shows how we might implement it. Say we have our examplemod, and we've added the attribute Constitution (see Overrides), but now we want to make it so that for every point added to constitution, a point is added to max health.

We go to the directory data/examplemod/attributes/ where we create the json file functions.json. Inside this file, we add the following:

{
    "values": {
        "examplemod:constitution": [
            {
                "attribute": "minecraft:generic.max_health",
                "behaviour": "FLAT",
                "multiplier": 1.0
            }
        ]
    }
}

That's all that is needed. Now whenever a point is added to Constitution, a point is also added to Max Health.

Structure

There are three items to a function:

  • attribute refers to the registry key of the attribute that will be added to.
  • behaviour refers to how the attribute will be modified (this can currently either be FLAT or DIMINISHING; this is a relatively big topic and has its own section below.
  • multiplier refers to a multiplier applied to a modifier's value before it is applied. To use the above example, a value of 1 means that for every one value of constitution changed, one value of max health is changed. A value of 2 would mean that if 5 points were added to constitution, 10 points would be added to max health (5 * 2 = 10).

Behaviour

The type FLAT simply means that whatever value that is added to our attribute (in the above example this would be max health) it is added using:

current value + adding value = resultant value.

The DIMINISHING type allows us to specify another addition type with diminishing returns. This type of addition is found in other games with advanced attribute systems: in Dota 2, Evasion is an attribute with a diminishing stacking bonus. If I have an entity with +20% evasion and the entity has an item that provides +30% evasion, one could expect a final value of +50% evasion - but this is wrong. The final value is actually about +38% evasion. It should be noted that this is assuming a max value of 100% evasion (a limit).

Data Attributes uses the following equation when adding a positive value (addition):

max value * ((current value + adding value) / (max value + adding value)) = resultant value

and the following equation when adding a negative value (subtraction):

adding value + current value - (adding value * current value / max value) = resultant value

Note that the behaviour is applied last (after the adding value is multiplied by the multiplier).

How it works

There are some things that should be noted about attribute functions:

  • "added to" refers to any change in the value of said attribute.
  • "attribute" in this context refers to attribute instances, which contain the current and base value of their attribute.
  • Therefore, a change in the value of an attribute instance implies the addition or removal of an entity attribute modifier.
    • Entity attribute modifiers are an object containing a UUID, a name, a value and an operation (ADDITION, MULTIPLY_BASE or MULTIPLY_TOTAL).

In the above example, when an attribute modifier is applied to constitution, an attribute modifier is also applied to max health. This is not necessarily intuitive, and involves some behind-the-scenes action.

Lets say an attribute modifier of the ADDITION type with a value of 4.0 is applied to constitution. An attribute modifier of the ADDITION type with a value of 4.0 will also be applied to max health - furthermore, it will have the same UUID and name. Because it has the same UUID, modders should think carefully about how they apply their attribute modifiers (i.e. use different UUID's for different attribute instances); just because modifiers carry a unique id, that does not mean that they are unique globally, it only means that they are unique locally to a given attribute instance.

So far so good. An entity exists with 4 constitution. What if we apply another modifier to constitution? This modifier will have another UUID, name, a value of 0.5 and be of the MULTIPLY_TOTAL type. Once applied, our entity will have a constitution of 6. How does this affect our max health?

You could be forgiven for thinking that our max health is also increased by 50%. Instead, the following happens:

A new attribute modifier is created, containing the same UUID, the same name, but it has a value of 2 and is of the ADDITION type. This is applied to our max health. Note that the value of the attribute modifier is equal to the difference in original and current values of constitution (from 4 to 6, giving a difference of 2). This differences is added to max health, and a similar order of events occurs for MULTIPLY_BASE.

The important points to take away from this wall of text are:

  • Attribute modifiers resulting from attribute functions will always be of the ADDITION type (this means that +30% Constitution would not necessarily mean +30% Max Health).
  • Upon the removal of modifiers, all modifiers applied as a result of attribute functions are removed as well.
  • Modifiers are slightly more dynamic; referencing the above scenario, if we increased our value of constitution from 4 to 8, our +50% would increase the amount added to 4, giving us a total value of 12. Likewise, this would increase the amount added to max health (from +2 to +4).

Example 3

Building off of the previous example (Example 2), lets say we want constitution to also increase our armor toughness; but, we want every 1 point of constitution to add 0.25 points in armor toughness (or, every 4 points in constitution to add 1 point in armor toughness). We modify functions.json to have the following:

{
    "values": {
        "examplemod:constitution": [
            {
                "attribute": "minecraft:generic.max_health",
                "behaviour": "FLAT",
                "multiplier": 1.0
            },
            {
                "attribute": "minecraft:generic.armor_toughness",
                "behaviour": "FLAT",
                "multiplier": 0.25
            }
        ]
    }
}

That's all that is needed; note how an attribute (in this case constitution) can have any number of functions attached - however, each function must be unique in that you cannot have two functions with the same attribute tag.

Recursion

The keen eye among you may have noticed that there is an opportunity for recursion/infinite attributes. Lets show how this might be done:

{
    "values": {
        "examplemod:constitution": [
            {
                "attribute": "minecraft:generic.max_health",
                "behaviour": "FLAT",
                "multiplier": 1.0
            }
        ],
        "minecraft:generic.max_health": [
            {
                "attribute": "examplemod:constitution",
                "behaviour": "FLAT",
                "multiplier": 1.0
            }
        ]
    }
}

Looking at our functions.json file, we can see that when we add a point to constitution, a point is added to max health; but when a point is added to max health, a point is added to constitution - "it would seem we have a vicious cycle on our hands". One could even see how a huge chain of attributes could accidentally become recursive. Constitution adds to Max Health adds to Strength adds to Armor adds to Knockback Resistance adds to Constitution... Not to mention that since each attribute can have many functions attached, the tree of possible combinations resulting in recursion can be extensive.

Therefore, Data Attributes implements protections against this. When the functions.json files are read and assembled, the functions tree is scanned for loops. Any loops found are broken by removing the first function found that results in a loop. The specific function that would be removed for a specific set of circumstances is unreliable, just that it is impossible for loops to form. This is done so that checks do not have to be made in game, which could cause lag.

That being said, developers and pack creators alike should still be careful not to create loops.