// Github: https://github.com/shdwjk/Roll20API/blob/master/APIHeartBeat/APIHeartBeat.js // By: The Aaron, Arcane Scriptomancer // Contact: https://app.roll20.net/users/104025/the-aaron var APIHeartBeat = APIHeartBeat || (function() { 'use strict'; var version = '0.4.2', lastUpdate = 1490362946, schemaVersion = 0.2, beatInterval = false, beatPeriod = 200, devScaleFactor = 5, beatCycle = 3000, scaleColorRange = function(scale, color1, color2) { return _.chain( _.zip( _.rest(color1.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/)), _.rest(color2.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/)) ) ) .map(function(d){ var b1 = parseInt(d[0],16), b2 = parseInt(d[1],16); return Math.min(255,Math.max(0,((b2-b1)*scale+b1).toFixed(0))).toString(16); }) .reduce(function(memo,d){ return memo+(1===d.length ? '0' : '')+d; },'#') .value(); }, animateHeartBeat = function() { var cycle = beatCycle * (state.APIHeartBeat.devMode ? 1 : devScaleFactor), x = ((Date.now()%cycle)/cycle)*Math.PI*2, scale = (Math.sin(x)+1)/2; _.chain(state.APIHeartBeat.heartBeaters) .map(function(d){ return { player: getObj('player',d.pid), color1: d.color1, color2: d.color2 }; }) .reject(function(d){ return !d.player || !d.player.get('online'); }) .each(function(d){ d.player.set({ color: scaleColorRange(scale,d.color1,d.color2) }); }); }, startStopBeat = function() { var userOnline=_.chain( _.keys(state.APIHeartBeat.heartBeaters) ) .map(function(pid){ return getObj('player',pid); }) .reject(_.isUndefined) .map(function(p){ return p.get('online'); }) .reduce(function(memo,os){ return memo||os; },false) .value(), period=beatPeriod*( state.APIHeartBeat.devMode ? 1 : devScaleFactor ); if(!beatInterval && _.keys(state.APIHeartBeat.heartBeaters).length && userOnline) { beatInterval = setInterval(animateHeartBeat,period); } else if(beatInterval && (!_.keys(state.APIHeartBeat.heartBeaters).length || !userOnline) ) { clearInterval(beatInterval); beatInterval=false; } }, 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 ''; }, showHelp = function(who) { sendChat('', '/w '+who+' ' +'
' +'
' +'APIHeartBeat v'+version +'
' +'
' +'

APIHeartBeat provides visual feedback that the API is running by changing a user'+ch("'")+'s color periodically.

' +'
' +'Commands' +'
' +'!api-heartbeat '+ch('<')+'--help|--off|--dev'+ch('>')+' '+ch('[')+ch('<')+'color'+ch('>')+ch(']')+' '+ch('[')+ch('<')+'color'+ch('>')+ch(']')+'' +'
' +'

This command allows you to turn off and on the monitor, as well as configure it.

' +'
    ' +'
  • ' +''+ch('<')+'--help'+ch('>')+' '+ch('-')+' Displays this help.' +'
  • ' +'
  • ' +''+ch('<')+'--off'+ch('>')+' '+ch('-')+' Turns off the heartbeat for the current player.' +'
  • ' +'
  • ' +''+ch('<')+'--dev'+ch('>')+' '+ch('-')+' Activates development mode. (Warning: This mode updates much more often and could contribute to performance issues, despite being great for script development.)' +'
  • ' +'
  • ' +''+ch('<')+'color'+ch('>')+' '+ch('-')+' The script alternates between two colors. If you specify 2 colors, it will use those. If you specify 1 color, it will use that and your configured color. If you specify no colors, it will go between your configured color and black or red based on brightness.' +'
  • ' +'
' +'
' +'
' +'
' ); }, counterColor = function(color) { if(parseInt( _.first( _.rest( (_.isString(color) ? color : '').match(/^#([0-9a-fA-F]{2})/) || [] ) ) || '00', 16) > 127 ){ return '#000000'; } return '#ff0000'; }, handleInput = function(msg) { var args, errors, player, who, color; if (msg.type !== "api" && !playerIsGM(msg.playerid)) { return; } player = getObj('player',msg.playerid); who = (player||{get:()=>'API'}).get('_displayname').split(' ')[0]; args = msg.content.split(/\s+/); switch(args.shift()) { case '!api-heartbeat': if(_.contains(args,'--help')) { showHelp(who); return; } if ( _.contains(args,'--off') ) { // turn off if(state.APIHeartBeat.heartBeaters[msg.playerid]) { color = state.APIHeartBeat.heartBeaters[msg.playerid].origColor; delete state.APIHeartBeat.heartBeaters[msg.playerid]; startStopBeat(); player.set({color: color}); } sendChat('APIHeartBeat', '/w '+who+' Off for '+player.get('displayname')+'.'); } else { if ( _.contains(args,'--dev') ) { state.APIHeartBeat.devMode = !state.APIHeartBeat.devMode; clearInterval(beatInterval); beatInterval=false; sendChat('APIHeartBeat', '/w '+who+' Dev Mode is now '+(state.APIHeartBeat.devMode ? 'ON' : 'OFF')+'.'); args = _.chain(args).without('--dev').first(2).value(); if( ! args.length ) { startStopBeat(); return; } } errors=_.reduce(args, function(memo,a){ if( ! a.match(/^(?:#?[0-9a-fA-F]{6})$/) ) { memo.push("Invalid color: "+a); } return memo; },[]); if(errors.length) { sendChat('APIHeartBeat', '/w '+who+' Errors: '+errors.join(' ')); } else { switch(args.length) { case 2: state.APIHeartBeat.heartBeaters[msg.playerid]= { pid: msg.playerid, origColor: player.get('color'), color1: args[0], color2: args[1] }; break; case 1: state.APIHeartBeat.heartBeaters[msg.playerid]= { pid: msg.playerid, origColor: player.get('color'), color1: player.get('color'), color2: args[0] }; break; default: state.APIHeartBeat.heartBeaters[msg.playerid]= { pid: msg.playerid, origColor: player.get('color'), color1: player.get('color'), color2: counterColor(player.get('color')) }; } sendChat('APIHeartBeat', '/w '+who+' Configured on for '+player.get('displayname')+'.'); } startStopBeat(); } break; } }, checkInstall = function() { log('-=> APIHeartBeat v'+version+' <=- ['+(new Date(lastUpdate*1000))+']'); if( ! _.has(state,'APIHeartBeat') || state.APIHeartBeat.version !== schemaVersion) { log(' > Updating Schema to v'+schemaVersion+' <'); state.APIHeartBeat = { version: schemaVersion, devMode: false, heartBeaters: {} }; } startStopBeat(); }, registerEventHandlers = function() { on('chat:message', handleInput); on('change:player:_online', startStopBeat); }; return { CheckInstall: checkInstall, RegisterEventHandlers: registerEventHandlers }; }()); on('ready',function() { 'use strict'; APIHeartBeat.CheckInstall(); APIHeartBeat.RegisterEventHandlers(); });