
UPDATE : A new version of this script has been posted here . My first attempt at an API script, thought I'd share, and possibly get some feedback/suggestions on this. Create a path using any token's last move. For each patrolling token, add a comma-delimited string into the GM Notes section. Example: patrol,SamplePath,0,0,12,0 GM Note Comma-Delimited String: patrol: always the same pathname: the saved path name direction: determines how the order of points is followed 1=foward, -1 backwards, 0=forward then backward nextpoint: offset point in path, 0 starts at the first waypoint speed: movement rate of the token. speed * 10 = yards moved in 60 seconds rotation: addition rotation of token to face waypoint. -1 = do not face waypoint Commands !WaypointAdd <pathname> - saves the last move of selected target as <pathname> !WaypointDelete <pathname> - deletes the saved path named <pathname> !WaypointList - displays in chat all saved waypoint path names !TogglePatrol - turns patrolling on and off !ToggleAllPages - sets if patrol should occur on all pages, or just the current campaign page !TogglePaths - sets if the path of tokens should be displayed. if disabled, tokens will move straight to the final destination !ToggleLogs - turns console logging on and off Debugging when LOGOUTPUT is true. state.WayPointPatrol = state.WayPointPatrol || {}
var REFRESHRATE = 5 // in seconds
var ENABLEPATROL = true // default action on script load
var ACTIVEPAGEONLY = true // only patrols token on active page
var SHOWPATHS = true // show the path taken when true
var LOGOUTPUT = false // log calculations in output console
//order of gm notes
var csv = {}
csv.patrol = 0
csv.path = 1
csv.direction = 2
csv.nextpoint = 3
csv.movementrate = 4
csv.rotation = 5
on ("chat:message", function(msg) {
var cmdName = "!WaypointList";
if (msg.type == "api" && msg.content.indexOf(cmdName) !== -1 ) {
// list all the saved names in state
if (Object.keys(state.WayPointPatrol).length == 0) {
sendChat("","/desc No Path Names in State");
} else {
sendChat("","/desc Path Names: " + Object.keys(state.WayPointPatrol).join(","));
}
}
}); // !WaypointList
on("chat:message", function(msg) {
var cmdName = "!WaypointDelete ";
if (msg.type == "api" && msg.content.indexOf(cmdName) !== -1 ) {
//removespaces from path name
var pathName = msg.content.replace(cmdName,"").split(" ").join("");
//delete the property in state
delete state.WayPointPatrol[pathName];
sendChat("","/desc Path [" + pathName + "] deleted.");
}
}); // !WaypointDelete
on("chat:message", function(msg) {
var cmdName = "!WaypointAdd ";
if (msg.type == "api" && msg.content.indexOf(cmdName) !== -1 ) {
//verify only one target selected
try {if (msg.selected.length !== 1) {
sendChat("","/desc Err: Multiple targets selected")
return;
}
} catch(err) {
sendChat("","/desc Err: No target selected")
return;
}
//remove spaces from waypoint path name and get target token
var pathName = msg.content.replace(cmdName,"").split(" ").join("");
var pathToken = getObj("graphic",msg.selected[0]._id);
//exit if there is no last move in token
if (pathToken.get("lastmove") == "") {
sendChat("","/desc Err: Selected has no last move")
return;
}
//read lastmove into an array
var pathPoints = pathToken.get("lastmove").split(",");
//add path name to state
state.WayPointPatrol[pathName] = {};
//save lastmove in state
state.WayPointPatrol[pathName].WayPoints = [];
var numOfPoints = pathPoints.length / 2;
for (var i = 0; i < numOfPoints; i++) {
state.WayPointPatrol[pathName].WayPoints[i] = {};
state.WayPointPatrol[pathName].WayPoints[i].Left = parseInt(pathPoints[2*i]);
state.WayPointPatrol[pathName].WayPoints[i].Top = parseInt(pathPoints[2*i+1]);
}
//save current position in state
state.WayPointPatrol[pathName].WayPoints[numOfPoints] = {};
state.WayPointPatrol[pathName].WayPoints[numOfPoints].Left = parseInt(pathToken.get("left"));
state.WayPointPatrol[pathName].WayPoints[numOfPoints].Top = parseInt(pathToken.get("top"));
//save the page id for path
state.WayPointPatrol[pathName].PageID = pathToken.get("_pageid");
//clear the lastmove from the token to keep from adding the same path twice
pathToken.set("lastmove","");
//send notification of save
sendChat("","/desc Path [" + pathName +"] added");
if (LOGOUTPUT) {
log("Path Added [" + pathName + "]");
log("- PageID: " + state.WayPointPatrol[pathName].PageID);
log("- Waypoints: " + state.WayPointPatrol[pathName].WayPoints.length);
}
}
}); // !WaypointAdd
on("chat:message", function(msg) {
var cmdName = "!ToggleLogs";
if (msg.type == "api" && msg.content.indexOf(cmdName) !== -1) {
// enable/disable Console Logging
LOGOUTPUT = !LOGOUTPUT;
var status = LOGOUTPUT ? "Enabled" : "Disabled";
sendChat("","/desc Console Logging " + status);
log("Console Logging " + status);
}
}); // !ToggleLogs
on("chat:message", function(msg) {
var cmdName = "!TogglePatrol";
if (msg.type == "api" && msg.content.indexOf(cmdName) !== -1) {
// enable/disable patrol
ENABLEPATROL = !ENABLEPATROL
var status = ENABLEPATROL ? "Enabled" : "Disabled";
sendChat("","/desc Waypoint Patrol " + status);
if (LOGOUTPUT) log("Waypoint Patrol " + status);
}
}); // !TogglePatrol
on("chat:message", function(msg) {
var cmdName = "!TogglePaths";
if (msg.type == "api" && msg.content.indexOf(cmdName) !== -1) {
// enable/disable paths
SHOWPATHS = !SHOWPATHS
var status = SHOWPATHS ? "Shown" : "Hidden";
sendChat("","/desc Waypoint Paths " + status);
if (LOGOUTPUT) log("Waypoint Paths " + status);
}
}); // !TogglePaths
on("chat:message", function(msg) {
var cmdName = "!ToggleAllPages";
if (msg.type == "api" && msg.content.indexOf(cmdName) !== -1) {
// enable/disable patrol
ACTIVEPAGEONLY = !ACTIVEPAGEONLY
var status = ACTIVEPAGEONLY ? "Current Page" : "All Pages";
sendChat("","/desc Patrol Set To " + status);
if (LOGOUTPUT) log("Patrol Set To " + status);
}
}); // !ToggleAllPages
on("ready", function() {
setInterval(function() {
//exit if patrol is turned off
if (!ENABLEPATROL) return;
//record start time for performance tracking
if (LOGOUTPUT) {
log("===BEGIN WAYPOINT PROCESS===");
var timeStart = +new Date();
}
//loop for all tokens on object layer
if (ACTIVEPAGEONLY) {
var allTokens = findObjs({_type:"graphic", layer:"objects", _pageid:Campaign().get("playerpageid")});
} else {
var allTokens = findObjs({_type:"graphic", layer:"objects", });
}
_.each(allTokens, function(obj) {
//read the gmnotes from the token, correct for formatting
var gmn = obj.get("gmnotes").replace(/\%2C/g,",").split(",");
//skip token if not flagged for patrol or token is dead
try {
if (gmn[csv.patrol] != 'patrol' || obj.get("status_dead") != false) return;
} catch (err) {
return;
}
if (LOGOUTPUT) log("MOVING TOKEN: " + obj.get("name"));
//skip if path not for page token is on or if state data doesn't exist
try{
var thisPath = state.WayPointPatrol[gmn[csv.path]];
if (thisPath.PageID !== obj.get("_pageid")) {
log("- Err: Path [" + gmn[csv.path] + "].PageID does not match obj.pageid");
return;
}
} catch (err) {
log("- Err: Path [" + gmn[csv.path] + "] not in state");
return;
}
if (LOGOUTPUT) log("- Following path ["+ gmn[csv.path] +"]");
//read direction
try { gmn[csv.direction] = parseInt(gmn[csv.direction]);
} catch (err) { gmn[csv.direction] = 0; }
//copy path and format based on direction
thisPath = thisPath.WayPoints.slice(0);
if ( gmn[csv.direction] == 1 ) {
//loop waypoints forwards
//do nothing, waypoints are loaded forward by default
} else if (gmn[csv.direction] == -1) {
//loop waypoints backwards
thisPath.reverse();
} else {
//loop waypoints forwards then backwards
gmn[csv.direction] = 0;
var thatPath = thisPath.slice(0);
thatPath.reverse();
thatPath.pop();
thatPath.shift();
thisPath = thisPath.concat(thatPath);
}
//read next waypoint
try {
gmn[csv.nextpoint] = parseInt(gmn[csv.nextpoint]);
if (gmn[csv.nextpoint] >= thisPath.length) gmn[csv.nextpoint] = 0;
} catch (err) { gmn[csv.nextpoint] = 0; }
//read movement rate
try{
gmn[csv.movementrate] = parseInt(gmn[csv.movementrate]);
//ADnD 2E: movement rate for human is 12
if (gmn[csv.movementrate] <= 0) csv.movementrate = 12;
} catch (err) { gmn[csv.movementrate] = 12; }
//read rotation
try {
gmn[csv.rotation] = parseInt(gmn[csv.rotation]);
if (gmn[csv.rotation] < 0) gmn[csv.rotation] = -1;
} catch (err) { gmn[csv.rotation] = 0; }
//determine the maximum movement in pixels
//ADnD 2E: movement rate * 10 = the number of yards token can move in 1 round out of combat
//assumes map distance is measured in feet
var remainingDistance = gmn[csv.movementrate] * 35 * REFRESHRATE / getObj("page",obj.get("_pageid")).get("scale_number");
if (LOGOUTPUT) log("- Max Distance: " + remainingDistance + " pixels");
//set referenece position for calculations in distance
refLeft = parseInt(obj.get("left"));
refTop = parseInt(obj.get("top"));
if (LOGOUTPUT) log("- Starting from " + refLeft + "," + refTop);
//set the waypoint movement of token to current location
obj.set("lastmove", refLeft + "," + refTop);
//move token as long as remainingDistance > 0
do {
//get the next waypoint and calculate distance
wayLeft = thisPath[gmn[csv.nextpoint]].Left;
wayTop = thisPath[gmn[csv.nextpoint]].Top;
var distanceToWayPoint = Math.sqrt(Math.pow(wayLeft-refLeft,2) + Math.pow(wayTop-refTop,2));
//to prevent an infinite loop
var loop = 0;
if (remainingDistance > distanceToWayPoint && loop < 10) {
//add waypoint to movement and subtract distance
if (SHOWPATHS) obj.set("lastmove", obj.get("lastmove") + "," + wayLeft + "," + wayTop);
remainingDistance -= distanceToWayPoint;
if (LOGOUTPUT) log("- Next Point: " + wayLeft + "," + wayTop + " distance: " + distanceToWayPoint.toFixed(1) + " remaining: " + remainingDistance.toFixed(1));
//use waypoint as the next reference point
refLeft = parseInt(wayLeft);
refTop = parseInt(wayTop);
//set for next waypoint, reset as needed
gmn[csv.nextpoint] = gmn[csv.nextpoint] + 1;
if (gmn[csv.nextpoint] >= thisPath.length) gmn[csv.nextpoint] = 0;
obj.set("gmnotes", gmn.toString());
//increment loop counter
loop += 1;
} else {
//calculate ratio of last movement
var moveRatio = remainingDistance / distanceToWayPoint;
//set the final position
refLeft = parseInt(moveRatio * (wayLeft - refLeft)) + refLeft;
refTop = parseInt(moveRatio * (wayTop - refTop)) + refTop;
//make sure final position is within the boundries of the map
var mapWidth = getObj("page",obj.get("_pageid")).get("width") * 70;
var mapHeight = getObj("page",obj.get("_pageid")).get("width") * 70;
refLeft = Math.min(Math.max(refLeft,0), mapWidth);
refTop = Math.min(Math.max(refTop,0), mapHeight);
if (LOGOUTPUT) {
log("- Last Point: " + wayLeft + "," + wayTop + " distance: " + distanceToWayPoint.toFixed(1));
log("- Move Ratio: " + (moveRatio*100).toFixed(2) + "% of " + distanceToWayPoint.toFixed(1));
log("- Move To: " + refLeft + "," + refTop);
}
//position token
obj.set("left", refLeft);
obj.set("top", refTop);
if (LOGOUTPUT && SHOWPATHS) log("- lastmove: " + obj.get("lastmove"));
//rotate token to face final waypoint
if (gmn[csv.rotation] != -1) {
var angleRotation = Math.atan2(wayTop - refTop, wayLeft - refLeft) * 180 / Math.PI - 90 + gmn[csv.rotation];
obj.set("rotation", angleRotation);
if (LOGOUTPUT) log("- rotation: " + angleRotation.toFixed(1));
}
//force the token to stop moving
remainingDistance = 0;
}
} while (remainingDistance > 0);
})
if (LOGOUTPUT) {
var timeProcess = parseInt(+new Date() - timeStart) / 1000;
log("Waypoint Process Time: " + timeProcess.toFixed(3) + " seconds");
}
}, REFRESHRATE * 1000);
});