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

Movement Template Auto-Placement

Hi All, Running a Gaslands game, and I'm trying to streamline play and cut down on the fiddliness of the templates by automating the placement as much as possible.&nbsp; I found the link below: <a href="https://app.roll20.net/forum/post/9598932/dropping-graphic-adjacent-to-selected-char-token-matching-rotation/?pageforid=9598968#post-9598968" rel="nofollow">https://app.roll20.net/forum/post/9598932/dropping-graphic-adjacent-to-selected-char-token-matching-rotation/?pageforid=9598968#post-9598968</a> ...which seems like a great solution, but one that only works when all of the tokens are identical in size.&nbsp; Unfortunately, in the game I'm running, we've got a wide variety of vehicles of different sizes, so I don't know that this solution will work for me.&nbsp; Even more unfortunately, I can handle macros but not scripting... My ideal solve would be to have the template ("Medium Straight", "Gentle Turn (Left)", etc.) selected via a chat button.&nbsp; The button would trigger a macro that would: - Pull the appropriate template from the GM layer (via TokenMod); - Set the template's rotation to match that of the selected token (???); - Compute the Left, Top coordinates at which to place the template; &nbsp; &nbsp; &nbsp;- To do this, I think I'd need to somehow pull the height value of the selected token (halved) and add it to the height value of the chosen template (halved) to get the radius of a circle, then use sin and cos values to find the Left, Top coordinates (relative to the vehicle token) to place the template (via TokenMod); I can find the x, y position of the selected template using this script from TheAaron:&nbsp; <a href="https://app.roll20.net/forum/permalink/4610415/" rel="nofollow">https://app.roll20.net/forum/permalink/4610415/</a> on('ready', function(){ "use strict"; on('change:graphic',function(obj,prev){ if( ( _.contains(['gmlayer','objects'],obj.get('layer')) ) &amp;&amp; ( obj.get('left') !== prev.left || obj.get('top') !== prev.top) &amp;&amp; obj.get('represents') ) { let a=_.chain(findObjs({type: 'attribute',characterid: obj.get('represents')})) .filter(a=&gt;a.get('name').match(/^position-[xy]$/)) .each(a=&gt;{ switch(a.get('name')){ case 'position-x': a.set({current: obj.get('left')}); break; case 'position-y': a.set({current: obj.get('top')}); break; } }); } }); }); But what I'm struggling with is the following: -&nbsp; Finding the rotation value of the token so I can match the template rotation to it; -&nbsp; Finding the height value of the token so I can compute the placement of the movement template; Any suggestions for how I can gather these values so I can cobble together a solve for this?&nbsp; Or is there a better approach that has already been explored and executed?&nbsp; Thanks in advance!
1614872008

Edited 1614872629
timmaugh
Forum Champion
API Scripter
I'm not at a pc to type out the specifics, but if you look at this thread, there is discussion of those exact points... <a href="https://app.roll20.net/forum/permalink/9863517/" rel="nofollow">https://app.roll20.net/forum/permalink/9863517/</a> The included script includes the rotation values for a token. Hopefully that gets you where you need to be.
Thanks for pointing me to that thread!&nbsp; With the info in there I was able to cobble together an adjustment to the posted script to save the token's rotation to an attribute in the file: on('ready', function(){ "use strict"; on('change:graphic',function(obj,prev){ if( ( _.contains(['gmlayer','objects'],obj.get('layer')) ) &amp;&amp; ( obj.get('left') !== prev.left || obj.get('top') !== prev.top || obj.get('rotation') !== prev.rotation) &amp;&amp; obj.get('represents') ) { let a=_.chain(findObjs({type: 'attribute',characterid: obj.get('represents')})) .filter(a=&gt;a.get('name').match(/^position-[xyr]$/)) .each(a=&gt;{ switch(a.get('name')){ case 'position-x': a.set({current: obj.get('left')}); break; case 'position-y': a.set({current: obj.get('top')}); break; case 'position-r': a.set({current: obj.get('rotation')}); break; } }); } }); }); Is there a bit of script that could be included in the above that would pull the value of the token's height (i.e. the length of the vehicle from tip to tail) and allow me to save it to an attribute?
Aaaaaand a hitch....&nbsp; here's what I'm trying to use to set the location of a template: !token-mod {{ --current-page --ids -MEsMTevagejnfLTJ7bP --ignore-selected --set layer|objects left|(131.5*((@{selected|position-r}*0.017453289) - (@{selected|position-r}*0.017453289)**3/(3*2) + (@{selected|position-r}*0.017453289)**5/(5*4*3*2) - (@{selected|position-r}*0.017453289)**7/(7*6*5*4*3*2))) top|(131.5*(1 - (@{selected|position-r}*0.017453289)**2/2 + (@{selected|position-r}*0.017453289)**4/(4*3*2) - (@{selected|position-r}*0.017453289)**6/(6*5*4*3*2))) rotation|@{selected|position-r} }} It's capturing the rotation, but not the left or top settings.&nbsp; Can TokenMod read computed values like this instead of single numbers...?
1614904627

