Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
AnthonyVadala committed Jul 20, 2020
2 parents 1fa6d30 + e42bddc commit 057c231
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 86 deletions.
77 changes: 77 additions & 0 deletions actor/5e_lay_on_hands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* System: D&D5e
* Apply lay-on-hands feat to a target character. Asks the player how many HP to heal and
* verifies the entered value is within range before marking down usage counter. If the player
* has OWNER permissions of target (such as GM or self-heal) the HP are applied automatically;
* otherwise, a 'roll' message appears allowing the target character to right-click to apply healing.
*/

let confirmed = false;
let actorData = actor || canvas.tokens.controlled[0] || game.user.character;
let featData = actorData ? actorData.items.find(i => i.name==="Lay on Hands") : null;

if(actorData == null || featData == null)
ui.notifications.warn(`Selected hero must have "Lay on Hands" feat.`);
else if (game.user.targets.size !== 1)
ui.notifications.warn(`Please target one token.`);
else
{
let featUpdate = duplicate(featData);
let targetActor = game.user.targets.values().next().value.actor;
let maxHeal = Math.clamped(featUpdate.data.uses.value, 0,
targetActor.data.data.attributes.hp.max - targetActor.data.data.attributes.hp.value);

let content = `<p><em>${actorData.name} lays hands on ${targetActor.data.name}.</em></p>
<p>How many HP do you want to restore to ${targetActor.data.name}?</p>
<form>
<div class="form-group">
<label for="num">HP to Restore: (Max = ${maxHeal})</label>
<input id="num" name="num" type="number" min="0" max="${maxHeal}"></input>
</div>
<div class="form-group">
<label for="flavor">Flavor:</label>
<input id="flavor" name="flavor" value="${featUpdate.data.chatFlavor}"></input>
</div>
</form>`;
new Dialog({
title: "Lay on Hands Healing",
content: content,
buttons: {
heal: { label: "Heal!", callback: () => confirmed = true },
cancel: { label: "Cancel", callback: () => confirmed = false }
},
default: "heal",

close: html => {
if (confirmed)
{
let number = Math.floor(Number(html.find('#num')[0].value));
if (number < 1 || number > maxHeal)
ui.notifications.warn(`Invalid number of charges entered = ${number}. Aborting action.`);
else
{
let flavor = `<strong>${html.find('#flavor')[0].value}</strong><br>`;
if (targetActor.permission !== CONST.ENTITY_PERMISSIONS.OWNER)
// We need help applying the healing, so make a roll message for right-click convenience.
new Roll(`${number}`).roll().toMessage({
speaker: ChatMessage.getSpeaker(),
flavor: `${actorData.name} lays hands on ${targetActor.data.name}.<br>${flavor}
<p><em>Manually apply ${number} HP of healing to ${targetActor.data.name}</em></p>` });
else {
// We can apply healing automatically, so just show a normal chat message.
ChatMessage.create({
speaker: ChatMessage.getSpeaker(),
content: `${actorData.name} lays hands on ${targetActor.data.name} for ${number} HP.<br>${flavor}`
});
game.actors.find(a => a._id===targetActor._id).update( {
"data.attributes.hp.value" : targetActor.data.data.attributes.hp.value + number
});
}

featUpdate.data.uses.value = featUpdate.data.uses.value - number;
actorData.updateEmbeddedEntity("OwnedItem", featUpdate);
};
}
}
}).render(true);
}
149 changes: 91 additions & 58 deletions actor/divine_smite.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,78 @@
* 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.

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

// 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.`);
}

