Timekeeper API !time-menu (Needs ChatSetAtter API) Tracks time in your game by minute, Turns, Hours, and Days. Can add custom timers that alert chat when expired (i.e. torches). var Timekeeper = Timekeeper || (function () { 'use strict'; var checkInstall = function () { state.Timekeeper = state.Timekeeper || { now: 0, timers: [] }; log('Timekeeper ready'); }; var pad2 = function (n) { n = parseInt(n, 10) || 0; return (n < 10 ? '0' : '') + n; }; var formatClock = function (totalMinutes) { totalMinutes = Math.max(0, parseInt(totalMinutes, 10) || 0); var day = Math.floor(totalMinutes / 1440), rem = totalMinutes % 1440, hour24 = Math.floor(rem / 60), minute = rem % 60, suffix = (hour24 >= 12 ? 'PM' : 'AM'), hour12 = hour24 % 12; if (hour12 === 0) { hour12 = 12; } return 'Day ' + day + ', ' + hour12 + ':' + pad2(minute) + ' ' + suffix; }; var unitToMinutes = function (amount, unit) { var a = parseInt(amount, 10), u = String(unit || 'min').toLowerCase(); if (isNaN(a)) return null; if (/^(m|min|mins|minute|minutes|round|rounds)$/.test(u)) return a; if (/^(turn|turns)$/.test(u)) return a * 10; if (/^(hour|hours|hr|hrs)$/.test(u)) return a * 60; if (/^(day|days)$/.test(u)) return a * 1440; return null; }; var purgeExpired = function () { var now = state.Timekeeper.now, active = [], expired = [], i, t; for (i = 0; i < state.Timekeeper.timers.length; i++) { t = state.Timekeeper.timers[i]; if (t.expiresAt <= now) { expired.push(t.label); } else { active.push(t); } } state.Timekeeper.timers = active; return expired; }; var showClock = function (changeText, expiredList) { var html = '', i, remaining; html += '<div style="border:1px solid #444;background:#fff;padding:6px 8px;">'; html += '<div style="font-weight:bold;font-size:1.1em;margin-bottom:4px;">Campaign Clock</div>'; html += '<div><b>Current:</b> ' + formatClock(state.Timekeeper.now) + '</div>'; if (changeText) { html += '<div><b>Change:</b> ' + changeText + '</div>'; } if (state.Timekeeper.timers.length) { html += '<div style="margin-top:4px;"><b>Active Timers:</b></div>'; for (i = 0; i < state.Timekeeper.timers.length; i++) { remaining = state.Timekeeper.timers[i].expiresAt - state.Timekeeper.now; html += '<div>- ' + state.Timekeeper.timers[i].label + ' (' + remaining + ' min remaining)</div>'; } } else { html += '<div style="margin-top:4px;"><b>Active Timers:</b> none</div>'; } if (expiredList && expiredList.length) { html += '<div style="margin-top:4px;color:#8a1c1c;"><b>Expired:</b> ' + expiredList.join(', ') + '</div>'; } html += '</div>'; sendChat('Timekeeper', '/direct ' + html); }; var showMenu = function () { var html = ''; html += '<div style="border:1px solid #444;background:#fff;padding:6px 8px;">'; html += '<div style="font-weight:bold;font-size:1.1em;margin-bottom:6px;">Timekeeper Menu</div>'; html += '<div>[Show Clock](!time-show)</div>'; html += '<div>[+1 Round](!time-advance 1 rounds)</div>'; html += '<div>[+1 Turn](!time-advance 1 turns)</div>'; html += '<div>[+1 Hour](!time-advance 1 hours)</div>'; html += '<div>[+1 Day](!time-advance 1 days)</div>'; html += '<div>[+Custom Time](!time-advance ?{Amount?|10} ?{Unit?|Minutes,min|Rounds,round|Turns,turn|Hours,hour|Days,day})</div>'; html += '<div>[Add Timer](!time-timer ?{Label?|Torch} | ?{Amount?|60} | ?{Unit?|Minutes,min|Rounds,round|Turns,turn|Hours,hour|Days,day})</div>'; html += '<div>[Clear Timer](!time-clear ?{Exact timer label?|Torch})</div>'; html += '<div>[Set Clock](!time-set ?{Day?|0} ?{Hour?|8} ?{Minute?|0})</div>'; html += '<div>[Reset Clock](!time-reset)</div>'; html += '</div>'; sendChat('Timekeeper', '/w gm ' + html); }; var handleInput = function (msg) { var parts, amount, unit, rest, timerBits, timerLabel, expired; if (msg.type !== 'api') return; if (msg.content === '!time-menu') { showMenu(); return; } if (msg.content === '!time-show') { showClock(); return; } if (!playerIsGM(msg.playerid)) return; if (msg.content.indexOf('!time-advance ') === 0) { parts = msg.content.split(/\s+/); amount = parts[1]; unit = parts[2]; amount = unitToMinutes(amount, unit); if (amount === null) { sendChat('Timekeeper', '/w gm Invalid time unit.'); return; } state.Timekeeper.now += amount; expired = purgeExpired(); showClock('Advanced ' + amount + ' minutes', expired); return; } if (msg.content.indexOf('!time-set ') === 0) { parts = msg.content.split(/\s+/); state.Timekeeper.now = (parseInt(parts[1], 10) * 1440) + (parseInt(parts[2], 10) * 60) + parseInt(parts[3], 10); expired = purgeExpired(); showClock('Clock set', expired); return; } if (msg.content.indexOf('!time-timer ') === 0) { rest = msg.content.replace('!time-timer ', ''); timerBits = rest.split(/\s*\|\s*/); if (timerBits.length < 3) { sendChat('Timekeeper', '/w gm Use: !time-timer Label | Amount | Unit'); return; } amount = unitToMinutes(timerBits[1], timerBits[2]); if (amount === null || amount <= 0) { sendChat('Timekeeper', '/w gm Invalid timer duration.'); return; } timerLabel = timerBits[0]; state.Timekeeper.timers.push({ label: timerLabel, expiresAt: state.Timekeeper.now + amount }); showClock('Added timer: ' + timerLabel); return; } if (msg.content.indexOf('!time-clear ') === 0) { timerLabel = msg.content.replace('!time-clear ', ''); state.Timekeeper.timers = state.Timekeeper.timers.filter(function (t) { return t.label !== timerLabel; }); showClock('Cleared timer: ' + timerLabel); return; } if (msg.content === '!time-reset') { state.Timekeeper.now = 0; state.Timekeeper.timers = []; showClock('Clock reset'); return; } }; var registerEventHandlers = function () { on('chat:message', handleInput); }; return { CheckInstall: checkInstall, RegisterEventHandlers: registerEventHandlers }; }()); on('ready', function () { 'use strict'; Timekeeper.CheckInstall(); Timekeeper.RegisterEventHandlers(); });