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

Snippet for Short Rest - Please Assist

1743401789

Edited 1743402062
case '!short-rest': if(!playerIsGM(msg.playerid)) { sendChat('','/w "'+who+'" '+ '<div style="border: 1px solid black; background-color: white; padding: 3px 3px;">'+ '<span style="font-weight:bold;color:#990000;">Error:</span> '+ 'Only the GM can initiate a short rest.'+ '</div>' ); } else { _.chain(state.UsePower.usedPowers.encounter) .uniq() .map(function(id){ return getObj('ability',id); }) .reject(_.isUndefined) .each(function(a){ a.set({ istokenaction: true }); }); state.UsePower.usedPowers.encounter=[]; } break; I am trying to add portions that include an argument !short-rest --Monday !short-rest --Tuesday etc..assuming characters have an attribute named GroupMonday or GroupTuesday with a 1 or a 0  or a yes no inside the value then, only sheets that have that attribute will have the final command to restore encounter powers. I have messed around with various methods, but I am stuck. Here is an example of what I tried to do; case '!short-rest': if (!playerIsGM(msg.playerid)) { sendChat('','/w "'+who+'" '+ '<div style="border: 1px solid black; background-color: white; padding: 3px 3px;">'+ '<span style="font-weight:bold;color:#990000;">Error:</span> '+ 'Only the GM can initiate a short rest.'+ '</div>' ); } else { // Check if the group flag is included var groupFlag = msg.content.match(/--(Tuesday|Wednesday|Saturday)/); // Look for any of the three groups if (!groupFlag) { sendChat('', '/w "' + who + '" ' + '<div style="border: 1px solid black; background-color: white; padding: 3px 3px;">'+ '<span style="font-weight:bold;color:#990000;">Error:</span> ' + 'Invalid group selected. Please choose either --Tuesday, --Wednesday, or --Saturday.' + '</div>' ); return; // Stop the function if no valid group is specified } var groupChoice = groupFlag[0].substring(2); // Extract group name (Tuesday, Wednesday, or Saturday) var groupAttrName = 'Group' + groupChoice; // The exact attribute name: GroupTuesday, GroupWednesday, GroupSaturday // Find all tokens and check if they belong to the selected group var selectedTokens = findObjs({ _type: 'graphic', _subtype: 'token' }); var encounteredPowersApplied = false; // Track if any powers were applied var noValidTokens = true; // Flag to track if we find any valid tokens _.each(selectedTokens, function(token) { var character = getObj('character', token.get('represents')); if (character) { // Check if the character has the group attribute and if it matches the selected group var groupAttr = getAttrByName(character.id, groupAttrName); // Get the Group attribute based on the groupChoice // Debugging: output group attribute value sendChat('', '/w "' + who + '" Group Attribute: ' + groupAttr + ' (for token ' + token.get('name') + ')'); if (groupAttr && groupAttr.toLowerCase() === 'yes') { // If the token belongs to the selected group, apply short rest _.chain(state.UsePower.usedPowers.encounter) .uniq() .map(function(id) { return getObj('ability', id); }) .reject(_.isUndefined) .each(function(a) { // Set the powers to be token actions a.set({ istokenaction: true }); encounteredPowersApplied = true; }); noValidTokens = false; // Mark that we found at least one valid token } } }); if (noValidTokens) { sendChat('', '/w "' + who + '" ' + '<div style="border: 1px solid black; background-color: white; padding: 3px 3px;">'+ '<span style="font-weight:bold;color:#990000;">Error:</span> ' + 'No valid characters in the selected group to apply the short rest.' + '</div>' ); } else if (encounteredPowersApplied) { sendChat('', '/w "' + who + '" ' + 'The short rest has been applied to the selected group (' + groupChoice + ').' ); } state.UsePower.usedPowers.encounter = []; } break; I know I am probably way off here, but if someone could help me out with this part, I think I can handle the rest of the code in the main body that I am wishing to do similar. IE !long-rest. It seems like the basic idea is doable? 
So I got it to mostly work, but I am experiencing odd behavior. I can use an encounter power on a character with GroupWednesday value of yes, and it will correctly identify which group it is in, and if I use either !short-rest, !short-rest All or !short-rest GroupWednesday  it will reinstate the correct power. . If I use !short-rest GroupTuesday it will correctly skip the player and not return the power to an unused state. However, the oddity that I ran into comes up after this. If I use the power again, it will give the error message, though it will correctly take the power from the list.  In addition, it will continually state each time I use !short-rest GroupWednesday or !short-rest, that its fixing it yet again, even though it's already been fixed. I know its something silly that I missed. Here is the altered code if someone could possibly take a look and see what I have done wrong here. case '!short-rest': if (!playerIsGM(msg.playerid)) { sendChat('', '/w "' + who + '" ' + '<div style="border: 1px solid black; background-color: white; padding: 3px 3px;">' + '<span style="font-weight:bold;color:#990000;">Error:</span> ' + 'Only the GM can initiate a short rest.' + '</div>' ); } else { let args = msg.content.split(/\s+/); // Split command by spaces let groupArg = args[1] ? args[1].toLowerCase() : 'all'; // Default to "all" if no group given let validGroups = ['all', 'grouptuesday', 'groupwednesday', 'groupsaturday']; // Allowed groups if (!validGroups.includes(groupArg)) { sendChat('GM', '<div style="border: 2px solid black; background-color: lightgray; padding: 5px;">' + '<span style="font-weight:bold;color:#990000;">Invalid group name: ' + args[1] + '.</span>' + '</div>' ); break; // Stop if group is invalid } let eligibleCharacters = new Set(); // Prevent duplicates let processedAbilities = new Set(); // Prevent duplicate processing let encounterPowers = state.UsePower.usedPowers.encounter || []; if (!Array.isArray(encounterPowers)) { log("⚠️ Error: state.UsePower.usedPowers.encounter is not an array. Resetting."); state.UsePower.usedPowers.encounter = []; encounterPowers = []; } _.chain(encounterPowers) .uniq() .each(function(id) { if (processedAbilities.has(id)) return; // Skip if already processed processedAbilities.add(id); let ability = getObj('ability', id); if (!ability) return; let character = getObj('character', ability.get('characterid')); if (!character) return; let isEligible = false; if (groupArg === 'all') { // If "all", check all valid groups isEligible = validGroups.slice(1).some(attrName => { let attrValue = getAttrByName(character.id, attrName, 'current') || ''; return attrValue.toLowerCase() === 'yes'; }); } else { // Check only the specified group let attrValue = getAttrByName(character.id, groupArg, 'current') || ''; isEligible = attrValue.toLowerCase() === 'yes'; } if (isEligible) { eligibleCharacters.add(character.get('name')); ability.set({ istokenaction: true }); } }); // Reset encounter powers in a way that avoids corruption state.UsePower.usedPowers.encounter = [...processedAbilities]; // Announce the short rest only if at least one character benefits if (eligibleCharacters.size > 0) { sendChat('GM', '<div style="border: 2px solid black; background-color: lightgray; padding: 5px;">' + '<span style="font-weight:bold;color:#006600;">A short rest has been taken for ' + (groupArg === 'all' ? 'all eligible groups' : groupArg) + '. The following characters benefited: ' + Array.from(eligibleCharacters).join(', ') + '.</span>' + '</div>' ); } else { sendChat('GM', '<div style="border: 2px solid black; background-color: lightgray; padding: 5px;">' + '<span style="font-weight:bold;color:#990000;">No characters in ' + (groupArg === 'all' ? 'any group' : groupArg) + ' were eligible for a short rest.</span>' + '</div>' ); } log("✅ Short Rest Processed: " + groupArg + " | Eligible: " + Array.from(eligibleCharacters).join(', ')); } break;
1743435049

Edited 1743435206
Alright, it looks like I solved my own problem here. This appears to be working now as intended. Here is the updated code to show how I tackled this issue. I'm sure someone could point out how I've done this sloppily, so any suggestions would still be appreciated. The end result here, is that since I have 3 separate groups in my game, on different nights. I wanted to ensure that I can specify which group gets the benifit of a short-rest and which groups are excluded. This was important, because there are times when we pause a game mid-battle and I didn't want one groups short-rest to infringe on another group. So all in all I am pleased with the progress I made, cause I suck at this. case '!short-rest': if (!playerIsGM(msg.playerid)) { sendChat('', '/w "' + who + '" ' + '<div style="border: 1px solid black; background-color: white; padding: 3px 3px;">' + '<span style="font-weight:bold;color:#990000;">Error:</span> ' + 'Only the GM can initiate a short rest.' + '</div>' ); } else { let args = msg.content.split(/\s+/); // Split command by spaces let groupArg = args[1] ? args[1].toLowerCase() : 'all'; // Default to "all" if no group given let validGroups = ['all', 'grouptuesday', 'groupwednesday', 'groupsaturday']; // Allowed groups if (!validGroups.includes(groupArg)) { sendChat('GM', '<div style="border: 2px solid black; background-color: lightgray; padding: 5px;">' + '<span style="font-weight:bold;color:#990000;">Invalid group name: ' + args[1] + '.</span>' + '</div>' ); break; // Stop if group is invalid } let eligibleCharacters = new Set(); // Prevent duplicates let processedAbilities = new Set(); // Prevent duplicate processing let encounterPowers = state.UsePower.usedPowers.encounter || []; if (!Array.isArray(encounterPowers)) { log("⚠️ Error: state.UsePower.usedPowers.encounter is not an array. Resetting."); state.UsePower.usedPowers.encounter = []; encounterPowers = []; } _.chain(encounterPowers) .uniq() .each(function(id) { if (processedAbilities.has(id)) return; // Skip if already processed processedAbilities.add(id); let ability = getObj('ability', id); if (!ability) return; let character = getObj('character', ability.get('characterid')); if (!character) return; let isEligible = false; if (groupArg === 'all') { // If "all", check all valid groups isEligible = validGroups.slice(1).some(attrName => { let attrValue = getAttrByName(character.id, attrName, 'current') || ''; return attrValue.toLowerCase() === 'yes'; }); } else { // Check only the specified group let attrValue = getAttrByName(character.id, groupArg, 'current') || ''; isEligible = attrValue.toLowerCase() === 'yes'; } if (isEligible) { eligibleCharacters.add(character.get('name')); ability.set({ istokenaction: true }); } }); // Reset encounter powers in a way that avoids corruption state.UsePower.usedPowers.encounter = [...processedAbilities]; // Announce the short rest only if at least one character benefits if (eligibleCharacters.size > 0) { sendChat('GM', '<div style="border: 2px solid black; background-color: lightgray; padding: 5px;">' + '<span style="font-weight:bold;color:#006600;">A short rest has been taken for ' + (groupArg === 'all' ? 'all eligible groups' : groupArg) + '. The following characters benefited: ' + Array.from(eligibleCharacters).join(', ') + '.</span>' + '</div>' ); } else { sendChat('GM', '<div style="border: 2px solid black; background-color: lightgray; padding: 5px;">' + '<span style="font-weight:bold;color:#990000;">No characters in ' + (groupArg === 'all' ? 'any group' : groupArg) + ' were eligible for a short rest.</span>' + '</div>' ); } log("✅ Short Rest Processed: " + groupArg + " | Eligible: " + Array.from(eligibleCharacters).join(', ')); } break;
1743435391
timmaugh
Forum Champion
API Scripter
Why are you setting: state.UsePower.usedPowers.encounter = [...processedAbilities]; Wouldn't you want to filter the listed abilities in that property to be those that *don't* match your processAbilities? let processedIDs = processedAbilities.map(a => a.id); state.UsePower.usedPowers.encounter = state.UsePower.usedPowers.encounter.filter(a => !processedIDs.includes(a.id));
1743435907

Edited 1743435935
Because I'm an idiot and I stuck the wrong code in there. The version i fixed is as follows. Thank you for pointing that out Timmaugh case '!short-rest': if (!playerIsGM(msg.playerid)) { sendChat('', '/w "' + who + '" ' + '<div style="border: 1px solid black; background-color: white; padding: 3px 3px;">' + '<span style="font-weight:bold;color:#990000;">Error:</span> ' + 'Only the GM can initiate a short rest.' + '</div>' ); } else { let args = msg.content.split(/\s+/); let groupArg = args[1] ? args[1].toLowerCase() : 'all'; let validGroups = ['all', 'grouptuesday', 'groupwednesday', 'groupsaturday']; if (!validGroups.includes(groupArg)) { sendChat('GM', '<div style="border: 2px solid black; background-color: lightgray; padding: 5px;">' + '<span style="font-weight:bold;color:#990000;">Invalid group name: ' + args[1] + '.</span>' + '</div>' ); break; } let eligibleCharacters = new Set(); let restoredAbilities = new Set(); let encounterPowers = state.UsePower.usedPowers.encounter || []; if (!Array.isArray(encounterPowers)) { log("⚠️ Error: state.UsePower.usedPowers.encounter is not an array. Resetting."); state.UsePower.usedPowers.encounter = []; encounterPowers = []; } _.chain(encounterPowers) .uniq() .each(function(id) { let ability = getObj('ability', id); if (!ability) return; let character = getObj('character', ability.get('characterid')); if (!character) return; let isEligible = false; if (groupArg === 'all') { isEligible = validGroups.slice(1).some(attrName => { let attrValue = getAttrByName(character.id, attrName, 'current') || ''; return attrValue.toLowerCase() === 'yes'; }); } else { let attrValue = getAttrByName(character.id, groupArg, 'current') || ''; isEligible = attrValue.toLowerCase() === 'yes'; } if (isEligible && !restoredAbilities.has(id)) { eligibleCharacters.add(character.get('name')); restoredAbilities.add(id); ability.set({ istokenaction: true }); } }); // Remove only the restored abilities from the encounter list state.UsePower.usedPowers.encounter = encounterPowers.filter(id => !restoredAbilities.has(id)); // Announce the short rest only if at least one character benefits if (eligibleCharacters.size > 0) { sendChat('GM', '<div style="border: 2px solid black; background-color: lightgray; padding: 5px;">' + '<span style="font-weight:bold;color:#006600;">A short rest has been taken for ' + (groupArg === 'all' ? 'all eligible groups' : groupArg) + '. The following characters benefited: ' + Array.from(eligibleCharacters).join(', ') + '.</span>' + '</div>' ); } else { sendChat('GM', '<div style="border: 2px solid black; background-color: lightgray; padding: 5px;">' + '<span style="font-weight:bold;color:#990000;">No characters in ' + (groupArg === 'all' ? 'any group' : groupArg) + ' were eligible for a short rest.</span>' + '</div>' ); } log("✅ Short Rest Processed: " + groupArg + " | Eligible: " + Array.from(eligibleCharacters).join(', ')); } break;
1743439063
timmaugh
Forum Champion
API Scripter
You say you suck at this, but this is not bad work. Does the above code still reflect some error you were trying to chase down? (I might have code efficiencies to offer later, but for right now just trying to make sure whether or not you still have an issue in what you're trying to do...)
1743441222

Edited 1743441331
Thanks for the encouragement Timmaugh the whole thing does seem to be working as intended and I have now expanded this to include !long-rest which restores dailys and is working in the same manner as !short-rest. !short-rest produces this; "Error: No attribute or sheet field found for character_id -OEH4pjOrtauvtgHMa-j named grouptuesday" "Error: No attribute or sheet field found for character_id -OEH4pjOrtauvtgHMa-j named grouptuesday" "✅ Short Rest Processed: all | Eligible: Nyfexes Brimstone" !short-rest GroupWednesday produced no errors and worked as it should. "✅ Short Rest Processed: groupwednesday | Eligible: Nyfexes Brimstone" !short-rest GroupTuesday where Nefexes is NOT part of gives me this error. "Error: No attribute or sheet field found for character_id -OEH4pjOrtauvtgHMa-j named grouptuesday" "✅ Short Rest Processed: grouptuesday | Eligible: " Certainly none of it is game breaking, but since I'd like it to be as clean as possible, any suggestion is welcome.
1743456875
timmaugh
Forum Champion
API Scripter
I think you're getting that because you are sometimes flattening the name of the group to lowercase, but you're not always then working with a restored PascalCase version of the name... you're looking up a lowercase version of the attribute name and not finding it. You can get around that with an object to provide a connection between lowercase versions and the PascalCase you want to use: let attrObj = {     'tuesday': 'GroupTuesday',     'wednesday': 'GroupWednesday',     'saturday': 'GroupSaturday' }; Always lookup against the object always using a .toLowerCase() version, but return the value of that property off the object... as an example: let pascalCaseVersion = attrObj[argProvided.toLowerCase()] || ''; So, to incorporate that (and because I don't like using the Underscore library unless I have to -- I find the native ES6 functions to be cleaner to read), I would replace everything in the   _chain()  operation with something like this, instead: [...new Set(encounterPowers)]     .map(id => {         let abil = getObj('ability', id) || { get: () => '' };         let char = abil.id             ? getObj('character', abil.get('characterid'))             : { get: () => '' };         let elig = char.id             ? findObjs({ type: 'attribute', characterid: char.id })                 .filter(a => (groupArg === 'all' && Object.keys(attrObj).includes(a.get('name').toLowerCase()))                     || a.get('name').toLowerCase() === groupArg)                 .reduce((m, a) => {                     return m || a.get('current').toLowerCase() === 'yes';                 }, false)             : false;         return { abil: abil, char: char, elig: elig };     })     .filter(o => o.elig)     .forEach(o => {         if (!restoredAbilities.has(o.abil.id)) {             eligibleCharacters.add(character.get('name'));             restoredAbilities.add(o.abil.id);             ability.set({ istokenaction: true });         }     }); caveat... that's air-coded, so double check me before using it. Finally, one last efficiency I might suggest is to decompose a messaging function that you can call from wherever you need to provide feedback. That way you don't have to reconstruct the look of the message every time, nor do you have to change the code in a bunch of spots if you later decide you want to update the look. Something like (very simple):  const sendMsg = (msg, sendto = 'gm', sendas = 'GM') => {     sendChat(sendas, `/${sendto} <div style="border: 2px solid black; background-color: lightgray; padding: 5px;">` +         msg +         `</div>`     ); }; ...which you can call like this: sendMsg('This is the message'); ...or... sendMsg('<span style="font-weight:bold;color:#990000;">Invalid group name: ' + args[1] + '.</span>'); Which, you can then see, really begs for a "make this bold" sort of function, maybe in a "style" function library: const style = {     bold: s => `<span style="font-weight:bold;color:#990000;">${s}</span>` }; So that you can simplify your "Invalid group name" message to: sendMsg(style.bold('Invalid group name: ' + args[1])); How far you take that decomposition is up to you, but really makes your code reusable. I've abstracted an entire "Messenger" library that my scripts can use. =D
1743457184
timmaugh
Forum Champion
API Scripter
Oh... is this an Aaron script? Pssh. He'll come up with something better and more wholistic for the overall treatment of the script, so use whatever he comes up with! I thought I was tweaking a script from an absent author. =D