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

[Script] Patrols using Waypoints

1455994821

Edited 1457374835
Matt
Pro
I had written a patrol script a while back and wanted to use it in a current campaign only to discover it was in need of an update. Now that I have it where I can easily use it in my game, thought I'd post the updated script. For a simple concept of moving a token, the instructions seem more complex than they really are. If you're interested in this script and have any questions, feel free to reach out to me. Before getting into the script, I must point out that the system uses a token's last move to create the patrol paths. If you're unfamiliar with creating waypoint moves with a token, here's the link to Roll20's wiki page on the topic. <a href="https://wiki.roll20.net/Manipulating_Graphics#Wayp" rel="nofollow">https://wiki.roll20.net/Manipulating_Graphics#Wayp</a>... What the Script Does The script moves tokens along a predefined path at 5 second intervals. Multiple tokens can follow the same path. The distance each token moves with each refresh can be independently set. Tokens can rotate to face the direction they are moving. All patrols are stopped when the turn order window is opened, and resume when it is closed. If a token is moved manually, it will stop patrolling while others continue. A stopped patrol can resume patrol with a command. Menu Commands Most of the common commands for setup can be given from the menu. However, there is a limitation with the menu in that the Campaign Ribbon must be on the map in which you will be creating paths. This is so the drop lists for buttons are created properly. (Commands entered manually do not have this restriction.) !wp menu Create a Patrol - starts moving selected tokens along a predefined patrol path. Create a Path - records the selected token's last move as a named patrol path in state data. Delete a Path - deletes a named patrol path from state data. Show All Paths - draws each patrol path onto the GM layer and labels each with the patrol name. Hide All Paths - deletes each drawn path and label from the GM layer. &nbsp;This does not delete the patrol path from state data. Commands Every menu command can also be entered directly into the chat window. Commands begin with the api command, then the action to take, then an optional list of parameters. The available list of actions and parameters and their explanations are listed below. Parameters are entered in&nbsp; key:value format !wp &lt;action&gt; &lt;optional parameters&gt; examples !wp addpath North Hall !wp patrol path:North Hall mode:0 speed:5 rotation:false !wp modify random:true actions !wp on - turn on all patrols. !wp off - turn off all patrols. !wp resume - restart a patrolling token that had stopped due to being manually moved. !wp help - displays the menu commands in chat, identical to !wp menu !wp menu - displays the menu commands in chat, identical to !wp help !wp addpath &lt;pathname&gt; - (same as Add Path button) saves a token's last move as a named patrol. !wp deletepath &lt;pathname&gt; - (same as Delete Path button) deletes a named patrol path. !wp patrol &lt;parameters&gt; - (same as Create Patrol button) sets the selected token(s) to follow the named patrol path. Additional parameters can be given to customize the patrolling token's movement !wp modify &lt;parameters&gt; - updates the selected token's patrol movement based on the given parameters. This only affects tokens that are already patrolling. !wp stoppatrol - stops a token from automatically patrolling. Has the same effect as if the token were moved manually. !wp showpaths - (same as Show Paths button) draws each saved path onto the GM layer and adds a text label to each path. !wp hidepaths - (same as Hide Paths button) deletes any drawn paths with showpaths command. This does not delete the patrol path, only the drawn lines. parameters path - (text, case insensitive) the name of path to follow mode - (integer -1, 0, or 1) defines how to follow the path, the default is 0 speed - how many units to move with each refresh. &nbsp;distance is calculated in straight-line distance. &nbsp;the default is 5 rotation - (0 to 360, or false) defines how a token rotates to face it's next point, or if no rotation should occur, the default is false random - (true/false) gives a token a 20% on each refresh to not make a patrol move, the default is false phase - (integer, 0-100) defines the percentage of time a token is phased out, the default is 0 Additional Parameter Information Mode: There are three modes for a token to patrol. A patrol mode of (1) tells a token to walk the patrol path in order, and when it gets to the last point of its patrol path its next move should be the first point of the path. A patrol mode of (-1) tells a token to walk the patrol path in reverse order, and when it gets to the first point of its patrol path its next move should be the last point of the path. A patrol mode of (0) tells a token to walk the patrol path in order, and when it gets to the last point of a patrol path, it should start walking the path in reverse order. Rotation : The rotation value is the number of degrees that a token needs to be rotated clockwise in order to face down. Tokens are not all created the same, and the script has no way of knowing which way a token is facing. I've found that most tokens are created with the token facing down (see the first token image). If I wanted this token to face the direction its moving when patrolling, I would use the parameter command of rotation:0 because the token needs no additional rotation to normally face down. However, if I have a token that normally is facing to the left (see the second token image), I would need to rotate the token an additional 270 degrees clockwise to have the token facing down. So to have this token face the direction it's moving as it is patrolling, I would use the parameter command of rotation:270 . A parameter of rotation:false will not rotate the token at all. It will retain the angle it had at the moment it started it's patrol. Random : This option is to add a little uncertainty to the patrol path. With random:true , on each refresh a percentage roll is made. If that roll is less than 20%, then the patrolling token does not move this refresh. With random:false , a token will move on every refresh. Phase : This option causes a patrolling token to move between the objects layer and GM layer. From the player's perspective, the patrolling token disappears. With each refresh, a percentage roll is made. If the roll is less than the phase value, the token is moved to gmlayer, otherwise it will appear on the objects layer. Using phase:0 or phase:false will disable phasing. Configuration Options WPP.Command - the api command given from the chat window WPP.SpeakAs - who the script sends its whispers as WPP.RefreshRate - the refresh rate in seconds, 5 seconds is the default, recommended not lower than 3, limited to no lower than 1 WPP.RetainLastMove - if the last move of patrol tokens is retained in lastmove WPP.ShowPaths - draw patrol paths on GM layer when a path is created or deleted Code state.WaypointPatrol = state.WaypointPatrol || {'PatrolStatus': true} var WPP = {}; WPP.Command = '!wp'; WPP.SpeakAs = 'Patrol'; WPP.RefreshRate = 5; // in seconds, minimum 1 WPP.RetainLastMove = true; //save patrol moves as lastmove WPP.ShowPaths = true; //refresh visible paths on add or delete WPP.AddPatrolPath = function(selected, patrolname) { //make sure the patrol name is provided if (!patrolname || patrolname.trim() === '' ) { return 'Err: patrol name missing'; } patrolname = patrolname.toLowerCase(); //verify only one token is selected if (!selected) { return 'Err: no token selected'; } if (selected.length !== 1) { return 'Err: multiple tokens selected'; } if (!selected[0]['_type'] || selected[0]['_type'] !== 'graphic') { return 'Err: selected must be a graphic'; } //get the token object var token = getObj('graphic', selected[0]['_id']); if (!token) { return 'Err: token not found'; } //read the patrol path from token's last move var lastmove = token.get('lastmove'); if (lastmove === '') { return 'Err: token missing last move'; } lastmove = lastmove.split(','); var pts = []; while(lastmove.length &gt; 0) { pts.push({ 'left': +lastmove.shift(), 'top' : +lastmove.shift(), }); } pts.push({ 'left': token.get('left'), 'top' : token.get('top'), }); //add patrol path to state if it doesn't already exist var pageid = token.get('_pageid'); if (!state.WaypointPatrol['paths']) { state.WaypointPatrol['paths'] = {}; } if (!state.WaypointPatrol['paths'][pageid]) { state.WaypointPatrol['paths'][pageid] = {}; } if (state.WaypointPatrol['paths'][pageid][patrolname]) { return 'Err: name already exists'; } state.WaypointPatrol['paths'][pageid][patrolname] = pts; if (WPP.ShowPaths) { WPP.ShowPatrolPaths(); } WPP.PatrolHelpMenu(); return 'Success: patrol path added'; } WPP.DeletePatrolPath = function(patrolname) { //make sure the patrol name is provided if (!patrolname || patrolname.trim() === '' ) { return 'Err: patrol name missing'; } patrolname = patrolname.toLowerCase(); //delete the patrol name if it exists var pageid = Campaign().get('playerpageid'); if (!state.WaypointPatrol['paths'][pageid] || !state.WaypointPatrol['paths'][pageid][patrolname]) { return 'Err: patrol name does not exist for this map'; } delete state.WaypointPatrol['paths'][pageid][patrolname]; if (WPP.ShowPaths) { WPP.ShowPatrolPaths(); } WPP.PatrolHelpMenu(); return 'Success: patrol path deleted.'; } WPP.PatrolHelpMenu = function() { //create a bracketed list of avaialble patrols for the current map var pageid = Campaign().get('playerpageid'); var paths = []; if (state.WaypointPatrol['paths'] && state.WaypointPatrol['paths'][pageid] && _.size(state.WaypointPatrol['paths'][pageid]) &gt; 0) { paths = _.keys(state.WaypointPatrol['paths'][pageid]); } var code = encodeURIComponent(':'); var out = ''; &nbsp; &nbsp; out = '&lt;div style="background-color: #FFF; border: 1px solid #000; border-radius: 0.5em; margin-left: 2px; margin-right: 2px; padding-top: 5px; padding-bottom: 5px;"&gt;' &nbsp; &nbsp; &nbsp; &nbsp; + '&lt;div style="font-weight: bold; text-align: center; border-bottom: 1px solid black; font-size: 100%"&gt;Map: ' + (getObj('page', pageid).get('name') || '(Untitled)') + '&lt;/div&gt;' &nbsp; &nbsp; &nbsp; &nbsp; + '&lt;div style="padding-left: 5px; padding-right: 5px; overflow: hidden; font-size: 90%"&gt;'; &nbsp; &nbsp; if (paths.length &gt; 0) { &nbsp; &nbsp; &nbsp; &nbsp; out += '&lt;div style="font-weight: bold; padding-top: 5px; padding-bottom: 5px;"&gt;Create a Patrol&lt;a style="float: right; padding: 0px 5px 0px 5px; margin: 0px 5px 0px 5px; border: 1px solid black; border-radius: 0.5em; background-color: #999999; color: #FFF" href="' + WPP.Command + ' patrol path' + code +'?{Name of the path|'+ paths.join('|') + '} mode' + code + '?{Mode|Forward and Backwards,0|Forward Only,1|Backwards Only,-1} speed' + code + '?{Distance moved on each refresh|5} rotation' + code + '?{Angle modifier to face waypoint|Do Not Rotate,false|0 degrees,0|90 degrees,90|180 degrees,180|270 degrees,270}"&gt;Patrol&lt;/a&gt;&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; out += '&lt;li style="padding-left: 10px;"&gt;Starts moving a token graphic along an existing patrol path.&lt;/li&gt;'; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; out += '&lt;div style="padding-top: 5px; padding-bottom: 5px;"&gt;You must add a path to this map from a token\'s last move before creating a patrol.&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; out += '&lt;div style="padding-top: 5px; padding-bottom: 5px;"&gt;Place a token to where you want the patrol path to begin. &nbsp;Move the token to where you want the patrol path to end. &nbsp;You can press the space bar to create turns in the path as you\'re moving the token. &nbsp;When the token is in the final position, click the \'Path\' button&lt;/div&gt;' &nbsp; &nbsp; } &nbsp; &nbsp; out += '&lt;div style="font-weight: bold; padding-top: 5px; padding-bottom: 5px;"&gt;Create a Path&lt;a style="float: right; padding: 0px 5px 0px 5px; margin: 0px 5px 0px 5px; border: 1px solid black; border-radius: 0.5em; background-color: #999999; color: #FFF" href="' + WPP.Command + ' addpath ?{Name of the new path}"&gt;Path&lt;/a&gt;&lt;/div&gt;'; &nbsp; &nbsp; out += '&lt;li style="padding-left: 10px;"&gt;Saves a token\'s last move as a named patrol path for other tokens to follow.&lt;/li&gt;'; &nbsp; &nbsp; if (paths.length &gt; 0){ &nbsp; &nbsp; &nbsp; &nbsp; out += '&lt;div style="font-weight: bold; padding-top: 5px; padding-bottom: 5px;"&gt;Delete a Path&lt;a style="float: right; padding: 0px 5px 0px 5px; margin: 0px 5px 0px 5px; border: 1px solid black; border-radius: 0.5em; background-color: #999999; color: #FFF" href="' + WPP.Command + ' deletepath ?{Name of existing path to delete|' + paths.join('|') + '}"&gt;Delete&lt;/a&gt;&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; out += '&lt;li style="padding-left: 10px;"&gt;Deletes an existing patrol path.&lt;/li&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; out += '&lt;div style="font-weight: bold; padding-top: 5px; padding-bottom: 5px;"&gt;Show All Paths&lt;a style="float: right; padding: 0px 5px 0px 5px; margin: 0px 5px 0px 5px; border: 1px solid black; border-radius: 0.5em; background-color: #999999; color: #FFF" href="' + WPP.Command + ' showpaths"&gt;Show&lt;/a&gt;&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; out += '&lt;li style="padding-left: 10px;"&gt;Draws onto the GM layer all existing patrol paths and labels them.&lt;/li&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; out += '&lt;div style="font-weight: bold; padding-top: 5px; padding-bottom: 5px;"&gt;Hide All Paths&lt;a style="float: right; padding: 0px 5px 0px 5px; margin: 0px 5px 0px 5px; border: 1px solid black; border-radius: 0.5em; background-color: #999999; color: #FFF" href="' + WPP.Command + ' hidepaths"&gt;Hide&lt;/a&gt;&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; out += '&lt;li style="padding-left: 10px;"&gt;Hides all drawn paths and labels.&lt;/li&gt;'; &nbsp; &nbsp; } &nbsp; &nbsp; out += '&lt;/div&gt;&lt;/div&gt;'; &nbsp; &nbsp; sendChat(WPP.SpeakAs, '/w GM ' + out); return; } WPP.ShowPatrolPaths = function() { WPP.HidePatrolPaths(); if (!state.WaypointPatrol['paths']) { return; } state.WaypointPatrol['traces'] = state.WaypointPatrol['traces'] || []; state.WaypointPatrol['labels'] = state.WaypointPatrol['labels'] || []; for(pageid in state.WaypointPatrol['paths']) { for(patrolname in state.WaypointPatrol['paths'][pageid]) { var pts = []; var ul = {'left': Infinity, 'top': Infinity}; var lr = {'left': 0, 'top': 0}; for (var pt = 0; pt &lt; state.WaypointPatrol['paths'][pageid][patrolname].length ; pt++) { var curr = state.WaypointPatrol['paths'][pageid][patrolname][pt]; ul = {'left': Math.min(ul['left'],curr['left']), 'top': Math.min(ul['top'], curr['top'])}; lr = {'left': Math.max(lr['left'],curr['left']), 'top': Math.max(lr['top'], curr['top'])}; pts.push(['L', curr['left'], curr['top']]); }; state.WaypointPatrol['traces'].push(createObj('path', { '_pageid' : pageid, '_path' : JSON.stringify(pts), //see notes 'stroke' : '#ff9900', 'stroke_width': 14, 'layer' : 'gmlayer', 'width' : lr['left'] - ul['left'], 'height' : lr['top'] - ul['top'], 'left' : (lr['left'] - ul['left']) / 2 + ul['left'], 'top' : (lr['top'] - ul['top']) / 2 + ul['top'], }).get('_id')); for ( var pt = 0; pt &lt; state.WaypointPatrol['paths'][pageid][patrolname].length - 1; pt++) { var p1 = state.WaypointPatrol['paths'][pageid][patrolname][pt]; var p2 = state.WaypointPatrol['paths'][pageid][patrolname][pt + 1]; state.WaypointPatrol['labels'].push(createObj('text', { '_pageid' : pageid, 'text' : 'path: ' + patrolname, 'font_size' : 14, 'color' : 'rgb(0,0,0)', 'font_family': 'Arial', 'layer' : 'gmlayer', 'left' : Math.abs(p1['left']-p2['left']) / 2 + Math.min(p1['left'],p2['left']), 'top' : Math.abs(p1['top']-p2['top']) / 2 + Math.min(p1['top'],p2['top']), 'rotation' : ((p2['left'] &lt; p1['left']) ? Math.atan2(p1['top'] - p2['top'], p1['left'] - p2['left']) : Math.atan2(p2['top'] - p1['top'], p2['left'] - p1['left'])) * 180 / Math.PI }).get('_id')); } } } WPP.ShowPaths = true; } WPP.HidePatrolPaths = function() { if (state.WaypointPatrol['traces']) { while (state.WaypointPatrol['traces'].length &gt; 0) { var path = getObj('path', state.WaypointPatrol['traces'].shift()); if (path) { path.remove(); } } } if (state.WaypointPatrol['labels']) { while (state.WaypointPatrol['labels'].length &gt; 0) { var text = getObj('text', state.WaypointPatrol['labels'].shift()); if (text) { text.remove(); } } } WPP.ShowPaths = false; } WPP.SetTokenToPatrol = function(selected, opts) { //cehck to see if a token has been selected if (!selected || selected.length === 0) { return 'Err: no token selected'; } if (!state.WaypointPatrol['tokens']) { state.WaypointPatrol['tokens'] = {}; } if (!opts || !opts['patrolname'] || opts['patrolname'].trim() === '' ) { return 'Err: patrol name missing'; } opts['patrolname'] = opts['patrolname'].toLowerCase(); var pageid = getObj(selected[0]['_type'], selected[0]['_id']).get('_pageid'); if (!state.WaypointPatrol['paths'] || !state.WaypointPatrol['paths'][pageid] || !state.WaypointPatrol['paths'][pageid][opts['patrolname']]) { return 'Err: patrol name does not exist for this map'; } var points = state.WaypointPatrol['paths'][pageid][opts['patrolname']]; for (i in selected) { if (selected[i]['_type'] === 'graphic') { state.WaypointPatrol['tokens'][selected[i]['_id']] = { 'patrolname' : opts['patrolname'], 'active' : opts['active'] || true, 'patrolmode' : opts['patrolmode'] || 0, 'movementrate' : opts['movementrate'] || getObj('page', pageid).get('scale_number'), 'rotation' : isNaN(opts['rotation']) ? false : opts['rotation'], 'random' : opts['random'] || false, 'phase' : opts['phase'] || 0, }; //move token to closest point on patrol path var obj = getObj('graphic', selected[i]['_id']); WPP.MoveTokenToPath(obj, points); } } return 'Success: patrol added'; } WPP.ModifyPatrollingToken = function(selected, opts) { if(!selected || selected.length === 0) { return 'Err: no token selected'; } var pageid = getObj(selected[0]['_type'], selected[0]['_id']).get('_pageid'); if('patrolname' in opts && !state.WaypointPatrol['paths'][pageid][opts['patrolname']]) { return 'Err: patrol name does not exist for this map';} if (state.WaypointPatrol['tokens']) { for (o in selected) { if (state.WaypointPatrol['tokens'][selected[o]['_id']]) { for (opt in opts) { if (opts.hasOwnProperty(opt)) { state.WaypointPatrol['tokens'][selected[o]['_id']][opt] = opts[opt]; switch(opt) { case 'patrolname': WPP.MoveTokenToPath(getObj('graphic',selected[o]['_id']), state.WaypointPatrol['paths'][pageid][opts[opt]]); break; } } } } } } return 'Success: patrol modified'; } WPP.RemoveTokenFromPatrol = function(selected) { if (!selected || selected.length === 0) { return 'Err: no token selected'; } if (state.WaypointPatrol['tokens']) { for (o in selected) { if (selected[o]['_type'] == 'graphic') { delete state.WaypointPatrol['tokens'][selected[o]['_id']]; } } } return 'Success: patrols removed'; } WPP.MovePatrols = function() { //loop through each token in state data, cleanup deleted if(!state.WaypointPatrol['tokens']) { return; } for (id in state.WaypointPatrol['tokens']) { //validate object exists var obj = getObj('graphic', id); if (!obj) { delete state.WaypointPatrol['tokens'][id]; continue; } //validate patrol exists var pageid = obj.get('_pageid'); var patrolname = state.WaypointPatrol['tokens'][id]['patrolname']; if (!patrolname || !state.WaypointPatrol['paths'][pageid] || !state.WaypointPatrol['paths'][pageid][patrolname]) { obj.set({'layer':'objects'}); delete state.WaypointPatrol['tokens'][id]; continue; } //only patrol active units if (!state.WaypointPatrol['tokens'][obj.get('_id')]['active']) { continue; } //check for random movement, 20% chance the token doesn't move if (state.WaypointPatrol['tokens'][obj.get('_id')]['random'] && Math.random() &lt; 0.2) { continue; } //set up the proper path var points = state.WaypointPatrol['paths'][pageid][patrolname].slice(0); switch(state.WaypointPatrol['tokens'][id]['patrolmode']) { case 1: //forward patrolling break; case -1: //reverse patrolling points.reverse(); break; default: //back and forth patrolling points = points.concat(points.slice(0).reverse()); break; } //set up control variables var nextindex = state.WaypointPatrol['tokens'][id]['nextpointindex'] &gt; points.length -1 ? 0 : state.WaypointPatrol['tokens'][id]['nextpointindex'] ; var moverate = state.WaypointPatrol['tokens'][id]['movementrate'] &lt;= 0 ? 5 : state.WaypointPatrol['tokens'][id]['movementrate']; var distremain = moverate * 70 / getObj('page', obj.get('_pageid')).get('scale_number'); var newpoint = {'left': obj.get('left'), 'top': obj.get('top')}; var lastmove = [newpoint['left'], newpoint['top']]; //move along the path based on the movement rate of the token do { var disttonext = WPP.Distance(newpoint, points[nextindex]); if (distremain &gt; disttonext) { newpoint = points[nextindex]; lastmove.push(points[nextindex]['left'],points[nextindex]['top']); distremain -= disttonext; nextindex = (nextindex &gt;= points.length - 1) ? 0 : nextindex + 1; } else { var moveratio = distremain / disttonext; var newrotation = (state.WaypointPatrol['tokens'][id]['rotation'] === false) ? obj.get('rotation') : Math.atan2(points[nextindex]['top'] - newpoint['top'], points[nextindex]['left'] - newpoint['left']) * 180 / Math.PI - 90 + state.WaypointPatrol['tokens'][id]['rotation'] ;; newpoint = { 'left': moveratio * (points[nextindex]['left'] - newpoint['left']) + newpoint['left'], 'top' : moveratio * (points[nextindex]['top'] - newpoint['top']) + newpoint['top'], }; obj.set({ 'left' : newpoint['left'], 'top' : newpoint['top'], 'lastmove' : WPP.RetainLastMove ? lastmove.join(',') : '', 'layer' : Math.random() &lt; state.WaypointPatrol['tokens'][id]['phase'] ? 'gmlayer' : 'objects' , 'rotation' : newrotation, }); state.WaypointPatrol['tokens'][id]['nextpointindex'] = nextindex; distremain = 0; } } while (distremain &gt; 0); } } WPP.Resume = function(selected) { if(!selected || !state.WaypointPatrol['tokens']) { return; } for (id in selected) { if (state.WaypointPatrol['tokens'][selected[id]['_id']]) { //set the token patrol status to active state.WaypointPatrol['tokens'][selected[id]['_id']]['active'] = true; //move token to the closest point on path var obj = getObj('graphic', selected[id]['_id']); WPP.MoveTokenToPath(obj, state.WaypointPatrol['paths'][obj.get('_pageid')][state.WaypointPatrol['tokens'][selected[id]['_id']]['patrolname']]); } } } WPP.Distance = function(p1, p2) { return Math.sqrt(Math.pow((p1['left'] - p2['left']),2) + Math.pow((p1['top'] - p2['top']),2)); } WPP.MoveTokenToPath = function(obj, path) { //set up control variables var pt = {'left': obj.get('left'), 'top': obj.get('top')}; var newpt = pt; var nextid = 0; var delta = {'left': 0, 'top': 0}; var dmin = Infinity; //loop through each path segment and find the closest point for( var i = 0; i &lt; path.length-1; i++) { //get the next line segment in the path var p1 = path[i]; var p2 = path[i+1]; //calculate the closest point on the segment to the token var delta = {'left': p2['left'] - p1['left'], 'top': p2['top'] - p1['top']}; var u = ((pt['left'] - p1['left']) * delta['left'] + (pt['top'] - p1['top']) * delta['top']) / (Math.pow(delta['left'], 2) + Math.pow(delta['top'], 2)); u = ( u &gt; 1 ) ? 1 : (u &lt; 0) ? 0 : u; var testpt = {'left': p1['left'] + u * delta['left'], 'top': p1['top'] + u * delta['top']}; var d = WPP.Distance(testpt, pt); //save point if it's the closest point of all segments if (d &lt; dmin) { dmin = d; nextid = i + 1; newpt = JSON.parse(JSON.stringify(testpt)); //deep copy of obj } } //move token to closest point obj.set({ 'left' : newpt['left'], 'top' : newpt['top'], 'lastmove' : obj.get('left') + ',' + obj.get('top'), }); //set the next index patrol point for token state.WaypointPatrol['tokens'][obj.get('_id')]['nextpointindex'] = nextid; } on('chat:message', function(msg) { var params = msg.content.split(' '); if (msg.type !== 'api' || !playerIsGM(msg.playerid) || params.shift().toLowerCase() !== WPP.Command.toLowerCase() || params.length === 0) { return; } var replyTo = '/w ' + msg.who.split(' ')[0] + ' '; var cmd = params.shift().toLowerCase(); var out = undefined; //set default parameters var options = {}; function ReadParam(key) { keys = { 'path' : function(val) { options['patrolname'] = val; }, 'random' : function(val) { options['random'] = (val.toLowerCase() === 'true'); }, 'phase' : function(val) { options['phase'] = isNaN(val) ? 0 : Math.min(Math.max(0,val),100) / 100; }, 'active' : function(val) { options['active'] = (val.toLowerCase() === 'true'); }, 'mode' : function(val) { options['patrolmode'] = (val =='1' || val =='-1') ? parseInt(val) : 0; }, 'speed' : function(val) { options['movementrate'] = (isNaN(val) || val &lt; 1) ? 1 : parseFloat(val); }, 'rotation' : function(val) { options['rotation'] = isNaN(val) ? false : parseInt(val % 360); }, 'default' : function() { out = 'Err: Unknown parameter [' + key + ']'; }, }; return (keys[key.toLowerCase()] || keys['default']); } while (params.length &gt; 0) { var opt = decodeURIComponent(params.shift()).split(':'); switch (opt.length) { case 1: if (options['patrolname']) { options['patrolname'] += ' ' + opt[0]; } else { options['patrolname'] = opt[0]; } break; case 2: ReadParam(opt[0])(opt[1]); break; default: out = 'Err: too many delimiters'; break; } } //exit if parameters are invalid if (out) { sendChat(WPP.SpeakAs, replyTo + out); return; } switch(cmd) { case 'on': case 'off': state.WaypointPatrol['PatrolStatus'] = (cmd === 'on'); out = 'Patrolling ' + cmd.toUpperCase(); break; case 'resume': WPP.Resume(msg.selected); break; case 'help': case 'menu': out = WPP.PatrolHelpMenu(); //ListPatrolPaths(); break; case 'addpath': out = WPP.AddPatrolPath(msg.selected, options['patrolname']); break; case 'deletepath': out = WPP.DeletePatrolPath(options['patrolname']); break; case 'patrol': out = WPP.SetTokenToPatrol(msg.selected, options); break; case 'modify': out = WPP.ModifyPatrollingToken(msg.selected, options); break; case 'stoppatrol': out = WPP.RemoveTokenFromPatrol(msg.selected); break; case 'showpaths': WPP.ShowPatrolPaths(); break; case 'hidepaths': WPP.HidePatrolPaths(); break; default: out = 'Err: unknown command [' + cmd + ']'; break; } if (out) { sendChat(WPP.SpeakAs, replyTo + out); } }); on('change:graphic', function(obj, old) { var id = obj.get('_id'); if (!state.WaypointPatrol['tokens'] || !state.WaypointPatrol['tokens'][id] || state.WaypointPatrol['tokens'][id]['active'] === false) return; if (obj.get('left') === old['left'] && obj.get('top') === old['top'] && obj.get('height') === old['height'] && obj.get('width') === old['width'] && obj.get('rotation') === old['rotation']) { return; } state.WaypointPatrol['tokens'][obj.get('_id')]['active'] = false; }); on('ready', function() { setInterval(function() { //do nothing if not enabled or initiative page is open if (!state.WaypointPatrol['PatrolStatus'] || Campaign().get('initiativepage')) { return; } WPP.MovePatrols(); }, Math.max(WPP.RefreshRate,1) * 1000); log('Script loaded: Waypoint Patrols' + (state.WaypointPatrol['PatrolStatus'] ? ', actively patrolling' : '')); });
Brilliant Script man! Using it now in all my dungeons, easy set up. Any reason the min refresh rate is 5 seconds? would love to have smoother patrols but otherwise it's absolutely brilliant!
1457172613

