Skip to content

Instantly share code, notes, and snippets.

@finalfrog
Last active May 15, 2022 12:25
Show Gist options
  • Save finalfrog/124f67ad84204546caf16fffd84115e4 to your computer and use it in GitHub Desktop.
Save finalfrog/124f67ad84204546caf16fffd84115e4 to your computer and use it in GitHub Desktop.
// 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';
// Special thanks to The Aaron for this function
var findContains = function(obj,layer){
"use strict";
var cx = obj.get('left'),
cy = obj.get('top');
if(obj) {
layer = layer || 'gmlayer';
return _.chain(findObjs({
_pageid: obj.get('pageid'),
_type: "graphic",
layer: layer
}))
.reduce(function(m,o){
var l=o.get('left'),
t=o.get('top'),
w=o.get('width'),
h=o.get('height'),
ol=l-(w/2),
or=l+(w/2),
ot=t-(h/2),
ob=t+(h/2);
if( ol <= cx && cx <= or
&& ot <= cy && cy <= ob && o.get('name') != ""
){
m.push(o);
}
return m;
},[])
.value();
}
return [];
};
var handleGraphicChange = function(obj) {
if (obj.get("layer") !== "objects") {
return; // Only teleport tokens on the object layer
}
// Check for token controlled by only one player
var characterId = obj.get("represents");
var characters = findObjs({
_type: "character",
_id: characterId,
});
if (characters.length == 0) {
// No characters found
return;
}
// Handle first matching representing character
var character = characters[0];
var controllers = character.get("controlledby");
if (controllers == "") {
return;
}
/* To use this system, you need to name two Teleportation locations the same
* with only an A and B distinction. For instance Teleport01A and Teleport01B
* will be linked together. When a token gets on one location, it will be
* Teleported to the other automatically */
// Find any teleporters in the same location as moved token
var sourceTeleporters = findContains(obj,"gmlayer");
if (sourceTeleporters.length == 0) {
// No source teleporters found
return;
} else {
// Handle first matching source teleporter
var sourceTeleporter = sourceTeleporters[0];
var sourceOffsetX = sourceTeleporter.get("left") - obj.get("left");
var sourceOffsetY = sourceTeleporter.get("top") - obj.get("top");
// Store the page of the source teleporter
var sourcePage = sourceTeleporter.get("_pageid");
// Get name of current teleporter
var CurrName = sourceTeleporter.get("name");
var Letters = new Array("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L");
// Number of doors in the cycle (second to last character)
var doorCount = CurrName.substr(CurrName.length - 2, 1);
// Current Letter of the Door
var currDoor = CurrName.substr(CurrName.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 {
i = i + 1;
}
var NewName = CurrName.substr(0,CurrName.length - 2) + doorCount + Letters[i];
var destTeleporters = findObjs({
_type: "graphic",
layer: "gmlayer", //target location MUST be on GM layer
name: NewName,
});
if (destTeleporters.length == 0) {
// No destination teleporters found
return;
} else {
// Handle first matching destination teleporter
var destTeleporter = destTeleporters[0];
// Coordinates of destination teleporter
var NewX = destTeleporter.get("left") - sourceOffsetX;
var NewY = destTeleporter.get("top") - sourceOffsetY;
// Page of destination teleporter
var destPage = destTeleporter.get("_pageid");
if (destPage == sourcePage) {
// Handle intra-page teleports by just changing position of
// original token.
obj.set("left", NewX);
obj.set("top", NewY);
} else {
// Handle inter-page teleports by searching for graphic token
// on destination page with same name as teleporting token
var teleportedTokens = findObjs({
_pageid: destPage,
_type: "graphic",
name: obj.get("name"),
});
if (teleportedTokens.length == 0) {
// No tokens with the same name as teleporting token found
return;
} else {
// 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 if it is hidden
teleportedToken.set("layer", "objects");
var controllerArray = controllers.split();
if (controllerArray.length == 1) {
// Teleport player to the page with the destination
// teleporter
teleport(controllerArray[0], destPage);
// NOTE: sendPing moveAll argument currently broken (See https://wiki.roll20.net/Talk:API:Utility_Functions)
sendPing(NewX, NewY, destPage, null, true);
// Hide original token if player has moved to new map
obj.set("layer", "gmlayer");
}
}
}
}
}
};
var teleport = function(playerId, pageId) {
var playerPages = Campaign().get("playerspecificpages");
if (playerPages === false) {
playerPages = {};
}
if (playerId == "all") {
// Move the whole group to the target page
Campaign().set("playerspecificpages", false);
Campaign().set("playerpageid", pageId);
}
// Remove player from playerPages
if (playerId in playerPages) {
delete playerPages[playerId];
}
// Update playerPages with player on target page
playerPages[playerId] = pageId;
Campaign().set("playerspecificpages", false);
Campaign().set("playerspecificpages", playerPages);
};
var registerEventHandlers = function() {
on('change:graphic:left', handleGraphicChange);
on('change:graphic:top', handleGraphicChange);
};
return {
RegisterEventHandlers: registerEventHandlers,
};
}());
on("ready", function() {
'use strict';
MapTeleporters.RegisterEventHandlers();
});
@finalfrog
Copy link
Author

Yeah, I stopped using this after a few sessions. Roll20's API engine is just not reliable enough for something like this. Frequently nothing would happen when you moved a token into the zone. Sometimes it would move a player to another map, but fail to hide their token on the current map or fail to unhide their token on the new map.

I've switched to Foundry and find its scripting engine much more reliable.

@Randalthor66
Copy link

Randalthor66 commented May 8, 2021 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment