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

[Help] Getting token coordinates using 'selected' and 'target'

1595886365

Edited 1595886512
Heya, hoping to get some help from more experienced folks! I'm trying to create a quick way to calculate range penalties in my Pathfinder game. Right now I use a calculation that looks like this: [[0-(ceil((?{Distance to target? (max = 300)|5}-50) / 50) * 2)]] This requires myself or the player to measure the distance between their token and the target token before making their attack. I was hoping to change this to a method that takes the position of the selected token and the target token as the attack is made, so the player simply has to have their token selected and click a target. That would look something like this: [[0-(ceil(([[round([[(((abs(@{selected|position-x}-@{target|Target|position-x})*5/70)**2) + ((abs(@{selected|position-y}-@{target|Target|position-y})*5/70)**2))**0.5]]/5)*5]]-50) / 50) * 2)]] I'm also stuck on how to properly calculate the distance in a way that complies with this: (from <a href="https://wiki.roll20.net/Ruler#Square_Grids_have_four_options_for_measuring" rel="nofollow">https://wiki.roll20.net/Ruler#Square_Grids_have_four_options_for_measuring</a>: ) Pathfinder/3.5E Compatible measures a diagonal move as 1.5 units (rounding down). Thus, when 1 unit equals 5ft, diagonal moves alternate between 5ft and 10ft increments (i.e. 5ft, 15ft, 20ft, 30ft, etc.). Aside from that, I came across this snippet that The Aaron made a few years back: <a href="https://app.roll20.net/forum/permalink/4610415/" rel="nofollow">https://app.roll20.net/forum/permalink/4610415/</a> on('ready', function(){ &nbsp; &nbsp; "use strict"; &nbsp; &nbsp; on('change:graphic',function(obj,prev){ &nbsp; &nbsp; &nbsp; &nbsp; if( ( _.contains(['gmlayer','objects'],obj.get('layer')) ) &amp;&amp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ( obj.get('left') !== prev.left || obj.get('top') !== prev.top) &amp;&amp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; obj.get('represents') ) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let a=_.chain(findObjs({type: 'attribute',characterid: obj.get('represents')})) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .filter(a=&gt;a.get('name').match(/^position-[xy]$/)) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .each(a=&gt;{ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; switch(a.get('name')){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'position-x':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a.set({current: obj.get('left')}); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'position-y':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a.set({current: obj.get('top')}); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }); }); It records the X and Y positions of a token within two sheet attributes of the token's connected character. This only works with characters represented by a single token. Not very useful when multiple enemy tokens are linked to the same sheet. The main things I'm trying to figure out: Can 'selected' and 'target' functions be used to get non-bar values from a token? (We make use of all 3 token bars, so I'd prefer to avoid changing that if possible; maybe using sight angle and reveal distance instead?) Can the snippet above be changed to store the 'left' and 'top' values in the token values in some way? If either 1 or 2 is impossible, what is an alternative way to store these values that is unique to each token? Maybe automatically creating a unique attribute for each token's x and y positions in a single dedicated character? Is there a way to improve my distance calculation so that it complies with the measurement system mentioned above? It's mostly accurate for distances under 10 units, but beyond that it gets inaccurate.
1595891398
The Aaron
Roll20 Production Team
API Scripter
Answering your questions: Very few.&nbsp; You can get the bars (current and max), the name, the character id, and token id. You could store them as part of the gm notes on the token, and decode them from there, but that's not useful for macros as you can't reference them. There are lots of places you could store the information (state, attributes on a character, part of a handout, etc), but no way to automatically reference them from a macro based on @{target|..} or @{selected|...}. I would suggest a script... I can probably help with that, if you're interested...
1595894215
Kurt J.
Pro
API Scripter
I found this article &nbsp;and translated his formula for 3.5e/Pathfinder distances to the following script (probably not the most elegant way to do it, but it should be fairly easy to follow the math written this way): on("ready",function() { on("chat:message",function(msg){ if (msg.type == "api" &amp;&amp; msg.content.match(/^!distance/)) { var args = msg.content.split(" "); var token1 = getObj("graphic", args[1]); var token2 = getObj("graphic", args[2]); if (token1 !== undefined &amp;&amp; token2 !== undefined) { var x1 = token1.get("left"); var x2 = token2.get("left"); var y1 = token1.get("top"); var y2 = token2.get("top"); var mindist = Math.min(Math.abs(x1 - x2), Math.abs(y1 - y2)); var maxdist = Math.max(Math.abs(x1 - x2), Math.abs(y1 - y2)); var pf_distance = Math.floor((maxdist - mindist) + (1.5 * mindist)); var thisPage = getObj("page",Campaign().get("playerpageid")); var gridSize = (thisPage.get("snapping_increment") * 70); var distance = Math.round(pf_distance/gridSize); var unitsDistance = distance * thisPage.get("scale_number"); sendChat("api", `Distance is : ${distance} grid units or ${unitsDistance} ${thisPage.get("scale_units")}`); } } }); }); And could be called with a simple macro: !distance @{selected|token_id} @{target|token_id} Comparing it with the measuring tool in Roll20, it is pretty close - the two methods might disagree by one square depending on the particular angles, and I don't know how the internal calculation works. That is also assuming that the math in the article is correct and that I didn't make any mistakes in translating it :)&nbsp;
1595894982

Edited 1595903833
@The Aaron, I'd be grateful for any help you can offer! Is there a way for the API to find the position of a token when it changes from GM layer to Object layer? I was just now thinking, a possible workaround could be using Bump to swap an enemy to the GM layer and back when a player wants to target it, so that the attributes in the sheet get updated without having to move the token's position. Or better yet, can the API tell when a token is simply clicked? Then I could just click the token in question to update the attributes.
1595895575

Edited 1595896410
@Kurt, thank you! That article was really helpful, the quote regarding the Pathfinder measurements threw me off with how it was worded. If I'm understanding correctly, it looks like all I need to do is wrap the distance part of my calculation in floor(....*1.5) instead of round(....). Like I was doing before. I did not understand correctly... Unfortunately the adjustments you made in the snippet won't work for the use I have in mind. I use the calculation inside the attack roll of the sheet itself, so I wouldn't be able to use an API command within the inline roll.
1595897783

Edited 1595903899
This seems to calculate the distance correctly, using the math from the article, then dividing by 5, rounding down, and multiplying by 5 to keep the measurement in multiples of 5. [[floor((([[{abs((@{selected|position-x}-@{target|Target|position-x})*5/70),abs((@{selected|position-y}-@{target|Target|position-y})*5/70)}kh1]]-[[{abs((@{selected|position-x}-@{target|Target|position-x})*5/70),abs((@{selected|position-y}-@{target|Target|position-y})*5/70)}kl1]])+floor(1.5*([[{abs((@{selected|position-x}-@{target|Target|position-x})*5/70),abs((@{selected|position-y}-@{target|Target|position-y})*5/70)}kl1]])))/5)*5]] So then plugging it into the penalty calculation... +[[0-(ceil(([[floor((([[{abs((@{selected|position-x}-@{target|Target|position-x})*5/70),abs((@{selected|position-y}-@{target|Target|position-y})*5/70)}kh1]]-[[{abs((@{selected|position-x}-@{target|Target|position-x})*5/70),abs((@{selected|position-y}-@{target|Target|position-y})*5/70)}kl1]])+floor(1.5*([[{abs((@{selected|position-x}-@{target|Target|position-x})*5/70),abs((@{selected|position-y}-@{target|Target|position-y})*5/70)}kl1]])))/5)*5]]-[[@{weapon_range}]]) / [[@{weapon_range}]]) * 2)]][RPEN] Thanks again for sharing that, Kurt!
1595901546