Edited 1457374604
Matt
Pro
Thanks, Ashley. &nbsp;Think the 5-sec was more a personal choice at the time, not really any specific limitation. &nbsp;I wanted to make sure I had time to catch what was going on with my group and not miss patrol movement. I've updated the original code to go as low as 1 sec update. Edit : While the script allows for a 1 sec update, in practice I've found that anything less than 3 seconds can cause some undesirable visual effects as tokens might still be moving on screen when the script updates a patrol. &nbsp;
1457197919
The Aaron
Pro
API Scripter
Nice!
This is amazing. &nbsp;Took me a minute to figure it out, but once I got it, pretty easy. One question, is there a way to have the script whisper the menu and stuff to the GM?
1457327918

Edited 1457328079
Matt
Pro
Indeed. &nbsp;No reason this should go to the players anyway. &nbsp;I wanted to revisit the "help" menu, and I'll be sure to keep this GM eyes only. Thanks.
1457369992

Edited 1457370002
Matt
Pro
Kaelev said: One question, is there a way to have the script whisper the menu and stuff to the GM? The original code has been updated to reflect this. &nbsp;Thanks.
Matt said: Kaelev said: One question, is there a way to have the script whisper the menu and stuff to the GM? The original code has been updated to reflect this. &nbsp;Thanks. Perfect! Thank you. &nbsp;Now I can add them seamlessly and no one is the wiser.
So this script really upped the immersion for a scene just played out by my group in Out of the Abyss. &nbsp;I'm truly enjoying this script. &nbsp;Makes certain scenes truly come alive visually. &nbsp;Just wanted to say thanks!
Josh Stotz said: So this script really upped the immersion for a scene just played out by my group in Out of the Abyss. &nbsp;I'm truly enjoying this script. &nbsp;Makes certain scenes truly come alive visually. &nbsp;Just wanted to say thanks! Thanks, Josh! &nbsp;Always appreciate hearing positive feedback!
1460658871
Ada L.
Marketplace Creator
Sheet Author
API Scripter
Sorry to revive an old thread, but have you considered contributing this to the Roll20 API scripts repository? This would make a great addition to the new Scripts Library.
Hey matt, been a bit, but just wanted to stop in a say I love the new updates man, looking real good!
Thank you both.&nbsp; :)&nbsp; I will have to look into adding to the repository, all new to me.&nbsp;