diff --git a/Dimension Bag b/Dimension Bag new file mode 100644 index 000000000..4e54d9ece --- /dev/null +++ b/Dimension Bag @@ -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); + } + }); +}); 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); + } + }); +}); 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: []