Created
June 14, 2018 04:12
-
-
Save finalfrog/e43438208515939f8ae908203a12371a to your computer and use it in GitHub Desktop.
MapTeleporters_WIP.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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