Skip to content

Commit

Permalink
Merge pull request #106 from Merudo/fix-smite-verification
Browse files Browse the repository at this point in the history
Add valid actor and spell slot verification for Divine Smite macro
  • Loading branch information
AnthonyVadala committed Jul 20, 2020
2 parents d7503ea + bfb74d4 commit e42bddc
Showing 1 changed file with 93 additions and 114 deletions.
207 changes: 93 additions & 114 deletions actor/divine_smite.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,152 +3,131 @@
* can be selected, which increases the number of damage dice, and smiting a fiend or undead
* will also increase the number of damage dice.
*
* First, select a token to perform the smite, then target an enemy to be smitten. Make your regular
* attack and then if you choose to use Divine Smite, run this macro.
* If a token is not selected, the macro will default back to the default character for the Actor.
* This allows for the GM to cast the macro on behalf a character that possesses it,
* without requiring that a PC have their character selected.
* To execute the macro a target MUST be specified and, unless configured otherwise, the character must have an available spell slot.
* Make your regular attack and then if you choose to use Divine Smite, run this macro.
*/

//Configurable variables
let maxSpellSlot = 5; //Highest spell-slot level that may be used.
let affectedCreatureTypes = ["fiend", "undead", "undead (shapechanger)"]; //Creature types that take extra damage.
let allowConsumeSpellSlotChoice = false; //Can the user choose whether to consume the spell slot?
let consumeSpellSlot = true; //Does the macro consume a spell slot?
let allowCriticalHit = true; //Can the user choose whether the dice should be doubled as a critical hit?
let macroFlavor = `Macro Divine Smite - Damage Roll (Radiant)`; //Flavor to show in the chat roll.
let maxSpellSlot = 5; // Highest spell-slot level that may be used.
let affectedCreatureTypes = ["fiend", "undead", "undead (shapechanger)"]; // Creature types that take extra damage.

let dieSize = `d8`; //Die size to use for the SMITE.
//
if (allowCriticalHit) {
macroFlavor += ` - Critical Hit!`;
}
let optionsText = `<p>Spell Slot level to use Divine Smite with.</p><form>
<div class="form-group"><label>Spell Slot Level:</label><select id="slot-level" name="slot-level">`;

let s_actor = canvas.tokens.controlled[0]?.actor || game.user.character;
// Use token selected, or default character for the Actor if none is.
let s_actor = canvas.tokens.controlled[0]?.actor || game.user.character;

