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

[Ammo Script] Special modifications for Ammo script?

1532832054

Edited 1532832076
I have incorporated The Aaron's Ammo script into my custom D&D 3.5 character sheet for tracking a number of resources (weapon ammunition, spells, item charges, etc.), building the call to Ammo within the sheet. Now I have run into two special situations and I would like to modify Ammo to cover them. My use cases are pretty specific to me, so I am not sure if these warrant addition to the standard Ammo script, but I would like suggestions for how to adjust the script to account for them. 1) Even if a ranged weapon does not use ammunition, the ranged attack macros generated by my sheet still call Ammo, which of course fails because no ammunition is specified for the weapon. How would I change Ammo to recognize that it should ignore this call and show nothing in chat? Preferably based on something in the ammunition name? 2) I have Full Attack macros for ranged weapons as well. When more than one shot is made, I send one call to Ammo deducting all the ammunition (e.g., 3 shots deducts 3 ammunition). If the character does not have enough ammunition to meet the requirement, Ammo gives the "not enough" warning (as it should), and does not deduct any ammunition. I would like Ammo to be able to sometimes use the available ammunition, sending a warning to chat (e.g., "Character cannot fire 3 arrows, but can fire 2 arrows. Character uses 2 arrows and has 0 remaining."). I guess this would be some flag or parameter included in the API command, as sometimes you would not want this behavior (e.g., a staff that uses 3 charges for a specific function). Any assistance or advice appreciated!
1533040988
The Aaron
Pro
API Scripter
How about adding a flag like --ignore-missing for the first case, causing it to have no output if the attribute or character don't exist, and adding an --allow-partial flag for the second case that will change the reporting and such? Give this a try... // Github: <a href="https://github.com/shdwjk/Roll20API/blob/master/Ammo/Ammo.js" rel="nofollow">https://github.com/shdwjk/Roll20API/blob/master/Ammo/Ammo.js</a> // By: The Aaron, Arcane Scriptomancer // Contact: <a href="https://app.roll20.net/users/104025/the-aaron" rel="nofollow">https://app.roll20.net/users/104025/the-aaron</a> var Ammo = Ammo || (function() { 'use strict'; var version = '0.3.8', lastUpdate = 1533040966, schemaVersion = 0.1, ch = function (c) { var entities = { '&lt;' : 'lt', '&gt;' : 'gt', "'" : '#39', '@' : '#64', '{' : '#123', '|' : '#124', '}' : '#125', '[' : '#91', ']' : '#93', '"' : 'quot', '-' : 'mdash', ' ' : 'nbsp' }; if(_.has(entities,c) ){ return ('&amp;'+entities[c]+';'); } return ''; }, sendMessage = function(message, who, whisper) { sendChat( 'Ammo', `${(whisper||'gm'===who)?`/w ${who} `:''}&lt;div style="padding:1px 3px;border: 1px solid #8B4513;background: #eeffee; color: #8B4513; font-size: 80%;"&gt;${message}&lt;/div&gt;` ); }, adjustAmmo = function (ec) { let who = ec.output.who; let attr = ec.operation.attr; let amount = ec.operation.amount; let label = ec.operation.label || 'ammo'; let playerid = ec.output.playerid; const chr = getObj('character',attr.get('characterid')); const val = parseInt(attr.get('current'),10)||0; const max = parseInt(attr.get('max'),10)||10000; let adjustedValue = (val+amount); let overage = 0; let valid = true; if(ec.options.allowPartial) { if (adjustedValue &lt; 0) { overage = Math.abs(adjustedValue); adjustedValue = 0; } else if( adjustedValue &gt; max ) { overage = adjustedValue - max; adjustedValue = max; } } if(adjustedValue &lt; 0 ) { sendMessage( '&lt;b&gt;'+chr.get('name') + '&lt;/b&gt; does not have enough '+label+'. Needs '+Math.abs(amount)+', but only has '+ '&lt;span style="color: #ff0000;"&gt;'+val+'&lt;/span&gt;.'+ '&lt;span style="font-weight:normal;color:#708090;&gt;'+ch('[')+'Attribute: '+attr.get('name')+ch(']')+'&lt;/span&gt;', who, ec.output.whisper ); valid = false; } else if( adjustedValue &gt; max) { sendMessage( '&lt;b&gt;'+chr.get('name') + '&lt;/b&gt; does not have enough storage space for '+label+'. Needs '+adjustedValue+', but only has '+ '&lt;span style="color: #ff0000;"&gt;'+max+'&lt;/span&gt;.'+ '&lt;span style="font-weight:normal;color:#708090;&gt;'+ch('[')+'Attribute: '+attr.get('name')+ch(']')+'&lt;/span&gt;', who, ec.output.whisper ); valid = false; } if( playerIsGM(playerid) || valid ) { attr.set({current: adjustedValue}); let verb = (adjustedValue &lt; val) ? 'use' : 'gain'; sendMessage( `&lt;b&gt;${chr.get('name')}&lt;/b&gt; ${verb}s ${Math.abs(amount)} ${label} and has ${adjustedValue} remaining. ${overage ? `Unable to ${verb} ${overage} ${label}.`:''}`, who, ec.output.whisper ); if(!valid) { sendMessage( 'Ignoring warnings and applying adjustment anyway. Was: '+val+'/'+max+' Now: '+adjustedValue+'/'+max, who, ec.output.whisper ); } } }, showHelp = function(who,playerid) { sendChat('', '/w "'+who+'" '+ '&lt;div style="border: 1px solid black; background-color: white; padding: 3px 3px;"&gt;'+ '&lt;div style="font-weight: bold; border-bottom: 1px solid black;font-size: 130%;"&gt;'+ 'Ammo v'+version+ '&lt;/div&gt;'+ '&lt;div style="padding-left:10px;margin-bottom:3px;"&gt;'+ '&lt;p&gt;Ammo provides inventory management for ammunition stored in a character '+ 'attribute. If the adjustment would change the attribute to be below 0 or above '+ 'it'+ch("'")+'s maximum value, a warning will be issued and the attribute will not be'+ 'changed.&lt;/p&gt;'+ ( (playerIsGM(playerid)) ? '&lt;p&gt;&lt;b&gt;Note:&lt;/b&gt; As the GM, bounds will not be '+ 'enforced for you. You will be whispered the warnings, but the operation '+ 'will succeed. You will also be told the previous and current state in case '+ 'you want to revert the change.' : '')+ '&lt;/div&gt;'+ '&lt;b&gt;Commands&lt;/b&gt;'+ '&lt;div style="padding-left:10px;"&gt;'+ '&lt;b&gt;&lt;span style="font-family: serif;"&gt;!ammo '+ch('&lt;')+'id'+ch('&gt;')+' '+ch('&lt;')+'attribute'+ch('&gt;')+' '+ch('&lt;')+'amount'+ch('&gt;')+' '+ch('[')+'resource name'+ch(']')+'&lt;/span&gt;&lt;/b&gt;'+ '&lt;div style="padding-left: 10px;padding-right:20px"&gt;'+ 'This command requires 3 parameters:'+ '&lt;ul&gt;'+ '&lt;li style="border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;"&gt;'+ '&lt;b&gt;&lt;span style="font-family: serif;"&gt;id&lt;/span&gt;&lt;/b&gt; -- The id of the character which has the attribute. You can pass this as '+ch('@')+ch('{')+'selected|token_id'+ch('}')+' and the character id will be pulled from represents field of the token.'+ '&lt;/li&gt; '+ '&lt;li style="border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;"&gt;'+ '&lt;b&gt;&lt;span style="font-family: serif;"&gt;attribute&lt;/span&gt;&lt;/b&gt; -- The name of the attribute representing ammunition.'+ '&lt;/li&gt; '+ '&lt;li style="border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;"&gt;'+ '&lt;b&gt;&lt;span style="font-family: serif;"&gt;amount&lt;/span&gt;&lt;/b&gt; -- The change to apply to the current quantity of ammo. Use negative numbers to decrease the amount, and positive numbers to increase it. You can use inline rolls to determine the number.'+ '&lt;/li&gt; '+ '&lt;li style="border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;"&gt;'+ '&lt;b&gt;&lt;span style="font-family: serif;"&gt;resource name&lt;/span&gt;&lt;/b&gt; -- Anything you put after the amount to adjust by will be used as the resource name (default: "ammo").'+ '&lt;/li&gt; '+ '&lt;/ul&gt;'+ '&lt;/div&gt;'+ '&lt;b&gt;&lt;span style="font-family: serif;"&gt;!wammo '+ch('&lt;')+'id'+ch('&gt;')+' '+ch('&lt;')+'attribute'+ch('&gt;')+' '+ch('&lt;')+'amount'+ch('&gt;')+' '+ch('[')+'resource name'+ch(']')+'&lt;/span&gt;&lt;/b&gt;'+ '&lt;div style="padding-left: 10px;padding-right:20px"&gt;'+ 'This command is identical to !ammo but will whisper all output.'+ '&lt;/div&gt;'+ '&lt;/div&gt;'+ '&lt;/div&gt;' ); }, attrLookup = function(character,name,caseSensitive){ let match=name.match(/^(repeating_.*)_\$(\d+)_.*$/); if(match){ let index=match[2], attrMatcher=new RegExp(`^${name.replace(/_\$\d+_/,'_([-\\da-zA-Z]+)_')}$`,(caseSensitive?'i':'')), createOrderKeys=[], attrs=_.chain(findObjs({type:'attribute', characterid:character.id})) .map((a)=&gt;{ return {attr:a,match:a.get('name').match(attrMatcher)}; }) .filter((o)=&gt;o.match) .each((o)=&gt;createOrderKeys.push(o.match[1])) .reduce((m,o)=&gt;{ m[o.match[1]]=o.attr; return m;},{}) .value(), sortOrderKeys = _.chain( ((findObjs({ type:'attribute', characterid:character.id, name: `_reporder_${match[1]}` })[0]||{get:_.noop}).get('current') || '' ).split(/\s*,\s*/)) .intersection(createOrderKeys) .union(createOrderKeys) .value(); if(index&lt;sortOrderKeys.length &amp;&amp; _.has(attrs,sortOrderKeys[index])){ return attrs[sortOrderKeys[index]]; } return; } return findObjs({ type:'attribute', characterid:character.id, name: name}, {caseInsensitive: !caseSensitive})[0]; }, HandleInput = function(msg_orig) { if (msg_orig.type !== "api") { return; } let msg = _.clone(msg_orig); if(_.has(msg,'inlinerolls')){ msg.content = _.chain(msg.inlinerolls) .reduce(function(m,v,k){ m['$[['+k+']]']=v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } let whisper = false; let ignoreMissing = false; let allowPartial = false; let who=(getObj('player',msg.playerid)||{get:()=&gt;'API'}).get('_displayname'); let attr, amount, chr, token, label; let args = msg.content.split(/\s+/); let switches = args.filter((a)=&gt;/^--/.test(a)); args = args.filter((a)=&gt;!/^--/.test(a)); switch(args.shift()) { case '!wammo': whisper = true; /* break; // intentional dropthrough */ /* falls through */ case '!ammo': if((args.length + switches.length) &gt; 1) { switches.forEach((s)=&gt;{ switch(s) { case '--help': return showHelp(who,msg.playerid); case '--ignore-missing': ignoreMissing = true; break; case '--allow-partial': allowPartial = true; break; } }); chr = getObj('character', args[0]); if( ! chr ) { token = getObj('graphic', args[0]); if(token) { chr = getObj('character', token.get('represents')); } } if(chr) { if(! playerIsGM(msg.playerid) &amp;&amp; ! _.contains(chr.get('controlledby').split(','),msg.playerid) &amp;&amp; ! _.contains(chr.get('controlledby').split(','),'all') ) { sendMessage( 'You do not control the specified character: '+chr.id , (playerIsGM(msg.playerid) ? 'gm' : who), whisper ); sendMessage( '&lt;b&gt;'+getObj('player',msg.playerid).get('_displayname')+'&lt;/b&gt; attempted to adjust attribute &lt;b&gt;'+args[1]+'&lt;/b&gt; on character &lt;b&gt;'+chr.get('name')+'&lt;/b&gt;.', 'gm', whisper ); return; } attr = attrLookup(chr,args[1],false); } amount=parseInt(args[2],10); label=_.rest(args,3).join(' '); if(attr) { adjustAmmo({ output: { who, playerid: msg.playerid, whisper }, operation: { attr, amount, label }, options: { ignoreMissing, allowPartial } }); } else if(!ignoreMissing) { if(chr) { sendMessage( `Attribute [${args[1]}] was not found. Please verify that you have the right name.`, (playerIsGM(msg.playerid) ? 'gm' : who), whisper ); } else { sendMessage( ( token ? 'Token id ['+args[0]+'] does not represent a character. ' : 'Character/Token id ['+args[0]+'] is not valid. ' ) + 'Please be sure you are specifying it correctly, either with '+ch('@')+ch('{')+'selected|token_id'+ch('}')+ ' or '+ch('@')+ch('{')+'selected|character_id'+ch('}')+'.', (playerIsGM(msg.playerid) ? 'gm' : who), whisper ); } } } else { showHelp(who,msg.playerid); } break; } }, checkInstall = function() { log('-=&gt; Ammo v'+version+' &lt;=- ['+(new Date(lastUpdate*1000))+']'); if( ! _.has(state,'Ammo') || state.Ammo.version !== schemaVersion) { log(' &gt; Updating Schema to v'+schemaVersion+' &lt;'); state.Ammo = { version: schemaVersion, config: { }, policies: { global: { recoveryUpdatesMaximum: false }, byAttribute: { }, byCharacter: { } } }; } }, RegisterEventHandlers = function() { on('chat:message', HandleInput); }; return { CheckInstall: checkInstall, RegisterEventHandlers: RegisterEventHandlers }; }()); on("ready",function(){ 'use strict'; Ammo.CheckInstall(); Ammo.RegisterEventHandlers(); });
1533175862

