I happened to have a few spare minutes this afternoon, so here you go: gloomTracker - track the tokens of marked characters and apply a marker when under a max light level. REQUIRED: checkLightLevel script Usage: !gloomtrack <commands> Commands: --add <character_id> adds a character to be tracked - all tokens which represent this character will be tracked --rem <character_id> remove a character from tracking --max <number> a number between 0 and 1, the maximum light level before invisibility is lost. This defaults to 0.15 as per Keith's suggestion. --marker <marker-name> the marker to use when invisibility is applied. Defaults to ninja-mask. To get started, all you should really need to do is select a token of the Gloomstalker, and run: !gloomtrack --add @{selected|character_id} And if you wanted to change the marker and give them a bit more leeway with light, something like: !gloomtrack --max 0.2 --marker half-heart Obviously this script relies on the tokens being correctly linked to the character sheet. Let me know if there's issues! const gloomTracker = (() => { const scriptName = 'gloomTracker'; const rxId = /[0-z-]{20}/; const rxScriptCommand = /^!gloomtrack/i; const chat = (message, gm = true) => { const prefix = gm ? '/w gm ' : ''; sendChat(scriptName, `${prefix}${message}`); } const editCharacterIdArray = (characterId, remove = false) => { const character = getObj('character', characterId); if (rxId.test(characterId) && character) { if (remove) { let removed = false; for (let i = state[scriptName].characters.length - 1; i >= 0; i--) { if (state[scriptName].characters[i] === characterId) { state[scriptName].characters.splice(i, 1); removed = true; } } if (removed) chat(`Removed character id ${characterId} from ${scriptName}`) } else { state[scriptName].characters.push(characterId); chat(`Added character ${character.get('name')} to ${scriptName}`); } } else { chat(`Could not find character from ID "${characterId}"`); } } const addCharacter = (characterId) => editCharacterIdArray(characterId); const removeCharacter = (characterId) => editCharacterIdArray(characterId, true); const setLightLevel = (newValue) => { const floatValue = parseFloat(newValue); if (isNaN(floatValue) || floatValue < 0 || floatValue > 1) { chat(`Max light level must be between 0 and 1.`); return } else { state[scriptName].maxLightLevel = floatValue; chat(`Max light level has been set at ${floatValue}`); } } const checkLightLevelBelowMax = (tokenId) => { const litByObject = checkLightLevel.isLitBy(tokenId); return litByObject.total <= state[scriptName].maxLightLevel; } const handleGraphicChange = (graphic) => { if (graphic.get('subtype') === 'token') { const represents = graphic.get('represents'); if (represents && state[scriptName].characters.includes(represents)) { const isInvisible = checkLightLevelBelowMax(graphic.get('id')); const marker = state[scriptName].statusMarker ?? 'ninja-mask'; const currentStatuses = (graphic.get('statusmarkers') ?? "").split(/\s*,\s*/); let newStatuses = null; if (isInvisible) { if (!currentStatuses.includes(marker)) newStatuses = [...currentStatuses, marker]; } else { if (currentStatuses.includes(marker)) newStatuses = currentStatuses.filter(status => status !== marker); } if (newStatuses) graphic.set('statusmarkers', newStatuses.join(',')); } } } const handleChatInput = (message) => { if (playerIsGM(message.playerid) && rxScriptCommand.test(message.content)) { const commandLine = message.content.replace(/^!\w+\s+/, ''); const commands = commandLine.split(/\s*--\s*/); commands.forEach(command => { if (/^add/i.test(command)) { const characterId = (command.match(rxId) ?? [])[0]; if (characterId) addCharacter(characterId); else chat(`Please supply a valid character ID`); } else if (/^rem/i.test(command)) { const characterId = (command.match(rxId) ?? [])[0]; if (characterId) removeCharacter(characterId); else chat(`Please supply a valid character ID`); } else if (/^max/i.test(command)) { const floatValue = (command.match(/\d?\.?\d+/) ?? [])[0]; if (!floatValue) chat(`Please supply a value between 0 and 1, e.g. 0.15 for 15% illumination`); else setLightLevel(floatValue); } else if (/^mark/i.test(command)) { const marker = command.replace(/^mark[\w]*/, '').trim(); if (marker.length) state[scriptName].statusMarker = marker; chat (`Status marker set to "${marker}"`); } }); } } const initialize = () => { state[scriptName] = state[scriptName] ?? {}; state[scriptName].characters = state[scriptName].characters ?? []; state[scriptName].maxLightLevel = state[scriptName.maxLightLevel] ?? 0.15; state[scriptName].statusMarker = state[scriptName].statusMarker ?? 'ninja-mask'; if (typeof(checkLightLevel) == undefined) { chat(`${scriptName} requires the checkLightLevel script, but it was not found. The script is disabled.`); } else { on('change:graphic', handleGraphicChange); on('chat:message', handleChatInput); } } on('ready', initialize); })();