Foundry Mob Attack Macros for D&D 5e

June 6, 2021 Update:

  • The macros broke in Foundry 8.x due to a change in the data structures for the 5e system. It’s fixed now, but these will likely no longer work in older Foundry versions.
  • I realized while using the AOE damage calculation during a game that in cases where an attack did more damage than the HP of the individuals in the group, the total damage across the group could end up grossly over-inflated. Added a check to make sure the per-person damage was capped at the individual HP level; if a member of the group is already injured, it will be randomly assigned to either the save or fail group for purposes of determining overall group damage.

The code below has been updated to include these fixes.

Some crafting is still (slowly) going on and I’m doing a fair amount of gaming. The remainder of my available hobby time of late has gone into prepping adventures for my homebrew-setting virtual D&D campaign (or grafting existing published adventures into it).

But the programmer in me has been wanting to start tinkering with the Foundry API to do interesting custom things. An upcoming scene in my campaign has, fortunately, given me the perfect excuse to do just that.

The PCs are fleeing ahead of an invading orc army. They’re making for a passage along the mountains to the north of them in hopes of escaping to safety through it. What they don’t know is that the orcs are searching for the same passage as a shortcut to towns and cities on the other side of that mountain range. So the orcs will send a detachment of troops in behind them!

At the other end of the passage is a defensive wall – old and run down but under repair and manned by a small garrison. On emerging from the passage and seeing the lightly manned defenses, the orc leader – looking to make a name for himself – will order an assault on the wall.

Nice army ya got there. Be a shame if somethin’ was ta happen to it.

I want to convey to the players that they’re involved in a large scale battle, but rolling attacks for 250 soldiers plus PCs and named NPCs would be ridiculous. There are a number of mass-combat systems designed for 5e with varying levels of realism and complexity, but for simplicity’s sake I decided to go with the “mob attack” rules on page 250 of the Dungeon Master’s Guide. Rather than using any dice rolls it relies on calculating the expected number of hits a group of similar creatures will and against the target armor class.

Sly Flourish has a calculator based on this idea, which can also be used to generate the number of successful saving throws among a group based on the members’ bonuses and the save DC, and I used this calculation as the basis for my macros.

Yes, there’s already a Mob Attack Tool module that does something like this, with some features for managing initiative and probably a bit less initial set-up effort. But it’s based around selecting groups of normal actors and running the numbers on those.

In contrast, I didn’t want to have to manage huge numbers of individual tokens/actors, their initiatives, etc. I wanted to treat a single token as a common group of creatures with a single large hit point pool, calculating the number of remaining individuals as a fraction of the total HP and basing the unit’s attack power on that. In addition, I wanted to be able to calculate not only the number of successful saves in the event of an area attack against a group, but the total damage to that group based on the attack’s damage roll and the number of successful saves. On top of all that, I wanted to account for possible cover, advantage/disadvantage (equating mathematically to +5 or -5 to hit), resistances, etc.

First, the code.

/*
Calculate average number of hits and total damage of a "mob" NPC attacking 
a target.
Based on the calculations from here: https://slyflourish.com/mob_calculator.html
... which are in turn based on the Mob Combat principles on page 250 of the DMG.
Creating a Mob actor:
 - Create a duplicate of an existing creature's actor and open its character sheet 
    *OR* create a new NPC and enter the relevant stats for a single version of the
    creature you're grouping up
 - Give it a name that lets you know it's the "mob version", like "Orc squad", "dwarf troops",
    or "A Gaggle of Gnomes"
 - Give it a feature called "Individual HP"; set its Activation Cost to the the number
    of hit points an individual creature of that type would have
 - Determine how many individuals will make up this group.  Multiply that the individual HP
    and set min and max HP for the "character" to that number to represent total group HP.
	As group HP drops, the macro will calculare the # of individuals still able to make attacks
 - For any weapons this group will use to attack as a group:
    - Change the name to end in "Mob Attack"
    - Set the attack roll bonus t the total bonus - the macro won't calculate it from stats
    - Optional: Set the damage # to the average damage expected for that attack
*/
let chatMsg = '';
let indHPObj = '';
let isMob = false, hasMobAttacks = false;
let numCreatures = 1, maxCreatures=1, indHP = 0;
let curActor = null;
var weapons = null;
const controlled = canvas.tokens.controlled;
if (controlled.length > 0 && controlled[0] !== null) {
	curActor = controlled[0].actor;
	if (curActor !== undefined && curActor!== null) {
		indHPObj = curActor.items.find(i => i.name === 'Individual HP');
		if (indHPObj !== undefined && indHPObj !== null) {
			isMob = true;
			weapons = curActor.data.items.filter(i => i.type == "weapon" && i.name.toUpperCase().includes('MOB ATTACK') );
			if (weapons.length > 0)
				hasMobAttacks = true;
		}
	}
}
if (isMob && hasMobAttacks) {
	indHP = indHPObj.data.data.activation.cost;
	maxCreatures = Math.floor(curActor.data.data.attributes.hp.max / indHP);
	numCreatures = Math.ceil(curActor.data.data.attributes.hp.value / indHP);
	
	let weapons = curActor.data.items.filter(i => i.type == "weapon" && i.name.toUpperCase().includes('MOB ATTACK') );
	let attackTypeValue = 0; // advantage = -5 AC, disadvantage = +5 AC
	let coverValue = 0;  // half = +2 AC, +2 dex save; three quarters = +5 AC, dex saves
	let targetAC = 10; // default if no target selected
	let targetName = "target";
	
	let mobTarget = null;
	
	if (game.user.targets.size > 0) {
		mobTarget = Array.from(game.user.targets)[0];
		targetName = mobTarget.name;
		targetAC = mobTarget.actor.data.data.attributes.ac.value;
	}
	
	let targetDlgText = ((targetName == 'target') ? 'None selected' : targetName);
	
	let weaponsListItems = '';
	
	weapons.forEach((wpn, index) => {
		weaponsListItems += `<option value="${index}" ${(index == 0) ? "selected" : ""}>${wpn.name}`;
	});
	
	
	let dialogHTML = `
	<form>
		<div class="form-group">
			<label>Weapon:</label>
			<select name="weaponSelect">
			${weaponsListItems}
			</select>
		</div>
		<div class="form-group">
			<label>Attack Type:</label>
			<select name="attackTypeSelect">
				<option value="-5">Advantage</option>
				<option value="0" selected>Normal</option>
				<option value="5">Disadvantage</option>
			</select>
		</div>
		<div class="form-group">
			<label>Target:</label>
			<input type="text" value="${targetDlgText}" disabled />
		</div>
		<div class="form-group">
			<label>Target AC</label>
			<input type='text' name='acField' value="${targetAC}"></input>
		</div>
		<div class="form-group">
			<label>Target Cover</label>
			<select name="coverSelect">
				<option value="0" selected>None</option>
				<option value="2">Half</option>
				<option value="5">Three quarters</option>
				<option value="99" disabled>Full - Cannot be attacked</option>
			</select>
		</div>
		
	</form>
	`;
	
	
	let dialogAttack = new Dialog({
		title:`Mob Attack by ${curActor.name}`,
		content: dialogHTML,
		buttons:{
			attack: {
				icon: "<i class='fas fa-dice-d20'></i>",
				label: `Attack`,
				callback: html => {
					
					let weaponIndex = html.find('[name="weaponSelect"]')[0].value;
					targetAC = parseInt(html.find('[name="acField"]')[0].value);
					attackTypeValue = parseInt(html.find('[name="attackTypeSelect"]')[0].value);
					coverValue = parseInt(html.find('[name="coverSelect"]')[0].value);
					
					let weapon = weapons[weaponIndex];  // default to 1st one
						
					let weaponDmg = weapon.data.data.damage.parts[0][0]; 
					let damageType = weapon.data.data.damage.parts[0][1]; 
					let toHitBonus = weapon.data.data.attackBonus;
					let numHits = calcHits(numCreatures, toHitBonus, targetAC, [attackTypeValue, coverValue]);
					let totalDmg = (numHits > 0) ? calcDamage(numHits, weaponDmg) : 0;
				
				
					
					chatMsg = `
						<div class="dnd5e red-full chat-card">
							<div class="dnd5e chat-card item-card">
								<header class="card-header flexrow red-header">
									<h3 class="item-name">${weapon.name}</h3>
								</header>
							</div>
							<div>
								<div class="dice-roll red-dual">
									<div class="br5e-roll-label">Hits</div>
									<div class="dice-result">
										<div class="dice-row red-totals">
											<h4 class="dice-total dice-row-item red-base-die ">
												${numHits}/${numCreatures}
											</h4>
										</div>
									</div>
								</div>
								<div class="dice-roll">
									<div class="br5e-roll-label">Damage - ${damageType} - @${weaponDmg}</div>
									<div class="dice-result">
										<div class="dice-row red-totals">
											<h4 class="dice-total dice-row-item red-base-die ">
												${totalDmg}
											</h4>
										</div>
									</div>
								</div>
								
							</div>							
						</div>
					`;
					
					let chatData = {
						user: game.user._id,
						speaker: ChatMessage.getSpeaker(),
						content: chatMsg
					};
					ChatMessage.create(chatData, {});
				}
			},
			cancel: {
				icon: "<i class='fas fa-times'></i>",
				label: `Cancel`
			}
		},
		default:'attack'
	});
	
	dialogAttack.render(true);
	
}
else 
{
	if (!isMob) ui.notifications.warn("Please select a token with the 'Individual HP' feature.");
	if (!hasMobAttacks) ui.notifications.warn("Please select a token with one or more weapons with 'Mob Attack' in the name.");
}
function calcHits(attackCount, toHitBonus, targetAC, acAdjustments) {
	targetAC += acAdjustments[0] + acAdjustments[1];
	
	let targetToHit = targetAC - toHitBonus;
	if (targetToHit > 20) targetToHit = 20;
	
	let pctHits = (21 - targetToHit) / 20;
	let numHits = Math.floor(pctHits * attackCount);
	console.log(numHits);
	return numHits;
}
function calcDamage(numHits, dmg) {
	let dmgFormula = "";
	for (let step = 1; step <= numHits; step++) { dmgFormula += "+" + dmg; }
	let dmgRoll = new Roll(dmgFormula).evaluate({async: false}).total;
	
	return dmgRoll;
}
/*
For area effect attacks against an actor defined as a mob;
Calculates number of successful saves, then calculates total
damage across the entire mob based on full damage for failures
and 1/2 damage on successes
Based on the calculations from here: https://slyflourish.com/mob_calculator.html
... which are in turn based on the Mob Combat principles on page 250 of the DMG.
Assumes Advantage/Disadvantage mathematically come out to about +/- 5.
Creating a Mob actor:
 - Create a duplicate of an existing creature's actor and open its character sheet 
    *OR* create a new NPC and enter the relevant stats for a single version of the
    creature you're grouping up
 - Give it a name that lets you know it's the "mob version", like "Orc squad", "dwarf troops",
    or "A Gaggle of Gnomes"
 - Give it a feature called "Individual HP"; set its Activation Cost to the the number
    of hit points an individual creature of that type would have
 - Determine how many individuals will make up this group.  Multiply that the individual HP
    and set min and max HP for the "character" to that number to represent total group HP.
	As group HP drops, the macro will calculare the # of individuals still able to make attacks
 - For any weapons this group will use to attack as a group:
    - Change the name to end in "Mob Attack"
    - Set the attack roll bonus t the total bonus - the macro won't calculate it from stats
    - Optional: Set the damage # to the average damage expected for that attack
*/
let chatMsg = '';
let indHPObj = '';
let isMob = false;
let numCreatures = 1, indHP = 0;
let curActor = null;
const controlled = canvas.tokens.controlled;
if (controlled.length > 0 && controlled[0] !== null) {
	curActor = controlled[0].actor;
	if (curActor !== undefined && curActor!== null) {
		indHPObj = curActor.items.find(i => i.name === 'Individual HP');
		isMob = true;
	}
}
if (isMob) {
	indHP = indHPObj.data.data.activation.cost;
	numCreatures = Math.ceil(curActor.data.data.attributes.hp.value / indHP);
	let overflowHP = curActor.data.data.attributes.hp.value - ((numCreatures - 1) * indHP);
	let saveTypeValue = 0; // advantage = -5 AC, disadvantage = +5 AC
	let saveDC = 10;
	let effectDmg = 0;
	let isResistant = false;
	let saveBonus = 0;
	let dialogHTML = `
		<form>
			<div class="form-group">
				<label>Save DC</label>
				<input type='text' name='dcField' value="${saveDC}"></input>
			</div>
			<div class="form-group">
				<label>Effect Damage</label>
				<input type='text' name='effectDmg' value="${effectDmg}"></input>
			</div>
			<div class="form-group">
				<label>Save Bonus</label>
				<input type='text' name='saveBonus' value="${saveBonus}"></input>
			</div>
			<div class="form-group">
				<label>Save Type:</label>
				<select name="saveTypeSelect">
					<option value="-5">Advantage</option>
					<option value="0" selected>Normal</option>
					<option value="5">Disadvantage</option>
				</select>
			</div>
			<div class="form-group">
				<label>Is Resistant?</label>
				<input type="checkbox" name="isResistant" value="true" />
			</div>
			<div class="form-group">
				<label># Hit (max ${numCreatures}):</label>
				<input type="text" name="numToRoll" value="${numCreatures}" />
			</div>		
		</form>
		`;
	let dialogAttack = new Dialog({
		title:`Mob Attack by ${curActor.name}`,
		content: dialogHTML,
		buttons:{
			save: {
				icon: "<i class='fas fa-dice-d20'></i>",
				label: `Roll Saves`,
				callback: html => {
					saveDC = parseInt(html.find('[name="dcField"]')[0].value);
					saveTypeValue = parseInt(html.find('[name="saveTypeSelect"]')[0].value);
					effectDmg = parseInt(html.find('[name="effectDmg"]')[0].value);
					saveBonus = parseInt(html.find('[name="saveBonus"]')[0].value);
					isResistant = html.find('[name="isResistant"]')[0].checked;
					let numToRoll = parseInt(html.find('[name="numToRoll"]')[0].value);
		
					let numSaves = calcSaves(numToRoll, saveBonus, saveDC, saveTypeValue);
					let totalDmg = calcDamage(numToRoll, numSaves, effectDmg, indHP, overflowHP, isResistant);
					
					chatMsg = `
						<div class="dnd5e red-full chat-card">
							<div class="dnd5e chat-card item-card">
								<header class="card-header flexrow red-header">
									<h3 class="item-name">${curActor.name} Saves</h3>
								</header>
							</div>
							<div>
								<div class="dice-roll red-dual">
									<div class="br5e-roll-label">Successful Saves</div>
									<div class="dice-result">
										<div class="dice-row red-totals">
											<h4 class="dice-total dice-row-item red-base-die ">
												${numSaves}/${numToRoll}
											</h4>
										</div>
									</div>
								</div>
								<div class="dice-roll">
									<div class="br5e-roll-label">Total Group Damage</div>
									<div class="dice-result">
										<div class="dice-row red-totals">
											<h4 class="dice-total dice-row-item red-base-die ">
												${totalDmg}
											</h4>
										</div>
									</div>
								</div>
								
							</div>							
						</div>
					`;
					
					let chatData = {
						user: game.user._id,
						speaker: ChatMessage.getSpeaker(),
						content: chatMsg
					};
					ChatMessage.create(chatData, {});
				}
			},
			cancel: {
				icon: "<i class='fas fa-times'></i>",
				label: `Cancel`
			}
		},
		default:'attack'
	});
	dialogAttack.render(true);
}
else
{
	ui.notifications.warn("Please select a token with the 'Individual HP' feature.");
}
function calcSaves(numTargets, saveBonus, saveDC, saveTypeValue) {
	saveDC += saveTypeValue;
	
	let targetDC = saveDC - saveBonus;
	if (targetDC > 20) targetDC = 20;
	
	let pctSaves = (21 - targetDC) / 20;
	let numSaves = Math.floor(pctSaves * numTargets);
	return numSaves;
}
function calcDamage(numToRoll, numSaves, dmg, hp, overflowHP, isResistant) {
	let saveDmg = Math.floor(dmg/2);
	
	/* cap individual damage before calculating total */
	if (dmg > hp) { dmg = hp; }
	if (saveDmg > hp) { saveDmg = hp; }
	
	let totalDmg = ((numToRoll - numSaves) * dmg) + (numSaves * saveDmg);
	
	/* if a member is already injured, and the damage it takes is more than its current hp,
	   randomly assing it to either the save success group or the fail group and adjust total
	   group damage accordingly  */
	
	if (overflowHP > 0) {
		let x = (Math.floor(Math.random() * 2) == 0);
		if(x && dmg > overflowHP) {
			totalDmg -= (dmg - overflowHP);
		}
		else if (saveDmg > overflowHP) {
			totalDmg -= (saveDmg - overflowHP);
		}
	}
	
	if (isResistant) { totalDmg = Math.floor(totalDmg/2) };
	
	return totalDmg;
}

To try these out, at the very least you’ll need to copy the text of one or both of the scripts above into macro buttons on the toolbar in your Foundry instance as macros of type “script”, not “chat”. See places like here, here, and here to learn some of the basics of using macros.

(Note: I’m one of those weirdos who for several reasons doesn’t use the Midi-QOL plugin to automate application of damage, so I have no idea if this will work with it, or what changes it would take to make that happen.)

The making of a mob

To make my first “mob” type actor, I’ll make a duplicate of the generic Orc actor and rename it Orc Squad. I could just as easily create a new blank NPC actor and fill in all the stats, but copying an existing one saves me some extra work. This squad will contain twenty orc soldiers so I’ll mark its size as “gargantuan” and upload a token showing an overhead view of a bunch of orcs.

The macro needs to know how many HP each member of the group has, so it can keep track of how many are still fighting. To do this, I’ll add a feature which must be named “Individual HP” and set the “Activation Cost” value (for lack of a better value to use) to the creature’s original hit points – 15 in this case. Finally, I’ll multiply the base creature HP times the number of creatures in the group. In this case, 15 HP x 20 orcs = 300HP, so 300/300 is entered in the current and max HP on the main character sheet.

Creating an orc squad "mob"

Next I need to set up which attacks will be used as mob attacks. I’ll keep the Greataxe that orcs carry by default, but I want these to have shortbows instead of javelins so I’ll delete the javelin and drag shortbow over from the SRD compendium. I’ll rename these “<Weapon name> Mob Attack” so the macro will know which features are fair game for applying to group attacks.