Edited 1595901666
The Aaron
Roll20 Production Team
API Scripter
Would this work for you? !pf-range &lt;range increment&gt; &lt;source token id&gt; &lt;target token id&gt; [&lt;target token id&gt; ...] For example: !pf-range 10 @{selected|token_id} @{target|1|token_id} @{target|2|token_id} @{target|3|token_id} @{target|4|token_id} You can click a token multiple times and it will eliminate duplicates. You can also use !wpf-range to have the chart whispered to the player in question. Script: on('ready',()=&gt;{ const getPageForPlayer = (playerid) =&gt; { let player = getObj('player',playerid); if(playerIsGM(playerid)){ return player.get('lastpage') || Campaign().get('playerpageid'); } let psp = Campaign().get('playerspecificpages'); if(psp[playerid]){ return psp[playerid]; } return Campaign().get('playerpageid'); }; const pageScaler = (page) =&gt; { let factor = (parseFloat(page.get('snapping_increment'))||1) * 70; let scaleNumber = (parseFloat(page.get('scale_number'))||5); return (...pts) =&gt; pts.slice(1).reduce((m,p,i)=&gt;{ let dx = Math.abs(p[0]-pts[i][0]); let dy = Math.abs(p[1]-pts[i][1]); m.diag += Math.floor(Math.min(dx,dy)/factor); let magDelta = Math.floor(m.diag / 2) * factor + Math.max(dx,dy); m.distGrid = magDelta / 70; // maybe this should be factor...? m.distScale = m.distGrid * scaleNumber; return m; },{diag:0,distGrid:0, distScale:0}); }; const processInlinerolls = (msg) =&gt; { if(_.has(msg,'inlinerolls')){ return _.chain(msg.inlinerolls) .reduce(function(m,v,k){ let ti=_.reduce(v.results.rolls,function(m2,v2){ if(_.has(v2,'table')){ m2.push(_.reduce(v2.results,function(m3,v3){ m3.push(v3.tableItem.name); return m3; },[]).join(', ')); } return m2; },[]).join(', '); m['$[['+k+']]']= (ti.length &amp;&amp; ti) || v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } else { return msg.content; } }; const css = (o)=&gt;Object.keys(o).reduce((m,k)=&gt;`${m}${k}:${o[k]};`,''); const s = { box: css({ ["font-weight"] : "bold", ["border-bottom"] : "2px solid #0F3DA0", ["border-top"] : "4px solid #0F3DA0", ["background-color"] : "#AEB6C6" }), heading: css({ ["font-weight"] : "bold", ["font-size"] : "1.3em" }), title: css({ ["font-weight"] : "bold" }), row: css({ ["margin"] : ".1em", ["border-bottom"] : "1px solid #0F3DA0" }), num: css({ ["display"] : "inline-block", ["float"] : "right", ["font-size"] : "1.3em", ["padding"] : ".1em" }), img: css({ ["max-width"] : "2em", ["max-height"] : "2em", ["float"] : "left", ["border"] : "1px solid #999999", ["border-radius"] : ".25em", ["background-color"] : "white" }), clear: css({ ["clear"] : "both" }) }; const f = { content: (t,c) =&gt; `&lt;div style="${s.content}"&gt;${f.heading(t)}${c}&lt;/div&gt;`, heading: (t) =&gt; `&lt;div style="${s.heading}"&gt;${t}&lt;/div&gt;`, box: (t) =&gt; `&lt;div style="${s.box}"&gt;${t}&lt;/div&gt;`, img: (url) =&gt; `&lt;img style="${s.img}" src="${url}"&gt;`, title: (t) =&gt; `&lt;div style="${s.title}"&gt;${t}&lt;/div&gt;`, clear: () =&gt; `&lt;div style="${s.clear}"&gt;&lt;/div&gt;`, num: (n) =&gt; `&lt;div style="${s.num}"&gt;${n}&lt;/div&gt;`, row: (...o) =&gt; `&lt;div style="${s.row}"&gt;${o.join(' ')}${f.clear()}&lt;/div&gt;` }; on('chat:message',msg=&gt;{ if('api'===msg.type &amp;&amp; /^!w?pf-range(\b\s|$)/i.test(msg.content) &amp;&amp; playerIsGM(msg.playerid)){ let who = (getObj('player',msg.playerid)||{get:()=&gt;'API'}).get('_displayname'); let page = getObj('page',getPageForPlayer(msg.playerid)); let u = page.get('scale_units'); let dist = pageScaler(page); // !pf-range dist source target [target ...] let args = processInlinerolls(msg).split(/\s+/); let whisper = /^!w/i.test(args[0]); let range = parseFloat(args[1]); let src = getObj('graphic',args[2]); if(src){ let pt0 = [src.get('left'),src.get('top')]; let trgs = [...new Set(args.slice(3))] .map(id=&gt;getObj('graphic',id)) .filter(o=&gt;undefined!==o) .map(t=&gt;({t,d:dist(pt0,[t.get('left'),t.get('top')])})) .map(o=&gt;({...o,p:Math.floor((o.d.distScale-1)/range)*(-2)})) .sort((a,b)=&gt;a.d.distScale-b.d.distScale) ; let output = f.box(trgs.map(d=&gt;f.row(f.img(d.t.get('imgsrc')),f.num(d.p),f.title(`${d.d.distScale.toFixed(0)}${u}`))).join('')); sendChat('', `${whisper ? `/w "${who}" ` : ''}${output}`); } } }); });
1595901791

Edited 1595901913
The Aaron
Roll20 Production Team
API Scripter
This function: const pageScaler = (page) =&gt; { let factor = (parseFloat(page.get('snapping_increment'))||1) * 70; let scaleNumber = (parseFloat(page.get('scale_number'))||5); return (...pts) =&gt; pts.slice(1).reduce((m,p,i)=&gt;{ let dx = Math.abs(p[0]-pts[i][0]); let dy = Math.abs(p[1]-pts[i][1]); m.diag += Math.floor(Math.min(dx,dy)/factor); let magDelta = Math.floor(m.diag / 2) * factor + Math.max(dx,dy); m.distGrid = magDelta / 70; // maybe this should be factor...? m.distScale = m.distGrid * scaleNumber; return m; },{diag:0,distGrid:0, distScale:0}); }; takes a page object and returns a function that will do PF measurements between an array of points for that page.&nbsp; I adapted this out of the code that is used for the measure tool on Roll20.&nbsp; Points are and array of the left and top of a graphic.&nbsp; You can specify 2 or more points to the function and it returns an object that measures the full walk along those points with proper diagonal counting (probably... I didn't actually test that part... =D).
1595903766

Edited 1595906998
I'll give that a try, thank you!! It looks like it could be useful for spellcasting situations, but I clarified in an earlier post (forgot to mention in the first post, sorry!) that my goal is to have the calculation within the inline roll of the attack, directly in the sheet (so API commands wouldn't work in this case). I solved that calculation issue in my last post thanks to formula Kurt shared, but it is still depending on the snippet in my first post. Since the main setback of the snippet is that it doesn't work well for enemy tokens that share a character sheet, I decided an acceptable workaround would be finding a quick way to update the attributes used by the snippet. Right now, it only updates when the token changes position, so I wonder if it could be adjusted to change either when the token switches layers without changing position, or by clicking/selecting the token. I realized using Bump to swap layers consequently deselects the token, so it can't be used in rapid succession. So that leaves me with these questions: Could the snippet update the attributes when the token swaps layers but doesn't change position? If yes to 1, can the API get that info if the layer swap is caused by Bump? Or would Bump not trigger it the same way as manually swapping layers? If yes to 2, would you be willing to add a 'flicker' option to Bump that quickly swaps the selected token from one layer to the other then back? If no to 1 or 2, can the API update the attributes when the token is simply clicked/selected?
1595907476

Edited 1595907510
The Aaron
Roll20 Production Team
API Scripter
No worries, it was fun to write and now I have a function for 3.5/pf style measurements. =D If you take out this line: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ( obj.get('left') !== prev.left || obj.get('top') !== prev.top) &amp;&amp; then it will update for any change on the token.&nbsp; You can similarly change it to something like: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ( ['left','top','rotation','layer'].find(p=&gt;obj.get(p) !== prev[p]) ) &amp;&amp; and just add the properties to the array that you want to monitor for changes to trigger an update. ... ... Ok, here's a version that updates based on left, top, width, height, rotation, and layer, responds to adjustments by tokenMod, and has a command that sets the properties (and creates the attributes) for each selected token: !update-pos Script: /* global GroupInitiative TokenMod */ on('ready', () =&gt; { const changeProps = [ 'left', 'top', 'width', 'height', 'rotation', 'layer' ]; const findOrCreateObj = (props) =&gt; [findObjs(props)[0] || createObj(props.type,props)]; const updateTokenAttrs = (token,create=false) =&gt; { (((create ? findOrCreateObj : findObjs)({ type: 'attribute', name: 'position-x', characterid: token.get('represents') })[0])||{set:()=&gt;{}}).set({current: token.get('left')}); (((create ? findOrCreateObj : findObjs)({ type: 'attribute', name: 'position-y', characterid: token.get('represents') })[0])||{set:()=&gt;{}}).set({current: token.get('top')}); }; const handleChange = (obj,prev) =&gt; { if( ( 'graphic' === prev._type ) &amp;&amp; ( ['gmlayer','objects'].includes(obj.get('layer')) ) &amp;&amp; ( changeProps.find(p=&gt;obj.get(p) !== prev[p]) ) &amp;&amp; obj.get('represents') ) { updateTokenAttrs(obj); } } on('chat:message',msg=&gt;{ if('api'===msg.type &amp;&amp; /^!update-pos(\b\s|$)/i.test(msg.content) ){ let tokens = (msg.selected || []) .map(o=&gt;getObj('graphic',o._id)) .filter(g=&gt;undefined !== g) .map(t=&gt;updateTokenAttrs(t,true)) ; } }); on('change:graphic',handleChange); if('undefined' !== typeof TokenMod &amp;&amp; TokenMod.ObserveTokenChange){ TokenMod.ObserveTokenChange(handleChange); } });
1595908909
timmaugh
Pro
API Scripter
...I know a script that, pretty soon, could use a "getdistance" kinda function... *whistles innocently*
That works great! And I was able to set up a macro that uses TokenMod to switch to GMlayer and back, achieving that 'flicker' effect to quickly update the attributes for mooks tokens with a single token action click. Thanks again!
1595911075
The Aaron
Roll20 Production Team
API Scripter
No problem.&nbsp; You could just call !update-pos with tokens selected, but TokenMod works, too. =D
Oh that's what you meant, I misunderstood it as simply creating the attributes. Even better!