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

Get and Set attributes on all sheets

October 05 (6 years ago)

Edited October 05 (6 years ago)

**I think this justifies a new post, but if it should be a comment in my previous thread please let me know and I'll reply to it instead**

With the help of several folks here on the boards, I've come up with a workable scenario to achieve my goal of having a conditional attack modifier based on NPC type. However, I have to make it as painless as possible for my GM, and that's where I'm stuck now.

I need to cycle through every NPC sheet in the game (and any new sheets he adds to the game), and create 2 new attributes that are based off of one existing attribute. Cycling through everything and only acting if npc_type already exists shouldn't hurt anything.

I started with Invincible Spleen's awesome updater (https://app.roll20.net/forum/post/2011765/add-attribute-api/?pageforid=2011765#post-2011765)) but I'm falling short needing to use per-character existing information for the new value, rather than simply having a constant pre-set value.

The default NPC sheets my GM is using all have the following attribute with a multiple string value that can have one of two formats: 

npc_type "<size> <type>,<alignment>"	        // "Medium undead, lawful evil"
npc_type "<size> <type> (<subtype>),<alignment>" // "Small humanoid (goblinoid), neutral evil"

I'll just look at the top case to start, but planning for the second version may change the way the split calls are made.

I need to read in the attribute value, split the resulting string, then create and assign a new attribute value using the split info (and do this to all sheets, already existing and yet to be created)

For example using a skeleton: npc_type = "Medium undead, lawful evil"

The desire is for npc_type_split[x] to fill with

npc_type_split[0] = "Medium"
npc_type_split[1] = "undead" //as I type I realize i'll need another (or more robust) split to remove the ","
npc_type_split[2] = "lawful"
npc_type_split[3] = "evil"

Then push [1] to the new attribute basic_type, resulting in basic_type = "undead"

My butchered attempt at doing so follows if it's of any help. The only changes are to the top two sections. One glaring issue is that there's no definition for characterID since what I have is outside of the loop that cycles through each sheet/assigns the info as a new one is made. The killer is that the script uses attributehash[x] to determine when it has finished with each sheet, so I still need a global variable list so it does something, but the value i want to put in is not constant.

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

///
/// Original Script by Invincible Spleen (https://app.roll20.net/forum/post/2011765/add-attribute-api/?pageforid=2011765#post-2011765)
/// Mangled by Takryn
///
/// Usage:
///	!initattributes				sets attributes to default values listed; creating new attributes if non-existing
///	!setAttribute [attribute] [newvalue]	sets attribute to value; can use incremental changes ie +1
///
	
	// Pull and parse type value from npc_type
	var pullandparse = function(characterID) {
			var npc_type = getObjs({
				_characterid: characterid,
				_type: "attribute",
				name: "npc_type"
			})[0];
			
			var npc_type_split = {npc_type.split(' ')};
			
			var basic_type = {npc_type_split[1]};
		},
		
		
    // Attributes all characters should have
    var attributeHash = {
            "basic_type": {
                "current": basic_type,
            }
        },
        

        // Set an attribute's value, or create it if it does not exist
        setAttribute = function(characterID, attributeName, newValue, operator) {
            var mod_newValue = {
                    "+": function (num) { return num; },
                    "-": function (num) { return -num; }
            },
            
            foundAttribute = findObjs({
                _characterid: characterID,
                _type: "attribute",
                name: attributeName
            })[0];
                        
            if (!foundAttribute) {
                
                if (typeof operator !== 'undefined' && !isNaN(newValue)) {
                    log (newValue + " is a number.");
                    newValue = mod_newValue[operator](newValue);
                }
                
                log("DefaultAttributes: Initializing " + attributeName + " on character ID " + characterID + " with a value of " + newValue + ".");
                sendChat("DefaultAttributes:", "/w GM Initializing " + attributeName + " on character ID " + characterID + " with a value of " + newValue + ".");
                
                createObj("attribute", {
                    name: attributeName,
                    current: newValue,
                    characterid: characterID
                });
            }
            else {
                if (typeof operator !== 'undefined' && !isNaN(newValue) && !isNaN(foundAttribute.get("current"))) {
                    newValue = parseFloat(foundAttribute.get("current")) + parseFloat(mod_newValue[operator](newValue));
                }

                log("DefaultAttributes: Setting " + attributeName + " on character ID " + characterID + " to a value of " + newValue + ".");
                sendChat("DefaultAttributes:", "/w GM Setting " + attributeName + " on character ID " + characterID + " to a value of " + newValue + ".");
                
                foundAttribute.set("current", newValue);
           
            }
        },

        // Add missing attributes and restore exiting ones to their default values
        initCharacterAttributes = function(char){
            log("DefaultAttributes: Initializing default attributes for character ID " + char.id + ".");
            sendChat("DefaultAttributes:", "/w GM Initializing default attributes for character ID " + char.id + ".");
            for (var key in attributeHash) {
                if (attributeHash.hasOwnProperty(key)) {
                    setAttribute(char.id, key, attributeHash[key]["current"]);                
                }
            }
        },

	showHelp = function () {
            sendChat("DefaultAttributes:", "/w GM Syntax is !setattribute <i>Attribute</i> [+/-] <i>Value</i>");
        },
                       
        handleInput = function(msg) {
            if(msg.type == "api") {
                
                var args = msg.content.split(/\s+/);
                
                switch(args[0]) {
                    case '!initattributes':
                        if (playerIsGM(msg.playerid)) {
                            log("DefaultAttributes: Initializing default attributes for all existing characters.");
                            sendChat("DefaultAttributes:", "/w GM Initializing default attributes for all existing characters.");
                            var allCharacters = findObjs({
                                _type: "character"
                            });
                            _.each(allCharacters, function(char) {
                                initCharacterAttributes(char);
                            });
                        }
                        break;
                    case '!setattribute':
                        if (args.length < 3 || args.length > 4) {
                            return showHelp();
                        }
                        
                        var foundCharacter = findObjs({
                            _type: "character",
                            name: msg.who
                        })[0];
                        
                        if (foundCharacter) {
                            if (args[2] == "+" || args[2] == "-") {
                                setAttribute(foundCharacter.id, args[1], args[3], args[2]);
                            }
                            else {
                                setAttribute(foundCharacter.id, args[1], args[2]);
                            }
                        }
                        else {
                            log("DefaultAttributes: No character associated with " + msg.who);
                            sendChat("DefaultAttributes:", "/w GM No character associated with " + msg.who);
                        }

                        break;
                }
                
            }
        },

        // Event triggers
        registerEventHandlers = function() {    
            on("add:character", initCharacterAttributes);
            on("chat:message", handleInput);
        };
    
    return {
        RegisterEventHandlers: registerEventHandlers
    };    
    
})();

on("ready", function() {
    'use strict';
    
    DefaultAttributes.RegisterEventHandlers();    
});

As always any help is appreciated!

October 05 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

First a disclaimer: if I was your GM, I wouldn't allow this script. Having to make global changes to all the characters to handle one script is a bad idea, because other players might need more changes later, and attribute bloat is one of the biggest causes of game lag in roll20. It's just best to avoid goign down that road.

There is a better solution, which is to simply have your script check a target when you attack them, and extract the type information in real-time. 

This has the advantage you dont need to bloat the npc characters.

When using the target keyword, you are prompted to pick a token, and can get the character_id linked to that token, and from there, you can get the npc_type.

Then you can split it. the way I'd do the split is to do it twice:

first split on the comma, and [1] is the alignment, and [0] is the types.

Then do a split on [0] to get the different elements of type - which seeing that humanoid entry does present a few problems. You can't simply split on spaces.

You can also build in some error checking to handle npcs where the npc_type is malformed, or absent.

Then your weapon attack script can use those details however you need.


If you do go ahead with looping through all the characters and making permanent changes (which I dont recommend), there's an old function Aaron posted in an old thread: https://app.roll20.net/forum/permalink/3281508/.

This cycles through every character and get an array of all characters with the specific attribute.

Create this function:

var getCharactersWithAttrByName = function(attributeName){
  "use strict";

  /* start the chain with all the attribute objects named 'player-name' */
  return _.chain(filterObj((o)=>{
   return (o.get('type')==='attribute' &&
      o.get('name')===attributeName);
    }))

    /* IN: Array of Attribute Objects */
    /* extract the characterid from each */
    .map((o)=>{return o.get('characterid');})

    /* IN: Array of Character IDs (Possible Duplicates) */
    /* Remove any duplicate IDs */
    .uniq()

    /* IN: Array of Character IDs */
    /* Get the character object for each id */ 
    .map((cid)=>{return getObj('character',cid);})

    /* IN: Array of Character Objects or Undefined */
    /* remove any entries that didn't have Characters */
    .reject(_.isUndefined)

    /* IN: Array of Character Objects */
    /* Unwrap Chain and return the array */
    .value();
};

and then call it like:

var chars = getCharactersWithAttrByName('npc-type');

That'll give you an array of character objects.

Then you can loop through them, and extract the npc_type attribute and do all the stuff you need.

October 05 (6 years ago)

Edited October 05 (6 years ago)

I'm totally open to doing it real time! My reasoning for making the attribute was so that I could revert to using basic macros instead of scripting the whole thing. I picked up the macros far quicker than the javascript, so it seemed like the easy way out. I figured it would also work for non-api folks that way too, provided the templates the GM was using were consistent with the pertinent attribute value.

To make sure I understand the realtime process:

I need a macro that starts with @{target|token_id} and then sends the result via !XYZ call to the script

The script will then pull the npc_type attribute (with getobj?) and work to extract the basic type from it, then

run through if/thens to check what bonuses should be applied and add them up and label the output accordingly


My follow up question is what tutorial do i search for the coding calls to convert what i have as a basic macro into js? 

Or since #@{target|type} works, is there a way to replace "@{target|type}" with output from the script, maybe #[[!xyz]]

or perhaps to simply have the script activate a premade macro?

I definitely see where y'all were coming from regards this hardly being worth all the effort, but it's piqued my interest now and learning all this stuff makes me happy.  

October 05 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

If your GM is okay with adding extra stats to the character sheet ,you could combine both approaches.

Instead of modifying all characters in one fell swoop, have you weapon attack macro have two parts:

1) a script that gets the npc-type attribute of the target and updates that character with the new attributes

2) your standard macro that uses those attributes as you intend.

Alternatively, step 1 could be replaced with a script that runs automatically when the gm adds a token to the map and updates the character linked to the token if needed, so by the time you get to attack it, the stats are guaranteed to be there.

This accounts for the fact that the GM will add new npcs from time to time, and they;ll need these attributes added.


Your script call would probably look something like

!updateStats @{target|foe|character_id}
/roll bla bla your macro goes here

and you'd have an UpdateStats macro that would handle all the stat creation (and is smart enough to do nothing if the character already has the relevant stats). Then in your /roll line, you'd have whatever your attack macro is.

But IIRC your macro is pretty complex, with a few conditional elements (like doing different things depending on whether the target is undead, etc.) That's not really possible in a basic macro without using queries which you dont want to use, so you probably need to do the full thing through the script.


I still think the API button approach is the best future-proof way to do this. Your script would likely need tweaking every now and then as you add extra capabilities, or change weapons. It's a pain to keep that kind of thing updated, especially once you want to handle other attack options.

October 05 (6 years ago)

Thanks for taking the time to work with me on this. I know I'm like a clueless child spouting nonstop questions. I'll take another stab at it tomorrow.

October 05 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

Can you post the the macro you have been using up to know, with all the queries you would have in it?

That's so I can present a version of it using API buttons for your consideration. Then if you don't like it, I'll help you with the script approach.