I’ll need edit the details of these attacks as well to make a few minor changes. I couldn’t find a direct way to call the existing feature within the 5e system that calculates “To Hit” values based on all the possible modifiers of that value, so rather than re-inventing that somewhat complex wheel, the macro just assumes the DM will fill in the “Attack roll bonus” with the total. In the case of these orc-wielded weapons, it’s STR bonus (+1) plus Proficiency bonus (+2) = 3.

For this example I’ve chosen to replace the normal damage formula with the average damage for that weapon type. This isn’t required, but I’ve found in testing that when it’s set it’s easier to keep track of how much damage a unit should be doing; so for instance in round one I can fire the macro once for a shortbow volley and know that each un-injured orc squad will do the same damage to its target… saving a number of steps along the way and streamlining the combat a bit. The standard dice-roll syntax can be left in place if you prefer the randomness of it, however.

Setting up Mob Attacks

Let the battle begin!

I created a group of soldiers to oppose the orcs, based on a stat block I found somewhere online (“commoner” was too squishy and “veteran” was too tough; these are trained troops with a small amount of experience but not battle-hardened career warriors). Set in groups of 15 with 18 hit points per soldier and wearing splint mail, they’re a little tougher than the orcs. They also have better bows and the advantage of cover behind the wall, but they’re outnumbered. If the orcs can break through the pair of gates and circle behind them without taking heavy losses, their high-damage axes and the removal of cover from the defenders will drastically change the battle. For purposes of this scenario and their resistance to being hacked away by orc axes, I’m going to say the gates have AC16 and 100 HP.

