Here's a first pass at it: it's pinched almost entirely from Aaron's code, it's currently expecting the cover object to be on the map layer - that's easy enough to change later. It's also expecting the standard R20 map scale, of 70 pixels per 5ft grid. You can change this variable if you need something else, at the top of the script. The script has the same limitations as Aaron's Tiles, in that it only detects the final resting point - that shouldn't be an issue for cover mechanics though. But something to keep in mind if a player moves out of cover, then back into cover, and is shot in between by reaction fire. If they don't manually place their token out of cover so the script can register it, they will still have a cover bonus while getting shot (I have no idea what rules you're playing, just pointing it out :) ) All the script does now is detection, and throws a message in chat when a token enters or leaves cover. The rest is reasonably easy though. Let me know if it works - I didn't test with circular auras, but it should act just like a square. Oh... it also ignores anything without an aura - if you want a token to register as a cover object it needs to (currently): - be on the map layer - have an aura greater than 0 feet const coverMe = (() => { // eslint-disable-line no-unused-vars const checkInstall = () => { // eslint-disable-next-line no-prototype-builtins if (!state.hasOwnProperty('coverMe')) { state.coverMe = {tokenList: {}}; } let activeTokens = _.pluck(JSON.parse(Campaign().get('turnorder')),'id'); let counter = 0; for (let t in state.coverMe.tokenList) { if (!activeTokens.includes(t)) { delete state.coverMe.tokenList[t]; counter ++; } } log(`-= coverMe started: ${counter} entries cleaned up from state object, ${Object.keys(state.coverMe.tokenList).length} entries left. =-`); } const pixelsPerFoot = 14; const findContains = (obj,filter,layer) => { if(obj) { let cx = obj.get('left'), cy = obj.get('top'); filter = filter || (() => true); layer = layer || 'gmlayer'; return findObjs({ _pageid: obj.get('pageid'), _type: "graphic", layer: layer }) .filter(filter) .reduce((m,o) => { let aura = parseInt(o.get('aura1_radius'))*pixelsPerFoot; let l=o.get('left'); let t=o.get('top'); let w=parseInt(o.get('width')) + 2*aura; let h=parseInt(o.get('height')) + 2*aura; let ol=l-(w/2); let or=l+(w/2); let ot=t-(h/2); let ob=t+(h/2); if( ol <= cx && cx <= or && ot <= cy && cy <= ob ){ m.push(o); log(o); } return m; },[]); } return []; }; const onMoveGraphic = (obj,prev) => { if ( ['objects','gm'].includes(obj.get('layer')) && ( parseInt(obj.get('left')) !== parseInt(prev.left) || parseInt(obj.get('top')) !== parseInt(prev.top) )) { let objId = obj.get('_id'); let coverObj = findContains(obj,(o)=>parseInt(o.get('aura1_radius'))>0,'map'); if (coverObj.length) { if (state.coverMe.tokenList[objId]) return; state.coverMe.tokenList[objId] = coverObj[0].get('name'); sendChat('coverMe',`${obj.get('name')} entered cover of ${coverObj[0].get('name')}`); // do stuff to Attributes } else if (state.coverMe.tokenList[objId]) { sendChat('coverMe',`${obj.get('name')} left the cover of ${state.coverMe.tokenList[objId]}`); state.coverMe.tokenList[objId] = null; // undo stuff to Attributes } } } const handleInput = (msg) => { if (msg.type === 'api' && msg.content.search(/^!coverme\s/i) !== -1) { let lastPlayer = getObj('player', msg.playerid).get('displayname'); let commands = msg.content.split(/\s*--\s*/); if (commands.length > 1) { //let args = []; commands.shift(); commands.forEach(cmd => { switch(cmd) { case 'list': getList(); break; case 'settings': // settings break; case 'help': // sendHelp(); break; } }) } else sendChat('coverMe', `/w ${lastPlayer} No commands detected.`); } } const getList = () => { let coverList = []; for (let t in state.coverMe.tokenList) { if ([t] !== null && getObj('graphic', t)) { coverList.push(`${getObj('graphic', t).get('name')} is in cover of ${state.coverMe.tokenList[t]}`) } } let body = (coverList.length === 0) ? `No tokens currently in cover.` : coverList.join('<br>'); sendChat('coverMe', `/w gm &{template:default}{{name=List}}{{${body}}}`) } on('ready', () => { checkInstall(); on('change:graphic',onMoveGraphic); on('chat:message', handleInput); }) })();