Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Token Moving and Teleporting Between Maps

1599688933
Pat
Pro
API Scripter
I'm looking for something in particular - I had found a "Teleporter" script that I don't think is in the list anymore... I have a copy of the code, I'd been trying to modify it to teleport within the same map - no dice, the script for movement even if you move to GM layer first and then back down - behaves in the typical fashion (releasing the thread is required etc or it just scoots the token). I'm hoping to either modify this old teleporter script (it was decent for detecting token collision and easier to implement than some others I've been trying but failing at) or find a new one - ideally no pop-ups, just move the token and player when they encounter it, maybe have the two tokens be two-way, or one way defined.  The goal is to make multiple floors in a building, or in a tower, or in a dungeon, that players can move between freely. Lacking this or multiple layers in the table-top I end up playing in flatland with all of my campaigns unless I'm manually moving players all the time. 
1599698111
The Aaron
Roll20 Production Team
API Scripter
Here's the much modified one I use: /* ************ TELEPORTING SCRIPT ************************** * The intention of this script is to allow the DM to teleport * one or all characters to a location based on a token placed * on the DM layer of the map. * To activate the script, type "!Teleport " and add the name * of the teleport location (must not contrain spaces) and then * the name of the party member to teleport there. They must be * seperated by commas. If you want all to teleport, type all. * ie. !Teleport teleport01, all - teleports all players to teleport01 * * AUTOTELEPORTING: This feature allows you to place a token on * One square (for example stairs) and it will auto move a token * to the linked location and back again should you choose. * Linked locations need to be tokens placed on the GMLayer. * Naming conventions: * Two way doors: XXXXXXXX2A, XXXXXXXXX2B * Three way dooes: XXXXXXXX3A, XXXXXXXXX3B, XXXXXXXXX3C * (in the case of one way doors, dont create a 3C) * This system can handle up to 9 way doors (9I max). ****************************************************************/ on('ready',() => { var Teleporter = Teleporter || {}; const statusmarkersToObject = (stats) => _.reduce(stats.split(/,/), function(memo, value) { let parts = value.split(/@/), num = parseInt(parts[1] || '0', 10); if (parts[0].length) { memo[parts[0]] = Math.max(num, memo[parts[0]] || 0); } return memo; }, {}); // const objectToStatusmarkers = (obj) => _.map(obj, function(value, key) { // return key === 'dead' || value < 1 || value > 9 ? key : key + '@' + parseInt(value); // }) // .join(','); Teleporter.AUTOTELEPORTER = true; //Set to true if you want teleports to be linked Teleporter.Teleport = function (CharName, TargetName) { "use strict"; var LocX = 0; var LocY = 0; //find the target location var location = findObjs({ _pageid: Campaign().get("playerpageid"), _type: "graphic", layer: "gmlayer", //target location MUST be on GM layer name: TargetName }); if (location.length === 0) { return; //exit if invalid target location } LocX = location[0].get("left"); LocY = location[0].get("top"); //if all are indicated, it lists all //finds all tokens with the name var targets = findObjs({ _pageid: Campaign().get("playerpageid"), _type: "graphic" }); //Move characters to target location _.each(targets, function(obj) { //Only player tokens if (CharName === "all") { if (obj.get("represents") !== "") { log("Setting all"); obj.set("left", LocX + 1); obj.set("top", LocY); } } else { if (obj.get("name").indexOf(CharName) !== -1) { if (obj.get("represents") !== "") { obj.set("left", LocX + 1); obj.set("top", LocY); } } } }); }; on("chat:message", function(msg) { "use strict"; var cmdName = "!Teleport "; if (msg.type === "api" && msg.content.indexOf(cmdName) !== -1 && playerIsGM(msg.playerid)) { var cleanedMsg = msg.content.replace(cmdName, ""); var commands = cleanedMsg.split(", "); var targetName = commands[0]; var i = 1; while ( i < commands.length ) { Teleporter.Teleport(commands[i], targetName); i = i + 1; } } }); var findContains = function(obj,layer){ "use strict"; let cx = obj.get('left'), cy = obj.get('top'); if(obj) { layer = layer || 'gmlayer'; return _.chain(findObjs({ _pageid: obj.get('pageid'), _type: "graphic", layer: layer })) .filter((o)=>/Teleport/.test(o.get('name'))) .reduce(function(m,o){ let 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 ){ m.push(o); } return m; },[]) .value(); } return []; }; const CheckLock = (portal, obj) => { let objKey=statusmarkersToObject(obj.get('statusmarkers')); return _.reduce(statusmarkersToObject(portal.get('statusmarkers')),(m,v,k) => m && _.has(objKey,k) && objKey[k] === v, true); }; on("change:graphic", function(obj) { "use strict"; if (obj.get("name").indexOf("Teleport") !== -1 || /(walls|map)/.test(obj.get('layer'))) { return; //Do not teleport teleport pads!! } if (Teleporter.AUTOTELEPORTER === false) { return; //Exit if auto Teleport is disabled } /* 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 */ //Finds the current teleportation location var CurrName = ""; var location = findContains(obj,'gmlayer'); if (location.length === 0) { return; } let Curr = location[0]; // let CurrLock = statusmarkersToObject(Curr.get('statusmarkers')); // let TokenKey = statusmarkersToObject(obj.get('statusmarkers')); // let valid = true; // _.each(CurrLock,(v,k)=>{ // if(!_.has(TokenKey,k) || ! TokenKey[k] === v){ // valid=false; // } // }); // if(!valid){ // return; // } if(!CheckLock(Curr,obj)){ return; } CurrName = location[0].get("name"); var Letters = ["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 = 0; if( CurrName.match(/^R:/) ) { i = randomInteger(doorCount)-1; } else { 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 NewX = 0; var NewY = 0; var newLocation = findObjs({ _pageid: obj.get('pageid'), _type: "graphic", layer: "gmlayer", //target location MUST be on GM layer name: NewName }); _.each(newLocation, function(Loc){ //Get the new Location NewX = Loc.get("left"); NewY = Loc.get("top"); }); if (NewX === 0 ) { return; } let currLayer=obj.get('layer'); obj.set({ layer: 'gmlayer' }); _.delay(()=>{ obj.set({ left: NewX, top: NewY, lastmove: '' }); _.delay(()=>{ obj.set({ layer: currLayer }); },500); },100); }); on("chat:message", function(msg) { "use strict"; if (msg.content.indexOf("!AUTOTELEPORTER") !== -1 && playerIsGM(msg.playerid)) { if ( Teleporter.AUTOTELEPORTER === true) { sendChat("System", "/w gm Autoteleporting Disabled."); Teleporter.AUTOTELEPORTER = false; } else { sendChat("System", "/w gm Autoteleporting Enabled."); Teleporter.AUTOTELEPORTER = true; } } }); });
1599698750
The Aaron
Roll20 Production Team
API Scripter
You put graphics on the GM layer and name them "Teleport<something>#A".  the <something is the unique identifier for the teleporter on the page.  The number tells how many endpoints there are. the trailing letter (A–I) tells what order in the sequence that one is.  Example of a 2 way, back and forth, teleporter: Teleport_MageRoom1_2A Teleport_MageRoom1_2B I added a few features to this.  First, if the teleport graphic has the Dead X marker, it doesn't function.  Any other combination of markers will only let a token with the same markers (including the same numbers) teleport on it.  That lets you set combinations.  In my game, I had different colored rings that were required to operate the teleport pads in the Wizards' Hostel.  For example, if the graphic on the GM layer has green:3   red:7,  only a player with green:3 and red:7  on their token would get teleported.
1599701663
Pat
Pro
API Scripter
Thanks! Quick question - does this support teleport between maps? Does it carry over the requirement from the original to have a player token pre-staged on each map? 
1599710777
The Aaron
Roll20 Production Team
API Scripter
This is just for same page teleportation. (At least, that's all I've used it for). Cross page is problematic because the API can't create marketplace graphics, so it's possible to fail to make a character's token. 
1599730867
Pat
Pro
API Scripter
I remember the difficulty with this now - the pop-overs on the tokens (three bar readouts etc.) all appear at the "last" click (teleport "from" point), and there isn't a way to move the player view to their new location. I think there supposedly was an API call but it's not honored for setting a "ping" or transitioning a player view. 
1599738763
The Aaron
Roll20 Production Team
API Scripter
Oh!  That's something I can at least partially fix. The ping function has been overhauled recently and can be set to pull a specific player. I'm not sure what you mean about he bars staying at the lady point, are you using Bump?
1599759076
Pat
Pro
API Scripter
Yes, please on ping! If the teleport script were upated with this or if I can help updating it with this I'd be immensely pleased! I'm not using any other setup other than the basics needed for "It's a Trap!" and "Token Health"  - I'll post a screenshot of what I'm seeing with teleport between two on-screen points.  Before Teleport: After Teleport: 
1599763582
The Aaron
Roll20 Production Team
API Scripter
I'll try and take a look at that tonight or this weekend.  =D Weird on the bars. Is that just for the GM, or for when you're logged in as a player also?  I'll see if I can duplicate that.
1599766051
Pat
Pro
API Scripter
It's as a player - thanks :) 
1599864553

Edited 1599881199
Pat
Pro
API Scripter
Got the ping working! Does it always have to show the ring, or is that adjustable, or is the follow the only thing adjustable?   ...managed to wire up a SFX at destination - probably could rig it up at start... just the big yellow server-generated ping obscures it, and I don't see any option to *just* move player viewpoint. The lagging/residual player bubbles still remains...   Just hit me that if the player "color" is "transparent" then the ping is invisible - and if I could swap the player color to transparent *just for the teleport* then I'd have a ping-less appearing teleport script! I'm going to dig through Heartbeat for how to swap player swatch IDs and restore them after teleport and ping...  Fouled it up - I wasn't getting a single player for the ping, I was defaulting to "all" because the attribute I got doesn't exist (empty string) so I'll have to get the representing object and get the player ID from that I suppose...   Okay, did it! - The ping it turns out is an ongoing effect, so it's not a one-and-done, so in changing the player ping to transparent, I have to wait a full second for the ping to complete before swapping it back...  This is inartful inelegant code, but dang blast it, it works!          /* ************ TELEPORTING SCRIPT ************************** * The intention of this script is to allow the DM to teleport * one or all characters to a location based on a token placed * on the DM layer of the map. * To activate the script, type "!Teleport " and add the name * of the teleport location (must not contrain spaces) and then * the name of the party member to teleport there. They must be * seperated by commas. If you want all to teleport, type all. * ie. !Teleport teleport01, all - teleports all players to teleport01 * * AUTOTELEPORTING: This feature allows you to place a token on * One square (for example stairs) and it will auto move a token * to the linked location and back again should you choose. * Linked locations need to be tokens placed on the GMLayer. * Naming conventions: * Two way doors: XXXXXXXX2A, XXXXXXXXX2B * Three way dooes: XXXXXXXX3A, XXXXXXXXX3B, XXXXXXXXX3C * (in the case of one way doors, dont create a 3C) * This system can handle up to 9 way doors (9I max). ****************************************************************/ on('ready',() => { var Teleporter = Teleporter || {}; const statusmarkersToObject = (stats) => _.reduce(stats.split(/,/), function(memo, value) { let parts = value.split(/@/), num = parseInt(parts[1] || '0', 10); if (parts[0].length) { memo[parts[0]] = Math.max(num, memo[parts[0]] || 0); } return memo; }, {}); // const objectToStatusmarkers = (obj) => _.map(obj, function(value, key) { // return key === 'dead' || value < 1 || value > 9 ? key : key + '@' + parseInt(value); // }) // .join(','); Teleporter.AUTOTELEPORTER = true; //Set to true if you want teleports to be linked Teleporter.Teleport = function (CharName, TargetName) { "use strict"; var LocX = 0; var LocY = 0; //find the target location var location = findObjs({ _pageid: Campaign().get("playerpageid"), _type: "graphic", layer: "gmlayer", //target location MUST be on GM layer name: TargetName }); if (location.length === 0) { return; //exit if invalid target location } LocX = location[0].get("left"); LocY = location[0].get("top"); //if all are indicated, it lists all //finds all tokens with the name var targets = findObjs({ _pageid: Campaign().get("playerpageid"), _type: "graphic" }); //Move characters to target location _.each(targets, function(obj) { //Only player tokens if (CharName === "all") { if (obj.get("represents") !== "") { log("Setting all"); obj.set("left", LocX + 1); obj.set("top", LocY); } } else { if (obj.get("name").indexOf(CharName) !== -1) { if (obj.get("represents") !== "") { obj.set("left", LocX + 1); obj.set("top", LocY); } } } }); }; on("chat:message", function(msg) { "use strict"; var cmdName = "!Teleport "; if (msg.type === "api" && msg.content.indexOf(cmdName) !== -1 && playerIsGM(msg.playerid)) { var cleanedMsg = msg.content.replace(cmdName, ""); var commands = cleanedMsg.split(", "); var targetName = commands[0]; var i = 1; while ( i < commands.length ) { Teleporter.Teleport(commands[i], targetName); i = i + 1; } } }); var findContains = function(obj,layer){ "use strict"; let cx = obj.get('left'), cy = obj.get('top'); if(obj) { layer = layer || 'gmlayer'; return _.chain(findObjs({ _pageid: obj.get('pageid'), _type: "graphic", layer: layer })) .filter((o)=>/Teleport/.test(o.get('name'))) .reduce(function(m,o){ let 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 ){ m.push(o); } return m; },[]) .value(); } return []; }; const CheckLock = (portal, obj) => { let objKey=statusmarkersToObject(obj.get('statusmarkers')); return _.reduce(statusmarkersToObject(portal.get('statusmarkers')),(m,v,k) => m && _.has(objKey,k) && objKey[k] === v, true); }; on("change:graphic", function(obj) { "use strict"; if (obj.get("name").indexOf("Teleport") !== -1 || /(walls|map)/.test(obj.get('layer'))) { return; //Do not teleport teleport pads!! } if (Teleporter.AUTOTELEPORTER === false) { return; //Exit if auto Teleport is disabled } /* 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 */ //Finds the current teleportation location var CurrName = ""; var location = findContains(obj,'gmlayer'); if (location.length === 0) { return; } let Curr = location[0]; // let CurrLock = statusmarkersToObject(Curr.get('statusmarkers')); // let TokenKey = statusmarkersToObject(obj.get('statusmarkers')); // let valid = true; // _.each(CurrLock,(v,k)=>{ // if(!_.has(TokenKey,k) || ! TokenKey[k] === v){ // valid=false; // } // }); // if(!valid){ // return; // } if(!CheckLock(Curr,obj)){ return; } CurrName = location[0].get("name"); var Letters = ["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 = 0; if( CurrName.match(/^R:/) ) { i = randomInteger(doorCount)-1; } else { 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 NewX = 0; var NewY = 0; var newLocation = findObjs({ _pageid: obj.get('pageid'), _type: "graphic", layer: "gmlayer", //target location MUST be on GM layer name: NewName }); _.each(newLocation, function(Loc){ //Get the new Location NewX = Loc.get("left"); NewY = Loc.get("top"); }); if (NewX === 0 ) { return; } let currLayer=obj.get('layer'); let character=getObj("character", obj.get('represents')); let player=getObj("player", character.get('controlledby')); log(player.get("_id")); let oldColor=player.get("color"); player.set({color:"transparent"}); log(player.get("color")); obj.set({ layer: 'gmlayer' }); _.delay(()=>{ obj.set({ left: NewX, top: NewY, lastmove: '' }); _.delay(()=>{ obj.set({ layer: currLayer }); // sendChat("System", player.color + "." ); // this is a player ID // Ping only the specified player to this location - (get controlling player) sendPing(NewX, NewY, Campaign().get("playerpageid"), player.get("_id"), true, player.get("_id")); spawnFx(NewX, NewY, "nova-magic", Campaign().get("playerpageid")); _.delay(()=>{player.set({color: oldColor});},1000); // obj.get("name").indexOf(CharName) },500); },100); }); on("chat:message", function(msg) { "use strict"; if (msg.content.indexOf("!AUTOTELEPORTER") !== -1 && playerIsGM(msg.playerid)) { if ( Teleporter.AUTOTELEPORTER === true) { sendChat("System", "/w gm Autoteleporting Disabled."); Teleporter.AUTOTELEPORTER = false; } else { sendChat("System", "/w gm Autoteleporting Enabled."); Teleporter.AUTOTELEPORTER = true; } } }); });
1599871294
Pat
Pro
API Scripter
This has me thinking now - if only one could rig up a "faux" player to use for this ping, you wouldn't have to swap - or if one could set the system color ping. Also, having the teleport spots be able to store the code for FX (if there is one, what the FX is when the spot is used, etc). 
1599914533
The Aaron
Roll20 Production Team
API Scripter
Hey, nice job sorting this out!
1599915577

Edited 1599917248
Pat
Pro
API Scripter
Thanks! You provided 99% of this, and it's not totally sorted - it's still got the lagging bubbles - can you replicate the issue on your side?  Today I discovered: a GM in "player mode" cannot control a token "controlled by" "all." 
1599932198
The Aaron
Roll20 Production Team
API Scripter
I don't have a problem controlling an "All Players" token when I'm GM as Player.  Does the token represent a character that is set correctly? I duplicated the bubbles thing.  A Ping-Pull should force the display to refresh, Other than that, you might have to pop it to the GM layer and back to force a deselect event.
1599941279
Pat
Pro
API Scripter
I duplicated the bubbles thing.  A Ping-Pull should force the display to refresh, Other than that, you might have to pop it to the GM layer and back to force a deselect event. That's the weird thing - The ping pull happens, and the GM layer and back happens...and the bubbles are still there... maybe it only happens because I'm still somehow coded as GM even when I'm joined "as a player?" 
1599981484

Edited 1600018812
Pat
Pro
API Scripter
To do for this (for me) Find the GM player ID for "all" pings.✔️ Find out if tokenmarker code is current. ✔️ Make sure the ping happens on the page the player of the token is actually on (not possible for "all" so...) in case they are separate from the player ribbon. Check the teleportation token GM notes for either a string (for basic effects) or an object (for custom immediately generated special effects) (basic effects and custom fx objects) ✔️ ❌ Look into supplying/defining sound effects on teleport (play sound, check for audio controller? add sfx and soundfx to GM notes for teleporter?)  Find out if select bubbles show for players vs. GM.  Ping toggle (y/n) ✔️
1600017026

Edited 1600018654
Pat
Pro
API Scripter
New problem: trying to parse the gmnotes on a token - I'm getting all sorts of crazy extra formatting, like the gmnotes is a Word Doc or somesuch, including a lot of font formatting - this is new, as it supposedly is coming in as a string, and when evaluated as a string it's fine - try and parse it as an object and kaboom. Looking around I'm not seeing anyone else with this issue...  Now it's not even working with strings - I get this at the start and end of any string every time: "%3Cp%3E" It's garbage "paragraph" tag start/ends, when I've not used any styles or styling... this kills any use of the dang gmnotes. 
1600023376
The Aaron
Roll20 Production Team
API Scripter
The gmnotes field on tokens is basically urlencoded HTML.  I've found the unescape() function does a nice job of turning it back into HTML.  Then you can parse it however you'd like.  You can get a raw string from it with something like: let rawString = unescape(token.get('gmnotes')).replace(/<[^>]*>/g,'');
1600026714

Edited 1600048086
Pat
Pro
API Scripter
Thanks - that worked! Also had to get through my skull that spawnFX is not the same function as spawnFXWithDefinition. Find the GM player ID for "all" pings.✔️ Find out if tokenmarker code is current. ✔️ Ping toggle (y/n) ✔️ Check the teleportation token GM notes for either a string (for basic effects) or an object (for custom immediately generated special effects) (basic effects and custom fx objects) ✔️ Make sure the ping happens on the page the player of the token is actually on (not possible for "all" so...) in case they are separate from the player ribbon. (use obj.get("_pageId"), silly!) ✔️ Catch edge cases of tokens with no character ✔️ Look into supplying/defining sound effects on teleport (play sound, check for audio controller? add sfx and soundfx to GM notes for teleporter?) ♻️ Find out if select bubbles show for players vs. GM. ♻️ - Doesn't matter for players. FX on departure too? ♻️
1600046922

Edited 1600047570
I keep getting this error **(Seems to happen with uncontrolled characters) For reference, the error message generated was:  TypeError: Cannot read property 'get' of undefined TypeError: Cannot read property 'get' of undefined at apiscript.js:9190:20 at eval (eval at <anonymous> (/home/node/d20-api-server/api.js:154:1), <anonymous>:65:16) at Object.publish (eval at <anonymous> (/home/node/d20-api-server/api.js:154:1), <anonymous>:70:8) at TrackedObj.set (/home/node/d20-api-server/api.js:1055:14) at updateLocalCache (/home/node/d20-api-server/api.js:1345:18) at /home/node/d20-api-server/api.js:1529:11 at /home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:560 at hc (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:39:147) at Kd (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:546) at Id.Mb (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:489)
1600048158
Pat
Pro
API Scripter
Just fixed this version of that:  /* ************ TELEPORTING SCRIPT  ************************** *   The intention of this script is to allow the DM to teleport *   one or all characters to a location based on a token placed  *   on the DM layer of the map.  *     To activate the script, type "!Teleport " and add the name *   of the teleport location (must not contrain spaces) and then  *   the name of the party member to teleport there. They must be  *   seperated by commas. If you want all to teleport, type all.  *   ie. !Teleport teleport01, all - teleports all players to teleport01 * *   AUTOTELEPORTING: This feature allows you to place a token on  *   One square (for example stairs) and it will auto move a token  *   to the linked location and back again should you choose. *   Linked locations need to be tokens placed on the GMLayer. *   Naming conventions: *   Two way doors:   XXXXXXXX2A, XXXXXXXXX2B *   Three way dooes: XXXXXXXX3A, XXXXXXXXX3B, XXXXXXXXX3C *       (in the case of one way doors, dont create a 3C) *   This system can handle up to 9 way doors (9I max). ****************************************************************/ on('ready',() => {     var Teleporter = Teleporter || {};     const statusmarkersToObject = (stats) => _.reduce(stats.split(/,/), function(memo, value) {             let parts = value.split(/@/),             num = parseInt(parts[1] || '0', 10);             if (parts[0].length) {                 memo[parts[0]] = Math.max(num, memo[parts[0]] || 0);             }             return memo;         }, {}); //    const objectToStatusmarkers = (obj) => _.map(obj, function(value, key) { //            return key === 'dead' || value < 1 || value > 9 ? key : key + '@' + parseInt(value); //        }) //        .join(',');           Teleporter.AUTOTELEPORTER = true; //Set to true if you want teleports to be linked     Teleporter.AUTOPINGMOVE = true; //Set to true if you want individual auto-teleports/teleports to also move the view.     // Run this onece to get a player object for the GM for "default" pings for "all"      Teleporter.DEFAULTPLAYER =  (function(){             let player;             let playerlist = findObjs({                                                 _type: "player",                                       });             _.each(playerlist, function(obj) {                   if(playerIsGM(obj.get("_id"))){                   player = obj;               };             });             return player;     })();     Teleporter.Teleport = function (CharName, TargetName) {         "use strict";                  var LocX = 0;         var LocY = 0;               //find the target location         var location = findObjs({             _pageid: Campaign().get("playerpageid"),                                           _type: "graphic",             layer: "gmlayer", //target location MUST be on GM layer             name: TargetName         });                  if (location.length === 0) {             return; //exit if invalid target location         }               LocX = location[0].get("left");         LocY = location[0].get("top");                  //if all are indicated, it lists all         //finds all tokens with the name         var targets = findObjs({             _pageid: Campaign().get("playerpageid"),                                           _type: "graphic"         });         //Move characters to target location         _.each(targets, function(obj) {             //Only player tokens             if (CharName === "all") {                 if (obj.get("represents") !== "") {                     log("Setting all");                     obj.set("left", LocX + 1);                     obj.set("top", LocY);                 }             }             else {                 if (obj.get("name").indexOf(CharName) !== -1) {                     if (obj.get("represents") !== "") {                         obj.set("left", LocX + 1);                         obj.set("top", LocY);                     }                 }             }         });     };           on("chat:message", function(msg) {            "use strict";         var cmdName = "!Teleport ";               if (msg.type === "api" && msg.content.indexOf(cmdName) !== -1 && playerIsGM(msg.playerid)) {             var cleanedMsg = msg.content.replace(cmdName, "");             var commands = cleanedMsg.split(", ");             var targetName = commands[0];                   var i = 1;             while ( i < commands.length ) {                 Teleporter.Teleport(commands[i], targetName);                 i = i + 1;             }         }     });            var findContains = function(obj,layer){         "use strict";         let cx = obj.get('left'),             cy = obj.get('top');         if(obj) {             layer = layer || 'gmlayer';             return _.chain(findObjs({                     _pageid: obj.get('pageid'),                                                   _type: "graphic",                     layer: layer                  }))                 .filter((o)=>/Teleport/.test(o.get('name')))                 .reduce(function(m,o){                     let 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                      ){                         m.push(o);                     }                     return m;                 },[])                 .value();         }         return [];      };      const CheckLock = (portal, obj) => {         let objKey=statusmarkersToObject(obj.get('statusmarkers'));         return _.reduce(statusmarkersToObject(portal.get('statusmarkers')),(m,v,k) => m && _.has(objKey,k) && objKey[k] === v, true);     };           on("change:graphic", function(obj) {         "use strict";         if (obj.get("name").indexOf("Teleport") !== -1 || /(walls|map)/.test(obj.get('layer'))) {             return; //Do not teleport teleport pads!!         }         if (Teleporter.AUTOTELEPORTER === false) {             return; //Exit if auto Teleport is disabled         }         /*  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 */                  //Finds the current teleportation location         var CurrName = "";                  var location = findContains(obj,'gmlayer');         if (location.length === 0) {             return;         }                  let Curr = location[0]; //        let CurrLock = statusmarkersToObject(Curr.get('statusmarkers')); //        let TokenKey = statusmarkersToObject(obj.get('statusmarkers')); //        let valid = true; //        _.each(CurrLock,(v,k)=>{ //            if(!_.has(TokenKey,k) || ! TokenKey[k] === v){ //                valid=false; //            } //        }); //        if(!valid){ //            return; //        }         if(!CheckLock(Curr,obj)){             return;         }                           CurrName = location[0].get("name");                  var Letters = ["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 = 0;                  if( CurrName.match(/^R:/) ) {             i = randomInteger(doorCount)-1;         } else {             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 NewX = 0;         var NewY = 0;         var LocFX = "";                  var newLocation = findObjs({             _pageid: obj.get('pageid'),             _type: "graphic",             layer: "gmlayer", //target location MUST be on GM layer             name: NewName         });         _.each(newLocation, function(Loc){                 //Get the new Location             NewX = Loc.get("left");             NewY = Loc.get("top");             LocFX = unescape(Loc.get('gmnotes')).replace(/<[^>]*>/g,'');         });                  if (NewX === 0 ) {             return;         }                           if(LocFX !== '' && LocFX.indexOf("{") !== -1){             LocFX = JSON.parse(LocFX); // convert LocFX to an object         }else{                      }         log(LocFX);         let currLayer=obj.get('layer');         let character=(obj.get('represents'))?getObj("character", obj.get('represents')):null;         let player, follow, oldColor;          var controller = (character)?character.get('controlledby'):'';         if(controller !== '' && controller !== 'all' ){             player=getObj("player", controller);             follow=true;         }else{             // set player to GM (eventually), and if not "all" set follow to false             player=Teleporter.DEFAULTPLAYER;             follow=(controller === 'all')?true:false;         }         // use override for send ping         if((!Teleporter.AUTOPINGMOVE && follow) || !follow){             follow=false;         }else{             oldColor=player.get("color");             player.set({color:"transparent"});         }                          obj.set({             layer: 'gmlayer'         });         _.delay(()=>{             obj.set({                 left: NewX,                 top: NewY,                 lastmove: ''             });             _.delay(()=>{                 obj.set({                     layer: currLayer                 });                 // Ping only the specified player to this location - (get controlling player, have to account for multiple/"all")                 if(follow){sendPing(NewX, NewY, obj.get('pageid'), player.get("_id"), true, player.get("_id"));}                 if(LocFX !== ''){                     if(_.isString(LocFX)){                         spawnFx(NewX, NewY, LocFX, obj.get('pageid'));                     }else{                         spawnFxWithDefinition(NewX, NewY, LocFX, obj.get('pageid'));                     }                 }                 _.delay(()=>{                     // Longer delay to re-set the player color to allow the ping to finish.                     if(follow){player.set({color: oldColor});}                 },1000);             },500);         },100);     });           on("chat:message", function(msg) {            "use strict";                  if (msg.content.indexOf("!AUTOTELEPORTER") !== -1 && playerIsGM(msg.playerid)) {                   if ( Teleporter.AUTOTELEPORTER === true) {                 sendChat("System", "/w gm Autoteleporting Disabled.");                 Teleporter.AUTOTELEPORTER = false;             }             else {                 sendChat("System", "/w gm Autoteleporting Enabled.");                 Teleporter.AUTOTELEPORTER = true;             }         }                  if (msg.content.indexOf("!AUTOPINGMOVE") !== -1 && playerIsGM(msg.playerid)) {                   if ( Teleporter.AUTOPINGMOVE === true) {                 sendChat("System", "/w gm Ping-move Disabled.");                 Teleporter.AUTOPINGMOVE = false;             }             else {                 sendChat("System", "/w gm Ping-move Enabled.");                 Teleporter.AUTOPINGMOVE = true;             }         }     });  });
1600059375

Edited 1600095469
Pat
Pro
API Scripter
New issue: may have to check "threading" and variable collision as I may have encountered a problem with too many tokens trying to cram through a teleportation point at once. I'll have to test with more players; side effects were not crash, but overlapping timeouts that ended the transfer from GM to Token layer and left the player color transparent.  I think the problem was one token/player firing off another teleport event for the same token before the first had "completed" - so I may have to look at locking a token from teleporting until it has "completed" its last teleport. 
1600126237
Pat
Pro
API Scripter
Trying to reify the whole thing with the text version of teleport, working on making it possible to use the ping too and make sure it works, when I ran across this:&nbsp; <a href="https://app.roll20.net/forum/post/9182277/autocenter-for-players-on-their-token" rel="nofollow">https://app.roll20.net/forum/post/9182277/autocenter-for-players-on-their-token</a> ...and I got to thinking that it might be possible to modify this script to do what they're looking for: on page change setting a player focus on associated tokens... just have to consider that some players have other effects, henchmen, pets, and so-on that they may also control, and not wanting multiple pings for a single player, and to exclude the GM...&nbsp;
1600170289
David M.
Pro
API Scripter
Sounds like Aaron's pull-players script would be a good place to start.
1600186695

Edited 1600186714
Pat
Pro
API Scripter
This is the tricky part:&nbsp; It will ping for every selected token, so if you're on several, your screen might jump around.&nbsp; Only the controlling players see the ping happen.&nbsp; If all is in the control list, everyone is pinged: ...which is the same question I want to answer - firing "!pull" on page load would be easy to do on its own, thanks for the reference!&nbsp;
1600186864
The Aaron
Roll20 Production Team
API Scripter
You'll want to delay the pull a bit as page loading can take longer for some than others.
1600188376
Pat
Pro
API Scripter
Would waiting on "on:ready" do that, or are some resources more delayed than others? Or is that a campaign event only, and not applicable to a page transition?&nbsp;
1601757972
Pat
Pro
API Scripter
Updated the script for the macro version to reify it with the teleporting version, still to do: make macro teleport ping.&nbsp; /* ************ TELEPORTING SCRIPT ************************** * The intention of this script is to allow the DM to teleport * one or all characters to a location based on a token placed * on the DM layer of the map. * To activate the script, type "!Teleport " and add the name * of the teleport location (must not contain spaces) and then * the name of the party member to teleport there. They must be * seperated by commas. If you want all to teleport, type all. * ie. !Teleport teleport01, all - teleports all players to teleport01 * * AUTOTELEPORTING: This feature allows you to place a token on * One square (for example stairs) and it will auto move a token * to the linked location and back again should you choose. * Linked locations need to be tokens placed on the GMLayer. * Naming conventions: * Two way doors: XXXXXXXX2A, XXXXXXXXX2B * Three way dooes: XXXXXXXX3A, XXXXXXXXX3B, XXXXXXXXX3C * (in the case of one way doors, dont create a 3C) * This system can handle up to 9 way doors (9I max). ****************************************************************/ on('ready',() =&gt; { var Teleporter = Teleporter || {}; const statusmarkersToObject = (stats) =&gt; _.reduce(stats.split(/,/), function(memo, value) { let parts = value.split(/@/), num = parseInt(parts[1] || '0', 10); if (parts[0].length) { memo[parts[0]] = Math.max(num, memo[parts[0]] || 0); } return memo; }, {}); // const objectToStatusmarkers = (obj) =&gt; _.map(obj, function(value, key) { // return key === 'dead' || value &lt; 1 || value &gt; 9 ? key : key + '@' + parseInt(value); // }) // .join(','); Teleporter.AUTOTELEPORTER = true; //Set to true if you want teleports to be linked Teleporter.AUTOPINGMOVE = true; //Set to true if you want individual auto-teleports/teleports to also move the view. Teleporter.AUTOPLAYFX = true; // Set to true if you want FX to play for teleport targets. Still checks for FX before playing. False turns all off. // Run this onece to get a player object for the GM for "default" pings for "all" Teleporter.DEFAULTPLAYER = (function(){ let player; let playerlist = findObjs({ _type: "player", }); _.each(playerlist, function(obj) { if(playerIsGM(obj.get("_id"))){ player = obj; }; }); return player; })(); /* The msg based teleporter works slightly differently from the teleporter-intersection script It relies on the list of passed tokens to grab and teleport tokens - it right now has no swap to GM layer, but it is hoped to integrate both functions into the same architecture to avoid redundancy, if it is at all possible. Right now: the msg based teleporter needs: - Swap to GM layer for each token DONE - Control for single FX generation on "All" DONE - Check on all "all" to make sure it is PLAYER or ALL controlled explicitly * otherwise this results in all tokens, player and GM controlled as long as they have a sheet, being teleported. DONE - Add ping if it is not "ALL", or add GM Ping (default) on any call of "ALL" - consider a pass of an fx for fun, maybe not - the way it is done now. (pass attr) - consider audio default for teleport as well.(pass and in destination) - function is inefficient since it performs a MASSIVE search multiple times to move a single token in series. DONE - Need to adapt this to digest all of the entries at once - can the findObjs only take a single entry? DONE */ Teleporter.Teleport = function (CharName, TargetName) { "use strict"; var LocX = 0; var LocY = 0; var LocFX = ""; var location = findObjs({ _pageid: Campaign().get("playerpageid"), _type: "graphic", layer: "gmlayer", //target location MUST be on GM layer name: TargetName }); if (location.length === 0) { return; //exit if invalid target location } LocX = location[0].get("left"); LocY = location[0].get("top"); LocFX = unescape(location[0].get('gmnotes')).replace(/&lt;[^&gt;]*&gt;/g,''); if(LocFX !== '' &amp;&amp; LocFX.indexOf("{") !== -1){ LocFX = JSON.parse(LocFX); // convert LocFX to an object } //just get tokens on the objects layer - don't specify name if all. if (CharName === "all"){ var targets = findObjs({ _pageid: Campaign().get("playerpageid"), _type: "graphic", layer: "objects" }); } else { var targets = findObjs({ _pageid: Campaign().get("playerpageid"), _type: "graphic", layer: "objects", name: CharName }); } // Move characters to target location - this does not use GMlayer, or ping, or sfx... // ...can we cross-link this with the other teleport script? // this teleport script doesn't shift to the GM Layer and then back... not useful for macros. // in the case of "All" we want to avoid sending multiple pings so we delay the ping until after all moves. _.each(targets, function(obj) { //Only player tokens if (CharName === "all") { // This is a problem, as it grabs ALL tokens, including enemies as long as they have a character sheet // need to validate that all is the same as all_tokens, which is unlikely if (obj.get("represents") !== "" &amp;&amp; getObj("character", obj.get('represents')).get('controlledby') !== "") { obj.set({layer:'gmlayer'}); _.delay(()=&gt;{ obj.set({ left: LocX + 1, top: LocY, lastmove: '' }); _.delay(()=&gt;{ obj.set({ layer: 'objects' }); },500); },100); } } else { obj.set({layer:'gmlayer'}); _.delay(()=&gt;{ obj.set({ left: LocX + 1, top: LocY, lastmove: '' }); _.delay(()=&gt;{ obj.set({ layer: 'objects' }); if(LocFX !== '' &amp;&amp; Teleporter.AUTOPLAYFX ){ if(_.isString(LocFX)){ spawnFx(LocX, LocY, LocFX, obj.get('pageid')); }else{ spawnFxWithDefinition(LocX, LocY, LocFX, obj.get('pageid')); } } },500); },100); } }); if (CharName === "all"){ _.delay(()=&gt;{ if(LocFX !== '' &amp;&amp; Teleporter.AUTOPLAYFX ){ if(_.isString(LocFX)){ spawnFx(LocX, LocY, LocFX, Campaign().get("playerpageid")); }else{ spawnFxWithDefinition(LocX, LocY, LocFX, Campaign().get("playerpageid")); } } },500); } }; on("chat:message", function(msg) { "use strict"; var cmdName = "!Teleport "; log("Entered TELEPORT message interface"); if (msg.type === "api" &amp;&amp; msg.content.indexOf(cmdName) !== -1 &amp;&amp; playerIsGM(msg.playerid)) { var cleanedMsg = msg.content.replace(cmdName, ""); var commands = cleanedMsg.split(", "); var targetName = commands[0]; var i = 1; // Fires off one command for each entry - meaning multiple SFX and refocus etc. unless ALL while ( i &lt; commands.length ) { Teleporter.Teleport(commands[i], targetName); i = i + 1; } } }); var findContains = function(obj,layer){ "use strict"; let cx = obj.get('left'), cy = obj.get('top'); if(obj) { layer = layer || 'gmlayer'; return _.chain(findObjs({ _pageid: obj.get('pageid'), _type: "graphic", layer: layer })) .filter((o)=&gt;/Teleport/.test(o.get('name'))) .reduce(function(m,o){ let 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 &lt;= cx &amp;&amp; cx &lt;= or &amp;&amp; ot &lt;= cy &amp;&amp; cy &lt;= ob ){ m.push(o); log("Teleporter Name:" + o.get('name')); } return m; },[]) .value(); } return []; }; const CheckLock = (portal, obj) =&gt; { let objKey=statusmarkersToObject(obj.get('statusmarkers')); return _.reduce(statusmarkersToObject(portal.get('statusmarkers')),(m,v,k) =&gt; m &amp;&amp; _.has(objKey,k) &amp;&amp; objKey[k] === v, true); }; on("change:graphic", function(obj) { "use strict"; if (obj.get("name").indexOf("Teleport") !== -1 || /(walls|map)/.test(obj.get('layer'))) { return; //Do not teleport teleport pads!! } if (Teleporter.AUTOTELEPORTER === false) { return; //Exit if auto Teleport is disabled } /* 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 */ //Finds the current teleportation location var CurrName = ""; var location = findContains(obj,'gmlayer'); if (location.length === 0) { return; } let Curr = location[0]; // let CurrLock = statusmarkersToObject(Curr.get('statusmarkers')); // let TokenKey = statusmarkersToObject(obj.get('statusmarkers')); // let valid = true; // _.each(CurrLock,(v,k)=&gt;{ // if(!_.has(TokenKey,k) || ! TokenKey[k] === v){ // valid=false; // } // }); // if(!valid){ // return; // } if(!CheckLock(Curr,obj)){ return; } CurrName = location[0].get("name"); var Letters = ["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 = 0; if( CurrName.match(/^R:/) ) { i = randomInteger(doorCount)-1; } else { 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 NewX = 0; var NewY = 0; var LocFX = ""; var newLocation = findObjs({ _pageid: obj.get('pageid'), _type: "graphic", layer: "gmlayer", //target location MUST be on GM layer name: NewName }); _.each(newLocation, function(Loc){ //Get the new Location NewX = Loc.get("left"); NewY = Loc.get("top"); LocFX = unescape(Loc.get('gmnotes')).replace(/&lt;[^&gt;]*&gt;/g,''); }); if (NewX === 0 ) { return; } if(LocFX !== '' &amp;&amp; LocFX.indexOf("{") !== -1){ LocFX = JSON.parse(LocFX); // convert LocFX to an object }else{ } // log(LocFX); let currLayer=obj.get('layer'); let character=(obj.get('represents'))?getObj("character", obj.get('represents')):null; let player, follow, oldColor; var controller = (character)?character.get('controlledby'):''; if(controller !== '' &amp;&amp; controller !== 'all' ){ player=getObj("player", controller); follow=true; }else{ // set player to GM (eventually), and if not "all" set follow to false player=Teleporter.DEFAULTPLAYER; follow=(controller === 'all')?true:false; } // use override for send ping if((!Teleporter.AUTOPINGMOVE &amp;&amp; follow) || !follow){ follow=false; }else{ oldColor=player.get("color"); player.set({color:"transparent"}); } obj.set({ layer: 'gmlayer' }); _.delay(()=&gt;{ obj.set({ left: NewX, top: NewY, lastmove: '' }); _.delay(()=&gt;{ obj.set({ layer: currLayer }); // Ping only the specified player to this location - (get controlling player, have to account for multiple/"all") if(follow){sendPing(NewX, NewY, obj.get('pageid'), player.get("_id"), true, player.get("_id"));} if(LocFX !== '' &amp;&amp; Teleporter.AUTOPLAYFX ){ if(_.isString(LocFX)){ spawnFx(NewX, NewY, LocFX, obj.get('pageid')); }else{ spawnFxWithDefinition(NewX, NewY, LocFX, obj.get('pageid')); } } _.delay(()=&gt;{ // Longer delay to re-set the player color to allow the ping to finish. if(follow){player.set({color: oldColor});} },1000); },500); },100); }); on("chat:message", function(msg) { "use strict"; if (msg.content.indexOf("!AUTOTELEPORTER") !== -1 &amp;&amp; playerIsGM(msg.playerid)) { if ( Teleporter.AUTOTELEPORTER === true) { sendChat("System", "/w gm Autoteleporting Disabled."); Teleporter.AUTOTELEPORTER = false; } else { sendChat("System", "/w gm Autoteleporting Enabled."); Teleporter.AUTOTELEPORTER = true; } } if (msg.content.indexOf("!AUTOPINGMOVE") !== -1 &amp;&amp; playerIsGM(msg.playerid)) { if ( Teleporter.AUTOPINGMOVE === true) { sendChat("System", "/w gm Ping-move Disabled."); Teleporter.AUTOPINGMOVE = false; } else { sendChat("System", "/w gm Ping-move Enabled."); Teleporter.AUTOPINGMOVE = true; } } if (msg.content.indexOf("!AUTOPLAYFX") !== -1 &amp;&amp; playerIsGM(msg.playerid)) { if ( Teleporter.AUTOPLAYFX === true) { sendChat("System", "/w gm Ping-move Disabled."); Teleporter.AUTOPLAYFX = false; } else { sendChat("System", "/w gm Ping-move Enabled."); Teleporter.AUTOPLAYFX = true; } } }); });