This turned out to be a little tricky but I've done it. I think i've caught all the bugs. So, for this to work you need a couple extra inputs for each stat, but you can hide them or move them somewhere else. First, you need a checkbox input: <input name="attr_stat_maximum" type="checkbox" value="1" > When this is checked, the sheet will use the attribute maximums and halve stats above them. When unchecked, it wont. You can put this input in a configuration section, or wherever you want. Then for the attributes: here's what the strength group looks like <input name="attr_str" type="number" value="10" readonly> <input name="attr_str_max" type="number" value="10" readonly> <input name="attr_str_ltd" type="hidden" value="10" readonly> <span>STR</span> <input name="attr_str_maxima" type="number" value="20" > <span >x1</span> <input name="attr_str_xp" type="number" value="0" > <input name="attr_str_bonus" type="number" value="0" > <input name="attr_str_adjust" type="number" value="0" > str is the current value - you could make it hidden, but as discussed earlier, if you are using aids, drains, and similar effect its a good idea to show it so players know their current status. I set it as readonly because players dont need to adjust this directly. str_max - that attribute max. you need this in case you take damage to the current score (like stun, end, body, or drains to other stats). str_ltd - this is a special stat that should always be hidden. It's used for calculating derived stats. Derived stats don't have this attribute. str_maxima - this is the stat maximum. usually 20 for primary stats. But if other races exist, sometimes it will vary. If your races all have maxima 20, or you aren't using the maxima rules, you can set this to type="hidden" and never worry about it. str_xp: the points invested in the stat. I added an _ to the name, to match all the other stat names. str_bonus: a bonus (or penalty) you add to a stat, which does affect derived stats. Derived stats don't have this attribute. str_adjust: a bonus or penalty applied to a stat that does not affect derived stats. The question of how to handle derived stats, and abilities that do or dont affect them made building the sheet workers a bit trickier than anticipated. _bonus and _adjust dont need to be next to the attributes, but its more convenient if they are. You could have them elsewhere on the sheet, a place for Most stat affecting things act like adjustment powers, and dont affect derived attributes. That's what the _adjust attribute is for. But sometimes players get benefits that do affect the derived stats, that's what the _bonus attribute is for. Players might have spells, or a magic item, or a racial power that has advantages or limitations applied, so its not bought like a normal stat, but you still need to include its effect. Thats what the _bonus attribute is for. I'll post the full set of stats I used for testing in the next post, for reference. So, once you have the stats set up, its time to add the sheet workers. Remember these go inspide the <script> block. const stats_core = { str: { cost: 1, base: 10, }, con: { cost: 2, base: 10, }, body: { cost: 2, base: 10}, dex: { cost: 3, base: 10}, int: { cost: 1, base: 10}, ego: { cost: 2, base: 10}, pre: { cost: 1, base: 10}, com: { cost: 0.5, base: 10}, }; const stats_derived = { pd: { cost: 1, base: 0, derived: {str: 0.2},}, ed: { cost: 1, base: 0, derived: {con: 0.2},}, spd: { cost: 1, base: 10, derived: {dex: 1}}, rec: { cost: 2, base: 0, derived: {str: 0.2, con: 0.2},}, end: { cost: 0.5, base: 0, derived: {con: 2},}, stun: { cost: 1, base: 0, derived: {str: 0.5, con: 0.5, body: 1},}, }; Object.keys(stats_core).forEach(stat => { const stat_properties = stats_core[stat]; let stat_array = [`${stat}_xp`, `${stat}_bonus`, `${stat}_adjust`, `${stat}_maxima`, `stat_maximum` ]; on(stat_array.reduce((changes, current) => changes + `change:${current} `, ''), () => { getAttrs(stat_array, values => { const int = stat => parseInt(values[stat],10)||0, settings = {}; let max_stat = int(stat + '_maxima'), score_bonus = stat_properties.base + Math.floor((int(stat + '_xp') + int(stat + '_bonus') )/stat_properties.cost), score_adjust = stat_properties.base + Math.floor((int(stat + '_xp') + int(stat + '_bonus') + int(stat + '_adjust'))/stat_properties.cost); if(int('stat_maximum') && score_bonus > max_stat) score_bonus = Math.round((max_stat + score_bonus)/2); if(int('stat_maximum') && score_adjust > max_stat) score_adjust = Math.round((max_stat + score_adjust)/2); settings[stat] = score_adjust; settings[`${stat}_max`] = score_adjust; settings[`${stat}_ltd`] = score_bonus; setAttrs(settings); }); }); }); Object.keys(stats_derived).forEach(stat => { const stat_properties = stats_derived[stat]; let derived_array = Object.keys(stat_properties.derived); let full_array = derived_array.map(stat_temp => stat_temp + '_ltd'); full_array.push(`${stat}_xp`, `${stat}_adjust`, `${stat}_maxima`, `stat_maximum` ); on(full_array.reduce((changes, current) => changes + `change:${current} `, ''), () => { getAttrs(full_array, values => { const int = stat => parseInt(values[stat],10)||0, settings = {}; let max_stat = int(stat + '_maxima'), score = derived_array.reduce((total, temp) => total + Math.round(int(temp + '_ltd') * stat_properties.derived[temp]), stat_properties.base + Math.floor((int(stat + '_xp') + int(stat + '_adjust'))/stat_properties.cost)); if(stat === 'spd') max_stat *= 10; if(int('stat_maximum') && score > max_stat) score = Math.round((max_stat + score)/2); if(stat === 'spd') score = score/ 10; settings[stat] = score; settings[`${stat}_max`] = score; setAttrs(settings); }); }); }); Add this code to your script block, and it will handle all the stats, updating their values automatically. If you have any questions about how it works, ask away, but you should be able to just drop it in to your sheet and forget about it. Also, remove the sheet workers I gave earlier - this replaces them.