Skip to content

Instantly share code, notes, and snippets.

@finalfrog
Created June 14, 2018 04:12
Show Gist options
  • Save finalfrog/e43438208515939f8ae908203a12371a to your computer and use it in GitHub Desktop.
Save finalfrog/e43438208515939f8ae908203a12371a to your computer and use it in GitHub Desktop.
MapTeleporters_WIP.js
// Github: https://gist.github.com/finalfrog/124f67ad84204546caf16fffd84115e4
//
// =Inspired by=
// MapChange by TheWhiteWolves (https://github.com/TheWhiteWolves/MapChange.git)
// Teleporter Without Movement Tracker by DarokinB (https://gist.github.com/DarokinB/5806230)
// =Author=
// FinalFrog
// =Contact=
// https://app.roll20.net/users/585874/finalfrog
//
// GENERAL RULES FOR CREATING AND USING TELEPORTERS:
//
// * Teleporters are defined by Tokens on the GM layer.
//
// * Tokens for Teleports must end in two digits followed by a letter between A and L.
// Example: "TeleportToken02A"
//
// * Teleporters will only teleport tokens which meet the following requriements:
// 1. Token represents a character controlled by a player.
// 2. Token is on the object layer.
// 3. Token with the same name exists on the page being teleported to.
//
// * The string preceeding the two digits and letters defines a unique teleport chain and all
// teleporters in a chain must have the same two digits.
// Example: "TeleportToken02A" and "TeleportToken02B" are both members of the "TeleportToken" chain
//
// * The two digits shared by all members of a chain define how many teleporters the chain will expect to find
// before looping back to the first.
// Example: "TeleportToken02A" will take you to "TeleportToken02B" which will take you back to "TeleportToken02A"
//
// * If an expected link in the teleporter chain does not exist then the teleport will break on the last
// existing expected teleporter in the chain.
// Example: If only "TeleportToken04A", "TeleportToken04B", and "TeleportToken04D" exist, then "TeleportToken04B" will teleport anywhere.
//
// EXAMPLE OF HOW TO CREATE AND USE AN SIMPLE TWO-WAY INTRA-MAP TELEPORTER PAIR:
//
// 1. Create a token on the GM layer named "Teleport02A"
// 2. Create a token on the GM layer named "Teleport02B"
// 3. Create a token on the Object layer named "Traveler"
// 4. When you move "Traveler" into the same space as "Teleport02A" it should jump to "Teleport02B" and vis versa.
// 5. You can change this to a one-way teleporter pair by naming the teleporters "Teleport03A" and "Teleport03B".
//
// EXAMPLE OF HOW TO CREATE AND USE AN SIMPLE TWO-WAY INTER-MAP TELEPORTER PAIR:
// 1. Create a map named "MAP 1"
// 2. Create a map named "MAP 2"
// 3. Create a token on the GM layer in "MAP 1" named "Teleport02A"
// 4. Create a token on the GM layer in "MAP 2" named "Teleport02B"
// 5. Create a character controlled only by a player (Eg. Jeanie).
// 6. Create a token on the Object layer in "MAP 1" named Jeanie and set it to represent the above character
// 7. Create a token on the Object layer in "MAP 2" named Jeanie and set it to represent the above character
// 8. When you move the Jeanie token on "MAP 1" onto the same space as "Teleport02A", the player (Jeanie) will be
// moved on their own to "MAP 2" and their token on "MAP 2" will jump to the location of "Teleport02B", and the
// Jeanie token on "MAP 1" is now moved to the GM layer
// 9. Moving the token off of "Teleport02B" and back will reverse the process and take the player (Jeanie)
// back to "Map 1", move the "MAP 2" token to the GM layer, and move the "MAP 1" token to the Object layer.
var MapTeleporters = MapTeleporters || (function() {
'use strict';
// Constants
var kDebug = 0;
var kInfo = 1;
var kNotice = 2;
var kWarning = 3;
var kError = 4;
var Levels = new Array('DEBUG', 'INFO', 'NOTICE', 'WARNING', 'ERRROR');
var Letters = new Array('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L');
// Configuration
var kLogLevel = kNotice;
/*
logger
Print formatter to add script name and log level to logging
*/
var logger = function(msg, level) {
if (level >= kLogLevel)
{
log("[MapTeleporters] "+Levels[level]+": "+msg);
}
};
/*
formatBounds
Converts a bounds object to a human readable string
*/
var formatBounds = function(bounds) {
return "Left["+bounds.left+"] "+
"Right["+bounds.right+"] "+
"Top["+bounds.top+"] "+
"Bottom["+bounds.bottom+"]"
};
/*
formatBounds
Converts a point object to a human readable string
*/
var formatPoint = function(point) {
return "("+point.x+","+point.y+")";
};
/*
getBounds
Fetches the bounds from a graphics object
*/
var getBounds = function(obj) {
// 'top' and 'left' are misnomers and refer to map offset of the center
// a graphic, not its top left corner. To calculate the real bounds
// we need to take its height and width into account
var objCenterX = obj.get('left'),
objCenterY = obj.get('top'),
objWidth = obj.get('width'),
objHeight = obj.get('height');
return {
left:objCenterX - (objWidth/2),
right:objCenterX + (objWidth/2),
top:objCenterY - (objHeight/2),
bottom:objCenterY + (objHeight/2)
};
};
/*
getCenter
Fetches the center from a graphics object
*/
var getCenter = function(obj) {
return {
x:obj.get('left'),
y:obj.get('top')
};
};
/*
checkCollision
Returns true if the supplied bounds overlap
*/
var checkCollision = function(boundsA, boundsB) {
if ( (boundsA.right <= boundsB.left ||
boundsA.left >= boundsB.right ||
boundsA.bottom <= boundsB.top ||
boundsA.top >= boundsB.bottom)
) {
return false;
} else {
return true;
}
};
/*
checkContains
Returns true if point lies within the second
supplied bounds.
*/
var checkContains = function(point, bounds) {
if( point.x <= bounds.left &&
point.x >= bounds.right &&
point.y <= bounds.top &&
point.y >= bounds.bottom
) {
return true;
} else {
return false;
}
};
/*
findContains
Returns list of all tokens on the same page and in the specified
layer which overlap
Special thanks to The Aaron for original version of this function
*/
var findContains = function(obj,layer,gridType) {
'use strict';
if (obj === null) {
logger("Cannot search for collisions of null graphic");
return [];
}
// Search requested layer or gmlayer by default
layer = layer || 'gmlayer';
// Default to overlapping squares behavior
gridType = gridType || 'square';
// To detect collisions we need to calculate the bounds of
// the object we're searching for and check whether it overlaps with
// the bounds of any other graphics on the same page.
var searchParam;
if (gridType === 'square') {
searchParam = getBounds(obj);
logger("Searching for tokens overlapping bounds "+formatBounds(searchParam), kDebug);
} else {
searchParam = getCenter(obj);
logger("Searching for tokens containing the point "+formatPoint(searchParam), kDebug);
}
return _.chain(findObjs({
_pageid: obj.get('pageid'),
_type: 'graphic',
layer: layer
}))
.reduce(function(matches,check){
var checkName = check.get('name');
// Don't include overlapping tokens with no name
if (checkName === ""){
return matches;
}
var checkBounds = getBounds(check);
logger("Comparing search params to token ["+checkName+"] bounds "+formatBounds(checkBounds), kDebug);
if (gridType === 'square' && checkCollision(searchParam, checkBounds)) {
logger("Token ["+checkName+"] overlaps with search bounds", kInfo);
matches.push(check);
} else if (gridType !== 'square' && checkContains(searchParam, checkBounds)) {
logger("Token ["+checkName+"] contains with search point", kInfo);
matches.push(check);
}
return matches;
},[])
.value();
};
/*
teleportPlayers
Moves player(s) controlling teleported token to destination page
*/
var teleportPlayers = function(characterName, controllers, pageId) {
logger("Controllers of teleported token: ["+controllers+"]", kDebug);
var controllerArray = controllers.split();
if (controllerArray.length == 0) {
logger("Teleported token is not controlled by any players", kInfo);
} else if (controllerArray.indexOf('all') !== -1) {
logger("Teleported token is controlled by all players, uniting players on destination page and sending ping", kNotice);
Campaign().set('playerspecificpages', false);
Campaign().set('playerpageid', pageId);
// NOTE: sendPing moveAll argument currently broken (See https://wiki.roll20.net/Talk:API:Utility_Functions)
sendPing(NewX, NewY, destPageId, null, true);
} else {
var playerPages = Campaign().get('playerspecificpages') || {};
var i;
for (i = 0; i < controllerArray.length; i++) {
var playerId = controllerArray[i];
var player = getObj('player', playerId);
var playerName = player.get('_displayname');
logger("Teleported token is controlled by player ["+playerName+"], moving them to destination page", kNotice);
// Move player to destination page
if (playerId in playerPages) {
delete playerPages[playerId];
}
playerPages[playerId] = pageId;
// Update player pages
Campaign().set('playerspecificpages', false);
Campaign().set('playerspecificpages', playerPages);
}
}
};
/*
handleGraphicChange
Teleports moved token if it has moved onto a source teleporter with
a valid destination teleporter
*/
var handleGraphicChange = function(obj) {
// Only teleport tokens with a name
var objName = obj.get('name');
if (objName === "") {
logger("Token has no name, ignoring...", kInfo);
return;
}
logger("Checking whether to teleport token ["+objName+"]...", kInfo);
// Only teleport tokens on the object layer
if (obj.get('layer') !== 'objects') {
logger("Token is not on the object layer, ignoring...", kInfo);
return;
}
// Only teleport tokens which represent a character
var characterId = obj.get('represents');
if(characterId === "") {
logger("Token does not represent a character, ignoring...", kInfo);
return;
}
var character = getObj('character', characterId);
if (character === null) {
logger("Unable to retrive character for token, ignoring...", kInfo);
return;
}
var characterName = character.get('name');
logger("Token represents character ["+characterName+"]...", kDebug);
// Store the last position of the token to check whether it has just
// entered any possible source teleporters
var lastMoves = obj.get('lastmove').split(',');
if (lastMoves < 2) {
logger("Token's lastmove is empty, cannot determine whether token was moved into teleporter, ignoring...", kInfo);
return;
}
// Store the page and grid type of the source page
var sourcePageId = obj.get('_pageid');
var sourcePage = getObj('page', sourcePageId);
var gridType = sourcePage.get('grid_type');
// Find any teleporters overlapping the token
var sourceTeleporters = findContains(obj,'gmlayer',gridType);
if (sourceTeleporters.length == 0) {
logger("No overlapping tokens found in gmlayer", kInfo);
return;
} else {
var lastCenter = {
x:parseInt(lastMoves[0]),
y:parseInt(lastMoves[1])
};
logger("Token's last center was ["+formatPoint(lastCenter)+"]", kDebug);
var lastBounds = {
left:lastCenter.x - (obj.get('width')/2),
right:lastCenter.x + (obj.get('width')/2),
top:lastCenter.y + (obj.get('height')/2),
bottom:lastCenter.y + (obj.get('height')/2)
};
logger("Token's last bounds were ["+formatBounds(lastBounds)+"]", kDebug);
var i;
for (i = 0; i < sourceTeleporters.length; i++) {
var sourceTeleporter = sourceTeleporters[i];
// Get name of possible teleporter
var sourceName = sourceTeleporter.get('name');
logger("Checking whether ["+sourceName+"] is a valid teleporter", kDebug);
// Number of doors in the cycle
var doorCount = sourceName.substr(sourceName.length - 2, 1);
if (isNaN(doorCount)) {
logger("Possible teleporter name does not contain door count, skipping...", kInfo);
continue;
}
// Current Letter of the Door
var currDoor = sourceName.substr(sourceName.length - 1, 1);
// Finds the pair location and moves target to that location
var i = Letters.indexOf(currDoor);
if (i == doorCount - 1) {
i = 0;
} else if (i >= 0) {
i = i + 1;
} else {
logger("Possible teleporter name does not end with valid door index letter, skipping...", kInfo);
continue;
}
// Only use teleport if the token was moved from a location which did not
// overlap the teleport to one which does overlap the teleporter.
var sourceBounds = getBounds(sourceTeleporter);
if (gridType === 'square') {
logger("Checking whether last token bounds ["+formatBounds(lastBounds)+"] overlaps source teleporter bounds ["+formatBounds(sourceBounds)+"]", kDebug);
if (checkCollision(lastBounds, sourceBounds))
{
logger("Token was overlapping this possible teleporter before it moved, skipping...", kInfo);
continue;
}
} else if (gridType !== 'square') {
logger("Checking whether last token center ["+formatPoint(lastCenter)+"] overlaps source teleporter bounds ["+formatBounds(sourceBounds)+"]", kDebug);
if (checkContains(lastCenter, sourceBounds))
{
logger("Token was contained in this teleporter before it moved, skipping...", kInfo);
continue;
}
}
// Generate expected destination teleporter name
var NewName = sourceName.substr(0,sourceName.length - 2) + doorCount + Letters[i];
logger("Searching for corresponding destination teleporter ["+NewName+"]", kDebug);
var destTeleporters = findObjs({
_type: 'graphic',
layer: 'gmlayer', //target location MUST be on GM layer
name: NewName,
});
if (destTeleporters.length == 0) {
logger("No destination teleporter with name ["+NewName+"] found, skipping...", kInfo);
continue;
} else {
var j;
for (j = 0; j < destTeleporters.length; j++) {
// Handle first matching destination teleporter
var destTeleporter = destTeleporters[j];
logger("Found destination teleporter ["+NewName+"]", kInfo);
// Destination coordinates should maintain the same
// offset relative to destination as token had
// relative to destination teleporter
var sourceOffsetX = sourceTeleporter.get('left') - obj.get('left');
var sourceOffsetY = sourceTeleporter.get('top') - obj.get('top');
var NewX = destTeleporter.get('left') - sourceOffsetX;
var NewY = destTeleporter.get('top') - sourceOffsetY;
// Page of destination teleporter
var destPageId = destTeleporter.get('_pageid');
if (destPageId == sourcePageId) {
logger("Executing intra-page teleport for token ["+objName+"]", kNotice);
// Handle intra-page teleports by just changing position of
// original token.
obj.set('left', NewX);
obj.set('top', NewY);
return;
} else {
var destPage = getObj('page', destPageId);
var destPageName = destPage.get('name');
logger("Searching destination page ["+destPageName+"] for token with name ["+objName+"] representing character ["+characterName+"]", kDebug);
// Handle inter-page teleports by searching for graphic token
// on destination page with same name as teleporting token
var teleportedTokens = findObjs({
_pageid: destPageId,
_type: 'graphic',
name: objName,
represents: characterId
});
if (teleportedTokens.length == 0) {
logger("No token with name ["+objName+"] representing character ["+characterName+"] found on destination page ["+destPageName+"], skipping...", kInfo);
continue;
} else {
logger("Executing inter-page teleport to page ["+destPageName+"] for token ["+objName+"]", kNotice);
if (teleportedTokens.length > 1) {
logger("More than one token named ["+objName+"] and representing character ["+characterName+"] found on destination page ["+destPageName+"]", kWarning);
} else {
logger("Found corresponding token ["+objName+"] on destination page ["+destPageName+"]", kInfo);
}
// Handle first matching teleported token
var teleportedToken = teleportedTokens[0];
// Update teleported token on new page with coordinates of
// destination teleporter
teleportedToken.set('left', NewX);
teleportedToken.set('top', NewY);
// Show teleported token
teleportedToken.set('layer', 'objects');
// Hide original token
obj.set('layer', 'gmlayer');
// Teleport all players with control of token to new page
var controllers = character.get('controlledby');
teleportPlayers(characterName, controllers, destPageId);
}
return;
}
}
}
}
}
};
/*
registerEventHandlers
Registers events to handle when token's position is adjusted
*/
var registerEventHandlers = function() {
on('change:graphic:left', handleGraphicChange);
on('change:graphic:top', handleGraphicChange);
logger("Ready", kNotice);
};
return {
RegisterEventHandlers: registerEventHandlers,
};
}());
on('ready', function() {
'use strict';
MapTeleporters.RegisterEventHandlers();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment