Computed Attributes RFC
See original GitHub issueScenario
I have a health
stat. I want equipment to be able to increase this stat by a static amount (+5 health
) or by a percentage (+10% health
)
Current Solution
You can currently accomplish the intended outcome by having two stats: health
and health_percent
the static bonus would apply to health
then +10% health
would be a static +10
to health_percent
. You would then create two helper methods somewhere in your bundles to get the intended value:
/*
* Example:
* Base health 100
* I equip a helm with +5 health
* I wear a ring with +10% health
* 115 = (100 + 5) * (1 + 10 / 100)
*/
function getEffectiveMaxHealth(player) {
// default getMaxAttribute automatically calculates baseHealth + staticHealthBonus
const health = player.getMaxAttribute('health');
// same for health_percent
const health_perc = player.getMaxAttribute('health_percent');
return Math.floor(health * (1 + (health_perc / 100));
}
function getEffectiveHealth(player) {
const max = getEffectMaxHealth(player);
const delta = player.attributes.get('health').delta;
return max + delta;
}
Now this isn’t the end of the world but you’d have to import these functions anywhere you wanted to get the effective health of the player. I think there may be a better way.
Proposal
It would be really cool if you could say something like this:
// glaring flaw with this approach explained in the Hurdles section
Attribute({
name: 'health_percent',
base: 0,
});
Attribute({
name: 'health',
base: 100,
computed: {
requires: ['health_percent'],
formula: function (health, healthPercent) {
return Math.floor(health * (1 + healthPercent / 100));
}
}
})
And then when you called player.getMaxAttribute('health')
it would do something like:
Character.getMaxAttribute(attr, formulate = false) {
const attribute = this.attributes.get(attr);
const effectiveValue = this.effects.evaluateAttribute(attribute);
if (!attribute.isComputed() || !formulate) {
return effectiveValue;
}
const { computed } = attribute;
// possibility of stack overflow here if there's a circular dependency
const deps = computed.requires.map(
reqAttr => this.getMaxAttribute(reqAttr, true)
);
return computed.formula.apply(null, [effectiveValue, ...deps]);
}
Character.getAttribute(attr, formulate = false) {
const attribute = this.attributes.get(attr);
return this.getMaxAttribute(attr, formulate) + attribute.delta;
}
Let’s say the player has base 100 health, is wearing a Helm of +5 Health, and just used a skill that increases max health by 10% so his attributes would look like this:
health: 100 base, with effects 105
health_percent: 0 base, with effects 10
player.getMaxAttribute('health');
// 105, didn't formulate
player.getMaxAttribute('health', true);
// 115, added +10% to 105
let's say they take 20 points of damage
health: 100 base, -20 delta, with effects 105
health_percent: same
player.getAttribute('health');
// 85 = 100 + 5 - 20
player.getAttribute('health', true);
// 95 = floor(((100 + 5) * 1.1) - 20)
Hurdles
Attributes right now are a hydrated property meaning that when the player is saved and reloaded we have to instantiate the original attribute object with the appropriate values. Currently that’s trivial, attributes consist of 3 scalar properties: name, base, delta.
So how would the developer specify this formula, where would they specify it, how would it get saved, and how would it get loaded?
Possible solutions
Maybe there is an attributes definition file similar to the channels definition file and all Character
’s in the game (NPCs/Players) can only use attributes from that? What are the pros/cons there?
… that’s it, I have no idea how else we could solve that.
Issue Analytics
- State:
- Created 5 years ago
- Comments:9 (9 by maintainers)
Top GitHub Comments
An added benefit of the attribute definition file is that I would be able to detect circularly dependent computed attributes at server startup, effectively at “compile” time" instead of having shit hit the fan while the game as running.
I like the idea of having the attributes definition file be the source of truth. It would be relatively simple and match up with other filesystem-based customization such as player-events and so on.
It could even just be a simple map of attribute names to formulas. Honestly, the rest of the attributes is serializable as is. The downside to that is that the attribute information would then be in two places.
I guess what I am saying is that my vote is for attributes to be in a separate definition file, and that it should define the whole thing. This would also be more extensible.
After all, as it is, I think they are defined in the middle of an input bundle, which is a bit odd.