Skip to content

Instantly share code, notes, and snippets.

@VoltCruelerz
Forked from finalfrog/MapTeleporters.js
Last active March 2, 2019 02:06
Show Gist options
  • Save VoltCruelerz/7962330ef9b8aaf516a69cb4acbd8d6d to your computer and use it in GitHub Desktop.
Save VoltCruelerz/7962330ef9b8aaf516a69cb4acbd8d6d to your computer and use it in GitHub Desktop.
An updated MapTeleporters script built for CashMaster.
/*
= GITHUB =
https://gist.github.com/VoltCruelerz/7962330ef9b8aaf516a69cb4acbd8d6d
= INSPIRATION =
- MapChange by TheWhiteWolves (https://github.com/TheWhiteWolves/MapChange.git)
-Teleporter Without Movement Tracker by DarokinB (https://gist.github.com/DarokinB/5806230)
= AUTHORS =
- FinalFrog:
https://app.roll20.net/users/585874/finalfrog
- Volt Cruelerz (Michael Greene)
https://app.roll20.net/users/1583758/michael-g
= SETUP =
0. Have CashMaster installed and have each player select their primary token and run this command:
!cm -sc Yes
1. Create a token named "Teleporter_[group id]_[node id]_[target node options]"
- Group Id: an alphanumeric string such as "01" or "Group1". NO SPACES!
- Node Id: an alphanumeric string such as "01" or "Node1". NO SPACES!
- Target Node Options: a comma-delimited string of node IDs such as "01" or "01,02,Node3"
2. Move the token to the GM Layer
3. Create a corresponding portal that has a node ID that was in the previous target node list.
- One-way portals can be made by not declaring a target list on the destination such as...
SOURCE: "Teleporter_OneWay01_01_02"
DESTINATION: "Teleporter_OneWay01_02"
- Two-way portals can be made by simply having the destination have the source as a target.
SOURCE: "Teleporter_TwoWayGroup_Node1_Node2"
DESTINATION: "Teleporter_TwoWayGroup_Node2_Node1"
4. If these portals are not on the same page, make sure there is another player token on the
destination page.
= USE =
GENERAL: Have the user move their token into the field of the portal.
PAGES: If they destination portal is on the same page, the token will just be moved.
If a different page, the user will be moved to the other page and will gain control of
the token there. Their old token will then be moved to the GM layer.
RANDOM NODES: If you have a node with a target list, one of them will be chosen at random
to be the target node each time a token lands on the portal.
CONVERGENCE: It is possible to have multiple nodes in a group point to the same destination
node.
= IMPROVEMENTS =
While FinalFrog's initial coding was useful, I've added a few more features...
- CM Integration: while a proper full solution is liable to occur eventually, until then,
I've made the script run on CM's default character system (see Step 0 above). This allows
you to have multiple players controlling multiple tokens. Only when your primary
character token is moved to a different page will you follow. As CM allows you to rebind
your primary token, you can always adapt on the fly.
- Overlap Coverage: the old code had a bug where when player A went through a portal to another
page, their now-GM-layer token would jam up the collision detection logic and block future
players from going through the portal through the same square.
- Regex: The original version did not support human-readable names.
*/
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);
let name = o.get('name');
if( ol <= cx && cx <= or
&& ot <= cy && cy <= ob
&& name)
{
if (name.startsWith('Teleporter_')){
m.push(o);
}
}
return m;
},[])
.value();
}
return [];
};
const getDefaultCharNameFromPlayer = (playerid) => {
const defaultName = state.CashMaster.DefaultCharacterNames[playerid];
if (!defaultName) {
return null;
}
return defaultName;
};
const getCharByAny = (nameOrId) => {
let character = null;
// Try to directly load the character ID
character = getObj('character', nameOrId);
if (character) {
return character;
}
// Try to load indirectly from the token ID
const token = getObj('graphic', nameOrId);
if (token) {
character = getObj('character', token.get('represents'));
if (character) {
return character;
}
}
// Try loading through char name
const list = findObjs({
_type: 'character',
name: nameOrId,
});
if (list.length === 1) {
return list[0];
}
// Default to null
return null;
};
var handleGraphicChange = function(obj) {
if (obj.get("layer") !== "objects") {
return; // Only teleport tokens on the object layer
}
// Get owner
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 sourceTeleporterName = sourceTeleporter.get("name");
log('Source Teleporter Name: ' + sourceTeleporterName);
// Teleporter_<groupId>_<myId>_<targetIds>
const regex = new RegExp(/Teleporter\_([a-z0-9]+)\_([a-z0-9]+)(_([a-z0-9]+(,[a-z0-9])*))?/,'i');
let values = regex.exec(sourceTeleporterName);
log('Values: ' + values);
if (!values) return;
if (values.length < 5) return;
let groupId = values[1];
let myId = values[2];
if (!values[4]) return;
let targetIds = values[4].split(',');
// Randomly select one of the targets
let targetId = targetIds[Math.floor(Math.random()*targetIds.length)];
let targetName = `Teleporter_${groupId}_${targetId}`;
log('Target Teleporter Name: ' + targetName);
let destTeleporters = filterObjs((obj) => {
if(obj.get('layer') === 'gmlayer'
&& obj.get('_type') === 'graphic'
&& obj.get('name').startsWith(targetName)) {
return obj
}
});
if (destTeleporters.length == 0) {
// No destination teleporters found
log('No destination teleporter discovered.');
return;
} else {
// Handle first matching destination teleporter
var destTeleporter = destTeleporters[0];
log('Dest Teleporter: ' + destTeleporter.get('name'));
// 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.
log('Dest Page = Source Page. Relocating 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
log('No token for character found on destination page. Aborting.');
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");
// iterate over controller list.
log('Controllers: ' + controllers);
var controllerArray = controllers.split(',');
log('Character: ' + character.get('name'));
controllerArray.forEach((controller) => {
log(' Try Controller: ' + controller);
let defaultCharName = getDefaultCharNameFromPlayer(controller);
let defaultChar = getCharByAny(defaultCharName);
if (!defaultChar) return;
log(' Char Comparison: ' + defaultChar.get('name') + ' vs ' + character.get('name'));
if (character === defaultChar) {
log(' Got default controller');
// Teleport player to the page with the destination
// teleporter
teleport(controller, 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();
log('[Teleporter - CM Edition]');
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment