Radar API
Radar API
/////////////////////////////////////
// Overview
///////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////
/*
The Radar script creates an animated wavefront from a selected token to reveal
visible and invisible tokens on the map
---The radar temporarily "pings" all token objects on the layers specified
by spawning the default token of a character named "RadarPing"
---the RadarPing token should be a completely transparent token with an
aura not visible to all players, with no "controlled by" properties set
---the "controlled by" property is set by the api to be the player that
initiated the radar wave, enabling that player to see the location of pinged tokens
---The RadarPing tokens will disappear after a period of time determined by
the user
*/
/*
SETUP:
(1) Create a character named "RadarPing"
(2) Set the default token to a 1x1 square transparent png, with a 0ft aura
NOT visible to all players.
(a) Leave the "represents" property of the token blank.
(b) Leave "edited & controlled by" property blank. This will be
assigned dynamically to the player calling the script
(3) Create a macro or ability using the commands & arguments described
below
(4) Select a token as the source of the radar prior to activating the macro
OUTPUT:
(1) Animated wavefront extending from the selected token out to the max
range
(2) Temporary "Ping" of target tokens satisfying filter criteria (if any).
Silent mode is also available, with gives visuals but no chat template
(a) if no filters used, will output a numbered list of all tokens
within range, along with directional information and distance from the origin token
(b) if filters are used, the output will be grouped by filter keyword,
then sequentially numbered along with directional information and distance from the
origin token
*/
///////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////
// Command descriptions
///////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////
/*
!radar {{
--range| <# <optional units> > //Default=350. How far the
radar range extends, in pixels. Measured from center of selected token. Accepts
inline rolls e.g. [[ 5*70 ]]
//optionally, can
specify units in "u" or the units in Page settings. e.g. "60u" or "60ft"
--wavetype| <circle/square/5e <optional coneDirection/tokenID
coneAngle>
//default=circle. range
determined by pythagorean theorem
//square. Diagonals squares
count as one unit
//5e. will produce a cone
of ~53.14 deg. The width of cone is equal to the cone legnth.
//coneDirection - the angle
of the center of the cone (clockwise positive, 0deg is straight up). If a tokenID
is entered, the angle between the source and target token will be used
//coneAngle - the angle of
the cone in degrees
--spacing| <#> //Default=35. The spacing
between waves, in pixels (lower number = slower wavefront)
--wavedelay| <#> //Default=50. How much time
to wait before next wave, in ms (higher number = slower wavefront)
--wavelife| <#> //Default=200. How long
each wave wil remain on screen, in ms (higher number = more waves present at any
one time)
--pinglife| <#> //Default=2000. How long
each "RaindarPing" token wil remain on screen, in ms
--layers| <gmlayer, objects, walls, map> //Default="gmlayer,
objects". Which layers to look for tokens. any or all may be included
//accepts "gmlayer" or
"gm"
//accepts "objects" or
"tokens"
//accepts "walls" or
"dl"
//accepts "map"
//NOTE: if target
tokens are found on DL(walls) or map layer, output will be in red text, indicating
token is invisible to selected token
--LoS| <yes/true/1> or <no/false/0> //Default=false. Will DL
walls block radar sensor if completely obscured? To block, all corners and the
center of the target token must be in LoS with the center of origin token
--title| <text> //Default="Radar Ping
Results". The title of the output template. e.g. "Divine Sense", "Tremorsense"
--silent| <yes/true/1> or <no/false/0> <gm> //Default=false. If
true, no output template will be sent to chat. animations only.
// optional "gm" flag
to send result output to gm chat
//e.g. --
silent| true gm will only output to gm chat
//-------------------------------------------------------------------------
-------------
//ONLY CHOOSE ONE OF THESE TWO FILTERS: (tokfilter or charfilter)
//if used, output template will group by filter keyword
//only pings tokens where bar3_value contains either celestial, fiend, or undead.
Ignore tokens with "cloak"
// "bar1_value"
// "bar2_value"
// "bar3_value"
// "bar1_max"
// "bar2_max"
// "bar3_max"
// "gmnotes"
//optional matchTypes:
// "#red" (default)
// "#green"
// "#blue"
// "#yellow"
//only ping tokens where npc_type attribute contains either celestial, fiend, or
undead. Ignore tokens with "cloak"
//if token does not represent a character, it is ignored if this filter is used
//optional matchTypes:
const pt = function(x,y) {
this.x = x,
this.y = y
};
tableLineCounter += 1;
if (useGroups) {
//results are grouped by a filter
cellContents = `<div style="display: table-cell; padding-left:
5px"><b>${group}</b></div>` +
`<div style="display: table-cell; text-align:
center"><b>${itemNum}</b></div>` +
`<div style="display: table-cell;">${text}</div>`
} else {
//results are numbered individually
cellContents = `<div style="display: table-cell; text-align:
center"><b>${itemNum}</b></div>` +
`<div style="display: table-cell;">${text}</div>`
}
if (tableLineCounter % 2 == 0) {
rowHTML = `<div style="display: table-row; vertical-align: top;
width:1px; white-space:nowrap; line-height: 1.7em; background:#dddddd;">` +
cellContents +
`</div>`
} else {
rowHTML = `<div style="display: table-row; vertical-align: top;
width:1px; white-space:nowrap; line-height: 1.7em; background:#ffffff;">` +
cellContents +
`</div>`
}
return rowHTML;
}
try {
let baseObj = JSON.parse(tokenJSON);
baseObj.controlledby = controlledby;
baseObj.playersedit_aura1 = true;
baseObj.aura1_color = auraColor;
//add pt2
finalPts.push(conePts[1]);
//add pt3
finalPts.push(conePts[2]);
return finalPts;
}
/*
~~~~5e cone~~~~
Have to size the svg [path] coordinate system to account for the corners of
triangle potentially extending beyond the "radius" when rotated
(z,0) (z+r,0)
(0,0) | | (2*(z+r), 0)
O-----------------------------------------------O
| |
| |
| |
| |
| O |
| * * |
| * * |
| * * |
| ** * |
| * * r * |
| * * * |
|* * * |
O * z * * * * * * * O (origin) O (2*(z+r), z+r)
| (z+r,z+r) |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
O_______________________________________________O(2*(z+r), 2*(z+r))
(0,2*(z+r))
*/
coneDirection = normalizeTo360deg(coneDirection);
//start path at the origin pt, connect to pts 1&2, then back to origin
pointsJSON = `[[\"M\",${ptOrigin.x},${ptOrigin.y}],[\"L\",${pt1.x},$
{pt1.y}],[\"L\",${pt2.x},${pt2.y}],[\"L\",${ptOrigin.x},${ptOrigin.y}],`;
//add "phantom" single points to path corresponding to the four corners to
keep the size computations correct
pointsJSON = pointsJSON + `[\"M\",${ptUL.x},${ptUL.y}],[\"L\",${ptUL.x},$
{ptUL.y}],[\"M\",${ptUR.x},${ptUR.y}],[\"L\",${ptUR.x},${ptUR.y}],[\"M\",${ptLR.x},
${ptLR.y}],[\"L\",${ptLR.x},${ptLR.y}],[\"M\",${ptLL.x},${ptLL.y}],[\"L\",$
{ptLL.x},${ptLL.y}],[\"M\",${ptUL.x},${ptUL.y}],[\"L\",${ptUL.x},${ptUL.y}]]`;
//log(pointsJSON);
return pointsJSON;
};
/*
const build5eConeBAD = function(rad, coneWidth, coneDirection) {
let pointsJSON = '';
let deg2rad = Math.PI/180;
let ptOrigin = new pt(rad, rad);
let ptUL = new pt(0,0);
let ptUR = new pt(2*rad,0);
let ptLR = new pt(2*rad,2*rad);
let ptLL = new pt(0,2*rad);
let oX = oY = rad;
//normalize rotation to 360deg and find defining angles (converted to
radians)
coneDirection = normalizeTo360deg(coneDirection);
//start path at the origin pt, connect to pts 1&2, then back to origin
pointsJSON = `[[\"M\",${ptOrigin.x},${ptOrigin.y}],[\"L\",${pt1.x},$
{pt1.y}],[\"L\",${pt2.x},${pt2.y}],[\"L\",${ptOrigin.x},${ptOrigin.y}],`;
//add "phantom" single points to path corresponding to the four corners to
keep the size computations correct
pointsJSON = pointsJSON + `[\"M\",${ptUL.x},${ptUL.y}],[\"L\",${ptUL.x},$
{ptUL.y}],[\"M\",${ptUR.x},${ptUR.y}],[\"L\",${ptUR.x},${ptUR.y}],[\"M\",${ptLR.x},
${ptLR.y}],[\"L\",${ptLR.x},${ptLR.y}],[\"M\",${ptLL.x},${ptLL.y}],[\"L\",$
{ptLL.x},${ptLL.y}],[\"M\",${ptUL.x},${ptUL.y}],[\"L\",${ptUL.x},${ptUL.y}]]`;
//log(pointsJSON);
return pointsJSON;
};
*/
const buildSquare = function(rad, coneWidth, coneDirection) {
let squarePointsJSON = '';
let deg2rad = Math.PI/180;
let ptOrigin = new pt(rad, rad);
let ptUL = new pt(0,0);
let ptUR = new pt(2*rad,0);
let ptLR = new pt(2*rad,2*rad);
let ptLL = new pt(0,2*rad);
let squarePtsArr = [ptUL, ptUR, ptLR, ptLL]; //array of square corner
pts
//**********THIS WORKS!!!**********
finalPtsArr = addCornerPtsToCone(conePtsArr, squarePtsArr);
//*********************************
//if isPointInCone(add stuff here for UL, UR, etc)
//log(squarePointsJSON);
return squarePointsJSON;
};
steps = Math.min(Math.max(Math.round(
(Math.PI*2*Math.sqrt((2*rad*rad)/2))/35),4),20);
let acc=[[],[],[],[]];
let th=0;
_.times(steps+1,()=>{
let pt=at(th);
acc[0].push([pt.x,pt.y]);
acc[1].push([-pt.x,pt.y]);
acc[2].push([-pt.x,-pt.y]);
acc[3].push([pt.x,-pt.y]);
th+=stepSize;
});
acc = acc[0].concat(
acc[1].reverse().slice(1),
acc[2].slice(1),
acc[3].reverse().slice(1)
);
let oX = oY = rad;
let x, y;
let startAngle = deg2rad * (coneDirection - coneWidth/2);
let endAngle = deg2rad * (coneDirection + coneWidth/2);
let ptUL = new pt(0,0);
let ptUR = new pt(2*rad,0);
let ptLR = new pt(2*rad,2*rad);
let ptLL = new pt(0,2*rad);
//start path at the origin pt
circlePoints = `[[\"M\",${oX},${oY}],`;
//for loop takes into account cumulative floating point precision error
for (let th=startAngle; th<endAngle+Number.EPSILON*steps; th+=stepSize)
{
//change in "normal" polar coord conversion due to 0deg being
straight up and positive Y being "down"
x = oX + oX * Math.sin(th);
y = oY + oY * Math.cos(th + deg2rad*180);
circlePoints = circlePoints + `[\"L\",${x},${y}],`
}
wave = createObj("path", {
pageid: pageID,
path: pathstring,
fill: fill,
stroke: stroke,
layer: layer,
stroke_width: thickness,
width: radius*2,
height: radius*2,
left: x,
top: y
});
waveID = wave.id
// rotate point
newX = p.x * c - p.y * s;
newY = p.x * s + p.y * c;
/*
log(pt1);
log(pt2);
log('gridIncrement = ' + gridIncrement);
log('scaleNumber = ' + scaleNumber);
log('dX = ' + dX);
log('dY = ' + dY);
log('maxDelta = ' + maxDelta);
log('MinDelta = ' + minDelta);
log('minFloor1pt5Delta = ' + minFloor1pt5Delta);
let temp = maxDelta - minDelta + minFloor1pt5Delta;
log('distU = ' + temp);
*/
return distPx;
}
//Find the center of the squares corresponding to the corners of the target
token (used to determine distance)
if (gridIncrement !== 0) {
pUL = new pt( tokX-w/2 + 35/gridIncrement, tokY-h/2 +
35/gridIncrement ) //Upper left
pUR = new pt( tokX+w/2 - 35/gridIncrement, tokY-h/2 +
35/gridIncrement ) //Upper right
pLR = new pt( tokX+w/2 - 35/gridIncrement, tokY+h/2 -
35/gridIncrement ) //Lower right
pLL = new pt( tokX-w/2 + 35/gridIncrement, tokY+h/2 -
35/gridIncrement ) //Lower left
} else {
pUL = new pt( tokX-w/2 + 35, tokY-h/2 + 35 ) //Upper left
pUR = new pt( tokX+w/2 - 35, tokY-h/2 + 35 ) //Upper right
pLR = new pt( tokX+w/2 - 35, tokY+h/2 - 35 ) //Lower right
pLL = new pt( tokX-w/2 + 35, tokY+h/2 - 35 ) //Lower left
}
closestPt = pUL;
closestDist = dist;
if (calcType==='Euclidean') {
//default case
minDist = Math.round(minDist, 1);
} else {
//No display rounding needed. This was handled previously in the
distBetweenPts function
}
return minDist = {
dist: minDist,
centerDistX: centerDistX,
centerDistY: centerDistY,
closestPt: closestPt,
closestDist: closestDist,
corners: corners
};
}
function processInlinerolls(msg) {
if(_.has(msg,'inlinerolls')){
return _.chain(msg.inlinerolls)
.reduce(function(m,v,k){
var ti=_.reduce(v.results.rolls,function(m2,v2){
if(_.has(v2,'table')){
m2.push(_.reduce(v2.results,function(m3,v3){
m3.push(v3.tableItem.name);
return m3;
},[]).join(', '));
}
return m2;
},[]).join(', ');
m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0;
return m;
},{})
.reduce(function(m,v,k){
return m.replace(k,v);
},msg.content)
.value();
} else {
return msg.content;
}
}
//Check for inline rolls for spawn qty e.g. [[1d4]] or [[ 1t[tableName] ]]
inlineContent = processInlinerolls(msg);
return html;
}
if (useGrid) {
//horizontal grid lines
gridHTML = `<div style=\"width: ${graphWidth}px; height: $
{rowHeight}px; left: 0px; top: 0px; color: rgba(238,238,238,0.15); border-top: $
{borderWidth}px solid; position: absolute;\"></div>`
for (let row=0; row<numRows; row++) {
gridHTML = gridHTML +`<div style=\"width: ${graphWidth}px; height:
${rowHeight}px; left: 0px; top: ${row*rowHeight}px; color: rgba(238,238,238,0.15);
border-bottom: ${borderWidth}px solid; position: absolute;\"></div>`
}
//vertical grid lines
gridHTML = gridHTML +`<div style=\"width: ${colWidth}px; height: $
{graphHeight}px; left: 0px; top: 0px; color: rgba(238,238,238,0.15); border-left: $
{borderWidth}px solid; position: absolute;\"></div>`
for (let col=0; col<numCols; col++) {
gridHTML = gridHTML +`<div style=\"width: ${colWidth}px; height: $
{graphHeight}px; left: ${col*colWidth}px; top: 0px; color: rgba(238,238,238,0.15);
border-right: ${borderWidth}px solid; position: absolute;\"></div>`
}
}
if (useCircles) {
//add four concentric circles
circlesHTML = `<div style=\"height: ${graphHeight}px; width: $
{graphWidth}px; top: -${borderWidth}px; left: -${borderWidth}px; border: $
{borderWidth}px solid #00ff00; border-radius: 50%; position: absolute;\"></div>` +
`<div style=\"height: ${graphHeight*0.75-borderWidth*2}px;
width: ${graphWidth*0.75-borderWidth*2}px; top: ${graphHeight*.25/2+borderWidth}px;
left: ${graphWidth*.25/2+borderWidth}px; border: ${borderWidth}px solid #00ff00;
border-radius: 50%; position: absolute;\"></div>` +
`<div style=\"height: ${graphHeight*0.5-borderWidth*2}px;
width: ${graphWidth*0.5-borderWidth*2}px; top: ${graphHeight*.5/2+borderWidth}px;
left: ${graphWidth*.5/2+borderWidth}px; border: ${borderWidth}px solid #00ff00;
border-radius: 50%; position: absolute;\"></div>` +
`<div style=\"height: ${graphHeight*0.25-borderWidth*2}px;
width: ${graphWidth*0.25-borderWidth*2}px; top: ${graphHeight*.75/2+borderWidth}px;
left: ${graphWidth*.75/2+borderWidth}px; border: ${borderWidth}px solid #00ff00;
border-radius: 50%; position: absolute;\"></div>`
}
return html;
}
if (outputCompact) {
right = 'R ';
left = 'L ';
up = 'U ';
down = 'D ';
}
//log(tokX + ',' + tokY + ',' + originX + ',' + originY);
if (xDist > 0) {
retVal = right + _h.inlineResult(Math.round(xDist,1)) + ',';
} else {
retVal = left + _h.inlineResult(Math.round(Math.abs(xDist), 1)) + ',';
}
if (yDist > 0) {
retVal = retVal + down + _h.inlineResult(Math.round(yDist, 1)) + '';
} else {
retVal = retVal + up + _h.inlineResult(Math.round(Math.abs(yDist), 1))
+ '';
}
return retVal;
}
return point;
}
/** Get relationship between a point and a polygon using ray-casting algorithm
* @param {{x:number, y:number}} P: point to check
* @param {{x:number, y:number}[]} polygon: the polygon
* @returns true for inside or along edge; false if outside
*/
//adapted from https://stackoverflow.com/posts/63436180/revisions
const isPointInPolygon = function(P, polygon) {
const between = (p, a, b) => p >= a && p <= b || p <= a && p >= b;
let inside = false;
for (let i = polygon.length-1, j = 0; j < polygon.length; i = j, j++) {
const A = polygon[i];
const B = polygon[j];
// corner cases
if (P.x == A.x && P.y == A.y || P.x == B.x && P.y == B.y) return true;
if (A.y == B.y && P.y == A.y && between(P.x, A.x, B.x)) return true;
} else {
sendChat(scriptName, `/w "${who}" Target token ID (${tokID}) was not
found. Unable to calculate cone angle. Setting to 0 degrees.`);
return 0;
}
}
//calculate "smallAngle" - this does not take into account the quadrant in
which the angle lies. More tests req'd to determine correct relative angle
if (dX===0) {
smallAngle = 90*deg2rad;
} else {
smallAngle = Math.atan(dY / dX);
}
//2nd test angle: Add 360deg to pAngle (to handle cases where startAngle is
a negative value and endAngle is positive)
let pAngle360 = pAngle + 360*deg2rad;
if (endAngle < startAngle) {
endAngle = endAngle + 360*deg2rad;
}
/*
log(pt);
log(oPt);
log('coneDirection = ' + coneDirection);
log('coneWidth = ' + coneWidth);
log('range = ' + rad);
log('polarRadius = ' + polarRadius);
log('startAngle = ' + startAngle/deg2rad);
log('endAngle = ' + endAngle/deg2rad);
log('smallAngle = ' + smallAngle/deg2rad);
log('pAngle = ' + pAngle/deg2rad);
log('pAngle360 = ' + pAngle360/deg2rad);
*/
if (isFlatCone) {
//for 5e-style cones. Basically a triangle (no rounded outer face)
//let z = (rad / (2*Math.sin(Math.atan(0.5)))) - rad;
dTheta = Math.abs(pAngle - centerAngle);
//criticalDist = ((rad+z)*Math.cos(halfConeWidth)) / Math.cos(dTheta);
criticalDist = rad / Math.cos(dTheta);
} else {
//compare to full radius cone
criticalDist = rad
}
//log('criticalDist = ' + criticalDist);
//------------------------------------------------------
//create an array containing the 5 line segments from the center of the
origin token to critical points on the target token
//critical points = the center and the 4 corners of the target
token
let originToTokSegs = [];
let seg = {
pt1: origin,
pt2: tokCenter
}
originToTokSegs.push(seg); //first element is the segment between the
centers of the origin & target tokens
for (let i = 0; i < tok.corners.length; i++) {
seg = {
pt1: origin,
pt2: tok.corners[i]
}
originToTokSegs.push(seg); //the next four elements are from origin
token center to target token corners
}
//------------------------------------------------------
if ( SegmentsIntersect(pathSegs[ps].pt1, pathSegs[ps].pt2,
originToTokSegs[os].pt1, originToTokSegs[os].pt2) ) {
//intersections += 1;
segBlocked[os] = true;
//log('[' + pathSegs[ps].pt1.x + ',' +
pathSegs[ps].pt1.y + ',' + pathSegs[ps].pt2.x + ',' + pathSegs[ps].pt2.y);
//log('[' + originToTokSegs[os].pt1.x + ',' +
originToTokSegs[os].pt1.y + ',' + originToTokSegs[os].pt2.x + ',' +
originToTokSegs[os].pt2.y);
break; //stop checking path segments and begin eval of
next originToTok segment
}
}
}
//LoS is blocked ONLY IF ALL FIVE origin-to-target line segments are
blocked.
let test = segBlocked.every(v => v === true);
return test;
filterIgnores.forEach(ignore => {
if (ignore=== false) {
includeCount +=1;
}
});
if (includeCount > 0) {
return false;
} else {
return true;
}
}
try {
if(msg.type=="api" && msg.content.indexOf("!radar") === 0 ) {
switch(option) {
case "range":
range = parseFloat(param);
let u = param.match(/[a-z]/i); //if not an empty
string, we will use page settings to convert range to "u" or other map-defined
units
if (u !== null) {
convertRange = u[0]
}
break;
case "wavetype":
let w = param.toLowerCase();
if (w.includes('sq')) {
wavetype = 'square';
} else if (w.includes('5e')) {
wavetype = '5e';
coneWidth = 53.14;
} else {
wavetype = 'circle';
}
let tempConeParams = param.split(",").map(layer =>
layer.trim() );
if (tempConeParams.length > 1) {
coneDirection = tempConeParams[1]; //this may be
an angle or a tokenID. Later, we will parseFloat or find the angle between selected
& target tokens
}
if (tempConeParams.length > 2 && wavetype !== '5e') {
coneWidth = parseFloat(tempConeParams[2]);
}
break;
case "wavespacing":
waveIncrement = parseInt(param);
break;
case "wavedelay":
waveDelay = parseInt(param);
break;
case "wavelife":
waveLife = parseInt(param);
break;
case "wavecolor":
if ( param.match(/#/) ) {
let f = param.split('#')
waveColor = toFullColor(f[1])
}
break;
case "pinglife":
tokLife = parseInt(param);
break;
case "layers":
layers = []; //delete default layers first
layers = param.split(",").map(layer => layer.trim() );
break;
case "charfilter": //fall through
case "tokfilter":
filter.type = option==='charfilter' ? "char" : "tok";
let tempFilter = param.split(":").map(x => x.trim() );
filter.attr = tempFilter[0];
filter.vals = tempFilter[1].split(",").map(x =>
x.trim() );
//check for user-defined aura colors by filter value
filter.vals.forEach((val, index) => {
let tempVal;
if ( val.match(/#/) ) {
let f = val.split('#')
switch (true) {
case f[1].toLowerCase().includes('red'):
filter.colors.push(hRED);
break;
case f[1].toLowerCase().includes('yellow'):
filter.colors.push(hYELLOW);
break;
case f[1].toLowerCase().includes('green'):
filter.colors.push(hGREEN);
break;
case f[1].toLowerCase().includes('blue'):
filter.colors.push(hBLUE);
break;
default:
filter.colors.push(toFullColor(f[1]));
}
tempVal = f[0]; //strips the color out of the
filter
} else {
//default color
filter.colors.push(hRED)
tempVal = val;
}
//log('tempVal.substring(0,1) = ' +
tempVal.substring(0,1));
switch (tempVal.substring(0,1)) {
case '@':
// exact match
filter.compareType.push('@');
filter.vals[index] =
tempVal.substring(1,tempVal.length)
filter.groups.push('@' +
filter.vals[index])
break;
case '>':
// numeric greater than comparison
filter.compareType.push('>');
filter.vals[index] =
tempVal.substring(1,tempVal.length)
filter.groups.push('>' +
filter.vals[index])
break;
case '<':
// numeric less than comparison
filter.compareType.push('<');
filter.vals[index] =
tempVal.substring(1,tempVal.length)
filter.groups.push('<' +
filter.vals[index])
break;
default:
// string comparison (contains)
filter.compareType.push('contains');
if (tempVal==='*') {
filter.anyValueAllowed = true;
filter.vals[index] = filter.attr;
} else {
filter.vals[index] = tempVal;
}
filter.groups.push(filter.vals[index])
break;
}
});
//log(filter);
break;
case "title":
title = param;
break;
case "silent":
let p = param.toLowerCase();
if (p.includes('true') || p.includes('yes') ||
p.includes('1') ) {
displayOutput = false;
} else if (p.includes('false') || p.includes('no') ||
p.includes('0')) {
displayOutput = true;
}
if (p.includes('gm')) {
includeGM = true;
}
break;
case "units":
let tempUnits = param.split(",").map(x => x.trim());
if (_.contains(['u', 'units', 'squares', 'square',
'hexes', 'hex'], tempUnits[0].toLowerCase())) {
displayUnits = 'u'
} else {
displayUnits = undefined; //will re-define from
the Page settings later.
}
if (tempUnits.length > 1) {
if (_.contains(['pf', 'pathfinder', '3.5'],
tempUnits[1].toLowerCase())) {
calcType = 'PF';
}
if (_.contains(['5e'], tempUnits[1].toLowerCase()))
{
calcType = '5e';
}
}
break
case "los":
if (_.contains(['true', 'yes', '1'],
param.toLowerCase())) {
losBlocks = true;
} else if (_.contains(['false', 'no', '0'],
param.toLowerCase())) {
losBlocks = false;
}
break;
case "selectedid":
selectedID = param;
break;
case "playerid":
playerID = param;
break;
case "visible":
let v = param.toLowerCase();
if (v.includes('true') || v.includes('yes') ||
v.includes('1') ) {
seeAnimation = true;
} else if (v.includes('false') || v.includes('no') ||
v.includes('0')) {
seeAnimation = false;
}
break;
case "graphoptions":
let options = param.split(",").map(opt => opt.trim());
options.forEach (o => {
if (o.match(/grid/i)) { useGrid = true; }
if (o.match(/circ/i) || o.match(/ring/i))
{ useCircles = true; }
if (o.match(/retic/i)) { useRadial = true; }
});
break;
case "output":
let outputTypes = param.split(",").map(ot =>
ot.trim());
outputTypes.forEach (ot => {
if (ot.match(/graph/i)) { outputGraph = true; }
if (ot.match(/table/i)) { outputTable = true; }
if (ot.match(/compact/i)) { outputCompact = true; }
});
break;
case "groupby":
if (_.contains(['true', 'yes', '1'],
param.toLowerCase())) {
groupBy = true;
} else if (_.contains(['false', 'no', '0'],
param.toLowerCase())) {
groupBy = false;
}
break;
case "public":
if (_.contains(['true', 'yes', '1'],
param.toLowerCase())) {
publicOutput = true;
} else if (_.contains(['false', 'no', '0'],
param.toLowerCase())) {
publicOutput = false;
}
break;
default:
retVal.push('Unexpected argument identifier (' + option
+ '). Choose from: (' + validArgs + ')');
break;
}
}); //end forEach arg
//double-check: if user wants output (to self and/or GM) but hasn't
defined any, then revert to default (table-only)
if (displayOutput===true && outputTable===false &&
outputGraph===false && includeGM===false) {
outputTable = true;
}
//-----------------------------------------------------------------
-------
//Check if !radar was called by another api script
//If so, it must pass both selected token ID and controlling
playerid
//Otherwise, get values directly from the msg object
//-----------------------------------------------------------------
-------
if('API' === msg.playerid) {
//RADAR WAS CALLED BY ANOTHER API SCRIPT
if (playerID === undefined || selectedID ===undefined) {
sendChat(scriptName, 'When Radar is called by another
script, it must pass both the selected token ID and the playerID');
return;
}
who = getObj('player',playerID).get('_displayname');
controlledby = playerID;
oTok = getObj("graphic",selectedID);
} else {
//RADAR WAS CALLED DIRECTLY BY A PLAYER VIA CHAT. Get values
from msg *IF* they weren't explicitly passed as arguments
who = getObj('player',msg.playerid).get('_displayname');
///////////////////////////////////////////////////////////////////
////////
///////// TOKEN FILTERS
///////////////////////////////////////////////////////////////////
////////
allToks = gmToks.concat(objToks, wallToks, mapToks);
//Initial Filter: only those within range of the radar (meas from
center of origin token to center of closest corner of target token)
if (wavetype === 'circle') {
toksInRange = tokIdDist.filter(tok => {
if (tok.name.toLowerCase().indexOf('conetarget')>-1) {
return false;
} else if (coneWidth === 360) {
//simple range calculation
return (tok.closestDist <= range) && (tok.id !==
oTok.id);
} else {
//only if tok is within the defined cone
return isPointInCone(tok.closestPt, originPt, range,
coneDirection, coneWidth, false, calcType, pageGridIncrement,
pageScaleNumber); //false flag denotes a "true cone" (rounded outer face)
}
});
} else if (wavetype === 'square') { //square-shaped region.
compare pt to full or "sliced" polygon
toksInRange = tokIdDist.filter(tok => {
if (tok.name.toLowerCase().indexOf('conetarget')>-1) {
return false;
} else {
polygon = [];
pathString = JSON.parse(buildSquare(range, coneWidth,
coneDirection));
pathString.forEach((vert) => {
let tempPt = GetAbsoluteControlPt(vert, originPt,
range*2, range*2, 0, 1, 1);
polygon.push(tempPt)
});
if (coneWidth !== 360) { polygon.splice(-11); }
//removes the "phantom" points added to the path JSON for scaling purposes
if (filter.type==="char" || filter.type==="tok") {
toksInRange.map(tok => {
let tempGroup = [];
//if charFilter, only check tokens linked to sheets. If
tokFilter, always check
if ( (filter.type==="char" && tok.represents) ||
(filter.type==="tok") ) {
//populate attrCurrentVal with either char attr or
token value
let attrCurrentVal = 'not found';
if (filter.type==="char") {
let tempAttr = findObjs({_type: "attribute",
_characterid: tok.represents, name: filter.attr});
if (tempAttr.length >= 1) { attrCurrentVal =
tempAttr[0].get("current") }
} else {
attrCurrentVal = tok[filter.attr].toString() ||
"NoMatch";
}
//LoS Filter. Test line segments from origin to 5pts per token
(center & 4 corners). If all 5 intersect DL path segments, LoS is considered
blocked
if ( losBlocks && pageDL ) {
let paths = findObjs({
_pageid: pageID,
_type: "path",
layer: "walls"
});
///////////////////////////////////////////////////////////////////
////////
///////// RADAR ANIMATION
///////////////////////////////////////////////////////////////////
////////
let z; //only use for 5e cones. See build5eCone function
documentation for details
let i = 0;
let oldRadius = 0;
polygon = [];
while ( radius <= range ) {
if (wavetype === 'circle') { //circular wavefront
pathstring = buildCircle(radius, coneWidth, coneDirection);
} else if (wavetype === 'square') { //square wavefront
pathstring = buildSquare(radius, coneWidth, coneDirection);
} else if (wavetype === '5e') { //5e-style cone
wavefront
z = (radius / (2*Math.sin(Math.atan(0.5)))) - radius;
pathstring = build5eCone(radius, z, coneWidth,
coneDirection);
}
oldRadius = radius;
radius += waveIncrement;
pathstring_old = pathstring;
//5e cone
if (wavetype === '5e') {
//just check distance, since tokens outside of
angle were already filtered out
///////////////////////////////////////////////////////////////////
////////
///////// OPTIONAL OUTPUT
///////// results appear in order of proximity to origin.
Possibly grouped by filter keywords
///////////////////////////////////////////////////////////////////
////////
if (displayOutput || includeGM) {
//-------------------------------------------------------------
-----------------------
//Optional Graphical Output Part 1 - we're going to piggyback
on the loop through toks in range
//-------------------------------------------------------------
----------------------
let pingHTML = ''; //html string for graphical representation
of pinged tokens
tableLineCounter = 0;
//-------------------------------------------------------------
-----------------------
//loop through toks in range for building text and/or graphical
output
//-------------------------------------------------------------
-----------------------
toksInRange.sort((a,b) => (a.dist > b.dist) ? 1 : ((b.dist >
a.dist) ? -1 : 0));
let counter;
if (filter.type !== "" && filterExcludeOnly === false &&
groupBy === true) {
let group = '';
let rowData = '';
let addNewRowHeader = true;
//normal output
content =
GetDirectionalInfo(parseInt(tok.closestPt.x), parseInt(tok.closestPt.y), originX,
originY, displayUnits, includeTotalDist, pageScaleNumber, pageScaleUnits,
pageGridIncrement, outputCompact, calcType);
rowData = buildRowOutput(true, group,
parseInt(counter+1)+'.', content);
outputLines.push(rowData);
group = ''; //only want the row
header on the first row of output for each filter
}
counter +=1;
}
});
//-------------------------------------------------------------
-----------------------
//Optional Graphical Output Part 2 - we've built the html for
graphical token pings, so build the rest now
//-------------------------------------------------------------
-----------------------
let backgroundHTML = buildBackgroundHTML(graphWidth,
graphHeight, numRows, numCols, colWidth, rowHeight, useGrid, useCircles,
useRadial);
let graphicalOutput = backgroundHTML + pingHTML +
'</div></div>';
//no whisper if public output is set to true
let whisperPrefix = publicOutput? '' : `/w "${who}" `;
}
catch(err) {
if (who === undefined) {
sendChat(scriptName,'Unhandled exception: ' + err.message);
} else {
sendChat(scriptName,`/w "${who}" `+ 'Unhandled exception: ' +
err.message);
}
}
};
on("ready",() => {
checkInstall();
registerEventHandlers();
});
})();