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