We use Cookies to help personalize and improve Roll20. For more information on our use of non-essential Cookies, visit our Privacy Policy here.
Accept
Advertisement Create a free account

A little help on a wallet script.

1556305448

Edited 1556308892
DXWarlock
Pro
Sheet Author
API Scripter
I've been working on a script for my players to spend/add money to their cash. Ive got most of it worked out other than internal 'make change' part of it. Example 1: Player spends 3 gold and they have only 1 gold piece, but have 5 platinum: Make it take the other 2 gold off the plat and set them to 4 plat, 8 gold. Example 2: Player spends 3 gold but have 0 plat, or gold, but have 50 silver. Make it take 30 silver, and set them to 20 silver left. (above examples are all using the 1:10 ratio of pathfinder for PP:GP:SP:CP) I thought of having it do the math to figure out a decimal total value of PP for both owned and spent money..but then it 'magically' exchanges coins to the cleanest sum amount in return..IE (an  extreme example): They spend 5 gold, they have 0 PP, 2 GP, 6080SP, 30 CP. It was figuring out they wanted to spend 0.5 PP, and their wallet was worth 7.03 PP. SO it would take the money (leaving them with 6.53 PP) and give them 6PP, 5 GP, 3SP back (not good at all) I cannot think of a way to do this, without a metric ton of if/else if/else loops. There has to be an easier way I'm not grasping here. This is what I have so far..the "do magic" inside the " TakeMoney " fuction part is where I am stuck. Anyone have any tips? on ( 'chat:message' , function ( msg ) { /*["!coins","Spend","0PP","0GP","0SP","0CP"]*/ "use strict" ; if (msg.type === "api" && msg.content. indexOf ( "!coins" ) !== - 1 ) { var cWho = findObjs ({ type: 'character' , name: msg.who })[ 0 ]; if (cWho === undefined ) { cWho = RollRight (msg.playerid); //outside function---- } msg.who = cWho. get ( "name" ); var oPP = findObjs ({ name: "PP" , type: "attribute" , characterid: cWho.id }, { caseInsensitive: true })[ 0 ]; var oGP = findObjs ({ name: "GP" , type: "attribute" , characterid: cWho.id }, { caseInsensitive: true })[ 0 ]; var oSP = findObjs ({ name: "SP" , type: "attribute" , characterid: cWho.id }, { caseInsensitive: true })[ 0 ]; var oCP = findObjs ({ name: "CP" , type: "attribute" , characterid: cWho.id }, { caseInsensitive: true })[ 0 ]; if (oPP === undefined || oGP === undefined || oSP === undefined || oCP === undefined ) { sendChat ( 'Coin Purse' , "Not All Coins Set" ); return ; } var msgFormula = msg.content. split ( / \s + / ); var Money = msgFormula. slice ( Math . max (msgFormula.length - 4 , 1 )); var spent = "" ; Money. forEach ( function ( type ) { //type sent as array format ["0PP","0GP","0SP","0CP"] var coinType = type. replace ( / [ ^ a-zA-Z] + / g , '' ); var coinCount = type. replace ( / [ ^ 0-9] / g , '' ); if (coinCount !== '0' ) { if (msgFormula[ 1 ] == "Spend" ) { TakeMoney (cWho, coinType, coinCount); } else { AddMoney (cWho, coinType, coinCount); } var addtext = coinCount + "" + coinType + " " ; spent = spent + addtext; } }); var currentPP = parseInt (oPP. get ( "current" ), 10 ); var currentGP = parseInt (oGP. get ( "current" ), 10 ); var currentSP = parseInt (oSP. get ( "current" ), 10 ); var currentCP = parseInt (oCP. get ( "current" ), 10 ); var coinSum = "Wallet: " + currentPP + "PP, " + currentGP + "GP, " + currentSP + "SP, " + currentCP + "CP" ; var chatmsg = "&{template:pf_block}{{color=darkgrey}}{{name=" + msgFormula[ 1 ] + ":" + spent + "}}{{description=" + coinSum + "}}" ; sendChat ( 'Coin Purse' , "/w " + msg.who + " " + chatmsg); if (msg.who !== "GM" ) { sendChat ( 'Coin Purse' , "/w GM " + chatmsg); } } }); /*---SPEND MONEY---*/ function TakeMoney ( cWho , type , amount ) { var oC = findObjs ({ name: type, _type: "attribute" , characterid: cWho.id }, { caseInsensitive: true })[ 0 ]; var currentCoins = parseInt (oC. get ( "current" ), 10 ); var spentCoins = parseInt (amount, 10 ); if (currentCoins < spentCoins) { spentCoins = spentCoins - currentCoins; //(do magic with remaining amount to spend)... } else { var total = parseInt (currentCoins - spentCoins, 10 ); oC. setWithWorker ( 'current' , total); } } /*---ADD MONEY---*/ function AddMoney ( cWho , type , amount ) { var oC = findObjs ({ name: type, _type: "attribute" , characterid: cWho.id }, { caseInsensitive: true })[ 0 ]; var currentCoins = parseInt (oC. get ( "current" ), 10 ); var newCoins = parseInt (amount, 10 ); var total = parseInt (currentCoins + newCoins, 10 ); oC. setWithWorker ( 'current' , total); }
1556323707

Edited 1556331083
GiGs
Pro
Sheet Author
API Scripter
I havent analyzed the script thoroughly so I'm just going to through out a bunch of thoughts and code snippets - all untested! (I'll try to come back and have a good look at the script later.) you call the same attributes (findobj) multiple times. Since you call them all in the on chat message section initially, i would save their values into an object, and don't call them again, just grab them from the object as you need them in the various functions. Instead of this var currentPP = parseInt(oPP.get("current"), 10); var currentGP = parseInt(oGP.get("current"), 10); var currentSP = parseInt(oSP.get("current"), 10); var currentCP = parseInt(oCP.get("current"), 10); Something like         let coins = {};         coins.PP = parseInt(oPP.get("current"), 10); coins.GP = parseInt(oGP.get("current"), 10); coins.SP = parseInt(oSP.get("current"), 10); coins.CP = parseInt(oCP.get("current"), 10); Then you can call those values and alter them as you need, with the same syntax (using coins.GP instead of GP), or with bracket syntax which is very handy in this case:  coins['GP'] , since that allows you to use variables, for instance,  coins[type] Just pass the money object to your functions in the parameters: function TakeMoney(cWho, type, amount) { becomes function TakeMoney(type, amount, coins) { A big problem I noticed. This: Money. forEach ( function ( type ) { //type sent as array format ["0PP","0GP","0SP","0CP"] var coinType = type. replace ( / [ ^ a-zA-Z] + / g , '' ); var coinCount = type. replace ( / [ ^ 0-9] / g , '' ); if (coinCount !== '0' ) { if (msgFormula[ 1 ] == "Spend" ) { TakeMoney (cWho, coinType, coinCount); } else { AddMoney (cWho, coinType, coinCount); } var addtext = coinCount + "" + coinType + " " ; spent = spent + addtext; } }); You definitely (I can not stress this enough) should not have the addmoney / takemoney functions inside this loop. get the values of coins in the loop, then operate on them as needed afterwards. If you use the coin object i suggest, you could set it up like so let newCoins = {CP: 0, SP: 0, GP: 0, PP: 0}; Money.forEach(function (type) {             var coinType = type.replace(/[^a-zA-Z]+/g, ''); var coinCount = type.replace(/[^0-9]/g, '');             newCoins[coinType] = coinCount; } You could just easily convert them into a value by replacing the newCoins line with  convertedValue =  convertedValue + getValue(coinCount,coinType) (dont forget to let convertedValue = 0 before the forEach above) and have a function: function getValue(amount, type) {       const steps = {CP: 1, SP: 10, GP: 100, PP: 1000};       return amount * steps[type]; } With a coins object you can get a value like so: const toValue = (coins) => coins.CP + coins.SP *10 + coins.GP *100 + coins.PP * 1000; Or a more traditional function function toValue(coins) { return coins.CP + coins.SP *10 + coins.GP *100 + coins.PP * 1000; } And you can convert a value to a coin object like so (this is a little clunky, i might come up with a more natural way later, probably by changing the way the coins object is defined): const getChange = (amount,divider) => (amount >= divider) ? Math.floor(amount / divider) : 0; function toCoins(value) { let newCoins = {}; let amount = Math.abs(value); let positive = value < 0 ? -1 : 1; newCoins.PP = getChange(amount,1000) * positive; amount -= newCoins.PP*1000 * positive; newCoins.GP = getChange(amount,100) * positive; amount -= newCoins.GP*100 * positive; newCoins.SP = getChange(amount,10) * positive; amount -= newCoins.SP*10 * positive newCoins.CP = amount * positive; return newCoins; } This one might look a bit strange. The value will be either positive or negative, and if its negative, it needs careful handling which is why that *positive   element is there on each line. But iimportantly, it allows you to handle adding and subtracting money values with the same function. Just pass a negative value when spending money. Hope thinking through how to use these or similar techniques will help. 
1556327964

Edited 1556380978
The Aaron
Forum Champion
API Scripter
That's a good start from GiG and I agree with everything except possibly the toCoins() function at the end (it doesn't adjust the existing purse, simply creates a purse with the minimum number of coins).  I similarly would convert to an object with properties for each denomination.  In my example, I went lowercase.  This is written purely stand alone, you can fit it in either directly, or use it as input to another solution. Here's the functions and setup, examples to follow: // the order of ascending value. const valueOrder = ['cp','sp','gp','pp']; // conversion between types const coinConversion = { pp: { pp: (n)=>n, gp: (n)=>10*n, sp: (n)=>100*n, cp: (n)=>1000*n }, gp: { pp: (n)=>n/10, gp: (n)=>n, sp: (n)=>10*n, cp: (n)=>100*n }, sp: { pp: (n)=>n/100, gp: (n)=>n/10, sp: (n)=>n, cp: (n)=>10*n }, cp: { pp: (n)=>n/1000, gp: (n)=>n/100, sp: (n)=>n/10, cp: (n)=>n } }; // calculate the value of a set of coins in cp const cpValue = (money) => Object.keys(money).reduce( (amount, type) => (amount + (coinConversion.hasOwnProperty(type) ? coinConversion[type].cp(money[type]) : 0)), 0 ); // Convenience money cloner const copyMoney = (money) => valueOrder.reduce( (newMoney, coin) => Object.assign(newMoney, {[coin]: money[coin]||0}),{}); // the spending function (magic!) const spendMoney = (spend, purse) => { let purseInCP = cpValue(purse); let spendInCP = cpValue(spend); // 1) can they spend it? if(purseInCP >= spendInCP){ // clone the purse to make changes to let remainPurse = copyMoney(purse); // recursive function to spend coins, borrowing from higher denominations as needed. const spendWithBorrow = (index, amount) => { let type = valueOrder[index]; // the named coin type, like 'cp' remainPurse[type]-=amount; // adjust by amount // negative implies borrow if(remainPurse[type]<0){ // get the next more valuable coin type let type2 = valueOrder[index+1]; // get the number of coins needed, rounding up to account for the carry let ask = Math.ceil(coinConversion[type][type2](Math.abs(remainPurse[type]))); // take those coins (which will recurse to higher denominations as needed) spendWithBorrow(index+1,ask); // add the ask amount in current coin type back to current coin amount. // this handles the remainder after borrowing remainPurse[type]+=coinConversion[type2][type](ask); } }; // do the spend starting at cp spendWithBorrow(0,spendInCP); // return it. return remainPurse; } // returns undefined if they can't spend that amount. }; First, your example, in the money object notation: // purse: 0pp 2gp 6080sp 30cp let p = { pp: 0, gp: 2, sp: 6080, cp: 30 }; // spend: 5gp let s = { pp: 0, gp: 5, sp: 0, cp: 0 }; This is actually a pretty straight forward problem, here's the result: { "cp": 0, "sp": 6033, "gp": 2, "pp": 0 } A far more interesting problem is this one: // purse: 1pp let p2 = { pp: 1, gp: 0, sp: 0, cp: 0 }; // spend: 1cp let s2 = { pp: 0, gp: 0, sp: 0, cp: 1 }; and the result: { "cp": 9, "sp": 9, "gp": 9, "pp": 0 } Now that we have those to talk about, let's talk about the implementation above. Here's the basic idea: Convert both purse and spend amount to the lowest valued denomination, then compare to see if there is enough money in the purse. Try and pay for it out of the lowest denomination If there isn't enough, borrow the rest from the next denomination of greater value Repeat until paid Return the final resultant purse. In the event there isn't enough money at step 1, it returns undefined. To do the above, I'm using a recursive function which you pass the index of the denomination in the valueOrder array, and the amount to spend.  Walking through the interesting example (spend 1cp with 1pp in purse), it goes like this: Convert purse to cp (1000cp) and spend to cp (1cp) and verify there is enough in the purse spendWithBorrow(0,1)  Subtract 1 from cp:0  creating  cp:-1 since negative, ask for 1sp ( ceil(abs(-1)/10) ) spendWithBorrow(1,1) Subtract 1 from sp:0  creating sp:-1 since negative, ask for 1gp (ceil(abs(-1)/10) ) spendWithBorrow(2,1) Subtract 1 from gp:0  creating gp:-1 since negative, ask for 1pp (ceil(abs(-1)/10) ) spendWithBorrow(3,1) Subtract 1 from pp:1  creating pp:0 Add the value of 1pp to gp:-1,   gp:(-1+10),  gp:9 Add the value of 1gp to sp:-1,   sp:(-1+10),  sp:9 Add the value of 1sp to cp:-1,   cp:(-1+10),   cp:9 Return {pp:0, gp: 9, sp:9,   cp:9} Hope that helps!
1556331303
GiGs
Pro
Sheet Author
API Scripter
Nice! I didnt like my toCoins function, I like your approach a lot better :)  I was planning on thinking about how to keep then original purse too because that is a much better principle to follow.
1556335211
The Aaron
Forum Champion
API Scripter
It's definitely an interesting problem and it took me a few stabs to get a good solution. =D  Now I need to think about how to support requiring exact change...  
1556335304
The Aaron
Forum Champion
API Scripter
One thing I like about this approach is that it's easily extendable by adjusting the conversion functions and ordering.  You could add in an ep coin pretty easily without changing the function, I believe.
1556336277
GiGs
Pro
Sheet Author
API Scripter
Whatever approach I'd have come up with would have been a lot clunkier because I'm still not used to embedding functions as values, like in this sp: { pp: (n)=>n/100, gp: (n)=>n/10, sp: (n)=>n, cp: (n)=>10*n That's still alien to me, but it's pretty cool and I'm looking forward to the day I can use it naturally :)
1556336864

Edited 1556336886
The Aaron
Forum Champion
API Scripter
It took a while to get it in my head. =D. It's not entirely the same as function(n){} but the differences are minor and generally mean it works like you expect. =D
1556379248
The Aaron
Forum Champion
API Scripter
One thing to note is that this will always spend the smallest coins first.  If you have 5gp and 50sp and want to buy something for 4gp, you'll end up with 5gp and 10sp.  Modifying it to spend same from same initially would probably be a two step process.
1556392760

Edited 1556392824
DXWarlock
Pro
Sheet Author
API Scripter
Wow, you guys went waaay farther into solving it than I would have imagined :) And a lot cleaner than my mess I was trying after I posted...mine was a rabbit hole of a lot of embedded if/else statements. A lot of what you guys put is alien to me also, but its working! Now I'm doing a lot of add a log(something) somewhere, run it and look at debug with "hmm fascinating!" going on..haha And smallest coin first is fine. I can imagine its plausible people traveling a lot, would try to spend the bulk coins first so they are not carrying 6 pounds of loose change.
1556392821
The Aaron
Forum Champion
API Scripter
=D  Awesome!  
1556406667
GiGs
Pro
Sheet Author
API Scripter
Great! :)
1556407453
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
I'm very interested in seeing this when it's finished. The Shaped sheet uses repeating fields for coins instead of discrete fields so I haven't been able to use the other cashmaster(?) script. Hopefully I can use or modify this one.
1556525375

Edited 1556525520
DXWarlock
Pro
Sheet Author
API Scripter
Here is the bulk of it Keith. It works, I don't think its as pretty as it could be, but it works for spending money (on pathfinder community sheet anyway). I need to add in the 'getting money' part but I spent the last 6 hours fixing it so it wasn't my usual personal script laziness of "No one else will need this, so I'll just accept it will probably error if players dont do it just right...and also...hard code EVERYTHING!"..haha. I'm sure it can and probably will error if someone does something weird, like them typing in they are spending "Elephant" amount of gold or such. But maybe it will give you a starting place. It uses a Macro set to visible to all players (as I said add option does nothing right now): !coins ?{Type|Spend|Add} cp:?{CP|0} sp:?{SP|0} gp:?{GP|0} pp:?{PP|0} <a href="https://raw.githubusercontent.com/dxwarlock/CoinPurse/master/Purse.js" rel="nofollow">https://raw.githubusercontent.com/dxwarlock/CoinPurse/master/Purse.js</a>
1556593338

Edited 1556593400
GiGs
Pro
Sheet Author
API Scripter
I'm a little worried at the use of eval() in there especially on user input. I'll post up a replacement for that section later. Also the way you find character names strikes me as a little weird. You could have that as a parameter in the macro instead of trying to find it automatically (like @{selected|character_name} and make it a token action so players cant use it without their token selected), but I guess it works. I only noticed it because I tried to use it without owning a character and it errorred out, so a more elegant way of handling that is probably a good idea. Especially since you need to have an association with a character for the script to work, so passing that in the macro call (or using msg.selected) is probably best practice.
1556594461

