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

[Help][Script] Pulsing light for puzzle

Hi! My players have gotten a quest to go to an puzzle dungeon and complete it. In the first puzzle room, the puzzle is focused around four crystals that pulses with light. I've tried to write a script for it, but I have no clue what I'm doing. So, I'm hoping someone here knows how to do it and might be able to give me some pointers. Essentially, the lights need to pulse on and off in three stages. Each stage lasts for 20 seconds. Stage 1: Green color (#00ff00), 2 seconds on, 2 seconds off. Stage 2: Yellow color (#ffff00), 1 second on, 1 second off. Stage 3: Red color (#ff0000), .5 second on, .5 second off. Additionally, there's a button in the room that restarts the cycle. Does anyone know how something like this can be made with a script? Or if it's even possible? Thanks in advance, - Jakob F.
Could you create a light token with the appropriate settings for brightness and color already assigned, then have a script that moves the token from the gm layer to the light layer on a timer?  I don’t currently have a way to verify if the setInterval function works in roll20, though there are a few timer scripts so I assume that it does. You’d just need a way to identify the token to switch layers and then do the switch. if you use pre-existing tokens on the map it should get around any of the remaining bugs regarding what tokens can see in dynamic lighting , something like tokenMod might be able to do it too, though I don’t know about the light color, you might have to set that manually 
1662830544