(It might take a little while, it's the weekend.)

October 05 (6 years ago)

Edited October 05 (6 years ago)

So far I've just clicked the weapon attacks from my character sheet. I've baked in the bonus effects of each creature type and planar warrior ability into separate options. I also have stand alones to just click for PW and fav enemy after using one of the other weapons because I hadn't set them up yet. These are the same pre-built macros I was planning to use. I pulled them from hitting "up" in chat after clicking from the sheet and just copied them into macros. In this case I have 6 options (normal or pw enhanced for undead *weapon and fav en*/fiend *fav en*/other), but if my first favored enemy was humanoid, it would be 8 due to splitting out undead/humanoid/fiend/other. I agree using the pop up menu would likely be required just to declutter the macro bar if i was clicking from that instead of the char sheet on a second monitor.

I'll use planar warrior almost every turn, but since it uses a bonus action I can't use it every time. It made sense to me to have something to click based on if i'm using it or not. I figure this is what the drop down queries will ask, but that adds an extra click to pick yes or no.

Ideally I'll end up with a choice of two buttons to hit for each weapon (normal attack or pw enhanced attack) and then the coding will apply the appropriate bonuses. I almost exclusively use the sun blade though. 



// Script required: ExpandedExpressions
// Manual attribute entries on player sheet for fav_en_1 = undead and fav_en_2 = fiend
// These two work and actually call the other macro but have no checking mechanism, 
// and therefore will break outside of undead or fiend targets
SB-attack
#sb-@{target|type}
SB-PW-attack
#sbpw-@{target|type}
// These two don't work, the OR functionality is only checking the first part 
// (though I swear it was earlier, and I can get it to work with numbers instead of strings) 
// but even when they evaluate correctly they just output the text as shown into chat without calling the macro
SB-attack
!extend `("@{target|type}" = ("@{Arkos|fav_en_1}"||"@{Arkos|fav_en_2}") ? "#sb-@{target|type}" : "#sb-base")`
SB-PW-attack
!extend `("@{target|type}" = ("@{Arkos|fav_en_1}"||"@{Arkos|fav_en_2}") ? "#sbpw-@{target|type}" : "#sbpw-base")`
// Copied out of chat log after clicking from offense section of shaped character sheet

sb-undead
@{Arkos|output_option} &{template:5e-shaped} {{character_name=@{Arkos|character_name}}} @{Arkos|show_character_name} {{title=Sun Blade (Undead)}} {{offense=1}}  @{Arkos|attacher_offense} @{Arkos|hide_gm_info} {{@{Arkos|shaped_d20}=1}} {{attack_type_macro=[Melee Weapon Attack:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsUZ-Owz7xL8ZF9clN_attack)}} {{has_attack_damage=1}} {{attack_damage_crit=[[2d8]]}} {{attack_damage=[[2d8[damage] + 3[dex] + 8[bonus]]]}} {{attack_damage_type=radiant}} {{has_attack_damage=1}} {{attack_damage_macro=[Hit:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsUZ-Owz7xL8ZF9clN_attack_damage)}} {{attack_damage_crit_macro=[Crit:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsUZ-Owz7xL8ZF9clN_attack_damage_crit)}} {{attack1=[[@{Arkos|shaped_d20}@{Arkos|d20_mod}cs>20 + 3[proficient] + 3[dex] + 2[bonus]]]}} {{reach=5}}
sbpw-undead
@{Arkos|output_option} &{template:5e-shaped} {{character_name=@{Arkos|character_name}}} @{Arkos|show_character_name} {{title=Sun Blade (Undead + PW)}} {{offense=1}}  @{Arkos|attacher_offense} @{Arkos|hide_gm_info} {{@{Arkos|shaped_d20}=1}} {{attack_type_macro=[Melee Weapon Attack:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsYH3NGU99ofSvOCNN_attack)}} {{has_attack_damage=1}} {{attack_damage_crit=[[2d8]]}} {{attack_damage=[[2d8[damage] + 3[dex] + 8[bonus]]]}} {{attack_damage_type=force}} {{has_attack_damage=1}} {{attack_damage_macro=[Hit:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsYH3NGU99ofSvOCNN_attack_damage)}} {{attack_damage_crit_macro=[Crit:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsYH3NGU99ofSvOCNN_attack_damage_crit)}}  {{attack_second_damage_crit=[[1d8]]}} {{attack_second_damage=[[1d8[damage]]]}} {{attack_second_damage_type=radiant}} {{attack1=[[@{Arkos|shaped_d20}@{Arkos|d20_mod}cs>20 + 3[proficient] + 3[dex] + 2[bonus]]]}} 
sb-fiend
@{Arkos|output_option} &{template:5e-shaped} {{character_name=@{Arkos|character_name}}} @{Arkos|show_character_name} {{title=Sun Blade (Fiend)}} {{offense=1}}  @{Arkos|attacher_offense} @{Arkos|hide_gm_info} {{@{Arkos|shaped_d20}=1}} {{attack_type_macro=[Melee Weapon Attack:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsYqKzdsZ5m2YNtjkO_attack)}} {{has_attack_damage=1}} {{attack_damage_crit=[[1d8]]}} {{attack_damage=[[1d8[damage] + 3[dex] + 8[bonus]]]}} {{attack_damage_type=radiant}} {{has_attack_damage=1}} {{attack_damage_macro=[Hit:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsYqKzdsZ5m2YNtjkO_attack_damage)}} {{attack_damage_crit_macro=[Crit:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsYqKzdsZ5m2YNtjkO_attack_damage_crit)}} {{attack1=[[@{Arkos|shaped_d20}@{Arkos|d20_mod}cs>20 + 3[proficient] + 3[dex] + 2[bonus]]]}} {{reach=5}} 
sbpw-fiend
@{Arkos|output_option} &{template:5e-shaped} {{character_name=@{Arkos|character_name}}} @{Arkos|show_character_name} {{title=Sun Blade (Fiend + PW)}} {{offense=1}}  @{Arkos|attacher_offense} @{Arkos|hide_gm_info} {{@{Arkos|shaped_d20}=1}} {{attack_type_macro=[Melee Weapon Attack:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsZFQ5o-RazMf9giDO_attack)}} {{has_attack_damage=1}} {{attack_damage_crit=[[2d8]]}} {{attack_damage=[[2d8[damage] + 3[dex] + 8[bonus]]]}} {{attack_damage_type=force}} {{has_attack_damage=1}} {{attack_damage_macro=[Hit:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsZFQ5o-RazMf9giDO_attack_damage)}} {{attack_damage_crit_macro=[Crit:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsZFQ5o-RazMf9giDO_attack_damage_crit)}} {{attack1=[[@{Arkos|shaped_d20}@{Arkos|d20_mod}cs>20 + 3[proficient] + 3[dex] + 2[bonus]]]}} 
sb-base
@{Arkos|output_option} &{template:5e-shaped} {{character_name=@{Arkos|character_name}}} @{Arkos|show_character_name} {{title=Sun Blade}} {{offense=1}} @{Arkos|attacher_offense} @{Arkos|hide_gm_info} {{@{Arkos|shaped_d20}=1}} {{attack_type_macro=[Melee Weapon Attack:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsFK7c7ngkvqIL-Hil_attack)}} {{has_attack_damage=1}} {{attack_damage_crit=[[1d8]]}} {{attack_damage=[[1d8[damage] + 3[dex] + 4[bonus]]]}} {{attack_damage_type=radiant}} {{has_attack_damage=1}} {{attack_damage_macro=[Hit:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsFK7c7ngkvqIL-Hil_attack_damage)}} {{attack_damage_crit_macro=[Crit:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsFK7c7ngkvqIL-Hil_attack_damage_crit)}} {{attack1=[[@{Arkos|shaped_d20}@{Arkos|d20_mod}cs>20 + 3[proficient] + 3[dex] + 2[bonus]]]}} {{reach=5}} sbpw-base
@{Arkos|output_option} &{template:5e-shaped} {{character_name=@{Arkos|character_name}}} @{Arkos|show_character_name} {{title=Sun Blade (PW)}} {{offense=1}}  @{Arkos|attacher_offense} @{Arkos|hide_gm_info} {{@{Arkos|shaped_d20}=1}} {{attack_type_macro=[Melee Weapon Attack:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsNrAHcesYEqXuceAV_attack)}} {{has_attack_damage=1}} {{attack_damage_crit=[[2d8]]}} {{attack_damage=[[2d8[damage] + 3[dex] + 4[bonus]]]}} {{attack_damage_type=force}} {{has_attack_damage=1}} {{attack_damage_macro=[Hit:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsNrAHcesYEqXuceAV_attack_damage)}} {{attack_damage_crit_macro=[Crit:](~-LNSkNQHTH31VWs3bonm|repeating_offense_-LNsNrAHcesYEqXuceAV_attack_damage_crit)}} {{attack1=[[@{Arkos|shaped_d20}@{Arkos|d20_mod}cs>20 + 3[proficient] + 3[dex] + 2[bonus]]]}} {{reach=5}} 
October 05 (6 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter

Just to throw something else into the mix. It looks like you are using the Shaped Sheet. You can save yourself a lot of pain by using the Modifiers section. You would only have one entry for each weapon, and one modifier for each condition. Your script could just bother with toggling the modifiers. This cleans up your sheet considerably, removing many repeating attributes, and allowing you to add new weapons easily as your character grows. Also, attachers can be your friend here, allowing you to easily add conditional damage with a single extra click.

I would finally like to echo GG's advice on not adding campaign wide custom attributes. Besides being a bit clunky and bloating an already impressively large character sheet, your GM would need to remember to add this attribute in to all future characters.

October 05 (6 years ago)

Ah, so that does exist on Shaped sheets! I'd seen reference to it from the OGL sheets but didn't make the connection. Playing with the modifiers, I'm not seeing an option to change the damage type. I'm a big fan of the card output inherent to the Shaped sheet: it does all of the rolls, adds everything up into one clean number per damage type and displays it, while still being able to hover over and see the components in the equation.

The Planar Warrior ability changing the damage type is a big driver towards making things messy; that's what I'm trying to clean up. 

October 05 (6 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter

Ah, that is a bit sticky. You can do a lot with modifiers and attachers. You might be able to put a query into the damage type. I don't know; I've never tried.

October 05 (6 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter

Just as a heads up, the modifiers can be turned on and off programmatically with the ChatSetAttr script.

October 07 (6 years ago)

Edited October 07 (6 years ago)

GG: to further answer your request, the queries would be as follows:

Tier 1: Am I using Planar Warrior? (only affects 1 attack each turn - and my character generally makes 2).
Tier 2: Which weapon am I using? (SunBlade or Long Bow - and very rarely a short sword)
Tier 3: Is the target type one that is pertinent to my character? (in my case: Undead or Fiend)
There's actually another Tier 1 character level ability that should be checked for, but I don't think its worth adding another extra level to the process yet.

Keith: How does one call the ChatSetAttr script to set a specific attribute if I'm determining what that attribute is in another script? It's my current hangup in option (a) below.

Following the guidelines from GG up above, I've cobbled together a script that takes input of <character_id> and <attribute name> and parses the "npc_type" attribute to split the string and make it usable as a check against Tier 3 above. It's a bit more generic than I need since I wanted it to be functional for any character attribute from that line (size, type, subtype, alignment, attitude, morality) to future-proof other abilities (Paladin gets bonus against evil, etc.).

I'm struggling with making the final string usable. I could either

a: send it back to a macro (but have been running into the problem of just getting #sb-undead printed instead of functioning as a macro).
I could hard code the script to do the check and output appropriately if I could run the script inline such that the return from the script isn't what the original request was, but one of 3 options to make my macros work (undead, fiend, base). I'm thinking along the lines of

#sb-!learn @{target|character_id} type
but it seems to get angry, and doesn't like
#sb-[[!learn @{target|character_id} type]] 
either. I've seen some other scripts use a !!! feature to make them work inline, but I couldn't get my head around the implementation within the script. Hard coding it also reduces it's flexibility, unless I increase the inputs to accept the player attributes to compare against as well.

b.1: assign it to a new attribute (on my character sheet - to be deleted after running the script/macro to prevent attribute bloat)
b.2: have extenededexpressions (or some other script) evaluate if it matches my favored enemy attributes I've manually added to my sheet already
b.3: run the appropriate attack/damage rolls macro

(b) is effectively what I have outlined above, just eventually checking against my char sheet instead of the target.


// base code from: https://app.roll20.net/forum/post/6068411/how-do-i-get-api-commands-and-macros-to-work-in-order
on('ready',function(){
    'use strict';

    var getSpeaker = function(msg) {
        var characters = findObjs({_type: 'character'});
        var speaking;
        characters.forEach(function(chr) { if(chr.get('name') == msg.who) speaking = chr; });
     
        if(speaking) return 'character|'+speaking.id;
        else return 'player|'+msg.playerid;
        //if(speaking) return speaking.id;
        //else return msg.playerid;
    };
    //var getSpeakerRaw = function(msg) {
    //    var characters = findObjs({_type: 'character'});
    //    var speaking;
    //    characters.forEach(function(chr) { if(chr.get('name') == msg.who) speaking = chr; });
    // 
    //    if(speaking) return speaking.id;
    //    else return msg.playerid;
    //};
    
    var getCharNameById = function (id) {
const character = getObj("character", id);
return (character) ? character.get("name") : "";
};

// FIX THIS SO THAT YOU CAN STILL FUNCTION IF GIVEN NAME INSTEAD OF ID
// var getCharIdByName = function (name) {
// const character = findObjs({
//           type: 'character',
//            represents: obj.get('character_id')})
// //getObj("_id", name);
// return (character) ? character.id : "";
// };
    
    on('chat:message',function(msg){
        if('api' === msg.type && msg.content.match(/^!learn/)  ){ 
            let args = msg.content.split(/\s+/);
 // Error catcher is not working because of splitting entry. API error trying to split an invalid input breaks sandbox before being caught. How to ensure clean input?
            if (args[1] === undefined) {
                sendChat("Error (Learn)","Input error. Invalid character id. Check syntax \"character_id");
            }
            if (args[2] === undefined) {
                sendChat("Error (Learn)","Input error. 2 arguments required: !learn [character_id] [attribute name]")
                return;
            };
            let tarid = args[1];
            let tarname = getCharNameById(tarid);
            let attname = args[2];
            
// Non-functioning section - trying to get id from name or name from id automatically
//            if (args[1][0] === "-") {
//                let tarid = args[1];
//                let tarname = getCharNameById(tarid);
//            } else {
//                let tarid = getIdByCharName(tarid);
//                let tarname = args[1];
//            }
//          let tarid2 = getCharIdByName(args[1]);
//          {{tarid2 (get id by name) = '+tarid2+'}}
//*            let vardata = '&{template:default} {{name=Basic Var Check}} {{args ='+args+'}} {{tarname = '+tarname+'}} {{tarid = '+tarid+'}} {{attname = '+attname+'}}))';
//*            sendChat(getSpeaker(msg),vardata);
            
// original error check
//            if(tarid === undefined) { //msg.inlinerolls
// sendChat("Error (Learn)","No character to query.");
// return;
// };
// if (attname === undefined) {
//              sendChat("Error (Learn)","No attribute to search for.");
// return;
//            };
let att = getAttrByName(tarid, "npc_type", "current");

// Parse the attribute string [npc_type]                    // att =        [medium undead, lawful evil]    |   [small humanoid (goblinoid), neutral]
            let attstr = att.split(', ');                   // attstr =     [medium undead],[lawful evil]   |   [small humanoid (goblinoid)],[neutral]
            let sizetype = attstr[0].split(/\s+/);          // sizetype =   [medium],[undead]               |   [small],[humanoid],[(goblinoid)]
            let charsize = sizetype[0];                     // charsize =   [medium]                        |   [small]
            let chartype = sizetype[1];                     // chartype =   [undead]                        |   [humanoid]
            let charsubtype = "";
            if (sizetype[2] != undefined) {                 // if subtype exists, strip ()
                charsubtype = sizetype[2].slice(1,sizetype[2].length-1); // no action                       |   [goblinoid]
            }
            let charalign = attstr[1].split(/\s+/);         // charalign =  [lawful],[evil]                 |   [neutral]
//*            let checktest = '&{template:default} {{name=Misc Variable Tester}} {{attstr = '+attstr+'}} {{attstr[1] = '+attstr[1]+'}} {{charalign = '+charalign+'}} {{charalign[0] = '+charalign[0]+'}} {{charalign[1] = '+charalign[1]+'}} {{charalign[1] !\=undef = '+(charalign[1] != undefined)+'}}))';
//*            sendChat(getSpeaker(msg),checktest);
            if (charalign[1] != undefined) {                // if morality exists, separate attitude and morality
                let charatti = charalign[0];                // charatti =   [lawful]                        |   no action
                let charmoral = charalign[1];               // charmoral =  [neutral]                       |   no action
            } else {
                let charatti = charalign;                   // no action                                    |   [neutral]
                let charmoral = charalign;                  // no action                                    |   [neutral]
            }
            
            switch (attname) {
                case "size":
                    return (charsize);
                case "type":
                    sendChat("","#"+chartype);
                    //return (chartype);
                case "subtype":
                    return (charsubtype);
                case "alignment":
                    return (attrstr[1]);
                case "attitude":
                    return (charatti);
                case "morality":
                    return (morality);
            }
// Troubleshooting outputs           
//            let vardata2 = '&{template:default} {{name=Type Parser Info}} {{attstr[0] ='+attstr[0]+'}} {{attstr[1] ='+attstr[1]+'}} {{sizetype = '+sizetype+'}} {{sizetype[2] = '+sizetype[2]+'}} {{size ='+charsize+'}} {{type ='+chartype+'}} {{subtype ='+charsubtype+'}} {{charalign ='+charalign+'}}{{charatti ='+charatti+'}} {{charmoral ='+charmoral+'}}))';
// sendChat(getSpeaker(msg),vardata2);
// let outPut = '&{template:default} {{name=Attribute Query}} {{Target='+tarname+'}} {{Attribute='+attname+'}} {{Value='+att+'}}))';
//            sendChat(getSpeaker(msg),outPut);
//            return(chartype);
        }
    });
});
October 07 (6 years ago)

Edited October 07 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

It'll take me a while to get my head around this script, but from a quick scan I notice one issue with one of the non-functional sections:

//            if (args[1][0] === "-") {
//                let tarid = args[1];
//                let tarname = getCharNameById(tarid);
//            } else {
//                let tarid = getIdByCharName(tarid);
//                let tarname = args[1];
//            }
//          let tarid2 = getCharIdByName(args[1]);

There's a thing called scope: when you use let to define an attribute, it exists only within it's stated scope. One area of scope is the block within a pair of curly brackets { }.

So you are defining tarid and tarname within the if block, but they stop existing as soon as you leave that if statement.

What you really need is something like this:

//            let tarid, tarname;  // you can set some default value here if needed.
//            if (args[1][0] === "-") {
//                tarid = args[1];
//                tarname = getCharNameById(tarid);
//            } else {
//                tarid = getIdByCharName(tarid);
//                tarname = args[1];
//            }

This ensures the variables exist in their proper scope, and the values assigned within the if statement persist when you leave it.


Also this looks iffy:

//            if (args[1][0] === "-") {
//                let tarid = args[1];

Are you meaning to assign an array to tarid?

The if statement use args[1][0], so args[1] must be an array.

October 08 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

oh hey, I recognise some of the contributors on that page

// base code from: https://app.roll20.net/forum/post/6068411/how-do-i-get-api-commands-and-macros-to-work-in-order


A question about your process. You list these tiers:

Tier 1: Am I using Planar Warrior? (only affects 1 attack each turn - and my character generally makes 2).
Tier 2: Which weapon am I using? (SunBlade or Long Bow - and very rarely a short sword)

These are selections you have to make. There is no way to write a script that just knows which of these you are using, you need to have some input to define it. 

Keith, I think, mentioned the concept of attachers in the sheet you are using, I'd strongly recommend investigating how they work because I think they are meant to handle these kind of situations. Other approaches would be to creating an attribute on your character (on the attributes & abilities tab) where you set them (either manually or with chatsetattr), and your attack macro draws the values from them when used.

Regarding the Tier 3 problem, getting the npc_data. My impression is your script is way more complex than it needs to be for this. I don't think you need a getspeaker function or a getcharacter id / name function, because you'll have these every time you use your macro (you are the speaker, and the other details can be gained via target or selected keywords).

So everything before this line seems like it could be removed:

 on('chat:message',function(msg){

You mention the split function is causing a sandbox error, but I'm not sure why that is. The syntax looks fine to me. I'll look at it in more detail later.

October 08 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

PS: I'd recommend testing the function only with perfectly formed inputs and targets with complete data. Once the function is working, you can add the error checking to account for problems. But you need to make sure it actually works first.

October 08 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

also I'm not sure how you are using this:

#sb-!learn @{target|character_id} type

but API commands need to start with the ! at the very start of the line. They wont work otherwise.

What does #sb- represent in this context, how is it being used?

October 08 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

Thinking about it, I'm also wondering if having a split function to get the various npc type elements is really necessary.

If you have an npc_type of (say) "medium undead, lawful evil"

and you need to know if it is undead, you can just 

if(npctype.toLowerCase().indexOf('undead') !== -1)

or

if(npctype.toLowerCase().includes('undead') )

where npctype is a variable that contains the contents of the npctype attribute.

the split method is handy to make sure you aren't grabbing data from the wrong part of the string, but of the top of my head, i cant think of situations where that would happen with npc_type. There aren't going to be any alignments that match creature types, or sizes that match creature types, etc.

October 08 (6 years ago)

Edited October 08 (6 years ago)

Sorry for all the confusion with what I've got. For what it's worth, the actual gathering and parsing actually "works" with perfectly formed inputs and assigns values as expected. It handles both scenarios for the npc_type as well, so all that is good. The non-working bits are just in there as a legacy for me to get back to later. 

I don't have a grasp on the return/chat side of things to actually get those values out and back into the tabletop/macro side of things though.

The scope info is super helpful, and probably explains several errors I'd been fighting.

For this section

//            if (args[1][0] === "-") {
//                let tarid = args[1];

I was trying to use the "-" at the start of the character_id to determine which input the script received, the ID or a Name, and then I was going to call the getNamebyID or getIDbyName accordingly.

Regarding the tiers:

A question about your process. You list these tiers:
Tier 1: Am I using Planar Warrior? (only affects 1 attack each turn - and my character generally makes 2).
Tier 2: Which weapon am I using? (SunBlade or Long Bow - and very rarely a short sword)

I have no problem selecting from two options per weapon, at least for the time being. As it stands I currently have 6 choices per weapon, so cutting that to a third is plenty.

The getSpeaker is there because it was in the script I started with and worked to put the text back into chat like I needed. I'd love to have a simple echo or something that posts it back without any excess effort.

The split error is only when given imperfect input, so that can be put aside.

The macro call is from a preset macro, in this case

#sb-undead
sb- is a prefix i'm using for sunblade
undead is the creature type i'm attacking

so i have a prebuilt attack macro for Sunblade vs Undead, see the second post above where i listed out the macros.

If the attribute for just "type" was properly filled (it's not, hence the need for the parsing script), i could simply call like this:

#sb-@{target|type} which evaluates to 
#sb-undead and properly rolls as expected

That's how i end up with 6 macros per weapon (base and with pw, and then the two favored enemy types:

sb-base, sbpw-base, sb-undead, sbpw-undead, sb-fiend, sbpw-fiend

So what I'm looking for at the end of the day is for me to choose the weapon (sb) and whether i'm using pw (sbpw), which calls a macro starting with either #sb- or #sbpw- and then for the script/remaining macro to determine <undead>, <fiend>, or <base> to get pushed back to the macro, completing it and making it fire. Using @{target|type} works, but trying to push the text back out from the script hasn't worked for me so far. 

The workaround I mentioned is to have the script push the type attribute to my own character sheet as something like target_type, then i could just call #sb-@{arkos|target_type} or #sbpw-@{arkos|target_type}.

October 08 (6 years ago)

Ha! of course there's a search function. Initially I needed to split it so i could assign it to an attribute. If everything ends up happening in the script, the search aspect should be fine.

October 08 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

To clarify, you already have macros by name of 

#sb-undead
#sb-fiend

etc, and you are just looking for a way to call them correctly?

You dont need to build the macro string, they already exist in your macro list?

October 08 (6 years ago)

Correct, see the last code block of my previous post: https://app.roll20.net/forum/post/6858810/get-and-set-attributes-on-all-sheets/?pageforid=6859169#post-6859169

Those macros are all set up based on what i created on my character sheet, so yes, I'm looking for a way to automatically apply the correct suffix based on my target selection when the macro fires. (or script fires or whatever ends up working)

The penultimate code block shows how i was originally attempting to make the comparison against my character sheet values for favored enemy as well.

October 08 (6 years ago)

Edited October 08 (6 years ago)

distilled as much as possible, i'm looking for a functioning version of these:

if (@{target|type} = (@{player|fav_en_1} OR @{player|fav_en_2}))
    return #sb-@{target|type}
else
    return #sb-base
if (@{target|type} = (@{player|fav_en_1} OR @{player|fav_en_2}))
    return #sbpw-@{target|type}
else
    return #sbpw-base

***where @{target|type} is either pulled from a properly assigned attribute on the target itself, or from a call against my character sheet, or having a script manipulate everything.

As y'all have pointed out, it's much easier to manage macros than the script, which is why I've been trying to minimize the scripting necessary and just using it to facilitate the attributes to call macros.

October 08 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

That simplifies things a lot. I had badly misunderstood where you were going with this. 

I think i'll have a solution for you before long.

October 08 (6 years ago)

It's taken me a while to come to terms with how to explain it. All of the terminology being completely foreign hasn't helped, so thanks for working with me.

Out of curiosity, what had you gleaned my end goal to be?

October 08 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

I assumed you wanted to build the output string (the huge monstrosity that starts with @{Arkos|output_option} &{template:5e-shaped} etc) from its component parts. My unfamiliarity with that sheet and system made it hard to figure out what was needed.

October 08 (6 years ago)

Ah, gotcha! Maybe someday when I'm bored and feel like torturing myself I'll look into that. Or when my next crazy idea hits. :)

October 08 (6 years ago)

Edited October 10 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

Here's a  first draft. It works on my limited testing, but likely needs some error trapping. Some notes on shortcomings below.

Edit: Updated to handle abilities, and report character types.

/*
Launch with attack macro like
!chooseam @{target|character_id} attackbase types|to|check 
!chooseam @{target|character_id} ?{Attack Type|Standard,sb|With PW,sbpw} fiend|Undead
If you want to use abilities, replace 
!chooseam
with 
!chooseaa @{YOURNAME|character_id}
example:
!chooseaa @{Thundarr|character_id} @{target|character_id} ?{Attack Type|Standard,sb|With PW,sbpw} fiend|Undead */

on('ready',function(){
    'use strict';
    let npc_types = [ 'type','npc_type', 'npcd_type', 'npc_typebase','type_from_srd']; //
    let getNPCType = function(target) {
        for (let i = 0; i < npc_types.length; i++) {
            let npc_Type = getAttrByName(target, npc_types[i]);
            if(npc_Type !== undefined && npc_Type !== '' ) { 
                return npc_Type;
            }
        }
    };
    
    
    let getSpeaker = function(msg) {
        var characters = findObjs({_type: 'character'});
        var speaking;
        characters.forEach(function(chr) { if(chr.get('name') == msg.who) speaking = chr; });
     
        if(speaking) return 'character|'+speaking.id;
        else return'player|'+msg.playerid;
    };
    
    let getMacro = function(name,action,pcid,who) {
        let m;
        if (action === 'ability') {
            m = findObjs({
    	        _characterid: pcid,
    	        _type: action,
    	        name: name
    	    });
        } else if (action === 'macro') {
            m = findObjs({
    	        _playerid: pcid,
    	        _type: action,
    	        name: name
    	    });
        }
	    if(m[0] === undefined) {
	        sendChat("Choose Macro","/w " + who + " Error: Macro Not Found");
	        return;
	    }
   
	    return m[0].get('action');
	    
	};
	
	let handleInput = function(msg, action) {
	    let args = msg.content.split(/\s+/);
            if (args.length < 3) {
                sendChat("Choose Macro","/w " + msg.who + " Input error: Not enough parameters.");
                return;
            }
            let pcid = msg.playerid;
            if (action === 'ability') {
                args.shift();
                pcid = args[0];
            }
            let target = args[1];
            let macrobase = args[2];
            let enemytypes = args[3] !== undefined ? args[3].split('|') : 'N/A';
            let tartype = getNPCType(target);
            let foundtype = 'base';
            if(tartype === undefined) {
                sendChat("Choose Macro","/w " + msg.who + " Error: NPC Type not found, proceeding with base macro");
            } else if ( enemytypes !== 'N/A') {
                for (let i = 0; i < enemytypes.length; i++) {
                    if(tartype.toLowerCase().includes(enemytypes[i].toLowerCase())) {
                        foundtype = enemytypes[i].toLowerCase();
                        break;
                    }
                }
            }
            sendChat(getSpeaker(msg),'foundtype = ' + foundtype) ;
            let macro = getMacro(`${macrobase}-${foundtype}`,action,pcid,msg.who);
            sendChat(getSpeaker(msg),macro) ;
	};
	
    on('chat:message',function(msg){
        if('api' === msg.type && msg.content.match(/^!chooseam/)  ){ 
            handleInput(msg,'macro');
        } else if('api' === msg.type && msg.content.match(/^!chooseaa/)  ){ 
            handleInput(msg,'ability');
        } else if('api' === msg.type && msg.content.match(/^!checktype/)  ){ 
            var npcs = findObjs({ type: 'character', controlledby: '' });
            
            let types = filterObjs(function(obj) {    
                if(obj.get("type") === 'attribute' && obj.get('name').toLowerCase().includes('type') && !obj.get('name').toLowerCase().includes('repeating') && obj.get('current') !== "" ) return true;    //NPCNew Test
                else return false;
            });
            types.forEach(type => {
                let character = getObj("character", type.get('characterid')).get('name');
                sendChat("testing","/w " + msg.who + " " + type.get('name') + ": " + type.get('current') + "; " + character);
            });
        } 
    });
});


It assumes macros are built with a base: sb or sbpw or whatever

and that if no favoured enemy applies, it is named base (eg sb-base)

You can feed it a list of favoured enemies separated by a pipe (fiend, fiend|undead, undead|beast|elf) and the script will loop through the npc type to see if any are found. If they are, it will assume a match macro exists (sb-elf, sb-beast, sbpw-undead, etc.)

It will then find the relevant macro and roll it.

However, it doesn't (yet) output the name of the target. Do you need that?

I think this covers the cases you described, if there's anything I've missed let me know.

It probably needs a bit more testing.

It is probably more elegant to go the other route and build the attack string from scratch, that can be more flexible, but requires that I know all the details of how that system works and how your feats apply to it, but i don't.

So, to summarise, you probably launch it like so:

!chooseam @{target|character_id} ?{Attack Type|Standard,sb|With PW,pw} undead|fiend

And can expand it if you add new macros.

October 08 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

By the way, I notice in your macros you have static values for things like dex. It would be a good idea to change them to attribute calls, so that you dont have to update the macros every time your stats change

Like, one of the macros has this in it:

{{attack1=[[@{Arkos|shaped_d20}@{Arkos|d20_mod}cs>20 + 3[proficient] + 3[dex] + 2[bonus]]]}}

The proficient, dex, and bonus are attributes somewhere on yoru character sheet and you could change it to something like

{{attack1=[[@{Arkos|shaped_d20}@{Arkos|d20_mod}cs>20 + @{Arkos|proficiency_bonus)[proficient] + @{Arkos|dex_bonus}[dex] + @{Arkos|weapon_bonus}[bonus]]]}}

I dont know what the attributes are actually called, but replacing all the numbers in your macros with the appropriate attribute will make your life easier.

October 08 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

And finally for the night, since you have these macros created, have you considered ticking the token action box on them?

If you do that, whenever you have your token selected, you'll get a button for each macro floating above your token, and you can click it to launch it. This may be way easier than using a script!

October 08 (6 years ago)

Wow, that is so clean! Thank you so much! You'd mentioned before the getSpeaker was likely superfluous. Did you find a reason to keep it set up that way?

I'm not sure what I'm doing wrong to make it fail to work though. There are no API errors, but nothing comes back to chat after hitting enter with the entry:

!choosam @{target|character_id} sb undead|fiend

sb-base, sb-undead, and sb-fiend are all confirmed working macros from the macros tab. Does being on the macro bar affect their usability?


I don't think outputting the target name is necessary, I should be able to output that separately in a larger macro with the @{target|name} call, and then the subsequent @{target|character_id} will be based on the same target selection if my understanding is correct.

October 08 (6 years ago)

Spelling...spelling would be the issue. Choose has an 'e' on the end. *headdesk*

October 08 (6 years ago)

Edited October 08 (6 years ago)

OK, so new issue brought up by the lack of consistency between character attribute lists. I just grabbed a chain devil from the compendium and dropped it in, but the template for that doesn't have an "npc_type" attribute. It does have the "type" attribute that I had originally hoped was standard. Trying to use the macro on a target without the "npc_type" attribute crashes the API as well.

Is there an efficient way to search every attribute on the target whos attribute name contains the word "type", without knowing what all the iterations are? In just the few monsters I've looked at, I've seen "npc_type" "npcd_type" "type" "npc_typebase" and several more. If all of those strings were just combined, the same loop function should catch them right? Even if it catches "hit_dice" and things like that, since we're only searching for the target string any extraneous info shouldn't have any affect.

October 08 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

getSpeaker was needed so that when the macro was sent to chat, it would show the calling player as the sender. I'd forgotten that was needed.


The type/npc-type/etc problem is a big problem. Why aren't they consistent??? Are these creatures all from the same source?

It is possible to filter out all attributes to those with only type in the name. What happens though if there's more than one such attribute, or they identifying attribute doesn't have type in the name?

I'll add some error correction to stop it crashing the sandbox.

Have you checked out the token action method I suggested? That may be the smoothest way and you dont have to deal with script issues.

October 08 (6 years ago)

Edited October 08 (6 years ago)

For the token action part, from what I'm seeing, it just moves all 6 macros to the quick bar instead of clicking them from the sheet. It doesn't do anything as far as having the automation to pick the correct one based on target, does it?

I don't know why they are not consistent. Perhaps some come from the purchased modules, and those differ from the standard 5e compendium on R20? Short of asking my GM to create/update a field with the appropriate data on every char/monster sheet in the game, I'm not sure what to do other than cycle through them all. My hope with the attributes is that by concatenating the values for everything that says has "*type*" in the name, it will capture something that has what I need. Luckily, I suppose, I will always be acting as a final check, so if I know something matches but the appropriate macro didn't fire, I can activate them manually. Then I could send the GM a request to fix that specific sheet.

It is possible to filter out all attributes to those with only type in the name. What happens though if there's more than one such attribute, or they identifying attribute doesn't have type in the name?

Duplicates in the final searched string shouldn't be a problem. As long as it either finds a match or doesn't, the number of hits is irrelevant. If none of the fields that get searched have it, it should still return "base" and carry on as normal. I'm planning to leave the descriptors in the macros (ie Sun Blade (vs Undead)) so I'll hopefully notice the discrepancy of "base" showing up when it should be something else.

October 08 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter


Takryn said:

For the token action part, from what I'm seeing, it just moves all 6 macros to the quick bar instead of clicking them from the sheet. It doesn't do anything as far as having the automation to pick the correct one based on target, does it?

Just to be clear we are talking about the same thing. I'm not talking about the macro bar across the bottom of the screen but the token action bar that appears above the token.

Though it doesnt matter too much. The macro bar would work just as well (you can rename the buttons to something short and sweet.)

My point is: your concern was that you didnt want to select several dropdowns everytime you launched the attack macro. But if you have the six buttons floating there, and you know which one you need to press, it's very quick to just click one of them. You dont need to do any fancy scripting, you have something that is a single click, easy-peasy. 


I don't know why they are not consistent. Perhaps some come from the purchased modules, and those differ from the standard 5e compendium on R20? Short of asking my GM to create/update a field with the appropriate data on every char/monster sheet in the game, I'm not sure what to do other than cycle through them all. My hope with the attributes is that by concatenating the values for everything that says has "*type*" in the name, it will capture something that has what I need. Luckily, I suppose, I will always be acting as a final check, so if I know something matches but the appropriate macro didn't fire, I can activate them manually. Then I could send the GM a request to fix that specific sheet.

It is possible to filter out all attributes to those with only type in the name. What happens though if there's more than one such attribute, or they identifying attribute doesn't have type in the name?

Duplicates in the final searched string shouldn't be a problem. As long as it either finds a match or doesn't, the number of hits is irrelevant. If none of the fields that get searched have it, it should still return "base" and carry on as normal. I'm planning to leave the descriptors in the macros (ie Sun Blade (vs Undead)) so I'll hopefully notice the discrepancy of "base" showing up when it should be something else.

My question about consistency was rhetorical, really. But it is strange, it makes me wonder how many other attributes are differently named, and how tricky that makes it for macro writers. I just dont understand why there wasn't a plan when making the characters to have a single scheme for naming the attributes that was set for all supplements.

I'll tweak the script when i get a chance, but its bedtime here now.


October 08 (6 years ago)

Edited October 08 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

I made a quick change to the script before bed, and updated it in the post above.  

If the npc_type attribute isnt found, it whispers you a message telling you this, and uses the base macro as a fallback.

I'll put a loop to search for type variants tomorrow.

If you could make a list of all the type variants you have found, that would be good too. I can search for those explicitly, and include a way for you to add extras as you find them. I'll also have a general "type" search as a fallback (because it will catch any attributes including type in the name, and they could be a lot of those, especially with repeating attributes, and gm's manually created attributes, so it can't be relied on and could lead to errors). 

October 08 (6 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

G G said:

also I'm not sure how you are using this:

#sb-!learn @{target|character_id} type

but API commands need to start with the ! at the very start of the line. They wont work otherwise.

What does #sb- represent in this context, how is it being used?

Dredging something up from the past, but this isn't quite correct. If you want the command to not be displayed in chat it needs to start with "!", but the API responds to any chat message, and that's really all that api commands are. Now, of course if you are gating your response based on the formatting or API type chat message that is a different story.

October 08 (6 years ago)

Types found in Character Sheet (shaped 5e), monster directly from R20 5e compendium, and from at least one module (Tomb of Annihilation)

Ones that matter (or could have the useful information in them):

type
type_from_srd
npc_typebase
npcd_type
npc_type

Ones that don't matter, but will get caught in the full "type" search anyway (probably not helpful, but included for completeness)

hitdie_final
npcd_actype
npc_actype
dtype
drop_itemtype
drop_damagetype
drop_attack_type
drop_damagetype2
drop_spellhldietype
drop_type
caster_type
item_type_from_srd
damage_type_from_srd

October 08 (6 years ago)

Edited October 08 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

Why is hitdie_final in that second list?

Here's a quick script to find all the attributes with type in the name, and their contents. Run this and see if you can identify all the ones that are appropriate to use.

It'll search through all characters in your campaign, and list the attribute name, its contents, and the creature that posses the attribute.

I'm curious if there are duplicate types on the same creature, and whether matches with (undead, fiend, elf, etc) are possible with types that aren't creature types.

on('ready',function(){
    'use strict';
    on('chat:message',function(msg){
        if('api' === msg.type && msg.content.match(/^!checktype/)  ){ 
            var npcs = findObjs({ type: 'character', controlledby: '' });
                  
            var types = filterObjs(function(obj) {    
            if(obj.get("type") === 'attribute' && obj.get('name').toLowerCase().includes('type') && !obj.get('name').toLowerCase().includes('repeating') && obj.get('current') !== "") return true;    //NPCNew Test
                else return false;
            });
            types.forEach(type => {
                let character = getObj("character", type.get('characterid')).get('name');
                sendChat("testing","/w " + msg.who + " " + type.get('name') + ": " + type.get('current') + "; " + character);             });         }     }); });
October 09 (6 years ago)

Edited October 09 (6 years ago)

All of the useful ones are there, the ones it didn't report are because they are empty. The hitdie_final was a mistake on my part when copy/pasting; "dietype" appears in the attribute value, but not the name.

I haven't been able to come up with why a creature that wasn't undead would have something like "undead' in one of it's attributes. I think it's safe to continue with the assumption that if we find a valid creature type, it's a match to the creature description itself. I'll just be especially careful not to have "type" in any attributes I add for things like favored enemy on my sheet, to avoid just such a mismatch.

The Goblin and Skeleton are from the module, the Chain Demon from the Compendium, and Arkos is my Shaped sheet.

(From testing): (GM) npcd_type: Small humanoid (goblinoid), neutral; Goblin
(From testing): (GM) npcd_actype: (leather armor, shield); Goblin
(From testing): (GM) npc_type: Small humanoid (goblinoid), neutral evil; Goblin
(From testing): (GM) npc_actype: leather armor, shield; Goblin
(From testing): (GM) dtype: full; Goblin
(From testing): (GM) type: humanoid (goblinoid); Goblin
(From testing): (GM) npcd_type: Medium undead, lawful evil; Skeleton
(From testing): (GM) npcd_actype: (armor scraps); Skeleton
(From testing): (GM) npc_type: Medium undead, lawful evil; Skeleton
(From testing): (GM) npc_actype: armor scraps; Skeleton
(From testing): (GM) dtype: full; Skeleton
(From testing): (GM) type: undead; Skeleton
(From testing): (GM) type: fiend (devil); Chain Devil
(From testing): (GM) caster_type: half; Arkos
(From testing): (GM) type: human; Arkos

I'm checking with my DM to verify that I populated the "type" field and that it was blank as original.

OK, confirmed that the inconsistency is correct. Some sheets have "type" some have "npc_type" or one of the others. The filled variant is only in the sandbox game I'm testing in.

October 09 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

It looks like the goblin has three attributes with goblinoid in them, and the skeleton has three with undead in them. Is this true (and if so, any idea why this is)?

October 09 (6 years ago)

Edited October 09 (6 years ago)

It is true, and I can only speculate as to why; however, in the main game, the "type" attribute is blank, so it's really only two that are readily filled. Same for skeleton.

My best guess is the npc_type and npcd_type must be from different import macros, or perhaps just the sheet used for the monsters. It definitely strikes me as odd that the most basic of fields aren't consistent between modules/compendiums.

I don't see it being an issue though, assuming the script will load up types into an array the same as your last snippet and then search each item in the array to see if it matches, then break out when it finds one.

Trying to add in a catch for hitting <favored enemy 1> AND <favored enemy 2> instead of just checking for an OR would add a level of complexity that is unnecessary for this. I greatly appreciate your help, and don't want to send you down any tangential rabbit holes that I don't see an immediate use case for.

October 09 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

Here's a new version of the script for you to try (it includes the check_types script too, so you'll want to delete that).

This doesnt cycle through every type attribute, only those that have been identified as containing appropriate type data.

There's a line near the top:

    let npc_types = [ 'type','npc_type', 'npcd_type', 'npc_typebase','type_from_srd']; // edit this line if you find more types

You can add extra types there if you find any that contain creature data. The script will look at them in the order listed, and stop when it finds the first that a) exists, and b) isn't empty.

If it finds none, it will use the base macro.

Here goes:

/*
Launch with attack macro like
!chooseam @{target|character_id} attackbase types|to|check  e.g.
!chooseam @{target|character_id} ?{Attack Type|Standard,sb|With PW,sbpw} fiend|undead|goblin
*/
on('ready',function(){
    'use strict';
    let npc_types = [ 'type','npc_type', 'npcd_type', 'npc_typebase','type_from_srd']; // edit this line if you find more types
    let getNPCType = function(target) {
        for (let i = 0; i < npc_types.length; i++) {
            let npc_Type = getAttrByName(target, npc_types[i]);
            if(npc_Type !== undefined && npc_Type !== '' ) { //&& getAttrByName(target,npc_Type) !== ''
                return npc_Type;
            }
        }
    };
   
    
    let getSpeaker = function(msg) {
        var characters = findObjs({_type: 'character'});
        var speaking;
        characters.forEach(function(chr) { if(chr.get('name') == msg.who) speaking = chr; });
     
        if(speaking) return 'character|'+speaking.id;
        else return'player|'+msg.playerid;
    };
    
    let getMacro = function(msg,name) {
    let m = findObjs({
        _playerid: msg.playerid,
        name: name
    });
    if(m[0] === undefined) {
        sendChat("Choose Macro","/w " + msg.who + " Error: Macro Not Found");
        return;
    }
   
    return m[0].get('action');
    
}

    on('chat:message',function(msg){
        if('api' === msg.type && msg.content.match(/^!chooseam/)  ){ 
            let args = msg.content.split(/\s+/);
            if (args.length < 3) {
                sendChat("Choose Macro","/w " + msg.who + " Input error: Not enough parameters.");
                return;
            }
 
            let target = args[1];
            let macrobase = args[2];
            let enemytypes = args[3].split('|');
            let tartype = getNPCType(target);
            let foundtype = 'base';
            if(tartype === undefined) {
                sendChat("Choose Macro","/w " + msg.who + " Error: NPC Type not found, proceeding with base macro");
            } else {
                for (let i = 0; i < enemytypes.length; i++) {
                    if(tartype.toLowerCase().includes(enemytypes[i].toLowerCase())) {
                        foundtype = enemytypes[i].toLowerCase();
                        break;
                    }
                }
            }
            
            let macro = getMacro(msg, `${macrobase}-${foundtype}`);
            sendChat(getSpeaker(msg),macro) ;
        } else if('api' === msg.type && msg.content.match(/^!checktype/)  ){ 
            var npcs = findObjs({ type: 'character', controlledby: '' });
            
            let types = filterObjs(function(obj) {    
                if(obj.get("type") === 'attribute' && obj.get('name').toLowerCase().includes('type') && !obj.get('name').toLowerCase().includes('repeating') && obj.get('current') !== "" ) return true;    //NPCNew Test
                else return false;
            });
            types.forEach(type => {
                let character = getObj("character", type.get('characterid')).get('name');
                sendChat("testing","/w " + msg.who + " " + type.get('name') + ": " + type.get('current') + "; " + character);
            });
        } 
    });
});
October 09 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter


Takryn said:

Trying to add in a catch for hitting <favored enemy 1> AND <favored enemy 2> instead of just checking for an OR would add a level of complexity that is unnecessary for this. I greatly appreciate your help, and don't want to send you down any tangential rabbit holes that I don't see an immediate use case for.

Can a creature ever be both, and do you get any advantage if they are?


October 09 (6 years ago)

Edited October 09 (6 years ago)

As far as I know, there is no overlapping of type (at least in 5e). For instance if a goblin dies and is reanimated, I'd expect it to be reclassified simply as undead, not undead goblin. Unless I come across a reason to need it, what you've created should work like a charm. I'll begin playing with it here shortly.

There could certainly be a bonus for another aspect of the npc_type line, but that would be another call with a different search word. As in looking for "lawful" or "evil" or some such. Again, the lack of consistency poses input problems, but that would be a reason to not break out when the first match is found.

October 09 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

There might be another attribute that holds alignment data. I notice the chain devil type doesnt list alignment.

October 09 (6 years ago)

Edited October 09 (6 years ago)

Right, the alignment and size are included in "npc_type" as far as I've seen.

The API is reporting "Error: When using sendChat() you must specify a speakingas and input property." when I push the case for not having the macro created on my character.

The API also crashes when not having sufficient inputs, ie if  I leave off the search types. The error catcher is there though, so I've got no idea.

!chooseam @{target|character_id} ?{Attack Type|Base,sb|PW,sbpw}

TypeError: Cannot read property 'split' of undefined

I have no expectation for you to keep tweaking the script, I'm just letting you know what I've come across. As it stands it'll do everything I need it to.

Thank you a ton for setting this up for me. Once I get a play session under my belt using it, I plan to write up a description of the macro system to go with the script then post it as a reference for others to use.

October 09 (6 years ago)

Edited October 09 (6 years ago)

Ah, I do have a follow up question: If I were to create abilities on my character sheet rather than the global macros (so that they go with my character from game to game rather than having to recreate them each time), what would need to change in the script call? How is 

 let macro = getMacro(msg, `${macrobase}-${foundtype}`);
let getMacro = function(msg,name) {
    let m = findObjs({
        _playerid: msg.playerid,
        name: name
    });
    if(m[0] === undefined) {
        sendChat("Choose Macro","/w " + msg.who + " Error: Macro Not Found");
        return;
    }
   
    return m[0].get('action');
    
}

converting it to have the # in front? I never see any reference to the # so can't change it to %{player|ability-name} format.

October 09 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter


Takryn said:

Right, the alignment and size are included in "npc_type" as far as I've seen.

The API is reporting "Error: When using sendChat() you must specify a speakingas and input property." when I push the case for not having the macro created on my character.

The API also crashes when not having sufficient inputs, ie if  I leave off the search types. The error catcher is there though, so I've got no idea.

!chooseam @{target|character_id} ?{Attack Type|Base,sb|PW,sbpw}

TypeError: Cannot read property 'split' of undefined

I have no expectation for you to keep tweaking the script, I'm just letting you know what I've come across. As it stands it'll do everything I need it to.

Thank you a ton for setting this up for me. Once I get a play session under my belt using it, I plan to write up a description of the macro system to go with the script then post it as a reference for others to use.

The first API isn't anything to worry about.

The second one is a bit weird, I'll investigate later. For now you should be able to avoid it by adding an extra argument that will never be matched, like "none". 

!chooseam @{target|character_id} ?{Attack Type|Base,sb|PW,sbpw} none


It is possible to change it to use abilities, but it'll need a tweak of the getmacro function. I just realised I left off an important element: 

            let m = findObjs({
        _playerid: msg.playerid, _type: 'macro',
        name: name
    });

You should be able to find abilities the same way, but will need to change the _type to 'ability' and add a character.id, too. Too late for me to work on this tonight, I'll look at it tomorrow.

You don't use the # in this script. The way the script works is to find the macro, extract the text within the macro, and print it directly to chat. 

Abilities will work the same way.

Honestly though, I'm thinking of rewriting the script to rebuild the attack text. I had a look at those 6 attacks you supplied a few posts back, and the bulk of them is identical, so it would be more elegant to use one of them as base, and just add in the extra bits you need for the different versions. It can include attribute calls for relevant stats, so it wont get out of date when your stats change.

I'll have a think about that in the next couple of days.

October 09 (6 years ago)

Edited October 09 (6 years ago)
I'll preface this post by saying I understand this could have been approached differently. My primary drivers have been to automate as much of the process as possible that isn't based on my active choices, and to create "pretty" output to the chat, where all of the damage is nicely combined by damage type and no additional calculations need be performed.
Having seen them, I am planning to use a combination of the token buttons, rather than queries, and the script for automation. I would rather have my inputs be to click one button for the weapon, and then to select my target and have the rest done automatically. That leaves me with 4 buttons per weapon (Base, Planar Warrior, Radiant Soul, PW+RS). 
I do like the idea of building the macros automatically as well since there are 12 macros per weapon. It would only be 8 for any non-SunBlade weapon, but due to the way we return the target type, each possible option from the search entries must have its own macro. The relevant stat changes I'm definitely planning to implement. I'm not sure why the shapedSheet call uses a snapshot of the ability mod instead of calling the ability_mod attribute itself. Probably some internal efficiency gains since the sheet macro is being generated by the sheet every time it's clicked, so maybe it will always snapshot it from the current state anyway.
Overall, I know I've spent far more time working on this than I'll ever save. Nonetheless, I've learned a ton and the play during gaming sessions will be smoother for me (and my cohorts once I roll this out to them). In addition, anyone else who decides to go this route will be positive from the start. 

Weapon Planar Warrior Radiant Soul Undead Fiend Macro Name Macro #
Sun Blade - - - - SB-base 1
Sun Blade Yes - - - SBpw-base 2
Sun Blade - Yes - - SBrs-base 3
Sun Blade Yes Yes - - SBpwrs-base 4
Sun Blade - - Yes - SB-undead 5
Sun Blade Yes - Yes - SBpw-undead 6
Sun Blade - Yes Yes - SBrs-undead 7
Sun Blade Yes Yes Yes - SBpwrs-undead 8
Sun Blade - - - Yes SB-fiend 9
Sun Blade Yes - - Yes SBpw-fiend 10
Sun Blade - Yes - Yes SBrs-fiend 11
Sun Blade Yes Yes - Yes SBpwrs-fiend 12
Long Bow - - - - LB-base 13
Long Bow Yes - - - LBpw-base 14
Long Bow - Yes - - LBrs-base 15
Long Bow Yes Yes - - LBpwrs-base 16
Long Bow - - Yes - LB-undead 17
Long Bow Yes - Yes - LBpw-undead 18
Long Bow - Yes Yes - LBrs-undead 19
Long Bow Yes Yes Yes - LBpwrs-undead 20
Long Bow - - - Yes LB-fiend 21 = LB-undead
Long Bow Yes - - Yes LBpw-fiend 22 = LBpw-undead
Long Bow - Yes - Yes LBrs-fiend 23 = LBrs-undead
Long Bow Yes Yes - Yes LBpwrs-fiend 24 = LBpwrs-undead
Short Sword - - - - SS-base 25
Short Sword Yes - - - SSpw-base 26
Short Sword - Yes - - SSrs-base 27
Short Sword Yes Yes - - SSpwrs-base 28
Short Sword - - Yes - SS-undead 29
Short Sword Yes - Yes - SSpw-undead 30
Short Sword - Yes Yes - SSrs-undead 31
Short Sword Yes Yes Yes - SSpwrs-undead 32
Short Sword - - - Yes SS-fiend 33 = SS-undead
Short Sword Yes - - Yes SSpw-fiend 34 = SSpw-undead
Short Sword - Yes - Yes SSrs-fiend 35 = SSrs-undead
Short Sword Yes Yes - Yes Spwrs-fiend 36 = SSpwrs-undead


October 09 (6 years ago)

Edited October 10 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

I just updated the origin script to handle abilities. See this post. I also fixed that issue when favoured enemies are missing.

When using abilities, you need to supply the id of the character who has the abilities, which you can do with @{WHATEVER CHARACTER NAME IS|character_id}, like so:

!chooseaa @{Thundarr|character_id} @{target|character_id} ?{Attack Type|Standard,sb|With PW,sbpw} fiend|undead

Note that you launch it with chooseaa, not chooseam, to distinguish the two versions.

I'll have a look over your last post as soon as I get a chance.