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

Calculate tokens in rectangle?

I want to get all tokens in an elongated rectangle, which represents a line area attack (for example, it might be 5 ft wide and 30 ft long). However, the rectangle can be rotated, so I'd need to do some translation on the tokens to do a simple x,y examination. Is there an easy way to accomplish this? I have a circle area attack and cone area attack working, so I might be able to hack them to work to some degree for a line attack.
Is this as the GM or as players?
1462408857

Edited 1462408925
The GM or players can call a macro that executes this script.
If there's collision detection for tokens, I can use that, since my area attack is represented by a spawned token.
1462426245

Edited 1462427134
Lithl
Pro
Sheet Author
API Scripter
// point: { x: ..., y: ... } // rectangle: { a: { x: ..., y: ... }, b: { x: ..., y: ... }, c: { x: ..., y: ... } } // assuming the line from A to B and the line from B to C are perpendicular function isPointInRectangle(point, rectangle) { var abx = rectangle.b.x - rectangle.a.x, aby = rectangle.b.y - rectangle.a.y, bcx = rectangle.c.x - rectangle.b.x, bcy = rectangle.c.y - rectangle.b.y; // The value of each of these calculations comes out positive or negative depending // on the *side* of the line the point is on if ((point.x - rectangle.a.x) * abx + (point.y - rectangle.a.y) * aby < 0) return false; if ((point.x - rectangle.b.x) * abx + (point.y - rectangle.b.y) * aby < 0) return false; if ((point.x - rectangle.a.x) * bcx + (point.y - rectangle.a.y) * bcy < 0) return false; if ((point.x - rectangle.c.x) * bcx + (point.y - rectangle.c.y) * bcy < 0) return false; return true; } function getRotatedRectangleCoords(token) { var left = token.get('left'), top = token.get('top'), halfWidth = token.get('width') / 2, halfHeight = token.get('height') / 2, x1 = left - halfWidth, // tok.left and tok.top are the center point of the token, but we want the corners y1 = top - halfHeight, x2 = left + halfWidth, y2 = top - halfHeight, x3 = left + halfWidth, y3 = top + halfHeight, x4 = left - halfWidth, y4 = top + halfHeight, ox1 = x1 - left, // essentially, move the token so it's located at (0, 0) to get the points we'll rotate oy1 = y1 - top, ox2 = x1 - left, oy2 = y2 - top, ox3 = x3 - left, oy3 = y3 - top, ox4 = x4 - left, oy4 = y4 - top, theta = token.get('rotation'), result = { a: {}, b: {}, c: {}, d: {} }; // rotate the corner coordinates around (0, 0) and then add the token's center back to it result.a.x = ox1 * Math.cos(theta) - oy1 * Math.sin(theta) + left; result.a.y = ox1 * Math.sin(theta) + oy1 * Math.cos(theta) + top; result.b.x = ox2 * Math.cos(theta) - oy2 * Math.sin(theta) + left; result.b.y = ox2 * Math.sin(theta) + oy2 * Math.cos(theta) + top; result.c.x = ox3 * Math.cos(theta) - oy3 * Math.sin(theta) + left; result.c.y = ox3 * Math.sin(theta) + oy3 * Math.cos(theta) + top; result.d.x = ox4 * Math.cos(theta) - oy4 * Math.sin(theta) + left; result.d.y = ox4 * Math.sin(theta) + oy4 * Math.cos(theta) + top; // isPointInRectangle doesn't care about point d, but we need it if we want to check all corners of potential target return result; } // Example use: var tok = getMyTargetToken(), tokCorners = getRotatedRectangleCoords(tok), blast = getMyBlastToken(), blastRect = getRotatedRectangleCoords(blast); // Check if center of token is inside rectangle isPointInRectangle({ x: tok.get('left'), y: tok.get('top') }, blastRect); // Check if token is *entirely* inside rectangle var isInside = true; _.each(tokCorners, function(pt) { isInside = isInside && isPointInRectangle(pt, blastRect); }); Disclaimer: above code is untested. I don't recall whether Roll20 stores token rotation as radians or degrees, so getRotatedRectangleCoords may need to convert the value of theta. (Math.cos and Math.sin expect radians.)
1462452332
The Aaron
Pro
API Scripter
(degrees)
1462458912

Edited 1462459217
Ada L.
Marketplace Creator
Sheet Author
API Scripter
You might have an easier time using the MatrixMath utility script to do those rotations.&nbsp; <a href="https://github.com/Roll20/roll20-api-scripts/tree/" rel="nofollow">https://github.com/Roll20/roll20-api-scripts/tree/</a>... It would probably also be easier (and involve cleaner math in isPointInRectangle) to transform the token's point P so that they are in a coordinate system where the rectangle is unrotated and its center point is at the origin.&nbsp; We'll call the transformed token point P'. Then, you know that the token is in the rectangle if abs(P'.x) &lt;= rect.width/2 and abs(P'.y) &lt;= rect.height/2. function isPointInRectangle(rectToken, point) { var rectX = rectToken.get('left'); var rectY = rectToken.get('top'); var halfWidth = rectToken.get('width')/2; var halfHeight = rectToken.get('height')/2; var angle = convertToRadians(rectToken.get('rotation')); // Get the rectangle's inverse transform. var m = MatrixMath.multiply( MatrixMath.translate([rectX, rectY]), MatrixMath.rotate(angle) ); var mInv = MatrixMath.inverse(m); // The point represented as homogenous 2D coordinates. var p = [point.x, point.y, 1]; // Use the rectangle's inverse transform to convert // the point to a coordinate space where the rectangle // is not rotated and its center is at the origin. var pTrans = MatrixMath.multiply(mInv, p); // The point is inside the rectangle if the transformed point is inside // the transformed rectangle (unrotated with its center at the origin). return ( Math.abs(pTrans[0]) &lt;= halfWidth && Math.abs(pTrans[1]) &lt;= halfHeight); }
Thanks so much! I haven't tried it yet, but hopefully I can tonight or tomorrow. It looks great, though. I haven't touched matrix math since college, so that info is all gone from my brain.
Hey Stephen, I tried using your code, but it doesn't appear to be working all the way for me. I'm getting the angle like so, which seems to give me good numbers: var degrees = -(rotation-90); //with rotation taken from the rectangle token while (degrees &lt; 0) { &nbsp; degrees += 360; } var theta = degrees * Math.PI / 180; Everything else is the same. It's not usually detecting a point in the rectangle when there is one in its area. To debug, I tried applying the inverse matrix transformation to the rectangle and the token providing the target point so I could visually see how they're transformed, and they don't go where they should.&nbsp;I don't know what the problem might be. Maybe it's something to do with the way rotation works (0 is upright, increases clockwise) vs. regular angles (0 is to the right, increases counterclockwise). Or maybe the matrix math gets screwed up when it tries to work against Roll20's coordinate system, which I think has (0,0) at the top left, with x increasing as it moves right and y increasing as it moves left. Anyone have any ideas?
1463116536

Edited 1463116755
Ada L.
Marketplace Creator
Sheet Author
API Scripter
I use very similar code for the rectangle token &nbsp;collisions in my TokenCollisions script, so I know the foundational math is fine. For the angle in radians, I just use theta = degrees*Math.PI/180. The best thing I can guess is the order of your matrix multiplications could be wrong. In my example, the matrix converts from rectangle-relative space to map space (so the inverse does the reverse to transform a map space point to rectangle space).&nbsp; Without seeing your code, there's not much I can help with though.
1463117425

Edited 1463117454
Ada L.
Marketplace Creator
Sheet Author
API Scripter
If you want to just avoid all the fancy mathemagicks, you can also just use TokenCollisions v1.3's isOverlapping function, which treats a token as rectangular if it has a square aura. So for your area of effect attack, for each character token you could do: if(TokenCollisions.isOverlapping(charToken, rectToken)) { &nbsp;doTheThing(); }
I glanced at TokenCollision last night. Unfortunately, my areas aren't square, so that's not going to work for me. I'll post up a better representation of my code. I can also send you my full script if you like, but it's a nightmare right now.
Here's my entire isPointInRectangle function. rectToken is 6.5 squares long and 1 square wide (with a square being 70 px in this instance) and represents a line area attack. pointX and pointY represent the center (left + width/2, top + height/2) of a token on the grid. We want to know if this token is being targeted by this area attack. Interestingly, if the rectToken is rotated at 45 degrees (pointing up and left), the code nearly works. It can detect a target in its bounds, but it also detects a target that should be one square out of its reach length-wise. isPointInRectangle = function(rectToken, pointX, pointY) { &nbsp;&nbsp;&nbsp; var rectTop = rectToken.get('top'); &nbsp;&nbsp;&nbsp; var rectLeft = rectToken.get('left'); &nbsp;&nbsp;&nbsp; var rotation = rectToken.get('rotation'); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; var degrees = -(rotation-90); &nbsp;&nbsp;&nbsp; while (degrees &lt; 0) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; degrees += 360; &nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; var theta = degrees * Math.PI / 180; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; var rectX = rectToken.get('left'); &nbsp;&nbsp;&nbsp; var rectY = rectToken.get('top'); &nbsp;&nbsp;&nbsp; var halfWidth = rectToken.get('width')/2; &nbsp;&nbsp;&nbsp; var halfHeight = rectToken.get('height')/2; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; // Get the rectangle's inverse transform. &nbsp;&nbsp;&nbsp; var m = MatrixMath.multiply( &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MatrixMath.translate([rectX, rectY]), &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MatrixMath.rotate(theta) &nbsp;&nbsp;&nbsp; ); &nbsp;&nbsp;&nbsp; var mInv = MatrixMath.inverse(m); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; // The point represented as homogenous 2D coordinates. &nbsp;&nbsp;&nbsp; var p = [pointX, pointY, 1]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; // Use the rectangle's inverse transform to convert &nbsp;&nbsp;&nbsp; // the point to a coordinate space where the rectangle &nbsp;&nbsp;&nbsp; // is not rotated and its center is at the origin. &nbsp;&nbsp;&nbsp; var pTrans = MatrixMath.multiply(mInv, p); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &nbsp;&nbsp;&nbsp; // The point is inside the rectangle if the transformed point is inside &nbsp;&nbsp;&nbsp; // the transformed rectangle (unrotated with its center at the origin). &nbsp;&nbsp;&nbsp; return ( Math.abs(pTrans[0]) &lt;= halfWidth && Math.abs(pTrans[1]) &lt;= halfHeight); }
1463156624

Edited 1463156757
Ada L.
Marketplace Creator
Sheet Author
API Scripter
Ok, it looks like your matrix operations are correct. Your angle math doesn't look right though, and your formulas for pointX/Y are actually giving you the bottom-right point of the town. Token.get("left") and token.get("top") actually return the center point of the token. For the angle, just use var theta = rectToken.get("rotation")*Math.PI/180. It's true that in the map coordinates, y is downward, but the math works fine since rotations are clockwise in the system (in a system where y is up, rotations are counter-clockwise).
I get what you were saying now. That works! Thanks a ton.