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 = { '@': '&commat;', '{': '&lcub;', '}': '&rcub;', '[': '&lsqb;', ']': '&rsqb;', '?': '&quest;', '|': '&vert;', ':': '&colon;', '"': '&quot;', '(': '&lpar;', ')': '&rpar;', } 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); } }); }); })();