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; <a href="https://app.roll20.net/forum/permalink/7323238/" rel="nofollow">https://app.roll20.net/forum/permalink/7323238/</a> 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(); });