Sixty orcs have approached the wall as thirty defenders scramble to prepare themselves. Sadly for the soldiers, the orc squad that’s charging for the gate gets the highest initiative! They charge up to the gate and attack it – select the orc group, target the gate, hit the macro button, and choose Greataxe Mob Attack as the weapon and… boom! Eighty points of damage already! Not an auspicious start for the soldiers.

Mob attacks round 1

The soldiers are up next and each do 40 points of damage to their targets. That’s enough to take out two orcs from each squad, meaning fewer attacks will be launched by them in the next round.

With 1/2 cover on the wall and 3/4 cover in the tower, the defenders take less damage – 20 points and 12 points, respectively, with only one soldier falling.

In round 2, the outer gate comes down already and the gate-crashers move forward to the second obstacle! Again the defenders damage the orcs and take some smaller hits themselves.

But at the top of round 3, what’s this? Spell-casting fake PC Testy McTesterson emerges atop the the east tower and casts a fireball down on the orcs!

Six of the remaining 15 orcs make their saving throws… but at 37 points of damage to everyone in the area of effect, half damage doesn’t help them. As a group they take a total of 441 points of damage – killing them all instantly!

More arrow fire is exchanged, with less-damaged soldier squads doing 35 points to the orcs, while the orcs only do 8 and 12. Still, the invaders press forward toward the remaining gate!

Testy fires off a trio of Scorching Rays, hitting twice for 16 points of damage. Each orc squad takes 35 more from another volley of arrows. The “Friendly” squad reaches the gate and swing their axes at it, but being down to only 12 members, the damage is only 40 points. It’s going to take them at least two more rounds to get through even if they don’t take any more damage… which is unlikely given the tower guards are still firing down on them.

The last orc group, down to only eight members, realizes the futility of the situation and turns to dash away.

At this point it’s clear the orcs are defeated, thanks in no small part to that timely fireball! In the next round they’ll flee, likely taking heavy damage from the defenders’ longbows as they do so.

3 thoughts on “Foundry Mob Attack Macros for D&D 5e”

  1. This is great.
    I am wondering about V10 or V11 of foundryvtt. Do you think it will work on those versions?
    Thank you.

    1. I’m planning to do the V11 upgrade on one of my instances in the next couple of days – I’ll try to remember to test them out afterward.

    2. Just tried them in V10 and they still work; I don’t think any of the changes in V11 will impact them so they should still work there.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top