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

Please help me fix an API for token saving throws

1745479858

Edited 1745479936
So to start with, the script is supposed to allow me to shift-click to select multiple tokens then have them all roll a saving throw against a dc and type by putting this command in chat: !savevsdc [Savetype] [DC] Ive been testing it with 1 Barlgura, 1 Unicorn, 3 Quaggoths, and 1 Water Weird. Its perfectly formatted as I want it to be, but the bonus to the rolls only uses the ability score modifier despite the save I tested it with the Barlgura was proficient. (Also, I know the Unicorn is supposed to have advantage on the save but honestly Im just gonna be happy if it can show the correct bonus). Im using 5e, Jumpgate, and I set the API sandbox to Experimental if any of that makes a difference. Please help me fix the bonuses if possible. Anyways here is the script: on('ready', function () {   on('chat:message', function (msg) {     if (msg.type === 'api' && msg.content.startsWith('!savevsdc')) {       const args = msg.content.split(' ');       const saveType = args[1];       const dc = parseInt(args[2]);       if (!saveType || isNaN(dc)) {         sendChat('System', 'Usage: !savevsdc [SaveType] [DC]');         return;       }       if (!msg.selected || msg.selected.length === 0) {         sendChat('System', 'Please select one or more tokens.');         return;       }       const results = {};       const lowerSaveType = saveType.toLowerCase();       _.each(msg.selected, function (sel) {         const token = getObj('graphic', sel._id);         if (!token) return;         const characterId = token.get('represents');         if (!characterId) return;         const name = token.get('name') || 'Unknown Creature';         const attr = findObjs({           type: 'attribute',           characterid: characterId,           name: `${lowerSaveType}_save_bonus`         })[0];         const bonus = parseInt(attr?.get('current')) || 0;         const roll = randomInteger(20);         const total = roll + bonus;         const success = total >= dc;         if (!results[name]) results[name] = [];         results[name].push({ roll, bonus, total, success });       });       let output = `<div style="text-align:center;"><b>${saveType.toUpperCase()} - DC ${dc}</b></div><br><br>`;       for (let name in results) {         const rolls = results[name];         let successCount = 0;         let failCount = 0;         output += `<div style="text-align:center;"><b><span style="color:gold;">${name}</span></b></div><br>`;         output += `<table style="border-collapse: collapse; margin-left: auto; margin-right: auto;">`;         output += `<tr><th style="border: 1px solid black; padding: 4px;">Roll</th><th style="border: 1px solid black; padding: 4px;">Bonus</th><th style="border: 1px solid black; padding: 4px;">Total</th><th style="border: 1px solid black; padding: 4px;">Result</th></tr>`;         rolls.forEach(r => {           const resultText = r.success ? '<span style="color:green;"><b>Success</b></span>' : '<span style="color:red;"><b>Failure</b></span>';           if (r.success) successCount++; else failCount++;           output += `<tr><td style="border: 1px solid black; padding: 4px; text-align: center;">${r.roll}</td><td style="border: 1px solid black; padding: 4px; text-align: center;">${r.bonus}</td><td style="border: 1px solid black; padding: 4px; text-align: center;">${r.total}</td><td style="border: 1px solid black; padding: 4px; text-align: center;">${resultText}</td></tr>`;         });         output += `</table><br>`;         output += `<table style="border-collapse: collapse; margin-left: auto; margin-right: auto;">`;         output += `<tr><th style="border: 1px solid black; padding: 4px;">Total Successes</th><th style="border: 1px solid black; padding: 4px;">Total Failures</th></tr>`;         output += `<tr><td style="border: 1px solid black; padding: 4px; text-align: center;">${successCount}</td><td style="border: 1px solid black; padding: 4px; text-align: center;">${failCount}</td></tr>`;         output += `</table><br><br>`;       }       sendChat('Saving Throws', output);     }   }); });
1745496269
timmaugh
Forum Champion
API Scripter
I'm not sure what this means: but the bonus to the rolls only uses the ability score modifier despite the save I tested it with the Barlgura was proficient What would it mean for the B to be proficient? Where does that show up on the sheet? Also, separate question, what would be the sheet indicator that a character would have advantage? Because if you can isolate that, you can account for individual token/characters rolling with advantage in the code. That's an easy fix if you can designate the attribute that would carry that information (or were willing to somehow tag the character). Prior to getting the answer to either of the above questions, I would look at this line: const bonus = parseInt(attr?.get('current')) || 0; I'm not sure what would be in the attribute, but this code is expecting a number since it is trying to coerce an integer from it. I only mention that because if you think the attribute should contain a reference to some other modifier (or other text), then that would be lost in the parseInt() function. Also, what might be going on, here, is that attributes exist in firebase until they are given an explicit value. Sometimes a calculated value can show in the sheet display, but that value isn't recorded in firebase because it has never changed or been applied. So I would wonder if you're not getting a bonus back from this line of code because the null coalescing operator is always giving you the 0. To test this, I would add the following logging right before this line: log(`===== Logging for ${character.get('name')} (token: ${token.id}) =====`); log(`ATTR FOUND: ${!!attr}`); log(`ATTR VALUE: ${attr ? attr.get('current') : 'NA'}`); log(`AFTER pINT: ${parseInt(attr?.get('current')) || 0}`); Then run your command and see what you're getting back. But we might be able to give better advice if you can answer the questions I posed, above.
1745518786

Edited 1745520063
timmaugh said: I'm not sure what this means: but the bonus to the rolls only uses the ability score modifier despite the save I tested it with the Barlgura was proficient What would it mean for the B to be proficient? Where does that show up on the sheet? Also, separate question, what would be the sheet indicator that a character would have advantage? Because if you can isolate that, you can account for individual token/characters rolling with advantage in the code. That's an easy fix if you can designate the attribute that would carry that information (or were willing to somehow tag the character). Prior to getting the answer to either of the above questions, I would look at this line: const bonus = parseInt(attr?.get('current')) || 0; I'm not sure what would be in the attribute, but this code is expecting a number since it is trying to coerce an integer from it. I only mention that because if you think the attribute should contain a reference to some other modifier (or other text), then that would be lost in the parseInt() function. Also, what might be going on, here, is that attributes exist in firebase until they are given an explicit value. Sometimes a calculated value can show in the sheet display, but that value isn't recorded in firebase because it has never changed or been applied. So I would wonder if you're not getting a bonus back from this line of code because the null coalescing operator is always giving you the 0. To test this, I would add the following logging right before this line: log(`===== Logging for ${character.get('name')} (token: ${token.id}) =====`); log(`ATTR FOUND: ${!!attr}`); log(`ATTR VALUE: ${attr ? attr.get('current') : 'NA'}`); log(`AFTER pINT: ${parseInt(attr?.get('current')) || 0}`); Then run your command and see what you're getting back. But we might be able to give better advice if you can answer the questions I posed, above. So before I answer the questions I input the recommended changes and got back this in the chatlog: (It is at this moment that I am realizing how painful yellow was as a choice when opening up the entire chatlog) I got back this in the Mod Output Console: "===== Logging for Barlgura (token: -OO_sOXYB5rOHlaWxbrd) =====" "ATTR FOUND: true" "ATTR VALUE: 2" "AFTER pINT: 2" "===== Logging for Quaggoth (token: -OO_nDXntpMgKDg440E-) =====" "ATTR FOUND: true" "ATTR VALUE: 1" "AFTER pINT: 1" "===== Logging for Unicorn (token: -OO_nGSRV3xANwY2dcHE) =====" "ATTR FOUND: true" "ATTR VALUE: 2" "AFTER pINT: 2" "===== Logging for Quaggoth (token: -OO_nIgD_QqFQ3TZZzYK) =====" "ATTR FOUND: true" "ATTR VALUE: 1" "AFTER pINT: 1" "===== Logging for Water Weird (token: -OO_nHzjYw2POt8DrfwC) =====" "ATTR FOUND: true" "ATTR VALUE: 3" "AFTER pINT: 3" "===== Logging for Quaggoth (token: -OO_nD-FdfRRfiAkRiGl) =====" "ATTR FOUND: true" "ATTR VALUE: 1" "AFTER pINT: 1" And to answer the questions. Firstly, Im realizing that advantage was never going to work because I was looking at traits in the statsheets like Magic Resistance which gives advantage on saving throws against spells and magical effects but I dont see how that could work, but hey Im dumb and very very inexperienced at any of this. But having it roll advantage for just the unicorn while the others roll normally based on that trait seems like it would overcomplicate things if even be possible.  Second, the bonus doesnt show proficiency or have an attribute determining proficiency bonus and if its applied AFAIK, but what it does have is a separate bonus to the roll. So Barlgura making a dexterity saving throw has a DEX score of 15 giving it a +2 modifier to dexterity checks and saving throws it isnt proficient in. However, given its proficiency bonus is 3 for its CR, and its proficient in dexterity saving throws, it should be given a +5 to its dexterity saving throw. In the attributes I found that 2 attributes show the correct number as 5 for this save type and they are "npc_dex_save_base" and "npc_dex_save". My goal is to have the API check those two attributes and if it finds a number higher than the ability score modifier for the saving throw type it will add that as the bonus, and if it doesnt it will use the attribute for [savetype]_save_bonus or [savetype]_mod which should show the ability score modifier. I feel like im being very confusing so im trying to be very thorough so using the example of Barlgura rolling a dexterity saving throw: npc_dex_save_base = 5 npc_dex_save = 5 dexterity_mod = 2 dexterity_save_bonus = 2 edit: so after typing this out I realize I am an idiot and the issue is in "dex" vs "dexterity" edit2: works as intended, if anyone wants the api here it is: on('ready', function () {   on('chat:message', function (msg) {     if (msg.type === 'api' && msg.content.startsWith('!savevsdc')) {       const args = msg.content.split(' ');       const inputType = args[1]?.toLowerCase();       const dc = parseInt(args[2]);       if (!inputType || isNaN(dc)) {         sendChat('System', 'Usage: !savevsdc [SaveType] [DC]');         return;       }       if (!msg.selected || msg.selected.length === 0) {         sendChat('System', 'Please select one or more tokens.');         return;       }       const shorthandMap = {         str: 'strength',         dex: 'dexterity',         con: 'constitution',         int: 'intelligence',         wis: 'wisdom',         cha: 'charisma'       };       const shorthand = inputType.length === 3 ? inputType : Object.keys(shorthandMap).find(k => shorthandMap[k] === inputType);       const fullname = shorthandMap[shorthand] || inputType;       function getBonus(characterId, shortKey, fullKey) {         const attrNames = [           [`npc_${shortKey}_save`, `npc_${fullKey}_save`],           [`npc_${shortKey}_save_base`, `npc_${fullKey}_save_base`],           [`${shortKey}_save_bonus`, `${fullKey}_save_bonus`],           [`${shortKey}_mod`, `${fullKey}_mod`]         ];         for (let [shortAttr, fullAttr] of attrNames) {           let attr = findObjs({ type: 'attribute', characterid: characterId, name: shortAttr })[0];           let val = parseInt(attr?.get('current'));           if (!isNaN(val) && val !== 0) return val;           attr = findObjs({ type: 'attribute', characterid: characterId, name: fullAttr })[0];           val = parseInt(attr?.get('current'));           if (!isNaN(val)) return val;         }         return 0;       }       const results = {};       _.each(msg.selected, function (sel) {         const token = getObj('graphic', sel._id);         if (!token) return;         const characterId = token.get('represents');         if (!characterId) return;         const name = token.get('name') || 'Unknown Creature';         const bonus = getBonus(characterId, shorthand, fullname);         const roll = randomInteger(20);         const total = roll + bonus;         const success = total >= dc;         if (!results[name]) results[name] = [];         results[name].push({ roll, bonus, total, success });       });       let output = `<div style="text-align:center;"><b>${fullname.toUpperCase()} - DC ${dc}</b></div><br><br>`;       for (let name in results) {         const rolls = results[name];         let successCount = 0;         let failCount = 0;         output += `<div style="text-align:center;"><b><span style="color:gold;">${name}</span></b></div><br>`;         output += `<table style="border-collapse: collapse; margin-left: auto; margin-right: auto;">`;         output += `<tr><th style="border: 1px solid black; padding: 4px;">Roll</th><th style="border: 1px solid black; padding: 4px;">Bonus</th><th style="border: 1px solid black; padding: 4px;">Total</th><th style="border: 1px solid black; padding: 4px;">Result</th></tr>`;         rolls.forEach(r => {           const resultText = r.success ? '<span style="color:green;"><b>Success</b></span>' : '<span style="color:red;"><b>Failure</b></span>';           if (r.success) successCount++; else failCount++;           output += `<tr><td style="border: 1px solid black; padding: 4px; text-align: center;">${r.roll}</td><td style="border: 1px solid black; padding: 4px; text-align: center;">${r.bonus}</td><td style="border: 1px solid black; padding: 4px; text-align: center;">${r.total}</td><td style="border: 1px solid black; padding: 4px; text-align: center;">${resultText}</td></tr>`;         });         output += `</table><br>`;         output += `<table style="border-collapse: collapse; margin-left: auto; margin-right: auto;">`;         output += `<tr><th style="border: 1px solid black; padding: 4px;">Total Successes</th><th style="border: 1px solid black; padding: 4px;">Total Failures</th></tr>`;         output += `<tr><td style="border: 1px solid black; padding: 4px; text-align: center;">${successCount}</td><td style="border: 1px solid black; padding: 4px; text-align: center;">${failCount}</td></tr>`;         output += `</table><br><br>`;       }       sendChat('Saving Throws', output);     }   }); });