Files
roll20-character-sheets-tester/Lion & Dragon/LionDragon.js
T

330 lines
13 KiB
JavaScript

/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
/*jshint -W014,-W084,-W030,-W033,-*/
//Sheet variables
const version = 0.1;
const toMonitor = ['strength','dexterity','constitution','intelligence','wisdom','charisma','base_attack'];
const baseGet = [...toMonitor,'strength_mod','dexterity_mod','constitution_mod','intelligence_mod','wisdom_mod','charisma_mod'];
const repeatMonitor = ['repeating_arms','repeating_miracles','repeating_lores','repeating_equipment:weight','repeating_parry'];
const repeating_section_details = [
/*TEMPLATE
{
section:'string',
fields:['string','string']
},
*/
{
section:'repeating_arms',
fields:['name','type','base_mod','custom_mod','damage','damage_ability','range','description']
},
{
section:'repeating_parry',
fields:['name','type','base_mod','custom_mod','block','block_mod','block_ability']
},
{
section:'repeating_miracles',
fields:['name','total_mod','custom_mod','description']
},
{
section:'repeating_lores',
fields:['name','total_mod','custom_mod','description']
},
{
section:'repeating_equipment',
fields:['weight']
},
];
//Async functions
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
});
};
//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);
});
};
const _getSections = function(){
let queue = _.clone(repeating_section_details);
const worker = async (repeatAttrs=[],sections={})=>{
let detail = queue.shift();
sections[detail.section] = sections[detail.section] || {};
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];
}
};
return worker();
};
//Utility Functions
const log = function(msg){
const sheetName = 'Lion & Dragon';
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 updateSheet = async function(){
let attributes = _getAttrs(['sheet_version',...baseGet]);
const setObj = {
sheet_version:version
};
log(`Sheet Update applied. Current Sheet Version ${version}`);
set(setObj);
}
const set = function(setObj,callback){
log({setObj});
return setAttrs(setObj,{silent:true},callback);
};
const expandRepeating = function(cascade,sections,attributes){
return _.keys(cascade).reduce((memo,key)=>{
if(/^repeating/.test(key)){
key.replace(/(repeating_[^_]+)_[^_]+_(.+)/,(match,section,field)=>{
_.each(sections[section],(id)=>{
let row = `${section}_${id}_${field}`;
memo[row]=_.clone(cascade[key]);
memo[row].affects = memo[row].affects.map((affected)=>{
return affected.replace(/(repeating_[^_]+_)[^_]+(.+)/,`$1${id}$2`);
});
memo[row].name = row;
if(memo[row].calculation && typeof memo[row].calculation === 'string'){
memo[row].calculation = memo[row].calculation.replace(/(@{repeating_[^_]+?_)[^_]+?(_.+?})/g,`$1${id}$2`);
}
});
});
}else{
memo[key] = _.clone(cascade[key]);
memo[key].affects = memo[key].affects.reduce((m,a)=>{
if(/^repeating/.test(a)){
a.replace(/(repeating_[^_]+?)_[^_]+?(_.+)/,(match,section,field)=>{
if(section === 'repeating_arms' && /_mod$/.test(key) && field==='_total_mod'){
sections[section].forEach((id)=>{
if(attributes[`${section}_${id}_type`]===`@{${key}}`){
m.push(`${section}_${id}${field}`);
log({m});
}
});
}else{
sections[section].forEach(id=>m.push(`${section}_${id}${field}`));
}
});
}else{
m.push(a);
}
return m;
},[])
}
return memo;
},{});
};
//Calculation Functions
const abilityMod = function(score){
let mod;
score = +score;
log({score});
if(score <= 3){
mod = -3;
}
else if(score <= 5){
mod = -2;
}
else if(score <= 8){
mod = -1;
}
else if(score <= 12){
mod = 0;
}
else if(score <= 15){
mod = 1;
}
else if(score <= 17){
mod = 2;
}else{
mod = 3;
}
log({mod});
return mod;
};
const sheetCalc = function(name,calculation,attributes,setObj){
let parsedCalc = calculation;
if(calculation === 'mod') return abilityMod(attributes[name.replace(/_mod/,'')]);
parsedCalc = attributeParse(parsedCalc,attributes);
parsedCalc = parsedCalc.replace(/floor|ceil|round|abs/g,'Math.$&').replace(/\[\[|\]\]|\[.*?\]|=/g,'');
let finalCalc;
try{
finalCalc = eval(parsedCalc);
}catch(err){
finalCalc = undefined;
}
return finalCalc;
};
const attributeParse = function(string,attributes){
log({string});
while(/@{[^}]+}/.test(string)){
log('attributeParse: Attribute call found');
string = string.replace(/@{([^}]+)}/g,(match,name)=>{
return attributes[name.toLowerCase()];
});
}
return string;
};
//converts a value to a number, it's default value, or 0
const value = function(val,def){
return (+val||def||0);
};
//Calculates total encumbrance
const totalEncumbrance = function(statObj,attributes,sections){
log('Calculating Encumbrance');
return sections.repeating_equipment.reduce((total,id)=>{
return total += value(attributes[`repeating_equipment_${id}_weight`]);
},0);
};
//Calculates total attack bonus
const attackBonus = function(statObj,attributes,sections){
return attributes.base_attack;
};
//Sheet Functions
const accessSheet = async function(event){
let [repeats,sections] = await _getSections();
const attributes = await _getAttrs([...baseGet,...repeats]);
log({sections,attributes});
const setObj = {}
const casc = expandRepeating(cascades,sections,attributes);
let trigger = casc[event.sourceAttribute];
log({event,trigger,casc});
let queue = [...trigger.affects];
log({queue});
while(queue.length){
let name = queue.shift();
let obj = casc[name];
log({obj,type:typeof obj.calculation});
let val = typeof obj.calculation === 'string' ? sheetCalc(obj.name,obj.calculation,attributes,setObj) : obj.calculation(obj,attributes,sections);
if(value(val) !== value(attributes[obj.name])){
setObj[obj.name] = value(val);
attributes[obj.name] = setObj[obj.name];
queue = [...queue,...obj.affects];
}
}
set(setObj);
};
const cascades = {
//Attributes
strength:{name:'strength',defaultValue:10,type:'number',affects:['strength_mod']},
dexterity:{name:'dexterity',defaultValue:10,type:'number',affects:['dexterity_mod']},
constitution:{name:'constitution',defaultValue:10,type:'number',affects:['constitution_mod']},
intelligence:{name:'intelligence',defaultValue:10,type:'number',affects:['intelligence_mod']},
wisdom:{name:'wisdom',defaultValue:10,type:'number',affects:['wisdom_mod']},
charisma:{name:'charisma',defaultValue:10,type:'number',affects:['charisma_mod']},
strength_mod:{name:'strength_mod',defaultValue:0,type:'number',calculation:'mod',affects:[]},
dexterity_mod:{name:'dexterity_mod',defaultValue:0,type:'number',calculation:'mod',affects:[]},
constitution_mod:{name:'constitution_mod',defaultValue:0,type:'number',calculation:'mod',affects:[]},
intelligence_mod:{name:'intelligence_mod',defaultValue:0,type:'number',calculation:'mod',affects:['repeating_lores_$X_total_mod']},
wisdom_mod:{name:'wisdom_mod',defaultValue:0,type:'number',calculation:'mod',affects:['repeating_miracles_$X_total_mod']},
charisma_mod:{name:'charisma_mod',defaultValue:0,type:'number',calculation:'mod',affects:[]},
//Equipment/Encumbrance
encumbrance:{name:'encumbrance',defaultValue:0,type:'number',affects:[],calculation:totalEncumbrance},
repeating_equipment_$X_weight:{name:'repeating_equipment_$X_weight',defaultValue:0,type:'number',affects:['encumbrance']},
//Base Attack/Arms/Def Combat
base_attack:{name:'base_attack',defaultValue:0,type:'number',affects:['repeating_arms_$X_base_mod','repeating_parry_$X_base_mod']},
repeating_arms_$X_name:{name:'repeating_arms_$X_name',defaultValue:0,type:'number',affects:['repeating_arms_$X_base_mod']},
repeating_arms_$X_type:{name:'repeating_arms_$X_type',defaultValue:0,type:'number',affects:['repeating_arms_$X_base_mod']},
repeating_arms_$X_base_mod:{name:'repeating_arms_$X_base_mod',defaultValue:0,type:'number',calculation:attackBonus,affects:[]},
repeating_arms_$X_custom_mod:{name:'repeating_arms_$X_custom_mod',defaultValue:0,type:'number',affects:['repeating_arms_$X_base_mod']},
repeating_arms_$X_damage:{name:'repeating_arms_$X_damage',defaultValue:0,type:'number',affects:['repeating_arms_$X_base_mod']},
repeating_arms_$X_damage_ability:{name:'repeating_arms_$X_damage_ability',defaultValue:0,type:'number',affects:['repeating_arms_$X_base_mod']},
repeating_arms_$X_range:{name:'repeating_arms_$X_range',defaultValue:0,type:'number',affects:['repeating_arms_$X_base_mod']},
repeating_arms_$X_description:{name:'repeating_arms_$X_description',defaultValue:0,type:'number',affects:['repeating_arms_$X_base_mod']},
repeating_parry_$X_name:{name:'repeating_parry_$X_name',defaultValue:0,type:'number',affects:['repeating_parry_$X_base_mod']},
repeating_parry_$X_type:{name:'repeating_parry_$X_type',defaultValue:0,type:'number',affects:['repeating_parry_$X_base_mod']},
repeating_parry_$X_base_mod:{name:'repeating_parry_$X_base_mod',defaultValue:0,type:'number',calculation:attackBonus,affects:[]},
repeating_parry_$X_custom_mod:{name:'repeating_parry_$X_custom_mod',defaultValue:0,type:'number',affects:['repeating_parry_$X_base_mod']},
repeating_parry_$X_block:{name:'repeating_parry_$X_damage',defaultValue:0,type:'number',affects:['repeating_parry_$X_base_mod']},
repeating_parry_$X_block_ability:{name:'repeating_parry_$X_block_ability',defaultValue:0,type:'number',affects:['repeating_parry_$X_base_mod']},
//Miracles
repeating_miracles_$X_name:{name:'repeating_miracles_$X_name',defaultValue:0,type:'number',affects:['repeating_miracles_$X_total_mod']},
repeating_miracles_$X_custom_mod:{name:'repeating_miracles_$X_custom_mod',defaultValue:0,type:'number',affects:['repeating_miracles_$X_total_mod']},
repeating_miracles_$X_total_mod:{name:'repeating_miracles_$X_total_mod',defaultValue:0,type:'number',calculation:'@{repeating_miracles_$X_custom_mod}+@{wisdom_mod}',affects:[]},
//Lores
repeating_lores_$X_name:{name:'repeating_lores_$X_name',defaultValue:0,type:'number',affects:['repeating_lores_$X_total_mod']},
repeating_lores_$X_custom_mod:{name:'repeating_lores_$X_custom_mod',defaultValue:0,type:'number',affects:['repeating_lores_$X_total_mod']},
repeating_lores_$X_total_mod:{name:'repeating_lores_$X_total_mod',defaultValue:0,type:'number',calculation:'@{repeating_lores_$X_custom_mod}+@{intelligence_mod}',affects:[]},
};
//Listeners
on('sheet:opened',updateSheet);
[...baseGet,...repeatMonitor].forEach(g =>
on(`change:${g}`,accessSheet)
);
log('Sheet Started');