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.