OK, I managed to test the script by putting it in another system, creating the sanity_loss attribute on a couple of characters, and changing all of the references to the "callofcthulhu" template to calls for the "default" template. That let me shake out the bugs and when I went back to a CoC 7E game, it was working. Here is an example output: That pulled the value out of a "2/1d10" attribute value and rolled the 1d10. The script uses !sanloss as the handle. Here are some example command lines: !sanloss > uses selected tokens and outputs the rolled/extracted roll from each to the chat !sanloss @{target|Choose Character|character_id} > uses the character ID of a target to retrieve the value !sanloss Bob the Bold, Sam the Squeamish, Cornelius Bedford > extracts the value of sanity_loss for each of the listed characters and rolls them independently Using just the script's handle (!sanloss) will instruct the script to use the selected tokens. After the handle, however, you can use a comma-delimited list of things that identify a character. This could be a token id (which represents a character), a character name, or a character id. You can obtain those datapoints however you wish -- by hardcoding them into your macro or by using a @{target...} statement. Feeding the script a character identifier in the chat will result in the script ignoring the selected tokens and instead outputting a panel for each of the characters specified. Hopefully this is something like what you were looking for! Here is the code you need: /* ========================================================= Name : SanLoss GitHub : Roll20 Contact : timmaugh Version : 0.0.1 ========================================================= */ var API_Meta = API_Meta || {}; API_Meta.SanLoss = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; { try { throw new Error(''); } catch (e) { API_Meta.SanLoss.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (12)); } } const SanLoss = (() => { const apiproject = 'SanLoss'; API_Meta[apiproject].version = '0.0.1'; const vd = new Date(1643834330912); const versionInfo = () => { //log(`\u0166\u0166 ${apiproject} v${vrs}, ${vd.getFullYear()}/${vd.getMonth() + 1}/${vd.getDate()} \u0166\u0166 -- offset ${API_Meta[apiproject].offset}`); return; }; const logsig = () => { // initialize shared namespace for all signed projects, if needed state.torii = state.torii || {}; // initialize siglogged check, if needed state.torii.siglogged = state.torii.siglogged || false; state.torii.sigtime = state.torii.sigtime || Date.now() - 3001; if (!state.torii.siglogged || Date.now() - state.torii.sigtime > 3000) { const logsig = '\n' + ' _____________________________________________ ' + '\n' + ' )_________________________________________( ' + '\n' + ' )_____________________________________( ' + '\n' + ' ___| |_______________| |___ ' + '\n' + ' |___ _______________ ___| ' + '\n' + ' | | | | ' + '\n' + ' | | | | ' + '\n' + ' | | | | ' + '\n' + ' | | | | ' + '\n' + ' | | | | ' + '\n' + '______________|_|_______________|_|_______________' + '\n' + ' ' + '\n'; log(`${logsig}`); state.torii.siglogged = true; state.torii.sigtime = Date.now(); } return; }; const levenshteinDistance = (a, b) => { if (a.length == 0) return b.length; if (b.length == 0) return a.length; var matrix = []; // increment along the first column of each row var i; for (i = 0; i <= b.length; i++) { matrix[i] = [i]; } // increment each column in the first row var j; for (j = 0; j <= a.length; j++) { matrix[0][j] = j; } // Fill in the rest of the matrix for (i = 1; i <= b.length; i++) { for (j = 1; j <= a.length; j++) { if (b.charAt(i - 1) == a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution Math.min(matrix[i][j - 1] + 1, // insertion matrix[i - 1][j] + 1)); // deletion } } } return matrix[b.length][a.length]; }; const tokenFromAmbig = (idOrName) => { let token = getObj('graphic', idOrName); if (token) { return token; } return findObjs({ type: 'graphic', subtype: 'token' }) .map(t => ({ dist: levenshteinDistance(idOrName, t.get('name')) })) .reduce((m, o) => ((m.dist < o.dist) ? m : o)); }; const charFromAmbig = (info) => { let character; character = findObjs({ type: 'character', id: info })[0] || findObjs({ type: 'character' }).filter(c => c.get('name') === info)[0] || findObjs({ type: 'character', id: (getObj("graphic", info) || { get: () => { return "" } }).get("represents") })[0]; return character; }; const handleInput = (msg) => { let sanrx = /^!sanloss/i; if (msg.type !== "api" || !sanrx.test(msg.content)) return; let tokens; let characters; const sendMessage = (m, c, s) => { sendChat('TheSleeper', (messages[m] || messages['default'])(c,s)); }; const templateMsg = (txt, chr = 'The Sleeper') => { return `/w ${msg.who} &{template:callofcthulhu}{{name=${chr}}}{{title=Sanity Loss}}{{roll=${txt}}}` } const messages = { notoken: () => { return templateMsg('No token specified or selected.') }, nochartoken: () => { return templateMsg('No character for selected tokens.') }, nochar: () => { return templateMsg('No character recognized.') }, unknownchar: () => { return templateMsg('Unrecognized character in command line.') }, nosanityloss: () => { return templateMsg('No die roll recognized in sanity loss attribute.') }, sanloss: (character, sanityloss) => { return templateMsg(`[[${sanityloss}]]`, character) }, default: () => { return templateMsg('What just happened?') } }; let args = msg.content.split(/\s+/); args.shift(); if (!args.length) { // naked call to handle if (!msg.selected || !msg.selected.length || !msg.selected.filter(t => undefined !== t).length) { sendMessage('notoken'); return; } tokens = (msg.selected || []) .map(t => tokenFromAmbig(t._id)) .filter(t => undefined !== t) .filter(t => t.get('represents')); if (!tokens.length) { sendMessage('nochartoken'); return; } characters = tokens.map(t => charFromAmbig(t.get('represents'))); } else { characters = args.join(' ').split(/\s*,\s*/g).map(a => charFromAmbig(a)).filter(c => undefined !== c); } if (!characters.length) { sendMessage('nochar'); return; } characters.forEach(c => { let rollrx = /\/(\d+d\d+)/gi; let sanityloss = getAttrByName(c.id, 'sanity_loss'); if (!rollrx.test(sanityloss)) { sendMessage('nosanityloss'); return; } rollrx.lastIndex = 0 let rolleq = rollrx.exec(sanityloss)[1]; sendMessage('sanloss', c.get('name'), rolleq); }); }; const registerEventHandlers = () => { on("chat:message", handleInput); }; on("ready", () => { logsig(); versionInfo(); registerEventHandlers(); }); return { }; })(); { try { throw new Error(''); } catch (e) { API_Meta.SanLoss.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.SanLoss.offset); } }