-
-
Save VoltCruelerz/7962330ef9b8aaf516a69cb4acbd8d6d to your computer and use it in GitHub Desktop.
An updated MapTeleporters script built for CashMaster.
This file contains hidden or 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/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