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

Initiative Macro Help: Creating Tracker Entry for Different Token

I'm setting up token actions for NPCs and I'm trying to see if something is possible. In the system I'm running (Onyx Path's Storypath system) characters have individual initiative rolls, but instead of creating entries in the turn order for each character a slot is created for their 'side' at the rolled initiative. Previously I did this manually by having a character token for each side, then i'd add multiple turns for the tokens and manually enter the roll results. So a turn order would look something like... Good Guys 4 Good Guys 3 Bad Guys 2 Bad Guys 2 Good Guys 1 Bad Guys 0 End of Round I know with macros I can use &{tracker} to create an entry in the tracker, but I'm wondering if there is a way to tell it which character to create an entry for. Ideally I'd have 'Mook 1' token selected, hit my 'Initiative' token action, and then an entry for a separate 'Opposition' token would be added to the turn tracker with the result of the roll I made for Mook 1. I've got the dice roll logic all sorted out, it's just the sending it to the tracker I can't solve.
1695348496
timmaugh
Forum Champion
API Scripter
If you use a script (like AddCustomTurn) you should be able to do what you want. In fact, you could script up all of the commands for both sides, as many as you want, and have it create them in one go. I would suggest doing that within a ZeroFrame batch so as to not have it drop lines: !{{   !act -1 3 --Good Guys --delete-on-zero   !act -1 3 --Bad Guys --delete-on-zero   !act -1 2 --Good Guys  --delete-on-zero   !act -1 2 --Bad Guys  --delete-on-zero   !act -1 1 --Good Guys  --delete-on-zero   !act -1 1 --Bad Guys  --delete-on-zero }} Then, if you really wanted to get crazy with the meta sauce, you could include Muler and give yourself a loop that would run X number of times -- which you could supply with a query. If you want an example of that, post back and I'll work one up.
1695349572
Andrew R.
Pro
Sheet Author
You can also do it with ScriptCards which can modify the Turn Tracker itself. I posted a sample for 13th Age in the ScriptCards thread. 
Thanks for both answers! I'm working through the AddCustomTurn first since it seemed a little easier, but I do see exactly the command I want on the ScriptCard wiki so that will be my next stop. I got this version working, where the initiative count is stored in an attribute. !act 0 @{initToTurnOrder} --Opposition That correctly creates a new slot set to the value stored in the attribute, and the turn value doesn't change. Problem is I'm having an issue with getting the attribute to update. Is is possible to instead just put the dice roll that sets that turn order value inside the macro? I tried a few versions and couldn't get it to work. The dice roll looks like this @{secondary_pool}d10!10}>@{defaultTargetNumber} I've tried a few different times and I'm sure I'm just making some rookie formatting mistake, in my defense I did start learning this morning though.
1695380889

