Ok, made an adjusted version that adds in !eot (or !feot in case you have something else doing eot stuff you don't want to trigger), as well as an end marker and resorting when things are changed in the turn order. That makes it easy to remove turns for interrupt (it will respect the change in sorting when your total drops lower than someone else's) or adjust the value of turns for some reason. !eot, !feot, and just clicking the arrow to advance the turn will remove the top turn from the turn order. Example: Gish interrupts and uses their 5 and 10, resulting in the tie breaker order for 6 swapping. In this case, the GM just trashes those two turns for Gish causing the resort to happen. Example: Running !eot, Rook's 2 entry has been removed. Only the GM or someone that controls the current token can issue !eot or !feot. Update 10/1/2022: Fixed for new turnorder pageid requirement. 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();
});