diff --git a/lang/en.json b/lang/en.json index 1512f7d36d..0896f737f0 100644 --- a/lang/en.json +++ b/lang/en.json @@ -724,6 +724,40 @@ "DND5E.Casting": "Casting", "DND5E.Conditions": "Conditions", + +"DND5E.CONTAINER": { + "Details": "Container Details", + "FIELDS": { + "capacity": { + "label": "Capacity", + "count": { + "label": "Item Count" + }, + "volume": { + "label": "Volume Capacity", + "units": { + "label": "Volume Units" + }, + "value": { + "label": "Maximum Volume" + } + }, + "weight": { + "label": "Weight Capacity", + "units": { + "label": "Weight Units" + }, + "value": { + "label": "Maximum Weight" + } + } + }, + "properties": { + "label": "Container Properties" + } + } +}, + "DND5E.Controls": { "Hint": "Enable various hints throughout the UI for certain mouse and keyboard controls.", "LeftClick": "Left Click", @@ -2229,14 +2263,6 @@ "DND5E.ItemConsumableStatus": "Consumable Status", "DND5E.ItemConsumableSubtype": "{category} Type", "DND5E.ItemConsumableType": "Consumable Type", -"DND5E.ItemContainerCapacity": "Capacity", -"DND5E.ItemContainerCapacityItems": "Items", -"DND5E.ItemContainerCapacityMax": "Max Capacity", -"DND5E.ItemContainerCapacityType": "Capacity Type", -"DND5E.ItemContainerCapacityWeight": "Weight", -"DND5E.ItemContainerDetails": "Container Details", -"DND5E.ItemContainerProperties": "Container Properties", -"DND5E.ItemContainerStatus": "Container Status", "DND5E.ItemCreate": "Create Item", "DND5E.ItemCritThreshold": "Critical Hit Threshold", "DND5E.ItemCritExtraDamage": "Extra Critical Hit Damage", @@ -2304,6 +2330,7 @@ "DND5E.ItemRecoveryRollMax": "{name} recovers all charges", "DND5E.ItemRecoveryFormulaWarning": "Unable to recover uses for {name}. Invalid recovery formula '{formula}' ({uuid}).", "DND5E.ItemRequiredStr": "Required Strength", +"DND5E.Items": "Items", "DND5E.ItemSiegeProperties": "Siege Properties", "DND5E.ItemSpeciesDetails": "Species Details", "DND5E.ItemSubclassDetails": "Subclass Details", @@ -3516,6 +3543,7 @@ "DND5E.Unidentified.Notice": "You must identify this item to learn its details.", "DND5E.Unidentified.Title": "Unidentified", "DND5E.Unidentified.Value": "???", +"DND5E.Unit": "Unit", "DND5E.UNITS": { "DISTANCE": { @@ -3537,6 +3565,31 @@ "Abbreviation": "mi" } }, + "VOLUME": { + "Label": "Volume Units", + "CubicFoot": { + "Label": "Cubic Feet", + "Abbreviation": "ft^3", + "Counted": { + "narrow": { + "one": "{number}ft^3", + "other": "{number}ft^3" + }, + "short": { + "one": "{number} cu ft", + "other": "{number} cu ft" + }, + "long": { + "one": "{number} cubic foot", + "other": "{number} cubic feet" + } + } + }, + "Liter": { + "Label": "Liters", + "Abbreviation": "L" + } + }, "WEIGHT": { "Label": "Weight Units", "Kilogram": { @@ -4073,6 +4126,10 @@ "Name": "Use Metric Length Units", "Hint": "Defaults to using meters instead of feet for movement and senses." }, + "VolumeUnits": { + "Name": "Use Metric Volume Units", + "Hint": "Defaults to using liters instead of cubic feet for container capacity." + }, "WeightUnits": { "Name": "Use Metric Weight Units", "Hint": "Replaces all reference to lbs with kgs and updates the encumbrance calculations to use metric weight units." diff --git a/module/config.mjs b/module/config.mjs index 2087869374..a5db610a5d 100644 --- a/module/config.mjs +++ b/module/config.mjs @@ -2358,6 +2358,10 @@ DND5E.defaultUnits = { imperial: "ft", metric: "m" }, + volume: { + imperial: "cubicFoot", + metric: "liter" + }, weight: { imperial: "lb", metric: "kg" @@ -2447,6 +2451,29 @@ preLocalize("distanceUnits"); /* -------------------------------------------- */ +/** + * The valid units for measurement of volume. + * @enum {UnitConfiguration} + */ +DND5E.volumeUnits = { + cubicFoot: { + label: "DND5E.UNITS.VOLUME.CubicFoot.Label", + abbreviation: "DND5E.UNITS.Volume.CubicFoot.Abbreviation", + counted: "DND5E.UNITS.Volume.CubicFoot.Counted", + conversion: 1, + type: "imperial" + }, + liter: { + label: "DND5E.UNITS.VOLUME.Liter.Label", + abbreviation: "DND5E.UNITS.Volume.Liter.Abbreviation", + conversion: 1 / 28.317, // Should we do an approximate conversion here? Does it matter? + type: "metric" + } +}; +preLocalize("volumeUnits", { keys: ["label", "abbreviation"] }); + +/* -------------------------------------------- */ + /** * The valid units for measurement of weight. * @enum {UnitConfiguration} diff --git a/module/data/item/container.mjs b/module/data/item/container.mjs index 38a2085bfe..12866af426 100644 --- a/module/data/item/container.mjs +++ b/module/data/item/container.mjs @@ -1,9 +1,10 @@ +import { defaultUnits } from "../../utils.mjs"; import { ItemDataModel } from "../abstract.mjs"; +import CurrencyTemplate from "../shared/currency.mjs"; import EquippableItemTemplate from "./templates/equippable-item.mjs"; import IdentifiableTemplate from "./templates/identifiable.mjs"; import ItemDescriptionTemplate from "./templates/item-description.mjs"; import PhysicalItemTemplate from "./templates/physical-item.mjs"; -import CurrencyTemplate from "../shared/currency.mjs"; const { NumberField, SchemaField, SetField, StringField } = foundry.data.fields; @@ -15,10 +16,15 @@ const { NumberField, SchemaField, SetField, StringField } = foundry.data.fields; * @mixes EquippableItemTemplate * @mixes CurrencyTemplate * - * @property {Set} properties Container properties. * @property {object} capacity Information on container's carrying capacity. - * @property {string} capacity.type Method for tracking max capacity as defined in `DND5E.itemCapacityTypes`. - * @property {number} capacity.value Total amount of the type this container can carry. + * @property {number} capacity.count Number of items that can be stored within the container. + * @property {object} capacity.volume + * @property {string} capacity.volume.units Units used to measure volume capacity. + * @property {number} capacity.volume.value Amount of volume that can be stored. + * @property {object} capacity.weight + * @property {string} capacity.weight.units Units used to measure weight capacity. + * @property {number} capacity.weight.value Amount of weight that can be stored. + * @property {Set} properties Container properties. */ export default class ContainerData extends ItemDataModel.mixin( ItemDescriptionTemplate, IdentifiableTemplate, PhysicalItemTemplate, EquippableItemTemplate, CurrencyTemplate @@ -29,21 +35,26 @@ export default class ContainerData extends ItemDataModel.mixin( /* -------------------------------------------- */ /** @override */ - static LOCALIZATION_PREFIXES = ["DND5E.SOURCE"]; + static LOCALIZATION_PREFIXES = ["DND5E.CONTAINER", "DND5E.SOURCE"]; /* -------------------------------------------- */ /** @inheritDoc */ static defineSchema() { return this.mergeSchema(super.defineSchema(), { - quantity: new NumberField({ min: 1, max: 1 }), - properties: new SetField(new StringField(), { label: "DND5E.ItemContainerProperties" }), capacity: new SchemaField({ - type: new StringField({ - required: true, initial: "weight", blank: false, label: "DND5E.ItemContainerCapacityType" + count: new NumberField({ min: 0, integer: true }), + volume: new SchemaField({ + value: new NumberField({ min: 0 }), + units: new StringField({ initial: () => defaultUnits("volume") }) }), - value: new NumberField({ required: true, min: 0, label: "DND5E.ItemContainerCapacityMax" }) - }, { label: "DND5E.ItemContainerCapacity" }) + weight: new SchemaField({ + value: new NumberField({ min: 0 }), + units: new StringField({ initial: () => defaultUnits("weight") }) + }) + }), + properties: new SetField(new StringField()), + quantity: new NumberField({ min: 1, max: 1 }) }); } @@ -74,6 +85,7 @@ export default class ContainerData extends ItemDataModel.mixin( /** @inheritDoc */ static _migrateData(source) { super._migrateData(source); + ContainerData.#migrateCapacity(source); ContainerData.#migrateQuantity(source); } @@ -91,6 +103,24 @@ export default class ContainerData extends ItemDataModel.mixin( /* -------------------------------------------- */ + /** + * Migrate capacity to support multiple fields and units. + * @param {object} source The candidate source data from which the model will be constructed. + */ + static #migrateCapacity(source) { + if ( !source?.capacity || !source.capacity?.type || !source.capacity?.value ) return; + if ( source.capacity.type === "weight" ) { + source.capacity.weight ??= {}; + source.capacity.weight.value = source.capacity.value; + } else if ( source.capacity.type === "item" ) { + source.capacity.count = source.capacity.value; + } + delete source.capacity.type; + delete source.capacity.value; + } + + /* -------------------------------------------- */ + /** * Force quantity to always be 1. * @param {object} source The candidate source data from which the model will be constructed. @@ -275,14 +305,15 @@ export default class ContainerData extends ItemDataModel.mixin( * @returns {Promise} */ async computeCapacity() { - const { value, type } = this.capacity; - const context = { max: value ?? Infinity }; - if ( type === "weight" ) { - context.value = await this.contentsWeight; - context.units = game.i18n.localize("DND5E.AbbreviationLbs"); - } else { + const context = { max: Infinity, value: 0 }; + if ( this.capacity.count ) { context.value = await this.contentsCount; - context.units = game.i18n.localize("DND5E.ItemContainerCapacityItems"); + context.max = this.capacity.count; + context.units = game.i18n.localize("DND5E.Items"); + } else if ( this.capacity.weight.value ) { + context.value = await this.contentsWeight; + context.max = this.capacity.weight.value; + context.units = CONFIG.DND5E.weightUnits[this.capacity.weight.units]?.label ?? ""; } context.value = context.value.toNearest(0.1); context.pct = Math.clamp(context.max ? (context.value / context.max) * 100 : 0, 0, 100); diff --git a/module/settings.mjs b/module/settings.mjs index 61ff2ae2ed..196e60b120 100644 --- a/module/settings.mjs +++ b/module/settings.mjs @@ -355,6 +355,16 @@ export function registerSystemSettings() { default: false }); + // Metric Volume Weights + game.settings.register("dnd5e", "metricVolumeUnits", { + name: "SETTINGS.DND5E.METRIC.VolumeUnits.Name", + hint: "SETTINGS.DND5E.METRIC.VolumeUnits.Hint", + scope: "world", + config: true, + type: Boolean, + default: false + }); + // Metric Unit Weights game.settings.register("dnd5e", "metricWeightUnits", { name: "SETTINGS.DND5E.METRIC.WeightUnits.Name", diff --git a/module/utils.mjs b/module/utils.mjs index 5c1d718590..784bddc433 100644 --- a/module/utils.mjs +++ b/module/utils.mjs @@ -122,6 +122,19 @@ export function formatText(value) { /* -------------------------------------------- */ +/** + * Form a number using the provided volume unit. + * @param {number} value The volume to format. + * @param {string} unit Volume unit as defined in `CONFIG.DND5E.volumeUnits`. + * @param {object} [options={}] Formatting options passed to `formatNumber`. + * @returns {string} + */ +export function formatVolume(value, unit, options={}) { + return _formatSystemUnits(value, unit, CONFIG.DND5E.volumeUnits[unit], options); +} + +/* -------------------------------------------- */ + /** * Form a number using the provided weight unit. * @param {number} value The weight to format. diff --git a/templates/items/details/details-container.hbs b/templates/items/details/details-container.hbs index faf201e147..1289ca04d4 100644 --- a/templates/items/details/details-container.hbs +++ b/templates/items/details/details-container.hbs @@ -1,25 +1,9 @@
- {{ localize "DND5E.ItemContainerDetails" }} + {{ localize "DND5E.CONTAINER.Details" }} {{!-- Container Properties --}} - {{ formField fields.properties options=properties.options label="DND5E.ItemContainerProperties" localize=true - input=inputs.createMultiCheckboxInput stacked=true classes="checkbox-grid checkbox-grid-3" }} - - {{!-- Capacity --}} -
- -
- - {{!-- Amount --}} - {{ formField fields.capacity.fields.value value=source.capacity.value label="DND5E.Amount" localize=true - classes="label-top" placeholder="—" }} - - {{!-- Type --}} - {{ formField fields.capacity.fields.type value=source.capacity.type label="DND5E.Type" localize=true - classes="label-top" choices=config.itemCapacityTypes }} - -
-
+ {{ formField fields.properties options=properties.options input=inputs.createMultiCheckboxInput stacked=true + classes="checkbox-grid checkbox-grid-3" }} {{!-- Attunement --}} {{#if properties.object.mgc}} @@ -42,3 +26,44 @@ {{/if}}
+ +
+ {{ localize "DND5E.CONTAINER.FIELDS.capacity.label" }} + + {{!-- Item Capacity --}} + {{ formField fields.capacity.fields.count value=source.capacity.count placeholder="—" }} + + {{!-- Volume Capacity --}} +
+ +
+ + {{!-- Amount --}} + {{ formField fields.capacity.fields.volume.fields.value value=source.capacity.volume.value + label="DND5E.Amount" localize=true classes="label-top" placeholder="—" }} + + {{!-- Units --}} + {{ formField fields.capacity.fields.volume.fields.units value=source.capacity.volume.units + label="DND5E.Unit" localize=true classes="label-top" choices=config.volumeUnits + labelAttr="label" }} + +
+
+ + {{!-- Weight Capacity --}} +
+ +
+ + {{!-- Amount --}} + {{ formField fields.capacity.fields.weight.fields.value value=source.capacity.weight.value + label="DND5E.Amount" localize=true classes="label-top" placeholder="—" }} + + {{!-- Units --}} + {{ formField fields.capacity.fields.weight.fields.units value=source.capacity.weight.units + label="DND5E.Unit" localize=true classes="label-top" choices=config.weightUnits + labelAttr="label" }} + +
+
+
diff --git a/templates/shared/inventory.hbs b/templates/shared/inventory.hbs index d361b84e39..47edf0566e 100644 --- a/templates/shared/inventory.hbs +++ b/templates/shared/inventory.hbs @@ -176,7 +176,7 @@ {{#with capacity}} -