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

D&D 5e sheet - adding attack via api

1725741889

Edited 1725742025
Hello! I've encountered a little problem with implementing spell attacks via API scripts. I made a neat importer to be able to add Polish-translated spells into the game and it all works perfect bar for adding attacks onto the sheet. I seem to be missing a few attributes but after checking all the "repeating_attack_XXX" values, some aren't values but instead roll queues, so I don't really know what to add there. Maybe there's some function that processes all the data that I can't seem to find? Appreciate any help in advance!
It's pretty hard to provide any help without seeing exactly what your script does (seeing the script code) and the command that you are using to run the script. Another script that can modify repeating attacks is ChatSetAttr. It would look something like this: !setattr {{ --sel --silent --repeating_npcaction_-CREATE_name|?{Attack Name?|Basic Attack} --repeating_npcaction_-CREATE_attack_flag|{{attack=1}} --repeating_npcaction_-CREATE_attack_type|?{Damage type?|melee|ranged} --repeating_npcaction_-CREATE_attack_range|?{Attack Range?|5 ft.} --repeating_npcaction_-CREATE_attack_tohit|?{Attack To Hit Value?|5} --repeating_npcaction_-CREATE_attack_target|?{Attack target?|One target} --repeating_npcaction_-CREATE_attack_damage|?{Attack Damage?|1d8} --repeating_npcaction_-CREATE_attack_damagetype|?{Damage type?|bludgeoning} }}  However, with all of that said, if you are creating spells and want them to show up in the Attack section, you don't need to create the repeating attack. You just need to modify the 'spelloutput' flag: --repeating_spell-1_-CREATE_spelloutput|ATTACK --repeating_spell-1_-CREATE_spelloutput|SPELLCARD But the CREATE function isn't correctly triggering the sheetworkers when creating a spell as an ATTACK.  It's something I noticed a while back in this thread . I don't know if it's an error with how ChatSetAttr is coded, or a Roll20 bug of some kind. In the meantime, it sounds like you can use the code to create spells, but you'll have to manually toggle the OUTPUT field  to ATTACK  each time a spell is created in order to trigger the sheetworkers until a better solution is found.
1725778877

Edited 1725778985
Hello Thanks for the answer. I am not using chatsetattr, I am writing my own api for all that stuff since it's not done through chat (well, it reacts to a command, but then everything happens in .js). You mentioned that simply modifying "spelloutput" would do the trick, but that does not seem to change anything. It does with roll20-compendium-imported spells, yes, but not ones I put in by hand. So I presume I might've done something incorrectly. Generally I've somehow managed to add attacks, but they seem "unlinked" from the character sheet - as in no preview values within update whenever character's stats change (like spell save DC isn't adjusted). So I take there's something amiss. That being said, you asked for my code, so here it is. It's rather rudimentary, as it's just for personal use. Generally the command is irrelevant, as I have special buttons for that, but basically, to add a spell with my script, I put a command like so: !sadd Fireball I have a JSON of all the d&d spells I translated to Polish (as initially I used this as a translation tool, but wanted to expand it to be able to add homebrew stuff easily) and this function reads the JSON, finds the spell, injects it into the character sheet. Since I couldn't find info about how to generate a neat repeating section ID like roll20 does it, I simply added arbitraty IDs to all spells I have (basically their ID on the array of spells I have in my translation project) and it seems to work neatly (but maybe this is the issue?). Initially I just did $0, $1, etc. but this seemed, well, prettier (as I didn't have to iterate through existing spells to find which ID is not taken). In any case, after filling the spell data I add the attack via the function at the bottom. It's not ready yet, as I am still toying with it, but it seems that if I did something wrong when importing the spell and the attack should be added automatically, the function to create attack is not needed. In any case, I added it on the bottom. on("chat:message", function(msg) { if (msg.who === "Magic") return; if (!msg.content.startsWith("!sadd")) return; var searchData = msg.content.replace("!sadd ", ""); if (msg.selected == null || msg.selected.length === 0) { sendChat("Magic", "Próbuję dodać czar o ID=" + searchData + " ale nie wykryłem zaznaczonej postaci"); return; } var obj = getObj(msg.selected[0]._type, msg.selected[0]._id); var character = getObj('character', obj.get('represents')); getTranslationHandout().get('notes', function (contents) { var data = JSON.parse(contents); for (var Di = 0; Di < data.spells.length; ++Di){ var spell = data.spells[Di]; if (spell.spellID != searchData) continue; var current = findSpellOfCharacter(character, searchData); if (!current) current = findSpellOfCharacter(character, spell.spellname); var attackID = ""; var prefix = ""; if (current) { prefix = current.get('name').replace('spellname', ''); var spellAttackAttribute = findObjs({type:'attribute', characterid: character.id}) .filter((o) => o.get('name') == current.get('name').replace('_spellname', '_spellattackid'))[0]; attackID = spellAttackAttribute.get('current'); } else { prefix = 'repeating_spell-' + spell.spellLevel + '_' + spell.uniqueID + '_'; attackID = spell.uniqueAttackID; } addAttribute(character, prefix, 'rollcontent', ''); addAttribute(character, prefix, 'spellhldietype', ''); addAttribute(character, prefix, 'spellhldie', ''); addAttribute(character, prefix, 'spellhlbonus', ''); addAttribute(character, prefix, 'spell_damage_progression', ''); addAttribute(character, prefix, 'spellcomp_s', ''); addAttribute(character, prefix, 'spellcomp_v', ''); addAttribute(character, prefix, 'spellcomp_m', ''); addAttribute(character, prefix, 'spellcomp_materials', ''); addAttribute(character, prefix, 'spellritual', ''); addAttribute(character, prefix, 'spellconcentration', ''); addAttribute(character, prefix, 'spellattack', ''); addAttribute(character, prefix, 'spellsave', ''); addAttribute(character, prefix, 'innate', ''); addAttribute(character, prefix, 'spellsource', ''); addAttribute(character, prefix, 'spellattackid', ''); addAttribute(character, prefix, 'spellclass', ''); addAttribute(character, prefix, 'roll_output_dc', 10); addAttribute(character, prefix, 'options-flag', 0); addAttribute(character, prefix, 'details-flag', 0); if (spell.spellcastingtime.includes('Koncentracja')) addAttribute(character, prefix, 'spellconcentration', '{concentration=1}'); addAttribute(character, prefix, 'spellname', spell.spellname); addAttribute(character, prefix, 'spellrange', spell.spellrange); addAttribute(character, prefix, 'spellcastingtime', spell.spellcastingtime); addAttribute(character, prefix, 'spelltarget', spell.spelltarget); addAttribute(character, prefix, 'spellduration', spell.spellduration); addAttribute(character, prefix, 'spelldescription', spell.spelldescription); addAttribute(character, prefix, 'spellathigherlevels', spell.spellathigherlevels); addAttribute(character, prefix, 'spellschool', spell.school); if (spell.spellLevel == 0) addAttribute(character, prefix, 'spelllevel', 'cantrip'); else addAttribute(character, prefix, 'spelllevel', spell.spellLevel); addAttribute(character, prefix, 'spelldamage', spell.damage1); addAttribute(character, prefix, 'spelldamagetype', spell.damage1Type); addAttribute(character, prefix, 'spelldamage2', spell.damage2); addAttribute(character, prefix, 'spelldamagetype2', spell.damage2Type); addAttribute(character, prefix, 'spellhealing', spell.healing); addAttribute(character, prefix, 'spell_ability', 'spell'); addAttribute(character, prefix, 'spellsavesuccess', spell.saveEffect); if (spell.attackType == 'Melee' || spell.attackType == 'Ranged') { addAttribute(character, prefix, 'spellattack', spell.attackType); addAttribute(character, prefix, 'spellattackid', attackID); } else if (spell.attackType == 'MeleeUnlinked' || spell.attackType == 'RangedUnlinked') { addAttribute(character, prefix, 'spellattack', spell.attackType.replace('Unlinked', '')); } else { addAttribute(character, prefix, 'spellsave', spell.attackType); } if (spell.noModToDamageHeal) addAttribute(character, prefix, 'spelldmgmod', ''); else addAttribute(character, prefix, 'spelldmgmod', 'Yes'); if (spell.isRitual) addAttribute(character, prefix, 'spellritual', 'Yes'); if (spell.verbal) addAttribute(character, prefix, 'spellcomp_s', '{{s=1}}'); if (spell.somatic) addAttribute(character, prefix, 'spellcomp_v', '{{v=1}}'); if (spell.spellcomp_materials.length > 0) { addAttribute(character, prefix, 'spellcomp_m', '{{m=1}}'); addAttribute(character, prefix, 'spellcomp_materials', spell.spellcomp_materials); } if (spell.damage1.length > 0 && !spell.damage1.startsWith('0') || spell.healing.length > 0) addAttribute(character, prefix, 'spelloutput', 'ATTACK'); else addAttribute(character, prefix, 'spelloutput', 'SPELLCARD'); if (spell.upcast.includes('Cantrip')) { addAttribute(character, prefix, 'spell_damage_progression', spell.upcast); } else { var data = spell.upcast; var constPart = ""; var die = ""; var dieCount = ""; if (data.includes('+')) { var parts = data.split('+'); data = parts[0]; constParts = parts[1]; } if (data.includes('d')) { dieCount = data.substring(0, 1); die = data.substring(1); } else constPart = data; addAttribute(character, prefix, 'spellhldietype', die); addAttribute(character, prefix, 'spellhldie', dieCount); addAttribute(character, prefix, 'spellhlbonus', constPart); } fillSpellAttack(character, spell, attackID); addAttribute(character, prefix, '_spellattackid', attackID); return; } }); }); var fillSpellAttack = function(character, spell, attackID) { var prefix = 'repeating_attack_' + attackID + '_'; addAttribute(character, prefix, 'atkattr_base', 'spell'); addAttribute(character, prefix, 'options-flag', '0'); var spellLevel = spell.spellLevel == 0 ? 'cantrip' : spell.spellLevel; addAttribute(character, prefix, 'spelllevel', spellLevel); if (!spell.attackType.includes('Unlinked')) { addAttribute(character, prefix, 'spellid', spell.uniqueID); } else { addAttribute(character, prefix, 'spellid', '0'); } if (spell.attackType == 'Melee' || spell.attackType == 'Ranged' || spell.attackType == 'MeleeUnlinked' || spell.attackType == 'RangedUnlinked') { addAttribute(character, prefix, 'atkflag', '{{attack=1}}'); addAttribute(character, prefix, 'saveflag', '0'); addAttribute(character, prefix, 'saveeffect', ''); addAttribute(character, prefix, 'saveattr', ''); addAttribute(character, prefix, 'atkbonus', '+' + getAttr(character, 'spell_attack_bonus')); } else { addAttribute(character, prefix, 'atkflag', '0'); addAttribute(character, prefix, 'saveflag', '{{save=1}} {{saveattr=@{saveattr}}} {{savedesc=@{saveeffect}}} {{savedc=[[[[@{savedc}]][SAVE]]]}}'); addAttribute(character, prefix, 'saveeffect', spell.saveEffect); addAttribute(character, prefix, 'saveattr', spell.attackType); addAttribute(character, prefix, 'atkbonus', 'ST ' + getAttr(character, 'spell_save_dc')); } var hldmg = ''; if (spell.spellLevel > 0 && spell.upcast.length > 0) { var upcastDie = spell.upcast.substring(1); hldmg = '[[(1*?{Na którym poziomie?'; for (var Li = spell.spellLevel; Li < 10; ++Li) { hldmg += '|Level ' + Li + ',' + (Li - spell.spellLevel); } hldmg += '})' + upcastDie + ']]'; addAttribute(character, prefix, 'hldmg', '{{hldmg=' + hldmg + '}}'); } addAttribute(character, prefix, 'savedc', '@{spell_save_dc}'); addAttribute(character, prefix, 'atkname', spell.spellname); var cantripDamage = '[[round((@{level} + 1) / 6 + 0.5)]]dX'; var cantripDamage0 = '[[(round((@{level} + 1) / 6 + 0.5))-1]]dX'; var dmg1 = '0'; var dmg2 = '0'; var dmg1Type = spell.damage1Type; var dmg2Type = spell.damage2Type; var atkdmgtype = ''; if (spell.damage1.length > 0) { var dmg = spell.damage1.replace('mod+', ''); if (spell.spellLevel == 0) { var die = dmg.substring(1); if (dmg.startsWith('0')) dmg = cantrimDamage0; else dmg = cantripDamage; dmg = dmg.replace('dX', die); } addAttribute(character, prefix, 'dmgbase', dmg); dmg1 = dmg; if (!spell.noModToDamageHeal) { dmg1 = '@{spellcasting_ability}' + dmg; } addAttribute(character, prefix, 'dmgflag', '{{damage=1}} {{dmg1flag=1}}'); if (!spell.noModToDamageHeal) addAttribute(character, prefix, 'dmgattr', 'spell'); else addAttribute(character, prefix, 'dmgattr', ''); addAttribute(character, prefix, 'dmgtype', spell.damage1Type); var atkdmgtype = dmg + ' ' + spell.damage1Type; } if (spell.damage2.length > 0) { var dmg = spell.damage2.replace('mod+', ''); if (spell.spellLevel == 0) { var die = dmg.substring(1); if (dmg.startsWith('0')) dmg = cantrimDamage0; else dmg = cantripDamage; dmg = dmg.replace('dX', die); } addAttribute(character, prefix, 'dmg2base', dmg); dmg2 = dmg; addAttribute(character, prefix, 'dmg2flag', '{{damage=1}} {{dmg2flag=1}}'); addAttribute(character, prefix, 'dmg2attr', ''); addAttribute(character, prefix, 'dmg2type', spell.damage2Type); atkdmgtype += ' ' + dmg2 + ' ' + spell.damage2Type; } var rollBaseDamage = '@{wtype}&{template:dmg} {{rname=@{atkname}}} @{atkflag} {{range=@{atkrange}}} @{dmgflag} {{dmg1=[[' + dmg1 + ']]}} {{dmg1type=' + dmg1Type +'}} @{dmg2flag} {{dmg2=[['+dmg2+']]}} {{dmg2type='+dmg2Type+'}} @{saveflag} {{desc=@{atk_desc}}} @{hldmg} {{spelllevel=@{spelllevel}}} {{innate=@{spell_innate}}} {{globaldamage=[[0]]}} {{globaldamagetype=@{global_damage_mod_type}}} {{spelldesc_link=[Show Spell Description](~repeating_attack_spelldesc_link)}} @{charname_output}'; var rollBase = 'rollbase @{wtype}&{template:dmg} {{rname=@{atkname}}} @{atkflag} {{range=@{atkrange}}} @{dmgflag} {{dmg1=[['+dmg1+']]}} {{dmg1type='+dmg1Type+'}} @{dmg2flag} {{dmg2=[['+dmg2+']]}} {{dmg2type='+dmg2Type+'}} @{saveflag} {{desc=@{atk_desc}}} @{hldmg} {{spelllevel=@{spelllevel}}} {{innate=@{spell_innate}}} {{globaldamage=[[0]]}} {{globaldamagetype=@{global_damage_mod_type}}} ammo=@{ammo} {{spelldesc_link=[Show Spell Description](~repeating_attack_spelldesc_link)}} @{charname_output}'; var rollBase_crit = '@{wtype}&{template:dmg} {{crit=1}} {{rname=@{atkname}}} @{atkflag} {{range=@{atkrange}}} @{dmgflag} {{dmg1=[['+dmg1+']]}} {{dmg1type='+dmg1Type+'}} @{dmg2flag} {{dmg2=[['+dmg2+']]}} {{dmg2type='+dmg2Type+'}} {{crit1=[['+dmg1+']]}} {{crit2=[['+dmg2+']]}} @{saveflag} {{desc=@{atk_desc}}} @{hldmg} {{hldmgcrit='+hldmg+'}} {{spelllevel=@{spelllevel}}} {{innate=@{spell_innate}}} {{globaldamage=[[0]]}} {{globaldamagecrit=[[0]]}} {{globaldamagetype=@{global_damage_mod_type}}} {{spelldesc_link=[Show Spell Description](~repeating_attack_spelldesc_link)}} @{charname_output}'; addAttribute(character, prefix, 'rollBaseDamage', rollBaseDamage); addAttribute(character, prefix, 'rollBase', rollBase); addAttribute(character, prefix, 'rollBase_crit', rollBase_crit); addAttribute(character, prefix, 'spelldesc_link', '%{-O6CVDVDqd58ha1x8X-K|repeating_spell-'+spellLevel+'_'+spell.uniqueID+'_output}'); addAttribute(character, prefix, 'atkrange', spell.spellrange); addAttribute(character, prefix, 'atkdmgtype', atkdmgtype); } var addAttribute = function(character, prefix, attributeName, value) { var name = prefix + attributeName; var attribute = findObjs({type:'attribute', characterid: character.id}) .filter((o) => o.get('name') == name)[0]; if (attribute) attribute.set('current', value); else { attribute = createObj('attribute', { characterid: character.id, name: prefix + attributeName, current: value }); } }