Skip to content

Instantly share code, notes, and snippets.

@StrictlySkyler
Last active July 16, 2020 17:45
Show Gist options
  • Save StrictlySkyler/ad9b192d3a864ff5ef7adeb23dc4c4c7 to your computer and use it in GitHub Desktop.
Save StrictlySkyler/ad9b192d3a864ff5ef7adeb23dc4c4c7 to your computer and use it in GitHub Desktop.
Roll20 API Round-By-Round Initiative Script
/*
Round-By-Round Initiative for Roll20
To use:
1. Select some tokens/objects
2. Type !init in the Roll20 chat to begin Initiative
3. When the initiative tracker indicates a new round,
announce, reroll, and sort automatically
4. When initiative is over, type !end in the chat to end
You can also optionally type !reroll in the chat to force
a new round.
I recommend assigning these commands to Macros for easy use.
Caveats:
- It doesn't (yet) support Advantage on Initiative rolls.
For this, I suggest using a Macro and letting any
player in question update it manually.
*/
const dieSize = 20; //rolling 1d20, change if you roll 1dXX
const byInit = (a, b) => {
first = a.pr;
second = b.pr;
return second - first;
};
const getTurnorder = () => (
(Campaign().get(`turnorder`) == `` && []) ||
JSON.parse(Campaign().get(`turnorder`))
);
const updateTurnorder = entry => {
const turnorder = getTurnorder();
turnorder.push(entry);
turnorder.sort(byInit);
Campaign().set("turnorder", JSON.stringify(turnorder));
};
const addChar = (obj, pre, id, breakdown, initString) => {
sendChat(`character|${obj.get(`represents`)}`, breakdown, (ops) => {
const { entry, composed, roll } = getEntry(id, ops[0]);
updateTurnorder(entry);
sendChat(
`character|${obj.get('represents')}`,
`${pre} rolled ${roll} ${composed} for Initiative!`
);
});
};
const getEntry = (id, result) => {
const content = JSON.parse(result.content);
let rolled = `${result.origRoll}`;
content.rolls[0].results.forEach(roll => rolled += `, ${roll.v}`);
const composed = `(${rolled})`;
const entry = {
id,
pr: content.total % 1 === 0 ?
parseInt(content.total, 10) :
parseFloat(content.total).toFixed(2)
};
return { entry, composed, roll: entry.pr };
}
const addToken = (obj, pre, id, breakdown) => {
sendChat(obj.get("name"), breakdown, (ops) => {
const { entry, composed, roll } = getEntry(id, ops[0]);
updateTurnorder(entry);
sendChat(
obj.get("name"),
`${pre} rolled ${roll} ${composed} for Initiative!`
);
});
};
const getInitString = (char, obj) => {
let initString = '+0';
let charInit;
let charInitBonus;
let objInit;
let objInitBonus;
try { charInit = getAttrByName(char.id, 'initiative'); }
catch (e) { }
try { charInitBonus = getAttrByName(char.id, 'initiative_bonus'); }
catch (e) { }
try { objInit = getAttrByName(obj.id, 'initiative'); }
catch (e) { }
try { objInitBonus = getAttrByName(obj.id, 'initiative_bonus'); }
catch (e) { }
if (char && charInit != undefined) { initString = charInit; }
else if (char && charInitBonus != undefined) {
initString = `+${charInitBonus}`;
}
else if (objInit != undefined) { initString = objInit; }
else if (objInitBonus != undefined) { initString = `+${objInitBonus}`; }
return initString;
};
const getAdvantageDice = (char, obj) => {
let initAdv;
let dice = 1;
try { initAdv = getAttrByName(char.id, 'initAdv'); }
catch (e) {
try { initAdv = getAttrByName(obj.id, 'initAdv'); }
catch (e) { initAdv = 0; }
}
if (initAdv) {
initAdv = parseInt(initAdv, 10);
dice = isNaN(initAdv) ? dice : dice + initAdv;
}
return dice;
};
const addInit = (id) => {
const obj = getObj("graphic", id);
const char = getObj("character", obj.get("represents"));
let initString = getInitString(char, obj);
let dice = getAdvantageDice(char, obj);
//https://roll20.zendesk.com/hc/en-us/articles/360037773133-Dice-Reference
const advantage = dice > 1 ? 'kh1' : '';
const breakdown = `/roll ${dice}d${dieSize}${advantage}${initString}`;
const pre = char && char.get("controlledby") != "" ? '' : '/w GM';
if (char) { addChar(obj, pre, id, breakdown, initString); }
else { addToken(obj, pre, id, breakdown); }
};
const startInit = msg => (
msg.type == "api"
&& msg.content.indexOf("!init") !== -1
&& msg.who.indexOf("(GM)") !== -1
);
const begin = (selected) => {
log(`Beginning Initiative.`);
sendChat("", "/desc Initiative begins!");
Campaign().set("initiativepage", true );
try { _.each(selected, (entry) => { addInit(entry._id); }); }
catch (err) { return; }
}
const endInit = msg => (
msg.type == "api"
&& msg.content.indexOf("!end") !== -1
&& msg.who.indexOf("(GM)") !== -1
);
const end = () => {
log(`Ending Initiative.`);
sendChat("", "/desc Initiative ends.");
Campaign().set("turnorder", "");
Campaign().set("initiativepage", false );
order = [];
round = 1;
};
const newRound = (msg) => (
msg.type == 'api'
&& msg.content.indexOf('!reroll') !== -1
&& msg.who.indexOf("(GM)") !== -1
);
const reroll = () => {
round++;
sendChat("", `/desc Round ${round} begins! Rerolling Initiative.`);
log(`Rerolling Initiative for round ${round}.`);
order = Campaign().get('turnorder');
order = order.length ? JSON.parse(order) : false;
if (order) {
Campaign().set("turnorder", "");
_.each(order, (entry) => { addInit(entry.id); });
}
}
const nextTurn = (currentOrder, prevOrder, sorted) => {
log(`*** Next up!
Was: ${JSON.stringify(prevOrder[0], null, 2)}
Now: ${JSON.stringify(currentOrder[0], null, 2)}
`);
log(`*** Checking for new round
Next up matches First? ${(currentOrder[0].id == sorted[0].id)}
Last up matches Last? ${(prevOrder[0].id == sorted[sorted.length-1].id)}
`);
if (
currentOrder[0].id == sorted[0].id &&
prevOrder[0].id == sorted[sorted.length-1].id
) {
log('*** New round detected, re-rolling initiative');
return true;
}
return false;
}
let order = [];
let round = 1;
on("chat:message", (msg) => {
if (startInit(msg)) { begin(msg.selected); }
else if (endInit(msg)) { end(); };
if (newRound(msg)) { reroll(); }
});
on('change:campaign:turnorder', (obj, prev) => {
const currentOrder = JSON.parse(obj.get('turnorder'));
const prevOrder = JSON.parse(prev.turnorder);
const sorted = Array.from(prevOrder).sort(byInit);
if (nextTurn(currentOrder, prevOrder, sorted)) { reroll(); }
});
log(`*** Round-By-Round Initiative successfully loaded! ***`);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment