[Idea] 'Stalker' tokens that follow when LoS is broken

1484145382

Edited 1484145460
So, I have no idea if this is possible, but I feel it was an interesting enough concept that I thought I might throw it up here. Basically the idea is the script in question would let the DM mark a PC token (or any token) so that another token would follow it a specified distance behind--the interesting part though would be for the 'stalker' token to only be able to move when not in the projected light or LoS of any other token. Kinda like those Weeping Angels from Doctor Who.   Again, like I said, I have no idea if something like this would even be possible with the API--it was just a thought I had that would be cool for my campaign and likely others--especially horror themed stuff. idk. Make of it what you will. haha 
1484145813
The Aaron
Pro
API Scripter
It would be possible, but might be a bit difficult. Keeping out of the light radius would be easy enough.  The hard part is in path planning and determining actual line of sight.  The script would basically need to understand the dynamic lighting lines and recreate the ray casting for light of sight in the API code. I wonder if Stephen S. has any thoughts about this...  =D
1484147014

Edited 1484147771
Stephen L.
Pro
Marketplace Creator
Sheet Author
API Scripter
I've made a script like this to create an  SCP-513-1 -like entity for one of my games which followed one of the PCs around for a while. This particular script doesn't do line of sight (the stalker entity in question here could phase through walls), but my It's A Trap script has line of sight raycasting code in it that could be used for this. Here's the code I used for the ethereal stalker: /**  * A script that implements an automated stalking behavior for the token named  * MAC-513. The token will stalk the cursed player, trying to remain at a   * fixed distance. It will stay a slightly further distance away from other  * character tokens. If pursued, it will disappear to the GM layer and reappear  * when it is no longer pursued.  */ (function() {          function getCursedToken() {         var cursedCharacter = getCursedCharacter();         if(cursedCharacter) {             var allTokens = findObjs({                                               _pageid: Campaign().get("playerpageid"),                                               _type: "graphic",                 _subtype: 'token'             });             return _.find(allTokens, function(token) {                return token.get('represents') == cursedCharacter.get('_id');              });         }         else             return null;     }          function getNonCursedTokens() {         var cursedCharacter = getCursedCharacter();         var allTokens = findObjs({                                           _pageid: Campaign().get("playerpageid"),                                           _type: "graphic",             _subtype: 'token'         });         return _.reject(allTokens, function(token) {             return token.get("represents") == cursedCharacter.get('_id') || token.get('name') == "MAC-513";          });     }               function getCursedCharacter() {         return findObjs({             _type: 'character',             name: 'Scarlette'         })[0];     }          function getMac513() {         var token = findObjs({                                           _pageid: Campaign().get("playerpageid"),                                           _type: "graphic",                name: "MAC-513"         })[0];         return token;     }          function getTokenPt(token) {         var left = token.get("left");         var top = token.get("top");                  return [left, top];     }          on("change:graphic", function(obj, prev) {                  var mac513 = getMac513();         var cursedToken = getCursedToken();         var otherTokens = getNonCursedTokens();                  if(mac513 && cursedToken && otherTokens) {             var mac513Pt = getTokenPt(mac513);             var cursedPt = getTokenPt(cursedToken);                          var bestPt;             var bestDist;                          var pt1 = TokenCollisions.getFirstCollisionPoint(mac513, [cursedToken], mac513Pt, cursedPt, 5*70);             var pt2 = TokenCollisions.getFirstCollisionPoint(mac513, otherTokens, mac513Pt, cursedPt, 10*70);                          if(pt1 && pt2) {                 var dist1 = VecMath.dist(mac513Pt, pt1);                 var dist2 = VecMath.dist(mac513Pt, pt2);                 if(dist1 < dist2) {                     bestPt = pt1;                     bestDist = dist1;                 }                  else {                     bestPt = pt2;                     bestDist = dist2;                 }             }             else if(pt1) {                 var dist1 = VecMath.dist(mac513Pt, pt1);                 bestDist = dist1;                 bestPt = pt1;             }             else {                 mac513.set("layer", "gmlayer");                 return;             }                                           mac513.set("left", bestPt[0]);             mac513.set("top", bestPt[1]);                          if(bestDist < 250)                 mac513.set("layer", "gmlayer");             else                 mac513.set("layer", "objects");         }     });      })(); (Note: TokenCollissions.getFirstCollisionPoint() is no longer part of TokenCollisions) From It's A Trap!, here are the interesting snippets that do the line-of-sight work: ... /** * Gets the tokens that a token has line of sight to. * @private * @param {Graphic} token * @param {Graphic[]} otherTokens * @param {number} [range=Infinity] * The line-of-sight range in pixels. * @param {boolean} [isSquareRange=false] * @return {Graphic[]} */ function _getTokensInLineOfSight(token, otherTokens, range, isSquareRange) { if(_.isUndefined(range)) range = Infinity; var pageId = token.get('_pageid'); var tokenPt = [ token.get('left'), token.get('top'), 1 ]; var tokenRW = token.get('width')/2-1; var tokenRH = token.get('height')/2-1; var wallPaths = findObjs({ _type: 'path', _pageid: pageId, layer: 'walls' }); var wallSegments = PathMath.toSegments(wallPaths); return _.filter(otherTokens, other => { var otherPt = [ other.get('left'), other.get('top'), 1 ]; var otherRW = other.get('width')/2; var otherRH = other.get('height')/2; // Skip tokens that are out of range. if(isSquareRange && ( Math.abs(tokenPt[0]-otherPt[0]) >= range + otherRW + tokenRW || Math.abs(tokenPt[1]-otherPt[1]) >= range + otherRH + tokenRH)) return false else if(!isSquareRange && VecMath.dist(tokenPt, otherPt) >= range + tokenRW + otherRW) return false; var segToOther = [tokenPt, otherPt]; return !_.find(wallSegments, wallSeg => { return PathMath.segmentIntersection(segToOther, wallSeg); }); }); } ... In addition to this line-of-sight stuff, you would likely also need some sort of pathfinding algorithm to help with the stalkers' movement.
1484166824

Edited 1484166877
Neat :D In some situations the 'phasing through walls' thing might not even be an issue for those using ghostly enemies.