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 .
×

Macro or script to roll custom check type, using item mods

Hello all, I'm looking for a way to make a custom Frost Check for the campaign I'm in. It's based on either a straight CON check (not a CON save, just an ability check) or Survival check (whichever is higher), and can be modified by various items. What I'd like to do is have a macro or API script that can find a custom entry in the MODS field on said items, i.e. I have all the items in my inventory & equipped -> add the bonus e.g. +5 to the MODS field -> macro/script can find all items that have "Frost Checks" mods and apply them to the roll. Any ideas how I might be able to do this?
1672350567
GiGs
Pro
Sheet Author
API Scripter
You can't do that with a macro, but it could be done with an API script. You might need a custom script, but might be able to do it with ScriptCards - you'd be best off asking in the script's dedicated thread for people familiar with the syntax. A script approach would also let you choose different takes (you could change it from Frost to Fire, for instance). Someone else would have to write that for you (or as suggested, ask in ScriptCards and hope it can do it). If you are creating a character sheet, this could be done with a sheet worker and built into the character sheet. It could be more robust because you could limit the MODS column to a dropdown list, making sure players didnt enter typos!
1672362069
Gauss
Forum Champion
KaffN8ed27, what character sheet are you using? 
1672455233

Edited 1672455873
Oosh
Sheet Author
API Scripter
I had a spare minute and gave it a crack - you can give this a go. Select a character and type !imod --check <custom check name> This will check the character's items and find any values in the item mods fields which match the format: <custom check name> check: +|-X It will then whisper a button to the character to prompt them to roll the required check. This means you still get all the character sheet settings (advantage, whisper settings etc), 3d dice, and a genuine roll from the player. It also means the GM can do the roll requests if they prefer, and the players just get prompted with the button. So, for example, if you enter this in an item's item mod field: Frost Check: +5 (case is not important, but the colon is necessary to keep consistent with the sheet's current behaviour, also make sure all mods are comma separated) Then run: !imod --check frost You can omit the word 'check' from the name when running the script, or add it in if you prefer (e.g. --check frost check ), but it does need to be in the item mod name. The default behaviour is what you said above - the highest of Survival and Con Mod. You can also roll against other mods or saves if you'd like: !imod --check fire --save int Would check gear for Fire Check: +/- X mods, then add that to the character's intelligence save bonus . !imod --check covid --ability cha Would check gear for Covid Check: +/-X mods and add it to the character's charisma modifier . I didn't spend much time on this - there could well be errors. Also... I assumed this is the standard 5e sheet from your description. It can be adapted for another sheet by changing the attribute names and the roll template. /* globals on, getObj, findObjs, sendChat */ const imod = (() => { //eslint-disable-line no-unused-vars const scriptName = 'customItemMod' const CHECK_TYPE = { SAVE: { STRENGTH: { attribute: 'strength_save_bonus', abbreviation: 'STR Save', }, DEXTERITY: { attribute: 'dexterity_save_bonus', abbreviation: 'DEX Save', }, CONSTITUTION: { attribute: 'constitution_save_bonus', abbreviation: 'CON Save', }, WISDOM: { attribute: 'wisdom_save_bonus', abbreviation: 'WIS Save', }, INTELLIGENCE: { attribute: 'intelligence_save_bonus', abbreviation: 'INT Save', }, CHARISMA: { attribute: 'charisma_save_bonus', abbreviation: 'CHA Save', }, }, MOD: { STRENGTH: { attribute: 'strength_mod', abbreviation: 'STR', }, DEXTERITY: { attribute: 'dexterity_mod', abbreviation: 'DEX', }, CONSTITUTION: { attribute: 'constitution_mod', abbreviation: 'CON', }, WISDOM: { attribute: 'wisdom_mod', abbreviation: 'WIS', }, INTELLIGENCE: { attribute: 'intelligence_mod', abbreviation: 'INT', }, CHARISMA: { attribute: 'charisma_mod', abbreviation: 'CHA', }, } }; const REPLACER = { CHARACTER_NAME : '%%CHARACTER_NAME%%', CHECK_MODIFIER : '%%CHECK_MODIFIER%%', CHECK_NAME : '%%CHECK_NAME%%', ABILITY_MODIFIER: '%%ABILITY_MODIFIER%%', } const customConstitutionSurvivalCheck = (character) => { const attributes = findObjs({ type: 'attribute', characterid: character.id }); let conMod = 0, survival = 0; const targetAttribute = attributes.reduce((output, attribute) => { if (attribute.get('name') === 'constitution_mod') { conMod = parseInt(attribute.get('current')) || 0; output = conMod > survival ? 'constitution_mod' : 'survival_bonus'; } else if (attribute.get('name') === 'survival_bonus') { survival = parseInt(attribute.get('current')) || 0; output = conMod > survival ? 'constitution_mod' : 'survival_bonus'; } return output; }, 'constitution_mod'); return { attribute: targetAttribute, abbreviation: /^sur/.test(targetAttribute) ? 'SURV' : 'CON', } } const defaultCheckType = customConstitutionSurvivalCheck const processCustomCheck = (commandsObject) => { const inputAbility = commandsObject.saveAbility[0] || commandsObject.checkAbility[0]; const ABILITY = commandsObject.saveAbility[0] ? CHECK_TYPE.SAVE : CHECK_TYPE.MOD; const targetAbility = !inputAbility ? defaultCheckType(commandsObject.character) : /^str/i.test(inputAbility) ? ABILITY.STRENGTH : /^dex/i.test(inputAbility) ? ABILITY.DEXTERITY : /^con/i.test(inputAbility) ? ABILITY.CONSTITUTION : /^wis/i.test(inputAbility) ? ABILITY.WISDOM : /^int/i.test(inputAbility) ? ABILITY.INTELLIGENCE : /^cha/i.test(inputAbility) ? ABILITY.CHARISMA : null; if (!targetAbility) return; const checkNameString = /check$/i.test(commandsObject.checkName) ? commandsObject.checkName.replace(/:/g, '') : `${commandsObject.checkName} check`.replace(/:/g, ''); const rxCheckName = new RegExp(escapeRegex(checkNameString), 'i'), itemMods = getAllItemMods(commandsObject.character), modTotal = itemMods.reduce((output, mod) => { if (rxCheckName.test(mod)) { const [ _, operator, number ] = mod.match(/:\s*(-|\+)?\s*(\d+)/); if (number) { return operator === '-' ? output - parseInt(number) : output + parseInt(number); } } return output; }, 0); sendCustomCheckRoll(emproper(checkNameString), modTotal, targetAbility, commandsObject.character.get('name')); } const escapeRollForButton = (rollString) => { const replacers = { '@': '@', '{': '{', '}': '}', '[': '[', ']': ']', '?': '?', '|': '|', ':': ':', '"': '"', '(': '(', ')': ')', } const rxReplacers = new RegExp(`[${escapeRegex(Object.keys(replacers).join(''))}]`, 'g'); return rollString.replace(rxReplacers, (match) => replacers[match]); } const sendCustomCheckRoll = (checkName, checkModifier, saveAbility, characterName) => { const rollTemplate = `@{${REPLACER.CHARACTER_NAME}|wtype}&{template:simple} {{rname=${REPLACER.CHECK_NAME}}} {{mod=[[${REPLACER.CHECK_MODIFIER}[Items] + @{${REPLACER.CHARACTER_NAME}|${REPLACER.ABILITY_MODIFIER}}[${saveAbility.abbreviation}] ]]}} {{r1=[[@{${REPLACER.CHARACTER_NAME}|d20}+${REPLACER.CHECK_MODIFIER}[Items]+@{${REPLACER.CHARACTER_NAME}|${REPLACER.ABILITY_MODIFIER}}[${saveAbility.abbreviation}] ]] }} @{${REPLACER.CHARACTER_NAME}|rtype}+${REPLACER.CHECK_MODIFIER}[Items]+@{${REPLACER.CHARACTER_NAME}|${REPLACER.ABILITY_MODIFIER}}[${saveAbility.abbreviation}] ]] }} {{global=@{${REPLACER.CHARACTER_NAME}|global_save_mod}}} @{${REPLACER.CHARACTER_NAME}|charname_output}`; const rollString = rollTemplate.replace(/%%\w+?%%/g, (m) => { if (m === REPLACER.CHARACTER_NAME) return characterName; else if (m === REPLACER.CHECK_MODIFIER) return checkModifier; else if (m === REPLACER.CHECK_NAME) return checkName; else if (m === REPLACER.ABILITY_MODIFIER) return saveAbility.attribute; else return ''; }); const buttonStyle = `color: darkblue; border: blue 1px solid; border-radius: 3px; padding: 1px 5px; font-variant: small-caps; margin-top: 1rem;` const escapedRoll = escapeRollForButton(rollString); // console.warn(escapedRoll); sendToChat(`&{template:npcaction}{{rname=${checkName} / ${saveAbility.abbreviation}}}{{description=%NEWLINE%[Roll ${checkName}](\`#${escapedRoll}" style="${buttonStyle})}}`, scriptName, characterName); } const sendToChat = (message, from, to) => { from = from || scriptName; to = to ? `/w "${to}" ` : '' sendChat(from, `${to}${message}`); } const getAllItemMods = (character) => { const allAttributes = findObjs({ type: 'attribute', characterid: character.id }); return allAttributes.reduce((output, attribute) => { return /_itemmodifiers$/.test(attribute.get('name')) && /^repeating_inv/i.test(attribute.get('name')) ? [ ...output, ...attribute.get('current').split(/\s*,\s*/) ] : output; }, []); } const getSelectedTokens = (selected, limit) => { const selectedIds = selected && selected.length ? selected.map(sel => sel._id) : null const tokens = selectedIds ? selectedIds.map(id => getObj('graphic', id)) : null; return tokens ? limit > 0 ? tokens.slice(0, limit) : tokens : []; } const getCharacterFromToken = (token) => { const representsId = token ? token.get('represents') : null; return representsId ? getObj('character', representsId) : null; } const escapeRegex = (string) => { return string.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&') } const emproper = (input) => { if (typeof(input) !== 'string') return; let words = input.replace(/_/g, ' ').trim().split(/\s+/g); let Words = words.map(w => `${w[0].toUpperCase()}${w.slice(1)}`); return Words.join(' '); } const isNpc = (character) => { const npcAttribute = findObjs({ type: 'attribute', name: 'npc', characterid: character.id })[0]; return (npcAttribute && `${npcAttribute.get('current')}` === '1') ? true : false; } const processCliArgs = (argsString) => { return argsString ? argsString.split(/\s*,\s*/) : []; } const processCliCommand = (command, args, commandObject) => { if (!command) return; switch(command.trim().toLowerCase()) { case 'check': { commandObject.customCheck = true; commandObject.checkName = processCliArgs(args); return; } case 'save': { commandObject.saveAbility = processCliArgs(args); return; } case 'ability': { commandObject.checkAbility = processCliArgs(args); return; } case 'mod': { commandObject.customMod = true; return; } default: { return; } } } on('ready', () => { on('chat:message', (message) => { if (message.type === 'api' && /^!imod\s/.test(message.content)) { const selectedCharacter = getCharacterFromToken(getSelectedTokens(message.selected, 1)[0]); if (!selectedCharacter) return sendToChat(`No character selected.`, scriptName, 'gm'); if (isNpc(selectedCharacter)) return sendToChat(`Target must be a player character`, scriptName, 'gm'); const cliOptions = message.content.split(/\s*--\s*/); cliOptions.shift(); const commandsObject = cliOptions.reduce((output, option) => { const [ _, command, args ] = (option.match(/([A-z]+)\s*(.*)/) || []); if (command) processCliCommand(command, args, output); return output; }, { customMod: false, customCheck: false, checkName: [], saveAbility: [], checkAbility: [], character: selectedCharacter, } ); if (commandsObject.customCheck && commandsObject.checkName.length) processCustomCheck(commandsObject); } }); }); })();
1672491242
GiGs
Pro
Sheet Author
API Scripter
You can see why I didn't volunteer to do the API version...