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 .
×
Create a free account

Bag of Holding

March 24 (1 week ago)

Edited 7:19AM (12 hours ago)

Edit: Script below in chat.

Edit 2: New updated verison here.

With the help of AI I have created a bag of holding that will create a handout, sort items, track weight and post a message to the gm if the bag is over 500lbs. It has a total weight and puts all coins to the top of the list from highest denomination down. Below is a screenshot of the handout. If anyone is interested let me know.


March 24 (1 week ago)

Edited March 24 (1 week ago)
Gold
Forum Champion

I think it's smart. I'm interested. My group has 2 PC that have Bag of Holding. By the way another one has Heward's Handy Haversack. 

Can the writer (player) can adjust the Weight of items by typing a different number in the parentheses? Not pulling from a "standard list" of item weights, right? Asking partly because I play a different game system that might have different weights. 

For sharing your Mod .... you can post the code directly right here in Roll20 Forums.... and anyone who wants it can copy-paste into their game's API Mods as a custom mod. This is commonly called a "Snippit" if you share it that way.

Otherwise you can find out how to get the Mod entered into the One-Click, that way anyone can find and install it easily when managing their Mods without knowing about the Forums. And you could get the Scripter tag applied to your Roll20 Profile which is pretty cool. 

March 24 (1 week ago)

It has to be entered by !dimensionbag add [quantity] [item name] [weight per item] and !dimensionbag remove [quantity] [item name]
I will be happy to post here just making sure it people are interested. It is set for dnd and I dont have plans to alter it but any who do can.

March 24 (1 week ago)

Here is the script

on('ready', () => {
    const HANDOUT_NAME = "Team Dimension Bag";
    const MAX_WEIGHT = 500;
    const COIN_ORDER = ['PP', 'GP', 'EP', 'SP', 'CP'];
    const QUANTITY_COLOR = 'blue';

    const getOrCreateHandout = () => {
        let handout = findObjs({ type: 'handout', name: HANDOUT_NAME })[0];
        if (!handout) {
            handout = createObj('handout', { name: HANDOUT_NAME, inplayerjournals: 'all' });
            handout.set('notes', '<b>Total Weight: 0 lbs</b><br><br>');
        }
        return handout;
    };

    const parseHandoutContent = (notes) => {
        let items = {};
        let totalWeight = 0;
        (notes || '').split('<br>').forEach(line => {
            let match = line.match(/<b>(.+?)<\/b>: <span style='color:(.*?)'>(\d+)<\/span> \((.*?) lbs\)/);
            if (match) {
                let [, item, , quantity, weight] = match;
                items[item] = { quantity: parseInt(quantity, 10), weight: parseFloat(weight) };
            }
        });
        return items;
    };

    const updateHandout = (items) => {
        let totalWeight = Object.entries(items).reduce((sum, [_, { quantity, weight }]) => sum + (quantity * weight), 0);
        let sortedItems = Object.entries(items).sort(([a], [b]) => {
            let aIndex = COIN_ORDER.indexOf(a);
            let bIndex = COIN_ORDER.indexOf(b);
            if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
            if (aIndex !== -1) return -1;
            if (bIndex !== -1) return 1;
            return a.localeCompare(b);
        });

        let content = `<b>Total Weight: ${totalWeight.toFixed(2)} lbs</b><br><br>`;
        sortedItems.forEach(([item, { quantity, weight }]) => {
            content += `<b>${item}</b>: <span style='color:${QUANTITY_COLOR}'>${quantity}</span> (${weight} lbs)<br>`;
        });

        getOrCreateHandout().set('notes', content);
    };

    const addItem = (count, item, weight) => {
        let handout = getOrCreateHandout();
        handout.get('notes', (notes) => {
            let items = parseHandoutContent(notes);
            count = parseInt(count, 10);
            weight = parseFloat(weight);
            let currentWeight = Object.entries(items).reduce((sum, [_, { quantity, weight }]) => sum + (quantity * weight), 0);
            if (currentWeight + (count * weight) > MAX_WEIGHT) {
                sendChat('Dimension Bag', `/w gm &{template:default} {{name=Dimension Bag}} {{Warning=Cannot add items! Exceeds 500 lbs limit.}}`);
                return;
            }
            items[item] = items[item] || { quantity: 0, weight };
            items[item].quantity += count;
            updateHandout(items);
        });
    };

    const removeItem = (count, item) => {
        let handout = getOrCreateHandout();
        handout.get('notes', (notes) => {
            let items = parseHandoutContent(notes);
            count = parseInt(count, 10);
            if (!items[item] || items[item].quantity < count) {
                sendChat('Dimension Bag', `/w gm &{template:default} {{name=Dimension Bag}} {{Warning=Not enough ${item} to remove!}}`);
                return;
            }
            items[item].quantity -= count;
            if (items[item].quantity <= 0) delete items[item];
            updateHandout(items);
        });
    };

    on('chat:message', (msg) => {
        if (msg.type !== 'api') return;
        let args = msg.content.split(' ');
        let command = args.shift();
        if (command === '!dimensionbag') {
            let action = args.shift();
            if (action === 'add' && args.length >= 3) {
                addItem(args[0], args.slice(1, -1).join(' '), args[args.length - 1]);
            } else if (action === 'remove' && args.length >= 2) {
                removeItem(args[0], args.slice(1).join(' '));
            }
        }
    });
});


