Thanks to GIGs for the Multiversals script post. I have created a character sheet for Maximum Apocalypse and have used it a lot. I did have one problem with rules where the attributes were not numbers. Specifying "values: -1" did not appear to work, so I made a small change the getData & processFunction code. (see below) I thought this might be a useful fix for the community, if you haven't come across it in the mean time. David.
/* ======================================================
BEGIN MULTIVERSAL SHEET WORKER GENERATOR
======================================================
const multistats = {
luck_uses_max: {rule: 'luck_uses', attributes: ['luck', 'has_faith', "has_luck_stars"], modifier: 10},
build: {rule: 'build', attributes: ['str', 'for', 'has_giant_genetics'], modifier: 1},
build_dc: {rule: 'build_dc', attributes: ['build']},
health_max: {rule: 'special_stat', attributes: ['build'], modifier: 5},
starving: {rule: 'hungry', attributes: ['hunger', 'for_quarter', 'has_tightening_the_belt'], modifier: 4},
emaciated: {rule: 'hungry', attributes: ['hunger', 'for_half', 'has_tightening_the_belt'], modifier: 8},
dead: {rule: 'greater_than_or_equal', attributes: ['hunger', 'for']},
init: {rule: 'init', values: -1, attributes: ['agi', 'luck', 'init_temp', 'encumbered', 'shaken', 'acute', 'starving', 'emaciated', 'has_moving_target'], modifier: 10},
init_5: {rule: 'add', attributes: ['init'], modifier: -5},
init_10: {rule: 'add', attributes: ['init'], modifier: -10},
init_15: {rule: 'add', attributes: ['init'], modifier: -15},
salvage: {rule: 'special_stat', attributes: ['ins', 'luck'], modifier: 2},
resilience: {rule: 'special_stat', attributes: ['str', 'fort'], modifier: 2},
resolve: {rule: 'special_stat', attributes: ['int', 'ins'], modifier: 2},
immunity: {rule: 'special_stat', attributes: ['for', 'luck'], modifier: 2},
dodge: {rule: 'dodge', attributes: ['agi', 'ins', 'has_reflex', 'has_moving_target'], modifier: 2},
full_dodge: {rule: 'full_dodge', attributes: ['dodge', 'str']},
parry: {rule: 'max', attributes: ['blades', 'brawl', 'clubs']},
riposte: {rule: 'max', attributes: ['blades', 'brawl', 'clubs']},
encum_max: {rule: 'add', attributes: ['build'], modifier: 200},
cp_total: {rule: 'sum', attributes: ['cp_inferior', 'cp', 'cp_superior', 'cx']},
encum: {rule: 'sum', attributes: ['armor_cp_total', 'attack_cp_total', 'gear_cp_total', 'cp_total']},
encumbered: {rule: 'encumbered', attributes: ['encum', 'encum_max', 'armor_max']}
};
const multifunctions = {
sum: (arr) => arr.reduce((total, add) => total + add), // add up any number of attributes
pick_from_list: (arr) => arr[ arr[0] ], // get the value of an attribute, from a select.
build_dc: (arr) => Math.floor(arr[0]/50)-1,
add: (arr, val) => arr[0]+val,
full_dodge: (arr) => Math.floor(arr[0]+(arr[1]/2)),
max: (arr) => Math.max(...arr),
encumbered: (arr) => arr[0] > arr[1] || arr[2] >= 12 ? 'on' : '0',
greater_than_or_equal: (arr) => arr[0] >= arr[1] ? 'on' : '0',
luck_uses: function(arr, divisor) {
let luck = arr[0];
let has_faith = arr[1];
let has_luck_stars = arr[2];
let luck_uses = Math.floor(luck / divisor)
luck_uses += has_faith > 0 ? 3 : 0;
luck_uses += has_luck_stars > 0 ? 3 : 0;
return luck_uses;
},
dodge: function(arr, divisor) {
let agi = arr[0];
let ins = arr[1];
let has_reflex = arr[2];
let has_moving_target = arr[3];
let dodge = Math.floor((agi + ins) / divisor)
dodge += has_reflex > 0 ? 10 : 0;
dodge += has_moving_target > 0 ? 5 : 0;
return dodge;
},
special_stat: function(arr, divisor) {
let sum = multifunctions.sum(arr);
let stat = Math.floor(sum / divisor);
return stat;
},
build: function(arr) {
let str = arr[0];
let fort = arr[1];
let hasGiantGenetics = arr[2];
return str + fort + (hasGiantGenetics > 0 ? 50 : 0);
},
hungry: function(arr, modifier) {
let hunger = arr[0];
let baseThreshold = arr[1];
let hasTighteningTheBelt = arr[2];
let threshold = baseThreshold + (hasTighteningTheBelt > 0 ? modifier : 0);
return hunger >= threshold ? 'on' : '0';
},
init: function(arr, divisor) {
let agi = parseInt(arr[0]) || 0;
let luck = parseInt(arr[1]) || 0;
let initTemp = parseInt(arr[2]) || 0;
let encumbered = arr[3];
let shaken = arr[4];
let acute = arr[5];
let starving = arr[6];
let emaciated = arr[7];
let hasMovingTarget = arr[8];
let init = Math.floor((agi+luck) / divisor);
init += initTemp;
init -= encumbered === 'on' ? 1 : 0;
init -= shaken === 'on' ? 3 : 0;
init -= acute === 'on' ? 5 : 0;
init -= starving === 'on' ? 2 : 0;
init -= emaciated === 'on' ? 2 : 0;
init += hasMovingTarget > 0 ? 1 : 0;
return init;
}
};
// ======================================================*/
// DO NOT EDIT BELOW THIS LINE //
// ======================================================*/
const mvlog = (title, text, color = 'green', style='font-size:12px; font-weight:normal;', headerstyle = 'font-size:13px; font-weight:bold;') => {
let titleStyle = `color:${color}; ${headerstyle} text-decoration:underline;`;
let textStyle = `color:${color}; ${style}`;
const output = `%c${title}:%c ${text}`;
console.log(output,titleStyle,textStyle);
};
// can use $ placeholder in attribute names. This converts '$_stat' to 'repeating_section_stat'
const rep = '$'; //placeholder for repeating_section_
const makeRepeatingName = (attribute, section) => attribute.startsWith(rep) ? attribute.replace(rep, `repeating_${section}_`) : attribute;
const makeRepeatingAttributes = (attributes, section) => attributes.map(a => makeRepeatingName(a, section));
const makeRepeatingID = (a, section, id) => a.replace(`repeating_${section}_`,`repeating_${section}_${id}_`);
// given array of attributes, find if any have repeating_ and return the section name
// section name will be 2nd element of name split on "_"
const findSection = (arr) => {
const s = arr.find(a => a.includes('repeating_'));
const section = (s ? s.split('_')[1] : null);
return section;
};
// check if attribute is one where a repeating section attribute depends on attributes both inside and outside the repeating section
const isMixed = (attributes, destination) => {
const some = someRepeating(attributes);
const all = allRepeating(attributes);
const repeatingdestination = destination.startsWith('repeating_');
return (some && !all && repeatingdestination);
};
const allRepeating = attributes => attributes.every(r => r.startsWith('repeating_'));
const someRepeating = attributes => attributes.some(r => r.startsWith('repeating_'));
const defaultDataType = 'array'; // might change this to object
const getData = (values, data = 'a', isnumbers = 0) => {
// only a is functional right now, so this function is redundant.
switch(data.charAt(0).toLowerCase()) {
case 'o': return values;
case 'a': return Object.values(values).map(i => 1 === isnumbers ? parseInt(i) ||0 : (0 === isnumbers ? +i || 0 : i));
case 'v': return Object.values(values)[0];
}
};
const processMax = (destination, result, max) => {
const settings = {};
if(max === 'current' || max === 'both') settings[destination] = result;
if(max === 'max' || max === 'both') settings[`${destination}_max`] = result;
return settings;
};
const isFunction = value => value && (Object.prototype.toString.call(value) === '[object Function]' || 'function' === typeof value || value instanceof Function);
const processFunction = (destination, values, section) => {
const rule = multistats[destination].rule;
const func = isFunction(rule) ? rule: multifunctions[rule]; // need to test if this works for arrow functions
const data = multistats[destination].data || defaultDataType;
const isnumbers = multistats[destination].values || 0;
const v = getData(values, data, isnumbers);
const modifier = multistats[destination].modifier || null;
const result = func(v, modifier);
mvlog(`${makeRepeatingName(destination,section).toUpperCase()} MULTIFUNCTION`, `RULE: ${rule}; VALUES: ${JSON.stringify(values)}; RESULT: ${result}`);
return result;
};
Object.keys(multistats).forEach(destination => {
// get the section name if it exists. It is needed for mixed workers
const attributes_base = multistats[destination].attributes;
const section = multistats[destination].section || findSection(attributes_base) || null;
const attributes = makeRepeatingAttributes(attributes_base, section);
const realdestination = makeRepeatingName(destination, section); // needed in case of $ in destination
mvlog(`MULTIVERSAL- ${realdestination}`,`${attributes.join(', ')}`,'green');
if (isMixed(attributes, realdestination)) {
const changes = attributes.reduce((change, step) => `${change} change:${step.replace('repeating_' + section + '_','repeating_' +section + ':')}`,
`remove:repeating_${section} sheet:opened`);
on(changes.toLowerCase(), function (event) {
const trigger = event.sourceAttribute || '';
const triggerRow = (trigger && trigger.includes('_') && trigger.length >2) ? trigger.split('_')[2] : '';
// if triggerRow, only update initial row
getSectionIDs(`repeating_${section}`, function (ids) {
const sectionAtts = attributes.filter(f => f.startsWith(`repeating_${section}`));
const fixedAtts = attributes.filter(f => !f.startsWith(`repeating_${section}`));
if (triggerRow) ids = [triggerRow];
const fieldNames = ids.reduce( (m,id) => [...m, ...(sectionAtts.map(field => makeRepeatingID(field,section,id) ))],[]);
getAttrs([...fieldNames,...fixedAtts], function (values) {
let settings = {};
const max = multistats[destination].max || 'current';
const fixedValues = fixedAtts.reduce((obj, a) => {
obj[a] = values[a];
return obj;
}, {});
ids.forEach(id => {
// first get all relevant attributes for this row of the section
const sectionValues = sectionAtts.reduce((obj, a) => {
const att = makeRepeatingID(a, section, id);
obj[att] = values[att];
return obj;}, {});
// now apply the formula for this row and add to settings
const combinedValues = {...sectionValues,...fixedValues};
const result = processFunction(destination,combinedValues, section);
const tempDestination = makeRepeatingID(realdestination,section,id);
const tempSettings = processMax(tempDestination, result, max);
settings = Object.assign({}, settings, tempSettings);
});
setAttrs(settings);
});
});
});
} else {
const changes = attributes.reduce((change, step) => `${change} change:${step.replace('repeating_' + section + '_','repeating_' +section + ':')}`,
`${someRepeating([...attributes,realdestination]) ? '' : 'sheet:opened '}${section ? `remove:repeating_${section}` : ''}`);
on(changes.toLowerCase(), function () {
getAttrs(attributes, function (values) {
const result = processFunction(destination,values, section);
const max = multistats[destination].max || 'current';
const settings = processMax(realdestination, result, max);
setAttrs(settings);
});
});
}
});
// ======================================================*/
// END Multiversal Sheet Worker Generator //
// ======================================================*/