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();
});
@Randalthor66
Copy link

So I have tried to use this, but it doesn't seem to work. I copied the code from line 58 (after all the instructional text, where it says " var MapTeleporters = MapTeleporters || (function() { ") and it didn't throw up an error, so I assume it was good. I copied the exact titling from the code ("Teleport02A" and "Teleport02B"). When I put the character token on 2A nothing happened. Does having the Dynamic Lighting set up matter?

@BudRog
Copy link

BudRog commented May 7, 2021

So I have tried to use this, but it doesn't seem to work. I copied the code from line 58 (after all the instructional text, where it says " var MapTeleporters = MapTeleporters || (function() { ") and it didn't throw up an error, so I assume it was good. I copied the exact titling from the code ("Teleport02A" and "Teleport02B"). When I put the character token on 2A nothing happened. Does having the Dynamic Lighting set up matter?

No Dynamic lighting is not a requirement.

The MapTeleporters.js works but it is finicky. Follow the instruction in the API.
1 It only works for player owned tokens
2 The Teleporter token must be on the GM layer
3 Teleporter token most be exactly aligned to a map square
4 place your player token right next to the left or right of the Teleporter token on the object layer and let go of it
6 Now move your player token into the Teleporter token space and again let go of it
If you just try to swipe your player token through the Teleporter token space it won't work.
If you just try to place your player token a space away from the Teleporter token space it won't work
It only works if it is exactly to the right or left of the Teleporter token space
7 Teleporter will work through dynamic lighting lines but a lighting line can not intersect the Teleporter token space at all and in fact you needs some space between the Teleporter token space and the dynamic lighting lines. If you expect the player to go back the way they can through the Teleporter you must provide at least one fully clear space to the right or left of the dynamic lighting lines or it won't work on the return trip.

8 I like this MapTeleporter and you can use it to navigate levels of a house or something within the same map but if you expect it to take characters to a different map page you must place a copy of the characters on the target map preferably on the GM layer.

I wish it wasn't as finicky as it is but it still expands what you can do as a GM/DM.

Engoy.

@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