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 .
×

The new D&D 2024 sheet is now available!

Create a free account

Token origin / start of turn location token?

I'm looking for a method of indicating on the map where a token is located at the start of the turn. Presently, I'm doing it manually: I have a token that's just a blue circle, and on each player's turn, I (the GM) move the blue circle to their current space, which allows them to see where their turn started, and thus makes it easier for them to determine the distance they can travel on their turn. Is there any way of automating this process? Currently, it gets a little tricky, as sometimes when I try to select the turn origin blue circle, I wind up selecting the actual creature's token (or vice versa).
1612806445
The Aaron
Roll20 Production Team
API Scripter
That's absolutely doable.  I've got some code laying about that does that, I'll try to find it tonight and put it in a usable snippet.  (or maybe someone else in the community can throw one together for fun?)
Thank you Aaron, that's fantastic! You're a machine, I'm using Tokenlock in my games right now and it's worked like a charm. Roll20 really ought to be paying you. :) I'm also a developer, though I'm not familiar with the Roll20 API, so the learning curve for me would be fairly steep.
1612809226
The Aaron
Roll20 Production Team
API Scripter
Ah, if you're interesting in learning, I can give you some links to get you started! Nice beginner videos:&nbsp; <a href="https://www.youtube.com/playlist?list=PLqhGF2nCu23kPeaUV_zIGwPRBZYUlcTGv" rel="nofollow">https://www.youtube.com/playlist?list=PLqhGF2nCu23kPeaUV_zIGwPRBZYUlcTGv</a> Some forum posts of interest: <a href="https://app.roll20.net/forum/post/6605115/namespaces-novice-seeks-help-exploring-the-revealing-module-pattern" rel="nofollow">https://app.roll20.net/forum/post/6605115/namespaces-novice-seeks-help-exploring-the-revealing-module-pattern</a> <a href="https://app.roll20.net/forum/post/6584105/creating-an-object-that-holds-specific-character-dot-id-and-character-name/?pagenum=1" rel="nofollow">https://app.roll20.net/forum/post/6584105/creating-an-object-that-holds-specific-character-dot-id-and-character-name/?pagenum=1</a> <a href="https://app.roll20.net/forum/post/6237754/slug%7D" rel="nofollow">https://app.roll20.net/forum/post/6237754/slug%7D</a>
Thank you Aaron, these videos were quite informative. Though I still have a long ways to go... any luck locating the token origin script?
1612846023
The Aaron
Roll20 Production Team
API Scripter
I found the code I was looking for, but I decided to just use it as a reference and write something new.&nbsp; This should handle most of what you want.&nbsp; It defaults to an image I had for the purpose, but you can change it by selecting a graphic and calling: !mark-start --set-marker You can also set the scale factor (defaults to 1.7): !mark-start --set-scale 1.5 And there's help: !mark-start --help MarkTurnStartLocation v0.1.0 Creates a marker to show where a token started on when its turn began. !mark-start [--set-marker] [--set-scale NUMBER] --set-marker &nbsp;Set the marker image to the image source of the single selected token. --set-scale NUMBER &nbsp;Set the scale of the marker image to the supplied NUMBER. Default is&nbsp; 1.7 .&nbsp; 1.0 &nbsp;makes the image the same size as the token. Code: /* global GroupInitiative */ on('ready',()=&gt;{ const scriptName = 'MarkTurnStartLocation'; const version = '0.1.0'; const schemaVersion = 0.1; const lastUpdate = 1612845685; const getTurnArray = () =&gt; ( '' === Campaign().get('turnorder') ? [] : JSON.parse(Campaign().get('turnorder'))); const checkInstall = () =&gt; { log(`-=&gt; ${scriptName} v${version} &lt;=- [${lastUpdate}]`); if ( !state.hasOwnProperty(scriptName) || state[scriptName].version !== schemaVersion ) { log(` &gt; Updating Schema to v${schemaVersion} &lt;`); switch (state[scriptName] &amp;&amp; state[scriptName].version) { case 0.1: /* break; // intentional dropthrough */ /* falls through */ case "UpdateSchemaVersion": state[scriptName].version = schemaVersion; break; default: state[scriptName] = { version: schemaVersion, options: { markerImage: `<a href="https://s3.amazonaws.com/files.d20.io/images/4996490/Ii0ukmjd-IFyjKHtfRrG5w/thumb.png?1406962627`" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/4996490/Ii0ukmjd-IFyjKHtfRrG5w/thumb.png?1406962627`</a>, scale: 1.7 } }; break; } } }; const showImg = (img) =&gt; `&lt;img src="${img}" style="max-width: 3em;max-height: 3em;border:1px solid #333; background-color: #999; border-radius: .2em;"&gt;`; const getCleanImgsrc = (imgsrc) =&gt; { let parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/); if(parts) { return parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`); } return; }; on('chat:message',msg=&gt;{ if('api'===msg.type &amp;&amp; /^!mark-start(\b\s|$)/i.test(msg.content) &amp;&amp; playerIsGM(msg.playerid)){ let who = (getObj('player',msg.playerid)||{get:()=&gt;'API'}).get('_displayname'); let tokens = (msg.selected || []) .map(o=&gt;getObj('graphic',o._id)) .filter(g=&gt;undefined !== g) ; let msgs = []; let args = msg.content.split(/\s+--/).slice(1); if(0 === args.length){ args.push('help'); } args.forEach((a)=&gt;{ let parts = a.split(/\s+/); switch(parts.shift().toLowerCase()){ case 'set-marker': { if(1 === tokens.length){ let imgsrc = getCleanImgsrc(tokens[0].get('imgsrc')); if(imgsrc){ state[scriptName].options.markerImage = imgsrc; msgs.push(`&lt;div&gt;&lt;div&gt;${showImg(imgsrc)}&lt;/div&gt;Updated start marker.&lt;/div&gt;`); } else { msgs.push(`&lt;div&gt;&lt;div&gt;${showImg(imgsrc)}&lt;/div&gt;&lt;code&gt;Error: Cannot use Marketplace image.&lt;/code&gt;&lt;/div&gt;`); } } else { msgs.push(`&lt;div&gt;&lt;code&gt;Error: Select a single token to collect the image from.&lt;/code&gt;&lt;/div&gt;`); } } break; case 'set-scale': { let scale = parseFloat(parts.shift())||0; if(scale){ if(scale &gt; 5.0){ msgs.push(`&lt;div&gt;&lt;code&gt;Warning: Scale seams pretty large.&lt;/code&gt;&lt;/div&gt;`); } else if( scale &lt; 0.1 ) { msgs.push(`&lt;div&gt;&lt;code&gt;Warning: Scale seams pretty small.&lt;/code&gt;&lt;/div&gt;`); } state[scriptName].options.scale = scale; msgs.push(`&lt;div&gt;Updated start marker scale to &lt;code&gt;${scale}&lt;/code&gt;.&lt;/div&gt;`); } else { msgs.push(`&lt;div&gt;&lt;code&gt;Error: Scale should be a decimal number where 1 means the same size as the token, 1.5 means 50% larger, etc.&lt;/code&gt;&lt;/div&gt;`); } } break; case 'help': { msgs.push(`&lt;h3&gt;MarkTurnStartLocation v${version}&lt;/h3&gt;`); msgs.push(`&lt;div&gt;Creates a marker to show where a token started on when its turn began.&lt;/div&gt;`); msgs.push(`&lt;div&gt;&lt;code&gt;!mark-start [--set-marker] [--set-scale NUMBER]&lt;/code&gt;&lt;/div&gt;`); msgs.push(`&lt;ul&gt;&lt;li&gt;&lt;code&gt;--set-marker&lt;/code&gt; Set the marker image to the image source of the single selected token.&lt;/li&gt;&lt;li&gt;&lt;code&gt;--set-scale NUMBER&lt;/code&gt; Set the scale of the marker image to the supplied NUMBER. Default is &lt;code&gt;1.7&lt;/code&gt;. &lt;code&gt;1.0&lt;/code&gt; makes the image the same size as the token.&lt;/li&gt;&lt;/ul&gt;`); } break; } }); sendChat('',`/w "${who}" &lt;div style="border:1px solid #999;background-color: white;padding:.5em;"&gt;${msgs.join('')}&lt;/div&gt;`); } if('api'===msg.type &amp;&amp; /^!eot\b/.test(msg.content)){ setTimeout(()=&gt;handleTurnOrderChange(Campaign(),{initiativepage:false,turnorder:JSON.stringify([{id:-1}])}),1000); } }); let ClearedIDs = {}; const clearMarkers = ()=&gt;{ findObjs({ type: 'graphic', controlledby: scriptName }).forEach(g=&gt;{ ClearedIDs[g.id]=true; g.remove(); }); }; const addMarker = (tid) =&gt; { let t = getObj('graphic',tid); if(t){ let layer = (t.get('layer') == 'gmlayer') ? 'gmlayer' : 'map'; let props = [ 'pageid', 'left', 'top', 'width', 'height' ] .reduce((m,p)=&gt;({...m,[p]:t.get(p)}),{ imgsrc: state[scriptName].options.markerImage, controlledby: scriptName, layer }); props.width *= state[scriptName].options.scale; props.height *= state[scriptName].options.scale; let m = createObj('graphic', props); if('gmlayer' === layer){ toBack(m); } else { toFront(m); } } }; const handleTurnOrderChange = (obj,prev) =&gt; { let force = (obj.get('initiativepage') !== prev.initiativepage); if(obj.get('initiativepage')){ setTimeout(()=&gt;{ let t = getTurnArray()[0]; if(t){ let pto = (''===prev.turnorder ? [] : JSON.parse(prev.turnorder)); if(pto[0].id !== t.id || force){ clearMarkers(); addMarker(t.id); } } },100); } else { clearMarkers(); } }; const handleChangeGraphic = (obj,prev) =&gt; { if(scriptName === prev.controlledby){ obj.set(prev); } }; const handleDestroyGraphic = (obj) =&gt; { if(ClearedIDs[obj.id]){ setTimeout(()=&gt;delete ClearedIDs[obj.id],1000); } else { let prev = JSON.parse(JSON.stringify(obj)); if(scriptName === prev.controlledby){ prev.pageid=prev._pageid; let m = createObj('graphic',prev); if('gmlayer' === prev.layer){ toBack(m); } else { toFront(m); } } } }; const registerEventHandlers = () =&gt; { on('change:campaign:turnorder',handleTurnOrderChange); on('change:campaign:initiativepage',handleTurnOrderChange); on('change:graphic',handleChangeGraphic); on('destroy:graphic',handleDestroyGraphic); if('undefined' !== typeof GroupInitiative &amp;&amp; GroupInitiative.ObserveTurnOrderChange){ GroupInitiative.ObserveTurnOrderChange((o,p)=&gt;handleTurnOrderChange(Campaign(),{initiativepage: false, turnorder:p})); } }; checkInstall(); registerEventHandlers(); if(Campaign().get('initiativepage')){ handleTurnOrderChange(Campaign(),{initiativepage:false,turnorder:JSON.stringify([{id:-1}])}); } });
1612877784
David M.
Pro
API Scripter
Neat!
Aaron -- YOU ARE AMAZING!!! This is exactly what I was looking for. Absolutely fantastic job, and thank you so much!!
1612880531
The Aaron
Roll20 Production Team
API Scripter
=D&nbsp; No problem. =D
@Aaron Smart how you use event on('ready', () =&gt;&nbsp; {&nbsp; &lt;... whatever... &gt; } to scope the whole functionality. I am following a template you posted a long time ago var XYZ = XYZ || (function() {&nbsp; &lt;... whatever ... &gt; } Why/when should I use one or the other method?
1612893089

Edited 1612930870
The Aaron
Roll20 Production Team
API Scripter
In my "full scripts" I use the Revealing Module Pattern , for lighter weight 1-offs, I tend to just stuff things in on('ready',...). It all has to do with scope and time of instantiation. In both cases, there's a Closure being created to encapsulate the private implementation details of the script.&nbsp; In the RMP case, that happens immediately on execution of the script at start up.&nbsp; With the on('ready',...) wrapper, it happens as soon as the ready event fires.&nbsp; Using RMP allows you to expose an interface object, which is captured as the return of the closure: const InterfaceFoo = (()=&gt;{ const myStuff = { thing: 'taco', amount: 'pi' }; const myPrivateFunc = (thing) =&gt; { thing.do(myStuff); }; return { func1: myPrivateFunc }; })(); /* ... */ InterfaceFoo.func1(myThing); Particularly for big scripts that want to expose some functionality to other scripts (notification events, Script Interfaces to do things, etc), this is an important way to do things.&nbsp; Sometimes you need to do things early, for example, libTokenMarkers loads all the custom markers as soon as they become available instead of waiting for on('ready',...), so that it can service requests as soon as possible. For small scripts that have no external interface exposed to other scripts, and are just going to respond to player things, wrapping in on('ready',...) gives a private scope, doesn't pollute the public namespace, and makes sure to avoid any problems (like processing chat events early).&nbsp; Really, just simpler and clean. Either method is really fine, there's just more to set up with the RMP (though I have a script for that...).&nbsp; Honestly, since this one is using the state and handling lots of events and registering for things, it could go either way... =D
1612893137

Edited 1612893210
The Aaron
Roll20 Production Team
API Scripter
Oh, and here's my current full script template (discussion on that API_Meta stuff here ): // Github: <a href="https://github.com/shdwjk/Roll20API/blob/master/NAME/NAME.js" rel="nofollow">https://github.com/shdwjk/Roll20API/blob/master/NAME/NAME.js</a> // By: The Aaron, Arcane Scriptomancer // Contact: <a href="https://app.roll20.net/users/104025/the-aaron" rel="nofollow">https://app.roll20.net/users/104025/the-aaron</a> var API_Meta = API_Meta||{}; API_Meta.NAME={offset:Number.MAX_SAFE_INTEGER,lineCount:-1}; {try{throw new Error('');}catch(e){API_Meta.NAME.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-6);}} const NAME = (() =&gt; { // eslint-disable-line no-unused-vars const scriptName = 'NAME'; const version = '0.1.0'; API_Meta.NAME.version = version; const lastUpdate = 1609291956; const schemaVersion = 0.1; const checkInstall = () =&gt; { log(`-=&gt; ${scriptName} v${version} &lt;=- [${new Date(lastUpdate*1000)}]`); if( ! state.hasOwnProperty(scriptName) || state[scriptName].version !== schemaVersion) { log(` &gt; Updating Schema to v${schemaVersion} &lt;`); switch(state[scriptName] &amp;&amp; state[scriptName].version) { case 0.1: /* break; // intentional dropthrough */ /* falls through */ case 'UpdateSchemaVersion': state[scriptName].version = schemaVersion; break; default: state[scriptName] = { version: schemaVersion }; break; } } }; const handleInput = (msg) =&gt; { if (msg.type !== "api") { return; } let args = msg.content.split(/\s+/); switch(args[0]) { case '!NAME': break; } }; const registerEventHandlers = () =&gt; { on('chat:message', handleInput); }; on('ready', () =&gt; { checkInstall(); registerEventHandlers(); }); return { // Public interface here }; })(); {try{throw new Error('');}catch(e){API_Meta.NAME.lineCount=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-API_Meta.NAME.offset);}}
1612910643

Edited 1612911014
Thank you, learned a lot tonight :) I like the API_Meta stuff.&nbsp;What i currently do is concatenate my own apiscript.js and then upload that single large blob (now 732,1kb) Wonder what's the best way of working in the end, as the one click scripts do not have the API_meta stuff included. This should be something that would be provided by the roll20 docker.
@Aaron - So, the MarkTurnStartLocation script is mostly working in my game; however, when I copy the script to another game, I'm getting the following error: Error: toFront() must be given an object either from an event or getObj() or similar. undefined "-=&gt; MarkTurnStartLocation v0.1.0 &lt;=-&nbsp; [1612845685]" "ERROR: You cannot set the imgsrc or avatar of an object unless you use an image that is in your Roll20 Library. See the API documentation for more info." Error: toFront() must be given an object either from an event or getObj() or similar. undefin
1612930828
The Aaron
Roll20 Production Team
API Scripter
Hmm.&nbsp; That's peculiar. That sounds like you've managed to get an image set that isn't in a user library?&nbsp; And that is causing it to fail to create the graphic, which is then causing the value passed to toFront() to be undefined.&nbsp; If you want to PM me an invite, and GM me, I'll come investigate.
So if anyone else is interested in running this (awesome) script, Aaron helped me troubleshoot the errors I was getting. The problem was that I had modified the script to change the image for the origin token to a file hosted on my own server. As per Aaron, this is why it wasn't working: The problem is that the API can only create graphics with images that are in a user library.&nbsp; That means you can't just specify any image on the internet, you have to place the image in the game, then collect its URL from there, which is what the above command does.&nbsp; If you edited the script to have that image URL in your other game and it continued to work, it's because it stores the image URL in the state object, which is persisted between executions of the game, so would not have gotten reset from the one I used despite you're having edited the script. Instead, drag the origin token you want to use into the game, select it, and then enter the following into chat: !mark-start --set-marker Once again, many many thanks to @The Aaron for putting this together, and helping me resolve the API errors. Roll20 is truly richer with him here!