Ziechael said: Ziechael said: Ziechael said: Hi Stephen, it's the pest from this thread . I asked about making a script that automatically updated the DL on change of graphic so i could use geomorphic tiles (like these that you kindly directed me to) in a rollable table to make a labyrinth that could be changed as a party adventures in it but keep the DL updated without manual effort (the WORST kind of effort!). You indicated that this script could do that with some simple modifications which I was hoping to achieve myself, however my javascript has been found sorely lacking since i'm very slowly teaching myself it... Do you think an update with the required changes would be of benefit to this script enough to warrant creator intervention? ;) ;) As always, love your work! Just slyly bumping this feature request since you've got some vacation coming up ;) ^^ Just make sure this thread doesn't die... however I can't do this without seriously skirting the issue of irritating repetition as outlined the Code of Conduct. Therefore, since the thread for your patrol script has closed now i'll enquire about it here: I have installed both the main script and the array but i'm getting an unexpected identfier ) error when attempting to use it... is it a finished product yet? ( <a href="https://app.roll20.net/forum/post/1779465/thinking-about-dynamic-lighting-eye-tracking-dot-dot-dot-or-sneaking-up-on-villager-vince-dot#post-1803717" rel="nofollow">https://app.roll20.net/forum/post/1779465/thinking-about-dynamic-lighting-eye-tracking-dot-dot-dot-or-sneaking-up-on-villager-vince-dot#post-1803717</a> ) No. I actaully went a different direction. <a href="http://i.imgur.com/AQjbSSQ.png" rel="nofollow">http://i.imgur.com/AQjbSSQ.png</a> A* search algorithm with API. The idea being mobs could find thier own path between points. Test code for that... var pathFinding = pathFinding || (function(){ 'use strict';
var version = '0.1.1',
lastUpdate = 1430550687,
schemaVersion = '0.1.1',
deferred={
batchSize: 30,
initialDefer: 10,
batchDefer: 10
},
objExtractKeysGraphic = ['id','pageid','layer','width','height','top','left'],
objExtractKeysPath = ['id','pageid','layer','width','height','top','left','path','stroke'],
// the world grid: a 2d array of tiles
world = [[]],
currentPath,
// size in the world in sprite tiles
worldWidth = 25,
worldHeight = 25,
// size of a tile in pixels
tileWidth = 70,
tileHeight = 70,
currentPageId,
heuristic = 'default',
// 'default' = default: no diagonals (Manhattan) linear movement - no diagonals - just cardinal directions (NSEW)
// 'DiagonalDistance' = diagonals allowed but no sqeezing through cracks diagonal movement - assumes diag dist is 1, same as cardinals
// 'DiagonalNeighboursFree' = diagonals and squeezing through cracks allowed
// 'EuclideanDistance' = euclidean but no squeezing through cracks
// 'EuclideanNeighboursFree' = euclidean and squeezing through cracks allowed
// Euclidean: diagonals are considered a little farther than cardinal directions
// diagonal movement using Euclide (AC = sqrt(AB^2 + BC^2))
// where AB = x2 - x1 and BC = y2 - y1 and AC will be [x3, y3]
deferredCreateObj = (function(){
var queue = [],
creator,
doCreates = function(){
var done = 0,
request;
while(queue.length && ++done < deferred.batchSize ){
request = queue.shift();
createObj(request.type,request.properties);
}
if( queue.length ){
creator = setTimeout(doCreates, deferred.batchDefer );
} else {
creator = false;
}
};
return function(type,properties){
queue.push({type: type, properties: properties});
if(!creator){
creator = setTimeout(doCreates, deferred.initialDefer );
}
};
}()),
getObjValue = function(obj, keys) {
return _.reduce( keys || objExtractKeys, function(m,prop){
m[prop] = obj.get(prop);
return m;
}, {});
},
createPath = function() {
var rp,url;
for (rp=0; rp<currentPath.length; rp++) {
switch(rp) {
case 0: // start
url = '<a href="https://s3.amazonaws.com/files.d20.io/images/9372382/64nmkiAegRAKK2t8Oxabhw/thumb.jpg?1431210220" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/9372382/64nmkiAegRAKK2t8Oxabhw/thumb.jpg?1431210220</a>';
break;
case currentPath.length-1: // end
url = '<a href="https://s3.amazonaws.com/files.d20.io/images/9372379/6jotbaghumajhMmWML3etw/thumb.jpg?1431210213" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/9372379/6jotbaghumajhMmWML3etw/thumb.jpg?1431210213</a>';
break;
default: // path node
url = '<a href="https://s3.amazonaws.com/files.d20.io/images/9372387/thW1wEVx37CuztiJEQVHYA/thumb.jpg?1431210233" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/9372387/thW1wEVx37CuztiJEQVHYA/thumb.jpg?1431210233</a>';
break;
}
deferredCreateObj('graphic', {
subtype: 'token',
pageid: currentPageId,
layer: 'map',
width: 70,
height: 70,
left: (currentPath[rp][0] * 70) + 35,
top: (currentPath[rp][1] * 70) + 35,
imgsrc: url
});
}
},
createWorld = function(){
var x,y,url,pathStart,pathEnd;
// create emptiness
for (x = 0; x < worldWidth; x = x + 1) {
world[x] = [];
for (y = 0; y < worldHeight; y = y + 1) {
world[x][y] = 0;
}
}
// scatter some walls
for (x = 0; x < worldWidth; x = x + 1) {
for (y = 0; y < worldHeight; y = y + 1) {
if (Math.random() > 0.75) {
world[x][y] = 1;
}
}
}
currentPath = [];
while (currentPath.length == 0) {
pathStart = [Math.floor(Math.random()*worldWidth),Math.floor(Math.random()*worldHeight)];
pathEnd = [Math.floor(Math.random()*worldWidth),Math.floor(Math.random()*worldHeight)];
if (world[pathStart[0]][pathStart[1]] == 0)
currentPath = findPath(world,pathStart,pathEnd);
}
for (x = 0; x < worldWidth; x = x + 1) {
for (y = 0; y < worldHeight; y = y + 1) {
switch(world[x][y]) {
case 1:
url = '<a href="https://s3.amazonaws.com/files.d20.io/images/9372233/WEb5s4bsLY_MNI2tr2NE2A/thumb.jpg?1431209718" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/9372233/WEb5s4bsLY_MNI2tr2NE2A/thumb.jpg?1431209718</a>';
break;
default:
url = '<a href="https://s3.amazonaws.com/files.d20.io/images/9372122/PI3s2Q7sJbGO3sqJLf3lWQ/thumb.jpg?1431209428" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/9372122/PI3s2Q7sJbGO3sqJLf3lWQ/thumb.jpg?1431209428</a>';
break;
}
deferredCreateObj('graphic', {
subtype: 'token',
pageid: currentPageId,
layer: 'map',
width: 70,
height: 70,
left: (x * 70) + 35,
top: (y * 70) + 35,
imgsrc: url
});
}
}
createPath();
},
// world is a 2d array of integers (eg world[10][15] = 0)
// pathStart and pathEnd are arrays like [5,10]
findPath = function(world, pathStart, pathEnd) {
// shortcuts for speed
var abs = Math.abs,
max = Math.max,
pow = Math.pow,
sqrt = Math.sqrt,
distanceFunction,
findNeighbours,
/*
The world data are integers: anything higher than this number is considered blocked this is handy
is you use numbered sprites, more than one of which is walkable road, grass, mud, etc
*/
maxWalkableTileNum = 0,
/*
Keeps track of the world dimensions. Note that this A-star implementation expects the world array to be square:
it must have equal height and width. If your game world is rectangular,just fill the array with dummy
values to pad the empty space.
*/
worldWidth = world[0].length,
worldHeight = world.length,
worldSize = worldWidth * worldHeight;
//Which heuristic should we use?
switch(heuristic){
case 'DiagonalDistance':
distanceFunction = DiagonalDistance;
findNeighbours = DiagonalNeighbours;
break;
case 'DiagonalNeighboursFree':
distanceFunction = DiagonalDistance;
findNeighbours = DiagonalNeighboursFree;
break;
case 'EuclideanDistance':
distanceFunction = EuclideanDistance;
findNeighbours = DiagonalNeighbours;
break;
case 'EuclideanNeighboursFree':
distanceFunction = EuclideanDistance;
findNeighbours = DiagonalNeighboursFree;
break;
default:
distanceFunction = ManhattanDistance;
findNeighbours = function(){};
break;
}
function ManhattanDistance(Point, Goal) {
return abs(Point.x - Goal.x) + abs(Point.y - Goal.y);
}
function DiagonalDistance(Point, Goal) {
return max(abs(Point.x - Goal.x), abs(Point.y - Goal.y));
}
function EuclideanDistance(Point, Goal) {
return sqrt(pow(Point.x - Goal.x, 2) + pow(Point.y - Goal.y, 2));
}
// Neighbours functions, used by findNeighbours function
// to locate adjacent available cells that aren't blocked
// Returns every available North, South, East or West
// cell that is empty. No diagonals,
// unless distanceFunction function is not Manhattan
function Neighbours(x, y) {
var N = y - 1,
S = y + 1,
E = x + 1,
W = x - 1,
myN = N > -1 && canWalkHere(x, N),
myS = S < worldHeight && canWalkHere(x, S),
myE = E < worldWidth && canWalkHere(E, y),
myW = W > -1 && canWalkHere(W, y),
result = [];
if(myN) {result.push({x:x, y:N}); }
if(myE) {result.push({x:E, y:y}); }
if(myS) {result.push({x:x, y:S}); }
if(myW) {result.push({x:W, y:y}); }
findNeighbours(myN, myS, myE, myW, N, S, E, W, result);
return result;
}
// returns every available North East, South East,
// South West or North West cell - no squeezing through
// "cracks" between two diagonals
function DiagonalNeighbours(myN, myS, myE, myW, N, S, E, W, result)
{
if(myN)
{
if(myE && canWalkHere(E, N))
result.push({x:E, y:N});
if(myW && canWalkHere(W, N))
result.push({x:W, y:N});
}
if(myS)
{
if(myE && canWalkHere(E, S))
result.push({x:E, y:S});
if(myW && canWalkHere(W, S))
result.push({x:W, y:S});
}
}
// returns every available North East, South East,
// South West or North West cell including the times that
// you would be squeezing through a "crack"
function DiagonalNeighboursFree(myN, myS, myE, myW, N, S, E, W, result){
myN = N > -1;
myS = S < worldHeight;
myE = E < worldWidth;
myW = W > -1;
if(myE) {
if(myN && canWalkHere(E, N)) {result.push({x:E, y:N}); }
if(myS && canWalkHere(E, S)) {result.push({x:E, y:S}); }
}
if(myW) {
if(myN && canWalkHere(W, N)) {result.push({x:W, y:N}); }
if(myS && canWalkHere(W, S)) {result.push({x:W, y:S}); }
}
}
// returns boolean value (world cell is available and open)
function canWalkHere(x, y) {
return ((world[x] != null) &&
(world[x][y] != null) &&
(world[x][y] <= maxWalkableTileNum));
}
// Node function, returns a new object with Node properties
// Used in the calculatePath function to store route costs, etc.
function Node(Parent, Point) {
var newNode = {
// pointer to another Node object
Parent:Parent,
// array index of this Node in the world linear array
value:Point.x + (Point.y * worldWidth),
// the location coordinates of this Node
x:Point.x,
y:Point.y,
// the heuristic estimated cost
// of an entire path using this node
f:0,
// the distanceFunction cost to get
// from the starting point to this node
g:0
};
return newNode;
}
// Path function, executes AStar algorithm operations
function calculatePath() {
// create Nodes from the Start and End x,y coordinates
var mypathStart = Node(null, {x:pathStart[0], y:pathStart[1]});
var mypathEnd = Node(null, {x:pathEnd[0], y:pathEnd[1]});
// create an array that will contain all world cells
var AStar = new Array(worldSize);
// list of currently open Nodes
var Open = [mypathStart];
// list of closed Nodes
var Closed = [];
// list of the final output array
var result = [];
// reference to a Node (that is nearby)
var myNeighbours;
// reference to a Node (that we are considering now)
var myNode;
// reference to a Node (that starts a path in question)
var myPath;
// temp integer variables used in the calculations
var length, max, min, i, j;
// iterate through the open list until none are left
while(length = Open.length) {
max = worldSize;
min = -1;
for(i = 0; i < length; i++) {
if(Open[i].f < max) {
max = Open[i].f;
min = i;
}
}
// grab the next node and remove it from Open array
myNode = Open.splice(min, 1)[0];
// is it the destination node?
if(myNode.value === mypathEnd.value) {
myPath = Closed[Closed.push(myNode) - 1];
do {
result.push([myPath.x, myPath.y]);
} while (myPath = myPath.Parent);
// clear the working arrays
AStar = Closed = Open = [];
// we want to return start to finish
result.reverse();
} else {// not the destination
// find which nearby nodes are walkable
myNeighbours = Neighbours(myNode.x, myNode.y);
// test each one that hasn't been tried already
for(i = 0, j = myNeighbours.length; i < j; i++) {
myPath = Node(myNode, myNeighbours[i]);
if (!AStar[myPath.value]) {
// estimated cost of this particular route so far
myPath.g = myNode.g + distanceFunction(myNeighbours[i], myNode);
// estimated cost of entire guessed route to the destination
myPath.f = myPath.g + distanceFunction(myNeighbours[i], mypathEnd);
// remember this new path for testing above
Open.push(myPath);
// mark this node in the world graph as visited
AStar[myPath.value] = true;
}
}
// remember this route as having no more untested options
Closed.push(myNode);
}
} // keep iterating until the Open list is empty
return result;
}
// actually calculate the a-star path!
// this returns an array of coordinates
// that is empty if no path is possible
return calculatePath();
}, // end of findPath() function
refreshData = function(){
var page = getObj('page', Campaign().get('playerpageid'));
currentPageId = page.get('id');
worldWidth = page.get('width');
worldHeight = page.get('height');
},
checkInstall = function() {
if( ! _.has(state,'pathFinding') || state.pathFinding.version !== schemaVersion) {
log('pathFinding: Resetting state');
state.pathFinding = {
version: schemaVersion
};
}
},
handleGraphicChange = function(obj) {
var objValues = getObjValue(obj, objExtractKeysGraphic);
},
handlePathAdd = function(obj) {
var objValues = getObjValue(obj, objExtractKeysPath);
},
handlePageChange = function(obj) {
if ( Campaign().get('playerpageid') !== currentPageId ) {
refreshData();
}
},
handleInput = function(msg) {
var message = _.clone(msg), messageArguments;
if ( 'api' !== message.type ) { return; }
refreshData();
createWorld();
},
registerEventHandlers = function() {
on('change:graphic', handleGraphicChange);
on('add:path', handlePathAdd);
on('change:campaign:playerpageid', handlePageChange);
on('chat:message', handleInput);
checkInstall();
};
return {
CheckInstall: checkInstall,
RegisterEventHandlers: registerEventHandlers
};
}());
on('ready',function(){
'use strict';
pathFinding.RegisterEventHandlers();
});