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 .
×
Due to an outage with an external service provider, we’re experiencing intermittent loading issues on Roll20. Please refresh if needed.
Create a free account

food healing macro

Hi, wanted to ask you guys about this macro. Its an idea for allowing food to heal. Would appreciate feedback if anything looks off. Thanks in advnace. ```javascript on('chat:message', function(msg) {     if (msg.type !== 'api') return;         if (msg.content.startsWith('! rest')) {         let players = findObjs({ _type: 'character' });         players.forEach(player => {             let food = findObjs({ _type: 'attribute', _characterid:  player.id , name: 'food' })[0];             let water = findObjs({ _type: 'attribute', _characterid:  player.id , name: 'water' })[0];             let hp = findObjs({ _type: 'attribute', _characterid:  player.id , name: 'hp' })[0];             if (!food || !water || !hp) return;             sendChat('GM', `${player.get('name')}, remember to eat before resting.`);             if (food.get('current') > 0 && water.get('current') > 0) {                 hp.set('current', Math.min(hp.get('max'), hp.get('current') + 5)); // Adjust healing value as needed                 food.set('current', food.get('current') - 1);                 water.set('current', water.get('current') - 1);             } else {                 sendChat('GM', `${player.get('name')} suffers a healing penalty due to lack of sustenance.`);                 hp.set('current', Math.max(0, hp.get('current') - 2)); // Apply penalty             }         });     } }); ```
1746800607
timmaugh
Forum Champion
API Scripter
Hey, Dave... It's a little odd to label character objects you return as "players", and then to use "player" as the element reference in an iteratee. If only for your future sanity, when you might code something that deals with the Roll20 object type of "player", I would label those as "characters", and iterate over them using something like "character", "char", or just "c". Next, you return all characters. Do you need to? Does *every* character in the game need a message... and will *every* character be resting? I think this may benefit from scripting an expected method of interaction. Here is a scripted interaction based on my best understanding of what might be what you're looking for: The player characters (who each have an attribute for each of food, water, and hp), decide to rest. The GM initiates the "rest" process with a command like "!rest" (FIRST MESSAGE TYPE) If the character has both food & water, the player can choose to have that character consume food & water in order to gain hp, so we'll prompt them to click a button to indicate they have opted to eat. Clicking it will send a command. (SECOND MESSAGE TYPE) to make sure they don't multi-click, we'll need a way to know that the rest opportunity has been fulfilled (like a report back to the GM) the GM message might even include the option to "roll-back" the inadvertent multi-click, setting the character's food, water, & hp back to what they should have been. This would be a button sending a command. (THIRD MESSAGE TYPE) If the character lacks food or water, there is no gaining of hp that can be done, so we'll prompt them with a message about their character's growing hunger, and how they gain no hp. Is that something like what you'd like to have? We can get that setup if that's what you want, none of it is very complicated or difficult, but I don't want to waste time if I've misunderstood something.
Hi, thank you for your reply. It seems to me that you have it right. Any further assistance you can give would be very welcome. Thank you.
1747244725

