Skip to content

Instantly share code, notes, and snippets.

@7H3LaughingMan
Last active January 2, 2025 19:10
Show Gist options
  • Select an option

  • Save 7H3LaughingMan/e53ce5458f07c3dc5b9b3cdb1a8ccf8b to your computer and use it in GitHub Desktop.

Select an option

Save 7H3LaughingMan/e53ce5458f07c3dc5b9b3cdb1a8ccf8b to your computer and use it in GitHub Desktop.
PF2e Treat Wounds / Battle Medicine Macro
/**
* @param {ActorPF2e} actor
* @param {string} slug
* @returns {boolean}
*/
function checkFeat(actor, slug) {
return actor.itemTypes.feat.some((feat) => feat.slug === slug);
}
/**
* @param {ActorPF2e} actor
* @param {string} slug
* @param {(attached | dropped | held | stowed | worn)} [carryType]
* @param {{0 | 1 | 2}} [handsHeld]
* @returns {boolean}
*/
function checkEquipment(actor, slug, carryType, handsHeld) {
return actor.itemTypes.equipment.some(
(equipment) =>
equipment.slug === slug &&
(!carryType || equipment.carryType === carryType) &&
(!handsHeld || equipment.handsHeld === handsHeld)
);
}
/**
* @param {ActorPF2e} actor
* @returns {Map<string, number>}
*/
function getAssurances(actor) {
let assurances = new Map();
actor.itemTypes.feat
.filter((feat) => feat.slug === "assurance" && feat.flags.pf2e.rulesSelections.assurance)
.map((feat) =>
assurances.set(
feat.flags.pf2e.rulesSelections.assurance,
10 + (token.actor.skills[feat.flags.pf2e.rulesSelections.assurance].rank * 2 + token.actor.level)
)
);
return assurances;
}
/**
* @param {ActorPF2e} actor
* @returns {Map<string, number>}
*/
function getSkills(actor) {
let skills = new Map();
if (actor.skills.medicine.rank >= 1) {
skills.set("medicine", actor.skills.medicine.rank);
} else if (checkFeat(actor, "clever-improviser")) {
skills.set("medicine", 1);
}
if (checkFeat(actor, "chirurgeon") && actor.skills.crafting.rank >= 1) {
skills.set("crafting", actor.skills.crafting.rank);
}
if (checkFeat(actor, "natural-medicine") && actor.skills.nature.rank >= 1) {
skills.set("nature", actor.skills.nature.rank);
}
if (checkFeat(actor, "spell-stitcher") && actor.skills.arcana.rank >= 1) {
skills.set("arcana", actor.skills.arcana.rank);
}
return skills;
}
/** Get DamageRoll & CheckRoll */
const DamageRoll = CONFIG.Dice.rolls.find((R) => R.name === "DamageRoll");
const CheckRoll = CONFIG.Dice.rolls.find((R) => R.name === "CheckRoll");
if (canvas.tokens.controlled.length !== 1) {
ui.notifications.error("You need to select one token to act as the healer.");
return;
}
if (game.user.targets.size < 1) {
ui.notifications.error("You must target at least one token to heal.");
return;
}
/** Setup */
const skills = getSkills(token.actor);
const assurances = getAssurances(token.actor);
const bonuses = [0, 0, 10, 30, 50];
const difficultyClasses = game.pf2e.settings.variants.pwol.enabled ? [10, 15, 20, 25, 30] : [10, 15, 20, 30, 40];
const inCombat = game.combats.active?.started;
const hasHealersToolkit =
checkEquipment(token.actor, "healers-toolkit") ||
checkEquipment(token.actor, "healers-toolkit-expanded") ||
checkEquipment(token.actor, "violet-ray") ||
checkEquipment(token.actor, "marvelous-medicines") ||
checkEquipment(token.actor, "marvelous-medicines-greater") ||
checkEquipment(token.actor, "medkit-commercial") ||
checkEquipment(token.actor, "medkit-tactical");
const hasHealersToolkitHeld =
!hasHealersToolkit ||
checkEquipment(token.actor, "healers-toolkit", "worn") ||
checkEquipment(token.actor, "healers-toolkit-expanded", "worn") ||
checkEquipment(token.actor, "violet-ray", "held", 2) ||
checkEquipment(token.actor, "marvelous-medicines", "held", 2) ||
checkEquipment(token.actor, "marvelous-medicines-greater", "held", 2) ||
checkEquipment(token.actor, "medkit-commercial", "worn") ||
checkEquipment(token.actor, "medkit-tactical", "worn");
const hasBattleMedicsBatonHeld = checkEquipment(token.actor, "battle-medics-baton", "held", 1);
const hasBattleMedicine = checkFeat(token.actor, "battle-medicine");
if (skills.size === 0) {
ui.notifications.error("The selected token is not trained in any skills that can be used for Treat Wounds.");
return;
}
if (inCombat && !hasBattleMedicine) {
ui.notifications.error("The selected token does not have Battle Medicine.");
return;
}
const dialog = new foundry.applications.api.DialogV2({
window: {
title: `${inCombat ? `Battle Medicine` : hasBattleMedicine ? `Treat Wounds / Battle Medicine` : `Treat Wounds`}`,
},
content: `
<div id="info">Attempt to heal the target/targets by 2d8 Hit Points.</div>
<div id="healers-toolkit"><b>The selected token does not have a Healer's Toolkit!</b></div>
<div class="form-group" id="built-in-tools">
<label id="built-in-tools">Is healer's toolkit one of your Built-In Tools?</label>
<input type="checkbox" id="built-in-tools"></input>
</div>
<div class="form-group" id="healing-plaster">
<label id="healing-plaster">Are you using Healing Plaster?</label>
<input type="checkbox" id="healing-plaster"></input>
</div>
<div class="form-group" id="action">
<label id="action">Action</label>
<select id="action">
${!inCombat ? `<option value="treat-wounds">Treat Wounds</option>` : ``}
${
hasBattleMedicine
? `<option value="battle-medicine" ${inCombat ? `selected` : ``}>Battle Medicine</option>`
: ``
}
</select>
</div>
<div class="form-group" id="assurance">
<label id="assurance">Use Assurance?</label>
<input type="checkbox" id="assurance"></input>
</div>
<div class="form-group" id="skill">
<label id="skill">Skill</label>
<select id="skill">
${skills.has("crafting") ? `<option value="crafting">Crafting</option>` : ``}
${skills.has("nature") ? `<option value="nature">Nature</option>` : ``}
${skills.has("arcana") ? `<option value="arcana">Arcana</option>` : ``}
${skills.has("medicine") ? `<option value="medicine">Medicine</option>` : ``}
</select>
</div>
<div class="form-group" id="dc-type">
<label id="dc-type">Skill DC</label>
<select id="dc-type">
<option value="1">Trained DC ${difficultyClasses[1]}</option>
<option value="2">Expert DC ${difficultyClasses[2]}, +10 Healing</option>
<option value="3">Master DC ${difficultyClasses[3]}, +30 Healing</option>
<option value="4">Legendary DC ${difficultyClasses[4]}, +50 Healing</option>
</select>
</div>
<div class="form-group" id="modifier">
<label id="modifier">DC Modifier</label>
<input id="modifier" type="number" />
</div>
<div class="form-group" id="risky-surgery">
<label id="risky-surgery">Risky Surgery</label>
<input type="checkbox" id="risky-surgery"></input>
</div>
<div class="form-group" id="mortal-healing">
<label id="mortal-healing">Mortal Healing</label>
<input type="checkbox" id="mortal-healing"></input>
</div>
<div class="form-group" id="right-hand-blood">
<label id="right-hand-blood">Right-Hand Blood</label>
<input type="checkbox" id="right-hand-blood"></input>
</div>
<div id="healers-toolkit-held">Note: You must have your Healer's Toolkit <b>WORN</b> due to how they are implemented in the PF2e System.</div>`,
buttons: [
{
action: "submit",
label: "Submit",
icon: "fa-solid fa-hand-holding-medical",
default: true,
callback: (event, button, dialog) => {
let form = new Map();
for (const [key, value] of Object.entries(button.form.elements)) {
if (isNaN(key)) {
if (value.type === "checkbox") {
form.set(key, value.checked);
} else {
form.set(key, value.value);
}
}
}
return form;
},
},
{
action: "cancel",
label: "Cancel",
icon: "fa-solid fa-times",
},
],
submit: (result) => {
if (result !== "cancel") {
onSubmit(result);
}
},
});
dialog.addEventListener("render", (event) => {
dialog.element.querySelector("select#action").addEventListener("change", (e) => {
onActionChange(dialog.element);
});
dialog.element.querySelector("select#skill").addEventListener("change", (e) => {
onSkillChange(dialog.element);
});
dialog.element.querySelector("select#dc-type").addEventListener("change", (e) => {
onDifficultyChange(dialog.element);
});
if (hasHealersToolkit) {
dialog.element.querySelector("div#healers-toolkit").setAttribute("style", "display: none;");
dialog.element.querySelector("div#built-in-tools").setAttribute("style", "display: none;");
dialog.element.querySelector("input#built-in-tools").checked = false;
dialog.element.querySelector("div#healing-plaster").setAttribute("style", "display: none;");
dialog.element.querySelector("input#built-in-tools").checked = false;
} else {
if (!checkFeat(token.actor, "built-in-tools")) {
dialog.element.querySelector("div#built-in-tools").setAttribute("style", "display: none;");
dialog.element.querySelector("input#built-in-tools").checked = false;
}
}
if (hasHealersToolkitHeld) {
dialog.element.querySelector("div#healers-toolkit-held").setAttribute("style", "display: none;");
}
onActionChange(dialog.element);
onSkillChange(dialog.element);
onDifficultyChange(dialog.element);
});
dialog.render({ force: true });
function onActionChange(element) {
const action = element.querySelector("select#action");
const selectedValue = action.options[action.selectedIndex].value;
if (action.options.length === 1) {
action.disabled = true;
}
if (selectedValue === "treat-wounds") {
if (checkFeat(token.actor, "risky-surgery")) {
element.querySelector("div#risky-surgery").setAttribute("style", "");
element.querySelector("input#risky-surgery").checked = false;
} else {
element.querySelector("div#risky-surgery").setAttribute("style", "display: none;");
element.querySelector("input#risky-surgery").checked = false;
}
if (checkFeat(token.actor, "mortal-healing")) {
element.querySelector("div#mortal-healing").setAttribute("style", "");
element.querySelector("input#mortal-healing").checked = false;
} else {
element.querySelector("div#mortal-healing").setAttribute("style", "display: none;");
element.querySelector("input#mortal-healing").checked = false;
}
if (checkFeat(token.actor, "right-hand-blood")) {
element.querySelector("div#right-hand-blood").setAttribute("style", "");
element.querySelector("input#right-hand-blood").checked = false;
} else {
element.querySelector("div#right-hand-blood").setAttribute("style", "display: none;");
element.querySelector("input#right-hand-blood").checked = false;
}
if (!hasHealersToolkit) {
dialog.element.querySelector("div#healing-plaster").setAttribute("style", "");
dialog.element.querySelector("input#built-in-tools").checked = false;
}
} else {
element.querySelector("div#risky-surgery").setAttribute("style", "display: none;");
element.querySelector("input#risky-surgery").checked = false;
element.querySelector("div#mortal-healing").setAttribute("style", "display: none;");
element.querySelector("input#mortal-healing").checked = false;
element.querySelector("div#right-hand-blood").setAttribute("style", "display: none;");
element.querySelector("input#right-hand-blood").checked = false;
if (!hasHealersToolkit) {
dialog.element.querySelector("div#healing-plaster").setAttribute("style", "display: none;");
dialog.element.querySelector("input#built-in-tools").checked = false;
}
}
}
function onSkillChange(element) {
const skill = element.querySelector("select#skill");
const selectedText = skill.options[skill.selectedIndex].text;
const selectedValue = skill.options[skill.selectedIndex].value;
if (skill.options.length === 1) {
skill.disabled = true;
}
element.querySelector("label#dc-type").innerHTML = `${selectedText} DC`;
element.querySelector("select#dc-type").innerHTML = `
${skills.get(selectedValue) >= 1 ? `<option value="1">Trained DC ${difficultyClasses[1]}</option>` : ``}
${
skills.get(selectedValue) >= 2
? `<option value="2">Expert DC ${difficultyClasses[2]}, +10 Healing</option>`
: ``
}
${
skills.get(selectedValue) >= 3
? `<option value="3">Master DC ${difficultyClasses[3]}, +30 Healing</option>`
: ``
}
${
skills.get(selectedValue) >= 4
? `<option value="4">Legendary DC ${difficultyClasses[4]}, +50 Healing</option>`
: ``
}
`;
if (assurances.has(selectedValue)) {
dialog.element.querySelector("div#assurance").setAttribute("style", "");
dialog.element.querySelector(
"label#assurance"
).innerHTML = `Use Assurance? <small>This will beat DC ${assurances.get(selectedValue)}</small>`;
dialog.element.querySelector("input#assurance").checked = false;
} else {
dialog.element.querySelector("div#assurance").setAttribute("style", "display: none;");
dialog.element.querySelector("label#assurance").innerHTML = `Use Assurance?`;
dialog.element.querySelector("input#assurance").checked = false;
}
onDifficultyChange(element);
}
function onDifficultyChange(element) {
const difficulty = element.querySelector("select#dc-type");
const selectedValue = difficulty.options[difficulty.selectedIndex].value;
const bonus = bonuses[selectedValue];
if (difficulty.options.length === 1) {
difficulty.disabled = true;
}
element.querySelector("div#info").innerHTML = `Attempt to heal the target/targets by ${
bonus !== 0 ? `2d8+${bonus}` : `2d8`
} Hit Points.`;
}
/**
* @param {Map<string, any>} formData
*/
async function onSubmit(formData) {
const builtInTools = formData.get("built-in-tools");
const healingPlaster = formData.get("healing-plaster");
const action = formData.get("action");
const assurance = formData.get("assurance");
const skill = formData.get("skill");
const dcType = Number(formData.get("dc-type"));
const modifier = Number(formData.get("modifier"));
const riskySurgery = formData.get("risky-surgery");
const mortalHealing = formData.get("mortal-healing");
const rightHandBlood = formData.get("right-hand-blood");
const battleMedicine = formData.get("action") === "battle-medicine";
const hasWardMedic = checkFeat(token.actor, "ward-medic");
const rollSubstitution = assurance
? token.actor.synthetics.rollSubstitutions[skill].find((substitution) => substitution.slug === "assurance")
: null;
const maxTargets = battleMedicine ? 1 : hasWardMedic ? 2 ** (skills[skill] - 1) : 1;
if (game.user.targets.size > maxTargets) {
ui.notifications.warn(`You can only target ${maxTargets} tokens.`);
return;
}
const statistic = token.actor.skills[skill];
const dc = { value: difficultyClasses[dcType] + modifier, visible: true };
const label = await renderTemplate("systems/pf2e/templates/chat/action/header.hbs", {
title: battleMedicine ? "Battle Medicine" : "Treat Wounds",
subtitle: `${statistic.label} Check`,
glyph: battleMedicine ? "A" : null,
});
const extraRollNotes = [];
const extraRollOptions = battleMedicine ? ["action:battle-medicine"] : ["action:treat-wounds"];
const modifiers = [];
if (riskySurgery) {
extraRollNotes.push({
title: "Risky Surgery",
text: "When you Treat Wounds, you can deal @Damage[1d8[slashing]] damage to your patient just before applying the effects of the Treat Wounds. If you do, gain a +2 circumstance bonus to your check to Treat Wounds, and if you roll a success, you get a critical success instead.",
});
extraRollOptions.push("risky-surgery");
modifiers.push(
new game.pf2e.Modifier({
slug: "risky-surgery",
label: "Risky Surgery",
modifier: 2,
type: "circumstance",
enabled: true,
})
);
(actor.synthetics.degreeOfSuccessAdjustments[skill] ??= []).push({
adjustments: {
success: {
label: "Risky Surgery",
amount: 1,
},
},
});
}
if (mortalHealing) {
if (skill !== "medicine") {
extraRollNotes.push({
title: "Mortal Healing",
text: "When you roll a success to @UUID[Compendium.pf2e.actionspf2e.Item.1kGNdIIhuglAjIp9]{Treat Wounds} for a creature that hasn't regained Hit Points from divine magic in the past 24 hours, you get a critical success on your check instead and restore the corresponding amount of Hit Points.",
});
}
(actor.synthetics.degreeOfSuccessAdjustments[skill] ??= []).push({
adjustments: {
success: {
label: "Mortal Healing",
amount: 1,
},
},
});
}
if (rightHandBlood) {
extraRollNotes.push({
title: "Right-Hand Blood",
text: "When you Treat Wounds, you can deal @Damage[2d8] damage to yourself to yourself just before applying the effects of the Treat Wounds. If you do, you don't need a healer's toolkit and you gain a +1 item bonus to your check.",
});
extraRollOptions.push("right-hand-blood");
modifiers.push(
new game.pf2e.Modifier({
slug: "right-hand-blood",
label: "Right-Hand Blood",
modifier: 1,
type: "item",
enabled: true,
})
);
}
const traits = battleMedicine
? ["general", "healing", "manipulate", "skill"]
: ["exploration", "healing", "manipulate"];
if (assurance) {
extraRollOptions.push("substitute:assurance");
rollSubstitution.selected = true;
}
await statistic.roll({
action,
dc,
label,
extraRollNotes,
extraRollOptions,
modifiers,
traits,
callback: async (roll, outcome) => {
console.log(roll);
console.log(outcome);
},
});
if (assurance) {
rollSubstitution.selected = false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment