I made a bunch of macros to make Block, Dodge, Parry smoother to run in Foundry VTT v11.

Note: I am not a very good programmer. I had ChatGPT write these for me, and tweaked them as needed.

I am using the Macro Wheel extension by TheRipper93 for quick-and-smooth macro selection, and Token Magic for cool token effects.

Paste these scripts into a new macro, and set them to ‘script’.

Quick Damage Rolls

These macros quickly roll damage for attacks. They do not take into account special weapon properties, but the non-d4 versions do ask if the attack should be Enhanced (also roll a d12).

d4

// Check if exactly one token is selected
if (canvas.tokens.controlled.length !== 1) {
  ui.notifications.warn("Please select exactly one token.");
  return;
}

// Get the selected token and its actor name
const token = canvas.tokens.controlled[0];
const actorName = token.actor.name;

// Roll a d4, including 3D dice animation
let roll = new Roll("1d4");
roll.roll({async: true}).then(result => {
  result.toMessage({
    flavor: `${actorName} rolls for damage!`,
    speaker: ChatMessage.getSpeaker({token: token})
  });
});

d6

// Check if exactly one token is selected
if (canvas.tokens.controlled.length !== 1) {
  ui.notifications.warn("Please select exactly one token.");
  return;
}

// Get the selected token and its actor name
const token = canvas.tokens.controlled[0];
const actorName = token.actor.name;

// Prompt the user for Enhanced Attack
let d = new Dialog({
  title: "Enhanced Attack?",
  content: "<p>Use an Enhanced attack?</p>",
  buttons: {
    yes: {
      icon: "<i class='fas fa-check'></i>",
      label: "Yes",
      callback: () => rollDice(true)
    },
    no: {
      icon: "<i class='fas fa-times'></i>",
      label: "No",
      callback: () => rollDice(false)
    }
  },
  default: "no",
  render: html => console.log("Register interactivity in the rendered dialog"),
  close: html => console.log("This always is logged no matter which option is chosen")
});
d.render(true);

async function rollDice(enhanced) {
  if (enhanced) {
    // Enhanced attack, roll a d6 and a d12
    let rollD6 = new Roll("1d6");
    let rollD12 = new Roll("1d12");

    // Roll and display the 3D dice if settings enable it
    await rollD6.roll({async: true});
    await rollD12.roll({async: true});
    
    // Display both rolls in chat without calculating the highest
    rollD6.toMessage({
      flavor: `${actorName} makes an Enhanced attack with a d6!`,
      speaker: ChatMessage.getSpeaker({token: token})
    });

    rollD12.toMessage({
      flavor: `${actorName} makes an Enhanced attack with a d12!`,
      speaker: ChatMessage.getSpeaker({token: token})
    });
  } else {
    // Regular attack with just 1d6, including 3D dice animation
    let roll = new Roll("1d6");
    await roll.roll({async: true});
    roll.toMessage({
      flavor: `${actorName} rolls for damage!`,
      speaker: ChatMessage.getSpeaker({token: token})
    });
  }
}

d8

// Check if exactly one token is selected
if (canvas.tokens.controlled.length !== 1) {
  ui.notifications.warn("Please select exactly one token.");
  return;
}

// Get the selected token and its actor name
const token = canvas.tokens.controlled[0];
const actorName = token.actor.name;

// Prompt the user for Enhanced Attack
let d = new Dialog({
  title: "Enhanced Attack?",
  content: "<p>Use an Enhanced attack?</p>",
  buttons: {
    yes: {
      icon: "<i class='fas fa-check'></i>",
      label: "Yes",
      callback: () => rollDice(true)
    },
    no: {
      icon: "<i class='fas fa-times'></i>",
      label: "No",
      callback: () => rollDice(false)
    }
  },
  default: "no",
  render: html => console.log("Register interactivity in the rendered dialog"),
  close: html => console.log("This always is logged no matter which option is chosen")
});
d.render(true);

