// CDF/ERF implementation from <a href="http://stackoverflow.com/a/14873282/386178" rel="nofollow">http://stackoverflow.com/a/14873282/386178</a> function cdf(x, mean, variance) { return 0.5 * (1 + erf((x - mean) / Math.sqrt(2 * variance))); } function erf(x) { var sign = Math.sign(x), a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741, a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911, t, y; x = Math.abs(x); t = 1 / (1 + p * x); y = 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x); return sign * y; } function skillXpChanged(governingAttrName) { return function(eventInfo) { var xpAttrName = eventInfo.sourceAttribute,
attrName = xpAttrName.substring(0, xpAttrName.lastIndexOf('-')),
lvlAttrName = attrName + '-lvl', gthAttrName = attrName + '-gth'; getAttrs([governingAttrName, gthAttrName, xpAttrName], function(values) {
var attrs = {};
attrs[lvlAttrName] = Math.round(cdf(parseInt(values[xpAttrName]), 100 - parseInt(values[governingAttrName]) - parseInt(values[gthAttrName]), 100) * 200);
setAttrs(attrs);
}); } } on('change:aggressive-combat-xp change:clashing-defense-xp change:athletics-xp ' + 'change:breaching-xp change:hauling-xp change:initiative-xp', skillXpChanged('strength-lvl')); on('change:eroding-combat-xp change:lasting-defense-xp change:drinking-xp ' + 'change:endurance-xp change:immunity-xp change:gait-xp', skillXpChanged('constitution-lvl')); on('change:counter-combat-xp change:slippery-defense-xp change:acrobatics-xp ' + 'change:stealth-xp change:thievery-xp change:tempo-xp', skillXpChanged('dexterity-lvl')); on('change:miming-combat-xp change:studied-defense-xp change:archana-xp ' + 'change:insight-xp change:literacy-xp change:white-xp', skillXpChanged('wisdom-lvl')); on('change:strategic-combat-xp change:predictive-defense-xp change:alchemy-xp ' + 'change:analysis-xp change:comprehension-xp change:problem-solving-xp', skillXpChanged('intelligence-lvl')); on('change:precise-combat-xp change:reflexive-defense-xp change:perception-xp ' + 'change:sorcery-xp change:recollection-xp change:reaction-xp', skillXpChanged('focus-lvl')); on('change:divinity-xp change:gambling-xp change:intuition-xp ' + 'change:persuasion-xp change:religion-xp change:bravery-xp', skillXpChanged('faith-lvl')); on('change:strength-xp change:constitution-xp change:dexterity-xp change:wisdom-xp ' + 'change:intelligence-xp change:focus-xp change:faith-xp', function(eventInfo) { var xpAttrName = eventInfo.sourceAttribute,
attrName = xpAttrName.substring(0, xpAttrName.lastIndexOf('-')),
lvlAttrName = attrName + '-lvl',
gthAttrName = attrName + '-gth', bonusAttrName = attrName + '-bonus'; getAttrs([gthAttrName, xpAttrName], function(values) { var attrs = {}; attrs[lvlAttrName] = Math.round(cdf(parseInt(values[xpAttrName]), -parseInt(values[gthAttrName]), 100) * 200) - parseInt(bonusAttrName); setAttrs(attrs); }); }); The above code is assuming the attribute names are all lower-case (eventInfo.sourceAttribute returns the name all lower-case, but getAttrs requires the original case) and that all the names are in the form {skill}-xp, {skill}-lvl, {skill}-gth, etc. You can see that skillXpChanged actually returns a function with a closure of the name of the governing attribute. The first seven on() each call skillXpChanged with a different governing attribute name, triggering on changes to any of the six skills under that attribute's purview. I also added in an eighth on() call for level calculations of the seven governing attributes. I'm also assuming the math is correct for the two Math.round() lines. I just grabbed one of the first JavaScript CDF implementations I could find from a quick Google search, so I make no assertions about its accuracy! =) You could also set up this code to make the various on() calls easier to read (or maybe it's just me?), and with less repetition, something like this: var attributes = { strength: [ 'aggressive-combat', 'clashing-defense', 'athletics', 'breaching', 'hauling', 'initiative' ], constitution: [ 'eroding-combat', 'lasting-defense', 'drinking' 'endurance', 'immunity', 'gait' ], // etc. } attributes = _.mapObject(attributes, v => 'change:' + v + '-xp'); _.each(attributes, (v, k) => { on(v.join(' '), skillXpChanged(k + '-lvl')); }); // replaces the first seven calls above on(_.chain(attributes).keys().map(v => 'change:' + v + '-xp').value().join(' '), function(eventInfo) { // same as previous code block above for change:strength-xp etc. }); If nothing else, putting all the attribute name strings in one place makes it easier to see if you made a typo. ~_^