Edit - Whoops! Forgot to mention this requires PathMath (available in one-click). Depending on your needs, you could use this - it's just some functions ripped out of another script to check for collisions. If a collision is found, it won't move the token, otherwise, it sends the command on to TokenMod. Usage is simple - select a token and use the top/left commands from tokenmod: !tmblock top|+70 left|+70 If it's useful I can probably fix up the command line so it sends the whole thing through to TokenMod if LOS isn't blocked - so you can use other TM commands in the same line and they'll be passed along. Currently, it only passes on the top and left arguments - you can use +, -, or a flat number as an absolute coordinate. It also just throws a public message out if it blocks the command... I didn't spend long on it. /* globals PathMath, findObjs, on, getObj, sendChat */ const tmBlock = (() => { const scriptName = 'tmBlock'; /** * Check for LOS blocking walls between token and light source * @param {object} token token object * @param {object} newPosition { x, y } * @param {number} range pixel range * @param {object} page map page object * @returns {null|object} returns null if no LOS block, or first path object which blocks the light source */ const checkLineOfSight = (token1, newPosition, page) => { const pos1 = { x: parseInt(token1.get('left')), y: parseInt(token1.get('top')) }, pos2 = newPosition, blockingPaths = findObjs({ type: 'path', pageid: page.id, layer: 'walls' }); const losPath = new PathMath.Path([[pos1.x, pos1.y, 0], [pos2.x, pos2.y, 0]]); let losBlocked = null; for (let i=0; i<blockingPaths.length; i++) { let pathData; try { pathData = JSON.parse(blockingPaths[i].get('path')); } catch(e) { console.error(e) } if (!pathData) continue; const pathTop = blockingPaths[i].get('top') - (blockingPaths[i].get('height')/2), pathLeft = blockingPaths[i].get('left') - (blockingPaths[i].get('width')/2); const pathVertices = pathData.map(vertex => [ vertex[1] + pathLeft, vertex[2] + pathTop, 0 ]); const wallPath = new PathMath.Path(pathVertices); const wallSegments = wallPath.toSegments(), losSegments = losPath.toSegments(); for (let w=0; w<wallSegments.length; w++) { if (losBlocked) break; for (let l=0; l<losSegments.length; l++) { const intersect = PathMath.segmentIntersection(wallSegments[w], losSegments[l]);//wallPath.intersects(losPath); if (intersect) { losBlocked = blockingPaths[i]; break; } } } if (losBlocked) break; } return losBlocked; } const handleInput = (msg) => { if (msg.type === 'api' && /^!tmblock/i.test(msg.content)) { const tokens = getSelectedTokens(msg.selected || []); if (!tokens.length) return; const commands = { top: (msg.content.match(/top\|\s*([\S]+)/i)||[])[1], left: (msg.content.match(/left\|\s*([\S]+)/i)||[])[1], }; const newPosition = parseNewPosition(commands, tokens[0]); if (!newPosition) return sendChat(scriptName, `Token isn't moving.`); const blocked = checkLineOfSight(tokens[0], newPosition, getPageOfToken(tokens[0])); if (blocked) { sendChat(scriptName, `Path is blocked!`); } else { const tokenModCommand = `!token-mod --ids ${tokens[0].id} --set ${commands.top ? `top|${commands.top}` : ''} ${commands.left ? `left|${commands.left}` : ''}`; sendChat(scriptName, tokenModCommand); } } } const getPageOfToken = (token) => token && token.id ? getObj('page', token.get('_pageid')) : null; const parseNewPosition = (commands, token) => { const position = [ 'top', 'left' ].reduce((output, direction) => { if (commands[direction]) { const number = parseInt(commands[direction].replace(/\D/g, '')) || 0; const newPosition = /^\+/.test(commands[direction]) ? token.get(direction) + number : /^-/.test(commands[direction]) ? token.get(direction) - number : number; if (direction === 'top') output.y = newPosition; else output.x = newPosition; } return output; }, { x: token.get('left'), y: token.get('top') }); return position; } /** * @param {object[]} selected array of simple token objects * @returns {object[] | null} array of actual token objects */ const getSelectedTokens = (selected) => { const selectedIds = selected && selected.length ? selected.map(sel => sel._id) : null return selectedIds ? selectedIds.map(id => getObj('graphic', id)) : null; } on('ready', () => { on('chat:message', handleInput); }); })();