async function rollDice(enhanced) {
  if (enhanced) {
    // Enhanced attack, roll a d8 and a d12
    let rollD8 = new Roll("1d8");
    let rollD12 = new Roll("1d12");

    // Roll and display the 3D dice if settings enable it
    await rollD8.roll({async: true});
    await rollD12.roll({async: true});
    
    // Display both rolls in chat without calculating the highest
    rollD8.toMessage({
      flavor: `${actorName} makes an Enhanced attack with a d8!`,
      speaker: ChatMessage.getSpeaker({token: token})
    });

    rollD12.toMessage({
      flavor: `${actorName} makes an Enhanced attack with a d12!`,
      speaker: ChatMessage.getSpeaker({token: token})
    });
  } else {
    // Regular attack with just 1d8, including 3D dice animation
    let roll = new Roll("1d8");
    await roll.roll({async: true});
    roll.toMessage({
      flavor: `${actorName} rolls for damage!`,
      speaker: ChatMessage.getSpeaker({token: token})
    });
  }
}

d10

// Check if exactly one token is selected
if (canvas.tokens.controlled.length !== 1) {
  ui.notifications.warn("Please select exactly one token.");
  return;
}

// Get the selected token and its actor name
const token = canvas.tokens.controlled[0];
const actorName = token.actor.name;

// Prompt the user for Enhanced Attack
let d = new Dialog({
  title: "Enhanced Attack?",
  content: "<p>Use an Enhanced attack?</p>",
  buttons: {
    yes: {
      icon: "<i class='fas fa-check'></i>",
      label: "Yes",
      callback: () => rollDice(true)
    },
    no: {
      icon: "<i class='fas fa-times'></i>",
      label: "No",
      callback: () => rollDice(false)
    }
  },
  default: "no",
  render: html => console.log("Register interactivity in the rendered dialog"),
  close: html => console.log("This always is logged no matter which option is chosen")
});
d.render(true);

async function rollDice(enhanced) {
  if (enhanced) {
    // Enhanced attack, roll a d10 and a d12
    let rollD10 = new Roll("1d10");
    let rollD12 = new Roll("1d12");

    // Roll and display the 3D dice if settings enable it
    await rollD10.roll({async: true});
    await rollD12.roll({async: true});
    
    // Display both rolls in chat without calculating the highest
    rollD10.toMessage({
      flavor: `${actorName} makes an Enhanced attack with a d10!`,
      speaker: ChatMessage.getSpeaker({token: token})
    });

    rollD12.toMessage({
      flavor: `${actorName} makes an Enhanced attack with a d12!`,
      speaker: ChatMessage.getSpeaker({token: token})
    });
  } else {
    // Regular attack with just 1d10, including 3D dice animation
    let roll = new Roll("1d10");
    await roll.roll({async: true});
    roll.toMessage({
      flavor: `${actorName} rolls for damage!`,
      speaker: ChatMessage.getSpeaker({token: token})
    });
  }
}

Save Macros

Universal Save

This macro prompts which property to save, thus being quicker than opening the character sheet. Select the token, select the type of Save, and it’s rolled and displayed.

// Check if an actor is selected
if (!actor) {
    ui.notifications.warn("Please select an actor.");
} else {
    let d = new Dialog({
        title: "Save Roll",
        content: `
            <p>What kind of Save?</p>
            <div style="display: flex; justify-content: space-around; margin: 10px 0;">
                <button class="save-btn" data-save="STR">STR</button>
                <button class="save-btn" data-save="DEX">DEX</button>
                <button class="save-btn" data-save="WIL">WIL</button>
            </div>
        `,
        buttons: {},
        render: html => {
            html.find(".save-btn").click(async event => {
                let saveType = event.currentTarget.dataset.save;
                await performSave(saveType, d); // Pass the dialog instance to close it later
            });
        }
    }).render(true);
}

