See if this does what you want it to. It's possible I got something wrong, math-wise. The command to trigger the script is: !psyroll {{name=Psy-Focus}} {{power=?{Power Level?|Fettered|Unfettered|Pushing,Pushing&rcub;&rcub; &lcub;&lcub;strength=?&lcub;Extra Strength&#124;0&rcub;}}} {{mods=Advance WP|8,Willpower|11,Situation|?{Sit Mod?|0}}} {{rating=4}} {{name}} can be changed to whatever you want the name of the roll to be. {{power}} recreates the Query you had in your macro (with nested Query for Pushing). {{mods}} contains whatever else you want to add on to the roll, mods separated by comma and label|value separated by pipe. The example above is hard-coded numbers, you'd want to change those back to your @{charname|willpower} references. {{rating}} is just for the {psyrating} attribute, separate to mods since it has different math applied ot it. {{roll}} isn't in the example, but you can change the core roll if you want, {{roll=1d4}}. Doubt you'll need this. You can also supply {{test=1}} (or any characters after the '=') to enable test mode - this will trigger loads of crits, fails and warps for testing. I have no idea what you want the Warp effect to look like (I just chucked some blue stuff at it), happy to change it to whatever you want. It looks a bit erm.... placeholder-y: :) And the script. /* globals on, sendChat, log */ const psyRoll = (() => { //eslint-disable-line no-unused-vars const scriptName = 'psyRoll'; // !psyroll {{name=Psy-Focus}} {{power=?{Power Level?|Fettered|Unfettered|Pushing,Pushing&rcub;&rcub; &lcub;&lcub;strength=?&lcub;Extra Strength&#124;0&rcub;}}} {{mods=Advance WP|8,Willpower|11,Situation|?{Sit Mod?|0}}} {{rating=4}} const styles = { outer: `margin-left: -7px; border: 1px solid #000099;`, header: `background-color: #000099; color: white; text-align: left; padding: 5px;`, title: `font-size: 1.1em;`, subtitle: `font-size: 0.9em;`, body: `line-height: 1.4em; padding: 5px; background-color: white;`, row: `text-align: center;`, rowKey: `font-weight: bold; padding-right: 10px; text-align: right; padding: 5px;`, rowValue: `font-weight: bold;`, footer: `display: none; padding: 5px;`, roll: `background-color: #fef68e;border: 2px solid #fef68e;padding: 0px 3px 0px 3px;font-weight: bold;cursor: help;font-size: 1.1em;`, warp: `padding: 2px 6px 2px 6px; border: 1px #8f8ff9 solid; border-radius: 3px; background-color: #a4cddb;`, warpText: `color: #006c8b; margin: 0px 0px 0px 5px; text-align: center;`, crit: `border: 2px solid #3fb315;`, fail: `border: 2px solid #b31515;`, critAndFail: `border: 2px solid darkblue;` } const outputTemplate = ` <div class="psyroll" style="${styles.outer}"> <div class="psyroll-header" style="${styles.header}"> <div class="psyroll-title" style="${styles.title}">%title%</div> <div class="psyroll-subtitle" style="${styles.subtitle}">%subtitle%</div> </div> <div class="psyroll-body" style="${styles.body}"> %body% </div> <div class="psyroll-footer" style="${styles.footer}">adsf</div> </div> `; const rowTemplate = `<div class="psyroll-row" style="${styles.row}"> <span class="row-key" style="${styles.rowKey}">%key%</span> <span class="row-value" style="${styles.rowValue}">%value%</span> </div>` const splitMacroData = async (msgContent) => { let output = { name: 'Psy Roll', mods: [], rating: 1, roll: '[[1d100]]', strength: 0 }; const dataArray = msgContent.split(/\s*\{\{\s*/g); dataArray.shift(); dataArray.forEach(v => { const parts = v.replace(/\s*\}\}\s*$/, '').split(/=/); if (parts && parts.length > 1) { if (/mods/i.test(parts[0])) { const rollMods = parts[1].split(/\s*,\s*/g); rollMods.forEach(rm => { const parts = rm.split(/\s*\|\s*/); if (parts && parts.length > 1) output.mods.push({ label: parts[0], value: parseInt(parts[1]) }); }); } else { output[parts[0]] = /(strength|rating)/i.test(parts[0]) ? parseInt(parts[1]) : parts[1]; } } }); output.roll = /\s*\[\[/.test(output.roll) && /\]\]\s*$/.test(output.roll) ? output.roll : `[[${output.roll}]]`; return output; } const buildTemplate = async (rollData) => { const critStyle = (rollData.crit && rollData.fail) ? styles.critAndFail : rollData.crit ? styles.crit : rollData.fail ? styles.fail : ``, warpStyle = rollData.warpRoll ? styles.warp : ``, warpText = rollData.warpRoll ? `<span class="warptext" style="${styles.warpText}">W</span>` : ``; const data = { title: rollData.name, subtitle: rollData.power, rows: [ { label: `Degree(s) of success`, value: `<div class="warp" style="display:inline-block;${warpStyle}"><span class="showtip" style="${styles.roll}${critStyle}" title="${rollData.tooltip}">${rollData.finalResult}</span>${warpText}</div>` } ] }; data.body = data.rows.map(r => rowTemplate.replace(/%key%/, r.label||'').replace(/%value%/, r.value||'')).join('<br>'); let chatTemplate = outputTemplate; for (let prop in data) { chatTemplate = chatTemplate.replace(`%${prop}%`, data[prop]) } return chatTemplate.replace(/%\w+%/g, ''); // remove any leftover placeholders } const buildPsyRoll = async (rollData) => { const ratingCalc = /push/i.test(rollData.power) ? (rollData.rating + rollData.strength)*5 : /unfett/i.test(rollData.power) ? (Math.ceil(rollData.rating/2))*5 : rollData.rating*5; let ratingCalcString = /push/i.test(rollData.power) ? `((${rollData.rating} + ${rollData.strength})` : /unfett/i.test(rollData.power) ? `(ceil(${rollData.rating}/2)` : `(${rollData.rating}`; ratingCalcString += `*5)[Psy Rating Calc]`; const modsString = rollData.mods.map(v => `${v.value}[${v.label}]`), rollString = `${modsString.join(' + ')} + ${ratingCalcString} - ${rollData.result}[${rollData.roll.replace(/[[\]]/g, '')}])/10`; const modSubtotal = rollData.mods.reduce((m,v) => m + v.value||0, 0), rollResult = (modSubtotal + ratingCalc - rollData.result)/10; // log(modSubtotal, rollResult); Object.assign(rollData, { finalResult: rollResult, tooltip: rollString, warpRoll: (rollData.result % 11 === 0 || rollData.result === 100) ? true : false, crit: rollData.result === 1 ? true : false, fail: rollData.result > 90 ? true : false, }); if (rollData.test) { Object.assign(rollData, { warpRoll: rollData.result % 2 === 0 ? true : false, crit: rollData.result > 65 ? true : false, fail: rollData.result < 34 ? true : false, }); } // log(rollData); return rollData; } const handleInput = async (msg) => { if (/^!psyroll\s/i.test(msg.content)) { const rollInput = await splitMacroData(msg.content); if (!rollInput || !rollInput.power) return helpers.toChat(`${scriptName} error: required input {{power}} not found.`, msg.who); const rollResult = await helpers.sendRoll(rollInput.roll); if (!rollResult || isNaN(rollResult)) return helpers.toChat(`${scriptName} error: bad roll result: ${rollResult}`, msg.who); rollInput.result = rollResult; rollInput.fullRoll = await buildPsyRoll(rollInput); let chatMsg = await buildTemplate(rollInput); chatMsg = chatMsg.replace(/\n/g, ''); helpers.toChat(chatMsg, null, msg.who); } } const helpers = (() => { const sendRoll = async (roll) => new Promise(res => { sendChat('', `${roll}`, v => res(v[0].inlinerolls[0].results.total)) }); const toChat = (content, target, who) => { const prefix = target ? `/w "${target}" ` : ``; sendChat((who || who === '') ? who : scriptName, `${prefix}${content}`); } return { sendRoll, toChat } })(); on('ready', () => on('chat:message', handleInput)); })();