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 .
×

Roll20 campaign XP manager for 1e AD&D.

It lets the GM or players track encounter XP, treasure XP, magic item XP, and sale XP , then automatically divide and award the final XP to selected party members by writing it directly to the character sheet’s XP&nbsp;attribute. For the old-schoolers. Could possibly be used for other editions.&nbsp; It's a menu-driven XP bookkeeping tool built for 1st Edition AD&amp;D style advancement . It is designed to handle the parts of XP tracking that are annoying to do by hand during a campaign. It's all based on the rules from the DMG and Players Handbook. It allows you to: Set the party, survivors, henchmen, and hirelings from selected tokens Add monster XP from a built-in monster database Record recovered coins and treasure separately from treasure that has actually been turned in for XP Turn in coins with money-changer fees applied Add magic items from a built-in magic item database Track magic items found, turned in, or sold later Apply merchant fees when magic items are sold Add custom XP awards or single-character awards Total all shared XP for the session Divide XP among survivors, henchmen, and hirelings Write the awarded XP directly onto character sheets Command: !xp-menu <a href="https://github.com/earmarkaudiology-png/Roll20-campaign-XP-manager-for-1e-AD-D" rel="nofollow">https://github.com/earmarkaudiology-png/Roll20-campaign-XP-manager-for-1e-AD-D</a>.
<a href="https://github.com/earmarkaudiology-png/Roll20-campaign-XP-manager-for-1e-AD-D./blob/main/adnd1e-xp.js" rel="nofollow">https://github.com/earmarkaudiology-png/Roll20-campaign-XP-manager-for-1e-AD-D./blob/main/adnd1e-xp.js</a>
1773972469
vÍnce
Pro
Sheet Author
Who tracks XP? What is this AD&amp;D 1e?&nbsp; lol&nbsp;&nbsp; I'm would like to add your macros and your XP Manager Mod to the AD&amp;D 1e sheet's wiki if you don't mind Rollo? Thank you for sharing.
Sounds good.. Been a 5e player a while and since 2024 came out been wanting to go old school. I have a ton more macros I’ll be posting but not sure the interest though&nbsp;
Timekeeper&nbsp; I put together a simple campaign time tracker for Roll20 that keeps one shared in-game clock and lets the group track timed effects directly in chat. This script tracks: Rounds as 1 minute. Turns as 10 minutes, Hours, Days Whenever time is advanced, the script posts the updated campaign clock in chat for everyone to see. It also supports custom timers for things like: torches lanterns spell durations wandering monster checks rests travel effects any other timed event Each player-created timer is labeled with the player’s display name, so the output is easy to follow in chat. Commands: !time-menu !time-show !time-clear-menu !time-advance AMOUNT UNIT !time-set DAY HOUR MINUTE !time-timer ITEM | AMOUNT | UNIT !time-reset <a href="https://github.com/earmarkaudiology-png/Timekeeper/blob/main/Timekeeper.txt" rel="nofollow">https://github.com/earmarkaudiology-png/Timekeeper/blob/main/Timekeeper.txt</a> var Timekeeper = Timekeeper || (function () { &nbsp; &nbsp; 'use strict'; &nbsp; &nbsp; var checkInstall = function () { &nbsp; &nbsp; &nbsp; &nbsp; var i; &nbsp; &nbsp; &nbsp; &nbsp; state.Timekeeper = state.Timekeeper || { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; now: 0, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; timers: [], &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; nextId: 1 &nbsp; &nbsp; &nbsp; &nbsp; }; &nbsp; &nbsp; &nbsp; &nbsp; if (typeof state.Timekeeper.nextId !== 'number' || state.Timekeeper.nextId &lt; 1) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state.Timekeeper.nextId = 1; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; for (i = 0; i &lt; state.Timekeeper.timers.length; i++) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (typeof state.Timekeeper.timers[i].id !== 'number') { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state.Timekeeper.timers[i].id = state.Timekeeper.nextId++; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } else if (state.Timekeeper.timers[i].id &gt;= state.Timekeeper.nextId) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state.Timekeeper.nextId = state.Timekeeper.timers[i].id + 1; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; log('Timekeeper ready'); &nbsp; &nbsp; }; &nbsp; &nbsp; var pad2 = function (n) { &nbsp; &nbsp; &nbsp; &nbsp; n = parseInt(n, 10) || 0; &nbsp; &nbsp; &nbsp; &nbsp; return (n &lt; 10 ? '0' : '') + n; &nbsp; &nbsp; }; &nbsp; &nbsp; var formatClock = function (totalMinutes) { &nbsp; &nbsp; &nbsp; &nbsp; totalMinutes = Math.max(0, parseInt(totalMinutes, 10) || 0); &nbsp; &nbsp; &nbsp; &nbsp; var day = Math.floor(totalMinutes / 1440), &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rem = totalMinutes % 1440, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hour24 = Math.floor(rem / 60), &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; minute = rem % 60, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; suffix = (hour24 &gt;= 12 ? 'PM' : 'AM'), &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hour12 = hour24 % 12; &nbsp; &nbsp; &nbsp; &nbsp; if (hour12 === 0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hour12 = 12; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; return 'Day ' + day + ', ' + hour12 + ':' + pad2(minute) + ' ' + suffix; &nbsp; &nbsp; }; &nbsp; &nbsp; var formatDuration = function (totalMinutes) { &nbsp; &nbsp; &nbsp; &nbsp; totalMinutes = Math.max(0, parseInt(totalMinutes, 10) || 0); &nbsp; &nbsp; &nbsp; &nbsp; var days = Math.floor(totalMinutes / 1440), &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rem = totalMinutes % 1440, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hours = Math.floor(rem / 60), &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; minutes = rem % 60, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; parts = []; &nbsp; &nbsp; &nbsp; &nbsp; if (days &gt; 0) parts.push(days + 'd'); &nbsp; &nbsp; &nbsp; &nbsp; if (hours &gt; 0 || days &gt; 0) parts.push(hours + 'h'); &nbsp; &nbsp; &nbsp; &nbsp; parts.push(minutes + 'm'); &nbsp; &nbsp; &nbsp; &nbsp; return parts.join(' '); &nbsp; &nbsp; }; &nbsp; &nbsp; var unitToMinutes = function (amount, unit) { &nbsp; &nbsp; &nbsp; &nbsp; var a = parseInt(amount, 10), &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; u = String(unit || 'min').toLowerCase(); &nbsp; &nbsp; &nbsp; &nbsp; if (isNaN(a)) return null; &nbsp; &nbsp; &nbsp; &nbsp; if (/^(m|min|mins|minute|minutes|round|rounds)$/.test(u)) return a; &nbsp; &nbsp; &nbsp; &nbsp; if (/^(turn|turns)$/.test(u)) return a * 10; &nbsp; &nbsp; &nbsp; &nbsp; if (/^(hour|hours|hr|hrs)$/.test(u)) return a * 60; &nbsp; &nbsp; &nbsp; &nbsp; if (/^(day|days)$/.test(u)) return a * 1440; &nbsp; &nbsp; &nbsp; &nbsp; return null; &nbsp; &nbsp; }; &nbsp; &nbsp; var escapeHtml = function (s) { &nbsp; &nbsp; &nbsp; &nbsp; return String(s) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .replace(/&amp;/g, '&amp;amp;') &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .replace(/&lt;/g, '&amp;lt;') &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .replace(/&gt;/g, '&amp;gt;') &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .replace(/"/g, '&amp;quot;') &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .replace(/'/g, '&amp;#39;'); &nbsp; &nbsp; }; &nbsp; &nbsp; var makeButton = function (label, command) { &nbsp; &nbsp; &nbsp; &nbsp; return '&lt;a href="' + escapeHtml(command) + '" ' + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'style="display:inline-block;background:#d9d9d9;border:1px solid #666;border-radius:4px;' + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'padding:3px 6px;margin:2px 2px 2px 0;color:#000;text-decoration:none;font-weight:bold;"&gt;' + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(label) + '&lt;/a&gt;'; &nbsp; &nbsp; }; &nbsp; &nbsp; var sortTimers = function () { &nbsp; &nbsp; &nbsp; &nbsp; state.Timekeeper.timers.sort(function (a, b) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (a.expiresAt !== b.expiresAt) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return a.expiresAt - b.expiresAt; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return a.id - b.id; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; }; &nbsp; &nbsp; var removeTimerById = function (id) { &nbsp; &nbsp; &nbsp; &nbsp; var i; &nbsp; &nbsp; &nbsp; &nbsp; for (i = 0; i &lt; state.Timekeeper.timers.length; i++) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (state.Timekeeper.timers[i].id === id) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return state.Timekeeper.timers.splice(i, 1)[0]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; return null; &nbsp; &nbsp; }; &nbsp; &nbsp; var getPlayerName = function (msg) { &nbsp; &nbsp; &nbsp; &nbsp; var player = getObj('player', msg.playerid); &nbsp; &nbsp; &nbsp; &nbsp; if (player &amp;&amp; player.get('_displayname')) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return player.get('_displayname'); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; return String(msg.who || 'Player').replace(/\s*\(GM\)\s*$/, ''); &nbsp; &nbsp; }; &nbsp; &nbsp; var purgeExpired = function () { &nbsp; &nbsp; &nbsp; &nbsp; var now = state.Timekeeper.now, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; active = [], &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; expired = [], &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; i, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; t; &nbsp; &nbsp; &nbsp; &nbsp; for (i = 0; i &lt; state.Timekeeper.timers.length; i++) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; t = state.Timekeeper.timers[i]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (t.expiresAt &lt;= now) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; expired.push(t.label); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; active.push(t); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; state.Timekeeper.timers = active; &nbsp; &nbsp; &nbsp; &nbsp; sortTimers(); &nbsp; &nbsp; &nbsp; &nbsp; return expired; &nbsp; &nbsp; }; &nbsp; &nbsp; var showClock = function (changeText, expiredList) { &nbsp; &nbsp; &nbsp; &nbsp; var html = '', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; i, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; remaining; &nbsp; &nbsp; &nbsp; &nbsp; sortTimers(); &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="border:1px solid #444;background:#fff;padding:6px 8px;"&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="font-weight:bold;font-size:1.1em;margin-bottom:4px;"&gt;Campaign Clock&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div&gt;&lt;b&gt;Current:&lt;/b&gt; ' + escapeHtml(formatClock(state.Timekeeper.now)) + '&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; if (changeText) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div&gt;&lt;b&gt;Change:&lt;/b&gt; ' + escapeHtml(changeText) + '&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (state.Timekeeper.timers.length) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="margin-top:4px;"&gt;&lt;b&gt;Active Timers:&lt;/b&gt;&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for (i = 0; i &lt; state.Timekeeper.timers.length; i++) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; remaining = state.Timekeeper.timers[i].expiresAt - state.Timekeeper.now; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div&gt;- ' + escapeHtml(state.Timekeeper.timers[i].label) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ' (' + formatDuration(remaining) + ' remaining)&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="margin-top:4px;"&gt;&lt;b&gt;Active Timers:&lt;/b&gt; none&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (expiredList &amp;&amp; expiredList.length) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="margin-top:4px;color:#8a1c1c;"&gt;&lt;b&gt;Expired:&lt;/b&gt; ' + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(expiredList.join(', ')) + '&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('Timekeeper', '/direct ' + html); &nbsp; &nbsp; }; &nbsp; &nbsp; var showClearTimerMenu = function () { &nbsp; &nbsp; &nbsp; &nbsp; var html = '', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; i, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; remaining, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; t; &nbsp; &nbsp; &nbsp; &nbsp; sortTimers(); &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="border:1px solid #444;background:#fff;padding:6px 8px;"&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="font-weight:bold;font-size:1.1em;margin-bottom:6px;"&gt;End Timer&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; if (!state.Timekeeper.timers.length) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div&gt;No active timers.&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for (i = 0; i &lt; state.Timekeeper.timers.length; i++) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; t = state.Timekeeper.timers[i]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; remaining = Math.max(0, t.expiresAt - state.Timekeeper.now); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="margin-bottom:4px;"&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; html += makeButton('End', '!time-clear-id ' + t.id); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; html += ' ' + escapeHtml(t.label) + ' (' + formatDuration(remaining) + ' remaining)'; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('Timekeeper', '/direct ' + html); &nbsp; &nbsp; }; &nbsp; &nbsp; var showMenu = function () { &nbsp; &nbsp; &nbsp; &nbsp; var html = ''; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="border:1px solid #444;background:#fff;padding:6px 8px;"&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="font-weight:bold;font-size:1.1em;margin-bottom:6px;"&gt;Timekeeper Menu&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="margin-bottom:4px;"&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += makeButton('Refresh Menu', '!time-menu'); &nbsp; &nbsp; &nbsp; &nbsp; html += makeButton('Show Clock', '!time-show'); &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="margin-bottom:4px;"&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += makeButton('Clear Timer Menu', '!time-clear-menu'); &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="margin-bottom:4px;"&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += makeButton('+1 Round', '!time-advance 1 rounds'); &nbsp; &nbsp; &nbsp; &nbsp; html += makeButton('+1 Turn', '!time-advance 1 turns'); &nbsp; &nbsp; &nbsp; &nbsp; html += makeButton('+1 Hour', '!time-advance 1 hours'); &nbsp; &nbsp; &nbsp; &nbsp; html += makeButton('+1 Day', '!time-advance 1 days'); &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="margin-bottom:4px;"&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += makeButton( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; '+Custom Time', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; '!time-advance ?{Amount?|10} ?{Unit?|Minutes,min|Rounds,round|Turns,turn|Hours,hour|Days,day}' &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="margin-bottom:4px;"&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += makeButton( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 'Add Timer', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; '!time-timer ?{Item?|Torch} | ?{Amount?|60} | ?{Unit?|Minutes,min|Rounds,round|Turns,turn|Hours,hour|Days,day}' &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="margin-bottom:4px;"&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += makeButton('Set Clock', '!time-set ?{Day?|0} ?{Hour?|8} ?{Minute?|0}'); &nbsp; &nbsp; &nbsp; &nbsp; html += makeButton('Reset Clock', '!time-reset'); &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;div style="margin-top:6px;font-size:0.9em;color:#555;"&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += 'Player timers are labeled with the player name.'; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; html += '&lt;/div&gt;'; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('Timekeeper', '/direct ' + html); &nbsp; &nbsp; }; &nbsp; &nbsp; var handleInput = function (msg) { &nbsp; &nbsp; &nbsp; &nbsp; var parts, amount, unit, rest, timerBits, timerLabel, expired, timerId, removedTimer, playerName; &nbsp; &nbsp; &nbsp; &nbsp; if (msg.type !== 'api') return; &nbsp; &nbsp; &nbsp; &nbsp; if (msg.content === '!time-menu') { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; showMenu(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (msg.content === '!time-show') { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; showClock(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (msg.content === '!time-clear-menu') { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; showClearTimerMenu(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (msg.content.indexOf('!time-advance ') === 0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; parts = msg.content.split(/\s+/); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; amount = parts[1]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; unit = parts[2]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; amount = unitToMinutes(amount, unit); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (amount === null) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('Timekeeper', '/w "' + msg.who + '" Invalid time unit.'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state.Timekeeper.now += amount; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; expired = purgeExpired(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; showClock('Advanced ' + formatDuration(amount), expired); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (msg.content.indexOf('!time-set ') === 0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; parts = msg.content.split(/\s+/); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state.Timekeeper.now = (parseInt(parts[1], 10) * 1440) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(parseInt(parts[2], 10) * 60) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;parseInt(parts[3], 10); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; expired = purgeExpired(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; showClock('Clock set', expired); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (msg.content.indexOf('!time-timer ') === 0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rest = msg.content.replace('!time-timer ', ''); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; timerBits = rest.split(/\s*\|\s*/); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (timerBits.length &lt; 3) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('Timekeeper', '/w "' + msg.who + '" Use: !time-timer Item | Amount | Unit'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; amount = unitToMinutes(timerBits[1], timerBits[2]); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (amount === null || amount &lt;= 0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('Timekeeper', '/w "' + msg.who + '" Invalid timer duration.'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; playerName = getPlayerName(msg); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; timerLabel = playerName + ': ' + timerBits[0]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state.Timekeeper.timers.push({ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; id: state.Timekeeper.nextId++, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; label: timerLabel, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; expiresAt: state.Timekeeper.now + amount &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sortTimers(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; showClock('Added timer: ' + timerLabel + ' (' + formatDuration(amount) + ')'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (msg.content.indexOf('!time-clear-id ') === 0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; timerId = parseInt(msg.content.replace('!time-clear-id ', ''), 10); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (isNaN(timerId)) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('Timekeeper', '/w "' + msg.who + '" Invalid timer selection.'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; removedTimer = removeTimerById(timerId); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (!removedTimer) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat('Timekeeper', '/w "' + msg.who + '" That timer no longer exists. Open Clear Timer Menu again.'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; showClock('Cleared timer: ' + removedTimer.label); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (msg.content === '!time-reset') { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state.Timekeeper.now = 0; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state.Timekeeper.timers = []; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; showClock('Clock reset'); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }; &nbsp; &nbsp; var registerEventHandlers = function () { &nbsp; &nbsp; &nbsp; &nbsp; on('chat:message', handleInput); &nbsp; &nbsp; }; &nbsp; &nbsp; return { &nbsp; &nbsp; &nbsp; &nbsp; CheckInstall: checkInstall, &nbsp; &nbsp; &nbsp; &nbsp; RegisterEventHandlers: registerEventHandlers &nbsp; &nbsp; }; }()); on('ready', function () { &nbsp; &nbsp; 'use strict'; &nbsp; &nbsp; Timekeeper.CheckInstall(); &nbsp; &nbsp; Timekeeper.RegisterEventHandlers(); });
BlackSmith API Adds and tracks available blacksmiths in the campaign Uses the selected player character token as the buyer Lets any buyer request work from any available blacksmith Stores each blacksmith’s race, skill roll, and item limits Shows which items a blacksmith can make and which they cannot Generates quotes for supported armor and basic smithwork Uses base item prices with random price variation Adjusts prices using the buyer’s reaction bonus Adds helper-trade costs when required Supports haggling before placing the order Charges half down when ordering and the balance at pickup Tracks active jobs and days remaining Lets you apply elapsed days to check progress Handles completed jobs and pickup payments Adds completed purchases to the buyer’s character sheet coin totals Includes a price list for reference Supports removing individual blacksmiths or clearing all blacksmiths Includes a campaign reset option for the blacksmith system !smith-menu <a href="https://github.com/earmarkaudiology-png/BlackSmith-1e/commit/0103f90bc3f21cbd4ea0ee7701ebed259b22c637" rel="nofollow">https://github.com/earmarkaudiology-png/BlackSmith-1e/commit/0103f90bc3f21cbd4ea0ee7701ebed259b22c637</a>
Retainer API !retainer-menu Roll20 tool for managing hirelings, henchmen, and NPC followers in AD&amp;D 1e. It handles recruiting, offers, loyalty, wages, treasure shares, contracts, and reports through chat menus, making it easier to track retainers during play. Features: Adds and manages hirelings, henchmen, experts, and NPC followers Uses selected player character as the employer Handles recruitment and market searches Generates candidate retainers and offer flows Tracks pending offers and delayed responses Creates character sheets for accepted retainers Stores loyalty and syncs it to morale Tracks wages, contracts, and treasure shares Supports pay, pay all, and share of treasure functions Includes loyalty info and loyalty adjustment tools Supports direct add options for NPCs and retainers Provides reports by employer and active retainer lists Includes wage tables, search costs, and help/reference menus Supports campaign reset for retainer data <a href="https://github.com/earmarkaudiology-png/Retainers-1e-/commit/d0e04f7a4e47fb960ab432ec4d79268475ba53b9" rel="nofollow">https://github.com/earmarkaudiology-png/Retainers-1e-/commit/d0e04f7a4e47fb960ab432ec4d79268475ba53b9</a>