
Custom Armor API Script - Roll20 DnD 5e
If you are a roll20 pro user. STRONGLY recommended, you can add API's to your game allowing you to spend less time on prep.
Below is the FA-Heavy-Armor script which should be added to your API. Just copy the text below and name it and hit save. This will allow you to import things like Robes, Heavy Armor, and Unarmored AC easily by importing the code. !armor:15con:plate for example will change your AC to 15 + CON modifier and create Plate armor in your inventory. !clear plate will remove it from your inventory, and !clear inventory will clear all items in your inventory.
/* ===============================
Familiar Arcana Custom Armor
Command format:
!armor:15con:chain mail
!armor:10cha:robes wisdom
=============================== */
function titleCase(str) {
return str.replace(/\w\S*/g, t => t.charAt(0).toUpperCase() + t.slice(1));
}
function normalizeArmorName(rawName) {
const STAT_NAMES = [
'strength',
'dexterity',
'constitution',
'intelligence',
'wisdom',
'charisma'
];
const lower = rawName.toLowerCase().trim();
if (lower.startsWith('robes ')) {
const stat = lower.replace('robes ', '').trim();
if (STAT_NAMES.includes(stat)) {
return `Robes (${titleCase(stat)})`;
}
}
return titleCase(rawName);
}
on('chat:message', function (msg) {
if (msg.type !== 'api') return;
const content = msg.content.trim();
/* ---------- ARMOR COMMAND ---------- */
if (content.toLowerCase().startsWith('!armor:')) {
if (!msg.selected !msg.selected.length) {
sendChat('Armor', '/w gm Select a token first.');
return;
}
const parts = content.split(':');
if (parts.length < 3) {
sendChat('Armor', '/w gm Format must be !armor:15con:chain mail');
return;
}
const baseStatPart = parts[1].toLowerCase();
const armorNameRaw = parts.slice(2).join(':').trim();
const baseMatch = baseStatPart.match(/^(\d+)/);
if (!baseMatch) {
sendChat('Armor', '/w gm Missing base AC.');
return;
}
const baseAC = parseInt(baseMatch[1], 10);
const statBlock = baseStatPart.slice(baseMatch[1].length);
const STAT_MAP = {
str: 'Strength',
dex: 'Dexterity',
con: 'Constitution',
int: 'Intelligence',
wis: 'Wisdom',
cha: 'Charisma'
};
const stats = [];
Object.keys(STAT_MAP).forEach(k => {
if (statBlock.includes(k)) stats.push(STAT_MAP[k]);
});
const armorName = normalizeArmorName(armorNameRaw 'Custom Armor');
/* ---------- ARMOR DATA ---------- */
const ARMOR_DATA = {
'robes wisdom': { cost: '20 gp', weight: '1', props: '' },
'robes intelligence': { cost: '20 gp', weight: '1', props: '' },
'robes charisma': { cost: '20 gp', weight: '1', props: '' },
'robes constitution': { cost: '20 gp', weight: '1', props: '' },
'robes dexterity': { cost: '20 gp', weight: '1', props: '' },
'robes strength': { cost: '20 gp', weight: '1', props: '' },
'plate': { cost: '1500 gp', weight: '65', props: 'Stealth: Disadvantage' },
'chain mail': { cost: '75 gp', weight: '55', props: 'Stealth: Disadvantage' },
'chain': { cost: '75 gp', weight: '55', props: 'Stealth: Disadvantage' },
'splint': { cost: '200 gp', weight: '60', props: 'Stealth: Disadvantage' },
'ring': { cost: '30 gp', weight: '40', props: 'Stealth: Disadvantage' }
};
const lookupKey = armorNameRaw.toLowerCase();
const fallbackKey = lookupKey.split(' ')[0];
const data =
ARMOR_DATA[lookupKey]
ARMOR_DATA[fallbackKey]
{ cost: '', weight: '', props: '' };
msg.selected.forEach(sel => {
const token = getObj('graphic', sel._id);
if (!token) return;
const charId = token.get('represents');
if (!charId) return;
function setAttr(name, value) {
let attr = findObjs({ type: 'attribute', characterid: charId, name })[0];
if (!attr) {
createObj('attribute', { characterid: charId, name, current: value });
} else {
attr.set('current', value);
}
}
/* ---------- CREATE INVENTORY ITEM ---------- */
const rowId = Math.random().toString(36).substring(2, 10);
const invAttrs = {
itemname: armorName,
itemvalue: data.cost,
itemweight: data.weight,
itemproperties: data.props,
itemmodifiers: 'Item Type: Armor',
itemcontent: `While wearing ${armorName.startsWith('Robes') ? 'these' : 'this'} ${armorName}, your base armor class is ${baseAC}${stats.length ? ' + your ' + stats.join(' modifier + your ') + ' modifier' : ''}.`,
itemcount: '1',
equipped: '1',
itemtype: 'armor',
hasattack: '0',
useasresource: '0',
itemdelete: '0'
};
Object.keys(invAttrs).forEach(k => {
createObj('attribute', {
characterid: charId,
name: `repeating_inventory_${rowId}_${k}`,
current: invAttrs[k]
});
});
/* ---------- APPLY CUSTOM AC ---------- */
setAttr('custom_ac_flag', '1');
setAttr('custom_ac_part1', stats[0] 'none');
setAttr('custom_ac_part2', stats[1] 'none');
setAttr('custom_ac_shield', 'yes');
setAttr('custom_ac_base', '');
setAttr('custom_ac_base', baseAC);
sendChat(
'Armor',
`/w gm ${armorName} applied. AC ${baseAC}${stats.length ? ' + ' + stats.join(' + ') : ''}`
);
});
return;
}
/* ---------- CLEAR ALL INVENTORY ---------- */
if (content.toLowerCase() === '!clearinventory') {
if (!msg.selected) return;
msg.selected.forEach(sel => {
const token = getObj('graphic', sel._id);
if (!token) return;
const charId = token.get('represents');
if (!charId) return;
findObjs({ type: 'attribute', characterid: charId }).forEach(a => {
if (a.get('name').startsWith('repeating_inventory_')) {
a.remove();
}
});
sendChat('Inventory', '/w gm Inventory cleared.');
});
return;
}
/* ---------- CLEAR SPECIFIC ITEM ---------- */
if (content.toLowerCase().startsWith('!clear ')) {
const target = content.slice(7).toLowerCase();
if (!msg.selected !target) return;
msg.selected.forEach(sel => {
const token = getObj('graphic', sel._id);
if (!token) return;
const charId = token.get('represents');
if (!charId) return;
const attrs = findObjs({ type: 'attribute', characterid: charId });
const rows = {};
attrs.forEach(a => {
const m = a.get('name').match(/^repeating_inventory_([^_]+)_itemname$/);
if (m) rows[m[1]] = a.get('current').toLowerCase();
});
Object.keys(rows).forEach(rowId => {
if (rows[rowId].includes(target)) {
attrs.forEach(a => {
if (a.get('name').startsWith(`repeating_inventory_${rowId}_`)) {
a.remove();
}
});
}
});
sendChat('Inventory', `/w gm Removed items matching "${target}".`);
});
}
});

