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

[Script] Game Timer

1549141321
Sad
Sheet Author
API Scripter
This is a simple script to court how long does player use the game room. Just upload a clock picture on the table and name game.clock. Then the picture will court the time. API sandbox will shutdown soon when all player exit the room. Support for multiple clock at same time. Red Bar be hours. Green Bar be minutes. Blue Bar be seconds. Problem The timer is not accurate. It have some lag. Demo
1549148552
The Aaron
Roll20 Production Team
API Scripter
Can you link to the script?  It probably assumes the interval function will be called on precisely the requested interval.  Instead, it should base the rotation on the delta in time between calls.
1549156522
Sad
Sheet Author
API Scripter
<a href="https://gist.github.com/zeteticl/766ced94a1624f4397118be3ac673b64" rel="nofollow">https://gist.github.com/zeteticl/766ced94a1624f4397118be3ac673b64</a> Forgot to post the link....
1549161726

Edited 1549162143
The Aaron
Roll20 Production Team
API Scripter
As suspected, the reason it is inaccurate is the assumption it will get called every second on the second.&nbsp; It should instead base the time and rotation on the current time. Here's a first pass at a complete rewrite that should be identical in functionality, but tied to time, rather than when the update call happens.&nbsp; It's also drastically more efficient as it only does a single findObjs() per interval, rather than (n+1) where n is the number of objects, and builds a single change set per object, rather than several dozen individual .set() calls.&nbsp; There are probably some things that could be done to make it even more precise, but this is probably good enough for now... on("ready", function () { let lastTime = Date.now(); let lastSecond = Math.floor(lastTime/1000)*1000; let rotPerSec = 360/60; const updateSpeed = 250 const statusLookup = [ "red", "red,blue", "red,blue,green", "red,blue,green,brown", "red,blue,green,brown,purple", "red,blue,green,brown,purple,yellow", "red,blue,green,brown,purple", "red,blue,green,brown", "red,blue,green", "red,blue" ]; const doUpdates = () =&gt; { let time = Date.now(); let deltaT = time-lastTime; let deltaS = time-lastSecond; lastTime=time; findObjs({ type: 'graphic', name: 'game.clock' }, {caseInsensitive: true}) .forEach( (obj) =&gt; { let c={ bar3_value: obj.get('bar3_value')||"0", bar1_value: obj.get('bar1_value')||"0", bar2_value: obj.get('bar2_value')||"0", gmnotes: obj.get('gmnotes')||"1", rotation: (((parseInt(obj.get('rotation'))+( rotPerSec*(deltaT/1000) ) )%360+360)%360), }; if(deltaS&gt;1000){ let gmn = parseInt(c.gmnotes)-1; c.statusmarkers = (statusLookup[gmn]||''); c.gmnotes = `${((gmn+1)%10)+1}`; lastSecond+=1000; let h= parseInt(c.bar3_value); let m= parseInt(c.bar1_value); let s= parseInt(c.bar2_value); s+=1; m+=(s&gt;=60?1:0); s%=60; h+=(m&gt;=60?1:0); m%=60; c.bar3_value=`${h}`; c.bar1_value=`${m}`; c.bar2_value=`${s}`; } obj.set(c); }); } setInterval( doUpdates, updateSpeed ); }); Note that you may occasionally see it lag behind, but then catch up.&nbsp; That happens when the API gets delayed by something.&nbsp; Being based on actual time, it will get caught back up to where it should be in a few steps.
1549170049
Sad
Sheet Author
API Scripter
The Aaron said: As suspected, the reason it is inaccurate is the assumption it will get called every second on the second.&nbsp; It should instead base the time and rotation on the current time. Here's a first pass at a complete rewrite that should be identical in functionality, but tied to time, rather than when the update call happens.&nbsp; It's also drastically more efficient as it only does a single findObjs() per interval, rather than (n+1) where n is the number of objects, and builds a single change set per object, rather than several dozen individual .set() calls.&nbsp; There are probably some things that could be done to make it even more precise, but this is probably good enough for now... on("ready", function () { let lastTime = Date.now(); let lastSecond = Math.floor(lastTime/1000)*1000; let rotPerSec = 360/60; const updateSpeed = 250 const statusLookup = [ "red", "red,blue", "red,blue,green", "red,blue,green,brown", "red,blue,green,brown,purple", "red,blue,green,brown,purple,yellow", "red,blue,green,brown,purple", "red,blue,green,brown", "red,blue,green", "red,blue" ]; const doUpdates = () =&gt; { let time = Date.now(); let deltaT = time-lastTime; let deltaS = time-lastSecond; lastTime=time; findObjs({ type: 'graphic', name: 'game.clock' }, {caseInsensitive: true}) .forEach( (obj) =&gt; { let c={ bar3_value: obj.get('bar3_value')||"0", bar1_value: obj.get('bar1_value')||"0", bar2_value: obj.get('bar2_value')||"0", gmnotes: obj.get('gmnotes')||"1", rotation: (((parseInt(obj.get('rotation'))+( rotPerSec*(deltaT/1000) ) )%360+360)%360), }; if(deltaS&gt;1000){ let gmn = parseInt(c.gmnotes)-1; c.statusmarkers = (statusLookup[gmn]||''); c.gmnotes = `${((gmn+1)%10)+1}`; lastSecond+=1000; let h= parseInt(c.bar3_value); let m= parseInt(c.bar1_value); let s= parseInt(c.bar2_value); s+=1; m+=(s&gt;=60?1:0); s%=60; h+=(m&gt;=60?1:0); m%=60; c.bar3_value=`${h}`; c.bar1_value=`${m}`; c.bar2_value=`${s}`; } obj.set(c); }); } setInterval( doUpdates, updateSpeed ); }); Note that you may occasionally see it lag behind, but then catch up.&nbsp; That happens when the API gets delayed by something.&nbsp; Being based on actual time, it will get caught back up to where it should be in a few steps. Hi, thankyou for rewrite the code. It is amazing. I have tried but there is a problem. Rotation is slow. Make a turn need about 90 seconds.
1549174552
GiGs
Pro
Sheet Author
API Scripter
Try tweaking this value near the top of the screipt: let rotPerSec = 360/60; Change it to&nbsp; let rotPerSec = 10; or something bigger, tweak it till you get the desired speed.
1549187393
Sad
Sheet Author
API Scripter
GiGs said: Try tweaking this value near the top of the screipt: let rotPerSec = 360/60; Change it to&nbsp; let rotPerSec = 10; or something bigger, tweak it till you get the desired speed. But why the&nbsp;rotation get wrong. Just want to know reason.@@
1549188115
The Aaron
Roll20 Production Team
API Scripter
Actually, try setting the updateSpeed to 500.&nbsp; What is probably happening is multiple updates occurring before one is applied, causing roughly the same rotation to be applied. I was afraid this might happen. &nbsp;I have an idea how better to fix it (when it’s not the middle of the night and I’m not on my phone), but that should make it better.&nbsp;
1549215482

Edited 1549216024
The Aaron
Roll20 Production Team
API Scripter
Ok, give this one a try: on("ready", function () { let lastTime = Date.now(); let lastSecond = Math.floor(lastTime/1000)*1000; let rotPerSec = 360/60; const updateSpeed = 250; const statusLookup = [ "red", "red,blue", "red,blue,green", "red,blue,green,brown", "red,blue,green,brown,purple", "red,blue,green,brown,purple,yellow", "red,blue,green,brown,purple", "red,blue,green,brown", "red,blue,green", "red,blue" ]; const doUpdates = () =&gt; { let nextSecond=false; let time = Date.now(); let deltaS = time-lastSecond; lastTime=time; findObjs({ type: 'graphic', name: 'game.clock' }, {caseInsensitive: true}) .forEach( (obj) =&gt; { let c={ bar3_value: obj.get('bar3_value')||"0", bar1_value: obj.get('bar1_value')||"0", bar2_value: obj.get('bar2_value')||"0", rotation: ((((parseInt(obj.get('bar2_value'))||0)*rotPerSec)+(rotPerSec*(deltaS/1000))%360+360)%360) }; if(deltaS&gt;1000){ let h= parseInt(c.bar3_value); let m= parseInt(c.bar1_value); let s= parseInt(c.bar2_value); s+=1; m+=(s&gt;=60?1:0); s%=60; h+=(m&gt;=60?1:0); m%=60; c.bar3_value=`${h}`; c.bar1_value=`${m}`; c.bar2_value=`${s}`; let phase = (s%(statusLookup.length)); c.statusmarkers = statusLookup[phase]; nextSecond=true; } obj.set(c); }); if(nextSecond){ lastSecond+=1000; } }; setInterval( doUpdates, updateSpeed ); }); It now bases the rotation on the seconds as well as a change in time, which removes the reliance on previous rotations being calculated correctly as it always explicitly calculates it. It also removes the use of gmnotes for calculating the phase of the status markers, so they will now align to 10 second intervals on the seconds. Edit: Corrected a bug with multiple game.clock instances
1549254992

Edited 1549255112
Sad
Sheet Author
API Scripter
on("ready", function () { let lastTime = Date.now(); let lastSecond = Math.floor(lastTime / 1000) * 1000; let rotPerSec = 360 / 60; const updateSpeed = 250; const statusLookup = [ "red", "red,blue", "red,blue,green", "red,blue,green,brown", "red,blue,green,brown,purple", "red,blue,green,brown,purple,yellow", "red,blue,green,brown,purple", "red,blue,green,brown", "red,blue,green", "red,blue" ]; const doUpdates = () =&gt; { let nextSecond = false; let time = Date.now(); let deltaS = time - lastSecond; lastTime = time; findObjs({ type: 'graphic', gmnotes: 'game.clock' }, { caseInsensitive: true }) .forEach((obj) =&gt; { let c = { bar3_value: obj.get('bar3_value') || "0", bar1_value: obj.get('bar1_value') || "0", bar2_value: obj.get('bar2_value') || "0", rotation: ((((parseInt(obj.get('bar2_value')) || 0) * rotPerSec) + (rotPerSec * (deltaS / 1000)) % 360 + 360) % 360) + (parseInt(obj.get('light_losangle')) || 0) }; if (deltaS &gt; 1000) { let h = parseInt(c.bar3_value); let m = parseInt(c.bar1_value); let s = parseInt(c.bar2_value); s += 1; m += (s &gt;= 60 ? 1 : 0); s %= 60; h += (m &gt;= 60 ? 1 : 0); m %= 60; c.bar3_value = `${h}`; //c.bar1_value = `${m}`; //c.bar2_value = `${s}`; c.bar1_value = (`${m}` &lt; 10) ? '0' + `${m}` : `${m}`; c.bar2_value = (`${s}` &lt; 10) ? '0' + `${s}` : `${s}`; c.name = c.bar3_value + ':' + c.bar1_value + ':' + c.bar2_value; let phase = (s % (statusLookup.length)); c.statusmarkers = statusLookup[phase]; nextSecond = true; } obj.set(c); }); if (nextSecond) { lastSecond += 1000; } }; setInterval( doUpdates, updateSpeed ); }); Thankyou Aaron. You are so nice. You make my idea be true. I have edit some code. 1) keyword "game.clock" move to gmnotes. So name can show the time now. 2) 0,1,2...9 changed to 00 01 02 03. 3)&nbsp;rotation add a&nbsp;variable. And final question. How can i make "game.clock" with other word? For example, game.clock_1_1_3_4 So i can make some function on and off.
1549292664
The Aaron
Roll20 Production Team
API Scripter
I would probably start storing seconds in one bar, and use the value in another as on/off. I’ll make a version tonight that works that way.&nbsp;
1549328762
The Aaron
Roll20 Production Team
API Scripter
Here, give this a go: on("ready", function () { let lastTime = Date.now(); let lastSecond = Math.floor(lastTime/1000)*1000; let rotPerSec = 360/60; const updateSpeed = 250; const statusLookup = [ "red", "red,blue", "red,blue,green", "red,blue,green,brown", "red,blue,green,brown,purple", "red,blue,green,brown,purple,yellow", "red,blue,green,brown,purple", "red,blue,green,brown", "red,blue,green", "red,blue" ]; const doUpdates = () =&gt; { let time = Date.now(); let deltaS = time-lastSecond; lastTime=time; findObjs({ type: 'graphic', bar1_value: 'game.clock' }, {caseInsensitive: true}) .filter( (obj) =&gt; '0'!==obj.get('bar2_value')) .forEach( (obj) =&gt; { let c={ bar3_value: obj.get('bar3_value')||"0", bar2_value: obj.get('bar2_value')||"1", name: obj.get('name')||'00:00:00', showname: true, rotation: (((((parseInt(obj.get('bar3_value'))||0)%60)*rotPerSec)+(rotPerSec*(deltaS/1000))%360+360)%360) }; if(deltaS&gt;1000){ let sec=parseInt(c.bar3_value); ++sec; let h=Math.floor(sec/3600); let m=Math.floor((sec-(h*60))/60); let s=Math.floor(sec%60); c.bar3_value=`${sec}`; let phase = (sec%(statusLookup.length)); c.statusmarkers = statusLookup[phase]; c.name = `${`00${h}`.slice(-2)}:${`00${m}`.slice(-2)}:${`00${s}`.slice(-2)}`; } obj.set(c); }); if(deltaS&gt;1000){ lastSecond+=1000; } }; setInterval( doUpdates, updateSpeed ); on('chat:message', (msg)=&gt;{ if('api'===msg.type &amp;&amp; /^!game.clock\b/i.test(msg.content)){ let cmd = msg.content.toLowerCase().split(/\s+/); let create = cmd.includes('--create'); let toggle = cmd.includes('--toggle'); let reset = cmd.includes('--reset'); (msg.selected||[]) .map((o)=&gt;getObj('graphic', o._id)) .filter(o =&gt; undefined !== o) .filter((o) =&gt; create || 'game.clock'===o.get('bar1_value')) .forEach((o) =&gt; { let c = {}; if(create){ c.bar1_value = 'game.clock'; c.bar3_value = 0; c.bar2_value = 1; c.rotation = 0; c.statusmarkers = ''; c.name='00:00:00'; } else { if(reset){ c.bar3_value = 0; c.rotation = 0; c.statusmarkers = ''; c.name='00:00:00'; } if(toggle){ c.bar2_value = ('1'===o.get('bar2_value'))?'0':'1'; } } o.set(c); }); } }); }); I moved seconds into just bar3_value, will continue to count up ad infinitum. Moved "game.clock" into bar1_value.&nbsp; Typing "game.clock" into GM Notes actually results in a bunch of HTML codes, which makes it hard to match.&nbsp; Much easier in bar1. Made bar2_value a toggle for pausing the clock.&nbsp; If it is 0, it won't run, anything else and it will. I also added commands to create, toggle, and reset.&nbsp; These all work on selected token(s). !game.clock --create !game.clock --toggle !game.clock --reset --create will turn a token into a running game clock, it will also reset all the properties on an existing game clock. --toggle will pause and unpause selected game clocks. --reset will reset selected game.clocks to 0 seconds, no rotation, no statusmarkers, and a name of '00:00:00' You can also specify --toggle and --reset together: !game.clock --toggle --reset
1549482377
Sad
Sheet Author
API Scripter
Thankyou Aaron!!!
1549493567
The Aaron
Roll20 Production Team
API Scripter
No problem! =D
1549608350

Edited 1549617115
Sad
Sheet Author
API Scripter
on("ready", function () { let lastTime = Date.now(); let lastSecond = Math.floor(lastTime / 1000) * 1000; let rotPerSec = 360 / 60; const updateSpeed = 250; let bar2Splitor = /^(\w|)(\w|)(\w|)(\w|)([.]|)(\w+|)/i; let bar2numSplitor = [/[-][-](\d+)/i]; let bar2num2Splitor = /[.](\d+)/i; let bar1max2Splitor = [/[:]([\s\S]+)/i]; const statusLookup = [ "red", "red,blue", "red,blue,green", "red,blue,green,brown", "red,blue,green,brown,purple", "red,blue,green,brown,purple,yellow", "red,blue,green,brown,purple", "red,blue,green,brown", "red,blue,green", "red,blue" ]; const aura1_colorLookup = [ "#ff0000", "#ffff00", "#00ff00", "#0000ff", "#ff00ff" ]; const aura1_radiusLookup = [ "0.7", "0.8", "0.9", "1", "1.1" ]; const doUpdates = () =&gt; { let time = Date.now(); let deltaS = time - lastSecond; lastTime = time; findObjs({ type: 'graphic', bar1_value: 'game.clock' }, { caseInsensitive: true }) .forEach((obj) =&gt; { let key = bar2Splitor.exec(obj.get('bar2_value')); if (key.indexOf("o") === -1) { let c = { bar3_value: obj.get('bar3_value') || "0", bar2_value: obj.get('bar2_value') || "", name: obj.get('name') || '00:00:00', showname: true }; let sec = parseInt(c.bar3_value); if (key.indexOf("r") === -1) { c.rotation = (((((parseInt(obj.get('bar3_value')) || 0) % 60) * rotPerSec) + (rotPerSec * (deltaS / 1000)) % 360 + 360) % 360) + parseInt(key[6] || 0) } else { c.rotation = parseInt(key[6] || 0); } if (key.indexOf("s") === -1) { let phase = (sec % (statusLookup.length)); c.statusmarkers = statusLookup[phase]; } else { c.statusmarkers = "" } if (key.indexOf("a") === -1) { let phase = (sec % (aura1_colorLookup.length)); c.aura1_color = aura1_colorLookup[phase]; c.aura1_radius = aura1_radiusLookup[phase]; } else { c.aura1_radius = "" } if (deltaS &gt; 1000) { ++sec; let h = Math.floor(sec / 3600); let m = Math.floor((sec - (h * 60)) / 60); let s = Math.floor(sec % 60); c.bar3_value = `${sec}`; c.name = obj.get('bar1_max') + `${`00${h}`.slice(-2)}:${`00${m}`.slice(-2)}:${`00${s}`.slice(-2)}`; } obj.set(c); }; }); if (deltaS &gt; 1000) { lastSecond += 1000; } }; setInterval( doUpdates, updateSpeed ); on('chat:message', (msg) =&gt; { if ('api' === msg.type &amp;&amp; /^!game.clock\b/i.test(msg.content)) { let cmd = msg.content.toLowerCase().split(/\s+/); let create = cmd.includes('--create') || cmd.includes('-c'); let toggle = cmd.includes('--toggle') || cmd.includes('-t'); let reset = cmd.includes('--reset') || cmd.includes('-re'); let rotation = cmd.includes('--rotation') || cmd.includes('-ro'); let marker = cmd.includes('--marker') || cmd.includes('-m'); let aura = cmd.includes('--aura') || cmd.includes('-a'); let angle = bar2numSplitor.some(check =&gt; check.test(cmd)); let name = bar1max2Splitor.some(check =&gt; check.test(cmd)); (msg.selected || []) .map((o) =&gt; getObj('graphic', o._id)) .filter(o =&gt; undefined !== o) .filter((o) =&gt; create || 'game.clock' === o.get('bar1_value')) .forEach((o) =&gt; { let c = {}; c.bar2_value = o.get('bar2_value'); if (create) { c.bar1_value = 'game.clock'; c.showname = true; //c.bar2_value = 1; //c.rotation = 0; //c.statusmarkers = ''; //c.name = '00:00:00'; } if (reset) { c.bar3_value = 0; c.bar2_value = ""; c.rotation = 0; c.statusmarkers = ''; c.name = '00:00:00'; } if (toggle) { c.bar2_value = (c.bar2_value.indexOf("o") === -1 ? 'o' + c.bar2_value : c.bar2_value.replace("o", "")); } if (rotation) { c.bar2_value = (c.bar2_value.indexOf("r") === -1 ? 'r' + c.bar2_value : c.bar2_value.replace("r", "")); } if (marker) { c.bar2_value = (c.bar2_value.indexOf("s") === -1 ? 's' + c.bar2_value : c.bar2_value.replace("s", "")); } if (aura) { c.bar2_value = (c.bar2_value.indexOf("a") === -1 ? 'a' + c.bar2_value : c.bar2_value.replace("a", "")); } if (angle) { c.bar2_value = (c.bar2_value.match(/[.](\d+)/i) ? (c.bar2_value.replace(bar2num2Splitor, "." + msg.content.match(/[-][-](\d+)/i)[1])) : c.bar2_value + "." + msg.content.match(/[-][-](\d+)/i)[1]); } if (name) { c.bar1_max = (msg.content.match(/[:]([\s\S]+)/i)[1]); } o.set(c); }); } }); }); Some idea. Create Game Clock !game.clock --create -c Stop the clock --toggle -t Reset the Clock --reset -re Turn on/off rotation --rotation -ro Turn on/off status markers --marker -m Turn on/off aura --aura -a Adjust angle --(number) like --90 Add word before clock :(word) like :StartFrom2019
Ok, this script crashes the game's API sandbox and returns the result of: An error was encountered Don't worry; it's not like you triggered the end of the world...right? If you message us about the error, please give us the error ID below so we can look it up: &nbsp; fc0a637d5e0b4da5b2fe9232a09e5112 I cannot access the API sandbox for this game anymore...not sure how to resolve, but why is this in the library if it's not fully functional?
other readers: &nbsp;this conversation continues here:&nbsp;<a href="https://app.roll20.net/forum/post/7189837/cannot-view-api-script-page" rel="nofollow">https://app.roll20.net/forum/post/7189837/cannot-view-api-script-page</a>