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

Newbie Q: custom initiative script - how to do stats for NPCs/monsters?

We're playing a system that is not supported natively on Roll20.  It's from the 80s and very crunchy.  I'm also semi-new to Roll20. I'd like to code an API script that tells me (the GM) the initiative order in combat, but I need multiple values from characters.  For the PCs I think this is straightforward, but what about for NPCs/monsters? What I do now when setting up a map (every one is custom in the our game), I drag a token from an art pack onto the map, and set the name & three bars and track other stuff manually.  I do have more than three values for each NPC/monster (health, attack, defense, multiple vals for initiative, etc.) So, what is the correct approach: Make every NPC/monster a character.  Is this what's done for supported systems?  If I have 20 NPCs/monsters on a screen (e.g., a couple groups of post-apocalyptic raiders or a pack of feral wolves, etc.), is each a separate character? Put the stats in GM notes and parse those? Thanks for advice!
1587736462
The Aaron
Roll20 Production Team
API Scripter
Usually you'll have PCs with a Character each, then for each Unique type of NPCs (monsters), you'll have a Character that all of their tokens represent.&nbsp; For example, you might have one Goblin character, then have 30 goblin tokens that all represent that one Goblin Character.&nbsp; This assumes that each individual Goblin has the same bonuses for things like initiative, or investigation, or whatever. For rolling initiative, I have a very configurable script:&nbsp; <a href="https://app.roll20.net/forum/post/7921574/script-update-groupinitiative-now-with-dice-modifiers-for-the-initial-roll" rel="nofollow">https://app.roll20.net/forum/post/7921574/script-update-groupinitiative-now-with-dice-modifiers-for-the-initial-roll</a> It's in the 1-click installs.&nbsp; For help, you can run: !group-init --help
I was able to make combat master work with other game systems than 5e. &nbsp;Though I’ll have to look to see if it can use multiple attributes as modifiers to the roll. &nbsp;it’s it, you’d set up a generic character sheet for each token with the attributes filled in that you use for initiative, then simply draw a square around all combatants and start combat. &nbsp;The API rolls for every combatant and places them in order then calls out their turns in chat.
The Aaron said: Usually you'll have PCs with a Character each, then for each Unique type of NPCs (monsters), you'll have a Character that all of their tokens represent.&nbsp; For example, you might have one Goblin character, then have 30 goblin tokens that all represent that one Goblin Character.&nbsp; This assumes that each individual Goblin has the same bonuses for things like initiative, or investigation, or whatever. I looked at your script, but the system is very different.&nbsp; Start with a Base Action Phase, subtract Phases Consumed in Action up to a Maximum Number of Actions, etc.&nbsp; It's from the 80s :-)&nbsp; Your suggestion nearly got me there but I think I'm still missing a concept.&nbsp; Now I find if I apply damage to one Goblin, it applies to them all.&nbsp; So if I have 5 Goblins, applying -1 damage to one of them applies -1 damage to all five.&nbsp; In Roll20 parlance, I'm clicking on the token and applying -1 to bar1. What I did is create a character, assign an avatar and default token, then filled out some stats (a character sheet) for that Goblin.&nbsp; Then I littered a few around the screen by dragging them from my journal to each place I wanted one.&nbsp; For each, I had to go in and change "Represents Character" to point to the Goblin character and change Bar 1 to represent hit points (different stat name in this system but same idea). Doesn't seem to make a difference if I use a custom character sheet or fill out "attributes" in the character editor. So how would I say "this represents Goblin, but track hit points separately for this instance of Goblin"?
You don't want to link hit points on the token to the sheet. See this video: <a href="https://www.youtube.com/watch?v=UTq3_e6MiRI" rel="nofollow">https://www.youtube.com/watch?v=UTq3_e6MiRI</a>
1587753870
The Aaron
Roll20 Production Team
API Scripter
What you have to do is not link the HP bars to the character's attributes.&nbsp; You can link them and then unlink them to get initial values set up, but for monsters, you need the default token to have them unlinked. Can you write out the way initiative works for the game, I can't quite grasp what you're saying about it.&nbsp; Does the initiative change each round, like initiative is the total number of actions a character can take, and as they consume those actions on a turn, up to some maximum number, it decreases their order in the turn order?
Thanks, I think I get it.&nbsp; This article also helped:&nbsp; <a href="https://wiki.roll20.net/Linking_Tokens_to_Journals" rel="nofollow">https://wiki.roll20.net/Linking_Tokens_to_Journals</a> &nbsp; Looks like I'm setting up Mooks (on that page's terminology) and then everything clicked.&nbsp; Thanks for everyone's help! The Aaron...the game is FGU's Aftermath! from 1981ish.&nbsp; Possibly the crunchiest thing ever made.&nbsp; I've heard people actually say it's unplayable LOL. For each character, a Base Action Phase and other values are calculated from attributes. (and there is a fiendish amount of round up, round nearest, grouping, etc...and this from an era when computers were just coming along!) Let's say Mary has a Base Action Phase of 13, a Maximum Number of Actions of 3, and a Phases Consumed in Action of 4.&nbsp; If Mary had higher Speed (an attribute) her BAP would be higher, if her Deftness was higher she'd get more actions, etc. So combat starts...find who has the highest Base Action Phase (could be a PC, NPC, animal, robot, whatever).&nbsp; Let's say someone goes on 14 (which would be a very fast character).&nbsp; You then count down 14, 13, 12, etc. to zero.&nbsp; In Mary's case, she goes on 13 (Base Action Phase), 9 (13-Phases Consumed in Action), and 5 (9-PCA), but not 1 because that would exceed her Maximum Number of Actions. I'm not defending the system - it clearly shows its age.&nbsp; We only play it because I've been playing with the same group since the mid-80s.&nbsp; We have radically streamlined the crunchiness but this part has remained.&nbsp; Could definitely use a random element but it is what it is.&nbsp; These are all my high school friends scattered to the four winds...previously we just continued an ancient campaign whenever 2 or 3 of us happened to be in the same locale, but with Roll 20 we're getting a lot more regular play in.&nbsp;&nbsp; Thanks again for your help!
1587755172
The Aaron
Roll20 Production Team
API Scripter
Ah!&nbsp; I can write a little script for that. Do each of the characters have standard attributes for each of those stats? (Base Action Phase, Phases Consumed in Action, and Maximum Number of Actions?&nbsp; It would be pretty easy to write a script that just finds all of those attributes, calculates each of the phases they go on, and inserts them in the turn order (and removes prior entries, etc.) and does it for all selected tokens.&nbsp; Probably 30 lines of code.
Here's what I came up with...keep in mind this is like my third script for Roll20 :-) on("ready",function() { &nbsp; &nbsp; log('==&gt; amathInit.js version 1.0'); }); on("chat:message", function(msg) { &nbsp; if(msg.type == "api" &amp;&amp; msg.content.indexOf("!init ") !== -1) { &nbsp; &nbsp; var phase = msg.content.replace("!init ", ""); &nbsp; &nbsp; sendChat('Init Bot','/w gm initiative phase ' + phase); &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; var currentPageID = Campaign().get('playerpageid'), &nbsp; &nbsp; &nbsp; &nbsp; currentPage = getObj('page', currentPageID); &nbsp; &nbsp; characters = filterObjs(function(obj) { &nbsp; &nbsp; &nbsp; &nbsp; if (obj.get('_pageid') !== currentPageID) return false; &nbsp; &nbsp; &nbsp; &nbsp; if (obj.get('controlledby') == "") return false; &nbsp; &nbsp; &nbsp; &nbsp; if (obj.get('type') !== 'graphic' || obj.get('subtype') !== 'token' ) return false; &nbsp; &nbsp; &nbsp; &nbsp; return obj; &nbsp; &nbsp; }); &nbsp; &nbsp; characters.forEach(function(char) { &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if ( char.get('represents') == "" || char.get('represents') == "undefined") { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; warnings.push(char.get('name') + " does not represent!"); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; // get BAP &nbsp; &nbsp; &nbsp; &nbsp; BAP = getAttrByName(char.get('represents'),'BAP'); &nbsp; &nbsp; &nbsp; &nbsp; if ( BAP == "" || BAP == "undefined") { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; log(char.get('name') + " has no BAP!"); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; // skip char if haven't reached BAP yet &nbsp; &nbsp; &nbsp; &nbsp; if ( phase &gt; BAP ) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; // get MNA &nbsp; &nbsp; &nbsp; &nbsp; MNA = getAttrByName(char.get('represents'),'MNA'); &nbsp; &nbsp; &nbsp; &nbsp; if ( MNA == "" || MNA == "undefined") { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; log(char.get('name') + " has no MNA!"); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; // get PCA &nbsp; &nbsp; &nbsp; &nbsp; PCA = getAttrByName(char.get('represents'),'PCA'); &nbsp; &nbsp; &nbsp; &nbsp; if ( PCA == "" || PCA == "undefined") { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; log(char.get('name') + " has no PCA!"); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // calc phases we go on &nbsp; &nbsp; &nbsp; &nbsp; phase_ctr = BAP; &nbsp; &nbsp; &nbsp; &nbsp; while ( phase_ctr &gt; 1 &amp;&amp; MNA &gt; 0 ) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if ( phase_ctr == phase ) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; BCS = getAttrByName(char.get('represents'),'BCS'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; weapon_name = getAttrByName(char.get('represents'),'weapon_name'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; weapon_damage = getAttrByName(char.get('represents'),'weapon_damage'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Init Bot","/w gm " + char.get('name') + " acts, BCS " + BCS + ": " + weapon_name + " " + weapon_damage); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; log(char.get("name") + " goes on " + phase_ctr); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; phase_ctr -= PCA; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; MNA--; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; }); &nbsp; } });
1587762298
The Aaron
Roll20 Production Team
API Scripter
The best aspect of a script is that it works for you, third one or no. =D Some thoughts: It's probably better to use the word "token" where you're using "character" above.&nbsp; It will be less confusing to keep with the common Roll20 terminology. "controlledby" on a token is ignored if it represents a character, and the character's controlledby is used instead. It's orders of magnitude more efficient to use findObjs() with a type and pair down the results, than to use filterObjs().&nbsp; filterObjs() considers every object in the whole of a Roll20 game with its filter function.&nbsp; findObjs() uses a cached index by type and a few other things to only look at a subset of objects. You might consider adding tokens to the TurnOrder so you can easily cycle through them, even if that just means deleting the top row as it has completed its turn.
1587762396

Edited 1587762428
GiGs
Pro
Sheet Author
API Scripter
Oh I recognise those. FGU's Aftermath, or maybe Bushido. Some clarifications: BAP: tells you how fast you are, or more accurately when you start acting each turn. MNA: max Number of Actions, is how many actions you get each turn.&nbsp; PCA: Phases Consumed in Action, is the number of phases you have to wait before you can take your next action. PCA is always BAP/MNA, rounded down IIRC. So someone with a BAP of 13 and MNA of 2, would act on 13 and then 7. Someone with a BAP of 8 and MNA of 4 would act on 8, 6, 4, and 2.
So I changed my filterObj to this, which appears to be working correctly: var characters = findObjs({&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; _pageid: Campaign().get("playerpageid"),&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; _type: "graphic", &nbsp; &nbsp; _subtype: "token" &nbsp; &nbsp; }).filter(function(char) { &nbsp; &nbsp; &nbsp; &nbsp; if (char.get('name') == "" || char.get('represents') == "" ) return false; &nbsp; &nbsp; &nbsp; &nbsp; return char; &nbsp; &nbsp; }); No need to check controlledby. Interestingly, I discovered that if I do this... characters.forEach(function(char) { &nbsp; &nbsp; log(char.get('name')); }); ...then I don't process all the characters.&nbsp; characters.length is set to, say, 18 but I'll only log some of them but not all.&nbsp; But if I do this: for (i = 0; i&lt;characters.length; i++) { &nbsp; log(i + " " + characters[i].get('name')); } Then I see exactly the right numbers.&nbsp; I'm going to chalk that up to some finer point of Javascript forEach or array mechanics that I'm ignorant of. Well, it's been quite an educational day!
1587777656
The Aaron
Roll20 Production Team
API Scripter
One thing is that "char" is actually a reserved word, so there might be some scope confusion or just plain weirdness with it.&nbsp; Another thought might be that log has some race condition with sending to the output. You can leave the _ off of property names in pretty much every context, except in events, which almost never comes up. let tokens =&nbsp;findObjs({ pageid: Campaign().get("playerpageid"), type: "graphic", subtype: "token" }) .filter( t =&gt; t.get("name") !== "" &amp;&amp; t.get("represents") !== "" ) ; I think it's better to leave them off.&nbsp; I justify this in two ways: If a property becomes writeable, you don't have to change any code.&nbsp; This doesn't happen often, but has happened in the past. _ are annoying to type and ugly. =D A case where the _ is required would be something like: on('change:player:_online', (p) =&gt; sendChat('', `${p.get('displayname')} ${ p.get("online") ? "has arrived" : "has left the building"}!`)); You could try concatenating your output and logging it all in one go: log( tokens.map(t=&gt;t.get('name')).join(', ') );
I'd completely forgotten about 'char' being a reserved word. I've commented out all of the logs before going into "production" actually. And I took you advice to populate the turn order on each action phase...makes it easy to track - I hadn't used that roll20 feature before. Thanks much!
1587779710
The Aaron
Roll20 Production Team
API Scripter
Awesome!&nbsp; No problem.