Skip to content

Instantly share code, notes, and snippets.

@MichaelMackus
Last active June 23, 2026 01:31
Show Gist options
  • Select an option

  • Save MichaelMackus/c84fee5f5acfe24a7627d0a53e3e9585 to your computer and use it in GitHub Desktop.

Select an option

Save MichaelMackus/c84fee5f5acfe24a7627d0a53e3e9585 to your computer and use it in GitHub Desktop.
roll20backup
class Attributes {
strength;
intelligence;
wisdom;
dexterity;
constitution;
charisma;
constructor(str, int, wis, dex, con, cha) {
this.strength = str;
this.intelligence = int;
this.wisdom = wis;
this.dexterity = dex;
this.constitution = con;
this.charisma = cha;
}
}
/**
* Character inventory
*/
class Inventory {
weapons = []; // Weapon[]
gear = []; // Item[]
armor = []; // Weapon[]
coins; // Coins
encumbrance;
}
class Weapon {
weaponName;
attackType;
weaponAttackBonus;
weaponDamageDie;
weaponDamageBonus;
weaponWeight;
weaponDescription;
static header() {
return [
'name',
'type',
'attack bonus',
'damage die',
'damage bonus',
'weight',
'description',
].join(' | ');
}
toString() {
return [
this.weaponName,
this.attackType,
this.weaponAttackBonus,
this.weaponDamageDie,
this.weaponDamageBonus,
this.weaponWeight,
this.weaponDescription,
].join(' | ');
}
}
class Item {
// generic item ("Gear")
ItemName;
ItemDescription;
static header() {
return [
'name',
'description',
].join(' | ');
}
toString() {
return [
this.ItemName,
this.ItemDescription,
].join(' | ');
}
}
class Armor {
armorName;
armorValue;
armorWeight;
armorWorn;
static header() {
return [
'name',
'value',
'weight',
'worn',
].join(' | ');
}
toString() {
return [
this.armorName,
this.armorValue,
this.armorWeight,
this.armorWorn === "on" ? "yes" : "",
].join(' | ');
}
}
class Coins {
pp;
gp;
ep;
sp;
cp;
}
class CharacterSheet {
name;
role;
avatar;
alignment;
attributes;
experience;
level;
// combat
hp;
max_hp;
ac;
thac0;
// gear & other
inventory;
// TODO spells
// TODO abilities & skills
// TODO notes
// TODO bio (has ID as key "bio" in the char obj)
toString() {
const lines = [].concat(
[
`Name: ${this.name}`, `Class: ${this.role}`, `Alignment: ${this.alignment}`, `Level: ${this.level}`, `Experience: ${this.experience}`,
``,
`HP: ${this.hp} / ${this.max_hp}`, `AC: ${this.ac} THAC0: ${this.thac0}`,
``,
`Strength: ${this.attributes.strength}`, `Intelligence: ${this.attributes.intelligence}`, `Wisdom: ${this.attributes.wisdom}`, `Dexterity: ${this.attributes.dexterity}`, `Constitution: ${this.attributes.constitution}`, `Charisma: ${this.attributes.charisma}`,
],
['', 'Armor:', Armor.header()],
this.inventory.armor.map((item) => item.toString()),
['', 'Weapons:', Weapon.header()],
this.inventory.weapons.map((item) => item.toString()),
['', 'Gear:', Item.header()],
this.inventory.gear.map((item) => item.toString()),
['', 'Coins:'],
[`PP: ${this.inventory.coins.pp} | GP: ${this.inventory.coins.gp} | EP: ${this.inventory.coins.ep} | SP: ${this.inventory.coins.sp} | CP: ${this.inventory.coins.cp}`],
);
return lines.map(l => `<code>${l}</code>`).join('<br>')
}
}
on('ready',function() {
"use strict";
on('chat:message', (msg) => {
// Usage: ![backup|bak] [QUERY|*]
if('api' === msg.type && /^!(backup|bak)/i.test(msg.content)) {
const query = msg.content.split(' ').slice(1).join(' ');
// lookup player
const player = getObj('player', msg.playerid);
if (!player || 'error' in player || !player.get('_displayname')) throw "Error finding player";
// lookup characters
let character_sheets = [];
if (query.trim().length == 0) {
usage(player);
return;
} else if (query == '*') {
character_sheets = find_character_sheets(msg.playerid, '');
} else {
character_sheets = find_character_sheets(msg.playerid, query);
}
// error checking
if (character_sheets.length == 0) {
sendChat('Harondalbar', `/w "${player.get('_displayname')}" No character with that name found.`, null, { noarchive: true });
if (state[msg.playerid] > 0) egg(player, state[msg.playerid]);
state[msg.playerid]++;
return;
}
state[msg.playerid] = 0;
// send message to user
const output = character_sheets.map(sheet => sheet.toString()).join('<hr style="background-color: black; border-color: black;">');
sendChat('Harondalbar', `/w "${player.get('_displayname')}"<br><br>${output}`);
// TODO any way to interact with "state" variable from browser?
// TODO technically should be possible via the "external journal"
// TODO this way I can use an external API to pull the character's journal from roll20, and then send the user an email/something
//
// see: https://app.roll20.net/forum/post/6572042/possible-to-access-roll20-character-info-from-external-site-slash-app
}
});
function usage(player) {
sendChat('Harondalbar', `/w "${player.get('_displayname')}" <br>Usage: !backup QUERY<br>Usage: !bak QUERY<br><br>You can either search by providing a character\'s name to query, or by querying all your characters with an asterisk.`, null, { noarchive: true });
}
function find_character_sheets(player_id, query) {
return find_characters(player_id, query).map(c => get_character_sheet(c.id));
}
function find_characters(player_id, query) {
return findObjs({ type: 'character' }).filter((c) => {
let in_journal = c.get('inplayerjournals') == 'all' || c.get('inplayerjournals').split(',').includes(player_id);
return (playerIsGM(player_id) || in_journal) && c.get('name').toLowerCase().includes(query);
});
}
function get_character_sheet(id) {
const character = getObj('character', id);
const attributes = findObjs({ type: 'attribute', characterid: character.id });
let sheet = new CharacterSheet();
sheet.name = character.get('name');
sheet.avatar = character.get('avatar');
sheet.role = get_attr('class');
sheet.alignment = get_attr('alignment');
sheet.experience = get_attr('XP');
sheet.level = get_attr('level');
sheet.attributes = new Attributes(
get_attr('str') || 10,
get_attr('int') || 10,
get_attr('wis') || 10,
get_attr('dex') || 10,
get_attr('con') || 10,
get_attr('cha') || 10,
);
sheet.hp = get_attr('HP');
sheet.max_hp = get_attr('HP', 'max');
sheet.ac = get_attr('dacAc');
sheet.thac0 = get_attr('thac0');
// see: https://app.roll20.net/forum/post/6967007/api-access-slash-selecting-repeating-character-sheet-fields
sheet.inventory = new Inventory();
sheet.inventory.weapons = get_items('repeating_weapons', Weapon);
sheet.inventory.gear = get_items('repeating_items', Item);
sheet.inventory.armor = get_items('repeating_armor', Armor);
sheet.inventory.coins = new Coins();
sheet.inventory.coins.pp = get_attr('PP') || 0;
sheet.inventory.coins.gp = get_attr('GP') || 0;
sheet.inventory.coins.ep = get_attr('EP') || 0;
sheet.inventory.coins.sp = get_attr('SP') || 0;
sheet.inventory.coins.cp = get_attr('CP') || 0;
sheet.inventory.encumbrance = get_attr('encumbrance'); // TODO undefined?
return sheet;
function get_attr(attr_name, attr_type) {
attr_type = attr_type || 'current';
const attr = attributes.filter((attr) => attr.get('name') == attr_name).pop();
return attr ? attr.get(attr_type) : undefined;
}
function get_repeating_attrs(repeating_attr_name) {
return attributes.filter((attr) => {
return attr.get('name').indexOf(repeating_attr_name) === 0;
}).reduce((attrs, attr) => {
const parts = attr.get('name').substr(repeating_attr_name.length + 1).split('_');
const id = parts[0];
const type = parts[1];
if (!(id in attrs)) {
attrs[id] = {};
}
attrs[id][type] = attr.get('current');
return attrs;
}, {});
}
function get_items(repeating_attr_name, class_name) {
return Object.values(get_repeating_attrs(repeating_attr_name)).map((attrs) => {
let item = new class_name();
let i;
for (i in attrs) {
item[i] = attrs[i];
}
return item;
});
}
}
function egg(player, f) {
switch (f) {
case 2:
sendChat('Harondalbar', `/w "${player.get('_displayname')}" You are testing my patience, mortal.`, null, { noarchive: true });
break;
case 3:
sendChat('Harondalbar', `/w "${player.get('_displayname')}" *Begins to breath fire...*`, null, { noarchive: true });
break;
case 4:
sendChat('Harondalbar', `/w "${player.get('_displayname')}" **Your character is burnt to a crisp!**`, null, { noarchive: true });
break;
default:
sendChat('Harondalbar', `/w "${player.get('_displayname')}" *laughs*`, null, { noarchive: true });
break;
}
}
});
@MichaelMackus

Copy link
Copy Markdown
Author

WIP backup system for roll20

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment