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

Wild Magic Surge Script

1669805093

Edited 1670260191
Most up to date version is here:&nbsp;<a href="https://app.roll20.net/forum/permalink/11219976/" rel="nofollow">https://app.roll20.net/forum/permalink/11219976/</a> Hello Roll20! I haven't posted on the forums for a bit so I hope I've got this in the right place.&nbsp; I've written a small script to handle wild magic surges for the wild magic sorcerer in my 5e game, inspired by/based on the magicalsurges.js script by Nic. The script detects whether the caster of the spell is a wild magic sorcerer when they cast a spell and rolls a d20. If the d20=1 then it rolls on the WildSurges table using The Aaron's recursive tables mod. If you don't have a WildSurges table, it adds one for you to populate - shamelessly plagarised from Nic's script. It probably has its bugs, and ive not shared a script before, but it seems to work so I figured I'd share it. Suggestions for improvements welcome - my js is kinda rusty. //Script - WildSurges.js //Version - v1.1 //Author - Lawmonger //Based on MagicalSurges.js by nmrcarroll on('ready',function(){ // check for state to store our sorcerers. surgeTable=findObjs({ // Is there a WildSurges rollable table? type: 'rollabletable', name: 'WildSurges', })[0]; if(!surgeTable){ //if no WildSurges table make one surgeTable=createObj('rollabletable',{ name: 'WildSurges', }); createObj('tableitem',{ //populate new WildSurges table with temporary entry name: 'DELETE ME WHEN YOU HAVE ADDED YOUR OWN SURGES', rollabletableid: surgeTable.id, }); } }); on("chat:message",function(msg){ var charID=(findObjs({ type:'character', name: (msg.content.match(/charname=([^\n{}]*[^"\n{}])/) || [])[1], //get the msg sender's character ID from roll template })[0] || {id: 'API',}).id; //if no char. ID, ID as API if (charID!=='API'){ //if message is not from the API var subclass = getAttrByName(charID,"subclass"); //get the character's subclasses var mc1subclass = getAttrByName(charID,"multiclass1_subclass"); // var mc2subclass = getAttrByName(charID,"multiclass2_subclass"); //ALL OF THEM! var mc3subclass = getAttrByName(charID,"multiclass3_subclass"); // if (subclass||mc1subclass||mc2subclass||mc3subclass=="Wild Magic"){ //if the subclass is Wild Magic var spellLevel=msg.content.match(/spelllevel=([^\n{}]*[^"\n{}])/); //check the roll template for spell level if(spellLevel!==null){ //if there is a spell level field spellLevel=Number(spellLevel[1]); //get the numerical value and treat it as a number } var cantrip = msg.content.includes('cantrip}}'); //is it a cantrip? var spellTemplate = msg.rolltemplate == 'spell'; //is it using the spell roll template? var roll=0; //initialise roll value as 0 var outputMsg =''; //initialise blank message if(!cantrip &amp;&amp; (spellLevel || spellTemplate)){ //if the msg is a spell and not a cantrip roll = randomInteger(20); //roll a d20 outputMsg = `${roll}`; //set the output message to report the roll if(roll==1){ //if the roll == 1, sendChat("WILD SURGE!",`!rt /em [[1t[WildSurges] ]]`); //emote WILD SURGE! to chat &amp; call recursive tables mod to roll on WildSurges }; sendChat("Wild Roll",`${outputMsg}`); //send output message (d20 roll) to chat. } } } });
1669824040
The Aaron
Roll20 Production Team
API Scripter
Not bad for a first script! There are a few minor Javascript things I'd suggest: Strike `var` from your JS vocabulary.&nbsp; In almost all cases, you should use `let` and `const` instead.&nbsp; Generally speaking, when I update old scripts I do a blanket replace of `var` for `const`, then change `const` to `let` where I'm actually changing the value.&nbsp; The semantics of `var` are unlike other languages (function scoped and hoisted can be very surprising), and `let` and `const` behave more like you would expect from traditional procedural programming languages. Either prefer `()=&gt;{}` "Fat Arrow" functions, or add `"use strict";` at the top of your `function()` bodies.&nbsp; That forces the interpreter to give you better warnings about things that might cause issues. Always use `===` and `!==` instead of `==` and `!=` until you know the difference (and at that point you'll just know you never want to use the shorter forms! =D).&nbsp; `==` and `!=` will perform automatic type coercion when doing comparisons, which can lead to baffling behavior that is hard to track down. For your script, there are a few organizational suggestions I have, but they're mostly personal preference: I'd put your `on("chat:message",...)` block inside your `on("ready",...)` block.&nbsp; Before "ready" occurs, various events are issued as objects are created and the chat log is populated.&nbsp; I don't think there is an issue right now with chat messages coming to chat handlers before "ready", but there have been cases in the past where that would happen.&nbsp; In general, it's a good idea to delay all your script activity until after "ready", unless you're specifically trying to capture initialization events for objects.&nbsp; I think I only have 2–3 scripts that do that. I usually like to create functions and then call those functions, rather than having bare code in my "ready" handler.&nbsp; I'd probably make an `assureSurgeTable()` function, and then call it.&nbsp; That makes it easier when you get to larger scripts as you can separate the "what" from the "when" more easily.&nbsp; Functional Decomposition FTW. =D&nbsp; Similarly, I'd probably write an "isWildMage(charID)" function and call that, rather than having the logic for it in the chat message handler.&nbsp; Then you could possibly use that function somewhere else later, or separate it into a "plugin" sort of script or make different versions for different sheets. Finally, here's a function you might find handy, it creates an object where the keys are the labels from a Roll Template, and the values are the contents of that label: const parseRollTemplate = (t) =&gt; [...t.matchAll(/{{([^=]*?)=(.*?)}}/g)].reduce((m,p)=&gt;({...m,[p[1]]:p[2]}),{}); That works for all valid Roll Templates, but if you want to support aberrant ones where the same label appears multiple times, you could use this one: const parseRollTemplateArray = (t) =&gt; [...t.matchAll(/{{([^=]*?)(?:=(.*?))?}}/g)].reduce((m,p)=&gt;({...m,[p[1]]:[...(m[p[1]]||[]),...(undefined===p[2]?[""]:[p[2]])]}),{}); which makes an object where the values are an array containing all the different contents of the label. Hope that helps!
1669824526
The Aaron
Roll20 Production Team
API Scripter
I should mention that the only place I still use `var` is for variables that I need to allow for multiple declarations.&nbsp; You can't re-declare a variable created with `let` or `const`, but `var` will let you re-declare all you like: let a = 1; let a = 2; //&lt; That's an error! a = 2; //&lt; that works. const b = 3; const b = 4; //&lt; That's an error! b = 4; //&lt; That's also an error! (it wouldn't be const if that worked! ;D ) var c = 5; var c = 6; //&lt; No problem! c = 7; //&lt; No problem! You're probably thinking, why would I ever need to support that!?&nbsp; I'm glad you asked!&nbsp; =D&nbsp; I have a global variable that I load with data, but I can't guarantee that it already exists and then selectively declare it in multiple scripts, so I need to allow for the fact that it needs to be declared in all my scripts, hence the need for using `var`.&nbsp; Here's details if you're interested:&nbsp;<a href="https://app.roll20.net/forum/permalink/9772430/" rel="nofollow">https://app.roll20.net/forum/permalink/9772430/</a>
The Aaron said: Not bad for a first script! Thank you very much! Made my day to get that comment from the Arcane Scriptomancer himself! Thank you for the suggestions too - using 'var' is probably showing just how rusty my JavaScript is - I don't think 'let' and 'const' were options last time I used it in anger! I can see the utility though! Yet another thank you for the explanation of the difference - its far clearer than any other explanation I've found! :)&nbsp;
1669826858
The Aaron
Roll20 Production Team
API Scripter
No problem. =D&nbsp; `let` and `const` were only introduced a few years ago with Javascript ES6.&nbsp; Lots of nice things came to the language since then as well. Fat arrow functions ()=&gt;{} , classes , template literals .&nbsp; Definitely ask if you have any questions about Javascript stuff, or Mod Script stuff, or just stuff in general. =D
Updated version with greater modularity. Also now checks for recursive tables script before calling it, and rolls on normal roll table if recursiveTables is not found.&nbsp; Let me know what you think.&nbsp; //Script - WildSurges.js //Version - v1.2 //Author - Lawmonger //Based on MagicalSurges.js by nmrcarroll //With thanks to The Aaron on('ready',function(){ "use strict"; confirmTable(); //make sure there is a rollable table to roll on. on("chat:message",function(msg){ wildMagic(msg); //make ths wild magic happen (or not) }); }); function wildMagic(msg){ //THIS IS WHERE THE WILD MAGIC HAPPENS if (isWildMage(msg)){ //is the caster a wild mage? boolean. If yes, let roll=0; //initialise roll value as 0 let outputMsg =''; //initialise blank message if(isSpell(msg) &amp;&amp; isSpell(msg)!=="cantrip"){ //is the msg a spell &amp; not a cantrip? if yes, roll = randomInteger(20); //roll a d20 outputMsg = `${roll}`; //set the output message to report the roll if(roll === 1){ //if the roll == 1 then, if(_.has(state,'RecursiveTable')){ //if RecursiveTables.js installed, sendChat("WILD SURGE!",`!rt /em [[1t[WildSurges] ]]`); //emote WILD SURGE! to chat &amp; call RecursiveTables to roll on WildSurges }else{ //otherwise sendChat("WILD SURGE!",`/em [[1t[WildSurges] ]]`); //emote WILD SURGE! to chat &amp; call roll on WildSurges } }; //for rolls &gt; 1, sendChat("Wild Roll",`${outputMsg}`); //send output message (d20 roll) to chat. } } }; function isWildMage(msg){ //CHECKS IF CASTER IS A WILD MAGE const charID = getCharID(msg); //get the character_id if(charID){ //if the msg includes a character id const subclass = getAttrByName(charID,"subclass"); //get the character's subclasses const mc1subclass = getAttrByName(charID,"multiclass1_subclass"); // const mc2subclass = getAttrByName(charID,"multiclass2_subclass"); //ALL OF THEM! const mc3subclass = getAttrByName(charID,"multiclass3_subclass"); // const wildMage = ( //Boolean: are any subclasses Wild Magic? subclass === "Wild Magic"|| mc1subclass=== "Wild Magic"|| mc2subclass=== "Wild Magic"|| mc3subclass === "Wild Magic"); return wildMage; }else{ return false; //if no character_id return false } }; function getCharID(msg){ //GETS THE CHARACTER ID FROM 5EOGL ROLLTEMPLATES FOR SPELLS if(msg.rolltemplate!==undefined){ //if the message is a rolltemplate const charID=(findObjs({ type:'character', name:(msg.content.slice(msg.content.lastIndexOf("=") + 1))})[0] //slice out the character name from the rolltemplate ).id; //get the character_id of that character return charID; }else{ return false //if msg not rolltemplate return false } }; function isSpell(msg){ //CHECKS IF MSG IS ROLLTEMPLATE if(msg.rolltemplate!==undefined){ //if the message is a roll template, let rt = parseRollTemplate(msg.content); //parse the rolltemplate, let level=false if(msg.rolltemplate === "spell"){ level = (rt.level.split(" "))[1]; } const spell = (rt.spelllevel || level); return spell; //return spell level }else{ return false; //if msg not a rollltemplate return false } }; const parseRollTemplate = (t) =&gt; [...t.matchAll(/{{([^=]*?)=(.*?)}}/g)].reduce( //PARSES ROLLTEMPLATE AND RETURNS LABELS AS OBJECTS - THANKS TO THE AARON! (m,p)=&gt;({...m,[p[1]]:p[2]}),{}); function confirmTable(){ //CONFIRM THERE IS A TABLE TO ROLL ON - STOLEN FROM NIC "use strict"; let surgeTable=findObjs({ // Is there a WildSurges rollable table? type: 'rollabletable', name: 'WildSurges', })[0]; if(!surgeTable){ //if no WildSurges table make one surgeTable=createObj('rollabletable',{ name: 'WildSurges', }); createObj('tableitem',{ //populate new WildSurges table with temporary entry name: 'DELETE ME WHEN YOU HAVE ADDED YOUR OWN SURGES', rollabletableid: surgeTable.id, }); } }; Happy rolling!
1670202206
The Aaron
Roll20 Production Team
API Scripter
That's nice!&nbsp; Here's how I might refactor what you have: //Script - WildSurges.js //Version - v1.2 //Author - Lawmonger //Based on MagicalSurges.js by nmrcarroll //With thanks to The Aaron on('ready',() =&gt; { const parseRollTemplate = (t) =&gt; [...t.matchAll(/{{([^=]*?)=(.*?)}}/g)].reduce( // PARSES ROLLTEMPLATE AND RETURNS LABELS AS OBJECTS - THANKS TO THE AARON! (m,p)=&gt;({...m,[p[1]]:p[2]}),{}); const confirmTable = () =&gt; { // CONFIRM THERE IS A TABLE TO ROLL ON - STOLEN FROM NIC let surgeTable=findObjs({ // Is there a WildSurges rollable table? type: 'rollabletable', name: 'WildSurges', })[0]; if(!surgeTable){ // if no WildSurges table make one surgeTable=createObj('rollabletable',{ name: 'WildSurges', }); createObj('tableitem',{ // populate new WildSurges table with temporary entry name: 'DELETE ME WHEN YOU HAVE ADDED YOUR OWN SURGES', rollabletableid: surgeTable.id, }); } }; const wildMagic = (msg) =&gt; { // THIS IS WHERE THE WILD MAGIC HAPPENS if (isWildMage(getCharID(msg))){ // is the caster a wild mage? boolean. If yes, let roll=0; // initialise roll value as 0 let outputMsg =''; // initialise blank message if(isSpell(msg) &amp;&amp; isSpell(msg) !== "cantrip"){ // is the msg a spell &amp; not a cantrip? if yes, roll = randomInteger(20); // roll a d20 outputMsg = `${roll}`; // set the output message to report the roll if(roll === 1){ // if the roll == 1 then, if(_.has(state,'RecursiveTable')){ // if RecursiveTables.js installed, sendChat("WILD SURGE!",`!rt /em [[1t[WildSurges] ]]`); // emote WILD SURGE! to chat &amp; call RecursiveTables to roll on WildSurges }else{ // otherwise sendChat("WILD SURGE!",`/em [[1t[WildSurges] ]]`); // emote WILD SURGE! to chat &amp; call roll on WildSurges } }; // for rolls &gt; 1, sendChat("Wild Roll",`${outputMsg}`); // send output message (d20 roll) to chat. } } }; const isWildMage = (charID) =&gt; { // CHECKS IF CASTER IS A WILD MAGE if(charID){ // if have a valid character id const subclass = getAttrByName(charID,"subclass"); // get the character's subclasses const mc1subclass = getAttrByName(charID,"multiclass1_subclass"); // const mc2subclass = getAttrByName(charID,"multiclass2_subclass"); // ALL OF THEM! const mc3subclass = getAttrByName(charID,"multiclass3_subclass"); // const wildMage = ( // Boolean: are any subclasses Wild Magic? subclass === "Wild Magic"|| mc1subclass=== "Wild Magic"|| mc2subclass=== "Wild Magic"|| mc3subclass === "Wild Magic"); return wildMage; }else{ return false; // if no character_id return false } }; const getCharID = (msg) =&gt; { // GETS THE CHARACTER ID FROM 5EOGL ROLLTEMPLATES FOR SPELLS if(msg.rolltemplate !== undefined){ // if the message is a rolltemplate const charID=(findObjs({ type:'character', name:(msg.content.slice(msg.content.lastIndexOf("=") + 1))})[0] // slice out the character name from the rolltemplate ).id; // get the character_id of that character return charID; }else{ return false // if msg not rolltemplate return false } }; const isSpell = (msg) =&gt; { // CHECKS IF MSG IS ROLLTEMPLATE if(msg.rolltemplate!==undefined){ // if the message is a roll template, let rt = parseRollTemplate(msg.content); // parse the rolltemplate, let level=false if(msg.rolltemplate === "spell"){ level = (rt.level.split(" "))[1]; } const spell = (rt.spelllevel || level); return spell; // return spell level }else{ return false; // if msg not a rollltemplate return false } }; confirmTable(); // make sure there is a rollable table to roll on. on("chat:message",wildMagic); // make ths wild magic happen (or not) }); The two primary things I changed (and I didn't test this, but it should be fine): I moved everything inside the on('ready',...) event handler.&nbsp; This has the effect of isolating them all in a closure and prevents them from showing up in the global scope.&nbsp; That also prevents a problem if some other script has defined a getCharID or isSpell or parseRollTemplate. Changed isWildMage() to take a character ID instead of the message.&nbsp; All it needs is the character ID, which is immediately extracted from the message via another function.&nbsp; I simply bubbled that call up to the calling point of isWildMagic().&nbsp; That decouples the implementation of isWildMagic() from the structure of a message object and also the getCharID() function, which means you could easily extract it and call it on a list of character IDs to find all the Wild Mages for some other script. The rest is just personal preference ( fat arrows over function declarations and some ordering of functions and calls, etc.). Hope that's useful!
1670260161

Edited 1670433364
Awesome, thank you! I've used your revised version and added a couple of further features: 1. it now rolls twice when a character gains the controlled chaos wild magic sorcerer feature. 2. it triggers a magic burst fx on the token when a wild magic surge occurs. I'd like to make the fx optional, but I haven't fully understood how to do that yet. Edited: to fix error caused by some roll templates not recognised by the script. //Script - WildSurges.js //Version - v1.3 //Author - Lawmonger //Based on MagicalSurges.js by nmrcarroll //With thanks to The Aaron on('ready',() =&gt; { const parseRollTemplate = (t) =&gt; [...t.matchAll(/{{([^=]*?)=(.*?)}}/g)].reduce( // PARSES ROLLTEMPLATE AND RETURNS LABELS AS OBJECTS - THANKS TO THE AARON! (m,p)=&gt;({...m,[p[1]]:p[2]}),{}); const confirmTable = () =&gt; { // CONFIRM THERE IS A TABLE TO ROLL ON - STOLEN FROM NIC let surgeTable=findObjs({ // Is there a WildSurges rollable table? type: 'rollabletable', name: 'WildSurges', })[0]; if(!surgeTable){ // if no WildSurges table make one surgeTable=createObj('rollabletable',{ name: 'WildSurges', }); createObj('tableitem',{ // populate new WildSurges table with temporary entry name: 'DELETE ME WHEN YOU HAVE ADDED YOUR OWN SURGES', rollabletableid: surgeTable.id, }); } }; const wildMagic = (msg) =&gt; { // THIS IS WHERE THE WILD MAGIC HAPPENS const charID =getCharID(msg); // get the character id from msg const token = getToken(charID); // get the token_id const wMlvl =isWildMage(charID); // get wildmage level if (wMlvl){ // is the caster a wild mage? If yes, let roll=0; // initialise roll value as 0 let outputMsg =''; // initialise blank message let chaos = 1; // initialise controlled chaos multiplier if (wMlvl&gt;=14){chaos = 2}; // if wildMage level is 14+,set controlled chaos multiplier to 2 if(isSpell(msg) &amp;&amp; isSpell(msg) !== "cantrip"){ // is the msg a spell &amp; not a cantrip? if yes, roll = randomInteger(20); // roll a d20 outputMsg = `${roll}`; // set the output message to report the roll if(roll === 1){ // if the roll == 1 then, for (let i = 0; i&lt;chaos; i++){ // send message a number of times === to chaos multiplier. if(_.has(state,'RecursiveTable')){ // if RecursiveTables.js installed, sendChat("WILD SURGE!",`!rt /em [[1t[WildSurges] ]]`); // emote WILD SURGE! to chat &amp; call RecursiveTables to roll on WildSurges }else{ // otherwise sendChat("WILD SURGE!",`/em [[1t[WildSurges] ]]`); // emote WILD SURGE! to chat &amp; call roll on WildSurges } }; if(token !== undefined){ // if token_id is known const x1 =Number (token.get("left")); // get the x coord const y1 =Number (token.get("top")); // and the y coord of caster token spawnFx(x1,y1,"burst-magic"); // trigger a magic burst fx on caster. }; }; // for rolls &gt; 1, sendChat("Wild Roll",`${outputMsg}`); // send output message (d20 roll) to chat. } } }; const isWildMage = (charID) =&gt; { // CHECKS IF CASTER IS A WILD MAGE if(charID){ // if have a valid character id const subclass = getAttrByName(charID,"subclass"); // get the character's subclasses const mc1subclass = getAttrByName(charID,"multiclass1_subclass"); // const mc2subclass = getAttrByName(charID,"multiclass2_subclass"); // ALL OF THEM! const mc3subclass = getAttrByName(charID,"multiclass3_subclass"); // let wildMage = false; // default assumption: not wild mage if(subclass === "Wild Magic"){ // Are any subclasses Wild Magic? wildMage=getAttrByName(charID,"base_level")}; // If yes, return wild mage level if(mc1subclass=== "Wild Magic"){ wildMage=getAttrByName(charID,"multiclass1_lvl")}; if(mc2subclass=== "Wild Magic"){ wildMage=getAttrByName(charID,"multiclass2_lvl")}; if(mc3subclass=== "Wild Magic"){ wildMage=getAttrByName(charID,"multiclass3_lvl")}; return wildMage; }else{ return false; // if no character_id return false } }; const getCharID = (msg) =&gt; { // GETS THE CHARACTER ID FROM 5EOGL ROLLTEMPLATES FOR SPELLS let charID ="" if(msg.rolltemplate!== undefined){ // if msg includes a roll template, let charname = false; // initialise charname as false if(msg.rolltemplate==="npc"){ // if the rolltemplate is 'npc' simple template charname = parseRollTemplate(msg.content).name; // get the name }else{ // otherwise charname = msg.content.match(/charname=([^\n{}]*[^"\n{}])/); // parse out the charname value (stolen from Nic) if(charname){ // if there is a charname value found charname=charname[1]; // get the second part - the name }; }; if (charname){ // if there is a defined charname charID=( findObjs({ // get character object type:'character', name:(charname.trim()) // by name, trimming off excess whitespace })[0] // remove the object from its array ).id; // get the character_id of that character return charID; }else{ return false; // if the charname cannot be determined return false }; }else{ return false; // if msg not rolltemplate return false }; }; const getToken = (charID) =&gt; { // GETS THE CASTER'S TOKEN token = findObjs({type:'graphic', represents: charID})[0]; // get the token object for caster from its array. return token; }; const isSpell = (msg) =&gt; { // CHECKS IF MSG IS ROLLTEMPLATE if(msg.rolltemplate!== undefined){ // if the message is a roll template, let rt = parseRollTemplate(msg.content); // parse the rolltemplate, let level=false if(msg.rolltemplate === "spell"){ // if using spell rolltemplate level = (rt.level.split(" "))[1]; // get the numerical value of level } const spell = (rt.spelllevel || level); return spell; // return spell level }else{ return false; // if msg not a rollltemplate return false } }; confirmTable(); // make sure there is a rollable table to roll on. on("chat:message",wildMagic); // make ths wild magic happen (or not) });