Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

CoC 7th Edition - Sanity Loss Macro

Hi folks, I'm looking for a way to have Sanity Loss rolled via a macro. Currently, Sanity Loss is captured as a value like "1/1D8" in the official sheet based on whether the investigator successfully rolls v Sanity or not. Is there a way to grab the 1D8 and make it a clickable entry somehow? Ideally as a macro that could pull the die value from the selected token for the monster in question. e.g. a Walter Corbitt which has a Sanity loss value of "  SANITY LOSS 1/1d8 to see him move  " in the character sheet. Ideally I don't want to roll a 1d8 outside of the &{template:callofcthulhu} format. So any ideas? Perhaps even taking it further... I'd like the macro to prompt for the 'target' monster and then pull the Sanity Loss die roll from the character sheet linked to the token I click on as "target". Thoughts? Of course if this is already built in to the 7e character sheet and I'm missing it please point that out to me! Cheers, Dave
Any ideas folks? I'm running Cthulhu on Thursday, so ideally if someone knows how to do this that'd be great.
1643806509
timmaugh
Forum Champion
API Scripter
It's easy enough to write a little script that would regex that field and drop a templated macro statement into an attribute, ability, or macro. I'm running into a couple of problems though, not knowing the system or the sheet. I can't get the sanity_loss attribute to auto-populate with a value, so I'm not sure I'm working with the right attribute... and... There isn't enough information on the wiki about using the parts of the roll template to build your own roll. I think the version of the template you want would look something like when you roll bonus, but that apparently is running an ability on the sheet that I can't see, so I can't get the verbiage of the syntax. If you can confirm (1) the attribute name that contains the information you want parsed, and (2) what the resulting template statement would look like, I can probably connect the dots for you real quick with a little scriptlet.
Thanks Tim. So the field is called sanity_loss but the way it's populated on the sheets is 1/1D8 meaning that if they pass a Sanity check they lose 1 Sanity but if they fail they roll 1D8 and lose that much Sanity. &{template:callofcthulhu}{{name=@{selected|token_name}}}{{title=Sanity Loss}}{{roll=[[@{selected|sanity_loss}]]}} gives me an output that is the same as /roll 1/1D8 but templated. I want it to pull the 1D8 bit only.
1643838286
timmaugh
Forum Champion
API Scripter
So, I have a solution to this put together, but I think the last Roll20 update borked the CoC7E sheet, so I'm struggling with testing it. Since you're using the same sheet, see if you can verify: if you look at your script log console, do you see an "Unexpected token '.'" error?
1643866287

Edited 1643892951
timmaugh
Forum Champion
API Scripter
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); } }
You're an absolute star Tim, that works a treat!