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

Spell: Mirror Image.

You tube vid showing how the mod words in game.&nbsp; <a href="https://www.youtube.com/watch?v=QAQGIsecuZY" rel="nofollow">https://www.youtube.com/watch?v=QAQGIsecuZY</a>
Do you have a place where you've posted these spell macros?&nbsp; It looks like you've put a hell of a lot of work into macros!
1788521265

Edited 1788534624
ROTTO
Elite
Here is a starting point you can try. You need token mod etc to run these. I might eventually make it one big API mod at some point but I'm still building it as I go.&nbsp; Select a token and use the command and it should show up in chat.&nbsp; &nbsp;!gm1e <a href="https://drive.google.com/file/d/1T0nwYG3AK4kfq0a4vPbHn8Tithda4c2g/view?usp=sharing" rel="nofollow">https://drive.google.com/file/d/1T0nwYG3AK4kfq0a4vPbHn8Tithda4c2g/view?usp=sharing</a> August 22 (1 week ago) ROTTO Elite Permalink Quote Report Edit Delete Pummel <a href="https://drive.google.com/file/d/1zVaeEB_0LkL7npInDwh6_j3w43hJAgZR/view?usp=sharing" rel="nofollow">https://drive.google.com/file/d/1zVaeEB_0LkL7npInDwh6_j3w43hJAgZR/view?usp=sharing</a> August 22 (1 week ago) ROTTO Elite Permalink Quote Report Edit Delete Grapple:&nbsp;&nbsp; <a href="https://drive.google.com/file/d/1pc-1KuLitkg6gti0sG3qERHVktPsbDAA/view?usp=sharing" rel="nofollow">https://drive.google.com/file/d/1pc-1KuLitkg6gti0sG3qERHVktPsbDAA/view?usp=sharing</a> August 22 (1 week ago) ROTTO Elite Permalink Quote Report Edit Delete Overbear:&nbsp;&nbsp; <a href="https://drive.google.com/file/d/1m0FRTeyogaMH5LaR11GmQwJm5iuKiTG8/view?usp=sharing" rel="nofollow">https://drive.google.com/file/d/1m0FRTeyogaMH5LaR11GmQwJm5iuKiTG8/view?usp=sharing</a> August 22 (1 week ago) ROTTO Elite Permalink Quote Report Edit Delete Disarm (() =&gt; { &nbsp; 'use strict'; &nbsp; const SCRIPT = 'Disarm1E'; &nbsp; const VERSION = '0.1.6-real-dice'; &nbsp; const CMD = '!disarm'; &nbsp; const H = { &nbsp; &nbsp; esc: (s) =&gt; { &nbsp; &nbsp; &nbsp; const entities = { &nbsp; &nbsp; &nbsp; &nbsp; '&amp;': String.fromCharCode(38, 97, 109, 112, 59), &nbsp; &nbsp; &nbsp; &nbsp; '&lt;': String.fromCharCode(38, 108, 116, 59), &nbsp; &nbsp; &nbsp; &nbsp; '&gt;': String.fromCharCode(38, 103, 116, 59), &nbsp; &nbsp; &nbsp; &nbsp; '"': String.fromCharCode(38, 113, 117, 111, 116, 59), &nbsp; &nbsp; &nbsp; &nbsp; "'": String.fromCharCode(38, 35, 51, 57, 59) &nbsp; &nbsp; &nbsp; }; &nbsp; &nbsp; &nbsp; return String(s ?? '').replace(/[&amp;&lt;&gt;"']/g, c =&gt; entities[c]); &nbsp; &nbsp; }, &nbsp; &nbsp; deferRoll20: (s) =&gt; &nbsp; &nbsp; &nbsp; String(s || '') &nbsp; &nbsp; &nbsp; &nbsp; .split('@').join('&amp;#64;') &nbsp; &nbsp; &nbsp; &nbsp; .split('{').join('&amp;#123;') &nbsp; &nbsp; &nbsp; &nbsp; .split('}').join('&amp;#125;') &nbsp; &nbsp; &nbsp; &nbsp; .split('|').join('&amp;#124;'), &nbsp; &nbsp; box: (c) =&gt; &nbsp; &nbsp; &nbsp; `&lt;div style="background:#000;background-color:#000;color:#fff;border:1px solid #aa2222;padding:6px;font-size:12px;line-height:1.25;display:block;"&gt;${c}&lt;/div&gt;`, &nbsp; &nbsp; title: (t) =&gt; &nbsp; &nbsp; &nbsp; `&lt;div style="background:#000;background-color:#000;color:#fff;font-weight:bold;font-size:14px;border-bottom:1px solid #aa2222;margin-bottom:4px;padding-bottom:3px;"&gt;${H.esc(t)}&lt;/div&gt;`, &nbsp; &nbsp; row: (c) =&gt; &nbsp; &nbsp; &nbsp; `&lt;div style="background:#000;background-color:#000;color:#fff;margin:2px 0;"&gt;${c}&lt;/div&gt;`, &nbsp; &nbsp; subrow: (c) =&gt; &nbsp; &nbsp; &nbsp; `&lt;div style="background:#000;background-color:#000;color:#ddd;margin:1px 0 4px 16px;font-size:11px;"&gt;${c}&lt;/div&gt;`, &nbsp; &nbsp; hr: () =&gt; &nbsp; &nbsp; &nbsp; `&lt;div style="background:#000;background-color:#000;border-top:1px solid #aa2222;margin:4px 0;height:0;"&gt;&lt;/div&gt;`, &nbsp; &nbsp; small: (c) =&gt; &nbsp; &nbsp; &nbsp; `&lt;span style="background:#000;background-color:#000;color:#ddd;font-size:11px;"&gt;${c}&lt;/span&gt;`, &nbsp; &nbsp; btn: (label, cmd, tip = '') =&gt; &nbsp; &nbsp; &nbsp; `&lt;a href="${H.esc(cmd)}" title="${H.esc(tip)}" style="background:#111;background-color:#111;color:#fff;border:1px solid #aa2222;border-radius:3px;padding:1px 5px;margin:1px;display:inline-block;text-decoration:none;font-size:11px;font-weight:bold;"&gt;${H.esc(label)}&lt;/a&gt;`, &nbsp; &nbsp; macroBtn: (label, cmd, tip = '') =&gt; &nbsp; &nbsp; &nbsp; `&lt;a href="${H.deferRoll20(cmd)}" title="${H.esc(tip)}" style="background:#111;background-color:#111;color:#fff;border:1px solid #aa2222;border-radius:3px;padding:1px 5px;margin:1px;display:inline-block;text-decoration:none;font-size:11px;font-weight:bold;"&gt;${H.esc(label)}&lt;/a&gt;`, &nbsp; &nbsp; btnOn: (label, cmd, tip = '') =&gt; &nbsp; &nbsp; &nbsp; `&lt;a href="${H.esc(cmd)}" title="${H.esc(tip)}" style="background:#1f6f1f;background-color:#1f6f1f;color:#fff;border:1px solid #aa2222;border-radius:3px;padding:1px 5px;margin:1px;display:inline-block;text-decoration:none;font-size:11px;font-weight:bold;"&gt;${H.esc(label)}&lt;/a&gt;`, &nbsp; &nbsp; btnOff: (label, cmd, tip = '') =&gt; &nbsp; &nbsp; &nbsp; `&lt;a href="${H.esc(cmd)}" title="${H.esc(tip)}" style="background:#111;background-color:#111;color:#fff;border:1px solid #aa2222;border-radius:3px;padding:1px 5px;margin:1px;display:inline-block;text-decoration:none;font-size:11px;font-weight:bold;"&gt;${H.esc(label)}&lt;/a&gt;`, &nbsp; &nbsp; pill: (label, bg = '#7a1f1f') =&gt; &nbsp; &nbsp; &nbsp; `&lt;span style="background:${bg};background-color:${bg};color:#fff;border:1px solid #aa2222;border-radius:3px;padding:1px 5px;margin:1px;display:inline-block;font-size:11px;font-weight:bold;"&gt;${H.esc(label)}&lt;/span&gt;`, &nbsp; &nbsp; infoPill: (label) =&gt; &nbsp; &nbsp; &nbsp; `&lt;span style="background:#222;background-color:#222;color:#ddd;border:1px solid #555;border-radius:3px;padding:1px 5px;margin:1px;display:inline-block;font-size:11px;font-weight:bold;"&gt;${H.esc(label)}&lt;/span&gt;`, &nbsp; &nbsp; diePill: (label) =&gt; &nbsp; &nbsp; &nbsp; `&lt;span style="background:#d8a11d;background-color:#d8a11d;color:#000;border:1px solid #f4d36a;border-radius:3px;padding:1px 6px;margin:1px;display:inline-block;font-size:12px;font-weight:bold;"&gt;${H.esc(label)}&lt;/span&gt;`, &nbsp; &nbsp; resultPill: (label, isHit) =&gt; { &nbsp; &nbsp; &nbsp; const bg = isHit ? '#1f6f1f' : '#7a1111'; &nbsp; &nbsp; &nbsp; const border = isHit ? '#51d66b' : '#ff4444'; &nbsp; &nbsp; &nbsp; return `&lt;span style="background:${bg};background-color:${bg};color:#fff;border:1px solid ${border};border-radius:3px;padding:1px 6px;margin:1px;display:inline-block;font-size:12px;font-weight:bold;"&gt;${H.esc(label)}&lt;/span&gt;`; &nbsp; &nbsp; }, &nbsp; &nbsp; good: (c) =&gt; &nbsp; &nbsp; &nbsp; `&lt;span style="color:#51d66b;font-weight:bold;"&gt;${H.esc(c)}&lt;/span&gt;`, &nbsp; &nbsp; bad: (c) =&gt; &nbsp; &nbsp; &nbsp; `&lt;span style="color:#ff5555;font-weight:bold;"&gt;${H.esc(c)}&lt;/span&gt;`, &nbsp; &nbsp; neutral: (c) =&gt; &nbsp; &nbsp; &nbsp; `&lt;span style="color:#ddd;"&gt;${H.esc(c)}&lt;/span&gt;`, &nbsp; &nbsp; section: (c) =&gt; &nbsp; &nbsp; &nbsp; `&lt;div style="background:#111;background-color:#111;color:#fff;border-left:3px solid #aa2222;padding:2px 4px;margin:5px 0 2px 0;font-weight:bold;text-transform:uppercase;font-size:11px;"&gt;${H.esc(c)}&lt;/div&gt;` &nbsp; }; &nbsp; const playDiceSound = () =&gt; { &nbsp; &nbsp; sendChat(SCRIPT, '!splay diceri'); &nbsp; }; &nbsp; const initState = () =&gt; { &nbsp; &nbsp; state[SCRIPT] = state[SCRIPT] || { sessions: {} }; &nbsp; }; &nbsp; const playerName = (playerId) =&gt; { &nbsp; &nbsp; const p = getObj('player', playerId); &nbsp; &nbsp; return p ? p.get('_displayname') : 'player'; &nbsp; }; &nbsp; const whisper = (playerId, html) =&gt; { &nbsp; &nbsp; sendChat(SCRIPT, `/w "${playerName(playerId)}" ${html}`); &nbsp; }; &nbsp; let REAL_DICE_QUEUE = {}; &nbsp; const realDiceInt = (v, fallback = 0) =&gt; { &nbsp; &nbsp; const n = parseInt(String(v ?? '').replace(/[^\d-]/g, ''), 10); &nbsp; &nbsp; return isNaN(n) ? fallback : n; &nbsp; }; &nbsp; const realDiceOnForToken = (tokenId) =&gt; !!( &nbsp; &nbsp; typeof state !== 'undefined' &amp;&amp; &nbsp; &nbsp; state.GM1E &amp;&amp; &nbsp; &nbsp; state.GM1E.realDice &amp;&amp; &nbsp; &nbsp; state.GM1E.realDice[String(tokenId || '')] &nbsp; ); &nbsp; const realDiceLabel = (s) =&gt; String(s || '') &nbsp; &nbsp; .replace(/[|{}?]/g, ' ') &nbsp; &nbsp; .replace(/\s+/g, ' ') &nbsp; &nbsp; .replace(/^\s+|\s+$/g, ''); &nbsp; const realDiceSuffix = (tokenId, specs) =&gt; { &nbsp; &nbsp; if (!realDiceOnForToken(tokenId)) return ''; &nbsp; &nbsp; return (specs || []).map(spec =&gt; { &nbsp; &nbsp; &nbsp; const sides = spec.sides || 20; &nbsp; &nbsp; &nbsp; const label = realDiceLabel(spec.label || ('d' + sides)); &nbsp; &nbsp; &nbsp; const def = spec.defaultValue || (sides === 100 ? 50 : Math.ceil(sides / 2)); &nbsp; &nbsp; &nbsp; return '|D' + sides + '=?{Roll Real Dice - enter ' + label + '|' + def + '}'; &nbsp; &nbsp; }).join(''); &nbsp; }; &nbsp; const setManualRealDiceArgs = (args) =&gt; { &nbsp; &nbsp; REAL_DICE_QUEUE = {}; &nbsp; &nbsp; (args || []).forEach(a =&gt; { &nbsp; &nbsp; &nbsp; const m = String(a || '').match(/^D(\d+)=(.*)$/i); &nbsp; &nbsp; &nbsp; if (!m) return; &nbsp; &nbsp; &nbsp; const sides = String(parseInt(m[1], 10)); &nbsp; &nbsp; &nbsp; const value = realDiceInt(m[2], NaN); &nbsp; &nbsp; &nbsp; if (isNaN(value)) return; &nbsp; &nbsp; &nbsp; REAL_DICE_QUEUE[sides] = REAL_DICE_QUEUE[sides] || []; &nbsp; &nbsp; &nbsp; REAL_DICE_QUEUE[sides].push(value); &nbsp; &nbsp; }); &nbsp; }; &nbsp; const realDiceRand = (n) =&gt; { &nbsp; &nbsp; const sides = Math.max(1, realDiceInt(n, 1)); &nbsp; &nbsp; const key = String(sides); &nbsp; &nbsp; let value; &nbsp; &nbsp; if (REAL_DICE_QUEUE[key] &amp;&amp; REAL_DICE_QUEUE[key].length) { &nbsp; &nbsp; &nbsp; value = realDiceInt(REAL_DICE_QUEUE[key].shift(), 1); &nbsp; &nbsp; &nbsp; if (value &lt; 1) value = 1; &nbsp; &nbsp; &nbsp; if (value &gt; sides) value = sides; &nbsp; &nbsp; &nbsp; playDiceSound(); &nbsp; &nbsp; &nbsp; return value; &nbsp; &nbsp; } &nbsp; &nbsp; return randomInteger(sides); &nbsp; }; &nbsp; const rand = (n) =&gt; realDiceRand(n); &nbsp; const toInt = (v, fallback = 0) =&gt; { &nbsp; &nbsp; const n = parseInt(String(v ?? '').replace(/[^\d-]/g, ''), 10); &nbsp; &nbsp; return isNaN(n) ? fallback : n; &nbsp; }; &nbsp; const signed = (n) =&gt; `${n &gt;= 0 ? '+' : ''}${n}`; &nbsp; const parseAction = (content) =&gt; { &nbsp; &nbsp; const parts = content.split(/\s+--/); &nbsp; &nbsp; if (parts.length &lt; 2) return { action: 'help', args: [] }; &nbsp; &nbsp; const bits = parts[1].split('|'); &nbsp; &nbsp; return { &nbsp; &nbsp; &nbsp; action: bits[0], &nbsp; &nbsp; &nbsp; args: bits.slice(1) &nbsp; &nbsp; }; &nbsp; }; &nbsp; const tokenById = (id) =&gt; getObj('graphic', id); &nbsp; const charByToken = (token) =&gt; { &nbsp; &nbsp; if (!token) return null; &nbsp; &nbsp; const cid = token.get('represents'); &nbsp; &nbsp; return cid ? getObj('character', cid) : null; &nbsp; }; &nbsp; const tokenName = (token) =&gt; token ? (token.get('name') || 'Token') : 'Unknown'; &nbsp; const attrObj = (charId, name) =&gt; { &nbsp; &nbsp; const found = findObjs({ &nbsp; &nbsp; &nbsp; type: 'attribute', &nbsp; &nbsp; &nbsp; characterid: charId, &nbsp; &nbsp; &nbsp; name &nbsp; &nbsp; }); &nbsp; &nbsp; return found &amp;&amp; found.length ? found[0] : null; &nbsp; }; &nbsp; const attr = (charId, names, fallback = null) =&gt; { &nbsp; &nbsp; if (!charId) return fallback; &nbsp; &nbsp; for (let i = 0; i &lt; names.length; i++) { &nbsp; &nbsp; &nbsp; const a = attrObj(charId, names[i]); &nbsp; &nbsp; &nbsp; if (a) { &nbsp; &nbsp; &nbsp; &nbsp; const cur = a.get('current'); &nbsp; &nbsp; &nbsp; &nbsp; if (cur !== undefined &amp;&amp; cur !== null &amp;&amp; String(cur).trim() !== '') return cur; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; } &nbsp; &nbsp; return fallback; &nbsp; }; &nbsp; const attrNumber = (charId, names, fallback = 0) =&gt; { &nbsp; &nbsp; const v = attr(charId, names, null); &nbsp; &nbsp; if (v === null) return fallback; &nbsp; &nbsp; return toInt(v, fallback); &nbsp; }; &nbsp; const tokenBar2AC = (token) =&gt; { &nbsp; &nbsp; if (!token) return null; &nbsp; &nbsp; const raw = token.get('bar2_value'); &nbsp; &nbsp; if (raw === undefined || raw === null || String(raw).replace(/^\s+|\s+$/g, '') === '') return null; &nbsp; &nbsp; const n = parseInt(raw, 10); &nbsp; &nbsp; return Number.isNaN(n) ? null : n; &nbsp; }; &nbsp; const baseACFromToken = (token, ch, fallback = 10) =&gt; { &nbsp; &nbsp; const barAC = tokenBar2AC(token); &nbsp; &nbsp; if (barAC !== null) return { &nbsp; &nbsp; &nbsp; value: barAC, &nbsp; &nbsp; &nbsp; source: 'token bar 2 AC' &nbsp; &nbsp; }; &nbsp; &nbsp; return { &nbsp; &nbsp; &nbsp; value: ch ? attrNumber(ch.id, ['armorclass', 'ac', 'AC'], fallback) : fallback, &nbsp; &nbsp; &nbsp; source: ch ? 'armorclass fallback' : `default AC ${fallback}` &nbsp; &nbsp; }; &nbsp; }; &nbsp; const exhaustionLevel = (charId) =&gt; { &nbsp; &nbsp; let level; &nbsp; &nbsp; if (!charId) return 0; &nbsp; &nbsp; /* &nbsp; &nbsp; &nbsp;* GM1E stores exhaustion in state.GM1E.exhaustion by character id. &nbsp; &nbsp; &nbsp;* These combat scripts are separate APIs, so they must read that shared &nbsp; &nbsp; &nbsp;* GM1E state first, then fall back to sheet attributes if present. &nbsp; &nbsp; &nbsp;*/ &nbsp; &nbsp; if ( &nbsp; &nbsp; &nbsp; typeof state !== 'undefined' &amp;&amp; &nbsp; &nbsp; &nbsp; state.GM1E &amp;&amp; &nbsp; &nbsp; &nbsp; state.GM1E.exhaustion &amp;&amp; &nbsp; &nbsp; &nbsp; state.GM1E.exhaustion[charId] !== undefined &nbsp; &nbsp; ) { &nbsp; &nbsp; &nbsp; level = toInt(state.GM1E.exhaustion[charId], 0); &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; level = attrNumber(charId, ['exhaustion', 'exhaustion_level', 'exhaustionlevel'], 0); &nbsp; &nbsp; } &nbsp; &nbsp; return Math.max(0, Math.min(6, level)); &nbsp; }; &nbsp; const exhaustionPenalty = (charId) =&gt; -exhaustionLevel(charId); &nbsp; const exhaustionPercentPenalty = (charId) =&gt; exhaustionPenalty(charId) * 5; &nbsp; const getSessionKey = (attackerId, targetId) =&gt; `${attackerId}:${targetId}`; &nbsp; const getSession = (attackerId, targetId) =&gt; { &nbsp; &nbsp; initState(); &nbsp; &nbsp; return state[SCRIPT].sessions[getSessionKey(attackerId, targetId)] || null; &nbsp; }; &nbsp; const setSession = (session) =&gt; { &nbsp; &nbsp; initState(); &nbsp; &nbsp; state[SCRIPT].sessions[getSessionKey(session.attackerId, session.targetId)] = session; &nbsp; }; &nbsp; const clearSession = (attackerId, targetId) =&gt; { &nbsp; &nbsp; initState(); &nbsp; &nbsp; delete state[SCRIPT].sessions[getSessionKey(attackerId, targetId)]; &nbsp; }; &nbsp; const DISARM_WEAPONS = [ &nbsp; &nbsp; { key: 'knife', label: 'Knife', rank: 1 }, &nbsp; &nbsp; { key: 'dagger', label: 'Dagger', rank: 2 }, &nbsp; &nbsp; { key: 'mace_horseman', label: "Mace, horseman's", rank: 3 }, &nbsp; &nbsp; { key: 'sword_short', label: 'Sword, short', rank: 4 }, &nbsp; &nbsp; { key: 'mace_footman', label: "Mace, footman's", rank: 5 }, &nbsp; &nbsp; { key: 'sword', label: 'Sword: broad, long, falchion, or khopesh', rank: 6 } &nbsp; ]; &nbsp; const weaponByKey = (key) =&gt; DISARM_WEAPONS.find(w =&gt; w.key === key) || DISARM_WEAPONS[1]; &nbsp; const defaultSession = (attackerId, targetId) =&gt; ({ &nbsp; &nbsp; attackerId, &nbsp; &nbsp; targetId, &nbsp; &nbsp; eligibility: { &nbsp; &nbsp; &nbsp; attackerEligibleClass: true, &nbsp; &nbsp; &nbsp; attackerProficient: true, &nbsp; &nbsp; &nbsp; targetUsingAllowedWeapon: true, &nbsp; &nbsp; &nbsp; targetWeaponOneHanded: true &nbsp; &nbsp; }, &nbsp; &nbsp; disarm: { &nbsp; &nbsp; &nbsp; attackerWeapon: 'sword', &nbsp; &nbsp; &nbsp; targetWeapon: 'sword_short', &nbsp; &nbsp; &nbsp; opponentGrappled: false, &nbsp; &nbsp; &nbsp; opponentProne: false, &nbsp; &nbsp; &nbsp; result: '', &nbsp; &nbsp; &nbsp; resultType: 'neutral', &nbsp; &nbsp; &nbsp; resultLines: [] &nbsp; &nbsp; } &nbsp; }); &nbsp; const ensureSessionShape = (session) =&gt; { &nbsp; &nbsp; session.eligibility = session.eligibility || {}; &nbsp; &nbsp; const e = session.eligibility; &nbsp; &nbsp; if (typeof e.attackerEligibleClass === 'undefined') e.attackerEligibleClass = true; &nbsp; &nbsp; if (typeof e.attackerProficient === 'undefined') e.attackerProficient = true; &nbsp; &nbsp; if (typeof e.targetUsingAllowedWeapon === 'undefined') e.targetUsingAllowedWeapon = true; &nbsp; &nbsp; if (typeof e.targetWeaponOneHanded === 'undefined') e.targetWeaponOneHanded = true; &nbsp; &nbsp; session.disarm = session.disarm || {}; &nbsp; &nbsp; if (!weaponByKey(session.disarm.attackerWeapon)) session.disarm.attackerWeapon = 'sword'; &nbsp; &nbsp; if (!weaponByKey(session.disarm.targetWeapon)) session.disarm.targetWeapon = 'sword_short'; &nbsp; &nbsp; if (typeof session.disarm.opponentGrappled === 'undefined') session.disarm.opponentGrappled = false; &nbsp; &nbsp; if (typeof session.disarm.opponentProne === 'undefined') session.disarm.opponentProne = false; &nbsp; &nbsp; if (!session.disarm.result) session.disarm.result = ''; &nbsp; &nbsp; if (!session.disarm.resultType) session.disarm.resultType = 'neutral'; &nbsp; &nbsp; if (!Array.isArray(session.disarm.resultLines)) session.disarm.resultLines = []; &nbsp; &nbsp; return session; &nbsp; }; &nbsp; const attackerChar = (session) =&gt; charByToken(tokenById(session.attackerId)); &nbsp; const targetChar = (session) =&gt; charByToken(tokenById(session.targetId)); &nbsp; const effectiveThac0 = (session) =&gt; { &nbsp; &nbsp; const ch = attackerChar(session); &nbsp; &nbsp; return { &nbsp; &nbsp; &nbsp; value: ch ? attrNumber(ch.id, ['thac0', 'thaco', 'THAC0', 'npc_thac0', 'npc_thaco'], 20) : 20, &nbsp; &nbsp; &nbsp; source: 'Sheet THAC0' &nbsp; &nbsp; }; &nbsp; }; &nbsp; const effectiveToHitBonus = (session) =&gt; { &nbsp; &nbsp; const ch = attackerChar(session); &nbsp; &nbsp; return { &nbsp; &nbsp; &nbsp; value: ch ? attrNumber(ch.id, ['meleebonus', 'attackbonus', 'tohitbonus', 'hitbonus'], 0) : 0, &nbsp; &nbsp; &nbsp; source: 'Sheet melee/to-hit bonus' &nbsp; &nbsp; }; &nbsp; }; &nbsp; const targetAC = (session) =&gt; { &nbsp; &nbsp; const token = tokenById(session.targetId); &nbsp; &nbsp; const ch = targetChar(session); &nbsp; &nbsp; return baseACFromToken(token, ch, 10); &nbsp; }; &nbsp; const petrificationSave = (session) =&gt; { &nbsp; &nbsp; const ch = targetChar(session); &nbsp; &nbsp; return { &nbsp; &nbsp; &nbsp; value: ch ? attrNumber(ch.id, [ &nbsp; &nbsp; &nbsp; &nbsp; 'savepetrificationpolymorph', &nbsp; &nbsp; &nbsp; &nbsp; 'save_petrification_polymorph', &nbsp; &nbsp; &nbsp; &nbsp; 'savepetrification', &nbsp; &nbsp; &nbsp; &nbsp; 'save_petrification', &nbsp; &nbsp; &nbsp; &nbsp; 'save_vs_petrification', &nbsp; &nbsp; &nbsp; &nbsp; 'savevsparalyzationpetrification', &nbsp; &nbsp; &nbsp; &nbsp; 'saveparalyzationpetrification', &nbsp; &nbsp; &nbsp; &nbsp; 'saveparalyzationpoisondeath' &nbsp; &nbsp; &nbsp; ], 20) : 20, &nbsp; &nbsp; &nbsp; source: ch ? 'savepetrificationpolymorph' : 'default save 20' &nbsp; &nbsp; }; &nbsp; }; &nbsp; const attackData = (session) =&gt; { &nbsp; &nbsp; const thac0 = effectiveThac0(session); &nbsp; &nbsp; const toHitBonus = effectiveToHitBonus(session); &nbsp; &nbsp; const aChar = attackerChar(session); &nbsp; &nbsp; const exPenalty = aChar ? exhaustionPenalty(aChar.id) : 0; &nbsp; &nbsp; const ac = targetAC(session); &nbsp; &nbsp; const needed = thac0.value - ac.value; &nbsp; &nbsp; const opponentGrappledBonus = session.disarm.opponentGrappled ? 2 : 0; &nbsp; &nbsp; const opponentProneBonus = session.disarm.opponentProne ? 4 : 0; &nbsp; &nbsp; const totalAttackBonus = toHitBonus.value + opponentGrappledBonus + opponentProneBonus + exPenalty; &nbsp; &nbsp; return { &nbsp; &nbsp; &nbsp; thac0, &nbsp; &nbsp; &nbsp; toHitBonus, &nbsp; &nbsp; &nbsp; exPenalty, &nbsp; &nbsp; &nbsp; ac, &nbsp; &nbsp; &nbsp; needed, &nbsp; &nbsp; &nbsp; opponentGrappledBonus, &nbsp; &nbsp; &nbsp; opponentProneBonus, &nbsp; &nbsp; &nbsp; totalAttackBonus &nbsp; &nbsp; }; &nbsp; }; &nbsp; const modifiedNumberNeeded = (data) =&gt; data.needed - data.totalAttackBonus; &nbsp; const weaponLengthOk = (session) =&gt; { &nbsp; &nbsp; const attackerWeapon = weaponByKey(session.disarm.attackerWeapon); &nbsp; &nbsp; const targetWeapon = weaponByKey(session.disarm.targetWeapon); &nbsp; &nbsp; return attackerWeapon.rank &gt;= targetWeapon.rank; &nbsp; }; &nbsp; const eligibilityPasses = (session) =&gt; { &nbsp; &nbsp; const e = session.eligibility; &nbsp; &nbsp; return !!( &nbsp; &nbsp; &nbsp; e.attackerEligibleClass &amp;&amp; &nbsp; &nbsp; &nbsp; e.attackerProficient &amp;&amp; &nbsp; &nbsp; &nbsp; e.targetUsingAllowedWeapon &amp;&amp; &nbsp; &nbsp; &nbsp; e.targetWeaponOneHanded &amp;&amp; &nbsp; &nbsp; &nbsp; weaponLengthOk(session) &nbsp; &nbsp; ); &nbsp; }; &nbsp; const modifierSummaryText = (data) =&gt; { &nbsp; &nbsp; const parts = []; &nbsp; &nbsp; if (data.toHitBonus.value !== 0) parts.push(`${signed(data.toHitBonus.value)} to-hit`); &nbsp; &nbsp; if (data.exPenalty !== 0) parts.push(`${signed(data.exPenalty)} exhaustion`); &nbsp; &nbsp; if (data.opponentGrappledBonus) parts.push(`${signed(data.opponentGrappledBonus)} opponent grappled`); &nbsp; &nbsp; if (data.opponentProneBonus) parts.push(`${signed(data.opponentProneBonus)} opponent prone`); &nbsp; &nbsp; if (!parts.length) return 'none'; &nbsp; &nbsp; return parts.join('; '); &nbsp; }; &nbsp; const attackRollHtml = (d20, data, total, isHit) =&gt; { &nbsp; &nbsp; const modNeeded = modifiedNumberNeeded(data); &nbsp; &nbsp; let out = ''; &nbsp; &nbsp; out += '&lt;div style="font-size:12px;line-height:1.35;"&gt;'; &nbsp; &nbsp; out += '&lt;div&gt;&lt;b&gt;Attack Roll:&lt;/b&gt; d20 = ' + H.diePill(String(d20)) + '&lt;/div&gt;'; &nbsp; &nbsp; out += '&lt;div&gt;&lt;b&gt;Modifiers:&lt;/b&gt; ' + H.esc(modifierSummaryText(data)) + '&lt;/div&gt;'; &nbsp; &nbsp; out += '&lt;div&gt;&lt;b&gt;Modified Number Needed:&lt;/b&gt; ' + H.pill(String(modNeeded)) + ' or greater&lt;/div&gt;'; &nbsp; &nbsp; out += '&lt;div style="display:block;margin:4px 0;min-height:20px;"&gt;'; &nbsp; &nbsp; out += '&lt;span style="display:inline-block;width:42%;text-align:left;"&gt;&lt;b&gt;Modified Roll:&lt;/b&gt;&lt;/span&gt;'; &nbsp; &nbsp; out += '&lt;span style="display:inline-block;width:18%;text-align:center;"&gt;' + H.resultPill(String(total), isHit) + '&lt;/span&gt;'; &nbsp; &nbsp; out += '&lt;span style="display:inline-block;width:40%;"&gt;&amp;nbsp;&lt;/span&gt;'; &nbsp; &nbsp; out += '&lt;/div&gt;'; &nbsp; &nbsp; out += '&lt;/div&gt;'; &nbsp; &nbsp; return out; &nbsp; }; &nbsp; const saveRollHtml = (roll, saveTarget, success) =&gt; { &nbsp; &nbsp; let out = ''; &nbsp; &nbsp; out += '&lt;div style="font-size:12px;line-height:1.35;"&gt;'; &nbsp; &nbsp; out += '&lt;div&gt;&lt;b&gt;Save vs Petrification:&lt;/b&gt; d20 = ' + H.diePill(String(roll)) + '&lt;/div&gt;'; &nbsp; &nbsp; out += '&lt;div&gt;&lt;b&gt;Save Needed:&lt;/b&gt; ' + H.pill(String(saveTarget)) + ' or greater&lt;/div&gt;'; &nbsp; &nbsp; out += '&lt;div style="display:block;margin:4px 0;min-height:20px;"&gt;'; &nbsp; &nbsp; out += '&lt;span style="display:inline-block;width:42%;text-align:left;"&gt;&lt;b&gt;Save Result:&lt;/b&gt;&lt;/span&gt;'; &nbsp; &nbsp; out += '&lt;span style="display:inline-block;width:18%;text-align:center;"&gt;' + H.resultPill(String(roll), success) + '&lt;/span&gt;'; &nbsp; &nbsp; out += '&lt;span style="display:inline-block;width:40%;"&gt;&amp;nbsp;&lt;/span&gt;'; &nbsp; &nbsp; out += '&lt;/div&gt;'; &nbsp; &nbsp; out += '&lt;/div&gt;'; &nbsp; &nbsp; return out; &nbsp; }; &nbsp; const renderHeader = (session) =&gt; { &nbsp; &nbsp; const attacker = tokenById(session.attackerId); &nbsp; &nbsp; const target = tokenById(session.targetId); &nbsp; &nbsp; return ( &nbsp; &nbsp; &nbsp; H.row(`&lt;b&gt;Attacker:&lt;/b&gt; ${H.esc(tokenName(attacker))}`) + &nbsp; &nbsp; &nbsp; H.row(`&lt;b&gt;Target:&lt;/b&gt; ${H.esc(tokenName(target))}`) &nbsp; &nbsp; ); &nbsp; }; &nbsp; const resultText = (type, text) =&gt; { &nbsp; &nbsp; if (type === 'section') return H.section(text); &nbsp; &nbsp; if (type === 'good') return H.good(text); &nbsp; &nbsp; if (type === 'bad') return H.bad(text); &nbsp; &nbsp; return H.neutral(text); &nbsp; }; &nbsp; const resultLine = (l) =&gt; { &nbsp; &nbsp; if (l.type === 'html') return H.row(l.text); &nbsp; &nbsp; if (l.type === 'section') return resultText(l.type, l.text); &nbsp; &nbsp; return H.row(resultText(l.type || 'neutral', l.text)); &nbsp; }; &nbsp; const setResultLines = (session, lines) =&gt; { &nbsp; &nbsp; session.disarm.resultLines = lines; &nbsp; &nbsp; session.disarm.result = lines.map(l =&gt; l.text || '').join(' '); &nbsp; &nbsp; const hasBad = lines.some(l =&gt; l.type === 'bad'); &nbsp; &nbsp; const hasGood = lines.some(l =&gt; l.type === 'good'); &nbsp; &nbsp; session.disarm.resultType = hasGood &amp;&amp; !hasBad ? 'good' : hasBad ? 'bad' : 'neutral'; &nbsp; }; &nbsp; const clearResults = (session) =&gt; { &nbsp; &nbsp; session.disarm.resultLines = []; &nbsp; &nbsp; session.disarm.result = ''; &nbsp; &nbsp; session.disarm.resultType = 'neutral'; &nbsp; }; &nbsp; const renderResultBlock = (session) =&gt; { &nbsp; &nbsp; const lines = session.disarm.resultLines || []; &nbsp; &nbsp; if (!lines.length &amp;&amp; !session.disarm.result) return ''; &nbsp; &nbsp; let out = H.hr() + H.row('&lt;b&gt;Last Result:&lt;/b&gt;'); &nbsp; &nbsp; if (lines.length) { &nbsp; &nbsp; &nbsp; out += lines.map(resultLine).join(''); &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; out += H.row(resultText(session.disarm.resultType, session.disarm.result)); &nbsp; &nbsp; } &nbsp; &nbsp; out += H.hr(); &nbsp; &nbsp; out += H.row( &nbsp; &nbsp; &nbsp; H.btn('Remove Roll', `${CMD} --clearresult|${session.attackerId}|${session.targetId}`) + &nbsp; &nbsp; &nbsp; H.btn('Back to Character Sheet', `!gm1e-token|${session.attackerId}`) &nbsp; &nbsp; ); &nbsp; &nbsp; return out; &nbsp; }; &nbsp; const renderPublicResultBlock = (session, lines) =&gt; { &nbsp; &nbsp; let out = H.title('Disarm Roll'); &nbsp; &nbsp; out += renderHeader(session); &nbsp; &nbsp; out += H.hr(); &nbsp; &nbsp; for (let i = 0; i &lt; lines.length; i++) { &nbsp; &nbsp; &nbsp; out += resultLine(lines[i]); &nbsp; &nbsp; } &nbsp; &nbsp; out += H.hr(); &nbsp; &nbsp; out += H.row( &nbsp; &nbsp; &nbsp; H.btn('Remove Roll', `${CMD} --clearresult|${session.attackerId}|${session.targetId}`) + &nbsp; &nbsp; &nbsp; H.btn('Back to Character Sheet', `!gm1e-token|${session.attackerId}`) &nbsp; &nbsp; ); &nbsp; &nbsp; return out; &nbsp; }; &nbsp; const sendPublicDisarmResult = (session, lines) =&gt; { &nbsp; &nbsp; sendChat(SCRIPT, H.box(renderPublicResultBlock(session, lines))); &nbsp; }; &nbsp; const renderBool = (session, label, value, action) =&gt; { &nbsp; &nbsp; const btn = value &nbsp; &nbsp; &nbsp; ? H.btnOn('Yes', `${CMD} --${action}|${session.attackerId}|${session.targetId}`) &nbsp; &nbsp; &nbsp; : H.btnOff('No', `${CMD} --${action}|${session.attackerId}|${session.targetId}`); &nbsp; &nbsp; return H.row(`${btn} ${H.esc(label)}`); &nbsp; }; &nbsp; const renderWeaponChoice = (session, who, weapon) =&gt; { &nbsp; &nbsp; const current = who === 'attacker' ? session.disarm.attackerWeapon : session.disarm.targetWeapon; &nbsp; &nbsp; const action = who === 'attacker' ? 'setaweapon' : 'settweapon'; &nbsp; &nbsp; const active = current === weapon.key; &nbsp; &nbsp; return active &nbsp; &nbsp; &nbsp; ? H.btnOn(String(weapon.rank), `${CMD} --${action}|${session.attackerId}|${session.targetId}|${weapon.key}`, weapon.label) &nbsp; &nbsp; &nbsp; : H.btnOff(String(weapon.rank), `${CMD} --${action}|${session.attackerId}|${session.targetId}|${weapon.key}`, weapon.label); &nbsp; }; &nbsp; const renderWeaponLegend = () =&gt; &nbsp; &nbsp; H.subrow(DISARM_WEAPONS.map(w =&gt; `${w.rank}. ${H.esc(w.label)}`).join(' | ')); &nbsp; const renderHelp = (playerId) =&gt; { &nbsp; &nbsp; whisper(playerId, H.box( &nbsp; &nbsp; &nbsp; H.title('Disarm API') + &nbsp; &nbsp; &nbsp; H.row('Use this token-action macro with a selected attacker token:') + &nbsp; &nbsp; &nbsp; H.row(H.pill('!disarm --start|@{selected|token_id}|@{target|Choose disarm target|token_id}')) + &nbsp; &nbsp; &nbsp; H.hr() + &nbsp; &nbsp; &nbsp; H.row(H.small('Only fighters, cavaliers, and sub-classes using an allowed proficient weapon may attempt disarming.')) &nbsp; &nbsp; )); &nbsp; }; &nbsp; const renderMenu = (playerId, session) =&gt; { &nbsp; &nbsp; session = ensureSessionShape(session); &nbsp; &nbsp; const data = attackData(session); &nbsp; &nbsp; const save = petrificationSave(session); &nbsp; &nbsp; const aWeapon = weaponByKey(session.disarm.attackerWeapon); &nbsp; &nbsp; const tWeapon = weaponByKey(session.disarm.targetWeapon); &nbsp; &nbsp; const eligible = eligibilityPasses(session); &nbsp; &nbsp; const weaponBtnsAttacker = DISARM_WEAPONS.map(w =&gt; renderWeaponChoice(session, 'attacker', w)).join(''); &nbsp; &nbsp; const weaponBtnsTarget = DISARM_WEAPONS.map(w =&gt; renderWeaponChoice(session, 'target', w)).join(''); &nbsp; &nbsp; const html = H.box( &nbsp; &nbsp; &nbsp; H.title('Disarming') + &nbsp; &nbsp; &nbsp; renderHeader(session) + &nbsp; &nbsp; &nbsp; H.hr() + &nbsp; &nbsp; &nbsp; H.section('Attack Matrix') + &nbsp; &nbsp; &nbsp; H.row(`&lt;b&gt;THAC0 ${H.pill(data.thac0.value)} vs AC ${H.pill(data.ac.value)} = Need ${H.pill(data.needed)}&lt;/b&gt;`) + &nbsp; &nbsp; &nbsp; H.subrow(`${H.esc(data.thac0.source)}; ${H.esc(data.ac.source)}.`) + &nbsp; &nbsp; &nbsp; H.row(`&lt;b&gt;To-Hit:&lt;/b&gt; ${H.pill(signed(data.totalAttackBonus))} ${H.small(data.toHitBonus.source)} | &lt;b&gt;Modified Needed:&lt;/b&gt; ${H.pill(modifiedNumberNeeded(data))} or greater`) + &nbsp; &nbsp; &nbsp; H.row(`&lt;b&gt;Defender Save:&lt;/b&gt; ${H.pill(save.value)} ${H.small(save.source)}`) + &nbsp; &nbsp; &nbsp; H.hr() + &nbsp; &nbsp; &nbsp; H.section('Weapon Length') + &nbsp; &nbsp; &nbsp; H.row(`&lt;b&gt;Attacker Weapon:&lt;/b&gt; ${weaponBtnsAttacker}`) + &nbsp; &nbsp; &nbsp; H.row(`&lt;b&gt;Target Weapon:&lt;/b&gt; ${weaponBtnsTarget}`) + &nbsp; &nbsp; &nbsp; renderWeaponLegend() + &nbsp; &nbsp; &nbsp; H.row(`&lt;b&gt;Current:&lt;/b&gt; ${H.infoPill(aWeapon.label)} vs ${H.infoPill(tWeapon.label)} ${weaponLengthOk(session) ? H.good('Eligible length') : H.bad('Too short to disarm target weapon')}`) + &nbsp; &nbsp; &nbsp; H.hr() + &nbsp; &nbsp; &nbsp; H.section('Disarm Modifiers') + &nbsp; &nbsp; &nbsp; H.row(`${session.disarm.opponentGrappled ? H.btnOn('Yes', `${CMD} --togoppgrappled|${session.attackerId}|${session.targetId}`) : H.btnOff('No', `${CMD} --togoppgrappled|${session.attackerId}|${session.targetId}`)} Opponent grappled ${H.pill('+2')}`) + &nbsp; &nbsp; &nbsp; H.row(`${session.disarm.opponentProne ? H.btnOn('Yes', `${CMD} --togoppprone|${session.attackerId}|${session.targetId}`) : H.btnOff('No', `${CMD} --togoppprone|${session.attackerId}|${session.targetId}`)} Opponent prone ${H.pill('+4')}`) + &nbsp; &nbsp; &nbsp; H.hr() + &nbsp; &nbsp; &nbsp; H.section('Eligibility') + &nbsp; &nbsp; &nbsp; renderBool(session, 'Attacker is fighter, cavalier, paladin, ranger, or valid sub-class', session.eligibility.attackerEligibleClass, 'togclass') + &nbsp; &nbsp; &nbsp; renderBool(session, 'Attacker is proficient with the disarming weapon', session.eligibility.attackerProficient, 'togprof') + &nbsp; &nbsp; &nbsp; renderBool(session, 'Target is using an allowed disarmable weapon', session.eligibility.targetUsingAllowedWeapon, 'togtargetweapon') + &nbsp; &nbsp; &nbsp; renderBool(session, 'Target weapon does not require two hands', session.eligibility.targetWeaponOneHanded, 'togonehanded') + &nbsp; &nbsp; &nbsp; H.row(`&lt;b&gt;Eligibility:&lt;/b&gt; ${eligible ? H.good('Eligible to disarm.') : H.bad('Not eligible yet.')}`) + &nbsp; &nbsp; &nbsp; H.hr() + &nbsp; &nbsp; &nbsp; H.row( &nbsp; &nbsp; &nbsp; &nbsp; H.btn('Roll Disarm', `${CMD} --rolldisarm|${session.attackerId}|${session.targetId}` + realDiceSuffix(session.attackerId, [{sides:20,label:'d20 disarm attack',defaultValue:10}])) + &nbsp; &nbsp; &nbsp; &nbsp; H.btn('Rules', `${CMD} --rules|${session.attackerId}|${session.targetId}`) + &nbsp; &nbsp; &nbsp; &nbsp; H.btn('Reset', `${CMD} --reset|${session.attackerId}|${session.targetId}`) &nbsp; &nbsp; &nbsp; ) + &nbsp; &nbsp; &nbsp; H.hr() + &nbsp; &nbsp; &nbsp; H.row(H.btn('Back to Character Sheet', `!gm1e-token|${session.attackerId}`)) + &nbsp; &nbsp; &nbsp; renderResultBlock(session) &nbsp; &nbsp; ); &nbsp; &nbsp; whisper(playerId, html); &nbsp; }; &nbsp; const renderRules = (playerId, session) =&gt; { &nbsp; &nbsp; const html = H.box( &nbsp; &nbsp; &nbsp; H.title('Disarming Rules Summary') + &nbsp; &nbsp; &nbsp; H.row('• Disarming may be used only by fighters, cavaliers, and their sub-classes such as paladins and rangers.') + &nbsp; &nbsp; &nbsp; H.row('• The attacker must be proficient with the weapon used to disarm.') + &nbsp; &nbsp; &nbsp; H.row('• The defender must also be using one of the allowed weapons.') + &nbsp; &nbsp; &nbsp; H.row('• A weapon requiring two hands can never be affected by this disarming attack.') + &nbsp; &nbsp; &nbsp; H.hr() + &nbsp; &nbsp; &nbsp; H.row('• Allowed weapons by increasing length: knife, dagger, horseman’s mace, short sword, footman’s mace, broad/long/falchion/khopesh sword.') + &nbsp; &nbsp; &nbsp; H.row('• A weapon can disarm only weapons of the same length or shorter.') + &nbsp; &nbsp; &nbsp; H.row('• A successful disarm attack inflicts no damage.') + &nbsp; &nbsp; &nbsp; H.hr() + &nbsp; &nbsp; &nbsp; H.row('• On a successful hit, the defender saves vs petrification.') + &nbsp; &nbsp; &nbsp; H.row('• If the save fails, the weapon is lost.') + &nbsp; &nbsp; &nbsp; H.row('• Same-length weapons fall at the owner’s feet. Shorter weapons are knocked 1-10 feet away using location 1-9.') + &nbsp; &nbsp; &nbsp; H.hr() + &nbsp; &nbsp; &nbsp; H.row( &nbsp; &nbsp; &nbsp; &nbsp; H.btn('Disarm Menu', `${CMD} --menu|${session.attackerId}|${session.targetId}`) + &nbsp; &nbsp; &nbsp; &nbsp; H.btn('Back to Character Sheet', `!gm1e-token|${session.attackerId}`) &nbsp; &nbsp; &nbsp; ) &nbsp; &nbsp; ); &nbsp; &nbsp; whisper(playerId, html); &nbsp; }; &nbsp; const requireSession = (playerId, attackerId, targetId) =&gt; { &nbsp; &nbsp; const attacker = tokenById(attackerId); &nbsp; &nbsp; const target = tokenById(targetId); &nbsp; &nbsp; if (!attacker || !target) { &nbsp; &nbsp; &nbsp; whisper(playerId, H.box( &nbsp; &nbsp; &nbsp; &nbsp; H.title('Disarming Error') + &nbsp; &nbsp; &nbsp; &nbsp; H.row(H.bad('Missing attacker or target token. Use this token-action macro:')) + &nbsp; &nbsp; &nbsp; &nbsp; H.row(H.pill('!disarm --start|@{selected|token_id}|@{target|Choose disarm target|token_id}')) &nbsp; &nbsp; &nbsp; )); &nbsp; &nbsp; &nbsp; return null; &nbsp; &nbsp; } &nbsp; &nbsp; let session = getSession(attackerId, targetId); &nbsp; &nbsp; if (!session) { &nbsp; &nbsp; &nbsp; session = defaultSession(attackerId, targetId); &nbsp; &nbsp; &nbsp; setSession(session); &nbsp; &nbsp; } &nbsp; &nbsp; return ensureSessionShape(session); &nbsp; }; &nbsp; const toggleEligibility = (session, field) =&gt; { &nbsp; &nbsp; session.eligibility[field] = !session.eligibility[field]; &nbsp; &nbsp; clearResults(session); &nbsp; &nbsp; setSession(session); &nbsp; }; &nbsp; const rollDisarm = (playerId, session) =&gt; { &nbsp; &nbsp; session = ensureSessionShape(session); &nbsp; &nbsp; const lines = []; &nbsp; &nbsp; if (!eligibilityPasses(session)) { &nbsp; &nbsp; &nbsp; lines.push({ type: 'section', text: 'Not Eligible' }); &nbsp; &nbsp; &nbsp; lines.push({ type: 'bad', text: 'Disarm attack not rolled.' }); &nbsp; &nbsp; &nbsp; if (!session.eligibility.attackerEligibleClass) lines.push({ type: 'bad', text: 'Attacker is not marked as an eligible class/sub-class.' }); &nbsp; &nbsp; &nbsp; if (!session.eligibility.attackerProficient) lines.push({ type: 'bad', text: 'Attacker is not marked proficient with the disarming weapon.' }); &nbsp; &nbsp; &nbsp; if (!session.eligibility.targetUsingAllowedWeapon) lines.push({ type: 'bad', text: 'Target is not marked as using an allowed disarmable weapon.' }); &nbsp; &nbsp; &nbsp; if (!session.eligibility.targetWeaponOneHanded) lines.push({ type: 'bad', text: 'Target weapon requires two hands and cannot be disarmed by this attack.' }); &nbsp; &nbsp; &nbsp; if (!weaponLengthOk(session)) lines.push({ type: 'bad', text: 'The attacker’s weapon is shorter than the target weapon.' }); &nbsp; &nbsp; &nbsp; setResultLines(session, lines); &nbsp; &nbsp; &nbsp; setSession(session); &nbsp; &nbsp; &nbsp; return renderMenu(playerId, session); &nbsp; &nbsp; } &nbsp; &nbsp; const data = attackData(session); &nbsp; &nbsp; const save = petrificationSave(session); &nbsp; &nbsp; const aWeapon = weaponByKey(session.disarm.attackerWeapon); &nbsp; &nbsp; const tWeapon = weaponByKey(session.disarm.targetWeapon); &nbsp; &nbsp; playDiceSound(); &nbsp; &nbsp; const d20 = rand(20); &nbsp; &nbsp; const total = d20 + data.totalAttackBonus; &nbsp; &nbsp; const hit = d20 &gt;= modifiedNumberNeeded(data); &nbsp; &nbsp; lines.push({ type: 'section', text: 'Disarm Attack' }); &nbsp; &nbsp; lines.push({ type: 'neutral', text: `Attacker: ${tokenName(tokenById(session.attackerId))}; Target: ${tokenName(tokenById(session.targetId))}.` }); &nbsp; &nbsp; lines.push({ type: 'neutral', text: `Weapon used: ${aWeapon.label}; target weapon: ${tWeapon.label}.` }); &nbsp; &nbsp; lines.push({ type: 'neutral', text: `THAC0 ${data.thac0.value} vs AC ${data.ac.value}; matrix number ${data.needed}.` }); &nbsp; &nbsp; lines.push({ type: 'html', text: attackRollHtml(d20, data, total, hit) }); &nbsp; &nbsp; if (!hit) { &nbsp; &nbsp; &nbsp; lines.push({ type: 'bad', text: 'Miss. No disarm attempt is forced.' }); &nbsp; &nbsp; &nbsp; setResultLines(session, lines); &nbsp; &nbsp; &nbsp; setSession(session); &nbsp; &nbsp; &nbsp; sendPublicDisarmResult(session, lines); &nbsp; &nbsp; &nbsp; return renderMenu(playerId, session); &nbsp; &nbsp; } &nbsp; &nbsp; lines.push({ type: 'good', text: 'Hit. No damage is inflicted; defender must save vs petrification or lose the weapon.' }); &nbsp; &nbsp; const saveRoll = rand(20); &nbsp; &nbsp; const defenderExPenalty = save.tChar ? exhaustionPenalty(save.tChar.id) : 0; &nbsp; &nbsp; const saveTotal = saveRoll + defenderExPenalty; &nbsp; &nbsp; const saveSuccess = saveTotal &gt;= save.value; &nbsp; &nbsp; lines.push({ type: 'html', text: saveRollHtml(saveTotal, save.value, saveSuccess) }); &nbsp; &nbsp; if (defenderExPenalty !== 0) lines.push({ type: 'neutral', text: `Defender exhaustion penalty applied to save: ${signed(defenderExPenalty)}.` }); &nbsp; &nbsp; if (saveSuccess) { &nbsp; &nbsp; &nbsp; lines.push({ type: 'good', text: 'Save succeeds. Defender keeps the weapon.' }); &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; lines.push({ type: 'bad', text: 'Save fails. Defender is disarmed.' }); &nbsp; &nbsp; &nbsp; if (aWeapon.rank === tWeapon.rank) { &nbsp; &nbsp; &nbsp; &nbsp; lines.push({ type: 'neutral', text: 'Weapon location: falls at the owner’s feet.' }); &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; const distance = rand(10); &nbsp; &nbsp; &nbsp; &nbsp; const location = rand(9); &nbsp; &nbsp; &nbsp; &nbsp; lines.push({ type: 'section', text: 'Weapon Scatter' }); &nbsp; &nbsp; &nbsp; &nbsp; lines.push({ type: 'neutral', text: `Weapon knocked ${distance} feet away.` }); &nbsp; &nbsp; &nbsp; &nbsp; lines.push({ type: 'neutral', text: `Location array result: ${location} of 9.` }); &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; lines.push({ type: 'neutral', text: 'Former wielder must spend a round to recover it, draw another weapon, or use weaponless combat.' }); &nbsp; &nbsp; } &nbsp; &nbsp; setResultLines(session, lines); &nbsp; &nbsp; setSession(session); &nbsp; &nbsp; sendPublicDisarmResult(session, lines); &nbsp; &nbsp; return renderMenu(playerId, session); &nbsp; }; &nbsp; on('chat:message', (msg) =&gt; { &nbsp; &nbsp; if (msg.type !== 'api' || !msg.content.startsWith(CMD)) return; &nbsp; &nbsp; initState(); &nbsp; &nbsp; const parsed = parseAction(msg.content); &nbsp; &nbsp; const action = parsed.action; &nbsp; &nbsp; const args = parsed.args; &nbsp; &nbsp; if (action === 'help') return renderHelp(msg.playerid); &nbsp; &nbsp; if (action === 'start') { &nbsp; &nbsp; &nbsp; const attackerId = args[0] || (msg.selected &amp;&amp; msg.selected.length ? msg.selected[0]._id : ''); &nbsp; &nbsp; &nbsp; const targetId = args[1]; &nbsp; &nbsp; &nbsp; const attacker = tokenById(attackerId); &nbsp; &nbsp; &nbsp; const target = tokenById(targetId); &nbsp; &nbsp; &nbsp; if (!attacker || !target || attackerId === targetId) { &nbsp; &nbsp; &nbsp; &nbsp; return whisper(msg.playerid, H.box( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; H.title('Disarming Error') + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; H.row(H.bad('Choose a valid disarm target.')) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; H.row(H.macroBtn('Choose Disarm Target', `${CMD} --start|${attackerId}|@{target|Choose disarm target|token_id}`)) &nbsp; &nbsp; &nbsp; &nbsp; )); &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; const session = defaultSession(attackerId, targetId); &nbsp; &nbsp; &nbsp; setSession(session); &nbsp; &nbsp; &nbsp; return renderMenu(msg.playerid, session); &nbsp; &nbsp; } &nbsp; &nbsp; const attackerId = args[0]; &nbsp; &nbsp; const targetId = args[1]; &nbsp; &nbsp; let session = requireSession(msg.playerid, attackerId, targetId); &nbsp; &nbsp; if (!session) return; &nbsp; &nbsp; if (action === 'menu') return renderMenu(msg.playerid, session); &nbsp; &nbsp; if (action === 'rules') return renderRules(msg.playerid, session); &nbsp; &nbsp; if (action === 'reset') { &nbsp; &nbsp; &nbsp; clearSession(attackerId, targetId); &nbsp; &nbsp; &nbsp; session = defaultSession(attackerId, targetId); &nbsp; &nbsp; &nbsp; setSession(session); &nbsp; &nbsp; &nbsp; return renderMenu(msg.playerid, session); &nbsp; &nbsp; } &nbsp; &nbsp; if (action === 'clearresult') { &nbsp; &nbsp; &nbsp; clearResults(session); &nbsp; &nbsp; &nbsp; setSession(session); &nbsp; &nbsp; &nbsp; return renderMenu(msg.playerid, session); &nbsp; &nbsp; } &nbsp; &nbsp; if (action === 'togclass') { &nbsp; &nbsp; &nbsp; toggleEligibility(session, 'attackerEligibleClass'); &nbsp; &nbsp; &nbsp; return renderMenu(msg.playerid, session); &nbsp; &nbsp; } &nbsp; &nbsp; if (action === 'togprof') { &nbsp; &nbsp; &nbsp; toggleEligibility(session, 'attackerProficient'); &nbsp; &nbsp; &nbsp; return renderMenu(msg.playerid, session); &nbsp; &nbsp; } &nbsp; &nbsp; if (action === 'togtargetweapon') { &nbsp; &nbsp; &nbsp; toggleEligibility(session, 'targetUsingAllowedWeapon'); &nbsp; &nbsp; &nbsp; return renderMenu(msg.playerid, session); &nbsp; &nbsp; } &nbsp; &nbsp; if (action === 'togonehanded') { &nbsp; &nbsp; &nbsp; toggleEligibility(session, 'targetWeaponOneHanded'); &nbsp; &nbsp; &nbsp; return renderMenu(msg.playerid, session); &nbsp; &nbsp; } &nbsp; &nbsp; if (action === 'togoppgrappled') { &nbsp; &nbsp; &nbsp; session.disarm.opponentGrappled = !session.disarm.opponentGrappled; &nbsp; &nbsp; &nbsp; clearResults(session); &nbsp; &nbsp; &nbsp; setSession(session); &nbsp; &nbsp; &nbsp; return renderMenu(msg.playerid, session); &nbsp; &nbsp; } &nbsp; &nbsp; if (action === 'togoppprone') { &nbsp; &nbsp; &nbsp; session.disarm.opponentProne = !session.disarm.opponentProne; &nbsp; &nbsp; &nbsp; clearResults(session); &nbsp; &nbsp; &nbsp; setSession(session); &nbsp; &nbsp; &nbsp; return renderMenu(msg.playerid, session); &nbsp; &nbsp; } &nbsp; &nbsp; if (action === 'setaweapon') { &nbsp; &nbsp; &nbsp; const w = weaponByKey(args[2]); &nbsp; &nbsp; &nbsp; session.disarm.attackerWeapon = w.key; &nbsp; &nbsp; &nbsp; clearResults(session); &nbsp; &nbsp; &nbsp; setSession(session); &nbsp; &nbsp; &nbsp; return renderMenu(msg.playerid, session); &nbsp; &nbsp; } &nbsp; &nbsp; if (action === 'settweapon') { &nbsp; &nbsp; &nbsp; const w = weaponByKey(args[2]); &nbsp; &nbsp; &nbsp; session.disarm.targetWeapon = w.key; &nbsp; &nbsp; &nbsp; clearResults(session); &nbsp; &nbsp; &nbsp; setSession(session); &nbsp; &nbsp; &nbsp; return renderMenu(msg.playerid, session); &nbsp; &nbsp; } &nbsp; &nbsp; if (action === 'rolldisarm') return rollDisarm(msg.playerid, session); &nbsp; &nbsp; return renderMenu(msg.playerid, session); &nbsp; }); &nbsp; on('ready', () =&gt; { &nbsp; &nbsp; initState(); &nbsp; &nbsp; log(`${SCRIPT} v${VERSION} ready. Token-action macro: ${CMD} --start|@{selected|token_id}|@{target|Choose disarm target|token_id}`); &nbsp; }); })(); August 22 (1 week ago) ROTTO Elite Permalink Quote Report Edit Delete Grenade:&nbsp;&nbsp; <a href="https://drive.google.com/file/d/1JkYqqnXj-o4u_X7sZSzbzhVEwiiGkbmW/view?usp=sharing" rel="nofollow">https://drive.google.com/file/d/1JkYqqnXj-o4u_X7sZSzbzhVEwiiGkbmW/view?usp=sharing</a> August 22 (1 week ago) ROTTO Elite Permalink Quote Report Edit Delete Start with these and see how they work.&nbsp; August 22 (1 week ago) ROTTO Elite Permalink Quote Report Edit Delete You should start a new game to load these mods since it will wipe out your current character sheets. You need to load token mod Place this in the Character sheet HTML section <a href="https://drive.google.com/file/d/1x4aqxm33s6kgFt_fEPbsyKutBDA82bWh/view?usp=sharing" rel="nofollow">https://drive.google.com/file/d/1x4aqxm33s6kgFt_fEPbsyKutBDA82bWh/view?usp=sharing</a> Place this in the Character Sheet CCS Section&nbsp; <a href="https://drive.google.com/file/d/1tlChz9Q8v8hbPSraT8bZVcEOCDCOEfzV/view?usp=sharing" rel="nofollow">https://drive.google.com/file/d/1tlChz9Q8v8hbPSraT8bZVcEOCDCOEfzV/view?usp=sharing</a>