
Many thanks to TheAaron for taking to time to fix up this. For more info on how it works, please refer to the original forum post about it; https://app.roll20.net/forum/permalink/7323238/
This fixed script works perfectly with the character sheet created for FFRPG 4e
on('ready',()=>{
const DIE_SIZE = 10;
const DEFAULT_NUM_DICE = 3;
const END_LABEL = '--- END ---';
const styles = {
entry: {
"font-size": ".8em"
},
name: {
"font-weight": "bold"
},
roll: {
"background-color": "#fff",
"border": "1px solid #ddd",
"border-radius": ".5em",
"color": "#966",
"font-family": "monospace",
"line-height": "2em",
"font-weight": "bold",
"width": "2em",
"height": "2em",
"overflow": "auto",
"text-align": "center",
"display": "inline-block"
}
};
const playerCanControl = (obj, playerid='any') => {
const playerInControlledByList = (list, playerid) => list.includes('all') || list.includes(playerid) || ('any'===playerid && list.length);
let players = obj.get('controlledby')
.split(/,/)
.filter(s=>s.length);
if(playerInControlledByList(players,playerid)){
return true;
}
if('' !== obj.get('represents') ) {
players = (getObj('character',obj.get('represents')) || {get: function(){return '';} } )
.get('controlledby').split(/,/)
.filter(s=>s.length);
return playerInControlledByList(players,playerid);
}
return false;
};
const processInlinerolls = (msg) => {
if(_.has(msg,'inlinerolls')){
return _.chain(msg.inlinerolls)
.reduce(function(m,v,k){
let ti=_.reduce(v.results.rolls,function(m2,v2){
if(_.has(v2,'table')){
m2.push(_.reduce(v2.results,function(m3,v3){
m3.push(v3.tableItem.name);
return m3;
},[]).join(', '));
}
return m2;
},[]).join(', ');
m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0;
return m;
},{})
.reduce(function(m,v,k){
return m.replace(k,v);
},msg.content)
.value();
} else {
return msg.content;
}
};
const s = (name) => {
if(styles.hasOwnProperty(name)){
return `style="${Object.keys(styles[name]).reduce((m,k)=>`${m}${k}:${styles[name][k]};`,'')}"`;
}
return '';
};
const getTurnArray = () => ( '' === Campaign().get('turnorder') ? [] : JSON.parse(Campaign().get('turnorder')));
const setTurnArray = (ta) => Campaign().set({turnorder: JSON.stringify(ta)});
const addTokenTurn = (id, pr, pageid) => setTurnArray([...getTurnArray(), {id,pr,_pageid:(pageid||(getObj('graphic',id)||{get:''}).get('pageid'))}]);
const addCustomTurn = (custom, pr) => Campaign().set({ turnorder: JSON.stringify( [...getTurnArray(), {id:'-1',custom,pr}]) });
const removeTokenTurn = (tid) => Campaign().set({ turnorder: JSON.stringify( getTurnArray().filter( (to) => to.id !== tid)) });
const clearTurnOrder = () => Campaign().set({turnorder:'[]'});
const sorter_asc = (a, b) => b.pr - a.pr;
const sorter_desc = (a, b) => a.pr - b.pr;
const sortTurnOrder = (sortBy = sorter_desc) => Campaign().set({turnorder: JSON.stringify(getTurnArray().sort(sortBy))});
const sorter_ascFF4 = (lookup) => (a, b) => {
if(a.custom===END_LABEL){
return 1;
}
if(b.custom===END_LABEL){
return -1;
}
let dir = a.pr - b.pr;
if( 0 === dir) {
dir = lookup[b.id] - lookup[a.id];
}
return dir;
};
const buildFF4Lookup = (ta) => ta.reduce((m,o)=>{
m[o.id]=(m[o.id]||0)+o.pr;
return m;
},{});
const doRolls = (num=DEFAULT_NUM_DICE) => [...Array(num)].map(()=>randomInteger(DIE_SIZE));
on('change:campaign:turnorder', (obj,prev) => {
let to = getTurnArray();
let told = (( '' === prev.turnorder) ? [] : JSON.parse(prev.turnorder));
let l = to.pop();
let f = told.shift();
if(f && l && f.id === l.id && f.pr === l.pr){
setTurnArray(to);
} else {
assureEndLabel();
}
to = getTurnArray();
let lookup = buildFF4Lookup(to);
sortTurnOrder(sorter_ascFF4(lookup));
});
const assureEndLabel = () => {
let to = getTurnArray();
let f = to.find((e)=>END_LABEL===e.custom);
if(!f){
addCustomTurn(END_LABEL,'');
to = getTurnArray();
let lookup = buildFF4Lookup(to);
sortTurnOrder(sorter_ascFF4(lookup));
}
};
on('chat:message',(msg)=>{
if('api'===msg.type) {
if(/^!ffi\b/i.test(msg.content)){
let c = processInlinerolls(msg);
// !ffi <NumRolls>
let args = c.split(/\s+/);
let output = [];
let outputPrivate = [];
if(msg.selected){
msg.selected
.map(o=>getObj('graphic',o._id))
.filter(g=>undefined !== g)
.map(t => ({token:t,rolls:doRolls(parseInt(args[1],10)||undefined)}))
.filter(o=>removeTokenTurn(o.token.id) || true)
.filter(o=>o.rolls.forEach(r=>addTokenTurn(o.token.id,r,o.token.get('pageid')))||true)
.forEach(o=>{
let m=`<div ${s('entry')}><span ${s('name')}>${o.token.get('name')}</span> rolled: ${
o.rolls.map(r=>`<span ${s('roll')}>${r}</span>`).join(' ')
}</div>`;
if('objects' === o.token.get('layer')){
output.push(m);
} else {
outputPrivate.push(m);
}
})
;
let to = getTurnArray();
let lookup = buildFF4Lookup(to);
sortTurnOrder(sorter_ascFF4(lookup));
if(output.length){
log(output.join(''));
sendChat('FF4 Init', output.join(''));
}
if(outputPrivate.length){
sendChat('FF4 Init', `/w "gm" ${outputPrivate.join('')}`);
}
}
} else if( /^!f?eot\b/i.test(msg.content)){
let to = getTurnArray();
let token = getObj('graphic',(to[0]||{id:-1}).id);
if(token && (playerIsGM(msg.playerid) || playerCanControl(token,msg.playerid))){
to.shift();
setTurnArray(to);
let lookup = buildFF4Lookup(to);
sortTurnOrder(sorter_ascFF4(lookup));
} else {
let who = getObj('player',msg.playerid).get('_displayname');
sendChat('FF4 Init', `/w "${who}" <div ${s('entry')}>It is not one of your character's turns right now.</div>`);
}
}
}
});
assureEndLabel();
});