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

[Measure] Can an option for ranged attack modifiers be calculated?

1551411410
Mike W.
Pro
Sheet Author
The Aaron     I noticed that not much has been done on the Measure script for some time, especially since the ruler update in Roll20. I was wondering if I could get a propitiatory option for GURPS. I usually use maps of 100 x 100 hexes, with my Tactical maps using hexes at 1 yard each, and my strategic maps using hexes at 200 yards each. It would be nice that in addition to showing the range that is also calculate the attack modifier for said range. For example: Range 0 to 2:    +0 Range 3            +1 Range 4 to 5    +2 Range 6 to 7     +3 Range 8 to 10   +4 Range 11 to 15 +5 Range 16 to 20 +6 Range 21 to 30 +7 Etc, Etc All the way out to say 20,000 @  -24 I can provide you the table. Perhaps there could be an option for direct manual input of said table so it would not be only for GURPS? Thanks for your time and keep up the GREAT work that you do for Roll20. Mike
1551414149
The Aaron
Roll20 Production Team
API Scripter
If you can give me the table, I can probably give you the script pretty easily.
1551416787
Mike W.
Pro
Sheet Author
Modifier Range 0 1 yd 0 2 yd -1 3 yd -2 5 yd -3 7 yd -4 10 yd -5 15 yd -6 20 yd -7 30 yd -8 50 yd -9 70 yd -10  100 yd -11 150 yd -12 200 yd -13 300 yd -14 500 yd -15 700 yd -16 1,000 yd -17 1,500 yd -18 2,000 yd -19 3,000 yd -20 5,000 yd -21 7,000 yd -22 10,000 yd -23 15,000 yd -24 20,000 yd
1551530927

Edited 1551531013
GiGs
Pro
Sheet Author
API Scripter
I wonder if you've had time to work on this Aaron. I'm interested in the solution too. Mainly because I have a related problem in another script - wondering the most economical way to find the index of a numerical list like the one above, but where there's no pattern to the range bands. 
1551567468

