Ok thanks for the info. I'm going to be needing some help (or recruiting a volunteer) to update Waypoint Patrol script snippit to work with PathV2 for Jumpgate. 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 > 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]) > 0) {
paths = _.keys(state.WaypointPatrol['paths'][pageid]);
}
var code = encodeURIComponent(':');
var out = '';
out = '<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;">'
+ '<div style="font-weight: bold; text-align: center; border-bottom: 1px solid black; font-size: 100%">Map: ' + (getObj('page', pageid).get('name') || '(Untitled)') + '</div>'
+ '<div style="padding-left: 5px; padding-right: 5px; overflow: hidden; font-size: 90%">';
if (paths.length > 0) {
out += '<div style="font-weight: bold; padding-top: 5px; padding-bottom: 5px;">Create a Patrol<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}">Patrol</a></div>';
out += '<li style="padding-left: 10px;">Starts moving a token graphic along an existing patrol path.</li>';
} else {
out += '<div style="padding-top: 5px; padding-bottom: 5px;">You must add a path to this map from a token\'s last move before creating a patrol.</div>';
out += '<div style="padding-top: 5px; padding-bottom: 5px;">Place a token to where you want the patrol path to begin. Move the token to where you want the patrol path to end. You can press the space bar to create turns in the path as you\'re moving the token. When the token is in the final position, click the \'Path\' button</div>'
}
out += '<div style="font-weight: bold; padding-top: 5px; padding-bottom: 5px;">Create a Path<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}">Path</a></div>';
out += '<li style="padding-left: 10px;">Saves a token\'s last move as a named patrol path for other tokens to follow.</li>';
if (paths.length > 0){
out += '<div style="font-weight: bold; padding-top: 5px; padding-bottom: 5px;">Delete a Path<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('|') + '}">Delete</a></div>';
out += '<li style="padding-left: 10px;">Deletes an existing patrol path.</li>';
out += '<div style="font-weight: bold; padding-top: 5px; padding-bottom: 5px;">Show All Paths<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">Show</a></div>';
out += '<li style="padding-left: 10px;">Draws onto the GM layer all existing patrol paths and labels them.</li>';
out += '<div style="font-weight: bold; padding-top: 5px; padding-bottom: 5px;">Hide All Paths<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">Hide</a></div>';
out += '<li style="padding-left: 10px;">Hides all drawn paths and labels.</li>';
}
out += '</div></div>';
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 < 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 < 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'] < 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 > 0) {
var path = getObj('path', state.WaypointPatrol['traces'].shift());
if (path) { path.remove(); }
}
}
if (state.WaypointPatrol['labels']) {
while (state.WaypointPatrol['labels'].length > 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() < 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'] > points.length -1 ? 0 : state.WaypointPatrol['tokens'][id]['nextpointindex'] ;
var moverate = state.WaypointPatrol['tokens'][id]['movementrate'] <= 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 > disttonext) {
newpoint = points[nextindex];
lastmove.push(points[nextindex]['left'],points[nextindex]['top']);
distremain -= disttonext;
nextindex = (nextindex >= 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() < state.WaypointPatrol['tokens'][id]['phase'] ? 'gmlayer' : 'objects' ,
'rotation' : newrotation,
});
state.WaypointPatrol['tokens'][id]['nextpointindex'] = nextindex;
distremain = 0;
}
} while (distremain > 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 < 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 > 1 ) ? 1 : (u < 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 < 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 < 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 > 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' : ''));
});