Last active
June 23, 2026 01:31
-
-
Save MichaelMackus/c84fee5f5acfe24a7627d0a53e3e9585 to your computer and use it in GitHub Desktop.
roll20backup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| } | |
| }); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
WIP backup system for roll20