Edited 1551569135
The Aaron
Roll20 Production Team
API Scripter
I haven't yet, but here's how I plan to do it: const modByRange = [ {r:1, m:0}, {r:2, m:0}, {r:3, m:-1}, {r:5, m:-2}, {r:7, m:-3}, {r:10, m:-4}, {r:15, m:-5}, {r:20, m:-6}, {r:30, m:-7}, {r:50, m:-8}, {r:70, m:-9}, {r:100, m:-10}, {r:150, m:-11}, {r:200, m:-12}, {r:300, m:-13}, {r:500, m:-14}, {r:700, m:-15}, {r:1000, m:-16}, {r:1500, m:-17}, {r:2000, m:-18}, {r:3000, m:-19}, {r:5000, m:-20}, {r:7000, m:-21}, {r:10000, m:-22}, {r:15000, m:-23}, {r:20000, m:-24} ].reverse(); const getModForDistance = (d) => { const entry = modByRange.find((o)=>o.r<=d ); return (entry && entry.m) || 0; }; And here's the UnitTests I wrote with Jest to test the function: describe('modByDistance', ()=>{ it('should return 0 for 0', ()=>{ expect(getModForDistance(0)).toBe(0); }); it('should return 0 for 1', ()=>{ expect(getModForDistance(1)).toBe(0); }); it('should return 0 for 2', ()=>{ expect(getModForDistance(2)).toBe(0); }); it('should return -3 for 9', ()=>{ expect(getModForDistance(9)).toBe(-3); }); it('should return -4 for 10', ()=>{ expect(getModForDistance(10)).toBe(-4); }); it('should return -4 for 11', ()=>{ expect(getModForDistance(11)).toBe(-4); }); it('should return -7 for 42', ()=>{ expect(getModForDistance(42)).toBe(-7); }); it('should return -24 for 500000', ()=>{ expect(getModForDistance(500000)).toBe(-24); }); }); PASS Measure/__tests__/modByDistance.test.js modByDistance ✓ should return 0 for 0 (1ms) ✓ should return 0 for 1 ✓ should return 0 for 2 ✓ should return -3 for 9 ✓ should return -4 for 10 ✓ should return -4 for 11 (1ms) ✓ should return -7 for 42 ✓ should return -24 for 500000 (1ms) Test Suites: 1 passed, 1 total Tests: 7 passed, 7 total Snapshots: 0 total Time: 0.208s, estimated 1s Ran all test suites matching /Measure/i. Watch Usage: Press w to show more. Just need to plug it in. Speaking to efficiency, this is probably "good enough".  It's O(n) performance.  Note the .reverse() at the bottom of the modByRange definition.  Often in Computer Science, picking the right representation can really simplify the algorithms.  By reversing, I can reduce this to a simple find operation which evaluates a function on each node of an array and returns the first node for which the function is true.  My function just finds the first entry where the range is less than or equal to the distance. Since the ranges are in reversed order, that means it will first compare to 20000, then 15000, etc.  If I were to iterate from smallest to largest, I'd have to find the node before the first node where the range is greater than the distance, and account for the weird case where the range is greater than the last node.   For small data sets, this is "good enough" because you'd waste more in overhead on any more complicated solution and not see any benefit.  If your dataset is really big, say measured in 1000s of rows of similar data, you might look into the Binary Search Algorithm.  It has a worst case performance is O(log n).  Basically, you look at the one in the middle, determine if it's right (return it), too big (this becomes your end point), too small (this becomes your begin point) and repeat.
1551569644
The Aaron
Roll20 Production Team
API Scripter
Here it is plugged in: // Github: <a href="https://github.com/shdwjk/Roll20API/blob/master/Measure/Measure.js" rel="nofollow">https://github.com/shdwjk/Roll20API/blob/master/Measure/Measure.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> const Measure = (()=&gt; { const version = '0.3.2'; const lastUpdate = 1551569557; const modByRange = [ {r:1, m:0}, {r:2, m:0}, {r:3, m:-1}, {r:5, m:-2}, {r:7, m:-3}, {r:10, m:-4}, {r:15, m:-5}, {r:20, m:-6}, {r:30, m:-7}, {r:50, m:-8}, {r:70, m:-9}, {r:100, m:-10}, {r:150, m:-11}, {r:200, m:-12}, {r:300, m:-13}, {r:500, m:-14}, {r:700, m:-15}, {r:1000, m:-16}, {r:1500, m:-17}, {r:2000, m:-18}, {r:3000, m:-19}, {r:5000, m:-20}, {r:7000, m:-21}, {r:10000, m:-22}, {r:15000, m:-23}, {r:20000, m:-24} ].reverse(); const getModForDistance = (d) =&gt; { const entry = modByRange.find((o)=&gt;o.r&lt;=d ); return (entry &amp;&amp; entry.m) || 0; }; const checkInstall = function() { log('-=&gt; MeasureGURPS v'+version+' &lt;=- ['+(new Date(lastUpdate*1000))+']'); }; const handleInput = (msg) =&gt; { var args, pageid, page, measurements, whisper = false, who; if (msg.type !== "api") { return; } args = msg.content.split(/\s+/); switch(args.shift()) { case '!wmeasure': whisper = true; who=(getObj('player',msg.playerid)||{get:()=&gt;'API'}).get('_displayname'); // break; // Intentional fall through case '!measure': measurements = _.chain(_.union(args,_.pluck(msg.selected,'_id'))) .uniq() .map(function(t){ return getObj('graphic',t); }) .reject(_.isUndefined) .map(function(t){ pageid=t.get('pageid'); return { name: t.get('name') || "Token @ "+Math.round(t.get('left')/70)+','+Math.round(t.get('top')/70), x: t.get('left'), y: t.get('top') }; }) .reduce(function(m,t,k,l){ _.each(_.rest(l,k+1),function(t2){ m.push({ name1: t.name, name2: t2.name, distance: (Math.sqrt( Math.pow( (t.x-t2.x),2)+Math.pow( (t.y-t2.y),2))/70) }); }); return m; },[]) .value() ; page=getObj('page',pageid); if(page) { _.chain(measurements) .reduce(function(m,e){ var d=Math.round(page.get('scale_number')*e.distance,2); m.push(`&lt;li&gt;${e.name1} to ${e.name2}: &lt;b&gt;${d} ${page.get('scale_units')}&lt;/b&gt; (&lt;code&gt;${getModForDistance(d)}&lt;/code&gt;)&lt;/li&gt;`); return m; },[]) .join('') .tap(function(o){ sendChat('Measure',(whisper ? '/w "'+who+'"' : '/direct')+' &lt;div&gt;&lt;b&gt;Measurements:&lt;/b&gt;&lt;ul&gt;'+o+'&lt;/ul&gt;&lt;/div&gt;'); }); } break; } }; const registerEventHandlers = () =&gt; { on('chat:message', handleInput); }; on('ready',function() { checkInstall(); registerEventHandlers(); }); return { }; })(); This just assumes that the page will be set in yards units.
1551574425
The Aaron
Roll20 Production Team
API Scripter
GiGs said: I wonder if you've had time to work on this Aaron. I'm interested in the solution too. Mainly because I have a related problem in another script - wondering the most economical way to find the index of a numerical list like the one above, but where there's no pattern to the range bands.&nbsp; If you want to start a new thread and talk about your data I’m happy to try and find a good way to organize it.&nbsp;
1551592978

Edited 1551593013
GiGs
Pro
Sheet Author
API Scripter
The Aaron said: GiGs said: I wonder if you've had time to work on this Aaron. I'm interested in the solution too. Mainly because I have a related problem in another script - wondering the most economical way to find the index of a numerical list like the one above, but where there's no pattern to the range bands.&nbsp; If you want to start a new thread and talk about your data I’m happy to try and find a good way to organize it.&nbsp; No need! Your method above will work for me: this ].reverse(); const getModForDistance = (d) =&gt; { const entry = modByRange.find((o)=&gt;o.r&lt;=d ); return (entry &amp;&amp; entry.m) || 0; }; reversing the array, and using .find is not something I would have thought of.&nbsp; I can remove a massive if/else structure :)
1551625812
The Aaron
Roll20 Production Team
API Scripter
Sweet. =D
1551739896

Edited 1551740543
Mike W.
Pro
Sheet Author
The Aaron The script works fine except I did not mention something about the values.If the range is in between the table range, you use the next higher range modifier. For example of the range is 6 yards, the modifier is -3 (currently shows -2). So @ 5yds, per the table, the modifier is -2 and @ 7yds is -3 -The range is 6yds so you use the next increment because it is greater than 5yds. - Hope that makes sense. One other thing, could you BOLD and same Font size the modifier result as well, otherwise a little difficult to see. Would also save room in chat if the results left out the word 'Measurements'. I am also going to use this in a Roll Template (the default one). Right now it is not very pretty. Mike
1551746831
The Aaron
Roll20 Production Team
API Scripter
That's ironic!&nbsp; Sure thing, give this a whirl: // Github: <a href="https://github.com/shdwjk/Roll20API/blob/master/Measure/Measure.js" rel="nofollow">https://github.com/shdwjk/Roll20API/blob/master/Measure/Measure.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> const Measure = (()=&gt; { const version = '0.3.2'; const lastUpdate = 1551746777; const modByRange = [ {r:1, m:0}, {r:2, m:0}, {r:3, m:-1}, {r:5, m:-2}, {r:7, m:-3}, {r:10, m:-4}, {r:15, m:-5}, {r:20, m:-6}, {r:30, m:-7}, {r:50, m:-8}, {r:70, m:-9}, {r:100, m:-10}, {r:150, m:-11}, {r:200, m:-12}, {r:300, m:-13}, {r:500, m:-14}, {r:700, m:-15}, {r:1000, m:-16}, {r:1500, m:-17}, {r:2000, m:-18}, {r:3000, m:-19}, {r:5000, m:-20}, {r:7000, m:-21}, {r:10000, m:-22}, {r:15000, m:-23}, {r:20000, m:-24} ]; const getModForDistance = (d) =&gt; { const entry = modByRange.find((o)=&gt;o.r&gt;=d ); return (undefined != entry ? entry.m : -24); }; const checkInstall = function() { log('-=&gt; MeasureGURPS v'+version+' &lt;=- ['+(new Date(lastUpdate*1000))+']'); }; const handleInput = (msg) =&gt; { var args, pageid, page, measurements, whisper = false, who; if (msg.type !== "api") { return; } args = msg.content.split(/\s+/); switch(args.shift()) { case '!wmeasure': whisper = true; who=(getObj('player',msg.playerid)||{get:()=&gt;'API'}).get('_displayname'); // break; // Intentional fall through case '!measure': measurements = _.chain(_.union(args,_.pluck(msg.selected,'_id'))) .uniq() .map(function(t){ return getObj('graphic',t); }) .reject(_.isUndefined) .map(function(t){ pageid=t.get('pageid'); return { name: t.get('name') || "Token @ "+Math.round(t.get('left')/70)+','+Math.round(t.get('top')/70), x: t.get('left'), y: t.get('top') }; }) .reduce(function(m,t,k,l){ _.each(_.rest(l,k+1),function(t2){ m.push({ name1: t.name, name2: t2.name, distance: (Math.sqrt( Math.pow( (t.x-t2.x),2)+Math.pow( (t.y-t2.y),2))/70) }); }); return m; },[]) .value() ; page=getObj('page',pageid); if(page) { _.chain(measurements) .reduce(function(m,e){ var d=Math.round(page.get('scale_number')*e.distance,2); m.push(`&lt;li&gt;${e.name1} to ${e.name2}: &lt;b&gt;${d} ${page.get('scale_units')}&lt;/b&gt; (&lt;b&gt;${getModForDistance(d)}&lt;/b&gt;)&lt;/li&gt;`); return m; },[]) .join('') .tap(function(o){ sendChat('',(whisper ? '/w "'+who+'"' : '/direct')+' &lt;div&gt;&lt;ul&gt;'+o+'&lt;/ul&gt;&lt;/div&gt;'); }); } break; } }; const registerEventHandlers = () =&gt; { on('chat:message', handleInput); }; on('ready',function() { checkInstall(); registerEventHandlers(); }); return { }; })();
1551755092
Mike W.
Pro
Sheet Author
The Aaron Looks 99.99%, only a ' : ' creeps in the chat results. When using the script alone, the first result shows a ' : ' and subsequent results do not. When using in a roll template the " : " always appears. Any idea what is causing it? ( I can certainly live with it) Thanks for your time and effort - GURPS GMs will love this. Mike
1551759434
The Aaron
Roll20 Production Team
API Scripter
It's from the chat output.&nbsp; It used to say "Measure" there, as in "who's speaking", I removed "Measure", but it's still going to put the : It could output into a Roll Template, but it would still have the :
1551770660
Mike W.
Pro
Sheet Author
The Arron No problem and again than you so much. Mike
1551773070

Edited 1551773097
GiGs
Pro
Sheet Author
API Scripter
If you do want something else to appear there, you could edit this line sendChat('',(whisper ? '/w "'+who+'"' : '/direct')+' &lt;div&gt;&lt;ul&gt;'+o+'&lt;/ul&gt;&lt;/div&gt;'); and put something in the first two quotes, like sendChat('Range',(whisper ? '/w "'+who+'"' : '/direct')+' &lt;div&gt;&lt;ul&gt;'+o+'&lt;/ul&gt;&lt;/div&gt;'); It might look better than the colon.
1551779748
Mike W.
Pro
Sheet Author
Thanks GiGs
1551834044
Mike W.
Pro
Sheet Author
The Aaron Are you going to put this in the GitHub Roll20 repository? If so, are you going to rename it like 'Measure_GURPS'? So that is it not confused with the original Measure script? Mike
1551836509
The Aaron
Roll20 Production Team
API Scripter
I’m probably going to add a confuguration option to the existing measure to supply a table and configure range based information to show with measurements. Not sure yet though, need to think about how to best generalize it.&nbsp;
1551836573
The Aaron
Roll20 Production Team
API Scripter
I’ve also got an idea about how to add it into a Roll Template.&nbsp;
1551837320
Mike W.
Pro
Sheet Author
Aaron That is great. Your first option was what I originally suggested so that the script would not be proprietary to GURPS only (But then again you probably had already thought about that).. Options for a Roll Template would be very nice. Right now mine is a very simple call to the script command. Mike
1551841251
The Aaron
Roll20 Production Team
API Scripter
Yup, it’s a good idea. Easier to get the first version done as a one off, then refactor it into the mainline. =D
1551841761
Mike W.
Pro
Sheet Author
Aaron, I am sure you are not tired of hearing this but GREAT WORK ! Thank you, thank you, thank you. Mike
1551849213
The Aaron
Roll20 Production Team
API Scripter
=D no worries.&nbsp;
1553609889
Mike W.
Pro
Sheet Author
Aaron I found that the measure are not accurate starting at Range 4 it reports that it Range 5, then at Range 11 it reports Range 13. One thing I failed to mention is that I am using Hexes and tried both Euclidean and Hex Path settings and get the same inaccurate ranges - not sure f that makes a difference or not.
1553611241
The Aaron
Roll20 Production Team
API Scripter
Hmm. I’ll have to try and duplicate that.&nbsp; Just for clarity, the distance measured is wrong, but the lookup gives the correct result for the incorrect distance, right?
1553612093
Mike W.
Pro
Sheet Author
Yes the modifier is correct for the range calculated.
1553612386
Mike W.
Pro
Sheet Author
I did notice that at some ranges, even beyond range 4, the ranger determination is correct.. I cannot figure out any pattern but of you start from a straight line though a hex side, the range calculation starts t go bad - then veer left or right to the adjacent hexes then the range calculation eventually is correct. I am sure it ha something to do with the math for hex ranges.
1553617139
The Aaron
Roll20 Production Team
API Scripter
Yeah, something in the maths. I’ll try and figure it out.&nbsp;
1553617649
Mike W.
Pro
Sheet Author
Thank you so very much!
1554004966
Mike W.
Pro
Sheet Author
Any luck yet?
1554036193
The Aaron
Roll20 Production Team
API Scripter
Haven’t had a chance to look. I’ll try to check today.&nbsp;
1554048550
The Aaron
Roll20 Production Team
API Scripter
Ok, starting to look at this.&nbsp; Are you wanting the measurement in number of hexes, or Euclidian?&nbsp; The two are not equal:
1554052754

Edited 1554052848
Mike W.
Pro
Sheet Author
Hex would be fine. The range there at 1.5 would be 2 hexes then.
1554052796
The Aaron
Roll20 Production Team
API Scripter
Bummer.&nbsp; Euclidian is easier. =D
1554053488

Edited 1554053774
Mike W.
Pro
Sheet Author
Well&nbsp; if you can use it as Yards, which is what GURPS uses - It reports all distances in Yards which is the custom scale i use. Does that help?So that range would be 2 yards.
1554058897
The Aaron
Roll20 Production Team
API Scripter
It's fine, I'll see what I can do.
1554060415
Mike W.
Pro
Sheet Author
Much thanks.
1557022269
Mike W.
Pro
Sheet Author
Aaron any luck yet? Mike
1557037796
The Aaron
Roll20 Production Team
API Scripter
I started looking at it, but I got distracted and haven't found a solution yet. I'll try and pick it up again soon.&nbsp;
1557104991
Mike W.
Pro
Sheet Author
Thanks Aaron
1557487745
Mike W.
Pro
Sheet Author
The Aaron I greatly appreciate this even though it appears I am the only one using this.
1559492800
Mike W.
Pro
Sheet Author
Any luck or ideas?
I am also interested in this script for my Pro account usage and my friend wants it as well - any chance you can look at this again Aaron?.
1560225153
The Aaron
Roll20 Production Team
API Scripter
Yeah, sorry, it's on my list but life is busy and this one is tedious. I did put a few hours in on it tonight implementing a full conversion to hex coordinates to do the measurement but that only seems to move the error. =(&nbsp; I'll keep poking on it as time allows.
1560235127
Mike W.
Pro
Sheet Author
How does Roll20 do it with their ruler? Us that something you can emulate? Thanks for the extra effort though, you do so much for so little thank. I greatly appreciate this.
1560285281
The Aaron
Roll20 Production Team
API Scripter
There isn't a way to tie into it, unfortunately. &nbsp;All I get is pixel coordinates to work with. I think I'm close on the hex stuff, there is just an error somewhere in my math I think. Further testing is needed, but then I'll have a general purpose square to hex conversion, should be handy.&nbsp;
1560537186
SᵃᵛᵃǤᵉ
Sheet Author
API Scripter
Found my error