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] Way Point Patrolling

1391061416

Edited 1458263900
Matt
Pro
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); });
1391112413

Edited 1391117415
Matt
Pro
I run mainly ADnD 2E, so I've modified the remainingDistance to be close to real-time movement. Using the script's refresh rate, this will calculate the distance each token is able to move in that amount of time. var remainingDistance = parseInt(gmn[3]) * 35 * TIMERDELAY / getObj("page",obj.get("_pageid")).get("scale_number"); If I increase my refresh rate from 5 sec to 10 sec, my tokens move less frequent, but cover more distance with each refresh.
Reworked the code so the waypoints paths do not have to be hard-coded. You can use the previous move of any token as a new path.
I've never been one for patrolling tokens, but I think it was mainly because I'm very specific as to where I want them to move and not good with coding to get them to do it. That being said, I love this idea and I will likely end up using this. Thanks Matt!
1391410380

Edited 1391410731
Matt
Pro
Cleaned up the code slightly, and added two features. ACTIVEPAGEONLY will only move tokens on the current campaign page when set to true. When false, it will move tokens across all pages. Added direction to the paths. A token can be set to follow a path in order, going back to the first point after reaching the end. It can also be set to follow the path in reverse order. Can also set the token to walk the path forward, then in reverse.
+1