async function performSave(saveType, dialog) {
    // Close the dialog
    dialog.close();

    // Retrieve the actor's save value
    let saveValue = actor.data.data.abilities[saveType].value;

    // Roll a d20 and ensure physical dice are rolled if module like Dice So Nice! is used
    let roll = new Roll("1d20");
    await roll.evaluate({async: true}); // Evaluate the roll asynchronously to allow for physical dice roll animations
    roll.toMessage({
        speaker: ChatMessage.getSpeaker({actor: actor}),
        flavor: `${actor.name} attempts a ${saveType} Save...`
    }, {rollMode: "roll"}); // Ensure the roll is displayed

    // Wait for dice animation if using a module like Dice So Nice!
    await new Promise(resolve => setTimeout(resolve, 2000)); // Adjust the timeout according to your needs

// Determine success or failure
let success = roll.total <= saveValue;
let resultMessage = `${actor.name} made a ${saveType} Save, rolling a <strong>${roll.total}</strong> against a target of <strong>${saveValue}</strong>, and <b>${success ? "succeeded" : "failed"}</b>!`;

// Display the result
ChatMessage.create({
    speaker: ChatMessage.getSpeaker({actor: actor}),
    content: resultMessage
});

}

STR Save

I use this for NPCs; it doesn’t display the roll, and splatters the token. Quick and easy.

// Check if a token is selected
if (canvas.tokens.controlled.length === 1) {
    let selectedToken = canvas.tokens.controlled[0];
    let actor = selectedToken.actor;
    let actorName = actor.name; // Get the actor's name for the message
    let strValue = actor.data.data.abilities.STR.value; // Get the actor's Strength value

    // Roll 1d20 and wait for the roll to complete, including any 3D dice animations
    (async () => {
        let roll = new Roll("1d20");
        await roll.evaluate({async: true}); // Evaluate the roll asynchronously

        // Wait for dice animation to complete if using a module like Dice So Nice!
        await roll.toMessage({
            speaker: ChatMessage.getSpeaker({actor: actor}),
            flavor: `${actorName} makes a Strength check!`
        });

        // Determine success or failure after the roll message to include the STR target
        let success = roll.total <= strValue;
        let messageContent = success ?
            `${actorName} rolls a <strong>${roll.total}</strong> against a STR target of <strong>${strValue}</strong> and <b>succeeds</b>!` :
            `${actorName} rolls a <strong>${roll.total}</strong> against a STR target of <strong>${strValue}</strong> and <b>fails</b>!`;

        // Display the result as a chat message
        await ChatMessage.create({
            speaker: ChatMessage.getSpeaker({actor: actor}),
            content: messageContent
        });

        if (!success) {
            // Apply the fail consequences only after displaying the failure message
            await actor.update({"data.abilities.STR.value": 0});

            // Apply token effect for failure
            let params = [{
                filterType: "splash",
                filterId: "mySplash",
                color: 0x900505,
                padding: 30,
                time: Math.random()*1000,
                seed: Math.random()/100,
                splashFactor: 2,
                spread: 7,
                blend: 1,
                dimX: 1,
                dimY: 1,
                cut: true,
                textureAlphaBlend: false
            }];

            await TokenMagic.addUpdateFiltersOnSelected(params);
        }
    })();
} else {
    ui.notifications.warn("Please select exactly one token.");
}

Damage

This macro asks for input of amount of damage, and then accounts for Armor, HP and STR.

