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

[Script Request] Keep player tokens’ status markers in sync

Hello all, I’m running a battle in a castle with multiple levels on different maps, and the PCs go from one level (map) to another as needed. When I apply a token status marker to a PC token, and then we later move to another map, I would like the new status markers to be reflected on the copy of that PC’s token on the map we are now on. Ideally this would be true across all maps. If the bard finishes a fight in the mountains with 3 levels of exhaustion, and then the party teleports to their home base, I’d like the token there to reflect that. If we then end for the night and I move the players back to the landing page, the bard’s token on that page should show their current exhaustion status as well. I was thinking that it would be easiest to tie token markers to character sheet attributes, and perhaps only update the tokens on a page when we arrive at a new page, but I’ll leave best implementation details to the pros (*cough*TheAaron*cough*). :-) I saw this post and script with a search, but it is very old, and I’m not sure if it would still work. If no one has a better answer, I’ll try that when I get a chance. Note that ideally I need this to work even when applying status markers via the API, such as when using TokenMod.  Maybe check status markers when leaving the page, if that is possible? Thanks for your time and assistance!
1607015853
The Aaron
Roll20 Production Team
API Scripter
If you could restrict setting the markers to purely the API, you could do this with TokenMod: !token-mod --set statusmarkers|+blue --ids @{selected| character_id } Keeping them in sync with manual changes and API changes will be doable with a script, but probably would need to poll unless all the scripts in question provide observation.  What are all the methods you foresee setting status markers with?
1607017607
David M.
Pro
API Scripter
Couldn't you also just copy the tokens prior to changing pages, then paste them after changing the page? This should preserve everything (including condition markers) currently on the tokens. I.e. don't "pre-load" the default PC tokens on all of your maps.  
1607017817
The Aaron
Roll20 Production Team
API Scripter
You could, but that's pretty manual, especially if you're using a teleport script and have tokens there already.
1607019846

