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?

March 01 (5 years ago)
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

March 01 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

If you can give me the table, I can probably give you the script pretty easily.

March 01 (5 years ago)
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

March 02 (5 years ago)

Edited March 02 (5 years ago)
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. 

March 02 (5 years ago)

Edited March 02 (5 years ago)
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.
March 02 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Here it is plugged in:

// Github:   https://github.com/shdwjk/Roll20API/blob/master/Measure/Measure.js
// By:       The Aaron, Arcane Scriptomancer
// Contact:  https://app.roll20.net/users/104025/the-aaron


const Measure = (()=> {

	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) => {
        const entry = modByRange.find((o)=>o.r<=d );
        return (entry && entry.m) || 0;
    };

	const checkInstall = function() {
		log('-=> MeasureGURPS v'+version+' <=-  ['+(new Date(lastUpdate*1000))+']');
	};

	const handleInput = (msg) => {
		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:()=>'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(`<li>${e.name1} to ${e.name2}: <b>${d} ${page.get('scale_units')}</b> (<code>${getModForDistance(d)}</code>)</li>`);
						return m;
					},[])
					.join('')
					.tap(function(o){
						sendChat('Measure',(whisper ? '/w "'+who+'"' : '/direct')+' <div><b>Measurements:</b><ul>'+o+'</ul></div>');
					});


				}
				break;
		}
	};

	const registerEventHandlers = () => {
		on('chat:message', handleInput);
	};


    on('ready',function() {
        checkInstall();
        registerEventHandlers();
    });

	return { };

})();
This just assumes that the page will be set in yards units.
March 03 (5 years ago)
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. 


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. 

March 03 (5 years ago)

Edited March 03 (5 years ago)
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. 


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. 

No need! Your method above will work for me: this

 ].reverse();

    const getModForDistance = (d) => {
        const entry = modByRange.find((o)=>o.r<=d );
        return (entry && entry.m) || 0;
    };
reversing the array, and using .find is not something I would have thought of. 

I can remove a massive if/else structure :)



March 03 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Sweet. =D

March 04 (5 years ago)

Edited March 04 (5 years ago)
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


March 05 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

That's ironic!  Sure thing, give this a whirl:

// Github:   https://github.com/shdwjk/Roll20API/blob/master/Measure/Measure.js
// By:       The Aaron, Arcane Scriptomancer
// Contact:  https://app.roll20.net/users/104025/the-aaron


const Measure = (()=> {

	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) => {
        const entry = modByRange.find((o)=>o.r>=d );
        return (undefined != entry ? entry.m : -24);
    };

	const checkInstall = function() {
		log('-=> MeasureGURPS v'+version+' <=-  ['+(new Date(lastUpdate*1000))+']');
	};

	const handleInput = (msg) => {
		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:()=>'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(`<li>${e.name1} to ${e.name2}: <b>${d} ${page.get('scale_units')}</b> (<b>${getModForDistance(d)}</b>)</li>`);
						return m;
					},[])
					.join('')
					.tap(function(o){
						sendChat('',(whisper ? '/w "'+who+'"' : '/direct')+' <div><ul>'+o+'</ul></div>');
					});


				}
				break;
		}
	};

	const registerEventHandlers = () => {
		on('chat:message', handleInput);
	};


    on('ready',function() {
        checkInstall();
        registerEventHandlers();
    });

	return { };

})();


March 05 (5 years ago)
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

March 05 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

It's from the chat output.  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 :

March 05 (5 years ago)
Mike W.
Pro
Sheet Author

The Arron

No problem and again than you so much.

Mike

March 05 (5 years ago)

Edited March 05 (5 years ago)
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')+' <div><ul>'+o+'</ul></div>');

and put something in the first two quotes, like

sendChat('Range',(whisper ? '/w "'+who+'"' : '/direct')+' <div><ul>'+o+'</ul></div>');

It might look better than the colon.

March 05 (5 years ago)
Mike W.
Pro
Sheet Author

Thanks GiGs


March 06 (5 years ago)
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

March 06 (5 years ago)
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. 

March 06 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter
I’ve also got an idea about how to add it into a Roll Template. 
March 06 (5 years ago)
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

March 06 (5 years ago)
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

March 06 (5 years ago)
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

March 06 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

=D no worries. 

March 26 (5 years ago)
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.

March 26 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Hmm. I’ll have to try and duplicate that. 

Just for clarity, the distance measured is wrong, but the lookup gives the correct result for the incorrect distance, right?

March 26 (5 years ago)
Mike W.
Pro
Sheet Author

Yes the modifier is correct for the range calculated.


March 26 (5 years ago)
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.

March 26 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Yeah, something in the maths. I’ll try and figure it out. 

March 26 (5 years ago)
Mike W.
Pro
Sheet Author

Thank you so very much!

March 31 (5 years ago)
Mike W.
Pro
Sheet Author

Any luck yet?

March 31 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Haven’t had a chance to look. I’ll try to check today. 

March 31 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Ok, starting to look at this.  Are you wanting the measurement in number of hexes, or Euclidian?  The two are not equal:


March 31 (5 years ago)

Edited March 31 (5 years ago)
Mike W.
Pro
Sheet Author

Hex would be fine.

The range there at 1.5 would be 2 hexes then.


March 31 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Bummer.  Euclidian is easier. =D

March 31 (5 years ago)

Edited March 31 (5 years ago)
Mike W.
Pro
Sheet Author

Well  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.

March 31 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

It's fine, I'll see what I can do.

March 31 (5 years ago)
Mike W.
Pro
Sheet Author

Much thanks.

May 05 (5 years ago)
Mike W.
Pro
Sheet Author

Aaron

any luck yet?


Mike

May 05 (5 years ago)
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. 

May 06 (5 years ago)
Mike W.
Pro
Sheet Author

Thanks Aaron

May 10 (5 years ago)
Mike W.
Pro
Sheet Author

The Aaron

I greatly appreciate this even though it appears I am the only one using this.

June 02 (5 years ago)
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?.

June 11 (5 years ago)
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. =(  I'll keep poking on it as time allows.

June 11 (5 years ago)
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.


June 11 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

There isn't a way to tie into it, unfortunately.  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. 

June 14 (5 years ago)
SᵃᵛᵃǤᵉ
Sheet Author
API Scripter

Found my error