Last active
July 16, 2020 17:45
-
-
Save StrictlySkyler/ad9b192d3a864ff5ef7adeb23dc4c4c7 to your computer and use it in GitHub Desktop.
Roll20 API Round-By-Round Initiative Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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