Character Vault
Any Concept / Any System
Compendium
Your System Come To Life
Roll20 for Android
Streamlined for your Tablet
Roll20 for iPad
Streamlined for your Tablet

Personal tools

Difference between revisions of "Script:Collision Detection"

From Roll20 Wiki

Jump to: navigation, search
m (Installation)
m
 
(5 intermediate revisions by one user not shown)
Line 1: Line 1:
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.
+
{{script overview
 +
|name=Collision Detection
 +
|author={{user profile|235259|Brian}}
 +
|version=2.2
 +
|lastmodified=2016-04-12}}
  
==== Installation ====
+
'''Collision Detection''' 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.
 +
<br clear="all">
  
There are three configuration variables near the top of the script. You may alter them to customize the script functionality:
+
=== Installation ===
  
<ul><li>'''coldtc.pathColor''' &ndash; 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.</li>
+
There are three configuration values. You may alter them to customize the script functionality:
<li>'''coldtc.layer''' &ndash; 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.</li>
+
<li>'''coldtc.behavior''' &ndash; There are currently three different behaviors dictating how the script will act when a collision event occurs:
+
<ul><li>coldtc.DONT_MOVE &ndash; return the token to its starting position prior to the movement</li>
+
<li>coldtc.WARN_PLAYER &ndash; sends a message to the token's controller warning that the token isn't supposed to be there</li>
+
<li>coldtc.STOP_AT_WALL &ndash; the token will be moved 1 pixel away from the wall it collided with</li></ul>
+
You can combine multiple behaviors by using the bitwise OR operator, "|". Example: to use DONT_MOVE and WARN_PLAYER, the behavior should be set to "coldtc.DONT_MOVE|coldtc.WARN_PLAYER". STOP_AT_WALL overrides DONT_MOVE if both are set.</li></ul>
+
  
==== Usage Notes ====
+
* '''Path Color''' &ndash; 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.
 +
* '''Layer''' &ndash; 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.
 +
* '''Behavior''' &ndash; There are currently three different behaviors dictating how the script will act when a collision event occurs:
 +
** "Don't Move" &ndash; return the token to its starting position prior to the movement
 +
** "Warn Player" &ndash; sends a message to the token's controller warning that the token isn't supposed to be there
 +
** "Stop at Wall" &ndash; the token will be moved 1 pixel away from the wall it collided with
 +
Any two behaviors can be combined except for "Don't Move" with "Stop at Wall".
 +
 
 +
=== 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.
 
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.
Line 23: Line 30:
 
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.
 
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 ====
+
=== Changelog ===
 
+
{{changelog version|2.2|2016-04-12|* Update globalconfig support}}
<pre data-language="javascript">
+
{{changelog version|2.1|2016-03-09|* Update for one-click install}}
var coldtc = coldtc || {};
+
{{changelog version|2.0|2015-01-08|* Release}}
 
+
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;
+
};
+
</pre>
+
[[Category:User API Scripts|Collision Detection]]
+

Latest revision as of 04:42, 13 April 2016

API ScriptAuthor: Brian
Version: 2.2
Last Modified: 2016-04-12
Code: Collision Detection
Dependencies: None
Conflicts: None

Collision Detection 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.

[edit] Installation

There are three configuration values. You may alter them to customize the script functionality:

  • Path Color – 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.
  • 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.
  • Behavior – There are currently three different behaviors dictating how the script will act when a collision event occurs:
    • "Don't Move" – return the token to its starting position prior to the movement
    • "Warn Player" – sends a message to the token's controller warning that the token isn't supposed to be there
    • "Stop at Wall" – the token will be moved 1 pixel away from the wall it collided with

Any two behaviors can be combined except for "Don't Move" with "Stop at Wall".

[edit] 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.

[edit] Changelog

v2.2 (2016-04-12)

  • Update globalconfig support


v2.1 (2016-03-09)

  • Update for one-click install


v2.0 (2015-01-08)

  • Release