let confirmed = false;
if (hasAvailableSlot(s_actor)) {

// Create a dialogue box to select spell slot level to use when smiting.
new Dialog({
title: "Divine Smite Damage",
content: `
<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">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</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);
smite(slotLevel);
// 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>`;
}
}
}).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.`);
}

/**
* Gives the spell slot information for a particular actor and spell slot level.
Expand All @@ -55,30 +83,32 @@ new Dialog({
* @returns {object} contains value (number of slots remaining), max, and override.
*/
function getSpellSlots(actor, level) {
let spells = actor.data.data.spells;
switch (level) {
case 1:
return spells.spell1;
case 2:
return spells.spell2;
case 3:
return spells.spell3;
case 4:
return spells.spell4;
case 5:
return spells.spell5;
}
return actor.data.data.spells[`spell${level}`];
}

/**
* 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 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.
*/
function smite(slotLevel) {
function smite(actor, slotLevel, criticalHit) {
let targets = game.user.targets;
let suseptible = ["fiend", "undead"];
let controlledActor = canvas.tokens.controlled[0].actor;
let chosenSpellSlots = getSpellSlots(controlledActor, slotLevel);
let chosenSpellSlots = getSpellSlots(actor, slotLevel);

if (chosenSpellSlots.value < 1) {
ui.notifications.error("No spell slots of the required level available.");
Expand All @@ -91,10 +121,13 @@ function smite(slotLevel) {

targets.forEach(target => {
let numDice = slotLevel + 1;
let type = target.actor.data.data.details.type.toLocaleLowerCase();
if (suseptible.includes(type)) numDice += 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 })
})

chosenSpellSlots.value -= 1;
let objUpdate = new Object();
objUpdate['data.spells.spell' + slotLevel + '.value'] = chosenSpellSlots.value - 1;
actor.update(objUpdate);
}
68 changes: 43 additions & 25 deletions misc/import_from_compendium.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
/**
* Import all the actors from a compendium.
* In order to find the packName, you can use the following in your console (F12): game.packs.map(p => p.collection);
* Author: KrishMero#1702
* Import all the entries from a compendium into the desired folder.
* @Author: KrishMero#1702
*/

let packOptions = game.packs.map(pack => `<option value="${pack.collection}">${pack.title}</option>`);
const form = `
<div style="display: inline-block; width: 100px">Folder:</div>
<input type="string" id="folderName">
<br />
<div style="font-size: 80%">leave blank to put into root directory</div>
<div style="font-size: 80%">leave blank to create a folder after the compendium name</div>
<br />
<div style="display: inline-block; width: 100px">Compendium:</div>
Expand All @@ -35,38 +34,57 @@ const dialog = new Dialog({
}
}).render(true);

function importCompendium(html) {
async function importCompendium(html) {
const folderName = html.find(`input#folderName`)[0].value;
const packName = html.find(`select#destinationPack`)[0].value;
const remove = html.find(`input#delete`)[0].checked;

let pack = game.packs.get(packName);
let folder = game.folders.find(f => f.name === folderName && f.type === pack.entity)?.id;
let type = getEntityType(pack);
let extra = folder ? { folder } : null
const pack = game.packs.get(packName);
const entity = pack.entity;
let folder = folderName ? findFolder(folderName, entity) : await createFolder(pack, entity);

if (!folder) return ui.notifications.error(`Your world does not have any ${entity} folders named '${folderName}'.`);

if (folderName && !folder) {
return ui.notifications.error(`Your world does not have any ${type} folders named '${folderName}'.`);
if (remove) removeDataFirst(folder.id, entity);
if (folder) importPack(pack, entity, folder.id)
}

async function importPack(pack, entity, folderId) {
const entityClass = CONFIG[entity].entityClass;
const content = await pack.getContent();

const createData = content.map(c => {
c.data.folder = folderId;
return c.data;
});
entityClass.create(createData);
}

function removeDataFirst(folderId, entity) {
let type = getEntityType(entity);
const removeableData = game[type].filter(t => t.data.folder === folderId);
if (typeof removeableData.delete !== "undefined") {
removeableData.delete();
} else {
removeableData.map(d => d.delete());
}
}

if (remove) removeDataFirst(type, folder);
pack.getIndex().then(index => index.forEach(entry => game[type].importFromCollection(packName, entry._id, extra)));
async function createFolder(pack, type) {
let name = pack.metadata.label;
let folder = await Folder.create({ name, type, parent: null});
return folder;
}

function getEntityType(pack) {
const entity = pack.metadata.entity;

function findFolder(folderName, entity)
{
return game.folders.find(f => f.name === folderName && f.type === entity)
}

function getEntityType(entity) {
switch (entity) {
case 'JournalEntry': return 'journal';
case 'RollTable': return 'tables';
default: return entity.toLowerCase() + 's';
}
}

function removeDataFirst(type, folder) {
let removableData = game[type].filter(t => t.data.folder === folder);
if (typeof removableData.delete !== "undefined") {
removableData.delete();
} else {
removableData.map(d => d.delete());
}
}
9 changes: 6 additions & 3 deletions roll/mass_roll_check.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
*/

let targetActors = getTargetActors().filter(a => a != null);
if (!targetActors.length > 0)
return;
function checkForActors(){
if (!targetActors.length > 0)
throw new Error('You must designate at least one token as the roll target');
};
checkForActors();

// Choose roll type dialog
let rollTypeTemplate = `
Expand Down Expand Up @@ -166,4 +169,4 @@ function getCheckDialogTitle(checkType) {
}
}

chooseCheckType.render(true);
chooseCheckType.render(true);
Loading

0 comments on commit 057c231

Please sign in to comment.