Gauss said: Nothing I've seen will do that, but one emergent property you might be able to leverage is that when ChatSetAttr does a calculation, it is set into the Chat Menu button. So hitting the button multiple times will give the same answer and make the same change every time. Example: We see: [Click me to deduct 5 points](!setattr --name @{character_name} --attribute|[[@{attribute}-5]]) Assume the attribute has 15 points, Roll20 processes it then ChatSetAttr sees: [Click me to deduct 5 points](!setattr --name @{character_name} --attribute|10) Every time the user pushes the button it will set the attribute to 10 points. I assume you are using Scriptcards? If so there may be some equivalent method there. Also, if using Scriptcards I'd ask the folks on the Scriptcards Discord server. All my chattattrb things are associated with a roll query. :-) Every time you click one of the coin buttons, you are asked for an input,, so I think I'm safe there. I do need to comment out that debug statement, though. This screen shot shows the aftermath of subtracting 2 copper pieces from a character. Though, of course, you can use the up arrow on the chat menu and repeat what was done previously, though it will still run the roll query to ask how much to do. No sciptcards for that, though I have a proc from daemonslye who helped me with an attack procedure on Macro a few months ago when I was relearning. The code for the above is not super long. TimGlobal functions ( I think there's only two) are for confirming a character is selected and for selecting character's selected colour (like the purple colour above). It's probably a dirty big mess, but here it is FWIW if you're interested. on('ready', () => { //Version information const ver = "Version 1.1" const verdate = "14 June 2026" //The following will be outputted after a restart sandbox or save: log("═╣ MyCoins.js !mycoins Tim, " + ver + ", " + verdate + " ╠═"); on("chat:message", function(msg) { if (msg.type !== "api") return; let chatcommand = msg.content.trim().toLowerCase(); if (!chatcommand.startsWith("!mycoins")) return; //log(chatcommand); let charID = TimGlobals.validatechar(msg);//Verify character is selected if (charID === "0") {return;} //OK, carry on! log (charID); let usercol = getAttrByName(charID,"color_option") || ""; let userhex = TimGlobals.hexColour(usercol); //ROTTO's CODE FOR panel AND btn //Altered background colour to #F1ECE5 by playing around on Affinity Photo 2. /* const panel = (usercolour, title, body) => '<div style="background:' + usercolour + ';border:1px solid #5b4632;border-radius:10px;padding:2px;max-width:760px;color:#f7f1e6;box-shadow:0 2px 6px rgba(0,0,0,0.35);">' + '<div style="font-size:22px;font-weight:bold;text-align:center;margin:0 0 10px 0;font-family:Georgia,serif;color:#fff8ec;line-height:1.15;word-break:break-word;">' + title + '</div>' + '<div style="background:#F1ECE5;color:#1b1712;padding:12px;border-radius:6px;">' + body + '</div>' + '</div>'; */ const panel = (usercolour, title, body) => '<div style="background:' + usercolour + ';border:1px solid #5b4632;border-radius:10px;padding:2px;max-width:760px;color:#f7f1e6;box-shadow:0 2px 6px rgba(0,0,0,0.35);">' + '<div style="font-size:22px;font-weight:bold;text-align:center;margin:0 0 10px 0;font-family:Arial;color:#fff8ec;line-height:1.15;word-break:break-word;">' + title + '</div>' + '<div style="background:#F1ECE5;color:#1b1712;padding:4px;border-radius:6px;">' + body + '</div>' + '</div>'; const btn = (label, cmd, width) => '<a style="display:inline-block;text-align:center;margin:1px 2px 1px 0;padding:3px 6px;min-width:' + (width || 90) + 'px;color:#ffffff;background:' + userhex + ';border:1px solid #6a4323;border-radius:6px;text-decoration:none;font-normal:100;" href="' + cmd + '">' + label + '</a>'; const btnBack = (label, cmd, width) => '<a style="display:inline-block;text-align:center;margin:1px 0px 1px 0;padding:0px 6px;min-width:' + (width || 90) + 'px;color:#202020;background:#CCCCCC;border:1px solid #6a4323;border-radius:6px;text-decoration:none;font-size:10px;font-normal:100;" href="' + cmd + '">' + label + '</a>'; let backTo = ""; let playerName = getAttrByName(charID,"character_name"); //----------------------------------------------------------------------- //Let's get the coin amounts let carCP = parseInt(getAttrByName(charID,"cp"),10) || 0; let carSP = parseInt(getAttrByName(charID,"sp"),10) || 0; let carEP = parseInt(getAttrByName(charID,"ep"),10) || 0; let carGP = parseInt(getAttrByName(charID,"gp"),10) || 0; let carPP = parseInt(getAttrByName(charID,"pp"),10) || 0; let coinsNo = carCP + carSP + carEP + carGP + carPP; let coinsWt = coinsNo/10; //Equivalent amounnts let gpvCP = parseInt((carCP/200).toFixed(2),10); //to fixed changes it to string, need to parseInt back to a number let gpvSP = parseInt((carSP/20).toFixed(2),10); let gpvEP = parseInt((carEP/2).toFixed(2),10); let gpvGP = parseInt((carGP),10); let gpvPP = parseInt((carPP*5),10); //Check to make sure there are no coins under total let totCP = parseInt(getAttrByName(charID,"cp-total"),10) || 0; let totSP = parseInt(getAttrByName(charID,"sp-total"),10) || 0; let totEP = parseInt(getAttrByName(charID,"ep-total"),10) || 0; let totGP = parseInt(getAttrByName(charID,"gp-total"),10) || 0; let totPP = parseInt(getAttrByName(charID,"pp-total"),10) || 0; let wealth = getAttrByName(charID,"wealthNotes") || '<div style="color:#6C1515;"><i>Nothing listed</i></div>'; let totalGBV = gpvCP + gpvSP + gpvEP + gpvGP + gpvPP; //totalGBV = totalGBV * 100; //totalGBV = totalGBV.toFixed(2) //totalGBV = totalGBV/100; //Title let strTitle = "$ " + playerName + " $" + "<p>Total GP Value: " + totalGBV + "<br>" + "Coins, no: " + coinsNo + "; Wt: " + coinsWt + " lbs</p>"; backTo = btnBack("Admin", "!admin", 10); let strBody = '<div style="background:#efe3cf;color:#6C1515;">Roll20 Character Sheet, ' + 'PLEASE list coins under "Carried Coins", only. <i>Do not use "Total Coins"</i>.' + '&ensp;' + backTo + '</div>'; //<div style="background:#efe3cf let cpAdd = btn("CP", "!changecoins --?{Copper Pieces - " + playerName + " has " + carCP + " CP. Add or subtract (-)|0} --" + charID + " --CP --Copper Pieces --" + playerName + " --" + carCP , 20) + " <b>" + carCP + "</b> Copper (<b>" + (carCP / 200) + "</b> gp)"; let spAdd = "<br>" + btn("SP", "!changecoins --?{Silver Pieces - " + playerName + " has " + carSP + " SP. Add or subtract (-)|0} --" + charID + " --SP --Silver Pieces --" + playerName + " --" + carSP , 20) + " <b>" + carSP + "</b> Silver (<b>" + (carSP / 20) + "</b> gp)"; let epAdd = "<br>" + btn("EP", "!changecoins --?{Electrum Pieces - " + playerName + " has " + carEP + " EP. Add or subtract (-)|0} --" + charID + " --EP --Electrum Pieces --" + playerName + " --" + carEP , 20) + " <b>" + carEP + "</b> Electrum (<b>" + (carEP / 2) + "</b> gp)"; let gpAdd = "<br>" + btn("GP", "!changecoins --?{Gold Pieces - " + playerName + " has " + carGP + " GP. Add or subtract (-)|0} --" + charID + " --GP --Gold Pieces --" + playerName + " --" + carGP , 20) + " <b>" + carGP + "</b> Gold (<b>" + (carGP) + "</b> gp)"; let ppAdd = "<br>" + btn("PP", "!changecoins --?{Platinum Pieces - " + playerName + " has " + carPP + " PP. Add or subtract (-)|0} --" + charID + " --PP --Platinum Pieces --" + playerName + " --" + carPP , 20) + " <b>" + carPP + "</b> Platinum (<b>" + (carPP * 5) + "</b> gp)"; strBody += "<p><div>" + cpAdd + spAdd + epAdd + gpAdd + ppAdd + "</div></p>"; strBody += '<div style="background:#efe3cf;text-align:left;"><b>Wealth Notes</b> (jewels, banks, etc) &nbsp;' + btn("?", "!btnaboutwealthnotes --" + charID,10) + '<br>' + wealth + '</div></p>'; sendChat ("My Coins", panel(userhex,strTitle, strBody)); }); //////////////////////////////////////////////////////////////////////////////// on("chat:message", function(msg) { if (msg.type !== "api") return; let chatcommand = msg.content.trim().toLowerCase(); if (!chatcommand.startsWith("!changecoins")) return; let args = msg.content.trim().split(/\s*--/); let NewCoins = parseInt(args[1], 10) || 0 let CoinCharId = (args[2] || "").trim(); //CharID let CoinP = args[3].trim().toLowerCase(); //for example PP let CoinName = (args[4] || "").trim(); //eg Platinum Pieces let CoinCharName = (args[5] || "").trim(); //character name let Coins = parseInt(args[6], 10) || 0; //current number of coins let commandvalue = ""; if (isNaN(Coins)) { sendChat("Sorry, but...", "<div style='color:red;'>Added number of " + CoinP + " must be a positive number or possibly zero. " + numerr + "</div>"); return; } else if (Coins + NewCoins < -1) { sendChat("Sorry, but...", "<div style='color:red;'>" + NewCoins + " is more than the current number of " + CoinP + " " + "that " + CoinCharName + " currently has. </div>"); return; } //log(CoinP.toLowerCase() + " new coins " + Coins + " Current Coins " + NewCoins); //let FinalCoins = NewCoins; //sendChat("Operation", CoinCharName + " current " + CoinP + ": " + Coins + " to a 'FinalCoins' Total: " + NewCoins); sendChat(CoinCharName, "CP on Sheet: " + getAttrByName(CoinCharId,"cp") + "; Added " + NewCoins); let attrs = findObjs({ type: "attribute", characterid: CoinCharId, name: "cp" }); attrs.forEach(a => { sendChat("CP Debug", "attr id: " + a.id + "<br>name: " + a.get("name") + "<br>current: " + a.get("current") ); }); //using getAttrByName(CoinCharId,'cp') where 'CoinCharId' is Character ID: " + getAttrByName(CoinCharId,"cp")); if (CoinP.toLowerCase() === "cp") { commandvalue = `!modattr --charid ${CoinCharId} --cp|${NewCoins}`; } else if (CoinP.toLowerCase() === "sp") { commandvalue = `!modattr --charid ${CoinCharId} --sp|${NewCoins}`; } else if (CoinP.toLowerCase() === "ep") { commandvalue = `!modattr --charid ${CoinCharId} --ep|${NewCoins}`; } else if (CoinP.toLowerCase() === "gp") { commandvalue = `!modattr --charid ${CoinCharId} --gp|${NewCoins}`; } else if (CoinP.toLowerCase() === "pp") { commandvalue = `!modattr --charid ${CoinCharId} --pp|${NewCoins}`; } else { sendChat( "Error", `<div style='color:red;'>Unknown coin type: ${CoinP}</div>` ); return; } /*sendChat("Debug", "CoinCharName: " + CoinCharName + "<br>CoinCharId: " + CoinCharId + "<br>CoinP: " + CoinP + "<br>NewCoins: " + NewCoins + "<br>Command: " + commandvalue );*/ sendChat ("Change " + CoinP, commandvalue); sendChat("My Coins, Altered", "!forselected !mycoins"); }); });