Edited 1695385122
timmaugh
Forum Champion
API Scripter
You should be able to enclose that in double brackets to create an inline roll, then use that in the place of the attribute: !act 0 [[ @{secondary_pool}d10!10}>@{defaultTargetNumber}]]  --Opposition Now if you want to use the same value for 2 entries (good guys and bad guys), you'll have to use the reuse rolls trick and refer to the correct roll marker index. However, you also need a second line for AddCustomTurn, which makes it (usually) impossible to refer to the same roll (every line is it's own command with its own set of inline rolls). That's where ZeroFrame batching can help again: !{{   !act 0 [[@{secondary_pool}d10!10}>@{defaultTargetNumber}]] --Good Guys    !act 0 $[[0 ]]  --Opposition }} That let's you refer to the same inline roll across multiple messages. The first roll is $[[0]], the next $[[1]], etc. It can get harder if you need to refer to the same roll to drive one set of commands, then another at the same value - 1, then at the value - 2, etc., for as long as the new value stays above 0. For that kind of conditional evaluation, you'll want either ScriptCards or a couple more metascripts (in addition to ZeroFrame) ... APILogic and Muler. I'm not as confident that I understand if the decrementing rolls are what you're looking for (for each side until you hit 0), but if it is post back and I'll show what that looks like.
Thank you! That got it working (and i figured out my rookie formatting mistake that was causing issues) for my antagonists! To explain further, in this system each character rolls initiative at the start of the combat and creates a slot for their side with a rating equal to what they rolled. The slot doesn't change after that point. So we have Cowboy Tom and Cutie Putie Pie as the player characters facing off against Robo-Nixon and Somehow-Still-Alive Kissinger as the antagonist NPCs. The players roll initiative, and Cowboy Tim gets a 2, Cutie Putie Pie gets 0, Robo-Nixon gets 0, and Somehow-Still-Alive Kissinger gets 5. This creates an initiative order that looks like this Bad Guys 5 Good Guys 2 Good Guys 0 Bad Guys 0 End of Round The players would decide who acts at 2 and who acts at 0. I'd decide which antagonist acts at 5 and which at 0. In roll20 I just use the advance button in the turn tracker to move through each turn. Once we finish with 0 we'd loop back to 5 and do it over again until the combat is over. So there isn't any need to change the value of the slot. (I do use the built in increment function in the turn tracker for End of Round so I have a a record of how many rounds we've had.) For a little more context, player characters and antagonists use different dice pools. Player characters roll dice equal to a Skill + an Attribute, so there are tons of combos and they just use a dice roller built into their sheet. Antagonists have a simplified set of Primary, Secondary, and Desperation pools with one of those being what they roll for initiative. Since Antagonists are so simple, I've been setting up their rolls as Token Actions so I can just click on their token and hit a button rather than open their sheet and use the dice roller there. If Cowboy Tom wants to shoot Somehow-Still-Alive Kissinger he'd use the roller in his sheet and select Aim + Dexterity. If Robo-Nixon wanted to grapple Cutie Putie Pie I'd look at this dice pools (Primary Pool - Nuclear Arms, Diplomacy; Secondary Pool - Wiretapping, Classically Trained Pianist; Desperation Pool - Everything else) and decide to roll his Primary Pool so he can hug Cutie Putie Pie with his nuclear arms. Initiative works similarly. Player characters roll a specific Skill + Attribute, but for Robo-Nixon it would probably be his Desperation Pool. Somehow-Still-Alive Kissinger, with his Primary Pool - War Crimes, would roll his Primary Pool.
1695401171
timmaugh
Forum Champion
API Scripter
Ohhh... 100 points to Gryffindor for the use of subtext and humor. Well done. For some reason I thought you were looking for something more like: Actual Cannibal Shia LaBeouf and Volcano Man are squaring off against the  Armageddon Chihuahua and Leper Khan. ACSL rolls a 6, so the team of ACSL and VM get an entry at 6... *as does* the team of AC and LK. Volcano Man rolls a 4, so an entry is created at 4 for both teams. Then the Armageddon Chihuahua rolls and gets a 7, so an entry is created at 7 for both teams... ...etc... Then I thought there was another mechanic of filling in the gap between that number and 0 (which I wasn't completely following). In any case, glad you got it working!
Haha, loved your examples! So another wrench in all of this, the dice roller built into the sheets uses the &{tracker} command for the built in initiative rolls it does. So my players are going to be creating slots in the initiative order as their tokens if they use their sheet. Worst case, this isn't a huge deal, we're all adults and there are just four players but I'm trying to think of ways around this. I can see when the sheet roller acts it first generates an entry in the chat showing the details of the roll and the result like so. Then if I scroll up in the chat to see the actual details, I see it does this after it generates the chat entry. [[/gr[[@{Cutie Putie Pie|initToTurnOrder}&{tracker}]]]] I can see an attribute exists on the character called 'initToTurnOrder' which is updated whenever init is rolled using the dice roller in the sheet, then this last command is just reading the entry and sending it to the turn tracker for the character. The cleanest solution seems like it would be to change things so it uses the AddCustomTurn logic instead of &{tracker}. I'm assuming this means I would need to dig into the guts of the sheet, but I'm not sure how to do that. I also thought about making actions for their tokens and putting them in the macro bar, but player characters have traits (inherent or optional to use) that can affect the roll (either by adding dice beyond the Skill + Attribute calculation, or by adding to the final result). I know I can have a macro ask for input, so I could do that for this, but then it starts to feel like I'm telling them to use the sheet for everything except for this one thing. I thought of a few other ideas but they seemed even more cludgey and probably very hard to do.
1695437301
timmaugh
Forum Champion
API Scripter
If I have this correct, the problem is that if the players roll initiative from their character sheet, then the entry is made as the name of their token rather than something team oriented like "Team" and "Opposition"... yes? If that's correct, I think the simplest solution would be a custom script that just "rebalances" the Turn Order after you, as GM, are satisfied everyone has rolled initiative for this encounter. Whether the players use their sheet or a custom ability you give them... once all of that rolling is done, you could run this "final encounter prep" script and it would change the Turn Order. For each entry, we test the associated name... skipping anything named "Team" or "Opposition" already. For the rest, if the TurnOrder entry represented a token that itself represented a character, then we test the first listing in the ControlledBy field on the character sheet. If it's a non-gm, the entry would be changed from "Cutie Putie Pie" to "Team"... while everyone else would become "Opposition". Then your TurnOrder is sorted and you can get on with your encounter. Would that work?
1695440294

Edited 1695440323
timmaugh
Forum Champion
API Scripter
If the above would work, here is a scriptlet to do that. Activate it to "fix" your turns by running the command: !fixto ...when you're ready. If you need to return the... um... turns... to their default state, you can run this command: !unfixto It's hot off the presses, so let me know if something isn't working correctly. If you want to change the names of the 2 teams, change lines 66 and 67. /* ========================================================= Name : TurnFixer GitHub : Roll20 Contact : timmaugh Version : 1.0.0 Last Update : 9/22/2023 ========================================================= */ var API_Meta = API_Meta || {}; API_Meta.TurnFixer = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; { try { throw new Error(''); } catch (e) { API_Meta.TurnFixer.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (12)); } } on('ready', () => {     const getPageForPlayer = (playerid) => {         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 getToken = (info) => {         return findObjs({ type: 'graphic', id: info })[0];     };     const isPlayerToken = (obj, pc = false) => {         let players;         if (!pc) {             players = obj.get('controlledby')                 .split(/,/)                 .filter(s => s.length);             if (players.includes('all') || players.filter((p) => !playerIsGM(p)).length) {                 return true;             }         }         if ('' !== obj.get('represents')) {             players = (getObj('character', obj.get('represents')) || { get: function () { return ''; } })                 .get('controlledby')                 .split(/,/)                 .filter(s => s.length);             return !!(players.includes('all') || players.filter((p) => !playerIsGM(p)).length);         }         return false;     };     const isNPC = (obj) => {         let players = (             obj.get('represents') && obj.get('represents').length                 ? getObj('character', obj.get('represents') || { get: function () { return ''; } })                 : obj         )             .get('controlledby').split(/,/)             .filter(s => s.length && !playerIsGM(s));         return !players.length;     };     on('chat:message', (msg) => {         if ('api' !== msg.type) { return; }         if (!/^!(fixto|unfixto)/i.test(msg.content)) { return; }         let teamname = 'Good Guys';         let oppname = 'Opposition';         let to = JSON.parse(Campaign().get('turnorder') || '[]');         if (/^!fixto/i.test(msg.content)) {             to.forEach(t => {                 if (t.id !== '-1') {                     t.origId = t.id;                     t.origCustom = t.custom;                     t.id = '-1';                     let token = getToken(t.origId);                     if (token && isNPC(token)) {                         t.custom = oppname;                     } else if (token && isPlayerToken(token)) {                         t.custom = teamname;                     }                 }             });         } else {             to.forEach(t => {                 if (t.id === '-1') {                     t.id = t.origId || t.id;                     t.custom = t.origCustom || t.custom;                     let token = getToken(t.origId);                 }             });         }         Campaign().set('turnorder', JSON.stringify(to));     }); }); { try { throw new Error(''); } catch (e) { API_Meta.TurnFixer.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.TurnFixer.offset); } }
You are exactly correct as to the issue, and your solution wounds amazing. A script that rebalances things was actually the idea I referenced at the end, but discounted as too hard, so I'm very pleased to hear it would actually work. I think the plan is to use 'Opposition' for the enemies, and 'Allies' for the player side at this point. I might come up with something jazzier later, but simple seems good for now.
1695469144
timmaugh
Forum Champion
API Scripter
Excellent, so change the lines I indicated for the names, and give the scriptlet a run. Let me know if you have any issues.
Ok, changed the line names and did some some setup. I also added an allied NPC (Justice, cub reporter and loyal steed) that uses AddCustomTurn to create a slot for Allies when it rolls. Various alien and tech tokens for my upcoming sci-fi game are playing the parts today. So everything looks good. Justice created a slot for Allies at 0, our foes created slots for Opposition. My player characters used the dice roller in the sheet which created entires attached to their tokens. I just typed !fixto in the chat, and it turned everyone but the Allies entry into Opposition entries. Here is what is loaded from the script. /* ========================================================= Name : TurnFixer GitHub : Roll20 Contact : timmaugh Version : 1.0.0 Last Update : 9/22/2023 ========================================================= */ var API_Meta = API_Meta || {}; API_Meta.TurnFixer = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; { try { throw new Error(''); } catch (e) { API_Meta.TurnFixer.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (12)); } } on('ready', () => { const getPageForPlayer = (playerid) => { 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 getToken = (info) => { return findObjs({ type: 'graphic', id: info })[0]; }; const isPlayerToken = (obj, pc = false) => { let players; if (!pc) { players = obj.get('controlledby') .split(/,/) .filter(s => s.length); if (players.includes('all') || players.filter((p) => !playerIsGM(p)).length) { return true; } } if ('' !== obj.get('represents')) { players = (getObj('character', obj.get('represents')) || { get: function () { return ''; } }) .get('controlledby') .split(/,/) .filter(s => s.length); return !!(players.includes('all') || players.filter((p) => !playerIsGM(p)).length); } return false; }; const isNPC = (obj) => { let players = ( obj.get('represents') && obj.get('represents').length ? getObj('character', obj.get('represents') || { get: function () { return ''; } }) : obj ) .get('controlledby').split(/,/) .filter(s => s.length && !playerIsGM(s)); return !players.length; }; on('chat:message', (msg) => { if ('api' !== msg.type) { return; } if (!/^!(fixto|unfixto)/i.test(msg.content)) { return; } let teamname = 'Allies'; let oppname = 'Opposition'; let to = JSON.parse(Campaign().get('turnorder') || '[]'); if (/^!fixto/i.test(msg.content)) { to.forEach(t => { if (t.id !== '-1') { t.origId = t.id; t.origCustom = t.custom; t.id = '-1'; let token = getToken(t.origId); if (token && isNPC(token)) { t.custom = oppname; } else if (token && isPlayerToken(token)) { t.custom = teamname; } } }); } else { to.forEach(t => { if (t.id === '-1') { t.id = t.origId || t.id; t.custom = t.origCustom || t.custom; let token = getToken(t.origId); } }); } Campaign().set('turnorder', JSON.stringify(to)); }); }); { try { throw new Error(''); } catch (e) { API_Meta.TurnFixer.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.TurnFixer.offset); } }
I also tried removing the Allies entry and redoing the token entries for the player characters to see if that was an unexpected use case, but same effect when I ran the script.
Found the issue, in my rush to setup the test characters they weren't controlled by anyone so that part of the script was reading them as gm controlled. For my table I typically make every character controlled by all, tokens are just used for resource tracking, and I tend to make things pretty transparent for players. Anything I want to keep secret lives in the GM notes section. So my players sometimes move tokens so they can better see the background image, or helpfully adjust health or some kind of power points while I'm busy. Since I'm able to create my Opposition entires using token actions, this actually works well. If my players use the sheet for an allied NPC to roll init !fixto will put them on the Allied side, which is a desired outcome. Thank you so so much!