Skip to content

Commit dc0f454

Browse files
authored
Merge branch 'Roll20:master' into master
2 parents 6187e10 + 0216cbd commit dc0f454

File tree

19 files changed

+11787
-283
lines changed

19 files changed

+11787
-283
lines changed

CommandMaster/5.0.3/CommandMaster.js

Lines changed: 6391 additions & 0 deletions
Large diffs are not rendered by default.

CommandMaster/CommandMaster.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,15 @@ API_Meta.CommandMaster={offset:Number.MAX_SAFE_INTEGER,lineCount:-1};
148148
* v5.0.1 23/09/2025 Fixed non-responsive [Add all Owned Weapons] button on Add Proficiencies dialog.
149149
* Added --noWaitMsg command to suppress spurious Please Wait messages.
150150
* v5.0.2 17/10/2025 Fixed crash on calling !cmd without a command
151+
* v5.0.3 23/10/2025 Fixed Priest class issue with bulk spell allocation
151152
*/
152153

153154
var CommandMaster = (function() { // eslint-disable-line no-unused-vars
154155
'use strict';
155-
var version = '5.0.2',
156+
var version = '5.0.3',
156157
author = 'RED',
157158
pending = null;
158-
const lastUpdate = 1760686967;
159+
const lastUpdate = 1761228496;
159160

160161
/*
161162
* Define redirections for functions moved to the RPGMaster library
@@ -3518,7 +3519,7 @@ var CommandMaster = (function() { // eslint-disable-line no-unused-vars
35183519
for (const c of classes) {
35193520
classData = c.obj[1].body;
35203521
classData = (classData.match(/}}\s*ClassData\s*=(.*?){{/im) || ['',''])[1];
3521-
classData = classData ? [...('['+classData+']').matchAll(/\[[\s\w\-\+\,\:\/\|]+?\]/g)] : [];
3522+
classData = classData ? [...('['+classData+']').matchAll(/\[[^\]]+?\]/g)] : [];
35223523
for (const d of classData) {
35233524
let data = parseData( d[0], reClassSpecs ),
35243525
spellType = data.spellLevels ? data.spellLevels.split('|')[3] : '';
@@ -3555,7 +3556,7 @@ var CommandMaster = (function() { // eslint-disable-line no-unused-vars
35553556
spellData = (spellData.match(/}}\s*SpellData\s*=(.*?){{/im) || ['',''])[1];
35563557
spellData = parseData( spellData, reSpellSpecs, false );
35573558
sphere = (spellData.sph+'|any').dbName().split('|');
3558-
if (_.some( sphere, s => (majorSpheres.includes(s) || (spellData.level < 4 && minorSpheres.includes(s))))) {
3559+
if (_.some( sphere, s => (majorSpheres.some(sph => s.startsWith(sph)) || (spellData.level < 4 && minorSpheres.some(sph => s.startsWith(sph)))))) {
35593560
if (_.isUndefined(spellBook[spellData.level])) spellBook[spellData.level] = [];
35603561
if (!spellBook[spellData.level].includes(spellName)) {
35613562
spellBook[spellData.level].push(spellName);

CommandMaster/script.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"$schema": "https://github.com/DameryDad/roll20-api-scripts/blob/CommandMasterAPI/CommandMaster/Script.json",
33
"name": "CommandMaster",
44
"script": "CommandMaster.js",
5-
"version": "5.0.2",
6-
"previousversions": ["1.020","1.022","1.024","1.025","2.027","1.3.00","1.3.01","1.3.02","1.3.03","1.3.04","1.4.01","1.4.02","1.4.04","1.4.05","1.4.06","1.4.07","1.5.01","2.1.0","2.2.0","2.3.0","2.3.1","2.3.2","2.3.3","3.0.0","3.1.2","3.2.0","3.2.1","3.3.0","3.5.0","3.5.1","4.0.1","4.0.2","5.0.0","5.0.1"],
5+
"version": "5.0.3",
6+
"previousversions": ["1.020","1.022","1.024","1.025","2.027","1.3.00","1.3.01","1.3.02","1.3.03","1.3.04","1.4.01","1.4.02","1.4.04","1.4.05","1.4.06","1.4.07","1.5.01","2.1.0","2.2.0","2.3.0","2.3.1","2.3.2","2.3.3","3.0.0","3.1.2","3.2.0","3.2.1","3.3.0","3.5.0","3.5.1","4.0.1","4.0.2","5.0.0","5.0.1","5.0.2"],
77
"description": "The CommandMaster API is part of the RPGMaster suite of APIs for Roll20, and manages the initialisation of a Campaign to use the RPGMaster APIs, communication and command syntax updates between the APIs and, most importantly for the DM, easy menu-driven setup of Tokens and Character Sheets to work with the APIs.\n\n[CommandMaster Documentation](https://wiki.roll20.net/Script:CommandMaster) \n[RPGMaster Documentation](https://wiki.roll20.net/RPGMaster) \n### Getting Started\n1. Run `!cmd --initialise` and add the player macros created to the Macro Bar, then\n2. Select tokens and use the `Token Setup` macro bar button just created to add all relevant Action Buttons to the token(s) (plus set the tokens/Characters up in any other way provided in the menu displayed) \n3. Once steps 1 & 2 have been done, the players and DM can then use the buttons displayed at the top of the screen when their character's token is selected to perform all actions needed in normal play.",
88
"authors": "Richard E.",
99
"roll20userid": "6497708",

EzExhaust2024/1.0/EzExhaust2024.js

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
// EzExhaust2024 (D&D 5e 2024) — v1.0
2+
// Created by Kahooty, licensed under MIT
3+
//
4+
// Commands:
5+
// !exh <character name> <#|clear>
6+
// !exh config
7+
8+
(() => {
9+
'use strict';
10+
11+
const SCRIPT = { NAME: 'EzExhaust2024', VERSION: '1.0', STATEKEY: 'EzExhaust2024' };
12+
13+
// rules per 2024
14+
const MAX_EXHAUSTION = 6;
15+
const PENALTY_PER_LEVEL = 2;
16+
17+
// config (only cosmetic/sheet fields)
18+
const DEFAULTS = Object.freeze({
19+
tokenMarker: 'sleepy', // marker for levels 1–5
20+
showMarkerNumber: true,
21+
adjustSpellDC: true,
22+
sheetAttr: {
23+
globalAbilityChecks: 'global_skill_mod',
24+
globalSaves: 'global_save_mod',
25+
globalAttacks: 'global_attack_mod',
26+
globalSpellDC: 'global_spell_dc_mod'
27+
}
28+
});
29+
30+
const ensureState = () => {
31+
state[SCRIPT.STATEKEY] = state[SCRIPT.STATEKEY] || {};
32+
const S = state[SCRIPT.STATEKEY];
33+
if (!S.config) S.config = JSON.parse(JSON.stringify(DEFAULTS));
34+
// rules are fixed (not user-configurable)
35+
delete S.config.maxExhaustion;
36+
delete S.config.perLevelPenalty;
37+
return S;
38+
};
39+
const getConfig = () => ensureState().config;
40+
41+
const say = (msg, who = 'gm') => sendChat(SCRIPT.NAME, (who === 'gm' ? `/w gm ${msg}` : msg));
42+
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
43+
44+
const getOrCreateAttr = (charId, name) => {
45+
if (!name) return null;
46+
let a = findObjs({ _type: 'attribute', _characterid: charId, name })[0];
47+
if (!a) a = createObj('attribute', { _characterid: charId, name, current: '' });
48+
return a;
49+
};
50+
const setAttrToSignedNumber = (attr, n) => {
51+
if (!attr) return;
52+
const str = n === 0 ? '' : (n > 0 ? `+${n}` : `${n}`);
53+
attr.set({ current: str });
54+
};
55+
56+
// token markers
57+
const getAllTokenMarkers = () => {
58+
try { return JSON.parse(Campaign().get('token_markers') || '[]'); } catch { return []; }
59+
};
60+
const resolveMarkerTag = (key) => {
61+
const all = getAllTokenMarkers();
62+
let m = all.find(x => x.tag === key) || all.find(x => (x.name||'').toLowerCase() === String(key).toLowerCase());
63+
return m ? m.tag : key;
64+
};
65+
const removeMarkerByTag = (tok, tag) => {
66+
const list = (tok.get('statusmarkers') || '').split(',').filter(Boolean);
67+
const cleaned = list.filter(m => !(m === tag || m.startsWith(`${tag}@`)));
68+
tok.set('statusmarkers', cleaned.join(','));
69+
};
70+
const addMarkerWithLevel = (tok, tag, level, showNumber) => {
71+
const entry = (showNumber && level > 0) ? `${tag}@${level}` : tag;
72+
const list = (tok.get('statusmarkers') || '').split(',').filter(Boolean);
73+
list.push(entry);
74+
tok.set('statusmarkers', list.join(','));
75+
};
76+
77+
// fuzzy character find
78+
const levenshtein = (a, b) => {
79+
a = (a||'').toLowerCase(); b = (b||'').toLowerCase();
80+
const dp = Array(b.length + 1).fill(0).map((_, i) => [i]);
81+
for (let j = 0; j <= a.length; j++) dp[0][j] = j;
82+
for (let i = 1; i <= b.length; i++) {
83+
for (let j = 1; j <= a.length; j++) {
84+
dp[i][j] = Math.min(
85+
dp[i-1][j] + 1,
86+
dp[i][j-1] + 1,
87+
dp[i-1][j-1] + (a[j-1] === b[i-1] ? 0 : 1)
88+
);
89+
}
90+
}
91+
return dp[b.length][a.length];
92+
};
93+
const findCharacterFuzzy = (name) => {
94+
const exact = findObjs({ _type: 'character', name })[0];
95+
if (exact) return exact;
96+
const chars = findObjs({ _type: 'character' });
97+
const lc = name.toLowerCase();
98+
99+
let pool = chars.filter(c => (c.get('name')||'').toLowerCase() === lc || (c.get('name')||'').toLowerCase().startsWith(lc));
100+
if (pool.length === 1) return pool[0];
101+
pool = chars.filter(c => (c.get('name')||'').toLowerCase().includes(lc));
102+
if (pool.length === 1) return pool[0];
103+
if (pool.length === 0) pool = chars;
104+
105+
let best = null, bestD = Infinity;
106+
pool.forEach(c => {
107+
const d = levenshtein(name, c.get('name')||'');
108+
if (d < bestD) { bestD = d; best = c; }
109+
});
110+
return best;
111+
};
112+
113+
// controllers + whispers
114+
const getControllingPlayers = (charObj) => {
115+
const raw = (charObj.get('controlledby') || '').trim();
116+
if (raw === 'all' || raw === '') {
117+
// 'all' => everybody can control; '' => no explicit controllers (GM implicitly)
118+
return findObjs({ _type: 'player' }) || [];
119+
}
120+
const ids = raw.split(',').map(s => s.trim()).filter(Boolean);
121+
const players = ids.map(id => getObj('player', id)).filter(Boolean);
122+
return players;
123+
};
124+
const whisperToPlayer = (player, html) => {
125+
const name = player.get('displayname') || player.get('_displayname') || 'Player';
126+
sendChat(SCRIPT.NAME, `/w "${name}" ${html}`);
127+
};
128+
const whisperToControllers = (charObj, html) => {
129+
const players = getControllingPlayers(charObj);
130+
const sent = new Set();
131+
players.forEach(p => {
132+
const pid = p.id;
133+
if (!sent.has(pid)) { sent.add(pid); whisperToPlayer(p, html); }
134+
});
135+
};
136+
const speedPenaltyByLevel = (level) => {
137+
// Levels 1–5: -5 ft per level; otherwise 0 for 0 or 6+
138+
if (level >= 1 && level <= 5) return -5 * level;
139+
return 0;
140+
};
141+
const whisperSpeedPenalty = (charObj, level) => {
142+
const pen = speedPenaltyByLevel(level);
143+
if (pen === 0) return;
144+
const cname = _.escape(charObj.get('name') || 'Unknown');
145+
const html = `<div style="font-family:monospace">
146+
<b>${cname}</b> — Exhaustion <b>${level}</b><br>
147+
Movement Speed Penalty: <b>${pen} ft</b>
148+
</div>`;
149+
whisperToControllers(charObj, html);
150+
};
151+
152+
// level 6: death helpers
153+
const enactDeath = (charId) => {
154+
const hpAttr = findObjs({ _type: 'attribute', _characterid: charId, name: 'hp' })[0];
155+
if (hpAttr) hpAttr.set({ current: 0 });
156+
const toks = findObjs({ _type: 'graphic', _subtype: 'token', represents: charId });
157+
toks.forEach(tok => tok.set('bar1_value', 0));
158+
};
159+
160+
const clearAllExhaustionMarkers = (charId, cfg) => {
161+
const sleepy = resolveMarkerTag(cfg.tokenMarker);
162+
const pummeled = resolveMarkerTag('pummeled');
163+
const dead = resolveMarkerTag('dead');
164+
findObjs({ _type: 'graphic', _subtype: 'token', represents: charId }).forEach(tok => {
165+
removeMarkerByTag(tok, sleepy);
166+
removeMarkerByTag(tok, pummeled);
167+
removeMarkerByTag(tok, dead); // ensure death icon drops when lowering from 6
168+
});
169+
};
170+
171+
const applyExhaustion = (charObj, rawLevel, cfg) => {
172+
const level = clamp(parseInt(rawLevel, 10) || 0, 0, MAX_EXHAUSTION);
173+
const S = cfg.sheetAttr;
174+
175+
// clean before setting fresh state
176+
clearAllExhaustionMarkers(charObj.id, cfg);
177+
178+
if (level === MAX_EXHAUSTION) {
179+
// clear mods (character is dead at 6; no ongoing penalties needed)
180+
setAttrToSignedNumber(getOrCreateAttr(charObj.id, S.globalAbilityChecks), 0);
181+
setAttrToSignedNumber(getOrCreateAttr(charObj.id, S.globalSaves), 0);
182+
setAttrToSignedNumber(getOrCreateAttr(charObj.id, S.globalAttacks), 0);
183+
if (cfg.adjustSpellDC && S.globalSpellDC) {
184+
setAttrToSignedNumber(getOrCreateAttr(charObj.id, S.globalSpellDC), 0);
185+
}
186+
187+
enactDeath(charObj.id);
188+
189+
// set pummeled@6 and 'dead' icon
190+
const pummeled = resolveMarkerTag('pummeled');
191+
const dead = resolveMarkerTag('dead');
192+
findObjs({ _type: 'graphic', _subtype: 'token', represents: charObj.id }).forEach(tok => {
193+
addMarkerWithLevel(tok, pummeled, 6, true);
194+
addMarkerWithLevel(tok, dead, 0, false);
195+
});
196+
197+
// no speed whisper at level 6
198+
return level;
199+
}
200+
201+
// levels 0–5: penalty = –2 × level (exactly)
202+
const penalty = -(PENALTY_PER_LEVEL * level);
203+
setAttrToSignedNumber(getOrCreateAttr(charObj.id, S.globalAbilityChecks), penalty);
204+
setAttrToSignedNumber(getOrCreateAttr(charObj.id, S.globalSaves), penalty);
205+
setAttrToSignedNumber(getOrCreateAttr(charObj.id, S.globalAttacks), penalty);
206+
if (cfg.adjustSpellDC && S.globalSpellDC) {
207+
setAttrToSignedNumber(getOrCreateAttr(charObj.id, S.globalSpellDC), penalty);
208+
}
209+
210+
// sleepy@level
211+
const sleepy = resolveMarkerTag(cfg.tokenMarker);
212+
findObjs({ _type: 'graphic', _subtype: 'token', represents: charObj.id }).forEach(tok => {
213+
if (level > 0) addMarkerWithLevel(tok, sleepy, level, cfg.showMarkerNumber);
214+
});
215+
216+
// whisper movement penalty to controllers for levels 1–5
217+
whisperSpeedPenalty(charObj, level);
218+
219+
return level;
220+
};
221+
222+
const renderConfig = (cfg) =>
223+
`<div style="font-family:monospace">
224+
<b>tokenMarker</b>: ${_.escape(cfg.tokenMarker)}<br>
225+
<b>showMarkerNumber</b>: ${cfg.showMarkerNumber}<br>
226+
<b>adjustSpellDC</b>: ${cfg.adjustSpellDC}<br>
227+
<b>sheetAttr.globalAbilityChecks</b>: ${_.escape(cfg.sheetAttr.globalAbilityChecks)}<br>
228+
<b>sheetAttr.globalSaves</b>: ${_.escape(cfg.sheetAttr.globalSaves)}<br>
229+
<b>sheetAttr.globalAttacks</b>: ${_.escape(cfg.sheetAttr.globalAttacks)}<br>
230+
<b>sheetAttr.globalSpellDC</b>: ${_.escape(cfg.sheetAttr.globalSpellDC)}<br>
231+
<hr>
232+
<b>Rules (fixed)</b>: max exhaustion = 6; penalty = -2×level; level 6 = death + pummeled + dead marker.
233+
</div>`;
234+
235+
const handleChat = (msg) => {
236+
if (msg.type !== 'api') return;
237+
const content = msg.content.trim();
238+
if (!content.startsWith('!exh')) return;
239+
240+
const parts = content.split(/\s+/);
241+
parts.shift();
242+
243+
// config view only (read-only for fixed rules)
244+
if (parts[0] && parts[0].toLowerCase() === 'config') {
245+
const cfg = getConfig();
246+
say(`<div><b>${SCRIPT.NAME} v${SCRIPT.VERSION} config</b></div>${renderConfig(cfg)}`);
247+
return;
248+
}
249+
250+
// usage
251+
if (parts.length < 2) {
252+
say('Usage: <code>!exh &lt;character name&gt; &lt;#|clear&gt;</code><br>Example: <code>!exh Tallus 2</code> or <code>!exh Tallus clear</code>', msg.who);
253+
return;
254+
}
255+
256+
const name = parts.shift();
257+
const op = parts.shift();
258+
const cfg = getConfig();
259+
260+
const charObj = findCharacterFuzzy(name);
261+
if (!charObj) { say(`No character found resembling <b>${_.escape(name)}</b>.`); return; }
262+
263+
if (op.toLowerCase && op.toLowerCase() === 'clear') {
264+
applyExhaustion(charObj, 0, cfg);
265+
say(`Exhaustion cleared for <b>${_.escape(charObj.get('name'))}</b>.`);
266+
return;
267+
}
268+
269+
if (!/^\d{1,2}$/.test(op)) {
270+
say('Level must be an integer (0–6) or <code>clear</code>.');
271+
return;
272+
}
273+
274+
const clamped = applyExhaustion(charObj, parseInt(op, 10), cfg);
275+
say(`Exhaustion set to <b>${clamped}</b> for <b>${_.escape(charObj.get('name'))}</b>.${clamped === 6 ? ' (Death applied.)' : ''}`);
276+
};
277+
278+
on('ready', () => {
279+
ensureState();
280+
on('chat:message', handleChat);
281+
log(`${SCRIPT.NAME} v${SCRIPT.VERSION} ready — Command: !exh`);
282+
});
283+
})();

EzExhaust2024/Readme.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Ez Exhaust 2024
2+
Automates and tracks **5E 2024 exhaustion effects** for GMs and players in Roll20.
3+
4+
## 1. What does this do?
5+
Provides automated exhaustion handling across six levels.
6+
- Applies the **sleepy icon** with the current exhaustion level.
7+
- Applies **-2 × exhaustion level** to all ability checks and saving throws.
8+
- **Whispers** the player controllers their current movement speed reduction.
9+
- Automatically sets **0 HP** and applies the **death icon** at exhaustion level 6.
10+
11+
## 2. What are some other features?
12+
- Exhaustion level is **capped at 6** (any value above is clamped).
13+
- Reducing exhaustion from 6 cleanly **removes death effects**.
14+
- Exhaustion can be **fully cleared** with a command.
15+
- **Fuzzy search** is used for player names, allowing quick targeting.
16+
17+
## 3. What are all the commands?
18+
!exh — Displays basic exhaustion info
19+
!exh playername # — Applies exhaustion level (# = 1–6)
20+
!exh playername clear — Clears exhaustion effects
21+
!exh config — Displays configuration options
22+
23+
## 4. Is this configurable for 2014 rules?
24+
Yes. The code is commented — a few variable changes can adapt it for **5E 2014** exhaustion rules.

EzExhaust2024/script.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "Ez Exhaust 2024",
3+
"script": "EzExhaust2024.js",
4+
"version": "1.0",
5+
"previousversions": [],
6+
"description": "Provides GMs and Players with a simple method for applying, and managing Exhaustion within the 5E 2024 ruleset.",
7+
"authors": "Kahooty.",
8+
"roll20userid": "14337689",
9+
"useroptions": [],
10+
"dependencies": [],
11+
"modifies": {},
12+
"conflicts": []
13+
}

0 commit comments

Comments
 (0)