Files
roll20-character-sheets/SCP The Tabletop Roleplaying Game/Source/Javascript/utility.js
T
2021-11-17 19:17:31 -07:00

268 lines
9.7 KiB
JavaScript

/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
/*jshint -W014,-W084,-W030,-W033,-*/
//# Roll20Async functions #
//These together allow the use of promises and await/async to allow us to escape callback hell
const setActiveCharacterId = function(charId){
var oldAcid=getActiveCharacterId();
var ev = new CustomEvent("message");
ev.data={"id":"0", "type":"setActiveCharacter", "data":charId};
self.dispatchEvent(ev);
return oldAcid;
};
const _getAttrs = function (props){
var acid=getActiveCharacterId();//save the current activeCharacterID in case it has changed when the promise runs
var prevAcid=null; //local variable defined here, because it needs to be shared across the promise callbacks defined below
return new Promise((resolve,reject)=>{
prevAcid=setActiveCharacterId(acid); //in case the activeCharacterId has changed, restore it to what we were expecting and save the current value to restore later
try{
getAttrs(props,(values)=>{ resolve(values); });
}catch{ reject(); }
}).finally(()=>{
setActiveCharacterId(prevAcid); //restore activeCharcterId to what it was when the promise first ran
});
};
const _getAllAttrs = async function(props=baseGet,sectionDetails=repeatingSectionDetails){
const [repeats,sections] = await _getSections(sectionDetails);
const attributes = await _getAttrs([...props,...repeats]);
const casc = expandCascade(cascades,sections,attributes);
return [attributes,repeats,sections,casc];
}
//use the same pattern for each of the following...
const _setAttrs = function (propObj, options){
var acid=getActiveCharacterId();
var prevAcid=null;
return new Promise((resolve,reject)=>{
prevAcid=setActiveCharacterId(acid);
try{
setAttrs(propObj,options,(values)=>{ resolve(values); });
}
catch{ reject(); }
}).finally(()=>{
setActiveCharacterId(prevAcid);
});
};
const _getSectionIDs = function(sectionName){
var acid=getActiveCharacterId();
var prevAcid=null;
return new Promise((resolve,reject)=>{
prevAcid=setActiveCharacterId(acid);
try{
getSectionIDs(sectionName,(values)=>{ resolve(values); });
}
catch{ reject(); }
}).finally(()=>{
setActiveCharacterId(prevAcid);
});
};
//# 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]);
}
});
}
};
const _getSections = function(section_details){
let queue = _.clone(section_details);
const worker = async (repeatAttrs=[],sections={})=>{
let detail = queue.shift();
let IDs = await _getSectionIDs(detail.section);
sections[detail.section] = IDs;
let attrs = IDs.forEach((i)=>{
detail.fields.forEach((f)=>{
repeatAttrs.push(`${detail.section}_${i}_${f}`);
});
});
if(queue.length){
return worker(repeatAttrs,sections);
}else{
return [repeatAttrs,sections];
}
};
if(!queue[0]){
return [[],{}];
}
return worker();
};
// Sets the attributes while always calling with {silent:true}
// Can be awaited to get the values returned from _setAttrs
const set = function(obj,vocal){
let setObj = verifyObj(obj);
return _setAttrs(setObj,{silent:vocal ? false : true});
};
//verifies that the property/keys of the setObj are valid
const verifyObj = function(obj){
return Object.keys(obj).reduce((m,p)=>{
if(obj[p] || obj[p]===0 || obj[p]===''){
m[p]=obj[p];
}else{
debug(`WARNING: setObj[${p}] = ${obj[p]}`);
}
return m;
},{})
};
//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 checkRollLog = function(sections,attributes){
debug({sections});
return;
sections.repeating_roll.forEach((id,index)=>{
let row =`repeating_roll_${id}`;
if(50 - index >=50 || (Date.now() - value(attributes[`${row}_date`])) > (1000*60*60*24)){
removeRepeatingRow(row);
delete attributes[`${row}_date`];
delete attributes[`${row}_json`];
}
});
};
const parseClickTrigger = function(string){
let match = string.match(/clicked:(?:(repeating_[^_]+)_([^_]+)_)?(.+)/);
match.shift();
return match;
};
//Sheet Updaters and styling functions
const updateSheet = async function(){
const [attributes,repeats,sections] = await _getAllAttrs(['sheet_version','debug_mode',...baseGet]);
debugMode = !debugMode ? !!attributes.debug_mode : debugMode;
const setObj = {
sheet_version:version,
};
if(!value(attributes.sheet_version)){
initialSetup(attributes,setObj,sections);
}
log(`Sheet Update applied. Current Sheet Version ${version}`);
styleOnOpen(attributes);
set(setObj);
};
const initialSetup = function(attributes,setObj,sections){
debug('Initial sheet setup');
['exertion_max','move_speed','reaction_defense','cognitive_resistance'].forEach((attr)=>{
setObj[attr] = cascades[attr].calculation(_.clone(cascades[attr]),attributes,setObj,sections);
debug(`default for ${attr} applied`);
});
['negotiation/persuade_mod','fashion/etiquette_mod','leadership_mod','disguise/blend_in_mod','intimidate/taunt_mod','showmanship_mod','resist_distress_mod','cognitive_resistance','initiative_mod','intuition_mod','occult/scp_lore_mod'].forEach((skill)=>{
setObj[`${skill}`] = actionModCalc({name:`${skill}`},attributes,setObj,sections);
debug({[`${skill}`]:setObj[`${skill}_mod`]});
});
debug({appearance:attributes.appearance,reasoning:attributes.reasoning});
setObj.operative_id = getActiveCharacterId();
};
const styleOnOpen = function(attributes){
styleSecurity({},attributes);
sectionDisplay({},attributes);
};
/*
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 checkNumber = function(name,casc,attributes,setObj){
if(!casc[name] || casc[name].type!=='number') return;
const re = /[^\-\d\.]/g;
if(re.test(attributes[name])){
setObj[name] = attributes[name].replace(re,'');
attributes[name] = setObj[name];
}
};
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);
};