Edited 1556596424
DXWarlock
Pro
Sheet Author
API Scripter
Yea, eval was the only way I could get it to work. :( I cringed with I put it in too..but that's my 'get the hammer and pound it in' solution. And it does error without a character, but honestly (speaking purely for just my group here) in the 6 years we've been playing on here, everyone always has a character. We are the same 8-10 people coming in and out of our games. I should put in a catch for the error for other people that borrow the code. As if your spending money without a character its a moot point anyway, just sendchat that they don't have anyone to spend money on.&nbsp; For getting the character from who sent the cmd, (for us anyway, personal preference is all) token actions for us is a last resort for not directly token related macro/api calls, as it is a new layer of unneeded complexity sometimes to us. For example: being on a visual only map with no tokens, and I have to drag them over one if they want to spend money, roll for treasure, do a skill check, etc or do any !command that uses character stats lookup and they mostly always forget to select themselves first when they DO have one on the table. So just easier for me to convert everything (literally, everything, internally script wise for my personal game scripts) to assume it was done with the character picked in the 'speaking as' drop down, so no matter what you type or !cmd you might do, it has full access to everything about your character, if it needs it. As it seems speaking 'as player' has a lot of limitation to the API usefulness, but 'as character' doesn't. So I just always use that :) Not defending it as the best universal practice to convert to 'as character' 100% of the time, not at all...Just for my group it seems to be. &nbsp;
1556596423
GiGs
Pro
Sheet Author
API Scripter
That's fair enough. We dont use the speakingAs drop down much in my games, but it does make sense the way you do it if you arent relying on tokens much. My players often have multiple characters, so for my use I'd probably want a check to make sure they are changing the correct character which is why the token approach appeals to me. There are other ways to solve that but i guess its not an issue for you so not needed.
1556596507
DXWarlock
Pro
Sheet Author
API Scripter
Yea, it would be a pain for that situation. We dont ever 'use' speaking as either. I just assume code wise they are :) I could see for you with more than one character, or people that do use the text chat for talking it being a huge hurdle.
1556622182
GiGs
Pro
Sheet Author
API Scripter
What sheet are you using for this? I've looked at the standard pathfinder sheet, and it doesnt use the same coinage attribute names.
1556628174
DXWarlock
Pro
Sheet Author
API Scripter
The pathfinder community sheet.
1556655418

Edited 1556655509
I know you just spent a lot of time on your script, but... I wrote my own for 5e that allows for buying, selling and distributing loot money. It even has a simple inventory system. Don't know why I never posted it... <a href="https://github.com/blawson69/PurseStrings" rel="nofollow">https://github.com/blawson69/PurseStrings</a>
1556777714
GiGs
Pro
Sheet Author
API Scripter
That looks very handy. I'm a bit disappointed that it is hardcoded to use specific currency unlike Aaron's code above, but it's understandable, and I can tweak that. It does seem very powerful and flexible. If it's well-tested, you should consider submitting it to roll20's API library.
1556804705
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Wow, I've been looking for something like that for a long time. I would rather have it read from the token's GM Notes rather than a character's GM notes, so I only need one merchant character sheet, but I'm going to give this a spin. Looks nice!
1556806125
DXWarlock
Pro
Sheet Author
API Scripter
Well crap, every time I start working on an API script, I make a paper airplane and someone comes along and goes "oh here I have a spare F16 jet". haha. Mind if I steal borrow some of your code Ben? :D
1556819427
keithcurtis said: Wow, I've been looking for something like that for a long time. I would rather have it read from the token's GM Notes rather than a character's GM notes, so I only need one merchant character sheet, but I'm going to give this a spin. Looks nice! That's definitely a tweak that could be added. I don't use a lot of merchants and they're often mooks, so using the character was a no brainer. Maybe it could look in the token's GM notes, then the character's if nothing was in the token's.&nbsp;
1556819734
GiGs said: That looks very handy. I'm a bit disappointed that it is hardcoded to use specific currency unlike Aaron's code above, but it's understandable, and I can tweak that. It does seem very powerful and flexible. If it's well-tested, you should consider submitting it to roll20's API library. Believe me, I wanted to make the currency flexible. But my first priority was getting the functionality it has first - buying/selling and distributing the loot. After the first real session with it, I added the inventory. That part could definitely use some finesse.
1556819833
keithcurtis said: I'm very interested in seeing this when it's finished. The Shaped sheet uses repeating fields for coins instead of discrete fields so I haven't been able to use the other cashmaster(?) script. Hopefully I can use or modify this one. This was the #1 reason for me writing my script. CashMaster is completely incompatible with the Shaped Sheet.
1556820438
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Ben L. said: keithcurtis said: Wow, I've been looking for something like that for a long time. I would rather have it read from the token's GM Notes rather than a character's GM notes, so I only need one merchant character sheet, but I'm going to give this a spin. Looks nice! That's definitely a tweak that could be added. I don't use a lot of merchants and they're often mooks, so using the character was a no brainer. Maybe it could look in the token's GM notes, then the character's if nothing was in the token's.&nbsp; That would be doubleplus good! I could create a marketplace map, with a dozen different merchants and only one character sheet!
1556852412
GiGs
Pro
Sheet Author
API Scripter
Ben L. said: GiGs said: That looks very handy. I'm a bit disappointed that it is hardcoded to use specific currency unlike Aaron's code above, but it's understandable, and I can tweak that. It does seem very powerful and flexible. If it's well-tested, you should consider submitting it to roll20's API library. Believe me, I wanted to make the currency flexible. But my first priority was getting the functionality it has first - buying/selling and distributing the loot. After the first real session with it, I added the inventory. That part could definitely use some finesse. Aaron's code above should be a pretty easy drop-in for managing a purse of any kind of currency (just need a way to modify the valueOrder array, and the equivalent structure in your script).&nbsp;