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

Need help with custom roll parsing.

February 26 (2 years ago)

Edited February 26 (2 years ago)

I have read the documentation: https://help.roll20.net/hc/en-us/articles/4403865972503-Custom-Roll-Parsing-for-Character-Sheets#GettingStarted-BuildingaCharacterSheet

and I have gotten Oosh's example working from the thread 'Adventures with startRoll(); which included pulling in the .js library from Oral Orcs: https://app.roll20.net/forum/post/10346883/slug%7D    and  https://github.com/onyxring/Roll20OralOrcs/blob/master/OralOrcs.pdf

Here is the background for my challenges...

I don't need the re-roll functionality that Oosh's example has. I simply need to parse the results of a single 'startRoll()' call.. On the surface, what I have to do is straight-forward. The implementation is proving troublesome.

In my basic sheet, my roll macros leverage a lot of built-in functionality. Here is an example rollstring:
&{template:trait} {{name=@{character_name}}} {{skill=Agility}} {{Agility Roll=[[{1d@{agility}, @{player}d6!}kh1 + ?{Modifier IE: -2 or 2|0}[Modifier] - @{fatigue}[Fatigue] - @{wound}[Wounds] - floor(@{anxiety} / 3)[Anxiety]   ]]        }} {{fatigue=@{fatigue}}} {{wounds=@{wound}}} {{anxiety=@{anxiety} }}  {{Madness = [[@{madness}d6[Madness]]]}} 

The request from my players is to highlight when Critical Failures are rolled, or when the first die of from two rolls are both 1's. I wasn't able to find a way to do that using the basic sheet functionality. So... I am experimenting with custom roll parsing. I have been successful with a very basic rolls. For example, the following works well:

const rollstring = '&{template:trait} {{name=@{character_name}}} {{skill=Agility}} {{roll1=[[{1d@{agility}, @{player}d6!}kh1 }}';

const results = await startRoll(rollstring);

const thedice= results.results.roll1.dice;

const firstroll=thedice[0];

const secondroll=thedice[1];

var critFail = 0;

if(firstroll == 1 && secondroll == 1) critFail=1;

finishRoll(

results.rollId, {

roll1: critFail,   

}

);

If I attempt to add some of the features from the rollstring above, the 'results' variable is not the same structure. For example:
const results = await startRoll(rollstring);
const thedice= results.results.roll1.dice;  <- this triggers an exception when extra stuff is in the rollstring:  Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'dice')

I am hoping that someone can point me to some more robust documentation, or even better, a working example of startRoll() that is processing more complex rollstrings.


Any help or direction is greatly appreciated. 

Kind regards,

-Sean

February 26 (2 years ago)
GiGs
Pro
Sheet Author
API Scripter

When I have a problem with this, I use console.log and peel back the results object to figure out what is actually inside it.

const results = await startRoll(rollstring);
const thedice= results.results.roll1;
console.log(thedice);
return;

Then if I havent figure out, go back an extra level

const results = await startRoll(rollstring);
const thedice= results.results;
console.log(thedice);
return;

and if necessary

const results = await startRoll(rollstring);
const thedice= results;
console.log(thedice);
return;

By analysing those I can see what results.results.roll1.dice is.

Though my guess is roll1 is not an array.


Looking up, I see I could have saved myself some typing - it looks like this might just be a syntax error. In your rolltemplate:

{{roll1=[[{1d@{agility}, @{player}d6!}kh1 }}

Notice there's no inline roll closing brackets. This should probably be

{{roll1=[[{1d@{agility}, @{player}d6!}kh1 ]] }}


February 26 (2 years ago)

Edited February 27 (2 years ago)

Thank you for your reply... The syntax error is a cut-and-paste mistake. The code has the proper closing ]]. But... You've made me notice another syntax issue that I need to work on. I will see if this takes care of the issues I'm having. 

Below is the utility function I use to explore datatypes that are unknown, and the code below is what is currently in my code. Currently, this function will trigger an exception with the complex rollstring. Before sharing any more details, I want to validate that I have the proper syntax.

function showProps(obj, objName) {

let result = "";

for (const i in obj) {

// Object.hasOwn() is used to exclude properties from the object's

// prototype chain and only show "own properties"

if (Object.hasOwn(obj, i)) {

result += `${objName}.${i} = ${obj[i]}\n`;

}

}

console.log('object='+obj);

console.log('properties='+result);

}


...

const results = await startRoll(rollstring);

console.log('-----------------');

showProps(results, 'results');

console.log('-----------------');

showProps(results.results, 'results.results');

console.log('-----------------');

showProps(results.results.roll1, 'results.results.roll1');

console.log('-----------------');

showProps(results.results.roll1.dice, 'results.results.roll1.dice');

console.log('-----------------');


February 27 (2 years ago)