Edited 1662872269
Oosh
Sheet Author
API Scripter
I'm not sure if this is quite what you want, but you can give it a go. It cycles between the original light color of the token, and whatever you specify for that stage. If you want the light to be completely off (and not cycling to another color) in between flashes I can add that in instead. Update - The script will pulse the light off to whatever the original state of each token in the selection was. So if it was OFF, the light will pulse off each time. If it was a different color, the 'off' state will flick back to that color. ISSUE : While a token has its low and bright light switched off, it doesn't seem to be possible to find the total light radius for that token. While the bright_light_distance is available, low_light_distance is set to 0 (this attribute would be less ambiguous as total_light_distance). The low light distance itself is stored in an attribute called light_ui (really horrid name for an attribute) which is not exposed to the sandbox. Go figure. WORKAROUND: If you want a light to pulse to total darkness (emits no bright or low light), it will default to having the total light radius set to the bright light radius, as no other radius is available from the token data. If you want low light emitting around the bright light, specify the low light radius (only low light, not combined) in the token's tooltip with the format low<radius>. Example: for a light which emits 10 feet of bright light, and a further 5 of dim, but is completely off between pulses: - set bright light distance to 10 in token settings - add the text low5 to the tooltip text (can occur anywhere in the tooltip, with or without other text) - turn the bright light & low light switches off - run the script Usage is !lightpulse [ --sel | --ids <token_ids> ]  [ --stageX <color> <timings> |  --break ] --ids is a comma-delimited list of token ids --sel will use the current selection of tokens instead of specifying with --ids --stageX is the stage number and settings - a color and 3 times in seconds: on, off, total. So stage1 above would be --stage1 #00ff00 2,2,20 --break can be used to stop a light animation in progress, this also needs the --id argument So for your example above, set up your token with the right radius & white or transparent light, then run: !lightpulse --id @{selected|token_id} --stage1 #00ff00 2,2,20 --stage2 #ffff00 1,1,20 --stage3 #ff0000 0.5,0.5,20 And the script: /* globals on, sendChat, getObj, */ // !lightpulse --sel --stage1 #00ff00 2,2,10 --stage2 #ff00ff 1,1,10 on('ready', () => { const scriptName = 'pulseLights'; const _activeLights = {}; const breakLight = ({ tokens }) => { tokens.forEach(token => { if (_activeLights[token.id]) _activeLights[token.id].interrupt = true }); } const cleanUpLights = (tokens) => { tokens = toArray(tokens); tokens.forEach(token => { if (_activeLights[token.id]) { setLightColor(_activeLights[token.id].color); delete _activeLights[token.id]; } }); } const checkInterrupts = (tokens) => { let lightsActive = 0; for (let i=tokens.length-1; i>=0; i--) { if (_activeLights[tokens[i].id] && _activeLights[tokens[i].id].interrupt) { // console.warn(`Interrupting light ${tokens[i].id}`); cleanUpLights(tokens[i]) tokens.splice(i, 1); } else { lightsActive ++; } } return lightsActive; } const startLightPulse = async ({ tokens, stages }) => { stages = stages.filter(v=>v); if (!tokens || !stages || !stages.length) return console.warn(`Bad data supplied to pulseLight`); tokens.forEach(token => { if (_activeLights[token.id]) cleanUpLights(token); const { color, lowLight, brightLight, lightRadius } = getTokenOriginals(token); _activeLights[token.id] = { interrupt: false, color, lowLight, brightLight, lightRadius }; }); // console.log(`Starting pulse with ${stages.length} stages...`); for (let i=0; i<stages.length; i++) { let lightsActive = 0; const stageStart = Date.now(); do { await pulseLight(tokens, stages[i]); lightsActive = checkInterrupts(tokens); } while ((stages[i].time.total > (Date.now() - stageStart)) && lightsActive); if (!lightsActive) break; } // console.log('Pulsing done'); cleanUpLights(tokens); } const pulseLight = async (tokens, { color, time }) => { // console.log('pulsing on', time, color); tokens.forEach(token => setLightColor(token, color)); await timeout(time.on); // console.log('pulsing off'); tokens.forEach(token => setLightColor(token, 'off')); await timeout(time.off); } const setLightColor = async (token, color) => { if (!_activeLights[token.id] || !color) return; const lightSettings = {}; if (color === 'off') { Object.assign(lightSettings, { lightColor: _activeLights[token.id].color, emits_low_light: _activeLights[token.id].lowLight, emits_bright_light: _activeLights[token.id].brightLight }); } else Object.assign(lightSettings, { lightColor: color, emits_low_light: true, emits_bright_light: true, low_light_distance: _activeLights[token.id].lightRadius }); // console.log(lightSettings); token.set(lightSettings); } const getTokenOriginals = (token) => { if (!token || !token.id) return; const settings = { color: token.get('lightColor'), lowLight: token.get('emits_low_light'), brightLight: token.get('emits_bright_light'), } const brightLight = token.get('bright_light_distance'), lowLightString = ((token.get('tooltip')||``).match(/low(\d+)/i)||[])[1], lowLight = token.get('low_light_distance'); settings.lightRadius = Math.max(brightLight + (parseInt(lowLightString)||0), lowLight); // console.log(settings); return settings; } const getSelected = (selection = []) => { return (selection||[]).reduce((out, sel) => { const tok = getObj('graphic', sel._id); return tok ? [ ...out, tok ] : out; }, []); } const timeout = async (ms) => new Promise(res => setTimeout(() => res(), ms)); const toChat = (msg, who) => { const whisper = who ? `/w "${who}" ` : ''; sendChat(scriptName, `${whisper}${msg}`, null, { noarchive: true }); } const toArray = (input) => Array.isArray(input) ? input : [ input ]; on('chat:message', (msg) => { if (msg.type === 'api' && /^!lightpulse\s/i.test(msg.content)) { // if (!playerIsGM(msg.playerid)) return; const commandLine = msg.content.match(/^!lightpulse\s+(.*)/)[1]; if (!commandLine) return; const commands = { tokens: null, stages: [], break: null }; commandLine.split(/\s*--\s*/g).forEach(cmd => { const parts = cmd.match(/([0-z]+)\b\s*(.*)/); // console.log(parts); if (!parts || !parts[1]) return; if (/^id/i.test(parts[1])) { if (!parts[2]) return; const ids = parts[2].split(/\s*,\s*/g); const tokens = ids.reduce((out, id) => { const tok = getObj('graphic', id); return tok ? [ ...out, tok ] : out; }, []); commands.tokens = tokens; } else if (/^sel/i.test(parts[1])) { commands.tokens = getSelected(msg.selected); } else if (/^stage\d/i.test(parts[1])) { if (!parts[2]) return; const stageColor = (parts[2].match(/#[0-9A-Fa-f]{6}/)||[])[0], stageTimerString = (parts[2].match(/[\d.]+,\s*[\d.]+,\s*[\d.]+/)||[])[0], stageTimers = stageTimerString ? stageTimerString.split(/\s*,\s*/g) : null, stageIndex = parts[1].replace(/\D/g, ''); if (stageColor && stageTimers) { commands.stages[stageIndex] = ({ color: stageColor, time: { on: stageTimers[0]*1000, off: stageTimers[1]*1000, total: stageTimers[2]*1000 } }); } } else if (/^break/i.test(parts[1])) commands.break = true; }); // console.info(commands); if (!commands.tokens.length) return toChat(`No valid tokens found.`, 'gm'); if (!commands.stages.length && !commands.break) return toChat(`No valid stage or break command`, 'gm'); if (commands.break) breakLight(commands); else startLightPulse(commands); } }); });
1662842378