if (s_actor?.data.items.find((i) => i.name === "Divine Smite") === undefined) {
return ui.notifications.error(
`No valid actor selected that can use this macro.`
);
// Verifies if the actor can smite.
if (s_actor?.data.items.find(i => i.name === "Divine Smite") === undefined){
return ui.notifications.error(`No valid actor selected that can use this macro.`);
}

//#region Functions
let confirmed = false;
let criticalhit = false;
if (getAvailableSlots(s_actor) || !consumeSpellSlot) {
for (let i = 1; i < maxSpellSlot; i++) {
let chosenSpellSlots = getSpellSlots(s_actor, i);
if (chosenSpellSlots.value > 0 && consumeSpellSlot) {
optionsText += `<option value="${i}">${i} - ${chosenSpellSlots.value} slots available</option>`;
} else if (!consumeSpellSlot) {
optionsText += `<option value="${i}">${i}</option>`;
if (hasAvailableSlot(s_actor)) {

// Get options for available slots
let optionsText = "";
for (let i = 1; i < maxSpellSlot; i++) {
let chosenSpellSlots = getSpellSlots(s_actor, i);
if (chosenSpellSlots.value > 0) {
optionsText += `<option value="${i}">${i} - ${chosenSpellSlots.value} slots available</option>`;
}
}
}
optionsText += `</select></div>`;
if (allowCriticalHit) {
optionsText += `<div class="form-group"><label>Critical Hit:</label>
<input type="checkbox" name="criticalCheckbox" /></div>`;
}
if (allowConsumeSpellSlotChoice) {
optionsText += `<div class="form-group"><label>Consume Spell Slot:</label>
<input type="checkbox" name="consumeSpellSlot" checked="${consumeSpellSlot}" /></div>`;
}
optionsText += `</div></form>`;
// Create a dialogue box to select spell slot level to use when smiting.
new Dialog({
title: "Divine Smite Damage",
content: optionsText,
buttons: {
one: {
icon: '<i class="fas fa-check"></i>',
label: "SMITE!",
callback: () => (confirmed = true),
},
two: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel",
callback: () => (confirmed = false),
},
},
default: "Cancel",
close: (html) => {
if (confirmed) {
let slotLevel = parseInt(html.find("[name=slot-level]")[0].value);
let userConsumeSpellSlot = html.find("[name=consumeSpellSlot]")[0]
?.checked;
let criticalHit = html.find("[name=criticalCheckbox]")[0].checked;
smite(
s_actor,
slotLevel,
criticalHit,
consumeSpellSlot || userConsumeSpellSlot
);
}
},
}).render(true);

// Create a dialogue box to select spell slot level to use when smiting.
new Dialog({
title: "Divine Smite Damage",
content: `
<form>
<p>Spell Slot level to use Divine Smite with.</p>
<div class="form-group">
<label>Spell Slot Level:</label>
<select name="slot-level">` + optionsText + `</select>
</div>
<div class="form-group">
<label>Critical Hit:</label>
<input type="checkbox" name="criticalCheckbox">
</div>
</form>
`,
buttons: {
one: {
icon: '<i class="fas fa-check"></i>',
label: "SMITE!",
callback: () => confirmed = true
},
two: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel",
callback: () => confirmed = false
}
},
default: "Cancel",
close: html => {
if (confirmed) {
let slotLevel = parseInt(html.find('[name=slot-level]')[0].value);
let criticalHit = html.find('[name=criticalCheckbox]')[0].checked;
smite(s_actor, slotLevel, criticalHit);
}
}
}).render(true);

} else {
return ui.notifications.error(
`No spell slots available to use this feature.`
);
return ui.notifications.error(`No spell slots available to use this feature.`);
}

/**
* Gives the spell slot information for a particular actor and spell slot level.
* @param {Actor5e} actor - the actor to get slot information from.
* @param {integer} level - the spell slot level to get information about. level 0 is deprecated.
* @returns {object} contains value (number of slots remaining), max, and override.
*/
function getSpellSlots(actor, level) {
return actor.data.data.spells[`spell${level}`];
return actor.data.data.spells[`spell${level}`];
}

/**
* Gives the spell slots available for
* Returns whether the actor has any spell slot left.
* @param {Actor5e} actor - the actor to get slot information from.
* @returns {boolean} True if any spell slots of any spell level are available to be used.
*/
function getAvailableSlots(actor) {
for (let slot in actor.data.data.spells) {
if (actor.data.data.spells[slot].value > 0) {
return true;
}
}
return false;
}
function hasAvailableSlot(actor) {
for (let slot in actor.data.data.spells) {
if (actor.data.data.spells[slot].value > 0) {
return true;
}
}
return false;
}

/**
* Use the controlled token to smite the targeted token.
* @param {Actor5e} actor - the actor that is performing the action.
* @param {integer} slotLevel - the spell slot level to use when smiting.
* @param {boolean} criticalHit - whether the hit is a critical hit.
* @param {boolean} consumeSpellSlot - whether to consume the selected spell slot.
*/
function smite(actor, slotLevel, criticalHit, consumeSpellSlot) {
let targets = game.user.targets;
let chosenSpellSlots = getSpellSlots(actor, slotLevel);
function smite(actor, slotLevel, criticalHit) {
let targets = game.user.targets;
let chosenSpellSlots = getSpellSlots(actor, slotLevel);

if (chosenSpellSlots.value < 1 && consumeSpellSlot) {
ui.notifications.error("No spell slots of the required level available.");
return;
}
if (targets.size !== 1) {
ui.notifications.error("You must target exactly one token to Smite.");
return;
}
if (chosenSpellSlots.value < 1) {
ui.notifications.error("No spell slots of the required level available.");
return;
}
if (targets.size !== 1) {
ui.notifications.error("You must target exactly one token to Smite.");
return;
}

targets.forEach(target => {
let numDice = slotLevel + 1;
let type = target.actor.data.data.details.type?.toLocaleLowerCase();
if (affectedCreatureTypes.includes(type)) numDice += 1;
if (criticalHit) numDice *= 2;
new Roll(`${numDice}d8`).roll().toMessage({ flavor: "Macro Divine Smite - Damage Roll (Radiant)", speaker })
})

targets.forEach((target) => {
let numDice = slotLevel + 1;
if (criticalHit) numDice *= 2;
let type = target.actor.data.data.details.type.toLocaleLowerCase();
if (affectedCreatureTypes.includes(type)) numDice += 1;
new Roll(`${numDice}${dieSize}`).roll().toMessage({
flavor: macroFlavor,
speaker,
});
});
if (consumeSpellSlot) {
let objUpdate = new Object();
objUpdate["data.spells.spell" + slotLevel + ".value"] =
chosenSpellSlots.value - 1;
objUpdate['data.spells.spell' + slotLevel + '.value'] = chosenSpellSlots.value - 1;
actor.update(objUpdate);
}
}
//#endregion
}

0 comments on commit e42bddc

Please sign in to comment.