March 25 (1 week ago)

I have updated the script to be able to handle more than one bag or what ever and added that when it exceeds the total allowed weight it deletes all of the contents and send a message to the gm.

The macro to add is !dimensionbag add "Bag Name" Quantity "Item Name" Weight   (quotes are needed)

To remove !dimensionbag remove "Bag Name" Quantity "Item Name"    (quotes are needed)
If you want to change the overall weight allowed change these lines number:
const MAX_WEIGHT = 500;
sendChat('Dimension Bag', `/w gm &{template:default} {{name=Dimension Bag}} {{Warning=Cannot add items! Exceeds 500 lbs limit. All items removed.}}`);

on('ready', () => {

    const MAX_WEIGHT = 500;

    const COIN_ORDER = ['PP', 'GP', 'EP', 'SP', 'CP'];

    const QUANTITY_COLOR = 'blue';


    const getOrCreateHandout = (bagName) => {

        let handout = findObjs({ type: 'handout', name: bagName })[0];

        if (!handout) {

            handout = createObj('handout', { name: bagName, inplayerjournals: 'all' });

            handout.set('notes', '<b>Total Weight: 0.00 lbs</b><br><br>');

        }

        return handout;

    };


    const parseHandoutContent = (notes) => {

        let items = {};

        (notes || '').split('<br>').forEach(line => {

            let match = line.match(/<b>(.+?)<\/b>: <span style='color:(.*?)'>(\d+)<\/span> \((.*?) lbs\)/);

            if (match) {

                let [, item, , quantity, weight] = match;

                items[item] = { quantity: parseInt(quantity, 10) || 0, weight: parseFloat(weight) || 0 };

            }

        });

        return items;

    };


    const updateHandout = (bagName, items) => {

        let totalWeight = Object.entries(items).reduce((sum, [_, { quantity, weight }]) => sum + (quantity * weight), 0);

        if (totalWeight > MAX_WEIGHT) {

            getOrCreateHandout(bagName).set('notes', '<b>Total Weight: 0.00 lbs</b><br><br>');

            sendChat('Dimension Bag', `/w gm &{template:default} {{name=Dimension Bag}} {{Warning=Cannot add items! Exceeds 500 lbs limit. All items removed.}}`);

            return;

        }


        let sortedItems = Object.entries(items).sort(([a], [b]) => {

            let aIndex = COIN_ORDER.indexOf(a);

            let bIndex = COIN_ORDER.indexOf(b);

            if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;

            if (aIndex !== -1) return -1;

            if (bIndex !== -1) return 1;

            return a.localeCompare(b);

        });


        let content = `<b>Total Weight: ${totalWeight.toFixed(2)} lbs</b><br><br>`;

        sortedItems.forEach(([item, { quantity, weight }]) => {

            content += `<b>${item}</b>: <span style='color:${QUANTITY_COLOR}'>${quantity}</span> (${weight.toFixed(2)} lbs)<br>`;

        });


        getOrCreateHandout(bagName).set('notes', content);

    };


    const addItem = (bagName, count, item, weight) => {

        let handout = getOrCreateHandout(bagName);

        handout.get('notes', (notes) => {

            let items = parseHandoutContent(notes);

            count = parseInt(count, 10);

            weight = parseFloat(weight);

            if (isNaN(count) || isNaN(weight) || count <= 0 || weight < 0) {

                sendChat('Dimension Bag', `/w gm &{template:default} {{name=Dimension Bag}} {{Warning=Invalid quantity or weight!}}`);

                return;

            }

            if (!items[item]) {

                items[item] = { quantity: 0, weight };

            } else {

                items[item].weight = weight;

            }

            items[item].quantity += count;

            updateHandout(bagName, items);

        });

    };


    const removeItem = (bagName, count, item) => {

        let handout = getOrCreateHandout(bagName);

        handout.get('notes', (notes) => {

            let items = parseHandoutContent(notes);

            count = parseInt(count, 10);

            if (isNaN(count) || count <= 0 || !items[item] || items[item].quantity < count) {

                sendChat('Dimension Bag', `/w gm &{template:default} {{name=Dimension Bag}} {{Warning=Not enough ${item} to remove!}}`);

                return;

            }

            items[item].quantity -= count;

            if (items[item].quantity <= 0) delete items[item];

            updateHandout(bagName, items);

        });

    };


    on('chat:message', (msg) => {

        if (msg.type !== 'api') return;

        let args = msg.content.match(/("[^"]+"|\S+)/g) || [];

        let command = args.shift();

        if (command === '!dimensionbag') {

            let action = args.shift();

            let bagName = args.shift().replace(/"/g, '');

            if (action === 'add' && args.length >= 3) {

                addItem(bagName, args[0], args.slice(1, -1).join(' ').replace(/"/g, ''), args[args.length - 1]);

            } else if (action === 'remove' && args.length >= 2) {

                removeItem(bagName, args[0], args.slice(1).join(' ').replace(/"/g, ''));

            }

        }

    });

});




March 25 (1 week ago)
Joe
Pro

Cool script, I may give it a try when my players inevitably get a Bag in this campaign. 


One suggestion: automatically deleting all contents would be nice to be optional. Many groups will allow take-backs if a mistake was made (especially if just a typo in number added), and others may want to quest for their lost items in the Astral Sea. Perhaps have an option to just notify everyone (dramatically? :-)), and/or change the permissions on the handout so that only the DM can see or edit it.


To get really fancy, you could then disallow a player from removing an item, which might currently have returned the sheet to being visible to everyone, but at that point you have to ask whether you have a healthy relationship with your players or not. The evidence of the mistake will be in the chat regardless. ;-) 

March 25 (1 week ago)

The first iteration allows for what you have highlighted but it can only create one bag. I will have a look at adding multiple bags without deleting the contents or maybe creating a new handout for all lost items

March 25 (1 week ago)

I second this! I love the idea behind this script as my players currently have both a Bag of Holding and a Handy Haversack. I think that deleting the contents when the container is overloaded is drastic; that's a severe punishment for a possible typo!

I like the idea of removing the "In Player's Journals" and "Can Be Edited By" permissions instead. Having a window pop up, or even a chat message that says "You hear a loud ripping sound as the container vanishes, along with all of it's contents".

Joe said:

Cool script, I may give it a try when my players inevitably get a Bag in this campaign. 


One suggestion: automatically deleting all contents would be nice to be optional. Many groups will allow take-backs if a mistake was made (especially if just a typo in number added), and others may want to quest for their lost items in the Astral Sea. Perhaps have an option to just notify everyone (dramatically? :-)), and/or change the permissions on the handout so that only the DM can see or edit it.


To get really fancy, you could then disallow a player from removing an item, which might currently have returned the sheet to being visible to everyone, but at that point you have to ask whether you have a healthy relationship with your players or not. The evidence of the mistake will be in the chat regardless. ;-) 




March 25 (1 week ago)

Edited March 25 (1 week ago)

I have redone the script to not delete items if you want a unique message you just need to change the highlight the section in bold to change to your desired text in the event of it going over weight.

sendChat('Dimension Bag', `/w gm &{template:default} {{name=Dimension Bag}} {{Warning=Cannot add items! Exceeds 500 lbs limit.}}`);

Hope this is a more suitable version if anyone wants to change or add to it you are most welcome.

To remove permission of the handout I have left that as a manual option for the GM to do in the handout itself.

on('ready', () => {

    const MAX_WEIGHT = 500;

    const COIN_ORDER = ['PP', 'GP', 'EP', 'SP', 'CP'];

    const QUANTITY_COLOR = 'blue';


    // Create or get the handout by name

    const getOrCreateHandout = (handoutName) => {

        let handout = findObjs({ type: 'handout', name: handoutName })[0];

        if (!handout) {

            log(`Creating handout: ${handoutName}`);

            handout = createObj('handout', { 

                name: handoutName, 

                inplayerjournals: 'all',

                notes: '<b>Total Weight: 0 lbs</b><br><br>'  // Initialize with basic content

            });

        } else {

            log(`Found existing handout: ${handoutName}`);

        }

        return handout;

    };


    // Parse the content of the handout notes

    const parseHandoutContent = (notes) => {

        let items = {};

        (notes || '').split('<br>').forEach(line => {

            let match = line.match(/<b>(.+?)<\/b>: <span style='color:(.*?)'>(\d+)<\/span> \((.*?) lbs\)/);

            if (match) {

                let [, item, , quantity, weight] = match;

                items[item] = { quantity: parseInt(quantity, 10), weight: parseFloat(weight) };

            }

        });

        return items;

    };


    // Update the handout notes with new item details

    const updateHandout = (handoutName, items) => {

        let totalWeight = Object.entries(items).reduce((sum, [_, { quantity, weight }]) => sum + (quantity * weight), 0);

        let sortedItems = Object.entries(items).sort(([a], [b]) => {

            let aIndex = COIN_ORDER.indexOf(a);

            let bIndex = COIN_ORDER.indexOf(b);

            if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;

            if (aIndex !== -1) return -1;

            if (bIndex !== -1) return 1;

            return a.localeCompare(b);

        });


        let content = `<b>Total Weight: ${totalWeight.toFixed(2)} lbs</b><br><br>`;

        sortedItems.forEach(([item, { quantity, weight }]) => {

            content += `<b>${item}</b>: <span style='color:${QUANTITY_COLOR}'>${quantity}</span> (${weight} lbs)<br>`;

        });


        let handout = getOrCreateHandout(handoutName);

        handout.set('notes', content);  // Update the notes field with the new content

        log(`Handout updated for ${handoutName}`);

    };


    // Add an item to the handout

    const addItem = (count, item, weight, handoutName) => {

        let handout = getOrCreateHandout(handoutName);

        handout.get('notes', (notes) => {

            let items = parseHandoutContent(notes);

            count = parseInt(count, 10);

            weight = parseFloat(weight);

            let currentWeight = Object.entries(items).reduce((sum, [_, { quantity, weight }]) => sum + (quantity * weight), 0);

            if (currentWeight + (count * weight) > MAX_WEIGHT) {

                sendChat('Dimension Bag', `/w gm &{template:default} {{name=Dimension Bag}} {{Warning=Cannot add items! Exceeds 500 lbs limit.}}`);

                return;

            }

            items[item] = items[item] || { quantity: 0, weight };

            items[item].quantity += count;

            updateHandout(handoutName, items);

        });

    };


    // Remove an item from the handout

    const removeItem = (count, item, handoutName) => {

        let handout = getOrCreateHandout(handoutName);

        handout.get('notes', (notes) => {

            let items = parseHandoutContent(notes);

            count = parseInt(count, 10);

            if (!items[item] || items[item].quantity < count) {

                sendChat('Dimension Bag', `/w gm &{template:default} {{name=Dimension Bag}} {{Warning=Not enough ${item} to remove!}}`);

                return;

            }

            items[item].quantity -= count;

            if (items[item].quantity <= 0) delete items[item];

            updateHandout(handoutName, items);

        });

    };


    // Listen for chat commands

    on('chat:message', (msg) => {

        if (msg.type !== 'api') return;


        // Use a regular expression to capture bag name in quotes and the rest of the command

        let args = msg.content.match(/^!dimensionbag (add|remove) "(.+?)" (.+)$/);


        if (!args) {

            sendChat('Dimension Bag', '/w gm &{template:default} {{name=Dimension Bag}} {{Warning=Invalid command syntax. Please use: !dimensionbag add "Bag Name" [quantity] [item] [weight] or !dimensionbag remove "Bag Name" [quantity] [item].}}');

            return;

        }


        let action = args[1];               // 'add' or 'remove'

        let bagName = args[2];              // Bag name with spaces in quotes

        let commandArgs = args[3].split(' '); // Rest of the command for quantity, item name, weight


        if (action === 'add' && commandArgs.length >= 3) {

            let count = commandArgs.shift();

            let weight = commandArgs.pop();

            let item = commandArgs.join(' '); // Join remaining parts as item name

            addItem(count, item, weight, bagName);

        } else if (action === 'remove' && commandArgs.length >= 2) {

            let count = commandArgs.shift();

            let item = commandArgs.join(' '); // Join remaining parts as item name

            removeItem(count, item, bagName);

        } else {

            sendChat('Dimension Bag', '/w gm &{template:default} {{name=Dimension Bag}} {{Warning=Invalid command syntax.}}');

        }

    });

});



March 26 (6 days ago)
Joe
Pro

Thanks for the quick update!

March 26 (6 days ago)

No problem was fiddling with all day yesterday as I have a player who just got one in my game and could not find one that suited I have since updated this to allow for different types of storage like a sack or a hole but need to make sure it is compliant with free use before I post

March 28 (4 days ago)

Edited 7:19AM (12 hours ago)

I have updated this and posted it to github  link here. the updated script now has the bag, sack and hole options and also a config option to change weight allowance, option to change coins and an option to change the quantity text color. If anyone wants to use this please visit the link.