I love traps!
This module allows the GM to press a hotkey and select several premade traps and loot caches to instantly create on the map. The trap is instantly surrounded by four “Hints” that can only be seen with the prerequisite passive perception for each. The exact same hint actors appear around loot caches that can also be created, forcing players to investigate and determine whether they have encountered a blessing or a curse. Ideally hints will be setup in a way vague enough that multiple hints in tandem are required to make this distinction. Most importantly, the module allows the users to create and edit their own list of traps and caches; setting up multiple hint sets for each that will be chosen randomly decreasing the likelihood of players memorizing hints. All hints and traps can be manually adjusted after creation for further customization.
Set your hotkey after installing to access the menu; should default to ctrl+t.
Dependencies:
• Monks Active Tile Triggers
• Stealthy (A compendium will be added with a macro to clear all banked perception and stealth checks.)
• Upon install a compendium of actors "The Horse's Actors" will be added for the hints and loot. Import these anywhere to you world as actors.
Recommended:
• “Item Piles” to set up caches.
• "The Horse's Actor Visibility Tools" add a menu to the protoype token's "appearance" tab with two options to enables actors to roll stealth upon creation and enables a minimum viewing distance, beyond which actors will not be able to be seen by players.
Trap Macro:
• Add this macro to the hotbar and point to it in the module menu; it will also be added to the compendium "The Horse's Macros":
// Trap Trigger Macro – Auto Roll with Adv/Dis + Flat Bonus (DC Hidden + Damage Only on Failure)
// For use with Trap Automator + Monk's Active Tiles.
(async () => {
// ---------- 1) Resolve token & actor ----------
let t = typeof token !== "undefined" ? token : null;
let a = typeof actor !== "undefined" ? actor : null;
if (!t || !a) {
const controlled = canvas.tokens.controlled[0];
if (controlled) {
t = controlled;
a = controlled.actor;
} else if (game.user.character) {
a = game.user.character;
const tok = canvas.tokens.placeables.find(tok => tok.actor?.id === a.id);
t = tok ?? null;
}
}
if (!t || !a) {
ui.notifications.error("⚠️ Trap macro: no token/actor available.");
return;
}
const speaker = ChatMessage.getSpeaker({ token: t, actor: a });
// ---------- 2) Rebuild raw JSON from args ----------
let raw = "";
if (Array.isArray(args) && args.length) raw = args.join(" ").trim();
else if (typeof args === "string") raw = args.trim();
if (!raw) {
raw = await new Promise(resolve => {
new Dialog({
title: "Trap JSON Missing",
content: `
<p>No trap data provided. Paste your JSON below:</p>
<textarea id="rawjson" style="width:100%;height:120px"></textarea>
`,
buttons: {
ok: {
icon: '<i class="fas fa-check"></i>',
label: "OK",
callback: html => resolve(html.find("#rawjson").val().trim())
}
},
default: "ok"
}).render(true);
});
}
if (!raw) {
ui.notifications.error("❌ Trap macro: no JSON data supplied.");
return;
}
if (raw.startsWith('"') && raw.endsWith('"')) raw = raw.slice(1, -1);
raw = raw.replace(/\\"/g, '"');
// ---------- 3) Parse JSON ----------
let trap;
try {
trap = JSON.parse(raw);
} catch (err) {
console.error("Trap macro JSON parse error:", err, raw);
ui.notifications.error("❌ Trap macro JSON parse error:\n" + err.message);
return;
}
// ---------- 4) Normalize trap fields ----------
const name = trap.name ?? "Unknown Trap";
const flavor = trap.flavor ?? "";
const saveKey = (trap.saveType ?? "dex").toLowerCase();
const saveLabel = saveKey.toUpperCase();
const dc = trap.DC ?? trap.hiddenDC ?? 10;
const dmgFormula = trap.damageFormula ?? "";
const dmgType = trap.damageType ?? "";
const failText = trap.failText ?? "You are harmed by the trap.";
const successText = trap.successText ?? "You avoid the worst of the trap.";
// ---------- 5) Compute actor’s save modifier ----------
const abilities = a.system?.abilities ?? {};
const attrs = a.system?.attributes ?? {};
const ab = abilities[saveKey] ?? {};
const profBonus = Number(attrs.prof ?? 0) || 0;
const abilityMod = Number(ab.mod ?? 0) || 0;
const profMult = Number(ab.proficient ?? 0) || 0;
const saveMod = abilityMod + profBonus * profMult;
// ---------- 6) Ask player for roll mode & bonus ----------
const modeChoice = await new Promise(resolve => {
new Dialog({
title: `Roll ${saveLabel} Save`,
content: `
<form>
<p><strong>${name}</strong></p>
<p>${flavor}</p>
<hr/>
<p>Choose how to roll your <strong>${saveLabel}</strong> save.</p>
<div class="form-group">
<label>Roll mode:</label>
<div>
<label><input type="radio" name="ta-save-mode" value="normal" checked/> Normal</label><br/>
<label><input type="radio" name="ta-save-mode" value="adv"/> Advantage</label><br/>
<label><input type="radio" name="ta-save-mode" value="dis"/> Disadvantage</label>
</div>
</div>
<div class="form-group">
<label for="ta-save-bonus">Extra flat bonus:</label>
<input type="number" id="ta-save-bonus" name="ta-save-bonus" value="0" style="width:4em;"/>
</div>
</form>
`,
buttons: {
roll: {
label: "Roll",
icon: '<i class="fas fa-dice-d20"></i>',
callback: html => {
const mode = html.find("input[name='ta-save-mode']:checked").val();
const bonus = parseInt(html.find("#ta-save-bonus").val(), 10) || 0;
resolve({ mode, bonus });
}
},
cancel: { label: "Cancel", callback: () => resolve(null) }
}
}).render(true);
});
if (!modeChoice) return;
const { mode, bonus } = modeChoice;
// ---------- 7) Build roll formula ----------
const baseTerm =
mode === "adv" ? "2d20kh1" :
mode === "dis" ? "2d20kl1" :
"1d20";
const totalBonus = saveMod + bonus;
let bonusPart = "";
if (totalBonus > 0) bonusPart = ` + ${totalBonus}`;
else if (totalBonus < 0) bonusPart = ` - ${Math.abs(totalBonus)}`;
const formula = `${baseTerm}${bonusPart}`;
// ---------- 8) Roll ----------
let roll;
try {
roll = new Roll(formula);
await roll.evaluate({ async: true });
} catch (err) {
console.error("Trap macro: error rolling saving throw:", err);
ui.notifications.error("❌ Trap macro: error rolling saving throw. See console.");
return;
}
const total = roll.total ?? 0;
let modeText =
mode === "adv" ? "with advantage" :
mode === "dis" ? "with disadvantage" :
"normally";
const extraBonusText =
bonus > 0 ? ` (+${bonus} bonus)` :
bonus < 0 ? ` (${bonus} bonus)` :
"";
// ---------- 9) Success / failure ----------
const success = total >= dc;
const resultLine = success
? `<strong>Success.</strong> ${successText}`
: `<strong>Failure.</strong> ${failText}`;
// Only show damage line **if the save actually failed**
const dmgLine = (!success && dmgFormula)
? `<p>Apply <strong>${dmgFormula} ${dmgType || "damage"}</strong>.</p>`
: "";
// ---------- 10) Post result (DC hidden) ----------
const content = `
<p><strong>Trap Triggered: ${name}</strong><br/>${flavor}</p>
<hr/>
<p><em>Roll result:</em> ${total} (${saveLabel} save rolled ${modeText}${extraBonusText}; formula ${roll.formula})</p>
<p>${resultLine}</p>
${dmgLine}
`;
await ChatMessage.create({ speaker, content });
})();