Skip to content

Instantly share code, notes, and snippets.

@schultzcole
Last active March 27, 2021 23:00
Show Gist options
  • Save schultzcole/b90c2a900cc717afeb622dc55075ecd2 to your computer and use it in GitHub Desktop.
Save schultzcole/b90c2a900cc717afeb622dc55075ecd2 to your computer and use it in GitHub Desktop.
/**
* Allows a user to quickly roll a high number of attack and damage rolls for a weapon.
* Paste as a script macro, select token (or have an actor assigned as your character), run macro
* D&D5e only
* @author cole#9640
* @version 1
*/
async function main() {
if (!actor) {
// User either does not have an "assigned" character or doesn't have a token selected.
ui.notifications.warn("You must select a token.");
return;
}
const weaponOptions = actor.items.filter(i => i.data.type === "weapon" && i.hasAttack).map(i => `<option value="${i.id}">${i.data.name}</options>`);
const dlgContent = `
<p class="hint">${actor.data.name} is rolling a multiattack.</p>
<div class="form-group">
<label for="weaponSelect">Weapon:</label><select name="weaponSelect">${weaponOptions.join("\n")}</select>
</div>
<div class="form-group">
<label for="numRolls">Number of Attacks:</label><input name="numRolls" type="number" value="1" />
</div>
`;
let result;
try {
result = await form({ title: "Multiattack", content: dlgContent, buttons: { adv:"Advantage", nor:"Normal", dis:"Disadvantage" }, defaultButton: "nor" });
} catch {
// User canceled the dialog: abort.
return;
}
try {
validateResult(result);
} catch (e) {
// User entered invalid data: warn them, then abort.
ui.notifications.warn(e.message);
return;
}
const item = actor.items.get(result.form.weaponSelect);
if (!item) {
// The item ID could not be found on the actor.
// This should not happen, but it is possible: for instance if someone opens the dialog, then deletes a weapon, then submits the form.
ui.notifications.error(`Unable to find item with ID ${result.form.weaponSelect}`);
return;
}
const attackOptions = { fastForward: true, advantage: result.button === "adv", disadvantage: result.button === "dis", chatMessage: false };
const damageOptions = { fastForward: true, chatMessage: false };
const rolls = []
for (let i = 0; i < result.form.numRolls; i++) {
rolls.push({
attack: await item.rollAttack(attackOptions),
damage: await item.rollDamage({ options: damageOptions }),
});
}
const rows = rolls.map(attack => `<tr>${makeCell(attack.attack, "attack")}${makeCell(attack.damage, "damage")}</tr>`);
const content = `
<div class="dnd5e chat-card">
<header class="card-header flexrow">
<img src="${item.data.img}" title="${item.data.name} width="36" height="36" />
<h3>${item.data.name} - Multiattack</h3>
</header>
<table class="multiattack-table">
<tr>
<th>Attack<br /><span class="header-roll">[[/r ${rolls[0].attack.formula}]]</span></th>
<th>Damage<br /><span class="header-roll">[[/r ${rolls[0].damage.formula}]]</span></th>
</tr>
${rows.join("\n")}
</table>
</div>`;
ChatMessage.create({ content, speaker: ChatMessage.getSpeaker({ actor }) });
}
main();
// Utility function for creating a dialog with a function and getting the contents of the form within
function form({ title, content, buttons={ submit: "Submit" }, defaultButton="submit", options } = {}) {
return new Promise((resolve, reject) => {
buttons = Object.entries(buttons).reduce((acc, [key, label]) => {
acc[key] = {
label,
callback: ($html) => {
const fd = new FormDataExtended($html.find("form")[0]);
resolve({ button: key, form: fd.toObject() });
}
}
return acc;
}, {});
const dialog = new Dialog({
title,
content: `<form onsubmit="event.preventDefault()">${content}</form>`,
buttons,
default: defaultButton,
close: reject,
}, options);
dialog.render(true);
});
}
function validateResult(result) {
if (!Number.isInteger(result.form.numRolls) || result.form.numRolls < 1) throw Error(`Invalid number of rolls: "${result.form.numRolls}". Must be an integer greater than 0.`);
}
function makeCell(roll, cssClass) {
return `
<td>
<a class="inline-roll inline-result multiattack-roll multiattack-${cssClass}" title="${roll.formula}" data-roll="${escape(JSON.stringify(roll))}">${roll.total}</a>
</td>`;
}
/* technically optional css styling to improve the appearance of the multiattack chat cards */
/* use with the Custom CSS module or as a world stylesheet */
.multiattack-table {
border-spacing: 2px;
table-layout: fixed;
text-align: center;
border: 1px #999 solid;
}
.multiattack-table td:first-child,
.multiattack-table th:first-child{
border-right: 1px #999 solid;
}
.multiattack-table th {
font-size: 18px;
}
.multiattack-table th .inline-roll {
font-size: 12px;
font-weight: normal;
}
.multiattack-table .multiattack-roll {
display: inline-block;
width: calc(100% - 4px);
height: 22px;
line-height: 20px;
margin: 2px;
padding: 0;
vertical-align: middle;
font-size: 18px;
font-weight: bold;
background: #00000020;
border: 1px solid #999;
box-shadow: 0 0 2px #FFF inset;
}
.multiattack-table .multiattack-roll > i {
float: left;
line-height: 1.5rem;
vertical-align: middle;
}
@schultzcole
Copy link
Author

Here's a screenshot of what the end result looks like with the optional css included:
FoundryVTT_amH4IehkAM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment