Last active
January 2, 2025 19:10
-
-
Save 7H3LaughingMan/e53ce5458f07c3dc5b9b3cdb1a8ccf8b to your computer and use it in GitHub Desktop.
PF2e Treat Wounds / Battle Medicine Macro
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * @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