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

[HELP | SCRIPT] Resolving Character Attribute Formula

March 12 (4 years ago)

Hi all,

I got an issue regarding the scripting using character attributes, which are using a formula to be computed. Let's be a little bit more precise.

I am using the "The Witcher TRPG" character sheets and I want to access the current stats and skills, e.g. the current stat for "Reflexes". The attribute in the character sheet is "ref" but the value is no number but the following formula:

[[((floor((@{total_ref}+@{stat_mod_ref})@{wound_mod})+1)+abs((floor((@{total_ref}+@{stat_mod_ref})@{wound_mod}))-1))/2]]

I am able to get exactly this formula extracted from a currently selected token, which represents a character sheet:

on('ready',function(){
    "use strict";

    on("chat:message", function(msg) {
        let targetID, selectedCharacterRef;
        // Check if we have an api message including the command !test
        if(msg.type == "api" && msg.content.indexOf("!test") !== -1) {
        
            let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname');

            // parse arguments into a hierarchy of objects
            let args = msg.content.split(/\s+--/).map(arg=>{
                let cmds = arg.split(/\s+/);
                return {
                    cmd: cmds.shift().toLowerCase(),
                    params: cmds
                };
            });

            let selected = args.find(c=>'selected' === c.cmd);
            let target = args.find(c=>'target' === c.cmd);
            if(target){
                targetID = target.params[0];
            }
            if(selected){
                selectedCharacterRef = getAttrByName(selected.params[0],'ref');
                if(selectedCharacterRef)
                {
                    sendChat("Success", "Test");
                    log(selectedCharacterRef);
                    sendChat("API",selectedCharacterRef);
                    //"[[((floor((@{total_ref}+@{stat_mod_ref})@{wound_mod})+1)+abs((floor((@{total_ref}+@{stat_mod_ref})@{wound_mod}))-1))/2]]"
                }
                else
                {
                    sendChat("Fail","No selected character found");
                }
            }
        }
    });
});

The error occures at the line "sendChat("API",selectedCharacterRef);" throwing an exception:

For reference, the error message generated was: SyntaxError: Expected "[" but "o" found. undefined

Apparently, I am missing something or I am running into some kind of bug? Every advise is gladly welcome!

If anyone needs more information please let me know.


Best regards and thanks in advance!

March 12 (4 years ago)

Edited March 12 (4 years ago)
timmaugh
Forum Champion
API Scripter

What are you trying to do with that string, once you have it? Because if you just un-remarked your lines, it's not going to like it... and those @{total_ref} @{stat_mod_ref} type references aren't going to resolve outside of the context of the character sheet.

It looks like you're trying to derive the selected character, then you'd want to pull the component parts of this roll equation out of the sheet and run it yourself? If so... and you're passing in a character reference data to the '--selected'  argument, you're going to have to use the id of that character to get the @{total_ref}, @{stat_mod_ref}, because those won't work outside of the sheet. Use the getAttrByName() to get the value, or findObjs() if you need to do something more involved.

Once you have those component pieces of information, you don't even need the inline roll equation... you can just handle the math yourself:

let total_ref = getAttrByName('total_ref',selectedCharacterRef);
let stat_mod_ref = getAttrByName('stat_mod_ref',selectedCharacterRef);
let wound_mod = getAttrByName('wound_mod',selectedCharacterRef);
let output = ((Math.floor((total_ref+stat_mod_ref)...

The problem is that your equation, as written, seems to expect @{wound_mod} will have some math operation in it (ie, +1, or -2, etc.). Because as written, that equation will break when there is no operator between the paren before and the wound mod. You will need to parse that, I think...

EDIT: Had to run to a meeting, so I didn't mean for that to be left without more info. If you have to parse the wound_mod, you're probably looking at data like I mentioned: +1, or -2, or +3. I can't imagine that with the inline equation structured the way it is that you would have a positive number without the positive sign, because that would break the equation, otherwise. So the parsing you would have to do would be to remove the '+' from the front. Then you could use an addition operator, yourself, to add what is left (even if negative). Something like:

let output = ((Math.floor((total_ref+stat_mod_ref) + Number(wound_mod.replace(/^+/,'')))+1)+Math.abs((Math.floor((total_ref+stat_mod_ref) + Number(wound_mod.replace(/^+,''))))-1))/2

I think that should work. If you have data that looks different than my assumptions, post back and we can tweak the process.

March 12 (4 years ago)

Hi timmaugh,

thank you very much for your reply. I will take a deeper look in your answer probably tomorrow. My assumptions were made on the basis of several other forums post, which suggested to use the build in Dice Roller Engine and send the formula to the chat. The Dice Roller Engine will then resolve the formula but i guess I just did a misinterpretation on my side.

If you don't mind I would like to explain my whole idea shortly so maybe you can give a short hint if I am on the correct track :-)

I want to be able for me and my players to select a token and attack a target. The Dice rolls shall be performed automatically. If the attack is higher than the defence the target location should be rolled and the damage applied to the target health.

So here is what I already THINK I know :-D

I can create a Token Action Macro which is emiting an API call using

!meleeCombat --target @{target|character_id}

On button click this will ask the user to select a target. From my API script I can already dissolve the input into the character_ids for the selected character and the target character. As I understand from your above post I should focus on doing the math inside the API avoiding unnecessary Dice rolls. So here I think of 2 Options:

1) I totally neglect the Dice Roller and use the built-In randomInteger function to perform a "Dice roll" inside the API, perform all logical operations and just present my players the result.

2) I know my players really love the Dice rolls because it puts some kind of a thrill in the game when the 3D dice are rolling over the screen (Feels more like classic in person p&p). As far as I know I may perform inline rolls using something like:

sendChat("API","/roll [[" + numberOfDice + "d10" + modifier +"]]");

This done I can use the Cookbook function for parsing inline rolls to get the total value of this roll. And here is my first real question:

If I use the sendChat function to perform a roll how am I able to remember the previously computed values for other attributes in another "on(chat:message)" body?. I think this has to be something like a global variable or do variables defined out of scope persist through the whole session? I read about saving state variables, which also persist through several sessions but this is not what I need.

As far as this is working and I get my Dice rolling I think I can use the public script "TokenMod" to modify the health and stamina bars of my tokens.

I hope what I just explained is somewhat understandable I only started using the API two days ago.

Any suggestions are much appreciated! If you need more information please let me know. I would love to get a little of advice before I head into the wrong direction.

Best regards and thank you very much for your advise!

Julian






March 12 (4 years ago)

Edited March 12 (4 years ago)
timmaugh
Forum Champion
API Scripter

Alright, alright, alright... a couple of things to unpack...

It's a bug (feature?) of the chat interface that if you use a target statement (@{target|character_id}), your message object (javascript) will not have a selected property. Maybe you were getting around that by providing the id of the originating token in that --selected argument, I just didn't see that reflected in your token action macro, above. All of that said, there is a way a combination of SelectManager and APILogic can help with that.

(I am a bit of a MetaMancer in that I've written stuff that plays in other script's messages... so the solution would probably work with what you're doing already, FYI)

If you want to know how to make that work for your application, post back. For now, let's assume that you're passing in the id of the source token for the attack in the --selected argument.

Next question is about the rolls and 3d dice...

If you want to see the dice, you have to send a simple (non-api) message to the chat:

/r 2d10
[[2d10]]
I roll [[2d10]]

If you send an api message that contains rolls (whether you submit them to the dice engine to create inline rolls or utilize the randomInteger() function to create them yourself), you won't see the dice, unfortunately. (I see a potential meta-script opportunity, there, but I will have to do some testing. That day is not today.) All of that means you won't be able to use the cookbook function to extract the roll total.

That means you're at the point of handling the rolls in an api message. You don't have to generate the rolls yourself (via randomInteger()), but you can. You can do inline rolls and unpack them using the cookbook function, or using the libInline library (it parses the roll for you and gives you easy handles for the data).

Now, to store information between calls to your script (if you need to), you could use the state variable, but that's probably better for script-level stuff (configuration, feature footprints, etc.). You mentioned global variables, but those can pollute the global namespace. Another coding option you have instead of global variables is script-scoped variables where your script's closure remains in memory. You can accomplish this with an IIFE:

const myscript = (() => {
    // script work goes on here
    let taco = {};          const handleInput = (msg) => {         // stuff to do when you get a message goes here         let boomboom = msg.selected;     };     on('ready', () => {         on('chat:message', handleInput);     };
    return {
        // public interface
    };
})();

In that case, boomboom will be reset every time you get a new message, but taco will be tracked between, and can be referenced and used in your handleInput.

(Not that that should stop you from using boomboom. "A big, strong man like you isn't afraid of a little boomboom?")

Aaron has a good template of this structure (called the revealing module pattern) here. It might be more than your script requires, but it is a way to implement session-permanent variables.

There's a lot of moving parts... the trick is to really just focus on the parts that  you need to actually accomplish the goal you're aiming at!

March 15 (4 years ago)

Hey timmaugh,

I was able to dig a little bit deeper into the API. Thank you very much for your detailed answer and sorry for the incomplete code fragment. Of course you are right my macro call looks like this:

!meleeCombat --selected @{selected|character_id} --target @{target|character_id}

Late night thoughts should be revised after a good sleep :)

I think understand the API now to an extend, where I may achieve everything I want. One last question arises, though. I don't really get how I am supposed to use the public interface provided by the return statement in a usefull way. I had a look at the revealing module pattern from Aaron and the cookbook explanation but I reallycan not think of a scenario where this might be uselfull.Am I able to call these public functions via other scripts?

I will most likely post my first full solution to my above problem here, as soon as I get one. I think there will be a lot of possibilities for efficency improvements and I would love to here your opinion and further suggestions :)


Best regards and have a nice week!

Julian


March 15 (4 years ago)
timmaugh
Forum Champion
API Scripter

Yep, that public interface is where you could expose anything you want another piece of code to be able to access. For instance, that libInline script doesn't listen for a chat event, but it does expose a few functions for other scripts to call:

    return {
        getRollData: getRollData,
        getDice: getDice,
        getValue: getValue,
        getTables: getTables,
        getParsed: getParsed,
        getRollTip: getRollTip
    };

That's how you would access it. By passing in an array of inline rolls (or some filtered set of inline rolls), you could use the getDice() function to get some subset of the dice, the getRollTip() function to get the little hover message, etc. The getRollData function would get all of the various pieces:

let rolldata = libInline.getRollData(msg.inlinerolls);

There are quite a few helper scripts that do that sort of thing... PathMath, libTokenMarkers... but it can be just another way for other scripts to interact with yours, if necessary. My APILogic exposes a "RegisterRule" function where other code can register to be used as a plugin.

There are lots of different reasons to use a public interface, but there's nothing to say you absolutely have one. There's nothing wrong with your script being self-contained, doing its thing, and calling it a day. =D

March 15 (4 years ago)
timmaugh
Forum Champion
API Scripter

Oh, but I did mean to say that your macro command line, as written, won't work because of the problem I mentioned of not having a selected property when you use a target statement. You could do it with 2 differentiated target calls, or by statically supplying the source of the attack in the "--selected" property... or through a couple of different slight of hands involving APILogic and/or SelectManager. Somehow you're going to have to provide that second ID.

March 15 (4 years ago)

Hmmm... for me this is working fine. I select a token on my page. This token is linked to a character sheet. The macro itself is a token action so it will only appear if a player has a token selected. After clicking on the macro button the games asks for selecting a target, which is also linked to a character sheet. After a few prompts everything works fine. The API successfully gets the character ID linked to the token selected and the character ID linked to the token targeted.

March 15 (4 years ago)

There are quite a few helper scripts that do that sort of thing... PathMath, libTokenMarkers... but it can be just another way for other scripts to interact with yours, if necessary. My APILogic exposes a "RegisterRule" function where other code can register to be used as a plugin.

There are lots of different reasons to use a public interface, but there's nothing to say you absolutely have one. There's nothing wrong with your script being self-contained, doing its thing, and calling it a day. =D

Yeah like you said I will see if this is a benefit or just additional workload.

Thank you very much again for your explanation. I am usually working with C++ and sometimes script-based languages like JavaScript are confusing to me because you do not need to initialize anything on purpose. Apparently, the Roll20 API also knows that the script "is just available flowing around in nirvana waiting for anyone to be used" :-D With this in mind the public interface totally makes sense. I will probably out source some of my functionalities to a HelperScript then to increase the readability.

Thank you very much timmaugh! Your help was really awesome. We can glady close this thread now. If I have more questions I will open a new Topic with the specific question.

Best regards!


March 15 (4 years ago)
timmaugh
Forum Champion
API Scripter

Huh. I wonder if having a selected property even after a target statement works specifically because it's a token action. Good info, if so! Now I need to run some tests!

March 15 (4 years ago)

Glad that I could point out a bug or feature :-D