Files
roll20-character-sheets/Supers-Revised-Edition/javascript/utility.js
T
2022-02-28 00:05:57 -07:00

336 lines
12 KiB
JavaScript

/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
/*jshint -W014,-W084,-W030,-W033*/
//# Attribute Obj Proxy handler
const createAttrProxy = function(attributes){
//creates a proxy for the attributes object so that values can be worked with more easily.
const attrTarget = {
updates:{},
attributes:{...attributes},
repOrders:{}
};
const attrHandler = {
get:function(obj,prop){//gets the most value of the attribute.
//If it is a repeating order, returns the array, otherwise returns the update value or the original value
if(prop === 'set'){
return function(){
set(obj.updates,...arguments);
}
}else if(Object.keys(obj).some(key=>key===prop)){
return Reflect.get(...arguments)
}else{
let retValue;
switch(true){
case obj.repOrders.hasOwnProperty(prop):
retValue = obj.repOrders[prop];
break;
case obj.updates.hasOwnProperty(prop):
retValue = obj.updates[prop];
break;
default:
retValue = obj.attributes[prop];
break;
}
let cascRef = prop.replace(/(repeating_[^_]+_)[^_]+/,'$1\$x');
let numRetVal = +retValue;
if(!Number.isNaN(numRetVal)){
retValue = numRetVal;
}/*else if(cascades[cascRef] && (cascades[cascRef].type === 'number')){
retValue = cascades[cascRef].defaultValue || 0;
}*/
return retValue;
}
},
set:function(obj,prop,value){
//Sets the value. Also verifies that the value is a valid attribute value
//e.g. not undefined, null, or NaN
if(value || value===0 || value===''){
if(/reporder|^repeating_[^_]+$/.test(prop)){
let section = prop.replace(/_reporder_/,'');
obj.repOrders[section] = value;
}else{
obj.updates[prop] = value;
}
}else{
debug(`!!!Warning: Attempted to set ${prop} to an invalid value:${value}; value not stored!!!`);
}
return true;
},
deleteProperty(obj,prop){
//removes the property from the original attributes, updates, and the reporders
Object.keys(obj).forEach((key)=>{
delete obj[key][prop.toLowerCase()];
});
}
};
return new Proxy(attrTarget,attrHandler);
};
const _setSectionOrder = function(section,order){
let trueSection = section.replace(/repeating_/,'');
setSectionOrder(trueSection,order);
};
const _removeRepeatingRow = function(row,attributes,sections){
debug(`removing ${row}`);
Object.keys(attributes.attributes).forEach((key)=>{
if(key.startsWith(row)){
delete attributes[key];
}
});
let [,section,rowID] = row.match(/(repeating_[^_]+)_(.+)/,'');
sections[section] = sections[section].filter((id)=>id!==rowID);
removeRepeatingRow(row);
};
const _getAttrs = function({callback,props=baseGet}){
getAttrs(props,(values)=>{
const attributes = createAttrProxy(values);
callback(attributes);
});
};
const getAllAttrs = function({callback,props=baseGet,sectionDetails=repeatingSectionDetails}){
getSections(sectionDetails,(repeats,sections)=>{
getAttrs([...props,...repeats],(values)=>{
const attributes = createAttrProxy(values);
orderSections(attributes,sections);
//const casc = expandCascade(cascades,sections,attributes);
callback(attributes,sections/*,casc*/);
})
});
};
const getSections = function(section_details,callback){
let queueClone = _.clone(section_details);
const worker = (queue,repeatAttrs=[],sections={})=>{
let detail = queue.shift();
getSectionIDs(detail.section,(IDs)=>{
sections[detail.section] = IDs;
IDs.forEach((id)=>{
detail.fields.forEach((f)=>{
repeatAttrs.push(`${detail.section}_${id}_${f}`);
});
});
repeatAttrs.push(`_reporder_${detail.section}`);
if(queue.length){
worker(queue,repeatAttrs,sections);
}else{
callback(repeatAttrs,sections);
}
});
};
if(!queueClone[0]){
callback([],{});
}
worker(queueClone);
};
// Sets the attributes while always calling with {silent:true}
// Can be awaited to get the values returned from _setAttrs
const set = function(obj,vocal=false){
debug({'Setting Attributes':obj});
setAttrs(obj,{silent:!vocal});
};
//sanitizes text for use as a regex
sanitizeForRegex = function(text){
return text.replace(/\.|\||\(|\)|\[|\]|\-|\+|\?|\/|\{|\}|\^|\$|\*/g,'\\$&');
};
//converts a value to a number, it's default value, or 0
const value = function(val,def){
return (+val||def||0);
};
//extracts the section, rowID, and field name from a repeating attribute name
/*
e.g. repeating_equipment_-09Jhu89z_bulk would give:
section: equipment
rowID: -09Jhu89z
field: bulk
*/
const parseRepeatName = function(string){
let match = string.match(/(repeating_[^_]+)_([^_]+)(?:_(.+))?/);
match.shift();
return match;
};
const parseTriggerName = function(string){
let match = string.match(/(?:(repeating_[^_]+)_([^_]+)_)?(.+)/);
match.shift();
return match;
};
const parseClickTrigger = function(string){
let match = string.match(/clicked:(?:(repeating_[^_]+)_([^_]+)_)?(.+)/);
match.shift();
return match;
};
const parseHTMLName = function(string){
let match = string.match(/(?:attr|act|roll)_(.+)/);
match.shift();
return match[0];
};
const capitalize = function(string){
return string.replace(/(?:^|\s+|\/)[a-z]/ig,(letter)=>letter.toUpperCase());
};
const extractQueryResult = async function(query){
debug('entering extractQueryResult');
let queryRoll = await startRoll(`!{{query=[[0[response=?{${query}}]]]}}`);
finishRoll(queryRoll.rollId);
debug({queryRoll});
return queryRoll.results.query.expression.replace(/^.+?response=|\]$/g,'');
};
const pseudoQuery = async function(value){
debug('entering pseudoQuery');
let queryRoll = await startRoll(`!{{query=[[0[response=${value}]]]}}`);
finishRoll(queryRoll.rollId);
return queryRoll.results.query.expression.replace(/^.+?response=|\]$/g,'');
};
//# Utility Functions #
const log = function(msg){
if(typeof msg === 'string'){
console.log(`%c${sheetName} log| ${msg}`,"background-color:#159ccf");
}else if(typeof msg === 'object'){
Object.keys(msg).forEach((m)=>{
if(typeof msg[m] === 'string'){
console.log(`%c${sheetName} log| ${m}: ${msg[m]}`,"background-color:#159ccf");
}else{
console.log(`%c${sheetName} log| ${typeof msg[m]} ${m}`,"background-color:#159ccf");
console.table(msg[m]);
}
});
}
};
const debug = function(msg,force){
if(!debugMode && !force && version > 0) return;
if(typeof msg === 'string'){
console.log(`%c${sheetName} DEBUG| ${msg}`,"background-color:tan;color:red;");
}else if(typeof msg === 'object'){
Object.keys(msg).forEach((m)=>{
if(typeof msg[m] === 'string'){
console.log(`%c${sheetName} DEBUG| ${m}: ${msg[m]}`,"background-color:tan;color:red;");
}else{
console.log(`%c${sheetName} DEBUG| ${typeof msg[m]} ${m}`,"background-color:tan;color:red;font-weight:bold;");
console.table(msg[m]);
}
});
}
};
//Section ordering
//orders the section id arrays to match the repOrder attribute
const orderSections = function(attributes,sections){
Object.keys(sections).forEach((section)=>{
attributes.attributes[`_reporder_${section}`] = commaArray(attributes[`_reporder_${section}`]);
orderSection(attributes.attributes[`_reporder_${section}`],sections[section]);
});
};
//orders a single ID array
const orderSection = function(repOrder,IDs=[]){
IDs.sort((a,b)=>{
return repOrder.indexOf(a.toLowerCase()) - repOrder.indexOf(b.toLowerCase());
});
};
//splits a comma delimited string into an array
const commaArray = function(string=''){
return string.toLowerCase().split(/\s*,\s*/);
};
//Creates a row ID specific for an add section for pseudo nested repeating sections
const generateCustomID = function(string,rowID){
rowID = rowID || generateRowID();
let re = new RegExp(`^.{${string.length}}`);
return `${string}${rowID.replace(re,'')}`;
};
//Sheet Updaters and styling functions
const updateSheet = function(){
debug('updating sheet');
getAllAttrs({props:['sheet_version','debug_mode','collapsed',...baseGet],callback:(attributes,sections,casc)=>{
debugMode = !!attributes.debug_mode;
attributes.sheet_version = version;
if(!attributes.sheet_version){
initialSetup(attributes,sections);
}
log(`Sheet Update applied. Current Sheet Version ${version}`);
styleOnOpen(attributes,sections);
setActionCalls({},attributes,sections);
attributes.set();
log('Sheet ready for use');
}})
};
const initialSetup = function(attributes,sections){
debug('Initial sheet setup');
};
const styleOnOpen = function(attributes,sections){
navigateSheet({triggerName:`clicked:${attributes.sheet_state}`});
setupSystem({},attributes,sections);
};
/*
Cascade Expansion functions
*/
//Expands the repeating section templates in cascades to reflect the rows actually available
const expandCascade = function(cascade,sections,attributes){
return _.keys(cascade).reduce((memo,key)=>{//iterate through cascades and replace references to repeating attributes with correct row ids.
if(/^repeating/.test(key)){//If the attribute is a repeating attribute, do special logic
expandRepeating(memo,key,cascade,sections,attributes);
}else{//for non repeating attributes do this logic
expandNormal(memo,key,cascade,sections);
}
return memo;
},{});
};
const expandRepeating = function(memo,key,cascade,sections,attributes){
key.replace(/(repeating_[^_]+)_[^_]+?_(.+)/,(match,section,field)=>{
(sections[section]||[]).forEach((id)=>{
memo[`${section}_${id}_${field}`]=_.clone(cascade[key]);//clone the details so that each row's attributes have correct ids
memo[`${section}_${id}_${field}`].name = `${section}_${id}_${field}`;
memo[`${section}_${id}_${field}`].affects = memo[`${section}_${id}_${field}`].affects.reduce((m,affected)=>{
if(section === affected){//otherwise if the affected attribute is in the same section, simply set the affected attribute to have the same row id.
m.push(applyID(affected,id));
}else if(/repeating/.test(affected)){//If the affected attribute isn't in the same repeating section but is still a repeating attribute, add all the rows of that section
addAllRows(affected,m,sections);
}else{//otherwise the affected attribute is a non repeating attribute. Simply add it to the computed affected array
m.push(affected);
}
return m;
},[]);
});
});
};
const applyID = function(affected,id){
return affected.replace(/(repeating_[^_]+_)[^_]+(.+)/,`$1${id}$2`);
};
const expandNormal = function(memo,key,cascade,sections){
memo[key] = _.clone(cascade[key]);
memo[key].affects = memo[key].affects.reduce((m,a)=>{
if(/^repeating/.test(a)){
addAllRows(a,m,sections);
}else{
m.push(a);
}
return m;
},[]);
};
const triggerFunctions = function(obj,attributes,sections){
debug(`triggering functions for ${obj.name}`);
obj.triggeredFuncs && obj.triggeredFuncs.forEach(func=>func(obj,attributes,sections));
};
const initialFunction = function(obj,attributes,sections){
debug(`initial functions for ${obj.name}`);
obj.initialFunc && obj.initialFunc(obj,attributes,sections);
};
const addAllRows = function(affected,memo,sections){
affected.replace(/(repeating_[^_]+?)_[^_]+?_(.+)/,(match,section,field)=>{
sections[section].forEach(id=>memo.push(`${section}_${id}_${field}`));
});
};
let clickDelay = 500;
const defaultThrottle = callback => _.throttle(callback,clickDelay,{trailing:false});
const isDieRoll = function(field){
return /(?:d(?:8|10|12)(?:Crit)?|exertion)/.test(field);
};