From e68c8ac9bdf3ae3fa6d96817692de5c25c86d5db Mon Sep 17 00:00:00 2001
From: Kingkiller546 <130240522+Kingkiller546@users.noreply.github.com>
Date: Fri, 21 Aug 2026 13:54:29 +0100
Subject: [PATCH 1/3] ## Summary Adds Initiative Pulse, an independently
authored Roll20 Mod script for timed combat announcements. - Announces
GM-created Actions when normal initiative crosses their configured
thresholds. - Counts down Effects from explicit round notifications and
announces remaining duration or expiry. - Integrates with Initiative Tracker
Plus without controlling turns, halos, end-of-turn handling, or round
advancement. - Provides native GM menu, optional ScriptCards menu, and
separate Clear Combat macros. - Includes `README.md`, `script.json`, and the
version 1.0.0 script. ## Validation - JavaScript syntax check passed. -
`script.json` passes the repository JSON Schema. - Mocked Roll20 behavior
tests passed for Action thresholds, repeating and one-shot Actions, Effect
countdown and expiry, duplicate round prevention, macro management, and ITP
clearing. - Live Roll20 behavior was tested before publication. ##
Provenance Initiative Pulse was independently authored from its behavior
specification. No ACT or ACT2 source code was copied, adapted, or reused.
## Summary
Adds Initiative Pulse, an independently authored Roll20 Mod script for timed combat announcements.
- Announces GM-created Actions when normal initiative crosses their configured thresholds.
- Counts down Effects from explicit round notifications and announces remaining duration or expiry.
- Integrates with Initiative Tracker Plus without controlling turns, halos, end-of-turn handling, or round advancement.
- Provides native GM menu, optional ScriptCards menu, and separate Clear Combat macros.
- Includes `README.md`, `script.json`, and the version 1.0.0 script.
## Validation
- JavaScript syntax check passed.
- `script.json` passes the repository JSON Schema.
- Mocked Roll20 behavior tests passed for Action thresholds, repeating and one-shot Actions, Effect countdown and expiry, duplicate round prevention, macro management, and ITP clearing.
- Live Roll20 behavior was tested before publication.
## Provenance
Initiative Pulse was independently authored from its behavior specification. No ACT or ACT2 source code was copied, adapted, or reused.
---
Initiative Pulse/InitiativePulse.js | 332 ++++++++++++++++++++++++++++
Initiative Pulse/README.md | 56 +++++
Initiative Pulse/script.json | 14 ++
3 files changed, 402 insertions(+)
create mode 100644 Initiative Pulse/InitiativePulse.js
create mode 100644 Initiative Pulse/README.md
create mode 100644 Initiative Pulse/script.json
diff --git a/Initiative Pulse/InitiativePulse.js b/Initiative Pulse/InitiativePulse.js
new file mode 100644
index 000000000..4c5b4bced
--- /dev/null
+++ b/Initiative Pulse/InitiativePulse.js
@@ -0,0 +1,332 @@
+/*
+ * Initiative Pulse v1.0.0
+ * Last updated: 2026-08-21
+ *
+ * Announces GM-authored initiative Actions without changing the turn tracker.
+ * Counts down Effects when !pulse-round notifications arrive.
+ *
+ * Commands:
+ * !pulse action Name %% Initiative %% Repeat
+ * !pulse effect Name %% Duration
+ * !pulse-menu
+ * !pulse install-macro
+ * !pulse install-scriptcards-macro
+ * !pulse install-clear-macro
+ * !pulse clear
+ * !pulse inspect
+ * !pulse clean
+ */
+
+var InitiativePulse = InitiativePulse || (function () {
+ 'use strict';
+
+ var SCRIPT = 'Initiative Pulse';
+ var VERSION = '1.0.0';
+ var STATE_KEY = 'InitiativePulse';
+ var SCHEMA_VERSION = 1;
+ var MENU_MACRO = 'Initiative-Pulse';
+ var SCRIPT_CARDS_MACRO = 'Initiative-Pulse-ScriptCards';
+ var CLEAR_MACRO = 'Clear-Combat';
+
+ function defaultState() {
+ return {
+ schemaVersion: SCHEMA_VERSION,
+ nextId: 1,
+ actions: [],
+ effects: [],
+ lastRound: null,
+ activeInitiative: null
+ };
+ }
+
+ function getState() {
+ if (!state[STATE_KEY] || state[STATE_KEY].schemaVersion !== SCHEMA_VERSION) {
+ state[STATE_KEY] = defaultState();
+ }
+ return state[STATE_KEY];
+ }
+
+ function escapeHtml(value) {
+ return String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
+ }
+
+ function panel(title, body) {
+ return '
' +
+ '
' +
+ escapeHtml(title) + '
' + body + '
';
+ }
+
+ function button(label, command) {
+ return '' +
+ escapeHtml(label) + '';
+ }
+
+ function announce(title, body) {
+ sendChat(SCRIPT, '/direct ' + panel(title, body));
+ }
+
+ function whisper(message) {
+ sendChat(SCRIPT, '/w gm ' + panel(SCRIPT, message));
+ }
+
+ function isAuthorized(msg) {
+ return msg.playerid === 'API' || playerIsGM(msg.playerid);
+ }
+
+ function requireGM(msg) {
+ if (!isAuthorized(msg)) {
+ whisper('Only a GM can manage Initiative Pulse.');
+ return false;
+ }
+ return true;
+ }
+
+ function nextId(prefix) {
+ var data = getState();
+ var id = prefix + data.nextId;
+ data.nextId += 1;
+ return id;
+ }
+
+ function parseRepeat(value) {
+ return /^(1|true|yes|y|repeat|repeating)$/i.test(String(value || '').trim());
+ }
+
+ function splitFields(text) {
+ return text.split('%%').map(function (field) { return field.trim(); });
+ }
+
+ function addAction(payload) {
+ var fields = splitFields(payload);
+ var initiative = Number(fields[1]);
+ if (!fields[0] || fields.length < 3 || !isFinite(initiative)) {
+ whisper('Usage: !pulse action Name %% Initiative %% Repeat. Repeat accepts yes or no.');
+ return;
+ }
+ getState().actions.push({
+ id: nextId('A'),
+ name: fields[0],
+ initiative: initiative,
+ repeat: parseRepeat(fields[2])
+ });
+ whisper('Added Action ' + escapeHtml(fields[0]) + ' at initiative ' +
+ escapeHtml(initiative) + (parseRepeat(fields[2]) ? ' (repeating).' : ' (once).'));
+ }
+
+ function addEffect(payload) {
+ var fields = splitFields(payload);
+ var duration = Number(fields[1]);
+ if (!fields[0] || fields.length < 2 || !isFinite(duration) || duration < 1 || Math.floor(duration) !== duration) {
+ whisper('Usage: !pulse effect Name %% Duration. Duration must be a positive whole number.');
+ return;
+ }
+ getState().effects.push({ id: nextId('E'), name: fields[0], remaining: duration });
+ whisper('Added Effect ' + escapeHtml(fields[0]) + ' for ' + duration + ' round(s).');
+ }
+
+ function parseTurnOrder(raw) {
+ var order;
+ if (!raw) { return []; }
+ try {
+ order = JSON.parse(raw);
+ return Array.isArray(order) ? order : [];
+ } catch (error) {
+ log(SCRIPT + ': could not parse turn order: ' + error.message);
+ return [];
+ }
+ }
+
+ function currentInitiative(campaign) {
+ var order = parseTurnOrder(campaign.get('turnorder'));
+ var value = order.length ? Number(order[0].pr) : NaN;
+ return isFinite(value) ? value : null;
+ }
+
+ function crossedThreshold(previous, current, threshold) {
+ if (previous === current) { return false; }
+ if (current < previous) {
+ return threshold < previous && threshold >= current;
+ }
+ return threshold < previous || threshold >= current;
+ }
+
+ function handleTurnOrder(campaign) {
+ var data = getState();
+ var current = currentInitiative(campaign);
+ var previous = data.activeInitiative;
+ var fired = [];
+
+ data.activeInitiative = current;
+ if (previous === null || current === null) { return; }
+
+ data.actions.forEach(function (action) {
+ if (crossedThreshold(previous, current, Number(action.initiative))) {
+ fired.push(action);
+ announce('Action', '' + escapeHtml(action.name) + '' +
+ 'Initiative ' + escapeHtml(action.initiative) + '
');
+ }
+ });
+
+ if (fired.length) {
+ data.actions = data.actions.filter(function (action) {
+ return action.repeat || fired.indexOf(action) === -1;
+ });
+ }
+ }
+
+ function handleRound(roundValue) {
+ var data = getState();
+ var round = String(roundValue || '').trim();
+ if (!round) {
+ whisper('Usage: !pulse-round Round.');
+ return;
+ }
+ if (data.lastRound === round) { return; }
+ data.lastRound = round;
+
+ data.effects.forEach(function (effect) {
+ effect.remaining -= 1;
+ if (effect.remaining <= 0) {
+ announce('Effect Expired', '' + escapeHtml(effect.name) + ' has expired.');
+ } else {
+ announce('Effect', '' + escapeHtml(effect.name) + '' +
+ '' + effect.remaining + ' round(s) remaining.
');
+ }
+ });
+ data.effects = data.effects.filter(function (effect) { return effect.remaining > 0; });
+ }
+
+ function scriptCardsInstalled() {
+ return typeof ScriptCards !== 'undefined' || !!state.ScriptCards;
+ }
+
+ function showMenu() {
+ var body = '' +
+ button('Add Action', '!pulse action ?{Action name} %% ?{Initiative|20} %% ?{Repeat|No,no|Yes,yes}') +
+ button('Add Effect', '!pulse effect ?{Effect name} %% ?{Duration in rounds|1}') +
+ '
' +
+ button('Inspect', '!pulse inspect') + button('Clear Combat', '!pulse clear') +
+ '
' +
+ button('Install Menu Macro', '!pulse install-macro') +
+ button('Install Clear Macro', '!pulse install-clear-macro') +
+ (scriptCardsInstalled() ? button('Install ScriptCards Macro', '!pulse install-scriptcards-macro') : '') +
+ '
';
+ whisper(body);
+ }
+
+ function upsertMacro(playerid, name, action) {
+ var matches = findObjs({ _type: 'macro', _playerid: playerid, name: name });
+ var macro = matches[0];
+ if (macro) {
+ macro.set({ action: action, visibleto: playerid });
+ } else {
+ createObj('macro', { _playerid: playerid, name: name, action: action, visibleto: playerid });
+ }
+ whisper('Installed GM macro ' + escapeHtml(name) + '.');
+ }
+
+ function installScriptCardsMacro(playerid) {
+ var action;
+ if (!scriptCardsInstalled()) {
+ whisper('ScriptCards is not installed, so no ScriptCards macro was created.');
+ return;
+ }
+ action = '!scriptcard {{ --#title|Initiative Pulse --#emotestate|hidden ' +
+ '--+Actions|[Add Action](!pulse action ?{Action name} %% ?{Initiative|20} %% ?{Repeat|No,no|Yes,yes}) ' +
+ '--+Effects|[Add Effect](!pulse effect ?{Effect name} %% ?{Duration in rounds|1}) ' +
+ '--+Tools|[Inspect](!pulse inspect) [Clear Combat](!pulse clear) }}';
+ upsertMacro(playerid, SCRIPT_CARDS_MACRO, action);
+ }
+
+ function clearCombat() {
+ var data = getState();
+ data.actions = [];
+ data.effects = [];
+ data.lastRound = null;
+ data.activeInitiative = currentInitiative(Campaign());
+ whisper('All stored Actions and Effects were cleared. Initiative Tracker Plus remains untouched.');
+ }
+
+ function inspect() {
+ var data = getState();
+ var actions = data.actions.length ? data.actions.map(function (item) {
+ return '' + escapeHtml(item.name) + ' — initiative ' + escapeHtml(item.initiative) +
+ (item.repeat ? ', repeating' : ', once') + '';
+ }).join('') : 'None';
+ var effects = data.effects.length ? data.effects.map(function (item) {
+ return '' + escapeHtml(item.name) + ' — ' + item.remaining + ' round(s)';
+ }).join('') : 'None';
+ whisper('ActionsEffects');
+ }
+
+ function clean(playerid) {
+ [MENU_MACRO, SCRIPT_CARDS_MACRO, CLEAR_MACRO].forEach(function (name) {
+ findObjs({ _type: 'macro', _playerid: playerid, name: name }).forEach(function (macro) { macro.remove(); });
+ });
+ delete state[STATE_KEY];
+ whisper('Removed this GM\'s Initiative Pulse macros and reset Initiative Pulse state.');
+ }
+
+ function handleInput(msg) {
+ var content;
+ var match;
+ var command;
+ var payload;
+ if (msg.type !== 'api') { return; }
+ content = String(msg.content || '').trim();
+
+ if (/^!eot(?:\s|$)/i.test(content)) {
+ return; // Deliberately observed without consuming, replacing, or advancing the turn.
+ }
+ match = content.match(/^!pulse-round(?:\s+(.+))?$/i);
+ if (match) {
+ if (requireGM(msg)) { handleRound(match[1]); }
+ return;
+ }
+ if (/^!itp\s+-clear(?:\s|$)/i.test(content)) {
+ if (requireGM(msg)) { clearCombat(); }
+ return;
+ }
+ if (/^!pulse-menu(?:\s|$)/i.test(content)) {
+ if (requireGM(msg)) { showMenu(); }
+ return;
+ }
+ match = content.match(/^!pulse(?:\s+([^\s]+))?(?:\s+([\s\S]*))?$/i);
+ if (!match) { return; }
+ if (!requireGM(msg)) { return; }
+ command = String(match[1] || '').toLowerCase();
+ payload = match[2] || '';
+
+ switch (command) {
+ case 'action': addAction(payload); break;
+ case 'effect': addEffect(payload); break;
+ case 'install-macro': upsertMacro(msg.playerid, MENU_MACRO, '!pulse-menu'); break;
+ case 'install-scriptcards-macro': installScriptCardsMacro(msg.playerid); break;
+ case 'install-clear-macro': upsertMacro(msg.playerid, CLEAR_MACRO, '!pulse clear'); break;
+ case 'clear': clearCombat(); break;
+ case 'inspect': inspect(); break;
+ case 'clean': clean(msg.playerid); break;
+ default: showMenu();
+ }
+ }
+
+ function checkInstall() {
+ var data = getState();
+ data.activeInitiative = currentInitiative(Campaign());
+ log(SCRIPT + ' v' + VERSION + ' ready.');
+ }
+
+ function registerEventHandlers() {
+ on('chat:message', handleInput);
+ on('change:campaign:turnorder', handleTurnOrder);
+ }
+
+ on('ready', function () {
+ checkInstall();
+ registerEventHandlers();
+ });
+
+ return { version: VERSION };
+}());
diff --git a/Initiative Pulse/README.md b/Initiative Pulse/README.md
new file mode 100644
index 000000000..f607e73d1
--- /dev/null
+++ b/Initiative Pulse/README.md
@@ -0,0 +1,56 @@
+# Initiative Pulse
+
+Initiative Pulse is an independent Roll20 Mod (API) script for timed combat announcements. It stores GM-created Actions and Effects, but leaves the campaign turn order and token presentation entirely alone.
+
+## Behavior
+
+- Actions announce when normal descending initiative crosses their threshold. One-shot Actions are then removed; repeating Actions remain for later rounds.
+- Effects decrement once for each distinct `!pulse-round` value and announce either their remaining duration or expiry.
+- Initiative Tracker Plus (ITP) remains responsible for turns, halos, `!eot`, and round handling. Initiative Pulse observes `!eot` without replying to it or changing its behavior.
+- `!itp -clear` also clears Initiative Pulse's stored Actions and Effects.
+- Games without ITP can install a separate **Clear-Combat** macro to clear stored combat entries explicitly.
+
+All commands that change or display Initiative Pulse data are GM-only. API-generated `!pulse-round` and `!itp -clear` messages are also accepted for integration.
+
+## Commands
+
+| Command | Purpose |
+| --- | --- |
+| `!pulse action Name %% Initiative %% Repeat` | Add an Action. Initiative may be any number. Repeat accepts `yes` or `no`. |
+| `!pulse effect Name %% Duration` | Add an Effect lasting a positive whole number of rounds. |
+| `!pulse-menu` | Open the native GM Action/Effect menu. |
+| `!pulse install-macro` | Create or update the **Initiative-Pulse** GM macro, which opens the native menu. |
+| `!pulse install-scriptcards-macro` | Create or update an optional ScriptCards menu macro when ScriptCards is installed. |
+| `!pulse install-clear-macro` | Create or update the separate **Clear-Combat** GM macro. |
+| `!pulse clear` | Clear all stored Actions and Effects without changing the tracker or ITP. |
+| `!pulse inspect` | List current Actions and Effects. |
+| `!pulse clean` | Remove the invoking GM's three Initiative Pulse macros and reset Initiative Pulse state. |
+
+Examples:
+
+```text
+!pulse action Lair action %% 20 %% yes
+!pulse action Falling portcullis %% 12.5 %% no
+!pulse effect Bless %% 3
+!pulse-round 4
+```
+
+## ITP integration
+
+Configure ITP (or another GM/API workflow) to send a notification in this form once per round:
+
+```text
+!pulse-round ROUND_IDENTIFIER
+```
+
+The identifier can be a round number or other unique text. Repeating the same identifier does not decrement Effects twice. When ITP sends `!itp -clear`, Initiative Pulse clears its own combat entries while allowing ITP's handler to process the same message normally.
+
+Initiative Pulse never calls an end-turn command and never writes `Campaign().turnorder`. Tracker changes are read only to detect movement from the previous active initiative value to the new one.
+
+## ScriptCards
+
+If ScriptCards is installed, `!pulse install-scriptcards-macro` creates a ScriptCards-styled launcher. ScriptCards is optional and is not a dependency of Initiative Pulse.
+
+## License and provenance
+
+Initiative Pulse was independently authored from the behavior described above. It contains no ACT or ACT2 source code and claims no credit for those projects. As part of the Roll20 API Scripts repository, this contribution is released under the repository's MIT License.
diff --git a/Initiative Pulse/script.json b/Initiative Pulse/script.json
new file mode 100644
index 000000000..294948c32
--- /dev/null
+++ b/Initiative Pulse/script.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://github.com/Roll20/roll20-api-scripts/master/script.json.schema",
+ "name": "Initiative Pulse",
+ "script": "InitiativePulse.js",
+ "version": "1.0.0",
+ "previousversions": [],
+ "description": "Initiative Pulse adds non-invasive timed announcements to normal Roll20 initiative. GM-created Actions announce when initiative crosses a configured threshold, while Effects count down from explicit round notifications. It integrates with Initiative Tracker Plus without taking control of turns, halos, end-of-turn handling, or rounds. Use `!pulse-menu` for the GM menu, `!pulse action Name %% Initiative %% Repeat` to add an Action, and `!pulse effect Name %% Duration` to add an Effect. Additional GM commands install menu, ScriptCards, and Clear Combat macros; inspect or clear current entries; and clean up Initiative Pulse state and macros. See the README for complete usage and integration details.",
+ "authors": "Marcus Hurrell",
+ "roll20userid": "2978722",
+ "useroptions": [],
+ "dependencies": [],
+ "modifies": {},
+ "conflicts": []
+}
From 0ad4c01df08c0154987c8ac83521bd20eb464208 Mon Sep 17 00:00:00 2001
From: Kingkiller546 <130240522+Kingkiller546@users.noreply.github.com>
Date: Fri, 21 Aug 2026 16:43:37 +0100
Subject: [PATCH 2/3] Update script.json
---
Initiative Pulse/script.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Initiative Pulse/script.json b/Initiative Pulse/script.json
index 294948c32..bb9835713 100644
--- a/Initiative Pulse/script.json
+++ b/Initiative Pulse/script.json
@@ -5,7 +5,7 @@
"version": "1.0.0",
"previousversions": [],
"description": "Initiative Pulse adds non-invasive timed announcements to normal Roll20 initiative. GM-created Actions announce when initiative crosses a configured threshold, while Effects count down from explicit round notifications. It integrates with Initiative Tracker Plus without taking control of turns, halos, end-of-turn handling, or rounds. Use `!pulse-menu` for the GM menu, `!pulse action Name %% Initiative %% Repeat` to add an Action, and `!pulse effect Name %% Duration` to add an Effect. Additional GM commands install menu, ScriptCards, and Clear Combat macros; inspect or clear current entries; and clean up Initiative Pulse state and macros. See the README for complete usage and integration details.",
- "authors": "Marcus Hurrell",
+ "authors": "Kingkiller546",
"roll20userid": "2978722",
"useroptions": [],
"dependencies": [],
From dd2095dad1af14381fecc0507b3aab36ba1d4bc0 Mon Sep 17 00:00:00 2001
From: Kingkiller546 <130240522+Kingkiller546@users.noreply.github.com>
Date: Fri, 21 Aug 2026 17:16:48 +0100
Subject: [PATCH 3/3] adding roll20-api toallowed list of scripts
---
Initiative Pulse/script.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Initiative Pulse/script.json b/Initiative Pulse/script.json
index bb9835713..328d34603 100644
--- a/Initiative Pulse/script.json
+++ b/Initiative Pulse/script.json
@@ -1,5 +1,5 @@
{
- "$schema": "https://github.com/Roll20/roll20-api-scripts/master/script.json.schema",
+ "$schema": "https://raw.githubusercontent.com/Roll20/roll20-api-scripts/master/script.json.schema",
"name": "Initiative Pulse",
"script": "InitiativePulse.js",
"version": "1.0.0",