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

[5e OGL] 5e Char Sheet - Trigger Script on Crit?

October 11 (8 years ago)

Edited October 11 (8 years ago)
Roll20 scripting n00b here, trying to build a crit effect table that triggers off of a crit roll from the 5E character sheet.  I've looked at the chat messages and don't see any difference between a non-crit roll result and a crit roll result (see below), so I'm wondering if there's some other variable or object property I need to look for...any suggestions?

Non-Crit Message:
{{mod=+7}}
{{rname=Longbow}}
{{r1=$[[0]]}}
{{always=1}}
{{r2=$[[1]]}}
{{attack=1}}
{{range=150/600}}
{{damage=1}}
{{dmg1flag=1}}
{{dmg1=$[[2]]}}
{{dmg1type=Piercing }} 
0 
{{dmg2=$[[3]]}}
{{dmg2type=}}
{{crit1=$[[4]]}}
{{crit2=$[[5]]}} 
0 
{{desc=}} ammo= charname=Human Fighter (Folk Hero)
Crit Message:
{{mod=+7}}
{{rname=Longbow}}
{{r1=$[[0]]}}
{{always=1}}
{{r2=$[[1]]}}
{{attack=1}}
{{range=150/600}}
{{damage=1}}
{{dmg1flag=1}}
{{dmg1=$[[2]]}}
{{dmg1type=Piercing }} 
0 
{{dmg2=$[[3]]}}
{{dmg2type=}}
{{crit1=$[[4]]}}
{{crit2=$[[5]]}} 
0 
{{desc=}} ammo= charname=Human Fighter (Folk Hero)
October 11 (8 years ago)

Edited October 11 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
I'd look at the inline roll result there (they correspond to the $[[X]]) if you really want to do this with a script. That's what is probably determining if the crit values are shown or not via the code for the sheet's roll template.

However, you don't really need the API to do this. Based on those template field names, I'm pretty sure that you are using the OGL sheet. You can tweak the output from the crit fields by putting custom dice rolls or table rolls in to the crit field on the attacks. Whatever you put in these crit fields is automatically put into inline rolls by the sheet before being sent to chat, so to trick it into doing two separate inline rolls, put:
XdY]] [[1t[CritEffectTable] 
in the appropriate crit field. The she sheet will then parse it into this (because it surrounds whatever text you put in there in inline brackets:
[[XdY]] [[1t[CritEffectTable] ]]

Which results in this output on crit (yeah, I artificially lowered the crit threat range to test):


What my attack looks like:


Happy rolling and hope that helps,

Scott
October 11 (8 years ago)
I'll take a look at that -- what I don't want to do is have to update every attack to refer to the crit table that I'm going to put together (which will also depend on the type of attack -- that, at least, is pretty obvious).  Seems like it can be done with a macro but also seems messy; I'd rather just listen to the results and if it's a crit, then do something via script.
October 11 (8 years ago)

Edited October 11 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
I can see that, but you're still going to have to put some way of determining which crit effect table to roll on, probably via a specific api command (e.g. !piercing, !bludgeoning, !element). Although, I suppose you could do some regex matching to find the damage type in the roll template and then trigger the table roll. Additionally, are you wanting to have the script insert the table roll into the output template? or just have it pop up as a separate chat message? The former will definitely require all attacks to be called via api command so that it isn't output to the chat until it has been processed.

Also, just had an idea to make my suggestion above a little easier to implement. Due to order of operations, ability/attribute calls are resolved before inline rolls, so instead of having a specific crit effect table roll in the crit field, you can simply put 1t[@{dmgtype}CritEffect] in as the table roll. It will then reference the damage type for the corresponding damage field (1 or 2) and will then roll on the crit effect table corresponding to that damage. Might be closer to what you are looking for.
October 11 (8 years ago)

Edited October 11 (8 years ago)
Parsing the attack for the type shouldn't be too difficult - it's right there in the JSON: {{dmg1type=Piercing }} ; I don't mind if it's a separate chat message -- the attack can be displayed as-is, then if it's a crit, the crit effect will be displayed right after it (one of the options I'm looking actually requires two nat20s before a critical effect is applied)...

Basically, this is the concept:

  1. Player rolls an attack as usual.
  2. After result is calculated by char sheet and displayed...
    1. If it's a crit, then the crit table is consulted for additional effects (if any)
    2. If it's not a crit, nothing happens.
I guess I just assumed that listening for a crit result would be a little easier than it is (I'd expect some variable passed, since the OGL sheet obviously relies on one -- otherwise the second damage roll couldn't be conditional).

Writing the JS to handle the conditional cases isn't a problem for me -- it's just a nested if or a case statement.  It's knowing that the trigger has occurred that's stumping the hell out of me.
October 11 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Well, the sheet's attack roll is always going to have the syntax 1d20>X which means that you are always going to wind up with inlinerolls[x].results.rolls[0].mods.customCrit as seen in this log of the message for an OGL sheet attack:
{"content":" {{mod=+2}} {{rname=[Custom Crit Test](~-KP3DuZxrlN9cgrtL5mi|repeating_attack_-KTp5HZk-YHVkiBIUQJp_attack_dmg)}} {{rnamec=[Custom Crit Test](~-KP3DuZxrlN9cgrtL5mi|repeating_attack_-KTp5HZk-YHVkiBIUQJp_attack_crit)}} {{r1=$[[0]]}} {{always=1}} {{r2=$[[1]]}} {{range=}} {{desc=}}  ammo= charname=Dreduph Eckob","inlinerolls":[{"expression":"1d20cs>1 + 2[PROF]","results":{"resultType":"sum","rolls":[{"dice":1,"mods":{"customCrit":[{"comp":">=","point":1}]},"results":[{"v":18}],"sides":20,"type":"R"},{"expr":"+2","type":"M"},{"text":"PROF","type":"L"}],"total":20,"type":"V"},"rollid":"-KTpFykMkFYBR5m2EICF","signature":"0759ee9d5d59ca0c183c1ae5f98e9173d5c3930b24d0757b435d5f24793c17e2163186cc0dced0b40211f351ed57c9cfe1e7dc2b245c6b7c85a37a946d783761"},{"expression":"1d20cs>1 + 2[PROF]","results":{"resultType":"sum","rolls":[{"dice":1,"mods":{"customCrit":[{"comp":">=","point":1}]},"results":[{"v":3}],"sides":20,"type":"R"},{"expr":"+2","type":"M"},{"text":"PROF","type":"L"}],"total":5,"type":"V"},"rollid":"-KTpFykMkFYBR5m2EICG","signature":"3459ad6fdb0d8305aa20e91d493f9795c804543c4e33c2d7d2775b4cf41dde3d1af84cf3fb7d3a0fcbe580b5bda06a4a975fb5dc3f50cc631a05cf00b37aa757"}],"playerid":"-KGjaNNHFr84Ghmzccnh","rolltemplate":"atk","type":"general","who":"Scott C. (GM)"}
This means you could compare the rolls[X].results[0].v value to the customCrit[0].point value to determine if the result was a critical or not. You could determine what result to look at by regex matching {{r1=$[[X]]}} and {{r2=$[[Y]]}} to extract the proper array key to call for inlinerolls.

Hope that helps,

Scott
October 11 (8 years ago)

Edited October 11 (8 years ago)
Roll Templates can be coded to include Helper Functions (e.g. rollWasCrit()), which allow for information to be conditonally displayed in certain circumstances.

The {{crit}} properties used by the Roll Templates used by the 5e OGL sheet make use of rollWasCrit().
October 11 (8 years ago)

Edited October 11 (8 years ago)
Scott C. said:

This means you could compare the rolls[X].results[0].v value to the customCrit[0].point value to determine if the result was a critical or not. You could determine what result to look at by regex matching {{r1=$[[X]]}} and {{r2=$[[Y]]}} to extract the proper array key to call for inlinerolls.
That sounds awesome - but I don't know how to get to that log message...nor to pick it apart for the data that I need (like I said, Roll20 scripting n00b).  How do I get to that via the scripting API?
October 11 (8 years ago)

Silvyre said:

Roll Templates can be coded to include Helper Functions (e.g. rollWasCrit()), which allow for information to be conditonally displayed in certain circumstances. This is what's occuring in this instance.

Cool, that makes sense; what I need to know, then is how to listen to those helper functions and their results via the API so that I can branch my own logic off the result.
October 11 (8 years ago)

Edited October 11 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Ah, sorry, here's the code snippet I use to look at chat messages when I'm figuring out how to attack something like this (as well as the base for pretty much every script I make).

var PFIMPORT = PFIMPORT || (function() {
    'use strict';


    var version = '0.1.01',
        lastUpdate = 1465657571,
        schemaVersion = 1.01,


    ch = function (c) {
        var entities = {
            '<' : 'lt',
            '>' : 'gt',
            "'" : '#39',
    		'@' : '#64',
			'{' : '#123',
			'|' : '#124',
			'}' : '#125',
			'[' : '#91',
			']' : '#93',
			'"' : 'quot',
			'-' : 'mdash',
			' ' : 'nbsp'
		};


		if(_.has(entities,c) ){
			return ('&'+entities[c]+';');
		}
		return '';
	},
    
    checkInstall = function() {    
        log('-=> PFImport v'+version+' <=-  ['+(new Date(lastUpdate*1000))+']');
        if( ! _.has(state,'PFIMPORT') || state.PFIMPORT.version !== schemaVersion) {
            log('  > Updating Schema to v'+schemaVersion+' <');
            state.PFIMPORT = {
    			version: schemaVersion
			};
		};
	},
    
    HandleInput = function(msg_orig) {
    	var msg = _.clone(msg_orig),
	    args,
            attr,
            amount,
            chr,
            token,
            text='',
            totamount;
        
	log(msg);


        if (msg.type !== 'api' || !playerIsGM(msg.playerid)){
                return;
        }


		if(_.has(msg,'inlinerolls')){//calculates inline rolls
			msg.content = _.chain(msg.inlinerolls)
				.reduce(function(m,v,k){
					m['$[['+k+']]']=v.results.total || 0;
					return m;
				},{})
				.reduce(function(m,v,k){
					return m.replace(k,v);
				},msg.content)
				.value();
		}
        
		args = msg.content.split(/\s+/);//splits the message contents into discrete arguments
		switch(args[0]) {
        
		}
	},
    
    RegisterEventHandlers = function() {
        on('chat:message', HandleInput);
    };
    
    return {
        CheckInstall: checkInstall,
    	RegisterEventHandlers: RegisterEventHandlers
	};
    
}());




on("ready",function(){
    'use strict';
    
    PFIMPORT.CheckInstall();
    PFIMPORT.RegisterEventHandlers();
});
*HEAVILY copied from The Aaron's scripts in general, as most of my script architecture is. I've bolded the log that outputs all the msg info. There's some extraneous variables in there from various other scripts, but it should show you what you need.

EDIT: as far as I know there isn't a way to listen to the roll template helper functions as they are part of the sheet (maybe sheetworkers) and don't output anything to the API.
October 11 (8 years ago)

Scott C. said:

Ah, sorry, here's the code snippet I use to look at chat messages when I'm figuring out how to attack something like this (as well as the base for pretty much every script I make).
Thanks, this looks like the best start I've gotten; just need to figure out how to break apart the message and pick the "CRIT" out if it's there (I think).  This gets me another 50% of the way (given that I started around 2%, that's a hefty lift!).
October 11 (8 years ago)

Edited October 11 (8 years ago)
The Aaron
Pro
API Scripter
On a similar topic, here's a simplified version function I use for debugging similar things:
var debugDisplay = (function(){
    "use strict";

	var esRE = function (s) {
		var escapeForRegexp = /(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g;
		return s.replace(escapeForRegexp,"\\$1");
	},

	htmlEntities = (function(){
		var entities={
			' ' : '&'+'nbsp'+';',
			'<' : '&'+'lt'+';',
			'>' : '&'+'gt'+';',
			"'" : '&'+'#39'+';',
			"*" : '&'+'#42'+';',
			'@' : '&'+'#64'+';',
			'{' : '&'+'#123'+';',
			'|' : '&'+'#124'+';',
			'}' : '&'+'#125'+';',
			'[' : '&'+'#91'+';',
			']' : '&'+'#93'+';',
			'"' : '&'+'quot'+';'
		},
		re=new RegExp('('+_.map(_.keys(entities),esRE).join('|')+')','g');
		return function(s){
			return s.replace(re, function(c){ return entities[c] || c; });
		};
	}());

	return function(i){
		sendChat( 'Debug',`/w gm <div style='border-width: 1px ; border-color: #0000ff ; border-style: solid ; border-radius: 5px ; padding: 5px 5px ; background-color: white ; font-size: 10px ; white-space: pre ; font-family: "pragmatapro" , "consolas" , "inconsolata" , monospace'>`+
					htmlEntities(JSON.stringify(i,undefined,'   ')||'').replace(/\n/g,'<br>')+
				'</div>'
		);
	};
}());
And here are a few examples of using it:
on('ready',function(){
	"use strict";

    sendChat('test','/roll 1d20+1d10',(o)=>{
        debugDisplay(o.content);
    });
    sendChat('test','[[1d20+1d10]]',(o)=>{
        debugDisplay(o.content);
    });
});

This outputs to the chat in a format that's easier to follow than just raw JSON on a single line.



October 11 (8 years ago)
Although TBH parsing this JSON looks like a nightmare; I might just start off with a simple !crit [type] command and go from there.  Sad that I can't automate the trigger easily, but that's a 20% problem that's going to take 80% of my time.
October 11 (8 years ago)

Edited October 11 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
You can parse it pretty easily, this is what I whipped up real quick to extract the (minimum) appropriate parts of the msg.
var r1,
r2,
dmg1type,
dmg2type;
if(_.has(msg,'rolltemplate') && _.has(msg,'inlinerolls')){
    if(['atk','atkdmg'].indexOf(msg.rolltemplate)>-1){
        r1 = msg.inlinerolls[msg.content.match(/(?:\{\{r1=\$\[\[(\d)]]}})/)];
        r2 = msg.inlinerolls[msg.content.match(/(?:\{\{r2=\$\[\[(\d)]]}})/)];
        dmg1type = msg.inlinerolls[msg.content.match(/(?:\{\{dmg1type=(\w+)}})/)];
        dmg2type = msg.inlinerolls[msg.content.match(/(?:\{\{dmg2type=(\w+)}})/)];
    };
};


EDIT: was wondering when we'd attract the attention of Arcane Scriptomancer Aaron :D.
October 11 (8 years ago)
It's not just extracting the values, it's knowing what they need to be compared to, making that comparison, then doing something based on that result.  Honestly, a simple "isCrit" boolean value somewhere would make this easy as pie (hell, isCrit() is a helper function, so it's gotta be available somewhere in the system), rather than parsing obscured values to base this on.

Thanks for the help -- I'm going to step away and come back with a clean head on this guy.
October 11 (8 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Heh, the problem isn't telling if a roll is a crit, it's telling where in the roll template that critical is. The helper function that Silvyre linked to above was specifically for when you are programming the roll template which is part of the sheet, and (Aaron or someone else may have to correct me here) runs on a separate API than the API scripts which means they don't talk to each other yet (I believe there are plans to remedy at least some of this in the future).

Anyways, good luck on coming back to it.
October 11 (8 years ago)
The Aaron
Pro
API Scripter
You're correct.  It's kind of a pseudo-function that tells the template parser how to treat that section of the roll template.

In the simple case, a roll is a crit if the value of the roll is equal to the number of sides.  It gets more complicated when you have things like [[1d20cs>19]], which will be a crit on a 19 or 20 (> in dice parlance is >=).  In that case, there will be a mods key on the roll:
                  "mods": {
                     "customCrit": [
                        {
                           "comp": ">=",
                           "point": 19
                        }
                     ]
                  },
Since the cs modifier can be specified multiple times, you can end up with multiple entries.  This shows the 3 possible types, all on one roll: [[1d20cs<3cs11cs>16]]
                  "mods": {
                     "customCrit": [
                        {
                           "comp": "<=",
                           "point": 3
                        },
                        {
                           "comp": "==",
                           "point": 11
                        },
                        {
                           "comp": ">=",
                           "point": 16
                        }
                     ]
                  },
Each type can be repeated multiple times.  Example: [[1d20cs1cs3cs5cs7cs9cs11cs13cs15cs17cs19]]
                  "mods": {
                     "customCrit": [
                        {
                           "comp": "==",
                           "point": 1
                        },
                        {
                           "comp": "==",
                           "point": 3
                        },
                        {
                           "comp": "==",
                           "point": 5
                        },
                        {
                           "comp": "==",
                           "point": 7
                        },
                        {
                           "comp": "==",
                           "point": 9
                        },
                        {
                           "comp": "==",
                           "point": 11
                        },
                        {
                           "comp": "==",
                           "point": 13
                        },
                        {
                           "comp": "==",
                           "point": 15
                        },
                        {
                           "comp": "==",
                           "point": 17
                        },
                        {
                           "comp": "==",
                           "point": 19
                        }
                     ]
                  },
Note that there are also other modifiers: [[6d20cf<3cs>19rokh1>3]]
                  "mods": {
                     "customCrit": [
                        {
                           "comp": ">=",
                           "point": 19
                        }
                     ],
                     "customFumble": [
                        {
                           "comp": "<=",
                           "point": 3
                        }
                     ],
                     "keep": {
                        "count": 1,
                        "end": "h"
                     },
                     "reroll": [
                        {
                           "maxrerolls": 1
                        }
                     ],
                     "success": {
                        "comp": ">=",
                        "point": 3
                     }
                  },

Anyway, if you're after crits, your isCrit() function would need to parse the mods section of the roll and for each entry in customCrit build up a rule engine that can handle determining if a roll is a crit.

October 11 (8 years ago)

Edited October 11 (8 years ago)
The Aaron
Pro
API Scripter
This should get you started... =D
on('ready',function(){
    'use strict';

  // builds a comparison function for all the crit rules
  var getCritComparitor = function(roll){
    let comp=[];

    // handle explicit custom rules
    if(_.has(roll,'mods') && _.has(roll.mods,'customCrit')){
      _.each(roll.mods.customCrit,function(cc){
        switch(cc.comp){
          case '<=':comp.push((o)=>o<=cc.point);break;
          case '==':comp.push((o)=>o==cc.point);break;
          case '>=':comp.push((o)=>o>=cc.point);break;
        }
      });
    } else {
      // default "max value" rule
      comp.push((o)=>o==roll.sides);
    } 
    // return a comparison function that checks each rule on a value 
    return function(v){
      let crit=false;
      _.find(comp,(f)=>crit=crit||f(v));
            return crit;
    };
  };

  var isCrit = function(roll){
    // builds a comparison function for crits in this roll type
    let comp=getCritComparitor(roll);
    _.each(roll.results,(r)=>{
      // check each value with the comparison function
      if(comp(r.v)){
        // If it was a crit, report it to chat 
// (replace with what you want or return true here and false outside the if for an inspection function)
        sendChat('isCrit',`The ${r.v} is a critical!`);       }     });   };   on('chat:message',function(msg){     if(msg.inlinerolls){ // for each inline roll // call isCrit() on each each entry in rolls       _.each(msg.inlinerolls,(ir)=>_.each(ir.results.rolls,(irrr)=>isCrit(irrr)));     }   }); });


(Note that the 1s in the image are both crits and fumbles for this roll)
October 11 (8 years ago)

The Aaron said:

This should get you started... =D

This is awesome - I moved on to just create the !crit [type] command which I'd need the logic for anyway.  Once I get that done, though, this should give me a good start to figuring out how to automate it completely!!
October 11 (8 years ago)
The Aaron
Pro
API Scripter
Thanks!  It was a fun exercise. =D