Edited 1607019875
Joe
Pro
David: Absolutely, that would work. But as The Aaron commented, it would be a pretty manual process, especially if the PCs are spread all over the map, and automating that kind of thing is a big part of why my group and I are paying for a Pro membership. :-) Aaron: We set conditions manually and with TokenMod in approximately equal measure, I think. I’ve provided the players with a macro to set conditions using the API, but I’m not sure how much they bother using it vs. just doing is manually. ;-) There are a couple other scripts that set markers, such as Concentration and PaladinAura, but I'm ok if those types of changes get missed since they don't happen often. Using TokenMod to update every page in the game is nifty, but not great for speed of execution, given the number of maps I keep around. :-) What triggers are available to the API for this kind of thing?  If you can detect when the player ribbon leaves a page and enters a page, that could do it; just read when leaving and write when entering. Polling to sync every couple seconds seems like it would work, downside is adding cycles to the API server, and introducing the unlikely possibility of switching pages before it gets updated. Another possibility that would still be useful would be a manual trigger to save the markers on the current page (ideally the one I’m looking at, rather than the one with the player ribbon, for added flexibility), and a manual trigger to write the saved markers on the current page (i.e. after I switch pages). Thoughts?
1607020525
David M.
Pro
API Scripter
Yep, very true! I didn't say it was an ideal solution ;) Now that you mention it, copying condition markers might be a useful request for page-to-page teleport cases with Pat's new Teleport script, since he's still actively developing it. 
1607021019
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
There used to be a script called TokenSync , which could do exactly what you are asking. the wiki page is here , the original thread here , but the git repository is returning a 404. There is a copy of some state of the code in this post , but it would be wise to test in a separate game.
1607021251
The Aaron
Roll20 Production Team
API Scripter
I have a copy of the code.  I don't remember anything about it, but see if it will work for you: var TokenSync = TokenSync || (function() { 'use strict'; var version = '1.6', lastUpdate = 1524849610, syncable = [ "imgsrc", "bar1_link", "bar2_link", "bar3_link", "width", "height", "rotation", "layer", "isdrawing", "flipv", "fliph", "name", "aura1_radius", "aura2_radius", "aura1_color", "aura2_color", "aura1_square", "aura2_square", "tint_color", "statusmarkers", "showname", "showplayers_name", "showplayers_bar1", "showplayers_bar2", "showplayers_bar3", "showplayers_aura1", "showplayers_aura2", "light_radius", "light_dimradius", "light_otherplayers", "light_hassight", "light_angle", "light_losangle", "light_multiplier" ], propsListened, syncProperty = function(updatedToken, properties) { var propList = _.intersection( (properties && properties.split && properties.split("|")) || syncable, state.TokenSync.syncList[updatedToken.get("represents")] || [] ), update = _.reduce(propList,function(m,p){ m[p] = updatedToken.get(p); return m; },{}); if (!Object.keys(update).length) return; _.each(findObjs({ _subtype: "token", represents: updatedToken.get("represents")}),function(t){ t.set(update); }); }, registerListener = function(prop) { // Keep track of what event handlers we've registered; since we can't unregister (AFAIK), we don't want to acidentally register the same event multiple times if (!_.contains(propsListened,prop)) { propsListened[propsListened.length] = prop; on("change:token:"+prop, function(obj) { syncProperty(obj,prop); }); } }, usage = function(who) { sendChat("TokenSync", "Usage: !tokensync [--add property(|properties)] [--remove property(|properties)] [--removeall] [--forcesync (property(|properties))]"); }, add = function(charID,properties,silent) { var propsRequested = properties.split("|"); var propsRejected = _.difference(propsRequested, syncable); var propList = _.intersection(propsRequested, syncable); var propsAlready = _.intersection(propList,(state.TokenSync.syncList[charID]||[])); propList = _.difference(propList,(state.TokenSync.syncList[charID]||[])); if (!silent) { if (propsRejected.length) sendChat("TokenSync","**Invalid sync propert"+(1===propsRejected.length ?'y':'ies')+":** " +propsRejected.join()); if (propsAlready.length) sendChat("TokenSync","**Already synchronizing:** " + propsAlready.join()); if (propList.length) sendChat("TokenSync","**Now synchronizing:** " +propList.join()); } state.TokenSync.syncList[charID] = _.union( (state.TokenSync.syncList[charID]||[]), propList); _.each(propList,registerListener); }, remove = function(charID,properties,silent) { if (properties === "") { delete state.TokenSync.syncList[charID]; if (!silent) sendChat("TokenSync","Removed all sync properties!"); return; } var propList = _.intersection(properties.split("|"),syncable); state.TokenSync.syncList[charID] = _.difference((state.TokenSync.syncList[charID]||[]),propList); if (state.TokenSync.syncList[charID].length === 0) delete state.TokenSync.syncList[charID]; if (!silent) sendChat("TokenSync","No longer synchronizing: " + propList.join()); }, HandleInput = function(msg) { var selected, tok, params, i; var cmd = "!tokensync" if (msg.type === "api" && msg.content.indexOf(cmd) !== -1 ) { selected = msg.selected; params = msg.content.split(" "); if (params.length === 1) { usage(msg.playerid); return; } //loop through selected tokens _.each(selected, function(obj) { tok = getObj("graphic", obj._id); for (i = 1; i < params.length; i++) { switch(params[i].trim()) { case "--add": // Make sure it isn't last in the params list, and that it isn't another option if ((i < (params.length - 1)) && params[i+1].indexOf("--") === -1) { add(tok.get("represents"),params[i+1]); i++; // Jump forward in the list, since we know the next param isn't an option } else sendChat("TokenSync", "**ERROR:** token property not specified"); break; case "--remove": // Make sure it isn't last in the params list, and that it isn't another option if ((i < (params.length - 1)) && params[i+1].indexOf("--") === -1) { remove(tok.get("represents"),params[i+1]); i++; // Jump forward in the list, since we know the next param isn't an option } else sendChat("TokenSync", "**ERROR:** token property not specified"); break; case "--removeall": remove(tok.get("represents"),""); break; case "--forcesync": syncProperty(tok); break; default: break; } } }); } }, updateSchema = function() { var oldList = _.clone(state.TokenSync.syncList); state.TokenSync.syncList = {}; state.TokenSync.schema = 2.0; // Old schema was property: character list, new schema is character: property list _.each(oldList,function(charList,prop) { _.each(charList,function(charID) { add(charID,prop,true); }); }); }, checkInstall = function() { propsListened = []; if (!state.TokenSync) state.TokenSync = { module: "TokenSync", syncList: {}, schema: 2.0 }; else if (_.isUndefined(state.TokenSync.schema)) updateSchema(); else _.each(_.uniq(_.flatten(_.values(state.TokenSync.syncList))),registerListener); if (state.TokenSync.propsListened) delete state.TokenSync.propsListened; log('-=> TokenSync v'+version+' <=- ['+(new Date(lastUpdate*1000))+']'); }, syncNewToken = function(newTok) { var done = false, charID = newTok.get('represents'); filterObjs(function(o){ if( !done && 'graphic'===o.get('type') && 'token'===o.get('subtype') && o.get('represents') === charID && o.id !== newTok.id ) { syncProperty(o); done = true; } return false; }); }, removeDeletedCharacter = function(oldChar) { remove(oldChar.get("_id"),"",true); }, RegisterEventHandlers = function() { on('chat:message', HandleInput); on('add:token', syncNewToken); on('destroy:character', removeDeletedCharacter); on('change:token:represents', syncNewToken); }; return { CheckInstall: checkInstall, RegisterEventHandlers: RegisterEventHandlers, syncProperty: syncProperty }; }()); on("ready",function(){ 'use strict'; TokenSync.CheckInstall(); TokenSync.RegisterEventHandlers(); });
Thanks, all! I’m not sure when I’ll have a chance to try it out, but I’ll post when I do. If you test or have other thoughts before then, feel free to let me know! :-)
1607032176
Pat
Pro
API Scripter
Well, if this is requested I can see about doing this on teleport to compare teleporting token to target token - if it just happened on teleport it might be a lot less chatty... make it a toggle so it can be turned on and off... version 1.3 along with crowding algorithm?