Edited 1747322035
timmaugh
Forum Champion
API Scripter
OK, great. Let's set that up. "Man... and... Wife!" Say, "Man and Wife!" If you don't want to sit through the discussion of why I approached the script this way, you can skip to the TL;DR heading to get the code and usage. In fact, even if you actually ARE interested in the story of how/why I made the design choices I did, I would suggest snagging the code and having it open in your favorite IDE on another monitor so you can see where the various lines are in the code. Structure First, instead of a naked call to the on('chat:message') function, let's put it in an on('ready') call so that we 1) make sure everything is ready, and 2) allow metascripts to function within the call (if you don't know what metascripts are and you have an hour to kill, you can watch this video ... either way, if it comes to a point when you *do* need the metascripts, they'll be there, and your script will be ready). Also, let's use The Aaron's API_Meta trick (discussed in this post under the section "For Scripters:...") to make your debugging life easier. Finally, let's put everything in a closure so that we can have persistent data without having to write to the state. All together, and referring to your script as "Rest" for the purposes of the API_Meta registration, that would look like this: /* */ var API_Meta = API_Meta || {}; API_Meta.Rest= { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; { try { throw new Error(''); } catch (e) { API_Meta.Rest.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (4)); } } const Rest = (() => {   // ...PERSISTENT DATA STRUCTURES CAN GO HERE (SUBJECT TO SANDBOX REBOOT)   // ...ALSO VARIOUS UTILITY FUNCTIONS   on('ready', function()   {     on('chat:message', msg => {     // ... MESSAGE-SPECIFIC ACTIONS WILL GO HERE      });   }); })(); { try { throw new Error(''); } catch (e) { API_Meta.Rest.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.Rest.offset); } } /* */ Note that the  on('chat:message')  is inside the  on('ready') ; that's what lets the metascripts work with your script. Also note that, you could, for ease of reading, construct the chat-event handler as it's own composed function in the main body of the script closure (where I point out the utility functions could go)... const handleInput = msg => { // ...MESSAGE SPECIFIC ACTIONS WILL GO HERE }; ...and then replace the  on('chat:message')  callback with a reference to that function:   on('chat:message', handleInput); That's how I would normally build things, but YMMV. I left it the other way just for a closer link to what you had initially started with. Command Lines The script envisions a handle of  !rest   followed by a handful of allowed arguments (like "open", "close", "recover", etc.; see Usage , below, for a full breakdown). The arguments represent the various interactions with the script our users might require. For instance: a GM will need to "open" the rest period characters will need to choose to consume food/water in order to "recover" characters who are out of food might put a "request" out for other characters to share rations with them other characters might choose to answer that request by "share"-ing the appropriate rations with the requesting character a GM might need to "rollback" a character to their starting values of food, water, and hp a GM will need to "close" the rest period, which imposes the penalty for lack of sustenance (-2hp) Catching the Messages Since the script has to handle all of those, we have to catch them all, so let's impose the rule that a command line will begin with our handle, followed by a series of arguments denoted by double-hyphens following any number of spaces. We'll deal with arguments in a minute. First, directly inside our chat event handler, we'll put this line:             if (!(msg.type === 'api' && /^!rest/i.test(msg.content))) { return; } That's a straightforward use of a regex to test the start of our command line for "!rest". Note that this is a fairly common handle (there are a lot of "Resting" scripts in the repo), so you could get unexpected results if you install another script listening for this handle in your game, as well. Parsing Arguments The script expects arguments to be formatted as the argument name (alone) if it requires no character reference, OR... the argument name followed by a pipe ( | ) or a hash ( # ), followed by a character reference (i.e., a character ID, token ID that represents a character, or a character name) The option to use either the pipe or hash is for flexibility should you need to include a command line in a query (where the pipe would break the query parsing), however for the most part, you will be able to operate the script from the menu panels it provides. In any case, these options lead to formations like: --argument --argument|Character Reference --argument#Character Reference In one instance (when sharing rations), we need to have multiple pieces of information following the argument name. Those parts can be separated by further uses of a pipe or hash (or a mixed use of those characters). --share|Part1|Part2|Part3 --share#Part1#Part2|Part3 This one case (for sharing) will have further parsing done to it, later, but for now, all of our arguments can be reduced to a pair of values: the argument type (ie, "open", "close", "share", etc.) everything else (everything that comes after the first pipe/hash) We can arrive at that with this line:             let args = (msg.content.split(/\s+--/)).slice(1).map(a => [(ret = (/([^|#]+)(?:(?:\||#)(.*))?/.exec(a)))[1].toLowerCase(), ret[2] ? ret[2].trim() : undefined]); The variable  args  will be filled with an array of parsed arguments. Each parsed argument will, itself, be an array of the 2 values just discussed. Since  args  is an array of arguments, we'll need to iterate over it to look at each argument in isolation. That's handled by  forEach  loop a couple lines later:             args.forEach(arg => {               // ...EACH ARG HANDLED HERE // arg[0] is the argument type, and arg[1] is everything else switch(arg[0]) { // ...cases for each argument type }             }); General Functions/Data You might notice that in showing the  forEach  loop line, I skipped over another line:             let chars = getPlayerCharacters(); This is a reference to a function held in the root level of the script closure, so let's look at those variables/functions quickly. In order... Script Name & Version     const scriptName = 'Rest';     API_Meta[scriptName].version = '1.0.0'; The  scriptName  variable is used in the various sendChat function calls as the "sender" of the message. It is also used (in the second line, here) to register the version of the script to the API_Meta object so, later, users can quickly tell what version they have installed. The rest D ata Structure     let rest = {       // ...     }; The  rest  object is a single object that tracks whether the party is currently resting, and, if so, various data that lets the script function (such as what characters are involved and who has already claimed a recovery). The object will be manipulated by the various command lines appropriately (ie, opening, closing, tracking characters, etc.). getPlayerCharacters() (Figuring you can find this in the code, I will not replicate the code here.) We don't need messages to go out to NPCs; we're going to assume the GM knows to take care of them. For our purposes, we only need to get the player characters. We'll define a "player character" (a PC) as a character who has a non-GM player as the first entry in their "controlledby" property. To get that set, we'll need to filter the character returns where the first item listed in that property is not "all" and also fails the playerIsGM() test. getCharacter() A function to return a specific character based on a piece of data and, where necessary, a player ID. The piece of data can be any of a character id, a token id (where the token represents a character), or a character name. In cases where this function is used, if we need to verify controlling rights (for instance, when looking to share rations from one character to another), we can pass in a player ID. If a player ID is not provided, the function assumes we don't need to check controlling rights. For instance, when a GM wants to rollback a character to their starting values (from the start of the rest period), we already test for GM privileges, so we don't need to worry about individual character access. gmRequired() Simple function to send a message if GM rights are required. parseIntOrZero() A  parseInt   wrapper to get an integer even out of values that don't evaluate to a number (those are coerced to be 0). getWhisperTo() Simple function to get a viable recipient from the message's who property, even if the message was sent from a script and even if the sender was a GM. Argument Processing OK, back to our arguments, which we are passing through a  switch   block. We have a handful of recognized arguments: ARG REQ GM EX. USAGE DESCRIPTION open yes --open GM issued command to open the rest period. Gives each character a chance to recover or to request help. close yes --close GM issued command to close the rest period. Imposes a penalty on the characters who did not choose to recover. rollback yes --rollback| CharacterRef GM issued command to set a character's 3 attributes back to starting values. recover no --recover| CharacterRef This character has chosen to consume rations and recover hp. Button only available if the character has adequate rations. request no --request| CharacterRef A character without adequate rations can send a message requesting other characters share with them. share no --share|food| ShareCharRef | RecCharRef A response from characters who have rations to share with the requesting character. These should be easy enough to follow, if you look at the Usage  section, below, to see screenshots. TL;DR Enough with the talking about why the script is written this way. Let's get to the code and the usage! First the usage (with screenshots), then the code. Setup/Prep Minimal setup is required: Install the script in your game Make sure each player character has a non-GM character listed in the "Cand Be Edited & Controlled By" setting Make sure that each PC has attributes for food , water , and hp Usage Begin a Rest As a GM, issue a command to open the rest: !rest --open You can put this on a macro or character ability and put a button on your macro bar. What You See You will see a panel like this: ...showing you what characters are involved in the rest [limited to 1) player characters who 2) have all 3 attributes], their starting values for the various attributes, and the option to roll them back to these starting values. Note that "Rollback" will only roll them back for food/water they have consumed, and hp gained from this recovery; it will NOT  undo sharing between characters. Also note the option to "Close" the rest. You'll choose this option after all PCs have made their decisions on sharing and/or taking recovery. We can see that all the characters involved in the rest have the capacity to recover (they all have at least 1 in Food and 1 in Water) except for Brigald. What your Players See Here is what a character who CAN recover (like Ruby) will see: ...and here is what a character (like Brigald) who CANNOT recover (they lack the required rations) will see: (Note I have multiple dummy accounts; the account controlling Ruby is set to Light mode; the account controlling Brigald is set to Dark mode; the difference in appearance does not owe to the script in any way.) Requesting Help A character like Brigald who lacks rations can request someone else share with them. To do this, that player would click the Request button. When Brigald does that, his controlling player sees: Other players controlling characters WHO HAVE ADEQUATE RATIONS and who are involved in the rest will see: This lets the player decide whether their character has enough to share -- this is important because it IS POSSIBLE for a character to share away their last rations and leave themselves UNABLE to recover, themselves! Also, both Share buttons appear, here, because the requesting character requires both food and water; should only one of them have been needed, the other button would have been omitted. Sharing A player who chooses to have their character share with a requesting character can click the appropriate button for what sort of ration to share. For instance, here is what Ruby sees when she shares food with Brigald: This panel gives Ruby's player an updated status of the appropriate attributes. Meanwhile, Brigald's controlling player sees: (Note that the Share buttons *should* still function even after the rest period is closed by the GM. In this case, the above message might read slightly differently, to reflect that while Brigald received a gift from Ruby, there is no present opportunity to recover.) If another character (or Ruby, again) shares a water ration with Brigald, they will get a similar message to the above message Ruby saw, but Brigald will now see: Now that Brigald has received the necessary rations, the panel includes the button to Recover . Recovering A player who chooses to have their character consume rations and take a recovery can click the Recover button. They will see a panel like this (produced when Brigald took a recovery): As we saw from the first panel (presented to the GM), Brigald was already at full health, so in this case he didn't gain hp. However, the fact that he consumed rations during this rest has been tracked so that he will not be penalized when the GM chooses to close the rest period. (The fact that Brigald was already at full health could play into whether another party member feels generous enough to share rations, especially if they would be left without the opportunity to recover, themselves, after having shared!) Here is what a character (like Ruby) might see if they do recover hp: A character who has shared their rations and, by so doing, eliminated their own chance to recover (having left themselves with less than 1 food and/or less than 1 water), will see a panel like this when they try to recover: Closing the Rest Once all players have made choices for the characters, the GM can click the Close button from the original panel to close the rest/recover opportunity. The GM will see a message reporting what characters took a penalty (of -2hp) for having not chosen to consume rations, followed by a notice that the rest period is closed, and no further recoveries will be allowed: Then all players will see a notice like this: Code This is a long post already! I'll put the code in the next reply...
1747244788

Edited 1747418750
timmaugh
Forum Champion
API Scripter
/* */var API_Meta = API_Meta || {}; var API_Meta = API_Meta || {}; API_Meta.Rest = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; { try { throw new Error(''); } catch (e) { API_Meta.Rest.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (4)); } } const Rest = (() => { const scriptName = 'Rest'; API_Meta[scriptName].version = '1.0.0'; let rest = { isOpen: false, startingValues: {}, // object tracking food, water, and hp for each character involved involved: [], // array of character IDs who can choose to eat/recover claimed: [], // array of character IDs of those who have elected to eat/recover open() { this.close(); this.isOpen = true; }, close() { this.isOpen = false; let report = this.involved.filter(cid => !this.claimed.includes(cid)).reduce((m,cid) => { (hp = findObjs({ type: 'attribute', characterid: cid, name: 'hp' })[0])?.set('current', Math.max(0, parseIntOrZero(hp.get('current')) - 2)); // Apply penalty return [...m, cid]; },[]); this.startingValues = {}; this.involved = []; this.claimed = []; if (report.length) { sendChat(scriptName, `/w gm The following characters suffered a healing penalty due to lack of sustenance: ${report.map(cid => getObj('character', cid).get('name')).join(', ')}`); } } }; const getPlayerCharacters = () => findObjs({ type: 'character' }) .filter(c => (pid = (c.get('controlledby') || '').split(/\s*,\s*/)[0]).length && pid.toLowerCase() !== 'all' && !playerIsGM(pid)); const getCharacter = (q, pid) => { if (typeof q !== 'string') { return; } let someCheck = id => id.toLowerCase() === 'all' || id === pid; const hasAccess = (c) => { if (!c) { return; } return (c.get('controlledby') || '').split(/\s*,\s*/).some(someCheck); }; let query = q.trim(); let character = findObjs({ type: 'character' }).filter(c => [c.id, c.get('name')].includes(query) && (pid ? hasAccess(c) : true))[0]; if (character) { return character; } character = getObj('character', (getObj('graphic', query) || { get: () => { return ''; } }).get('represents')); if (character) { if (!pid || (pid && hasAccess(character))) { return character; } else { return; } } }; const gmRequired = pid => sendchat(scriptName, 'You must be a GM to do that.'); const parseIntOrZero = (val) => isNaN(tempPI = parseInt(val,10)) ? 0 : tempPI; const getWhisperTo = (who) => who.toLowerCase() === 'api' ? 'gm' : who.replace(/\s\(gm\)$/i, ''); on('ready', function () { on('chat:message', msg => { /** * !rest * !rest --[open/close] * !rest --recover|characterid * !rest --rollback|characterid * !rest --request|characterid * !rest --share|[water/food]|characterid-give|characterid-receive */ if (!(msg.type === 'api' && /^!rest/i.test(msg.content))) { return; } let args = (msg.content.split(/\s+--/)).slice(1).map(a => [(ret = (/([^|#]+)(?:(?:\||#)(.*))?/.exec(a)))[1].toLowerCase(), ret[2] ? ret[2].trim() : undefined]); let chars = getPlayerCharacters(); args.forEach(arg => { let food, water, hp, shareAttr, recAttr, char, shareChar; switch (arg[0]) { case 'close': if (!playerIsGM(msg.playerid)) { gmRequired(msg.playerid); return; } if (!rest.isOpen) { return; } rest.close(); sendChat(scriptName, '/w gm Rest period is closed. No further recoveries will be allowed.'); sendChat(scriptName, `&{template:default}{{name=Rest Over}}{{=This opportunity for rest and recovery is now closed. You may still share ` + `rations using existing <b>Request</b> and <b>Share</b> buttons, but there will be no opportunity for recovery until you rest again.}}`); break; case 'recover': char = chars.filter(c => [c.id, c.get('name')].includes(arg[1]))[0]; if (!char) { // no character supplied, or character not found sendChat(scriptName, `/w ${getWhisperTo(msg.who)} &{template:default}{{name=No Character}}` + `{{=You must supply a character who is involved in this rest.}}`); return; } if (!rest.involved.includes(arg[1])) { // character not involved in rest sendChat(scriptName, `/w ${getWhisperTo(msg.who)} &{template:default}{{name=Character Not Resting}}` + `{{=You must supply a character who is involved in this rest.}}`); return; } if (rest.claimed.includes(arg[1])) { // recovery already claimed sendChat(scriptName, `/w ${getWhisperTo(msg.who)} &{template:default}{{name=${char.get('name')} Already Recovered}}` + `{{=This character already recovered during this rest.}}`); return; } if (!rest.isOpen) { // rest was closed by GM sendChat(scriptName, `/w ${char.get('name')} &{template:default} {{name=No Recovery Available}}` + `{{=The opportunity to recover during this rest has passed.}}`); return; } food = findObjs({ type: 'attribute', characterid: char.id, name: 'food' })[0]; water = findObjs({ type: 'attribute', characterid: char.id, name: 'water' })[0]; hp = findObjs({ type: 'attribute', characterid: char.id, name: 'hp' })[0]; if (parseIntOrZero(food.get('current')) < 1 || parseIntOrZero(water.get('current')) < 1) { sendChat(scriptName, `/w ${char.get('name')} &{template:default}{{name=Not Enough Rations}}` + `{{=You lack the necessary rations to recover. Most likely you shared with a party memeber.}}`); return; } let currentHP = hp?.get('current'); let adjustedHP = Math.min(parseIntOrZero(hp?.get('max')), parseIntOrZero(hp?.get('current')) + 5); // Adjust healing value as needed hp?.set('current', adjustedHP); food?.set('current', parseIntOrZero(food.get('current')) - 1); water?.set('current', parseIntOrZero(water.get('current')) - 1); rest.claimed.push(char.id); sendChat(scriptName, `/w ${char.get('name')} &{template:default}{{name=Recovered}} {{=You rested and recovered ${adjustedHP - currentHP}hp ` + `to be at ${adjustedHP}hp. ${`${adjustedHP}` === `${hp?.get('max')}` ? 'You are at full health.' : ''} }}`); break; case 'request': char = getCharacter(arg[1], msg.playerid); if (!char) { return; } let needFood = parseIntOrZero(findObjs({ type: 'attribute', characterid: char.id, name: 'food' })[0]?.get('current')) < 1; let needWater = parseIntOrZero(findObjs({ type: 'attribute', characterid: char.id, name: 'water' })[0]?.get('current')) < 1; (rest.isOpen ? rest.involved : chars.map(c => c.id)).filter(cid => cid !== arg[1]).forEach(cid => { shareChar = getObj('character', cid); food = parseIntOrZero(findObjs({ type: 'attribute', characterid: shareChar.id, name: 'food' })[0]?.get('current')); water = parseIntOrZero(findObjs({ type: 'attribute', characterid: shareChar.id, name: 'water' })[0]?.get('current')); hp = `${(tempAttr = findObjs({ type: 'attribute', characterid: shareChar.id, name: 'hp' })[0]).get('current')} / ${tempAttr.get('max')}`; let foodRequest = needFood ? `{{ =<div style="text-align:right"><a href="!
!rest --share|food|${shareChar.id}|${char.id}">Share Food</a></div>}}` : ''; let waterRequest = needWater ? `{{ =<div style="text-align:right"><a href="!
!rest --share|water|${shareChar.id}|${char.id}">Share Water</a></div>}}` : ''; if ((food < 1 && water < 1) || (needFood && !needWater && food < 1) || (needWater && !needFood && water < 1)) { return; } sendChat(scriptName, `/w ${shareChar.get('name')} &{template:default}{{name=Sharing Request}}{{=${char.get('name')} lacks ` + `adequate rations to recover and is asking for help. Will you share?%NEWLINE%%NEWLINE%**YOUR STATUS:**<ul>` + `<li><b>Food</b>: ${food}</li>` + `<li><b>Water</b>: ${water}</li>` + `<li><b>HP</b>: ${hp}</li>` + `</ul>}}` + `${foodRequest}${waterRequest}`); }); sendChat(scriptName, `/w ${char.get('name')} &{template:default}{{name=Request Made}}{{=Your request has been made.}}`); break; case 'share': let parts = arg[1].split(/(?:\||#)/); let shareType = ['food', 'water'].includes(parts[0].toLowerCase()) ? parts[0].toLowerCase() : undefined; shareChar = getCharacter(parts[1], msg.playerid); char = getCharacter(parts[2]); if (!shareType) { sendChat(scriptName, `/w ${getWhisperTo(msg.who)} &{template:default}{{name=Invalid Share Type}}{{=You must include either ` + `'food' or 'water' as the thing to share.}}`); return; } else if (!shareChar || !char) { sendChat(scriptName, `/w ${getWhisperTo(msg.who)} &{template:default}{{name=Can't Find Characters}}{{=Could not find one or both ` + `of the characters required based on the information you supplied.}}`); return; } // if we're still here, then this character has chosen to share // sharing character reduction shareAttr = findObjs({ type: 'attribute', characterid: shareChar.id, name: shareType })[0]; if (shareAttr && parseIntOrZero(shareAttr.get('current')) >= 1) { shareAttr.set('current', parseIntOrZero(shareAttr.get('current')) - 1); if (rest.isOpen) { rest.startingValues[shareChar.id][shareType] = rest.startingValues[shareChar.id][shareType] - 1; } // receiving character adjustment recAttr = findObjs({ type: 'attribute', characterid: char.id, name: shareType })[0]; if (recAttr) { recAttr.set('current', parseIntOrZero(recAttr.get('current')) + 1); if (rest.isOpen) { rest.startingValues[char.id][shareType] = rest.startingValues[char.id][shareType] + 1; } } // report to sharing character food = rest.isOpen ? rest.startingValues[shareChar.id].food : parseIntOrZero(findObjs({ type: 'attribute', characterid: shareChar.id, name: 'food' })[0].get('current')) ; water = rest.isOpen ? rest.startingValues[shareChar.id].water : parseIntOrZero(findObjs({ type: 'attribute', characterid: shareChar.id, name: 'water' })[0].get('current')) ; hp = `${(tempAttr = findObjs({ type: 'attribute', characterid: shareChar.id, name: 'hp' })[0]).get('current')} / ${tempAttr.get('max')}`; sendChat(scriptName, `/w ${shareChar.get('name')} &{template:default}{{name=Success}}{{=You shared with ${char.get('name')}. ` + `Don't ever let them forget it.%NEWLINE%%NEWLINE%**STATUS:**<ul>` + `<li><b>Food</b>: ${food}</li>` + `<li><b>Water</b>: ${water}</li>` + `<li><b>HP</b>: ${hp}</li>` + `</ul>}}`); // report to receiving character food = rest.isOpen ? rest.startingValues[char.id].food : parseIntOrZero(findObjs({ type: 'attribute', characterid: char.id, name: 'food' })[0].get('current')) ; water = rest.isOpen ? rest.startingValues[char.id].water : parseIntOrZero(findObjs({ type: 'attribute', characterid: char.id, name: 'water' })[0].get('current')) ; hp = `${(tempAttr = findObjs({ type: 'attribute', characterid: char.id, name: 'hp' })[0]).get('current')} / ${tempAttr.get('max')}`; let lackingRation = food >= 1 && water >= 1 ? '' : food < 1 ? 'food' : 'water' ; let status = `%NEWLINE%%NEWLINE%**STATUS:**<ul>` + `<li><b>Food</b>: ${food}</li>` + `<li><b>Water</b>: ${water}</li>` + `<li><b>HP</b>: ${hp}</li>` + `</ul>`; let nextAction = rest.isOpen ? lackingRation.length ? `You still lack ${lackingRation} rations before you can recover.${status}}}` : `You may now recover.${status}}}{{ =<div style="text-align:right"><a href="!
!rest --recover|${char.id}">Recover</a></div>}}` : lackingRation.length ? `This rest period is closed, but you still lack a ${lackingRation} before you can recover in the next rest period.${status}}}` : `This rest period is closed, but you now have sufficient rations to recover the next time you are resting.${status}}}` ; sendChat(scriptName, `/w ${char.get('name')} &{template:default}{{name=You Have Received a Gift}}{{=${shareChar.get('name')} ` + `shared ${shareType} with you. ${nextAction}`); } else { food = parseIntOrZero(findObjs({ type: 'attribute', characterid: shareChar.id, name: 'food' })[0].get('current')); water = parseIntOrZero(findObjs({ type: 'attribute', characterid: shareChar.id, name: 'water' })[0].get('current')); hp = `${(tempAttr = findObjs({ type: 'attribute', characterid: shareChar.id, name: 'hp' })[0]).get('current')} / ${tempAttr.get('max')}`; sendChat(scriptName, `/w ${getWhisperTo(msg.who)} &{template:default}{{name=Unable to Share}}{{=You no longer have enough ${shareType} ` + `to share. It is possible you recovered and consumed your last ration, or that you already shared it.` + `%NEWLINE%%NEWLINE%**STATUS:**<ul>` + `<li><b>Food</b>: ${food}</li>` + `<li><b>Water</b>: ${water}</li>` + `<li><b>HP</b>: ${hp}</li>` + `</ul>` + `}}`); return; } break; case 'rollback': if (!playerIsGM(msg.playerid)) { gmRequired(msg.playerid); return; } char = getCharacter(arg[1]); if (rest.startingValues.hasOwnProperty(char.id)) { findObjs({ type: 'attribute', characterid: char.id, name: 'food' })[0]?.set('current', rest.startingValues[char.id].food); findObjs({ type: 'attribute', characterid: char.id, name: 'water' })[0]?.set('current', rest.startingValues[char.id].water); findObjs({ type: 'attribute', characterid: char.id, name: 'hp' })[0]?.set('current', rest.startingValues[char.id].hp); rest.claimed = rest.claimed.filter(cid => cid !== char.id); sendChat(scriptName, `/w gm Successfully rolled back that character. Their **[Recovery]** button should work again.`); } break; case 'open': if (!playerIsGM(msg.playerid)) { gmRequired(msg.playerid); return; } rest.open(); if (!chars.length) { sendChat(scriptName, '/w gm No player characters found.'); return; } rest = { ...rest, ...chars.reduce((m, c) => { let foodAttr = findObjs({ type: 'attribute', characterid: c.id, name: 'food' })[0]; let waterAttr = findObjs({ type: 'attribute', characterid: c.id, name: 'water' })[0]; let hpAttr = findObjs({ type: 'attribute', characterid: c.id, name: 'hp' })[0]; if (!hpAttr || !waterAttr || !foodAttr) { // character is missing attributes sendChat(scriptName, `/w gm ${c.get('name')} does not have all three attributes (hp, food, water) required ` + `to take part in the Rest script. They will be excluded until you correct that situation, then open the ` + `rest period again.`); } else { food = parseIntOrZero(foodAttr.get('current')); water = parseIntOrZero(waterAttr.get('current')); hp = parseIntOrZero(hpAttr.get('current')); m.involved.push(c.id); m.startingValues = { ...m.startingValues, [c.id]: { hp, food, water } }; } return m; }, rest) }; let menuParts = chars.map(c => { food = rest.startingValues[c.id].food; water = rest.startingValues[c.id].water; hp = `${(tempAttr = findObjs({ type: 'attribute', characterid: c.id, name: 'hp' })[0]).get('current')} / ${tempAttr.get('max')}`; if (rest.involved.includes(c.id)) { if (food < 1 || water < 1) { sendChat(scriptName, `/w ${c.get('name')} &{template:default} {{name=${c.get('name')} No Recovery Opportunity}}` + `{{=You are resting, but you lack the necessary food and water in order to recover HP and/or ` + `avoid taking damage from a lack of sustenance. Do you wish to request help?%NEWLINE%%NEWLINE%**STATUS:**<ul>` + `<li><b>Food</b>: ${food}</li>` + `<li><b>Water</b>: ${water}</li>` + `<li><b>HP</b>: ${hp}</li>` + `</ul>}}` + `{{ =<div style="text-align:right"><a href="!
!rest --request|${c.id}">Request</a></div>}}`); } else { sendChat(scriptName, `/w ${c.get('name')} &{template:default} {{name=${c.get('name')} Recovery Opportunity}}` + `{{=You are resting. Do you wish to consume food and water rations in order to gain HP?%NEWLINE%%NEWLINE%**STATUS:**<ul>` + `<li><b>Food</b>: ${food}</li>` + `<li><b>Water</b>: ${water}</li>` + `<li><b>HP</b>: ${hp}</li>` + `</ul>}}` + `{{ =<div style="text-align:right"><a href="!
!rest --recover|${c.id}">Recover</a></div>}}`); } return `{{${c.get('name')}%NEWLINE%` + `F: ${food}, W: ${water}, HP: ${hp}=` + `<a href="!
!rest --rollback|${c.id}">Rollback</a>}}`; } }); sendChat(scriptName, `/w gm &{template:default}{{name=Rest Opened}}{{Rest period has opened, and the characters have received their messages. ` + `Use this menu to rollback any character who requires it, or to close the rest period.}} ${menuParts.join('')} ` + `{{Close Rest Period=<a href="!
!rest --close">Close</a>}}`); default: } }); }); }); })(); { try { throw new Error(''); } catch (e) { API_Meta.Rest.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.Rest.offset); } } /* */ .
timmaugh  that is amazing. Thank you so much!
1747418803
timmaugh
Forum Champion
API Scripter
No problem! BTW, I caught two bugs* in the code, above, so I edited it. * Bug 1 : the getCharacter function tested your access to the character if it found the character by testing the name or id, but not if you supplied a token ID...which it should have since you can get a token ID just by @target syntax... and Bug 2 : a sharing player didn't get a resulting message if they tried to share but didn't have enough resources (ie, if they recovered using their last food ration, then tried to share food). They will now get a message about it. Neither bug was a real game changer, but, I figured I should update it for completeness/accuracy sake.
Thank you