// Check if a token is selected
if (canvas.tokens.controlled.length === 1) {
    let selectedToken = canvas.tokens.controlled[0];
    let actorName = selectedToken.actor.name; // Get the actor's name for the message

    // Step 1: Request numerical input
    new Dialog({
      title: "Apply Damage",
      content: "<p>Enter the amount of damage:</p><input id='damageInput' type='number' style='width: 100%;'/>",
      buttons: {
        apply: {
          icon: '<i class="fas fa-check"></i>',
          label: "Apply",
          callback: (html) => {
            let damage = parseInt(html.find('#damageInput').val());
            if (isNaN(damage)) {
              ui.notifications.error("Please enter a valid number.");
              return;
            }

            let armorValue = selectedToken.actor.data.data.armor ?? 0;
            // Ensure damage after armor cannot be negative
            let damageAfterArmor = Math.max(0, damage - armorValue);

            let messageContent = `${actorName} took ${damage} damage!`;
            if (armorValue > 0 && damageAfterArmor === 0) {
              messageContent += `<br>🛡️ All damage was stopped by armor!`;
            } else if (armorValue > 0) {
              messageContent += `<br>🛡️ ${armorValue} damage was stopped by armor!`;
            }

            let currentHp = selectedToken.actor.data.data.hp.value;
            let newHp = Math.max(0, currentHp - damageAfterArmor);
            let overflow = 0;

            // If damageAfterArmor is positive, calculate overflow
            if (damageAfterArmor > 0) {
              let damageToHp = currentHp - newHp;
              messageContent += `<br>🫀 ${damageToHp} damage was dealt to HP!`;
              overflow = damageAfterArmor - damageToHp;
            }

            // Update HP
            selectedToken.actor.update({"data.hp.value": newHp});

            // Check for overflow and update Strength if necessary
            if (overflow > 0) {
              let currentStr = selectedToken.actor.data.data.abilities.STR.value;
              let newStr = Math.max(0, currentStr - overflow);
              selectedToken.actor.update({"data.abilities.STR.value": newStr});

              messageContent += `<br>💪 ${overflow} damage done to STR!`;
            }

            // Post the message to chat
            ChatMessage.create({
              speaker: ChatMessage.getSpeaker({actor: selectedToken.actor}),
              content: messageContent
            });
          }
        }
      },
      default: "apply"
    }).render(true);
} else {
    ui.notifications.warn("Please select exactly one token.");
}

Magic

Arcane Magic

This macro allows for the selection of number of Magic Dice, and outputs all the relevant information to calculate actual spell damage, depending on the configuration.

// Check if there's an actor selected
if (!actor) {
    ui.notifications.warn("Please select an actor.");
    return;
}

// Prompt the user for the number of Magic Dice
let d = new Dialog({
 title: "Magic Dice Roll",
 content: "<p>How many Magic Dice? 1/2/3/4</p>",
 buttons: {
  one: {
   icon: '<i class="fas fa-dice-one"></i>',
   label: "1",
   callback: () => rollDice(1)
  },
  two: {
   icon: '<i class="fas fa-dice-two"></i>',
   label: "2",
   callback: () => rollDice(2)
  },
  three: {
   icon: '<i class="fas fa-dice-three"></i>',
   label: "3",
   callback: () => rollDice(3)
  },
  four: {
   icon: '<i class="fas fa-dice-four"></i>',
   label: "4",
   callback: () => rollDice(4)
  }
 },
 default: "one",
 close: () => {}
}).render(true);

// Function to roll dice and display results
function rollDice(numberOfDice) {
    // Roll the dice
    let roll = new Roll(`${numberOfDice}d6`);
    roll.evaluate({async: false}); // Make sure the roll is evaluated synchronously

    // Extracting individual dice results
    let diceResults = roll.terms[0].results;
    let resultsText = diceResults.map(r => r.result).join(", ");
    let sum = roll.total;
    let fatigue = diceResults.filter(r => r.result >= 4).length;
    let feedback = 0;
    let counts = {};

    // Calculate Arcane Feedback
    diceResults.forEach(d => {
        counts[d.result] = (counts[d.result] || 0) + 1;
    });
    for (let num in counts) {
        if (counts[num] > 1) {
            feedback += num * counts[num];
        }
    }

    // Create the chat message
    let messageContent = `
        🧙🏻‍♂️ ${actor.name} casts magic!<br>
        🎲 Number of dice: ${numberOfDice}<br>
        🎲 Results: ${resultsText}<br>
        ⚡ Total sum: ${sum}<br>
        🏋🏻‍♂️ Fatigue: ${fatigue}<br>
        💫 Arcane Feedback: ${feedback}
    `;

    ChatMessage.create({
        speaker: ChatMessage.getSpeaker({actor: actor}),
        content: messageContent
    });
}

Arcane Feedback

A macro that first damages HP and then WIL, ignoring armor.

