Skip to content

Instantly share code, notes, and snippets.

@Kurohyou
Last active January 25, 2017 02:11
Show Gist options
  • Save Kurohyou/5c6a1d64bbd55f132c66b0a712522a60 to your computer and use it in GitHub Desktop.
Save Kurohyou/5c6a1d64bbd55f132c66b0a712522a60 to your computer and use it in GitHub Desktop.
/*
PAGENAVIGATOR script:
Author: Scott C.
Contact: https://app.roll20.net/users/459831/scott-c
Thanks to: The Aaron, Arcane Scriptomancer and Stephen for their help with the bulk of this script.
Script goal: to simplify moving between maps in Roll20.
*/
var PAGENAVIGATOR= PAGENAVIGATOR|| (function(){
'use strict';
var version = '0.1.03',
lastUpdate = 1463518426,
schemaVersion = 1.03,
defaults = {
css: {
button: {
'border': '1px solid #cccccc',
'border-radius': '1em',
'background-color': '#006dcc',
'margin': '0 .1em',
'font-weight': 'bold',
'padding': '.1em 1em',
'color': 'white'
}
}
},
templates = {},
ch = function (c) {
var entities = {
'<' : 'lt',
'>' : 'gt',
"'" : '#39',
'@' : '#64',
'{' : '#123',
'|' : '#124',
'}' : '#125',
'[' : '#91',
']' : '#93',
'"' : 'quot',
'-' : 'mdash',
' ' : 'nbsp'
};
if(_.has(entities,c) ){
return ('&'+entities[c]+';');
}
return '';
},
buildTemplates = function() {
templates.cssProperty =_.template(
'<%=name %>: <%=value %>;'
);
templates.style = _.template(
'style="<%='+
'_.map(css,function(v,k) {'+
'return templates.cssProperty({'+
'defaults: defaults,'+
'templates: templates,'+
'name:k,'+
'value:v'+
'});'+
'}).join("")'+
' %>"'
);
templates.button = _.template(
'<a <%= templates.style({'+
'defaults: defaults,'+
'templates: templates,'+
'css: _.defaults(css,defaults.css.button)'+
'}) %> href="<%= command %>"><%= label||"Button" %></a>'
);
},
makeButton = function(command, label, backgroundColor, color){
return templates.button({
command: command,
label: label,
templates: templates,
defaults: defaults,
css: {
color: color,
'background-color': backgroundColor
}
});
},
checkInstall = function() {
log('-=> Page Navigator v'+version+' <=- ['+(new Date(lastUpdate*1000))+']');
if( ! _.has(state,'PAGENAVIGATOR') || state.PAGENAVIGATOR.version !== schemaVersion) {
log(' > Updating Schema to v'+schemaVersion+' <');
switch(state.PAGENAVIGATOR&& state.PAGENAVIGATOR.version) {
case 'UpdateSchemaVersion':
state.PAGENAVIGATOR.version = schemaVersion;
break;
default:
state.PAGENAVIGATOR= {
version: schemaVersion,
};
break;
}
}
buildTemplates();
},
movePlayer = function(destination, playerid){
let pp = Campaign().get('playerspecificpages');
pp = (_.isObject(pp) ? pp : {} );
var iteration = 0;
sendChat('Page Navigator', 'Some players are being split from the party.');
_.each(playerid, function(){
pp[playerid[iteration]] = destination;
iteration++;
});
Campaign().set({playerspecificpages: false});
Campaign().set({playerspecificpages: pp});
},
moveCurrentParty = function(destination){
sendChat('Page Navigator', 'The current party is being moved to a new map.');
Campaign().set({playerpageid: destination});
},
moveAllPlayers = function(destination){
sendChat('Page Navigator', 'All players are being moved to a new map');
Campaign().set({playerpageid: destination, playerspecificpages: false});
},
getDestinationCollisions = function(token) {
var pageId,
destinations,
collisions,
tokenControl,
controlList = '',
iteration = 0,
character,
msg = '',
destination,
overlapCollision,
lastCollision;
pageId = token.get('_pageid');
destinations = getDestinations(pageId);
collisions = TokenCollisions.getCollisions(token, destinations);
lastCollision = _.last(collisions);
if(token.get('represents')){
character = getObj('character', token.get('represents'));
tokenControl = character.get('controlledby').split(",");
}else{
tokenControl = token.get('controlledby').split(",");
}
if(lastCollision && token.get('name') && tokenControl){
overlapCollision = TokenCollisions.isOverlapping(token, lastCollision, false);
if(overlapCollision){
destination = findObjs({
type: 'page',
name: lastCollision.get('name')
});
if(lastCollision && _.contains(tokenControl, 'all') && destination[0]){
msg += makeButton(
'!nav whole ' + destination[0].id,
'Whole Party', '#fff8dc', '#191970'
);
msg += makeButton(
'!nav current ' + destination[0].id,
'Current Party', '#fff8dc', '#191970'
);
} else if(lastCollision && !_.contains(tokenControl, 'all') && destination[0]){
_.each(tokenControl, function(){
controlList += tokenControl[iteration] + ' ';
iteration++;
})
iteration = 0;
msg += makeButton(
'!nav whole ' + destination[0].id,
'Whole Party', '#fff8dc', '#191970'
);
msg += makeButton(
'!nav current ' + destination[0].id,
'Current Party', '#fff8dc', '#191970'
);
msg += makeButton(
'!nav player ' + destination[0].id + ' ' + controlList,
'Controlling Player(s)', '#fff8dc', '#191970'
);
}
if(destination[0]){
if(tokenControl && destination[0].get('name').indexOf('"players"')>=0){
sendChat('Page Navigation', '/w ' + token.get('name') +'<div style="border: 1px solid black; background-color: white; padding: 3px 3px;">'
+'<div style="font-weight: bold; border-bottom: 1px solid black;font-size: 110%;">'
+ token.get('name') + ' would like to move to ' + destination[0].get('name') + '. Who is traveling with them? </div>'
+'<p>' + msg + '<p>' +
'</div>'
);
}
sendChat('Page Navigation', '/w gm <div style="border: 1px solid black; background-color: white; padding: 3px 3px;">'
+'<div style="font-weight: bold; border-bottom: 1px solid black;font-size: 110%;">'
+ token.get('name') + ' would like to move to ' + destination[0].get('name') + '. Who is traveling with them? </div>'
+'<p>' + msg + '<p>' +
'</div>'
);
}
}
}
},
getDestinations = function(pageId){
return findObjs({
pageid: pageId,
type: 'graphic',
layer: /*'map' ||*/ 'gmlayer',
statusmarkers: 'flying-flag'
});
},
/*finds all the nonarchived pages in the campaign
*/
getPages = function(access) {
var pages = findObjs({
type: 'page',
archived: false
}),
fpages;
if(access === 'player'){
fpages = _.filter(pages, function(p){
return (p.get('name').indexOf('"players"')>=0);
})
return fpages;
}else{
return pages;
}
},
/*Takes an array of tokens and gets who they are controlled by. Creates API buttons to send the indicated player(s) to specific maps and outputs thos buttons
to Chat.
*/
pagesPlayerDialog = function(whoToMove, access, speaker) {
var pages = getPages(access),
msg = '',
tokensControl,
allCount = 0,
iteration = 0,
tokensControl = _.map(whoToMove, function(obj){
return obj[0].get('controlledby').split(',');
}),
players,
character = _.map(whoToMove, function(obj){
log('obj represents:');
log(obj[0].get('represents'));
return findObjs({
type: 'character',
id: obj[0].get('represents')
});
}),
idList = '',
names,
nameList = '';
if(character){
tokensControl = _.map(character, function(obj){
return obj[0].get('controlledby').split(',');
});
log('each tokensControl:');
log(tokensControl);
}else{
tokensControl = _.map(whoToMove, function(obj){
return obj[0].get('controlledby').split(',');
});
}
if(pages.length === 0 || !pages){
sendChat('Page Navigation', '/w ' + speaker + ' <div style="border: 1px solid black; background-color: white; padding: 3px 3px;">'
+'<div style="font-weight: bold; border-bottom: 1px solid black;font-size: 100%;">'
+' There are no valid destinations. This is either because there are no pages in the campaign, or because the GM has not specified any as player accessible via adding <b>"players"</b> to the page name.'
);
}
_.each(tokensControl, function(){
if(_.contains(tokensControl[iteration], 'all') && allCount === 0){
pagesPartyDialog('all');
allCount++;
}
iteration++;
});
iteration = 0;
if(allCount===0){
players = _.map(tokensControl, function(id){
return findObjs({
type: 'player',
id: id[0]
});
});
names = _.map(players, function(obj){
return obj[0].get('_displayname');
});
_.each(names, function(){
nameList += names[iteration] + ' ';
iteration++;
})
iteration = 0;
_.each(tokensControl, function(){
idList += tokensControl[iteration] + ' ';
iteration++;
})
iteration = 0;
_.each(pages, function(){
msg += makeButton(
'!nav player ' + pages[iteration].id + ' ' + idList,
pages[iteration].get('name'), '#fff8dc', '#191970'
);
iteration++;
});
sendChat('Page Navigation', '/w ' + speaker + ' <div style="border: 1px solid black; background-color: white; padding: 3px 3px;">'
+'<div style="font-weight: bold; border-bottom: 1px solid black;font-size: 110%;">'
+ 'As ' + speaker + ' you can move ' + nameList + ' to:' + '</div>'
+'<p>' + msg + '<p>' +
'</div>'
);
};
},
/* Makes and outputs buttons to send the whole party, or just the current party to the indicated page*/
pagesPartyDialog = function(whoToMove, access, speaker) {
var pages = getPages(access),
msg = '',
iteration = 0;
log(pages.length);
log(pages);
if(pages.length >= 1 && whoToMove === 'whole' || whoToMove === 'current') {
log('make button if entered');
_.each(pages, function(){
msg += makeButton(
'!nav ' + whoToMove + ' ' + pages[iteration].id,
pages[iteration].get('name'), '#fff8dc', '#191970'
);
iteration++;
});
sendChat('Page Navigation', '/w ' + speaker + ' <div style="border: 1px solid black; background-color: white; padding: 3px 3px;">'
+'<div style="font-weight: bold; border-bottom: 1px solid black;font-size: 110%;">'
+ 'As ' + speaker + ' you can set the ' + whoToMove + ' party' + ch("'") + 's page to:</div>'
+'<p>' + msg + '<p>' +
'</div>'
);
return;
}else if(pages.length === 0){
sendChat('Page Navigation', '/w ' + speaker + ' <div style="border: 1px solid black; background-color: white; padding: 3px 3px;">'
+'<div style="font-weight: bold; border-bottom: 1px solid black;font-size: 100%;">'
+' There are no valid destinations. This is either because there are no pages in the campaign, or because the GM has not specified any as player accessible via adding <b>"players"</b> to the page name.'
);
}
},
showHelp = function(who) {
sendChat('','/w "'+who+'" '
+'<div style="border: 1px solid black; background-color: white; padding: 3px 3px;">'
+'<div style="font-weight: bold; border-bottom: 1px solid black;font-size: 130%;">'
+'Page Navigator v'+version
+'</div>'
+'<div style="padding-left:10px;margin-bottom:3px;">'
+'<p>Page Navigator allows you to more easily move your players from page to page.</p>'
+'</div>'
+'<b>Commands</b>'
+'<div style="padding-left:10px;">'
+'<b><span style="font-family: serif;">!nav</span></b>'
+'<div style="padding-left: 10px;padding-right:20px">'
+'<p>The api command to trigger the script.</p>'
+'<ul>'
+'<li style="border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;">'
+'<b><span style="font-family: serif;">help</span></b> '+ch('-')+' Shows the Help screen'
+'</li> '
+'<li style="border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;">'
+'<b><span style="font-family: serif;">pages</span></b> '+ch('-')+' Brings up a dialog of all non-archived pages in the Campaign for specifying a page to send players to.'
+'<br>The exact behavior is dependent on the next argument, which can be <b>"whole"</b>, <b>"current"</b>, or <b>"player"</b>. <br>If "player" is the next argument, it needs to be followed by a list of token_ids passed via '+ch('@')+'{target|target1|token_id} ... '+ch('@')+'{target|targetn|token_id} or '+ch('@')+'{selected|token_id (only the id(s) of one token can be passed this way)}.'
+'</li> '
+'</div>'
+'<b>Buttons</b>'
+'<ul><li style="border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;">'
+'<b><span style="font-family: serif;">Whole Party</span></b> '+ch('-')+' Moves the player ribbon to the destination page and moves all players to the player ribbon.'
+'</li> '
+'<li style="border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;">'
+'<b><span style="font-family: serif;">Current Party</span></b> '+ch('-')+' Moves the player ribbon to the destination page, but does not affect any players split from the party.'
+'</li> '
+'<li style="border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;">'
+'<b><span style="font-family: serif;">Controlling Player(s)</span></b> '+ch('-')+' Moves the player(s) who controls the triggering token to the destination page (splits the party).'
+'</li> '
+'</ul>'
+'<b>Access</b>'
+'<ul><li style="border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;">'
+'<b><span style="font-family: serif;">Setting Access</span></b> '+ch('-')+' Pages are GM access only by default. To set a page to Player access, simply add <b>"players"</b> anywhere in the page name.'
+'<ul>'
+'Example: <b>Test Page</b> - GM only page<br>vs.<br><b>Test Page "players"</b> - player visible page'
+'</ul>'
+'</li> '
+'<li style="border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;">'
+'<b><span style="font-family: serif;">GM Access</span></b> '+ch('-')+' If the !nav pages ... command is sent from a GM, all pages are populated as API buttons. The GM receives a whispered movement prompt for all token collision prompted navigation requests.'
+'</li> '
+'<li style="border-top: 1px solid #ccc;border-bottom: 1px solid #ccc;">'
+'<b><span style="font-family: serif;">Player Access</span></b> '+ch('-')+' If the !nav pages ... command is sent from a player, only those pages flagged as player pages are populated as API buttons. Players only receive a whispered movement prompt when their token collides with a player accessible destination.'
+'</li> '
+'</ul>'
+'</div>'
+'</div>'
);
},
handleInput = function(msg_orig){
var msg = _.clone(msg_orig),
args,
who,
page,
tokens,
tokenIds,
iteration = 0,
player,
speaker,
access;
if (msg.type !== "api") {
return;
}
if(playerIsGM(msg.playerid)) {
access = 'gm';
speaker = 'gm';
}else if(!playerIsGM(msg.playerid)){
access = 'player';
speaker = msg.who;
}
who=getObj('player',msg_orig.playerid).get('_displayname');
args = msg.content.split(/\s/);
switch(args[0]) {
case '!nav':
if(_.contains(args,'help')) {
showHelp(who);
return;
}
switch(args[1]){
case 'whole':
if(args[2]){
moveAllPlayers(args[2]);
}else{
sendChat('','/w "'+who+'" '+
'<div><b>No pageid associated with that destination:</div> </div>'
);
}
break;
case 'current':
if(args[2]){
moveCurrentParty(args[2]);
} else{
sendChat('','/w "'+who+'" '+
'<div><b>No pageid associated with that destination:</div> </div>'
);
}
break;
case 'player':
if(args[2] && _.rest(args, 3)){
movePlayer(args[2], _.rest(args, 3));
}else{
sendChat('','/w "'+who+'" '+
'<div><b>Player case Error: No pageid associated with that destination or no specific player named:</div> </div>'
);
}
break;
case 'pages':
if(args[2]){
if(args[2] === 'whole' || args[2] === 'current'){
pagesPartyDialog(args[2], access, speaker);
}else if(args[2] === 'player' && _.rest(args, 3)){
tokens = _.map(_.rest(args,3), function(id){
return findObjs({
type: 'graphic',
id: id
});
});
pagesPlayerDialog(tokens, access, speaker);
//sendChat('Page Navigator', '/w ' + who + ' The player case is not finished yet and does not do anything atm.');
//pagesPlayerDialog(tokens);
}
}else{
sendChat('Page Navigator', '/w ' + who + ' The previous !nav command did not have sufficient arguments.');
showHelp(who);
}
}
break;
}
},
RegisterEventHandlers = function() {
on('change:graphic', getDestinationCollisions);
on('chat:message', handleInput);
};
return {
CheckInstall: checkInstall,
RegisterEventHandlers: RegisterEventHandlers
};
}());
on("ready",function(){
'use strict';
PAGENAVIGATOR.CheckInstall();
PAGENAVIGATOR.RegisterEventHandlers();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment