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 .
×
Create a free account

Gloomstalker script

Hi everyone! One of my players is playing a Gloomstalker, which is practically invisible when in complete darkness. To reflect that on Roll20 and to make it easy to know when he is invisible, I am looking for a script that adds the Invisible marker on the players token when he is not in any kind of light, and removes that token when he is in dim or bright light. I already have Tokenmod (to change/add markers) and CheckLightLevel (to check if the token is in light) installed, but I'm not good at writing the script itself. Hope someone can push me in the right direction! Thanks!
1699946608
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
It think it would take a custom script, but it sounds doable. It would use the  on("change:graphic", function(obj, prev)  event. Both CheckLightLevel and Token mod are exposed for other scripts to use. It would also need to check if the moved graphic represented the gloomstalker character. Given the way that light falls off in Roll20, I would give them the bonus in even 10-15% darkness. It's certainly possible. I'm pretty sure even I could write it, but I am a painfully slooooow coder.
1700282864

Edited 1700604961
Oosh
Sheet Author
API Scripter
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); })();
1700284855
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
It never ceases to amaze me how fast some of you scripters are!
Oosh, you are legend! Just tried this out with a custom marker and it works perfectly. I'm speechless. Thank you!
1700526703
David M.
Pro
API Scripter
Bookmarking this page (for nefarious GM purposes). Thanks, Oosh!
+1 David M. said: Bookmarking this page (for nefarious GM purposes). Thanks, Oosh!
OMG! Kurt, thanks for the idea. Super helpful feature for sure!! And Oosh... thanks for the "spare few minutes" to make this happen. Truly amazing! Is it possible to spend a "few more minutes" to make it so the script can track both darkness and dim light? I was thinking that I could have 2 simultaneous triggers, a 0.15 light level to indicate darkness and a 0.5 light level to indicate dim light... now my players always know what lighting they're in and they can act accordingly. I tried using 2 different levels with the current script and it only tracks the last one I enter in the command line!