Edited 1662845980
Holy heck Oosh! That's exactly what I had in mind! You're a godsend! Thank you so much for this. Can't express how much it means! My players are gonna be gobsmacked thanks to you :D Is it possible for the players to start the script? And, as for the token ID's, does it work if the command reads as below, specifying each token ID so they  dont  have to be in a separate !lightpulse command? :) !lightpulse --id -NBd12jg50h637bCICvy -NBccJc3PxrQQcuLKWhu -NBd12GY8dGim92Z5viM -NBd12X6AFBuXZv1Vspd --stage1 #00ff00 2,2,20 --stage2 #ffff00 1,1,20 --stage3 #ff0000 0.5,0.5,20 Oosh said: I'm not sure if this is quite what you want, but you can give it a go. It cycles between the original light color of the token, and whatever you specify for that stage. If you want the light to be completely off (and not cycling to another color) in between flashes I can add that in instead. That'd be great. I'd love for my players to panic a bit ;) Edit: Thinking about it, would it be possible to add it as a variable? I can see multiple uses for a script like this in the future, both where it kills the light, and switches between the original setting and another color. I also noticed that the tokens can only be affected by the script once. After it's run it's course, nothing happens if I try to run the script again. Edit: Even if I use --break to stop it mid cycle, the token can't be affected by the script again. If I put in a new lightsource token, that one can be affected, but also only once. I don't know if you're free to fix these things Oosh, and I don't want to abuse your generosity, so just tell me if some of the things are too much of an ask. I am eternally in your debt either way!
1662870151

Edited 1662870236
Oosh
Sheet Author
API Scripter
Right, I think I've got all of that in the script. The only issue is with calculating the light distance when switching a light on - if the token had all emission turned off before, it's not possible to calculate the original light radius as per the issue & workaround posted above with the script. The GM check is on its own line and commented out for now - if you ever need it back on it's a few lines in to the last block, just remove the //  comment slashes from the playerIsGM() line. Multiple tokens can be passed in separated with commas, or you can just use --sel to grab the currently selected token(s). All tokens will pulse off to whatever state they were in when the script started - whether that's a different color, or not emitting any light at all. You can run --break for multiple tokens, or just one at a time, to stop the animation.
any chance we can get a gif or vid of api in action for funsies
1662872100

Edited 1662872160
Oosh
Sheet Author
API Scripter
Novercalis said: any chance we can get a gif or vid of api in action for funsies I'm not actively using Roll20 for games any more so I don't have anything interesting to share. But lights with different base settings go flashy flash: Maybe Jakob wants to share their creation when they get the lights working though? I'm sure it looks a whole lot better than my blank map with an orc dragged on.
1662897206

Edited 1662898038
Thank you&nbsp; Oost ! It works exactly how I'd dreamed of. Really appreciate it :D Here's the gif Novercalis, but it's quite long to see it from start to end (just over a minute). The size is too big to post here for the full script without having to manually cut over half the frames, so I've put it on Imgur. It can be found here:&nbsp; <a href="https://imgur.com/a/OV1ItAJ" rel="nofollow">https://imgur.com/a/OV1ItAJ</a> &nbsp; Edit: It's far more laggy on Imgur than it is on Roll20. T here's barely any stutters on Roll20, but there's a lot when viewed on Imgur.
1662907378
Oosh
Sheet Author
API Scripter
No problem - let me know if there are any issues.