After several hours of trial and error, and after fixing a couple missing {}, I have figured out that the order of the contents of the rollstring has a significant impact on the results. I don't know how the engine works, but I have got things working with one final exception. In the rollstring there is an Roll Query:  ?{Target Number IE: 4, 6, 8, etc.}[TN]

How do I pull the number entered for the Roll Query from the results? There was no clear property listed in any of the objects.

February 27 (2 years ago)
GiGs
Pro
Sheet Author
API Scripter

If you put that inside an inline roll and set it's own key, you'll be able to access it in the rolltemplate and the CRP, like

{{modifier=[[? ?{Target Number IE: 4, 6, 8, etc.}]]}}

You can use it in your roll as normal, but creating a key for it here means you can access it directly.

February 27 (2 years ago)

Edited February 27 (2 years ago)
GiGs
Pro
Sheet Author
API Scripter

To answer you specific example, the syntax looks like it should work. I prefer to print the entire object, rather then use a loop like for..in, so I can see it in completion.

You can expand the object in the console.


PS: is this if necessary?

if (Object.hasOwn(obj, i)) {

You are inside a loop, and obj is previously defined, and i will be created on each iteration of the loop so has to exist.

February 27 (2 years ago)

Edited February 27 (2 years ago)

You ask expert questions of a 'novice'. I copied that code from somewhere and it answered my questions.

To give you some context, I am an old-school developer from way back - C++/Windows in the mid 80s and Smalltalk/Windows/OS2 in the mid-90's to 2000. I think that I was pretty good at coding 'back in the day'. However, since then I've been a systems engineer and now a chief architect. While I am familiar with coding and software, I have not really coded much in the past 20 years. I did some HTML/Javascript in the early 2000's, but it would be a mistake to call me an HTML5/CSS/Javascript developer. I can spell JQuery, Angular, and React, and I know that technically it's called ECMA Script, but that's mostly buzzword compliance on my part. I can "hack' pretty well with some examples to plagiarize, though. :)

So... Exploring custom sheet development is my way to get back into development again, as a personal hobby of sorts. Your input and your questions has been an enormous help in my learning. Maybe someday I'll muster the courage to post my sheet to the Git repository for someone to learn from what I've learned. 

I played with that showProps() function and agree that filter doesn't really do anything worthwhile. I've deleted it from my code. 




February 27 (2 years ago)
GiGs
Pro
Sheet Author
API Scripter

You've jumped in at the deep end then :) if you have started with CRP and Oral Orcs (by the way, the latter is IIRC discouraged now - its very inefficient).


Can you post the actual rolls you are using, not simplified versions of them, and we might be able to figure out a solution, If I understand your first post, you want to highlight if a roll is a critical failure, or if the first (?) of two dice is a 1.

How do you decide which die is first? What is that die being rolled for?

February 27 (2 years ago)

Edited February 27 (2 years ago)
Oosh
Sheet Author
API Scripter

PS: is this if necessary?

if (Object.hasOwn(obj, i)) {

You are inside a loop, and obj is previously defined, and i will be created on each iteration of the loop so has to exist.

Just to expand on that - the prototype properties on a standard object are non-enumerable, and won't be visited by a for...in loop - so you generally don't need to worry about checking for own property on vanilla JS objects - the only keys the interpreter iterates over are the ones you've defined (unless you've gone and messed with the Object prototype.... but the solution there is "don't mess with the Object prototype")

If you're extending a Class, and you only want the subclass properties, then hasOwn() is exactly what you'd want to use.




February 27 (2 years ago)

Edited February 28 (2 years ago)

This system is Savage Worlds, if that helps at all. Here is the essence of the roll. There are parameters that vary the die, but this captures the important details.

{{ roll1= [[{1d8!, 1d6!}kh1 ]]  }}

This is the parameterized version.

{{roll1=[[{1d@{agility}, @{player}d6!}kh1  ]] }}


If the 1d8! and the 1d6! both result in a 1, it is a critical failure. The first thought would be to check the result for 2 to determine a critical failure. There are modifiers to the result: modifiers for fatigue, for wounds, and a Roll Query. In addition, I have added my own modifiers. So that simple approach doesn't work.


Here is my current action-function to perform the rolls.

    on('clicked:dev_test-action', async (eventInfo) => {

const rollstring = await MakeRollString('agility', 'devtest', true, true, 'throwing', '  {instruction=nothing for Agility} ');

const results = await startRoll(rollstring);

const theRolldice= results.results.Roll.dice;

const firstroll=theRolldice[0];

const secondroll=theRolldice[1];

let theRoll= results.results.Roll.result;

let critFail = 0;

if(firstroll == 1 && secondroll == 1) critFail=1;

const theMadnessdice = results.results.Madness.dice;    //if there is no success (first and second rolls are less than TN,

                                                                                             // and if either first or second roll are a 1

            // and if any madness dice are 1, then critFail = true

const theCorruptiondice = results.results.Corruption.dice; // Failure (less than TN of 4) = add one corruption.

const theTN = results.results.TN.result;

let successes = 0;

let runningTotal = theRoll;

while(runningTotal >= theTN)

{

successes++;

runningTotal -= 4;

}

let raises = 0;

if (successes > 1) raises = successes - 1;

finishRoll(

results.rollId,

{

Roll: critFail,  

Raises: raises, // this is wrong and doesn't work

}

);

    });


Here is my current function to make the roll-string.

async function MakeRollString(statName, templateName, includeMadness, includeCorruption, damageType, instructionString) {

let madnessString = '';

let corruptionString = '';

let damageString = '';

const trainedString = statName+'Trained';

const statString = toTitleCase(statName);

const tnString = ' {{TN=[[0d2 + ?{Target Number IE: 4, 6, 8, etc.}]] }}';

if (includeMadness == true) {

madnessString = ' {{Madness=[[@{madness}d6[Madness]]]}}';

}

if (includeCorruption == true) {

corruptionString = ' {{Corruption=[[1d@{corruption}[Corruption]]]}}';

}

switch (damageType) {

case 'fighting':

damageString = ' {{Damage=[[@{weapon_damage}[Weapon Damage] + 1d@{strength}[Strength Bonus] + @{meleedamagemod}[Melee Damage Mod]]]}}';

break;

case 'shooting':

damageString = ' {{Damage=[[@{ranged_damage}[Weapon Damage] + @{ranged_damage_bonus}[Strength Bonus] + @{shootingdamagemod}[Shooting Damage Mod]]]}}';

break;

case 'throwing':

damageString = '{{Damage=[[1d4![Throwing Damage] + 1d@{strength}[Strength Bonus] + @{throwingdamagemod}[Thrown Damage Mod]]]}}';

break;

default: // do nothing, leave string as '';

}

const result = '&{template:'+templateName+'}   {{name=@{character_name}}}  {{Roll=[[{1d@{'+statName+'}, @{player}d6!}kh1 - {(1 - @{'+trainedString+'}) * 2}[Untrained Mod]    + ?{Modifier IE: -2 or 2|0}[Modifier]  - @{fatigue}[Fatigue] - @{wound}[Wounds] - floor(@{anxiety} / 3)[Anxiety]    ]]  }} {{skill='+statString+'}} {{trained=@{'+trainedString+'}}} {{fatigue=@{fatigue}}} {{wounds=@{wound}}} {{anxiety=@{anxiety}}}' + madnessString + corruptionString + damageString + tnString + instructionString;

return result;

};




February 28 (2 years ago)

Edited February 28 (2 years ago)
GiGs
Pro
Sheet Author
API Scripter

Aha that second die is a wild die.

I might not have much time to look today, but one thing I'd say. For savage worlds, I wouldn't recommend using this:

{{ roll1= [[{1d8!, 1d6!}kh1 ]]  }}

Instead use

{{ roll1= [[{1d8!!, 1d6!!}kh1 ]]  }}

The !! explode produces bigger dice, so if you roll 8 then 4, you get a total of 12. With the first version you get separate dice of 8 and 4, and the kh1 modifier won't produce the correct number (It'll just give 8 instead of 12).

February 28 (2 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

I certainly understand the desire to make your own sheet, but I would point out that there is a very powerful official SWADE sheet already available that is supported and maintained by Sigil. If that sheet doesn't meet your needs or you just want to build your own, it may be worth looking at some of the code to see how others have tackled similar issues. I will note though that the official SWADE sheet doesn't use CRP as it was built before CRP existed.

March 01 (2 years ago)

Hi Scott, Can you post a link to the SWADE character sheet? I would like to take a look at what they're doing. I looked in the GitHub repository, https://github.com/Roll20/roll20-character-sheets, but couldn't find anything related to SWADE.

March 01 (2 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

It's the official savage worlds sheet.

March 01 (2 years ago)

Edited March 01 (2 years ago)
GiGs
Pro
Sheet Author
API Scripter

I think Scott's suggestion of checking out the existing savage worlds sheet is a great one. From what I hear, it's pretty mature and you may find you don't need your own sheet.

In the meantime, some ideas to consider: I would rewrite your MakeRollString function something like this:

async function MakeRollString(statName, templateName, includeMadness, includeCorruption, damageType, instructionString) {
        const damage_strings = {
            fighting: ' {{Damage=[[@{weapon_damage}[Weapon Damage] + 1d@{strength}[Strength Bonus] + @{meleedamagemod}[Melee Damage Mod]]]}}',
            shooting: ' {{Damage=[[@{ranged_damage}[Weapon Damage] + @{ranged_damage_bonus}[Strength Bonus] + @{shootingdamagemod}[Shooting Damage Mod]]]}}',
            throwing: ' {{Damage=[[1d4![Throwing Damage] + 1d@{strength}[Strength Bonus] + @{throwingdamagemod}[Thrown Damage Mod]]]}}'
        };
        let damageString = damage_strings.hasOwnProperty(damageType) ? damage_strings[damageType] : '';

        const madnessString = includeMadness ? ' {{Madness=[[@{madness}d6[Madness]]]}}' : '';
        const corruptionString = includeCorruption ? ' {{Corruption=[[1d@{corruption}[Corruption]]]}}' : '';
       
        const trainedString = statName+'Trained';
        const statString = toTitleCase(statName);
        const tnString = ' {{TN=[[0d2 + ?{Target Number IE: 4, 6, 8, etc.}]] }}';

        const result = '&{template:'+templateName+'}   {{name=@{character_name}}}  {{Roll=[[{1d@{'+statName+'}, @{player}d6!}kh1 - {(1 - @{'+trainedString+'}) * 2}[Untrained Mod]    + ?{Modifier IE: -2 or 2|0}[Modifier]  - @{fatigue}[Fatigue] - @{wound}[Wounds] - floor(@{anxiety} / 3)[Anxiety]    ]]  }} {{skill='+statString+'}} {{trained=@{'+trainedString+'}}} {{fatigue=@{fatigue}}} {{wounds=@{wound}}} {{anxiety=@{anxiety}}}' + madnessString + corruptionString + damageString + tnString + instructionString;
        return result;
    };

This uses two techniques. One is the ternary operator, where you can present an if statement like this:
const something = question ? value if true : value if false;
This is a really good approach iMO for simplifying if statements whose only prupose is to calculate a single value. You don't have to pre-declare the variable, and have an if statement to get its value - just do it all at once.

Sevondly it uses an object in place of the switch (I rarely find myself using switch these days, there's usually a simpler alternative). This command:
damage_strings[damageType]

looks at the object named damage_strings, and finds the key named damage_type, and grabs that value.

The rest of that line is a ternary operator that looks to see if the damageType exists in damage_strings, and if not, uses ''.


Another thing to try in your attack rolls: you could break the skill roll, the wild card roll, and modifiers into separate {{ = }} properties, then you can check skill and wild dice to see if they are both 1/critical faulure, and in the worker determine what the highest value is then add the modifiers. (Though for the attack rolls, you probably don't need to use CRP.)

March 01 (2 years ago)

Edited March 01 (2 years ago)

Thank you for this provocation. There are some great things in there, and I learned a lot just by walking through the code. I'd like my knowledge and skills at level that created this sheet. :)

This certainly makes me re-think what I am doing. My campaign has many modifications, so do I a) copy some of the code from the official sheet into mine, or do I b) go back to the official and re-make my modifications? Either way, it's going to be a significant amount of work. My sheet and campaign have been 'in development' for a year. In other words, we've been playing for a year and I have been making incremental improvements to the sheet as the players have made suggestions. We started with a different Savage Worlds character sheet (Savage Worlds - Fallout fan conversion) and now I wish I would have started with this one.


Here is a list of some custom modifications that I've made to the game and my sheet:

1) All high-tech Edges, Skills, and Hindrances have been removed. The campaign is set in 15th century Wallachia/Transylvania/Romania with a 'Gothic Horror' theme. 
2) All of the Arcane Powers have been replaced. The campaign is 'low fantasy' with minimal supernatural abilities. 
3) A construct has been added for Anxiety (similar to Sanity in Call of Cthulhu).
4) A construct has been added for Madness (worse than Anxiety with a different effect).
5) A construct has been added call Corruption, which is a side-effect of using Arcane Powers for selfish intent.
6) Skills added/removed/changed to match the theme of the campaign.
7) I have updated the sheet from legacy to CSE. 
8) I have added weapon traits like Zweihander ( Reach, Finesse, Vicious, etc.)
9) Edges, Hindrances, and Adventure Cards have been added/removed/changed to match the theme of the campaign.

Some things that I like about my sheet.

1) It is less than 1/3 of the lines of code of the official sheet - both in the HTML and CSS. With what I've learned from reviewing this code, I can likely cut the size of mine in half.
2) It has been updated to CSE.
3) I have removed html tables from the code and use CSS instead.
4) I have all of the Hindrances, Edges, and Adventure Cards entered with 'fly-over' help.
5) I copied the checkbox styling from the Zweihander character sheet. 
6) I have updated the code to follow the 'Responsive Design' patterns. In other words, the fields resize as the screen is resized. The players call it "rubber-bandy".
7) I have not followed standards that support translation, which makes for less work.


Lots to ponder... When I figure out what I'm going to do, I'll post an update. This seems like an example that others can learn from.