// Check if a token is selected
if (canvas.tokens.controlled.length === 1) {
    let selectedToken = canvas.tokens.controlled[0];
    let actorName = selectedToken.actor.name; // Get the actor's name for the message

    // Step 1: Request numerical input
    new Dialog({
      title: "Apply Damage",
      content: "<p>Enter the amount of damage:</p><input id='damageInput' type='number' style='width: 100%;'/>",
      buttons: {
        apply: {
          icon: '<i class="fas fa-check"></i>',
          label: "Apply",
          callback: (html) => {
            let damage = parseInt(html.find('#damageInput').val());
            if (isNaN(damage)) {
              ui.notifications.error("Please enter a valid number.");
              return;
            }

            let messageContent = `${actorName} took ${damage} damage!`;

            let currentHp = selectedToken.actor.data.data.hp.value;
            let newHp = Math.max(0, currentHp - damage);
            let overflow = damage > currentHp ? damage - currentHp : 0;

            // Update HP
            selectedToken.actor.update({"data.hp.value": newHp});

            if (overflow > 0) {
              let currentWil = selectedToken.actor.data.data.abilities.WIL.value;
              let newWil = Math.max(0, currentWil - overflow);
              selectedToken.actor.update({"data.abilities.WIL.value": newWil});

              messageContent += `<br>${overflow} damage done to WIL!`;
            } else {
              messageContent += `<br>${damage} damage was dealt to HP!`;
            }

            // Post the message to chat
            ChatMessage.create({
              speaker: ChatMessage.getSpeaker({actor: selectedToken.actor}),
              content: messageContent
            });
          }
        }
      },
      default: "apply"
    }).render(true);
} else {
    ui.notifications.warn("Please select exactly one token.");
}

Divine Magic

Similar to Arcane Magic, but outputs all relevant information for Miracles.

// Check if there's a token selected
const token = canvas.tokens.controlled[0];
if (!token) {
    ui.notifications.warn("Please select a token.");
    return;
}

// Prompt the user for the number of Faith Dice
let d = new Dialog({
 title: "Faith Dice Roll",
 content: "<p>How many Faith Dice? 1/2/3/4</p>",
 buttons: {
  one: {
   icon: '<i class="fas fa-dice-one"></i>',
   label: "1",
   callback: () => rollFaithDice(1)
  },
  two: {
   icon: '<i class="fas fa-dice-two"></i>',
   label: "2",
   callback: () => rollFaithDice(2)
  },
  three: {
   icon: '<i class="fas fa-dice-three"></i>',
   label: "3",
   callback: () => rollFaithDice(3)
  },
  four: {
   icon: '<i class="fas fa-dice-four"></i>',
   label: "4",
   callback: () => rollFaithDice(4)
  }
 },
 default: "one",
 close: () => {}
}).render(true);

// Function to roll Faith Dice and display results
function rollFaithDice(numberOfDice) {
    // Roll the dice
    let roll = new Roll(`${numberOfDice}d6`);
    roll.evaluate({async: false}); // Make sure the roll is evaluated synchronously

    // Extracting individual dice results for display and calculating WIL Damage
    let diceResults = roll.terms[0].results;
    let resultsText = diceResults.map(r => r.result).join(", ");
    let sum = roll.total;
    let wilDamage = diceResults.reduce((acc, r) => {
        return acc + (r.result <= 3 ? 1 : 2);
    }, 0);

    // Create the chat message
    let messageContent = `
        🙌🏻 ${token.actor.name} casts a miracle!<br>
        🎲 Number of dice: ${numberOfDice}<br>
        🎲 Results: ${resultsText}<br>
        ⚡ Total sum: ${sum}<br>
        🧠 WIL Damage: ${wilDamage}
    `;

    ChatMessage.create({
        speaker: ChatMessage.getSpeaker({actor: token.actor}),
        content: messageContent
    });
}

Leave a Reply

Trending

Discover more from Dice Goblin

Subscribe now to keep reading and get access to the full archive.

Continue reading