Edited 1533175888
A world of thanks, Aaron! These updates look like they will do the trick. I did add a few tweaks, mostly for personal taste, though you may want to update your code above for one of them in case anyone else wants to use it. You may have a more sleek and elegant way of accomplishing these. For the odd case when using --allow-partial and the character is totally out of ammo, it gave the partial use message which sounded weird. So I added the following additional if() block in line 65 and changed line 65 to an else if(). if(val == 0 &amp; amount &lt; 0) { sendMessage( '&lt;b&gt;' + chr.get('name') + '&lt;/b&gt; does not have enough ' + label + '. Needs ' + Math.abs(amount) + ', but only has ' + val + '.', who, ec.output.playerid, ec.output.whisper ); valid = false; You may also notice I am passing playerid back there. I wanted player ammunition reports to be sent to chat, but mine to be whispered only to me. The only way I found to do this was passing playerid to the sendMessage function and changing line 36 to the following: `${(whisper|| playerIsGM(playerid ))?`/w ${who} `:''}&lt;div style="padding:1px 3px;border: 1px solid #8B4513;background: #eeffee; color: #8B4513; font-size: 80%;"&gt;${message}&lt;/div&gt;` Lastly, I wanted the partial use message to show the true count used or gained, so I altered line 89 to subtract overage from the abs(amount). It seems to work in my testing. Anyone see anything wrong with this?: `&lt;b&gt;${chr.get('name')}&lt;/b&gt; ${verb}s ${Math.abs(amount) -overage } ${label} and has ${adjustedValue} remaining. ${overage ? `Unable to ${verb} ${overage} ${label}.`:''}`, Again, I can't thank you enough for all your help, Aaron!
1533254394
The Aaron
Pro
API Scripter
That's great! I'll definitely back port those ideas into the script... probably next week... GenCon is quite exhausting...