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

OnMyTurn API

Hi, I use the OnMyTurn API a lot which is really good for flagging things up when a tokens initiative turn starts. I'm wondering if there's something similar (EndMyTurn ??) for the end of a tokens turn so for example a condition that is in place until the end of the tokens turn can automatically be removed  Thanks
1713966516
timmaugh
Pro
API Scripter
I don't know of one, though I can't imagine it would be super difficult to build. I would wonder about the possibility of building a framework within your game that could handle it... the basic concept would look like this: 1) Create a centralized "OnMyTurnController" ability on a library character that you mark as being able to be controlled by "all" players, but don't put it in their journals 2) For each character that currently has an OnMyTurn ability, rename that ability to be "LocalOnMyTurn" 3) For each character that now has a "LocalOnMyTurn" ability, create a new ability named "OnMyTurn" that runs the centralized "OnMyTurnController" ability on the library character: %{LibraryCharacter|OnMyTurnController} 4) For any character that might need an end-of-turn ability, add it to their sheet. Standardize the naming so that they're all named "LocalEndMyTurn" 5) Edit the LibraryCharacter's "OnMyTurnController" ability to read something like: !{{   The previous token was @(tracker-1.token_name).   %(tracker-1.LocalEndMyTurn)   %(tracker.LocalOnMyTurn) }} You can remove the first command in that batch (announcing the previous token's name)... I just included it as a proof of concept. Here is the result when I mock this up, when I advanced the Turn Tracker from Igtharian's turn to Kokoro's turn: Caveat and Next Step The way I included the calls to the LocalEndMyTurn and LocalOnMyTurn abilities in the "OnMyTurnController" ability, they will be sent as basic chat messages (skipping the API), meaning that you couldn't rely on something like TokenMod to remove a token marker. We can fix this. I just wanted to show a clean proof of concept so as to not confuse the idea, above. To fix this issue, we're going to make a couple of changes. 1) First, in the "OnMyTurnController" ability, make both of the tracker lines begin with exclamation points. This will clue ZeroFrame (the script providing the batching ability -- enclosing everything in the double braces) that these lines should not automatically be flattened to standard chat messages. 2) Also in this message, add a "batch deferral" character just inside the opening double brace. The character you choose should be enclosed in parentheses. Note: you must choose a character that will be, for any of the "LocalOnMyTurn" or "LocalEndMyTurn" abilities, uniquely dedicated to this job; otherwise this has the potential to break those abilities. In other words, if I choose the caret character, then in any of the local abilities in this game the caret should NOT be used for any other job. It should only be used for escaping constructions (you'll see how that works in a second). The final result of my OnMyTurnController looks like this: !{{(^)   !%(tracker-1.LocalEndMyTurn)   !%(tracker.LocalOnMyTurn) }} 3) Edit each LocalOnMyTurn and each LocalEndMyTurn ability. Judge whether it is a call to a script, or a message intended for the chat output. For those intended to be output to the chat panel, include this text: {^&simple} Note I used the same caret character as I declared as my deferral character in the controller ability. That is important, because ZeroFrame will now remove that caret at the appropriate time and let any message marked with this construction to output to the chat (instead of continuing on to scripts). To put a fine point on it, here was Kokoro's LocalOnMyTurn prior to the change: This is from Kokoro's LocalOnMyTurn And the same ability after adding the simple tag: This is from Kokoro's LocalOnMyTurn{^&simple} And the result of advancing the Turn Tracker again:
1713966936
timmaugh
Pro
API Scripter
Required Scripts I should have said, the required scripts for this approach would be the MetaScriptToolbox and OnMyTurn . Oh, I would probably stay away from a multi-line LocalOnMyTurn or LocalEndMyTurn, as those might not play well with the ZeroFrame batching in the OnMyTurnController.
1714024270

Edited 1714047675
The Aaron
Roll20 Production Team
API Scripter
Here's a completely untested version of the script which adds `AfterMyTurn` as a possible macro or ability: on('ready', () => { const playerCanControl = (obj, playerid='any') => { const playerInControlledByList = (list, playerid) => list.includes('all') || list.includes(playerid) || ('any'===playerid && list.length); let players = obj.get('controlledby') .split(/,/) .filter(s=>s.length); if(playerInControlledByList(players,playerid)){ return true; } if('' !== obj.get('represents') ) { players = (getObj('character',obj.get('represents')) || {get: function(){return '';} } ) .get('controlledby').split(/,/) .filter(s=>s.length); return playerInControlledByList(players,playerid); } return false; }; const resolver = (token,character) => (text) => { const attrRegExp = /@{(?:([^|}]*)|(?:(selected)|(target)(?:\|([^|}]*))?)\|([^|}]*))(?:\|(max|current))?}/gm; const attrResolver = (full, name, selected, target, label, name2, type) => { let simpleToken = JSON.parse(JSON.stringify(token)); let charName = character.get('name'); type = ['current','max'].includes(type) ? type : 'current'; const getAttr = (n, t) => ( findObjs({type: 'attribute', name:n, characterid: character.id})[0] || {get:()=>getAttrByName(character.id,n,t)} ).get(t); const getFromChar = (n,t) => { if('name'===n){ return charName; } return getAttr(n,t); }; const getProp = (n, t) => { switch(n){ case 'token_name': return simpleToken.name; case 'token_id': return simpleToken._id; case 'character_name': return charName; case 'bar1': case 'bar2': case 'bar3': return simpleToken[`${n}_${'max'===t ? 'max' : 'value'}`]; } return getFromChar(n,t); }; if(name){ return getFromChar(name,type); } return getProp(name2,type); }; return text.replace(attrRegExp, attrResolver); }; const runCommandsForToken = (token,commandName="OnMyTurn") => { if(token && token.get('represents')){ let character = getObj('character',token.get('represents')); let ability = findObjs({ name: commandName, type: 'ability', characterid: character.id }, {caseinsensitive: true})[0]; let TCRes = resolver(token,character); if(ability){ let content = TCRes(ability.get('action')).replace(/\[\[\s+/g,'[['); try { sendChat(character.get('name'),content); } catch(e){ log(`${commandName}: ERROR PARSING: ${content}`); log(`${commandName}: ERROR: ${e}`); } } // look for a macro findObjs({ type: 'macro', name: commandName }, {caseinsensitive: true}) .forEach(macro=>{ // if gm macro, always run. Otherwise, run if the token is controlled by the player if(playerIsGM(macro.get('playerid')) || playerCanControl(character, macro.get('playerid'))){ let content = TCRes(macro.get('action')).replace(/\[\[\s+/g,'[['); try { sendChat(character.get('name'),content); } catch(e){ log(`${commandName}: ERROR PARSING: ${content}`); log(`${commandName}: ERROR: ${e}`); } } }); } }; const checkOnMyTurn = (obj,prev) => { let to=JSON.parse(obj.get('turnorder')||'[]'); let toPrev=JSON.parse(prev.turnorder||'[]'); if(toPrev.length && toPrev[0].id!=='-1' && toPrev[0].id !== (to[0]||{}).id){ let token = getObj('graphic',toPrev[0].id); runCommandsForToken(token,'AfterMyTurn'); } if(to.length && to[0].id!=='-1' && to[0].id !== (toPrev[0]||{}).id){ let token = getObj('graphic',to[0].id); runCommandsForToken(token,'OnMyTurn'); } }; on('chat:message', (msg)=>{ if('api'===msg.type && /^!omt\b/i.test(msg.content) && playerIsGM(msg.playerid)){ checkOnMyTurn(Campaign(),{turnorder:JSON.stringify([{id:-1}])}); } }); on( 'change:campaign:turnorder', (obj,prev)=>setTimeout(()=>checkOnMyTurn(Campaign(),prev),1000) ); on('chat:message', (msg) => { if('api'===msg.type && /^!eot\b/.test(msg.content)){ setTimeout(()=>checkOnMyTurn(Campaign(),{turnorder:JSON.stringify([{id:-1}])}),1000); } }); });
Guys, Many thanks to both of you. I'll give the script option a go tonight and will report back.
The Aaron, The script works but has an odd side effect. The OnMyTurn script has always duplicated any text in chat so any output comes through twice. I've often meant to post about this to see if there is a way to stop this but it's not a major issue. With the AfterMyTurn script active I know have chat output occur 4 times for both the start of turn and end of turn. If I deactivate the script it goes back to a double output. Is there a way to change this ?
1714619655
The Aaron
Roll20 Production Team
API Scripter
What other scripts do you have installed?  Can you post a screenshot of the quadrupled output? 
The Aaron, Apologies for the delay. Screen Shot of Scripts Running I use scriptcards to whisper to GM and relvant player Screen Shot of GM (left) and Player (right) text output with After My Turn active   Screen Shot of GM (left) and Player (right) text output without After My Turn active I think from the GM view it's probably right to see the output twice as this is whispered to the GM and the Player but from the player view I think we should only see 1 whispered output Hopefully make sense. Not the end of the world if I can't get After my Turn working
1714929083
The Aaron
Roll20 Production Team
API Scripter
Hmm. Do you still get the duplicate output of your run: !omt While Torbin has the top turn?
1714929234
The Aaron
Roll20 Production Team
API Scripter
Oh!  You have OnMyTurn and AfterMyTurn in scripts, but they aren't separate, AfterMyTurn includes all the functionality of OnMyTurn, with the addition of doing AfterMyTurn abilities. That's what you get 4 now. That's doesn't explain the doubling though. Disable OnMyTurn to start with. 
The Aaron, Have disabled OnMyTurn so only have AfterMyTurn active. GM and player still get 2 duplicate messages to chat when advancing through the turn order. When entering !omt when any token is at the top of the turn order only 1 message gets displayed
1714942647
The Aaron
Roll20 Production Team
API Scripter
Ok, interesting!  Probably I can fix that by debouncing the execution of the check.  Try this version? on('ready', () => { const playerCanControl = (obj, playerid='any') => { const playerInControlledByList = (list, playerid) => list.includes('all') || list.includes(playerid) || ('any'===playerid && list.length); let players = obj.get('controlledby') .split(/,/) .filter(s=>s.length); if(playerInControlledByList(players,playerid)){ return true; } if('' !== obj.get('represents') ) { players = (getObj('character',obj.get('represents')) || {get: function(){return '';} } ) .get('controlledby').split(/,/) .filter(s=>s.length); return playerInControlledByList(players,playerid); } return false; }; const resolver = (token,character) => (text) => { const attrRegExp = /@{(?:([^|}]*)|(?:(selected)|(target)(?:\|([^|}]*))?)\|([^|}]*))(?:\|(max|current))?}/gm; const attrResolver = (full, name, selected, target, label, name2, type) => { let simpleToken = JSON.parse(JSON.stringify(token)); let charName = character.get('name'); type = ['current','max'].includes(type) ? type : 'current'; const getAttr = (n, t) => ( findObjs({type: 'attribute', name:n, characterid: character.id})[0] || {get:()=>getAttrByName(character.id,n,t)} ).get(t); const getFromChar = (n,t) => { if('name'===n){ return charName; } return getAttr(n,t); }; const getProp = (n, t) => { switch(n){ case 'token_name': return simpleToken.name; case 'token_id': return simpleToken._id; case 'character_name': return charName; case 'bar1': case 'bar2': case 'bar3': return simpleToken[`${n}_${'max'===t ? 'max' : 'value'}`]; } return getFromChar(n,t); }; if(name){ return getFromChar(name,type); } return getProp(name2,type); }; return text.replace(attrRegExp, attrResolver); }; const runCommandsForToken = (token,commandName="OnMyTurn") => { if(token && token.get('represents')){ let character = getObj('character',token.get('represents')); let ability = findObjs({ name: commandName, type: 'ability', characterid: character.id }, {caseinsensitive: true})[0]; let TCRes = resolver(token,character); if(ability){ let content = TCRes(ability.get('action')).replace(/\[\[\s+/g,'[['); try { sendChat(character.get('name'),content); } catch(e){ log(`${commandName}: ERROR PARSING: ${content}`); log(`${commandName}: ERROR: ${e}`); } } // look for a macro findObjs({ type: 'macro', name: commandName }, {caseinsensitive: true}) .forEach(macro=>{ // if gm macro, always run. Otherwise, run if the token is controlled by the player if(playerIsGM(macro.get('playerid')) || playerCanControl(character, macro.get('playerid'))){ let content = TCRes(macro.get('action')).replace(/\[\[\s+/g,'[['); try { sendChat(character.get('name'),content); } catch(e){ log(`${commandName}: ERROR PARSING: ${content}`); log(`${commandName}: ERROR: ${e}`); } } }); } }; const debounce = (f,d=300) => { let handle; return (...a) => { if(!handle) { handle = setTimeout(()=>{ f(...a); handle=undefined; },d); } }; }; const checkOnMyTurn = debounce((obj,prev) => { let to=JSON.parse(obj.get('turnorder')||'[]'); let toPrev=JSON.parse(prev.turnorder||'[]'); if(toPrev.length && toPrev[0].id!=='-1' && toPrev[0].id !== (to[0]||{}).id){ let token = getObj('graphic',toPrev[0].id); runCommandsForToken(token,'AfterMyTurn'); } if(to.length && to[0].id!=='-1' && to[0].id !== (toPrev[0]||{}).id){ let token = getObj('graphic',to[0].id); runCommandsForToken(token,'OnMyTurn'); } }); on('chat:message', (msg)=>{ if('api'===msg.type && /^!omt\b/i.test(msg.content) && playerIsGM(msg.playerid)){ checkOnMyTurn(Campaign(),{turnorder:JSON.stringify([{id:-1}])}); } }); on( 'change:campaign:turnorder', (obj,prev)=>setTimeout(()=>checkOnMyTurn(Campaign(),prev),1000) ); on('chat:message', (msg) => { if('api'===msg.type && /^!eot\b/.test(msg.content)){ setTimeout(()=>checkOnMyTurn(Campaign(),{turnorder:JSON.stringify([{id:-1}])}),1000); } }); });
The Aaron, works perfectly now. Many thanks and really appreciate you taking the time out to resolve this
1715004358
The Aaron
Roll20 Production Team
API Scripter
Happy to help!  Glad that's working for you!