From b993ba8d6ce00f3923d91091678aca086c2a2196 Mon Sep 17 00:00:00 2001 From: DavidCQ80 Date: Wed, 26 Mar 2025 19:13:55 +0000 Subject: [PATCH 1/4] Create Dimension Bag This script creates a bag, sack, or hole handout that will track Items, amount, weight and overall weight. To add items to the bag, sack or hole you will need to use one of these macros: !dimensionbag add ["Bag Name"] [Amount] ["Item"] [Weight] !sack add ["Sack Name"] [Amount] ["Item"] [Weight] !hole add ["Hole Name"] [Amount] ["Item"] [Weight] To remove items from the bag, sack or hole use these macros: !dimensionbag remove ["Bag Name"] [Amount] ["Item"] !sack remove ["Sack Name"] [Amount] ["Item"] !hole remove ["Hole Name"] [Amount] ["Item"] If the weight threshold is reached a message is posted in chat to alert the players that the bag or sack is overloaded and its contents are lost. The hole has unlimited weight but its up to the GM to determine if too much space is taken up to be able to fit anymore in. --- Dimension Bag | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 Dimension Bag diff --git a/Dimension Bag b/Dimension Bag new file mode 100644 index 000000000..287bbd591 --- /dev/null +++ b/Dimension Bag @@ -0,0 +1,117 @@ +on('ready', () => { + const BAG_MAX_WEIGHT = 500; + const SACK_MAX_WEIGHT = 900; + const HOLE_MAX_WEIGHT = Infinity; + const COIN_ORDER = ['PP', 'GP', 'EP', 'SP', 'CP']; + const QUANTITY_COLOR = 'blue'; + + 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: 'Total Weight: 0 lbs

' + }); + } else { + log(`Found existing handout: ${handoutName}`); + } + return handout; + }; + + const parseHandoutContent = (notes) => { + let items = {}; + (notes || '').split('
').forEach(line => { + let match = line.match(/(.+?)<\/b>: (\d+)<\/span> \((.*?) lbs\)/); + if (match) { + let [, item, , quantity, weight] = match; + items[item] = { quantity: parseInt(quantity, 10), weight: parseFloat(weight) }; + } + }); + return items; + }; + + 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 = `Total Weight: ${totalWeight.toFixed(2)} lbs

`; + sortedItems.forEach(([item, { quantity, weight }]) => { + content += `${item}: ${quantity} (${weight} lbs)
`; + }); + + let handout = getOrCreateHandout(handoutName); + handout.set('notes', content); + log(`Handout updated for ${handoutName}`); + }; + + const addItem = (count, item, weight, handoutName, maxWeight) => { + 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) > maxWeight) { + sendChat(handoutName, `&{template:default} {{name=${handoutName}}} {{Warning=The ${handoutName} is overloaded and its contents are lost.}}`); + return; + } + items[item] = items[item] || { quantity: 0, weight }; + items[item].quantity += count; + updateHandout(handoutName, items); + }); + }; + + 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(handoutName, `/w gm &{template:default} {{name=${handoutName}}} {{Warning=Not enough ${item} to remove!}}`); + return; + } + items[item].quantity -= count; + if (items[item].quantity <= 0) delete items[item]; + updateHandout(handoutName, items); + }); + }; + + on('chat:message', (msg) => { + if (msg.type !== 'api') return; + + let args = msg.content.match(/^!(dimensionbag|sack|hole) (add|remove) "(.+?)" (.+)$/); + + if (!args) { + sendChat('Storage', '/w gm &{template:default} {{name=Storage}} {{Warning=Invalid command syntax. Use: !dimensionbag add "Bag Name" [quantity] [item] [weight], !sack add ..., or !hole add ...}}'); + return; + } + + let type = args[1]; + let action = args[2]; + let storageName = args[3]; + let commandArgs = args[4].split(' '); + let maxWeight = type === 'dimensionbag' ? BAG_MAX_WEIGHT : type === 'sack' ? SACK_MAX_WEIGHT : HOLE_MAX_WEIGHT; + + if (action === 'add' && commandArgs.length >= 3) { + let count = commandArgs.shift(); + let weight = commandArgs.pop(); + let item = commandArgs.join(' '); + addItem(count, item, weight, storageName, maxWeight); + } else if (action === 'remove' && commandArgs.length >= 2) { + let count = commandArgs.shift(); + let item = commandArgs.join(' '); + removeItem(count, item, storageName); + } else { + sendChat('Storage', '/w gm &{template:default} {{name=Storage}} {{Warning=Invalid command syntax.}}'); + } + }); +}); From c074cec3dc1783b2914377afddc1836400bb95e6 Mon Sep 17 00:00:00 2001 From: DavidCQ80 Date: Fri, 28 Mar 2025 18:26:46 +0000 Subject: [PATCH 2/4] Update Dimension Bag --- Dimension Bag | 89 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 67 insertions(+), 22 deletions(-) diff --git a/Dimension Bag b/Dimension Bag index 287bbd591..4e54d9ece 100644 --- a/Dimension Bag +++ b/Dimension Bag @@ -1,21 +1,74 @@ +/** +* Storage Management + * !storageconfig — Opens the configuration menu. + +*Configuration Commands +* !setbagmax [number] — Sets the max weight for the Dimension Bag. +* !setsackmax [number] — Sets the max weight for the Sack. +* !setholemax — Resets the Moveable Hole to unlimited weight. +* !setcoins [comma-separated values] — Updates the order of coin types (e.g., !setcoins PP, GP, SP, CP). +* !setcolor [color name or hex] — Changes the text color for item quantity. + +*Storage Commands +* !dimensionbag add "Bag Name" [quantity] [item] [weight] — Adds an item to a Dimension Bag. +* !dimensionbag remove "Bag Name" [quantity] [item] — Removes an item from a Dimension Bag. +* !sack add "Sack Name" [quantity] [item] [weight] — Adds an item to a Sack. +* !sack remove "Sack Name" [quantity] [item] — Removes an item from a Sack. +* !hole add "Hole Name" [quantity] [item] [weight] — Adds an item to a Moveable Hole. +* !hole remove "Hole Name" [quantity] [item] — Removes an item from a Moveable Hole. +*Each storage type has its own max weight (except the hole, which is unlimited). +*/ on('ready', () => { - const BAG_MAX_WEIGHT = 500; - const SACK_MAX_WEIGHT = 900; - const HOLE_MAX_WEIGHT = Infinity; - const COIN_ORDER = ['PP', 'GP', 'EP', 'SP', 'CP']; - const QUANTITY_COLOR = 'blue'; + let CONFIG = { + bagMaxWeight: 500, + sackMaxWeight: 900, + holeMaxWeight: Infinity, + coinOrder: ['PP', 'GP', 'EP', 'SP', 'CP'], + quantityColor: 'blue' + }; + + const showConfigMenu = () => { + let menu = `/w gm &{template:default} ` + + `{{name=Storage Configuration}} ` + + `{{Bag Max Weight=[${CONFIG.bagMaxWeight}](!setbagmax ?{New Max Weight})}} ` + + `{{Sack Max Weight=[${CONFIG.sackMaxWeight}](!setsackmax ?{New Max Weight})}} ` + + `{{Hole Max Weight=[Unlimited](!setholemax)}} ` + + `{{Coin Order=[${CONFIG.coinOrder.join(', ')}](!setcoins ?{Enter Coins Separated by Commas})}} ` + + `{{Quantity Color=[${CONFIG.quantityColor}](!setcolor ?{Enter Color Name or Hex})}}`; + sendChat('Storage', menu); + }; + + on('chat:message', (msg) => { + if (msg.type !== 'api') return; + + if (msg.content === '!storageconfig') { + showConfigMenu(); + return; + } + + let match; + if (match = msg.content.match(/^!setbagmax (\d+)$/)) { + CONFIG.bagMaxWeight = parseInt(match[1], 10); + } else if (match = msg.content.match(/^!setsackmax (\d+)$/)) { + CONFIG.sackMaxWeight = parseInt(match[1], 10); + } else if (match = msg.content.match(/^!setcoins (.+)$/)) { + CONFIG.coinOrder = match[1].split(',').map(c => c.trim()); + } else if (match = msg.content.match(/^!setcolor (.+)$/)) { + CONFIG.quantityColor = match[1]; + } else { + return; + } + showConfigMenu(); + }); 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: 'Total Weight: 0 lbs

' }); - } else { - log(`Found existing handout: ${handoutName}`); } return handout; }; @@ -35,8 +88,8 @@ on('ready', () => { 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); + let aIndex = CONFIG.coinOrder.indexOf(a); + let bIndex = CONFIG.coinOrder.indexOf(b); if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex; if (aIndex !== -1) return -1; if (bIndex !== -1) return 1; @@ -45,12 +98,11 @@ on('ready', () => { let content = `Total Weight: ${totalWeight.toFixed(2)} lbs

`; sortedItems.forEach(([item, { quantity, weight }]) => { - content += `${item}: ${quantity} (${weight} lbs)
`; + content += `${item}: ${quantity} (${weight} lbs)
`; }); let handout = getOrCreateHandout(handoutName); handout.set('notes', content); - log(`Handout updated for ${handoutName}`); }; const addItem = (count, item, weight, handoutName, maxWeight) => { @@ -87,19 +139,14 @@ on('ready', () => { on('chat:message', (msg) => { if (msg.type !== 'api') return; - let args = msg.content.match(/^!(dimensionbag|sack|hole) (add|remove) "(.+?)" (.+)$/); + if (!args) return; - if (!args) { - sendChat('Storage', '/w gm &{template:default} {{name=Storage}} {{Warning=Invalid command syntax. Use: !dimensionbag add "Bag Name" [quantity] [item] [weight], !sack add ..., or !hole add ...}}'); - return; - } - - let type = args[1]; + let type = args[1]; let action = args[2]; let storageName = args[3]; let commandArgs = args[4].split(' '); - let maxWeight = type === 'dimensionbag' ? BAG_MAX_WEIGHT : type === 'sack' ? SACK_MAX_WEIGHT : HOLE_MAX_WEIGHT; + let maxWeight = type === 'dimensionbag' ? CONFIG.bagMaxWeight : type === 'sack' ? CONFIG.sackMaxWeight : CONFIG.holeMaxWeight; if (action === 'add' && commandArgs.length >= 3) { let count = commandArgs.shift(); @@ -110,8 +157,6 @@ on('ready', () => { let count = commandArgs.shift(); let item = commandArgs.join(' '); removeItem(count, item, storageName); - } else { - sendChat('Storage', '/w gm &{template:default} {{name=Storage}} {{Warning=Invalid command syntax.}}'); } }); }); From 6f94562f190e374c5f88dd72ad93907c0e305225 Mon Sep 17 00:00:00 2001 From: DavidCQ80 Date: Tue, 15 Jul 2025 00:12:04 +0100 Subject: [PATCH 3/4] Create Dimension Storage --- Dimension Storage/Dimension Storage | 162 ++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 Dimension Storage/Dimension Storage diff --git a/Dimension Storage/Dimension Storage b/Dimension Storage/Dimension Storage new file mode 100644 index 000000000..4e54d9ece --- /dev/null +++ b/Dimension Storage/Dimension Storage @@ -0,0 +1,162 @@ +/** +* Storage Management + * !storageconfig — Opens the configuration menu. + +*Configuration Commands +* !setbagmax [number] — Sets the max weight for the Dimension Bag. +* !setsackmax [number] — Sets the max weight for the Sack. +* !setholemax — Resets the Moveable Hole to unlimited weight. +* !setcoins [comma-separated values] — Updates the order of coin types (e.g., !setcoins PP, GP, SP, CP). +* !setcolor [color name or hex] — Changes the text color for item quantity. + +*Storage Commands +* !dimensionbag add "Bag Name" [quantity] [item] [weight] — Adds an item to a Dimension Bag. +* !dimensionbag remove "Bag Name" [quantity] [item] — Removes an item from a Dimension Bag. +* !sack add "Sack Name" [quantity] [item] [weight] — Adds an item to a Sack. +* !sack remove "Sack Name" [quantity] [item] — Removes an item from a Sack. +* !hole add "Hole Name" [quantity] [item] [weight] — Adds an item to a Moveable Hole. +* !hole remove "Hole Name" [quantity] [item] — Removes an item from a Moveable Hole. +*Each storage type has its own max weight (except the hole, which is unlimited). +*/ +on('ready', () => { + let CONFIG = { + bagMaxWeight: 500, + sackMaxWeight: 900, + holeMaxWeight: Infinity, + coinOrder: ['PP', 'GP', 'EP', 'SP', 'CP'], + quantityColor: 'blue' + }; + + const showConfigMenu = () => { + let menu = `/w gm &{template:default} ` + + `{{name=Storage Configuration}} ` + + `{{Bag Max Weight=[${CONFIG.bagMaxWeight}](!setbagmax ?{New Max Weight})}} ` + + `{{Sack Max Weight=[${CONFIG.sackMaxWeight}](!setsackmax ?{New Max Weight})}} ` + + `{{Hole Max Weight=[Unlimited](!setholemax)}} ` + + `{{Coin Order=[${CONFIG.coinOrder.join(', ')}](!setcoins ?{Enter Coins Separated by Commas})}} ` + + `{{Quantity Color=[${CONFIG.quantityColor}](!setcolor ?{Enter Color Name or Hex})}}`; + sendChat('Storage', menu); + }; + + on('chat:message', (msg) => { + if (msg.type !== 'api') return; + + if (msg.content === '!storageconfig') { + showConfigMenu(); + return; + } + + let match; + if (match = msg.content.match(/^!setbagmax (\d+)$/)) { + CONFIG.bagMaxWeight = parseInt(match[1], 10); + } else if (match = msg.content.match(/^!setsackmax (\d+)$/)) { + CONFIG.sackMaxWeight = parseInt(match[1], 10); + } else if (match = msg.content.match(/^!setcoins (.+)$/)) { + CONFIG.coinOrder = match[1].split(',').map(c => c.trim()); + } else if (match = msg.content.match(/^!setcolor (.+)$/)) { + CONFIG.quantityColor = match[1]; + } else { + return; + } + showConfigMenu(); + }); + + const getOrCreateHandout = (handoutName) => { + let handout = findObjs({ type: 'handout', name: handoutName })[0]; + if (!handout) { + handout = createObj('handout', { + name: handoutName, + inplayerjournals: 'all', + notes: 'Total Weight: 0 lbs

' + }); + } + return handout; + }; + + const parseHandoutContent = (notes) => { + let items = {}; + (notes || '').split('
').forEach(line => { + let match = line.match(/(.+?)<\/b>: (\d+)<\/span> \((.*?) lbs\)/); + if (match) { + let [, item, , quantity, weight] = match; + items[item] = { quantity: parseInt(quantity, 10), weight: parseFloat(weight) }; + } + }); + return items; + }; + + 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 = CONFIG.coinOrder.indexOf(a); + let bIndex = CONFIG.coinOrder.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 = `Total Weight: ${totalWeight.toFixed(2)} lbs

`; + sortedItems.forEach(([item, { quantity, weight }]) => { + content += `${item}: ${quantity} (${weight} lbs)
`; + }); + + let handout = getOrCreateHandout(handoutName); + handout.set('notes', content); + }; + + const addItem = (count, item, weight, handoutName, maxWeight) => { + 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) > maxWeight) { + sendChat(handoutName, `&{template:default} {{name=${handoutName}}} {{Warning=The ${handoutName} is overloaded and its contents are lost.}}`); + return; + } + items[item] = items[item] || { quantity: 0, weight }; + items[item].quantity += count; + updateHandout(handoutName, items); + }); + }; + + 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(handoutName, `/w gm &{template:default} {{name=${handoutName}}} {{Warning=Not enough ${item} to remove!}}`); + return; + } + items[item].quantity -= count; + if (items[item].quantity <= 0) delete items[item]; + updateHandout(handoutName, items); + }); + }; + + on('chat:message', (msg) => { + if (msg.type !== 'api') return; + let args = msg.content.match(/^!(dimensionbag|sack|hole) (add|remove) "(.+?)" (.+)$/); + if (!args) return; + + let type = args[1]; + let action = args[2]; + let storageName = args[3]; + let commandArgs = args[4].split(' '); + let maxWeight = type === 'dimensionbag' ? CONFIG.bagMaxWeight : type === 'sack' ? CONFIG.sackMaxWeight : CONFIG.holeMaxWeight; + + if (action === 'add' && commandArgs.length >= 3) { + let count = commandArgs.shift(); + let weight = commandArgs.pop(); + let item = commandArgs.join(' '); + addItem(count, item, weight, storageName, maxWeight); + } else if (action === 'remove' && commandArgs.length >= 2) { + let count = commandArgs.shift(); + let item = commandArgs.join(' '); + removeItem(count, item, storageName); + } + }); +}); From 471b46037ba0769201dcd2576e02c909e3f6d24a Mon Sep 17 00:00:00 2001 From: DavidCQ80 Date: Tue, 15 Jul 2025 00:13:43 +0100 Subject: [PATCH 4/4] Create script.json --- Dimension Storage/script.json | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 Dimension Storage/script.json diff --git a/Dimension Storage/script.json b/Dimension Storage/script.json new file mode 100644 index 000000000..3a72ff480 --- /dev/null +++ b/Dimension Storage/script.json @@ -0,0 +1,25 @@ +name: Dimension Storage +script: Dimension Storage.js +version: 1.0.0 +description: Storage Management +A way to list items the players have collected during their adventures in a magical bag, sack or hole that calculates weight and amount. +It stores this in a handout created when !dimensionbag add "Bag Name" "quantity item weight" is run. +!storageconfig — Opens the configuration menu. +*Configuration Commands + +!setbagmax [number] — Sets the max weight for the Dimension Bag. +!setsackmax [number] — Sets the max weight for the Sack. +!setholemax — Resets the Moveable Hole to unlimited weight. +!setcoins [comma-separated values] — Updates the order of coin types (e.g., !setcoins PP, GP, SP, CP). +!setcolor [color name or hex] — Changes the text color for item quantity. +*Storage Commands + +!dimensionbag add "Bag Name" "quantity item weight" — Adds an item to a Dimension Bag. +!dimensionbag remove "Bag Name" "quantity item" — Removes an item from a Dimension Bag. +!sack add "Sack Name" "quantity item weight" — Adds an item to a Sack. +!sack remove "Sack Name" "quantity item" — Removes an item from a Sack. +!hole add "Hole Name" "quantity item weight" — Adds an item to a Moveable Hole. +!hole remove "Hole Name" "quantity item" — Removes an item from a Moveable Hole. *Each storage type has its own max weight (except the hole, which is unlimited). */ +authors: David Q +roll20userid: 408069 +dependencies: []