Edited 1614904642
timmaugh
Forum Champion
API Scripter
The height of the token is just another get() from the object, so: obj.get('height') You can do it in the above code (with some alterations I'll get to)... but do you need to? The above code fires every time the token moves/rotates. If the height (length) of the vessel is never changing, why would you set it here? Better to set it when the vessel is introduced to the board, I think. If that fits your game and makes sense, we can figure that out. For now, to make it work in the above code, you would need to open up your regex to allow for an attribute of the appropriate name... let's say the attribute is 'beam'. This is what it would look like (updating your code slightly so you don't need the underscore commands -- these are all native to vanilla js). on('ready', () =&gt; { on('change:graphic', (obj, prev) =&gt; { if (['gmlayer', 'objects'].includes(obj.get('layer')) &amp;&amp; (obj.get('left') !== prev.left || obj.get('top') !== prev.top || obj.get('rotation') !== prev.rotation) &amp;&amp; obj.get('represents')) { findObjs({ type: 'attribute', characterid: obj.get('represents') }) .filter(a =&gt; a.get('name').match(/^(position-[xyr]|beam)$/)) .forEach(a =&gt; { switch (a.get('name')) { case 'position-x': a.set({ current: obj.get('left') }); break; case 'position-y': a.set({ current: obj.get('top') }); break; case 'position-r': a.set({ current: obj.get('rotation') }); break; case 'beam': a.set({ current: obj.get('height') }); break; } }); } }); }); That all assume that those attributes exist on the character sheet, of course. If they don't, you can code a "create if they don't exist" catch, but let's see if this gets you where you need to go.
1614904924
timmaugh
Forum Champion
API Scripter
Jeff H. said: Aaaaaand a hitch....&nbsp; here's what I'm trying to use to set the location of a template: !token-mod {{ --current-page --ids -MEsMTevagejnfLTJ7bP --ignore-selected --set layer|objects left|(131.5*((@{selected|position-r}*0.017453289) - (@{selected|position-r}*0.017453289)**3/(3*2) + (@{selected|position-r}*0.017453289)**5/(5*4*3*2) - (@{selected|position-r}*0.017453289)**7/(7*6*5*4*3*2))) top|(131.5*(1 - (@{selected|position-r}*0.017453289)**2/2 + (@{selected|position-r}*0.017453289)**4/(4*3*2) - (@{selected|position-r}*0.017453289)**6/(6*5*4*3*2))) rotation|@{selected|position-r} }} It's capturing the rotation, but not the left or top settings.&nbsp; Can TokenMod read computed values like this instead of single numbers...? I will leave it to Aaron to comment on what token-mod can/can't do with regard to the math. However, I will say that part of the update on APILogic I'm working hard to get out this week (as part of this week's git pull) includes giving this sort of inline-calculation ability to other scripts. Whether or not you need to use the if/else functionality, you can use the math ability to do things like *, /, +, -, % operations (including grouping heirarchy), as well as math operations like min, max, abs, sqrt, round (to a number of decimal points), ceiling, floor... it's a lot. But you'll be able to do it natively in other scripts when I release it. So if token-mod can't do it now, it soon will be able to.
timmaugh said: The height of the token is just another get() from the object, so: obj.get('height') You can do it in the above code (with some alterations I'll get to)... but do you need to? The above code fires every time the token moves/rotates. If the height (length) of the vessel is never changing, why would you set it here? Better to set it when the vessel is introduced to the board, I think. If that fits your game and makes sense, we can figure that out. For now, to make it work in the above code, you would need to open up your regex to allow for an attribute of the appropriate name... let's say the attribute is 'beam'. This is what it would look like (updating your code slightly so you don't need the underscore commands -- these are all native to vanilla js). on('ready', () =&gt; { on('change:graphic', (obj, prev) =&gt; { if (['gmlayer', 'objects'].includes(obj.get('layer')) &amp;&amp; (obj.get('left') !== prev.left || obj.get('top') !== prev.top || obj.get('rotation') !== prev.rotation) &amp;&amp; obj.get('represents')) { findObjs({ type: 'attribute', characterid: obj.get('represents') }) .filter(a =&gt; a.get('name').match(/^(position-[xyr]|beam)$/)) .forEach(a =&gt; { switch (a.get('name')) { case 'position-x': a.set({ current: obj.get('left') }); break; case 'position-y': a.set({ current: obj.get('top') }); break; case 'position-r': a.set({ current: obj.get('rotation') }); break; case 'beam': a.set({ current: obj.get('height') }); break; } }); } }); }); That all assume that those attributes exist on the character sheet, of course. If they don't, you can code a "create if they don't exist" catch, but let's see if this gets you where you need to go. Thanks so much -- really appreciate it!&nbsp; To clarify, the heights will actually all be different.&nbsp; I've got dozens of vehicles, all of which have different dimensions.&nbsp; The templates are also different heights, so having the ability for a script to pull the height directly from the token and store it as an attribute will be a huge time saver versus having to find and input the values myself. :) One more question about the code above...&nbsp; Using a chat button macro to pull the template down from the GM layer and set its rotation to match the selected vehicle's doesn't seem to trigger the " on('change:graphic', (obj, prev) " condition, so the template's "position-r" value doesn't get updated to match the new rotation value.&nbsp; This is causing a problem with an occasionally required next step where a second movement template gets placed adjacent to the first (right now it reads the old "position-r" value instead of the template's actual rotation).&nbsp; Is there a different or secondary trigger that could be used to ensure that the "position-r" attribute gets updated when it gets pulled down by a macro? The ability of TokenMod to interpret inline calculations is still my big blocker, but it sounds like this inquiry was timed almost perfectly to an incoming solution;&nbsp; really appreciate the help, timmaugh!
1614915237
timmaugh
Forum Champion
API Scripter
Yep... I understand that the height of the token (the length of the vessel) will be different from one to the next, and how that can be important. My point was more about whether the size of an *individual* vessel was changing every time it moved. I would expect not... that a vehicle stays the same length no matter how it moves. If that is true, you wouldn't have to track the length every time it moved, but only once when it entered the game. Not that it is going to hurt anything to write that same value every time the code runs... but you'll just be overwriting the same value each time. Ok... on to the problem of not triggering the event... If you're not getting that event to fire, you need a slightly more complex structure. What you want is to isolate the behavior you want to produce (tracking the data) in a function that can be called directly as well as from the event as necessary. That would require something more like: // start with a comment for reasons const MoveTemplater = (() =&gt; { &nbsp; &nbsp; const graphicChange = (obj, prev) =&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (['gmlayer', 'objects'].includes(obj.get('layer')) &amp;&amp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (obj.get('left') !== prev.left || obj.get('top') !== prev.top || obj.get('rotation') !== prev.rotation) &amp;&amp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; obj.get('represents')) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; findObjs({ type: 'attribute', characterid: obj.get('represents') }) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .filter(a =&gt; a.get('name').match(/^(position-[xyr]|beam)$/)) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .forEach(a =&gt; { &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; case 'position-x': &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; break; &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; a.set({ current: obj.get('top') }); &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; case 'position-r': &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a.set({ current: obj.get('rotation') }); &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; case 'beam': &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a.set({ current: obj.get('height') }); &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; &nbsp;&nbsp;&nbsp;&nbsp;const handleInput = (msg) =&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (!(msg.type === 'api' &amp;&amp; /^!templater\s+./.test(msg.content))) return; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let obj = getObj('graphic',msg.content.split(/\s+/)[1]); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (obj) graphicChange(obj); &nbsp;&nbsp;&nbsp;&nbsp;}; &nbsp; &nbsp; on('ready', () =&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; on('change:graphic', graphicChange); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;on('chat:message', handleInput); &nbsp; &nbsp; }); &nbsp; &nbsp; return { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; GraphicChange: graphicChange &nbsp; &nbsp; }; })(); That's air-coded, so normal testing caveats apply. The basic idea is that you're still registering the graphic change listener, you're just pointing it at the custom graphicChange() function (which is still doing the same thing as before). You've also exposed the graphicChange() function to other scripts in the return{ } block as GraphicChange(), meaning that another script could call it from MoveTemplater.GraphicChange(token); (you don't have to pass in the `prev` object since you don't ever use it.) If you don't want to code another script to call that function from javascript, I also included the api handle 'templater' to let you trigger the action from the chat (or a macro). You'd enter that followed by a token's ID: !templater @{selected|token_id}
timmaugh said: If you don't want to code another script to call that function from javascript, I also included the api handle 'templater' to let you trigger the action from the chat (or a macro). You'd enter that followed by a token's ID: !templater @{selected|token_id} As I'm sure you've guessed, I don't know thing 1 about javascript, so can't thank you enough for all of the help! Tried using the above API handle to update the position attributes but it crashed out with the following error: TypeError: Cannot read property 'left' of undefined TypeError: Cannot read property 'left' of undefined at graphicChange (apiscript.js:5888:39) at handleInput (apiscript.js:5914:18) at eval (eval at &lt;anonymous&gt; (/home/node/d20-api-server/api.js:154:1), &lt;anonymous&gt;:65:16) at Object.publish (eval at &lt;anonymous&gt; (/home/node/d20-api-server/api.js:154:1), &lt;anonymous&gt;:70:8) at /home/node/d20-api-server/api.js:1663:12 at /home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:560 at hc (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:39:147) at Kd (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:546) at Id.Mb (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:93:489) at Zd.Ld.Mb (/home/node/d20-api-server/node_modules/firebase/lib/firebase-node.js:94:425) Even just where I'm at now (summoning the movement templates to the active vehicle and dismissing them to the GM layer when done) is going to be a massive improvement for my game.&nbsp; If you have time to take another look at the code and suggest a fix, though, I'd be grateful!&nbsp; In the meantime, I'll keep my fingers crossed for implementation of inline calculations in scripts and hope that TokenMod is updated to support that sooner rather than later.
1614920954
timmaugh
Forum Champion
API Scripter
Sorry about that, Jeff... I'd missed that you actually do compare to the prev object, so you'll need something in that second argument position. In the case where you call it directly from the script, you don't actually have a prev state of the object. Sometimes it works to just feed the same object in twice, but looking at your code, it looks like you want to trigger based on differences, so let's try an empty object. Try replacing this line in the handleInput function: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (obj) graphicChange(obj); ...with: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (obj) graphicChange(obj, {});
Hi timmaugh -- the updated code fixed the crash!&nbsp; My only question is on the !templater call syntax.&nbsp;&nbsp; !templater @{selected|token_id} &nbsp;works just fine, but I think in order to make it work in exactly the way I'd like, I'll need it to call the character_id directly.&nbsp; However,&nbsp; !templater&nbsp;-MEsKjvtQ9bBGqFNweNz &nbsp;doesn't update the targeted character_id's position attributes.&nbsp; Is there a different syntax I should be using to pass the character_id to the script?
1615012293
timmaugh
Forum Champion
API Scripter
You can get a number of things off the token, including the character id of the character the token represents... !templater @{selected|character_id} But the script is set up to use that next piece of data in the command line as a token's id. It won't return anything when it looks for a 'graphic' with an id that actually belongs to a character. Are you wanting to provide a character id and have the script locate the representative token, and then go from there? If so, the handleInput function should be something more along the lines of... &nbsp;&nbsp;&nbsp;&nbsp;const handleInput = (msg) =&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (!(msg.type === 'api' &amp;&amp; /^!templater\s+./.test(msg.content))) return; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let obj = getObj('graphic',msg.content.split(/\s+/)[1]) || getObj('character',msg.content.split(/\s+/)[1]); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (!obj) return; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (obj.type === 'character') { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;obj = findObjs({ type: 'graphic', represents: obj.id})[0]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (obj) graphicChange(obj); &nbsp;&nbsp;&nbsp;&nbsp;}; It's late and I'm air-coding, but I think that should get you what you're looking for.
1615038224

Edited 1615039450
timmaugh said: You can get a number of things off the token, including the character id of the character the token represents... !templater @{selected|character_id} But the script is set up to use that next piece of data in the command line as a token's id. It won't return anything when it looks for a 'graphic' with an id that actually belongs to a character. Are you wanting to provide a character id and have the script locate the representative token, and then go from there? If so, the handleInput function should be something more along the lines of... &nbsp;&nbsp;&nbsp;&nbsp;const handleInput = (msg) =&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (!(msg.type === 'api' &amp;&amp; /^!templater\s+./.test(msg.content))) return; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let obj = getObj('graphic',msg.content.split(/\s+/)[1]) || getObj('character',msg.content.split(/\s+/)[1]); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (!obj) return; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (obj.type === 'character') { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;obj = findObjs({ type: 'graphic', represents: obj.id})[0]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (obj) graphicChange(obj); &nbsp;&nbsp;&nbsp;&nbsp;}; It's late and I'm air-coding, but I think that should get you what you're looking for. Understood, yeah, and my apologies for not explaining very well.&nbsp; I'll ultimately need the position attributes to update in both cases:&nbsp; when the character token is moved/changed and (since the code doesn't seem to trigger when the character token is moved/changed with a macro) when the !templater is manually triggered with a macro using the character_id as the identifier.&nbsp; If the snippet you pasted in does that, I should be all set for now.&nbsp; Only thing is that when I use the new snippet to replace the previous handleInput it kicks back with&nbsp; SyntaxError: Unexpected end of input .&nbsp; I'm sure it's a simple fix that I could handle myself if I had an ounce of javascript in me, so I'd appreciate if you could take one last look and let me know how to tweak.&nbsp; Thank you!
1615042478
timmaugh
Forum Champion
API Scripter
So, I do see the last line of that handleInput function should have had the empty object as the second argument it passed... so instead of: if(obj) graphicChange(obj); ...it should be (again)... if(obj) graphicChange(obj, {}); If that still doesn't work, post back the state of the code at this point, and we'll straighten it out! =D
timmaugh said: So, I do see the last line of that handleInput function should have had the empty object as the second argument it passed... so instead of: if(obj) graphicChange(obj); ...it should be (again)... if(obj) graphicChange(obj, {}); If that still doesn't work, post back the state of the code at this point, and we'll straighten it out! =D Thanks for the quick reply -- still giving me the unexpected-end-of-input error, though.&nbsp; It's totally possible (I'd venture to say likely!) that the code is fine but I botched something somewhere, but here's what I've currently got in the script: const MoveTemplater = (() =&gt; { const graphicChange = (obj, prev) =&gt; { if (['gmlayer', 'objects'].includes(obj.get('layer')) &amp;&amp; (obj.get('left') !== prev.left || obj.get('top') !== prev.top || obj.get('rotation') !== prev.rotation) &amp;&amp; obj.get('represents')) { findObjs({ type: 'attribute', characterid: obj.get('represents') }) .filter(a =&gt; a.get('name').match(/^(position-[xyr]|beam)$/)) .forEach(a =&gt; { switch (a.get('name')) { case 'position-x': a.set({ current: obj.get('left') }); break; case 'position-y': a.set({ current: obj.get('top') }); break; case 'position-r': a.set({ current: obj.get('rotation') }); break; case 'beam': a.set({ current: obj.get('height') }); break; } }); } }; const handleInput = (msg) =&gt; { if (!(msg.type === 'api' &amp;&amp; /^!templater\s+./.test(msg.content))) return; let obj = getObj('graphic',msg.content.split(/\s+/)[1]) || getObj('character',msg.content.split(/\s+/)[1]); if (!obj) return; if (obj.type === 'character') { obj = findObjs({ type: 'graphic', represents: obj.id})[0]; } if(obj) graphicChange(obj, {}); };
1615079623
timmaugh
Forum Champion
API Scripter
Yeah, you lost the bottom part of the script... Let me see if I can piece it all together... const MoveTemplater = (() =&gt; { &nbsp; &nbsp; const graphicChange = (obj, prev) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; if (['gmlayer', 'objects'].includes(obj.get('layer')) &amp;&amp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (obj.get('left') !== prev.left || obj.get('top') !== prev.top || obj.get('rotation') !== prev.rotation) &amp;&amp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; obj.get('represents')) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; findObjs({ type: 'attribute', characterid: obj.get('represents') }) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .filter(a =&gt; a.get('name').match(/^(position-[xyr]|beam)$/)) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .forEach(a =&gt; { &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; case 'position-x': &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; break; &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; a.set({ current: obj.get('top') }); &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; case 'position-r': &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a.set({ current: obj.get('rotation') }); &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; case 'beam': &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a.set({ current: obj.get('height') }); &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; const handleInput = (msg) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; if (!(msg.type === 'api' &amp;&amp; /^!templater\s+./.test(msg.content))) return; &nbsp; &nbsp; &nbsp; &nbsp; let obj = getObj('graphic', msg.content.split(/\s+/)[1]) || getObj('character', msg.content.split(/\s+/)[1]); &nbsp; &nbsp; &nbsp; &nbsp; if (!obj) return; &nbsp; &nbsp; &nbsp; &nbsp; if (obj.type === 'character') { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; obj = findObjs({ type: 'graphic', represents: obj.id })[0]; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (obj) graphicChange(obj, {}); &nbsp; &nbsp; }; &nbsp; &nbsp; on('ready', () =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; on('change:graphic', graphicChange); &nbsp; &nbsp; &nbsp; &nbsp; on('chat:message', handleInput); &nbsp; &nbsp; }); &nbsp; &nbsp; return { &nbsp; &nbsp; &nbsp; &nbsp; GraphicChange: graphicChange &nbsp; &nbsp; }; })(); Give that a whirl and see what you get.
Figured it was probably user error. :) Running the above solves the crashing issue, but it still doesn't seem to be accepting the character_id as an argument when running !templater.&nbsp; Again, to eliminate the likelihood that it's just me being dumb, can you confirm that the below syntax is how I should be calling it? !templater -MEsKjvtQ9bBGqFNweNz
1615298616
timmaugh
Forum Champion
API Scripter
Sorry for letting this drop for a couple of days, Jeff. I was getting the APILogic update out. I'll test this and see if I can tell why it isn't working for you. In the meantime, the APILogic update I just released has the inline math calculations you were looking for (so you could use them in token-mod), so from what you've said you might be able to solve your issue that way.
No worries, timmaugh!&nbsp; Completely appreciate that you've got other irons in the fire. :) If there is a solve for the !templater not updating the position attributes, that would be huge for me and will get me a good chunk of the way to the functionality I'm looking for. Out of curiosity, will the APILogic update allow for sin and cos calculations?&nbsp; If so, does theAaron need to update Token-Mod to enable allow for the new calculations, or does that happen automatically?
1615302697
timmaugh
Forum Champion
API Scripter
Yes, sin and cos are available to the processor. In fact, most of the built-in javascript Math library functions are. And Aaron won't have to do anything to token-mod to have this work -- APILogic intercepts the message and does its thing, so by the time token-mod sees the command line, APILogic will have parsed the math down to a simple value. For instance, if you drop this into a token-mod line: {&amp; math round(sin(20),2)} Then by the time token-mod sees the command line, the above will have been replace with&nbsp; 0.91 . Here is a link to the MATH tag instructions ... and here is a link to the math functions available to you. Let me know if you have any questions!
timmaugh said: Yes, sin and cos are available to the processor. In fact, most of the built-in javascript Math library functions are. And Aaron won't have to do anything to token-mod to have this work -- APILogic intercepts the message and does its thing, so by the time token-mod sees the command line, APILogic will have parsed the math down to a simple value. For instance, if you drop this into a token-mod line: {&amp; math round(sin(20),2)} Then by the time token-mod sees the command line, the above will have been replace with&nbsp; 0.91 . Here is a link to the MATH tag instructions ... and here is a link to the math functions available to you. Let me know if you have any questions! Fantastic --&nbsp; thanks for confirming! In the meantime, while I dive into the equations, can you confirm if the below syntax on the !templater call is correct? !templater -MEsKjvtQ9bBGqFNweNz The script you provided works perfectly for updating a token's position attributes if a player drags or rotates it, but I haven't managed to get them to update by calling !templater + character_id (or with !templater @{selected|character_id}) in a macro or chat window input.
1615348499

Edited 1615348568
timmaugh
Forum Champion
API Scripter
OK, I had a chance to test it and worked out the bugs. The new version, below, has a few enhancements... 1) it will answer a graphic change event 2) it will answer a call to&nbsp; !templater &nbsp;with either a token id or a character id: !templater @{selected|token_id} !templater @{selected|character_id} provided that the supplied token id refers to a token that represents a character, or provided that the character has a token on the board that represents them (if you're getting it from the @{selected|character_id} shortcut, you know you're fine; this is more a warning for if you want to supply a character's id directly and they don't yet have a token in play). 3) It will create these attributes if they don't exist: position-x position-y position-r position-beam so now you won't have to go through the tedium of creating the attributes every time you add a character (if, in fact, you had to do that before). In fact, if you use this with SelectManager and run the forselected process, you can quickly baseline your ENTIRE set of tokens, creating the attributes if they don't exist, and then tracking the information for each one to their respective character sheets. First, select all of the tokens you want to baseline, then send the following command to chat: !forselected templater @{selected|token_id} And you're done. Here is the code: const MoveTemplater = (() =&gt; { &nbsp; &nbsp; const graphicChange = (obj, prev) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; if (['gmlayer', 'objects'].includes(obj.get('layer')) &amp;&amp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (obj.get('left') !== prev.left || obj.get('top') !== prev.top || obj.get('rotation') !== prev.rotation) &amp;&amp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; obj.get('represents')) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; trackPoints(obj); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }; &nbsp; &nbsp; const trackPoints = (o) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; if (!o || !o.get('represents')) return; &nbsp; &nbsp; &nbsp; &nbsp; let attrs = findObjs({ type: 'attribute', characterid: o.get('represents') }) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .filter(a =&gt; a.get('name').match(/^position-([xyr]|beam)$/)); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (attrs.length &lt; 4) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ['position-x', 'position-y', 'position-r', 'position-beam'] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .filter(n =&gt; !attrs.map(a =&gt; a.get('name')).includes(n)) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .forEach(n =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; attrs.push(createObj('attribute',{ name: n, characterid: o.get('represents') })); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; attrs.forEach(a =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; switch (a.get('name')) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'position-x': &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a.set({ current: o.get('left') }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'position-y': &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a.set({ current: o.get('top') }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'position-r': &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a.set({ current: o.get('rotation') }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'position-beam': &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a.set({ current: o.get('height') }); &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; const handleInput = (msg) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; if (!(msg.type === 'api' &amp;&amp; /^!templater\s+./.test(msg.content))) return; &nbsp; &nbsp; &nbsp; &nbsp; let tempid = msg.content.split(/\s+/)[1], &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; obj = getObj('graphic', tempid) || getObj('character', tempid); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (obj.get('type') === 'character') { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; trackPoints(findObjs({ type: 'graphic', represents: obj.id })[0]); &nbsp; &nbsp; &nbsp; &nbsp; } else if (obj.get('type') === 'graphic') { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; trackPoints(obj); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }; &nbsp; &nbsp; on('ready', () =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; on('change:graphic', graphicChange); &nbsp; &nbsp; &nbsp; &nbsp; on('chat:message', handleInput); &nbsp; &nbsp; }); &nbsp; &nbsp; return { &nbsp; &nbsp; &nbsp; &nbsp; TrackPoints: trackPoints &nbsp; &nbsp; }; })();
Amazing -- this is a huge help!&nbsp; This new code does exactly what I need it to do, and the added bonus of having it generate the attributes if they aren't already there will be a big time-saver.&nbsp; I really appreciate the time you put into helping me out (and my players will certainly appreciate as well!).&nbsp; Thanks so much!
1615352935
timmaugh
Forum Champion
API Scripter
Not a problem! Post back if you run into difficulties!
Not sure if this qualifies as a difficulty so much as it is--... unexpected behavior?&nbsp; I've found that after a character's position attributes are updated by the script (both by manually running !templater as well as on change), I can no longer edit that character's ability macros unless I reload the page.&nbsp; (This is a new quirk with the updated script that I hadn't seen with the previous version.) I can open up the Attributes and Abilities tab of the character sheet and can see the Abilities, but clicking on the pencil to edit does nothing.&nbsp; I can add *new* Abilities, and (curiously) I can open/edit those new Abilities, but I can't edit anything that existed prior to the position updater script being run. As I mentioned, it's not a *problem* so to speak, as unless something goes awry I shouldn't need to edit macros during the games, and even if I do the issue can be corrected by reloading the page, but it seemed odd so wanted to bring it to your attention.
1615394898
timmaugh
Forum Champion
API Scripter
Fortunately (or unfortunately?) this isn't the script... I think it's due to the character sheet changes Roll20 rolled out. Here is a forum post reporting that problem: <a href="https://app.roll20.net/forum/permalink/9885076/" rel="nofollow">https://app.roll20.net/forum/permalink/9885076/</a>
timmaugh said: Fortunately (or unfortunately?) this isn't the script... I think it's due to the character sheet changes Roll20 rolled out. Here is a forum post reporting that problem: <a href="https://app.roll20.net/forum/permalink/9885076/" rel="nofollow">https://app.roll20.net/forum/permalink/9885076/</a> Oh -- interesting!&nbsp; Thanks for the info, and a valuable reminder that correlation is not causation. :D
1615436552

Edited 1615438046
This is almost certainly another instance of user error, but I'm having an issue where !templater is working just fine for half of my templates (all the straights and right turns);&nbsp; it correctly updates their position attributes with the correct X/Y/R values.&nbsp; For all the left turns, though, the !templater is setting the position attributes to weird values (currently x:1107, y:589, r:0) which, as far as I can tell, doesn't correspond to any existing character or token, or previous location, or anything else that I can identify.&nbsp; If I manually move the token, the on-change function sets the values correctly, but as soon as I run the !templater, it resets them to those apparently unrelated values. My thought was that there was another instance of the same character somewhere on a different map and it was assigning *that* character's X/Y/R values to the position attributes instead of the one I intended, but I've been through every map multiple times and deleted every character and token that could possibly be conflicting and I'm still encountering the issue.&nbsp; With that seemingly ruled out, I'm at a loss as to what could be causing this.&nbsp; Apologies for having to ask for more help, but do you have any ideas that could point me towards a solution?
1615474128
timmaugh
Forum Champion
API Scripter
hmm... I can try more testing later. I can probably add a debug output to validate the token data you're collecting is the correct token's data. But, can you be more specific about the problem? For instance, start with a token and list the baseline information (x/y/r), then turn right and relate the same. Then turn left. Then move straight. MOVE&nbsp; &nbsp; &nbsp; |&nbsp; &nbsp; &nbsp; X &nbsp; &nbsp; &nbsp;|&nbsp; &nbsp; &nbsp;Y &nbsp; &nbsp; &nbsp;|&nbsp; R ===========|====================|===================|========== Base&nbsp; &nbsp; &nbsp; | 1090.2620398460451 | 717.1991098515416 | 480 R Turn | 1090.2620398460451 | 717.1991098515416 | 540 L Turn | 1090.2620398460451 | 717.1991098515416 | 450 Straight* | 811.9215298845339 | 717.1991098515416 | 450 *straight-ish - dragged by hand Those were all generated by dragging the token around and match exactly what I would expect (tracking the same token through movements). In fact, all of those measurements were created by manipulating the token directly. How are you implementing it differently to that, and can you share a table of similar results?
Sure thing! Here's the the data for the Left Gentle turn template: MOVE&nbsp; &nbsp; &nbsp; |&nbsp; &nbsp; &nbsp; X &nbsp; &nbsp;|&nbsp; &nbsp; &nbsp;Y &nbsp; &nbsp; &nbsp;|&nbsp; R ===========|==================|====================|=================== Base&nbsp; &nbsp; &nbsp; | 2144.28714227964 | 1479.0207144198594 | 88.63453408557456 R Turn | 2144.28714227964 | 1479.0207144198594 | 180.32450143606812 L Turn | 2144.28714227964 | 1479.0207144198594 | 0.20154059508963873 Straight* | 2348.28714227964 | 1475.0207144198594 | 89.31062328193975 Manual manipulation of the token's position/rotation does seem to store the correct values to the character/token attributes in all cases, from what I can tell.&nbsp; However, running the !templater to update the position attributes after it has been moved by a macro only works for the straight and right-turn templates (and for the actual vehicles as well).&nbsp; For the left turn templates (and only the left turn templates), it still assigns the below values, regardless of where the token is actually located on the map: MOVE&nbsp; &nbsp; &nbsp; |&nbsp; &nbsp; &nbsp; X &nbsp; &nbsp;|&nbsp; &nbsp; &nbsp;Y &nbsp; &nbsp; &nbsp;|&nbsp; R ===========|==================|====================|=================== Base&nbsp; &nbsp; &nbsp; | 1107 | 589 | 0 Any manual manipulation of the token after running !templater results in the correct position values being stored.&nbsp; For example, after manually rotating the template by a pixel it sets back to: MOVE&nbsp; &nbsp; &nbsp; |&nbsp; &nbsp; &nbsp; X &nbsp; &nbsp;|&nbsp; &nbsp; &nbsp;Y &nbsp; &nbsp; &nbsp;|&nbsp; R ===========|==================|====================|=================== Base&nbsp; &nbsp; &nbsp; | 2144.28714227964 | 1479.0207144198594 | 88.63453408557456 It occurred to me last night that all of my left turn templates had been generated by horizontally flipping the right turn images and then saving those as the default tokens for the left turn journal entities, and I noticed that if I right-click the template token and open the Advanced context menu, "Flip Horizontal" is still highlighted.&nbsp; Given that the problem only seems to be affecting the left turns I thought maybe this could be related the problem;&nbsp; however, even after generating a new Gentle Left template offline and uploading the new (unflipped) image to use as the default token it still has the same issue, so I'm still at a loss. Just in case there's a more fundamental error in my thinking that could help get to a solution, I'm going to step through the entire process here: -&nbsp; Player selects the vehicle they wish to move and clicks the "Activate" token ability.&nbsp; "Activate" runs !templater @{selected|token_id} and calls up a set of chat buttons; -&nbsp; Player clicks the "Call Template" chat button, which populates another set of chat buttons from which the player can choose their movement template (until called, the templates live on the GM layer).&nbsp; "Call Template" then runs the below: !token-mod {{ --current-page --ids -MEsMka-Izx4L71Ik8vz --ignore-selected --set layer|objects left|@{selected|position-x} top|@{selected|position-y} rotation|@{selected|position-r} --move [[(120+220.61)/29.22]]ft }} !templater -MEsMka-Izx4L71Ik8vz In the above, "selected" is still the vehicle, and " -MEsMka-Izx4L71Ik8vz " references the character_id of the template (not the token_id -- I want to make sure if a token is accidentally deleted that I don't have to update the macro when I spawn a new instance of the template).&nbsp; In theory, this should set the template to the same position and rotation as the active vehicle and then place it in front of the vehicle, then update the template's position-x, position-y, and position-r attributes to match its new location.&nbsp; (for the record, I realize the value in&nbsp; --move &nbsp;is not very clean, but I'm still experimenting with whether the movement should be based on the vehicle's beam or if it should just be a fixed value, so leaving it as-is for now...); -&nbsp; Depending on the player's roll, it is sometimes necessary to place a secondary "Slide" template next to the original template.&nbsp; To do so, the player selects the just-placed movement template and clicks the "Slide" token ability, which runs the below: !token-mod {{ --current-page --ids -MEsMTevagejnfLTJ7bP --ignore-selected --set layer|objects left|@{selected|position-x} top|@{selected|position-y} rotation|@{selected|position-r} --move [[(-1)/14.61]]ft --move +75!|[[(85)/14.61]]ft }} !templater -MEsMTevagejnfLTJ7bP In this case, "selected" is the just-placed movement template, and&nbsp; -MEsMTevagejnfLTJ7bP &nbsp;is the character_id of the Slide template. -&nbsp; In either case, the player will then select the template that will be used to place the vehicle (either the original movement template or the Slide template) and click the "Move" token ability, which will move the (targeted) vehicle to a new position relative to the selected template: !token-mod {{ --current-page --ignore-selected --ids @{target|token_id} --move [[(100+75)/14.61]]ft --move -30!|[[(100+60)/14.61]]ft }} (in all these placement and movement macros, the actual values for the movement and rotation will vary depending on the template, but are otherwise identical); -&nbsp; Finally, the player will select the movement template(s) and click the "Dismiss" token ability, which will kick it back to the GM layer; All of these steps seem to be working as intended save for the !templater calls, and even then they're only not working for my left turns, which is so confusing.
1615485993
The Aaron
Roll20 Production Team
API Scripter
Jeff H. said: Hi All, Running a Gaslands game, and I'm trying to streamline play and cut down on the fiddliness of the templates by automating the placement as much as possible.&nbsp; I found the link below: <a href="https://app.roll20.net/forum/post/9598932/dropping-graphic-adjacent-to-selected-char-token-matching-rotation/?pageforid=9598968#post-9598968" rel="nofollow">https://app.roll20.net/forum/post/9598932/dropping-graphic-adjacent-to-selected-char-token-matching-rotation/?pageforid=9598968#post-9598968</a> ...which seems like a great solution, but one that only works when all of the tokens are identical in size.&nbsp; Unfortunately, in the game I'm running, we've got a wide variety of vehicles of different sizes, so I don't know that this solution will work for me.&nbsp;&nbsp; If that script handled tokens of various sizes, would that solve your problem?
If that script handled tokens of various sizes, would that solve your problem? I think at this point I've got a good solution in place for actually placing and orienting the template;&nbsp; my only remaining hitch is the weird issue where the script that timmaugh provided to write a token's position and rotation values to its attributes works perfectly for everything except my left-turn movement templates;&nbsp; for some reason it instead writes a weirdly specific set of coordinates that (so far as I can tell) doesn't correspond to the position of anything on the game map.
1615489219

Edited 1615489249
timmaugh
Forum Champion
API Scripter
Alright, Jeff, let's try this... The only difference between directly manipulating the token and you calling the templater handle is that you're providing the character's id and based on that the script is attempting to locate the right token to represent that character. I didn't have it limited to only the current page for the player, so maybe that was the issue? I've absconded with a bit of The Aaron's code (shhh... don't tell him; pretty sure he won't notice... just act cool... act cool!.... I SAID COOL...) *sigh* I can't do it: Aaron, I stole your code. And grafted it onto what I had. I may, or may not, have made bootleg copies and sold them out of the trunk of my car. I'm sorry. Ok. Whew! I feel better, now. Here's the new code to try: const MoveTemplater = (() =&gt; { &nbsp; &nbsp; const graphicChange = (obj, prev) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; if (['gmlayer', 'objects'].includes(obj.get('layer')) &amp;&amp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (obj.get('left') !== prev.left || obj.get('top') !== prev.top || obj.get('rotation') !== prev.rotation) &amp;&amp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; obj.get('represents')) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; trackPoints(obj); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }; &nbsp; &nbsp; const trackPoints = (o) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; if (!o || !o.get('represents')) return; &nbsp; &nbsp; &nbsp; &nbsp; let attrs = findObjs({ type: 'attribute', characterid: o.get('represents') }) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .filter(a =&gt; a.get('name').match(/^position-([xyr]|beam)$/)); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (attrs.length &lt; 4) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ['position-x', 'position-y', 'position-r', 'position-beam'] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .filter(n =&gt; !attrs.map(a =&gt; a.get('name')).includes(n)) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .forEach(n =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; attrs.push(createObj('attribute',{ name: n, characterid: o.get('represents') })); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; attrs.forEach(a =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; switch (a.get('name')) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'position-x': &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a.set({ current: o.get('left') }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'position-y': &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a.set({ current: o.get('top') }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'position-r': &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a.set({ current: o.get('rotation') }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'position-beam': &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a.set({ current: o.get('height') }); &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; const getPageForPlayer = (playerid) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; let player = getObj('player',playerid); &nbsp; &nbsp; &nbsp; &nbsp; if(playerIsGM(playerid)){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return player.get('lastpage') || Campaign().get('playerpageid'); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let psp = Campaign().get('playerspecificpages'); &nbsp; &nbsp; &nbsp; &nbsp; if(psp[playerid]){ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return psp[playerid]; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return Campaign().get('playerpageid'); &nbsp; &nbsp; }; &nbsp; &nbsp; const handleInput = (msg) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; if (!(msg.type === 'api' &amp;&amp; /^!templater\s+./.test(msg.content))) return; &nbsp; &nbsp; &nbsp; &nbsp; let tempid = msg.content.split(/\s+/)[1], &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; obj = getObj('graphic', tempid) || getObj('character', tempid); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (obj.get('type') === 'character') { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; trackPoints(findObjs({ type: 'graphic', represents: obj.id, pageid: getPageForPlayer(msg.playerid) })[0]); &nbsp; &nbsp; &nbsp; &nbsp; } else if (obj.get('type') === 'graphic') { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; trackPoints(obj); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }; &nbsp; &nbsp; on('ready', () =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; on('change:graphic', graphicChange); &nbsp; &nbsp; &nbsp; &nbsp; on('chat:message', handleInput); &nbsp; &nbsp; }); &nbsp; &nbsp; return { &nbsp; &nbsp; &nbsp; &nbsp; TrackPoints: trackPoints &nbsp; &nbsp; }; })(); If that doesn't work, maybe PM me an invite to the game and promote me to GM. I can poke around and see what else might be going on.
Knocking on wood, and I'll do more testing tonight, but that tweak seems to have solved it!&nbsp; It's so strange -- that makes me think that there's got to be another instance of something on another map at those specific coordinates that the !templater was pulling the position from, but for the life of me I can't figure our what.&nbsp; I made a little "summoner" character that I could place into a page and draw any templates on the map to its position and came up empty. Thanks again for your help (and to TheAaron for Token-Mod as well as the other bit of script that helped to solve this problem)!&nbsp; This whole template auto-placement and auto-move is going to be a huuuuuge QoL upgrade for my campaign.