Difference between revisions of "Script:Collision Detection"
From Roll20 Wiki
m (→Installation) |
m (235259 moved page User:235259/Collision Detection to Script:Collision Detection) |
Revision as of 17:58, 8 January 2015
This script will watch for collisions between tokens and a subset of the paths on the player page. When an event occurs, some configurable behavior will be applied.
Installation
There are three configuration variables near the top of the script. You may alter them to customize the script functionality:
- coldtc.pathColor – The script only considers paths of a specific color, allowing you to also use paths of other colors which your players will not collide with. By default, this is fuchsia; the color is specified as a hexadecimal web color, which you can see when selecting a color from the drawing interface. A path's fill color is ignored.
- coldtc.layer – The script will only look at paths on the specified layer (map, objects, gmlayer, or walls). You can also set this value to "all" and paths on every layer will be considered.
- coldtc.behavior – There are currently three different behaviors dictating how the script will act when a collision event occurs:
- coldtc.DONT_MOVE – return the token to its starting position prior to the movement
- coldtc.WARN_PLAYER – sends a message to the token's controller warning that the token isn't supposed to be there
- coldtc.STOP_AT_WALL – the token will be moved 1 pixel away from the wall it collided with
Usage Notes
Currently, this script only considers polygons and polylines as "walls" to collide with, which means no freehand drawings or circles/ovals. Additionally, the math in the script does not handle paths which have been resized or rotated.
Tokens which can only be moved by the GM (no player is assigned to control it, and the token isn't linked to a character sheet which is assigned to any player) do not collide with walls. This will let the GM move things around at will. However, if the GM is assigned as the controlling player for a token, or if the token is linked to a character sheet which has the GM assigned as the controlling player, the token will collide with the walls.
The script will break if you go "warp speed" by holding down an arrow key to move, and the token passes through multiple walls before the script catches up. (If you drag a token through multiple walls, the script will collide at the first one.)
In most cases, the dynamic lighting will not update before the token's position is reset to the correct side of the wall (assuming a relevant behavior is set), meaning the player won't see what's on the other side (if the wall is on the DL layer or there's a similarly-positioned wall on the DL layer). However, sometimes the DL will update first, and the player will catch a glimpse of the other side.
Code
var coldtc = coldtc || {}; coldtc.polygonPaths = []; coldtc.DONT_MOVE = 1; coldtc.WARN_PLAYER = 2; coldtc.STOP_AT_WALL = 4; /*** SCRIPT SETTINGS ***/ coldtc.pathColor = '#ff00ff'; // Only paths with this color will be used for collisions coldtc.layer = 'walls'; // Only paths on this layer will be used for collisions; set to 'all' to use all layers coldtc.behavior = coldtc.STOP_AT_WALL|coldtc.WARN_PLAYER; // behavior for collision events on('add:path', function(obj) { if(obj.get('pageid') != Campaign().get('playerpageid') || obj.get('stroke').toLowerCase() != coldtc.pathColor) return; if(coldtc.layer != 'all' && obj.get('layer') != coldtc.layer) return; var path = JSON.parse(obj.get('path')); if(path.length > 1 && path[1][0] != 'L') return; // Add fushcia paths on current page's gm layer coldtc.polygonPaths.push(obj); }); on('destroy:path', function(obj) { for(var i = 0; i < coldtc.polygonPaths.length; i++) { if(coldtc.polygonPaths[i].id == obj.id) { coldtc.polygonPaths = coldtc.polygonPaths.splice(i, 1); // Delete path if they're the same break; } } }); on('change:path', function(obj, prev) { if(coldtc.layer == 'all') return; // changing path layer doesn't matter if(obj.get('layer') == coldtc.layer && prev.layer != coldtc.layer) // May need to add to list { if(obj.get('pageid') != Campaign().get('playerpageid') || obj.get('stroke').toLowerCase() != coldtc.pathColor) return; var path = JSON.parse(obj.get('path')); if(path.length > 1 && path[1][0] != 'L') return; coldtc.polygonPaths.push(obj); } if(obj.get('layer') != coldtc.layer && prev.layer == coldtc.layer) // May need to remove from list { for(var i = 0; i < coldtc.polygonPaths.length; i++) { if(coldtc.polygonPaths[i].id == obj.id) { coldtc.polygonPaths = coldtc.polygonPaths.splice(i, 1); break; } } } }); on('change:graphic', function(obj, prev) { if(obj.get('subtype') != 'token' || (obj.get('top') == prev.top && obj.get('left') == prev.left)) return; if(obj.get('represents') != '') { var character = getObj('character', obj.get('represents')); if(character.get('controlledby') == '') return; // GM-only token } else if(obj.get('controlledby') == '') return; // GM-only token var l1 = coldtc.L(coldtc.P(prev.left, prev.top), coldtc.P(obj.get('left'), obj.get('top'))); _.each(coldtc.polygonPaths, function(path) { var pointA, pointB; var x = path.get('left') - path.get('width') / 2; var y = path.get('top') - path.get('height') / 2; var parts = JSON.parse(path.get('path')); pointA = coldtc.P(parts[0][1] + x, parts[0][2] + y); parts.shift(); _.each(parts, function(pt) { pointB = coldtc.P(pt[1] + x, pt[2] + y); var l2 = coldtc.L(pointA, pointB); var denom = (l1.p1.x - l1.p2.x) * (l2.p1.y - l2.p2.y) - (l1.p1.y - l1.p2.y) * (l2.p1.x - l2.p2.x); if(denom != 0) // Parallel { var intersect = coldtc.P( (l1.p1.x*l1.p2.y-l1.p1.y*l1.p2.x)*(l2.p1.x-l2.p2.x)-(l1.p1.x-l1.p2.x)*(l2.p1.x*l2.p2.y-l2.p1.y*l2.p2.x), (l1.p1.x*l1.p2.y-l1.p1.y*l1.p2.x)*(l2.p1.y-l2.p2.y)-(l1.p1.y-l1.p2.y)*(l2.p1.x*l2.p2.y-l2.p1.y*l2.p2.x) ); intersect.x /= denom; intersect.y /= denom; if(coldtc.isBetween(pointA, pointB, intersect) && coldtc.isBetween(l1.p1, l1.p2, intersect)) { // Collision event! if((coldtc.behavior&coldtc.DONT_MOVE) == coldtc.DONT_MOVE) { obj.set({ left: Math.round(l1.p1.x), top: Math.round(l1.p1.y) }); } if((coldtc.behavior&coldtc.WARN_PLAYER) == coldtc.WARN_PLAYER) { var who; if(obj.get('represents')) { var character = getObj('character', obj.get('represents')); who = character.get('name'); } else { var controlledby = obj.get('controlledby'); if(controlledby == 'all') who = 'all'; else { var player = getObj('player', controlledby); who = player.get('displayname'); } } who = who.indexOf(' ') > 0 ? who.substring(0, who.indexOf(' ')) : who; if(who != 'all') sendChat('SYSTEM', '/w '+who+' You are not permitted to move that token into that area.'); else sendChat('SYSTEM', 'Token '+obj.get('name')+' is not permitted in that area.'); } if((coldtc.behavior&coldtc.STOP_AT_WALL) == coldtc.STOP_AT_WALL) { var vec = coldtc.P(l1.p2.x - l1.p1.x, l1.p2.y - l1.p1.y); var norm = Math.sqrt(vec.x * vec.x + vec.y * vec.y); vec.x /= norm; vec.y /= norm; obj.set({ left: intersect.x - vec.x, top: intersect.y - vec.y }); } } } pointA = coldtc.P(pointB.x, pointB.y); }); }); }); coldtc.P = function(x, y) { return {x: x, y: y}; }; coldtc.L = function(p1, p2) { return {p1: p1, p2: p2}; }; coldtc.isBetween = function(a, b, c) { var withinX = (a.x <= c.x && c.x <= b.x) || (b.x <= c.x && c.x <= a.x); var withinY = (a.y <= c.y && c.y <= b.y) || (b.y <= c.y && c.y <= a.y); return withinX && withinY; };