/** * Highlight critical success or failure on d20 rolls */ export const highlightCriticalSuccessFailure = function (message, html, data) { if (!message.isRoll || !message.isContentVisible) return; // Highlight rolls where the first part is a d20 roll const roll = message.roll; if (!roll.dice.length) return; const d = roll.dice[0]; // Ensure it is an un-modified d20 roll const isD20 = d.faces === 20 && d.values.length === 1; if (!isD20) return; const isModifiedRoll = "success" in d.results[0] || d.options.marginSuccess || d.options.marginFailure; if (isModifiedRoll) return; // Highlight successes and failures const critical = d.options.critical || 20; const fumble = d.options.fumble || 1; if (d.total >= critical) html.find(".dice-total").addClass("critical"); else if (d.total <= fumble) html.find(".dice-total").addClass("fumble"); else if (d.options.target) { if (roll.total >= d.options.target) html.find(".dice-total").addClass("success"); else html.find(".dice-total").addClass("failure"); } }; /* -------------------------------------------- */ /** * Optionally hide the display of chat card action buttons which cannot be performed by the user */ export const displayChatActionButtons = function (message, html, data) { const chatCard = html.find(".sw5e.chat-card"); if (chatCard.length > 0) { const flavor = html.find(".flavor-text"); if (flavor.text() === html.find(".item-name").text()) flavor.remove(); // If the user is the message author or the actor owner, proceed let actor = game.actors.get(data.message.speaker.actor); if (actor && actor.isOwner) return; else if (game.user.isGM || data.author.id === game.user.id) return; // Otherwise conceal action buttons except for saving throw const buttons = chatCard.find("button[data-action]"); buttons.each((i, btn) => { if (btn.dataset.action === "save") return; btn.style.display = "none"; }); } }; /* -------------------------------------------- */ /** * This function is used to hook into the Chat Log context menu to add additional options to each message * These options make it easy to conveniently apply damage to controlled tokens based on the value of a Roll * * @param {HTMLElement} html The Chat Message being rendered * @param {Array} options The Array of Context Menu options * * @return {Array} The extended options Array including new context choices */ export const addChatMessageContextOptions = function (html, options) { let canApply = (li) => { const message = game.messages.get(li.data("messageId")); return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length; }; options.push( { name: game.i18n.localize("SW5E.ChatContextDamage"), icon: '', condition: canApply, callback: (li) => applyChatCardDamage(li, 1) }, { name: game.i18n.localize("SW5E.ChatContextHealing"), icon: '', condition: canApply, callback: (li) => applyChatCardDamage(li, -1) }, { name: game.i18n.localize("SW5E.ChatContextDoubleDamage"), icon: '', condition: canApply, callback: (li) => applyChatCardDamage(li, 2) }, { name: game.i18n.localize("SW5E.ChatContextHalfDamage"), icon: '', condition: canApply, callback: (li) => applyChatCardDamage(li, 0.5) } ); return options; }; /* -------------------------------------------- */ /** * Apply rolled dice damage to the token or tokens which are currently controlled. * This allows for damage to be scaled by a multiplier to account for healing, critical hits, or resistance * * @param {HTMLElement} li The chat entry which contains the roll data * @param {Number} multiplier A damage multiplier to apply to the rolled damage. * @return {Promise} */ function applyChatCardDamage(li, multiplier) { const message = game.messages.get(li.data("messageId")); const roll = message.roll; return Promise.all( canvas.tokens.controlled.map((t) => { const a = t.actor; return a.applyDamage(roll.total, multiplier); }) ); } /* -------------------------------------------- */